This commit is contained in:
wyw 2026-04-30 13:54:26 +08:00
commit 4542b53d26
11 changed files with 2675 additions and 288 deletions

48
package-lock.json generated
View File

@ -51,6 +51,7 @@
"react-loadable": "5.5.0",
"react-orgchart": "^1.0.5",
"react-resizable": "^1.10.1",
"react-slick": "^0.31.0",
"react-to-print": "^2.0.0-alpha-2",
"react-umeditor": "1.0.12",
"react-websocket": "2.0.1",
@ -2637,6 +2638,22 @@
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-2.1.1.tgz",
"integrity": "sha512-jCH+k2Vjlno4YWl6g535nHR09PwCEmTBKAG6VqF+rhkrSPRLfgpU2maagwbZPLjaHuU5Jd1DFQ2KJpQuI6uG8w=="
},
"node_modules/antd/node_modules/react-slick": {
"version": "0.25.2",
"resolved": "https://registry.npmmirror.com/react-slick/-/react-slick-0.25.2.tgz",
"integrity": "sha512-8MNH/NFX/R7zF6W/w+FS5VXNyDusF+XDW1OU0SzODEU7wqYB+ZTGAiNJ++zVNAVqCAHdyCybScaUB+FCZOmBBw==",
"dependencies": {
"classnames": "^2.2.5",
"enquire.js": "^2.1.6",
"json2mq": "^0.2.0",
"lodash.debounce": "^4.0.8",
"resize-observer-polyfill": "^1.5.0"
},
"peerDependencies": {
"react": "^0.14.0 || ^15.0.1 || ^16.0.0",
"react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0"
}
},
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz",
@ -21055,19 +21072,18 @@
"license": "BSD-3-Clause"
},
"node_modules/react-slick": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.25.2.tgz",
"integrity": "sha512-8MNH/NFX/R7zF6W/w+FS5VXNyDusF+XDW1OU0SzODEU7wqYB+ZTGAiNJ++zVNAVqCAHdyCybScaUB+FCZOmBBw==",
"version": "0.31.0",
"resolved": "https://registry.npmmirror.com/react-slick/-/react-slick-0.31.0.tgz",
"integrity": "sha512-zo6VLT8wuSBJffg/TFPbzrw2dEnfZ/cUKmYsKByh3AgatRv29m2LoFbq5vRMa3R3A4wp4d8gwbJKO2fWZFaI3g==",
"dependencies": {
"classnames": "^2.2.5",
"enquire.js": "^2.1.6",
"json2mq": "^0.2.0",
"lodash.debounce": "^4.0.8",
"resize-observer-polyfill": "^1.5.0"
},
"peerDependencies": {
"react": "^0.14.0 || ^15.0.1 || ^16.0.0",
"react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0"
"react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-test-renderer": {
@ -22508,7 +22524,6 @@
"version": "1.8.1",
"resolved": "https://registry.npmmirror.com/slick-carousel/-/slick-carousel-1.8.1.tgz",
"integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==",
"license": "MIT",
"peerDependencies": {
"jquery": ">=1.8.0"
}
@ -28673,6 +28688,18 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-2.1.1.tgz",
"integrity": "sha512-jCH+k2Vjlno4YWl6g535nHR09PwCEmTBKAG6VqF+rhkrSPRLfgpU2maagwbZPLjaHuU5Jd1DFQ2KJpQuI6uG8w=="
},
"react-slick": {
"version": "0.25.2",
"resolved": "https://registry.npmmirror.com/react-slick/-/react-slick-0.25.2.tgz",
"integrity": "sha512-8MNH/NFX/R7zF6W/w+FS5VXNyDusF+XDW1OU0SzODEU7wqYB+ZTGAiNJ++zVNAVqCAHdyCybScaUB+FCZOmBBw==",
"requires": {
"classnames": "^2.2.5",
"enquire.js": "^2.1.6",
"json2mq": "^0.2.0",
"lodash.debounce": "^4.0.8",
"resize-observer-polyfill": "^1.5.0"
}
}
}
},
@ -42912,12 +42939,11 @@
}
},
"react-slick": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.25.2.tgz",
"integrity": "sha512-8MNH/NFX/R7zF6W/w+FS5VXNyDusF+XDW1OU0SzODEU7wqYB+ZTGAiNJ++zVNAVqCAHdyCybScaUB+FCZOmBBw==",
"version": "0.31.0",
"resolved": "https://registry.npmmirror.com/react-slick/-/react-slick-0.31.0.tgz",
"integrity": "sha512-zo6VLT8wuSBJffg/TFPbzrw2dEnfZ/cUKmYsKByh3AgatRv29m2LoFbq5vRMa3R3A4wp4d8gwbJKO2fWZFaI3g==",
"requires": {
"classnames": "^2.2.5",
"enquire.js": "^2.1.6",
"json2mq": "^0.2.0",
"lodash.debounce": "^4.0.8",
"resize-observer-polyfill": "^1.5.0"

View File

@ -52,6 +52,7 @@
"react-loadable": "5.5.0",
"react-orgchart": "^1.0.5",
"react-resizable": "^1.10.1",
"react-slick": "^0.31.0",
"react-to-print": "^2.0.0-alpha-2",
"react-umeditor": "1.0.12",
"react-websocket": "2.0.1",

View File

@ -0,0 +1,228 @@
// ClassBuild.js - 班组建设页面组件
import React from 'react';
import { Table } from 'antd';
import styles from './../fullinter.less';
import echarts from 'echarts';
class ClassBuild extends React.Component {
constructor(props) {
super(props);
this.echartsInstances = {
typeBarChart: null, // 班组风险类别柱状图
};
this.chartResizeHandlers = {};
this.isUnmounted = false;
}
waitForElement = (elementId, maxRetries = 10) => {
return new Promise((resolve) => {
let retries = 0;
const checkInterval = setInterval(() => {
const element = document.getElementById(elementId);
if (element || retries >= maxRetries) {
clearInterval(checkInterval);
resolve(!!element);
}
retries++;
}, 50);
});
};
// 班组风险类别柱状图
renderTypeBarChart = async () => {
if (this.isUnmounted) return;
const elementExists = await this.waitForElement('typeBarChart');
if (!elementExists || this.isUnmounted) return;
if (this.echartsInstances.typeBarChart) {
this.echartsInstances.typeBarChart.dispose();
this.echartsInstances.typeBarChart = null;
}
const chartDom = document.getElementById('typeBarChart');
if (!chartDom) return;
this.echartsInstances.typeBarChart = echarts.init(chartDom);
const { classSubData } = this.props;
const companyList = classSubData || [];
if (companyList.length === 0) {
this.echartsInstances.typeBarChart.setOption({
title: {
text: '班组建设统计情况',
x: 'center',
y: '25%',
textStyle: { fontSize: 16, color: '#999' },
},
graphic: {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '暂无数据',
fill: '#999',
fontSize: 14,
},
},
});
return;
}
// 注意:数据结构是 { companyName: '邦泰', rate: '52%' }
const companyNames = companyList.map((item) => item.companyName);
const rates = companyList.map((item) => {
// 处理 rate 可能是字符串带百分号的情况
const rateValue = typeof item.rate === 'string' ? parseFloat(item.rate.replace('%', '')) : item.rate;
return isNaN(rateValue) ? 0 : rateValue;
});
const option = {
title: {
text: '本月班前会议及时完成率分析',
x: 'center',
y: '5%',
textStyle: { fontSize: 16, color: '#000', fontWeight: 'bold' },
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function (params) {
let result = `${params[0].axisValue}<br/>`;
params.forEach((param) => {
result += `${param.marker}${param.seriesName}: ${param.value}%<br/>`;
});
return result;
},
},
grid: {
left: '8%',
right: '5%',
top: '18%',
bottom: '8%',
containLabel: true,
},
xAxis: [
{
type: 'category',
data: companyNames,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
textStyle: { color: '#000' },
interval: 0,
fontSize: 12,
},
},
],
yAxis: [
{
type: 'value',
show: true,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
show: true,
textStyle: { color: '#000' },
formatter: '{value}%',
},
splitLine: { show: true, lineStyle: { color: '#e0e0e0', type: 'dashed' } },
},
],
series: [
{
name: '班组建设完成率',
type: 'bar',
data: rates,
itemStyle: {
normal: {
color: '#4285F4',
borderRadius: [4, 4, 0, 0],
},
},
label: {
show: true,
position: 'top',
textStyle: { color: '#4285F4', fontSize: 12 },
formatter: (params) => `${params.value}%`,
},
barWidth: '50%',
},
],
};
this.echartsInstances.typeBarChart.setOption(option);
this.setupResizeHandler('typeBarChart', this.renderTypeBarChart);
};
setupResizeHandler = (chartName, renderMethod) => {
const resizeHandler = () => {
if (this.echartsInstances[chartName] && !this.isUnmounted) {
this.echartsInstances[chartName].resize();
}
};
this.chartResizeHandlers[chartName] = resizeHandler;
window.addEventListener('resize', resizeHandler);
};
initAllCharts = () => {
if (this.isUnmounted) return;
setTimeout(() => {
if (this.isUnmounted) return;
this.renderTypeBarChart();
}, 100);
};
disposeAllCharts = () => {
Object.keys(this.echartsInstances).forEach((key) => {
if (this.echartsInstances[key]) {
try {
this.echartsInstances[key].dispose();
} catch (e) {
console.warn(`Dispose chart ${key} error:`, e);
}
this.echartsInstances[key] = null;
}
});
Object.keys(this.chartResizeHandlers).forEach((key) => {
if (this.chartResizeHandlers[key]) {
window.removeEventListener('resize', this.chartResizeHandlers[key]);
}
});
this.chartResizeHandlers = {};
};
componentDidMount() {
this.isUnmounted = false;
this.initAllCharts();
}
// 修复:监听 classSubData 而不是 riskSubData
componentDidUpdate(prevProps) {
if (prevProps.classSubData !== this.props.classSubData) {
this.renderTypeBarChart();
}
}
componentWillUnmount() {
this.isUnmounted = true;
this.disposeAllCharts();
}
render() {
return (
<div className={styles.trainingContentWrapper}>
<div className={styles.trainingGrid}>
<div className={styles.trainingRow}>
<div className={styles.trainingCard}>
<div id="typeBarChart" className={styles.trainingChartContainer}></div>
</div>
</div>
</div>
</div>
);
}
}
export default ClassBuild;

View File

@ -0,0 +1,312 @@
// DangerJob.js - 危险作业页面组件
import React from 'react';
import { Table, Select, Pagination } from 'antd';
import styles from './../fullinter.less';
const { Option } = Select;
class DangerJob extends React.Component {
constructor(props) {
super(props);
this.state = {
currentPage: 1,
pageSize: 10,
};
this.isUnmounted = false;
}
// 生成公司选项(与 TrainingContent 保持一致)
getCompanyOptions = () => {
const { companyData } = this.props;
if (!companyData || companyData.length === 0) {
return <Option value="">暂无公司</Option>;
}
return companyData.map((company, index) => (
<Option key={company.ID || index} value={company.ID}>
{' '}
{/* 使用 ID 作为 value与 TrainingContent 一致 */}
{company.NAME}
</Option>
));
};
// 处理公司筛选变化
handleCompanyChange = (value) => {
const { onCompanyChange } = this.props;
this.setState({ currentPage: 1 }, () => {
if (onCompanyChange) {
onCompanyChange(value);
}
});
};
// 处理页码变化
handlePageChange = (page, pageSize) => {
this.setState({ currentPage: page, pageSize });
};
// 处理每页条数变化
handleShowSizeChange = (current, size) => {
this.setState({ currentPage: 1, pageSize: size });
};
// 获取过滤后的数据(添加类型检查和默认值)
getFilteredData = () => {
const { dangerSubData, selectedCompany, companyData } = this.props;
// 确保 dangerSubData 是数组
let dataArray = Array.isArray(dangerSubData) ? dangerSubData : [];
if (dataArray.length === 0) {
return [];
}
// 根据选中的公司进行过滤(使用 ID 进行匹配)
if (selectedCompany) {
// 查找选中的公司名称
const selectedCompanyObj = companyData?.find((company) => company.ID === selectedCompany);
const selectedCompanyName = selectedCompanyObj?.NAME;
if (selectedCompanyName) {
return dataArray.filter((item) => item.companyName === selectedCompanyName);
}
}
return dataArray;
};
// 获取当前页数据
getCurrentPageData = () => {
const { currentPage, pageSize } = this.state;
const filteredData = this.getFilteredData();
// 确保 filteredData 是数组
const dataArray = Array.isArray(filteredData) ? filteredData : [];
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return {
data: dataArray.slice(startIndex, endIndex),
total: dataArray.length,
};
};
// 渲染危险作业表格
renderDangerTable = () => {
const { selectedCompany, companyData } = this.props;
const { currentPage, pageSize } = this.state;
const { data: tableData, total } = this.getCurrentPageData();
if (!tableData || tableData.length === 0) {
// 获取选中的公司名称用于显示
let companyName = '';
if (selectedCompany && companyData) {
const selectedCompanyObj = companyData.find((company) => company.ID === selectedCompany);
companyName = selectedCompanyObj?.NAME || '';
}
return (
<div style={{ textAlign: 'center', padding: '50px', color: '#999' }}>
{selectedCompany ? `${companyName}暂无危险作业数据` : '暂无危险作业数据'}
</div>
);
}
// 表格列配置
const columns = [
{
title: '公司',
dataIndex: 'companyName',
key: 'companyName',
align: 'center',
width: 120,
fixed: 'left',
render: (text) => <strong>{text}</strong>,
},
{
title: '开始时间',
dataIndex: 'startDate',
key: 'startDate',
align: 'center',
width: 160,
render: (text) => <span>{text || '-'}</span>,
},
{
title: '结束时间',
dataIndex: 'endDate',
key: 'endDate',
align: 'center',
width: 160,
render: (text) => <span>{text || '-'}</span>,
},
{
title: '作业名称',
dataIndex: 'jobName',
key: 'jobName',
align: 'center',
minWidth: 120,
render: (text) => <span style={{ fontWeight: 'bold' }}>{text || '-'}</span>,
},
{
title: '作业区域',
dataIndex: 'areaName',
key: 'areaName',
align: 'center',
minWidth: 120,
render: (text) => <span>{text || '-'}</span>,
},
{
title: '作业地点',
dataIndex: 'place',
key: 'place',
align: 'center',
minWidth: 150,
render: (text) => <span>{text || '-'}</span>,
},
{
title: '作业人员',
dataIndex: 'users',
key: 'users',
align: 'center',
minWidth: 150,
render: (text) => <span>{text || '-'}</span>,
},
{
title: '监护人',
dataIndex: 'monitor',
key: 'monitor',
align: 'center',
width: 120,
render: (text) => <span>{text || '-'}</span>,
},
{
title: '审批领导',
dataIndex: 'approveUsers',
key: 'approveUsers',
align: 'center',
minWidth: 150,
render: (text) => <span>{text || '-'}</span>,
},
];
// 计算横向滚动宽度(如果列数过多)
const scrollX = columns.reduce((sum, col) => sum + (col.width || 120), 0);
const scrollConfig = scrollX > 1200 ? { x: scrollX } : {};
// 表格数据转换添加唯一key
const dataSource = tableData.map((item, index) => ({
key: `${item.companyName}_${item.startDate}_${index}`,
companyName: item.companyName,
startDate: item.startDate,
endDate: item.endDate,
jobName: item.jobName,
areaName: item.areaName,
place: item.place,
users: item.users,
monitor: item.monitor,
approveUsers: item.approveUsers,
}));
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div
style={{
textAlign: 'center',
padding: '20px 0 0 0',
fontWeight: 'bold',
fontSize: '17px',
color: '#000',
}}
>
当日各公司危险作业清单
</div>
<div style={{ flex: 1, overflow: 'auto', padding: '10px' }}>
<Table
columns={columns}
dataSource={dataSource}
pagination={false}
scroll={scrollConfig}
size="small"
bordered
className={styles.certificateTable}
/>
</div>
{/* 分页组件 */}
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
padding: '12px 16px',
borderTop: '1px solid #f0f0f0',
backgroundColor: '#fff',
}}
>
<Pagination
current={currentPage}
pageSize={pageSize}
total={total}
showSizeChanger
showQuickJumper
showTotal={(total) => `${total} 条记录`}
onChange={this.handlePageChange}
onShowSizeChange={this.handleShowSizeChange}
pageSizeOptions={['10', '20', '50']}
/>
</div>
</div>
);
};
componentDidMount() {
this.isUnmounted = false;
}
componentDidUpdate(prevProps) {
// 当筛选条件变化时,重置到第一页
if (prevProps.selectedCompany !== this.props.selectedCompany) {
this.setState({ currentPage: 1 });
}
// 当数据变化时,如果当前页没有数据且不是第一页,重置到第一页
if (prevProps.dangerSubData !== this.props.dangerSubData) {
const { currentPage } = this.state;
const filteredData = this.getFilteredData();
const dataArray = Array.isArray(filteredData) ? filteredData : [];
const maxPage = Math.ceil(dataArray.length / this.state.pageSize) || 1;
if (currentPage > maxPage) {
this.setState({ currentPage: 1 });
}
}
}
componentWillUnmount() {
this.isUnmounted = true;
}
render() {
const { companyData, selectedCompany } = this.props;
return (
<div className={styles.trainingContentWrapper}>
<div className={styles.trainingGrid}>
<div className={styles.trainingRow}>
<div className={styles.trainingCard}>
{/* 公司筛选器 */}
<div className={styles.monthSelectorWrapper}>
<span className={styles.monthSelectorLabel}>选择公司</span>
<Select
value={selectedCompany}
onChange={this.handleCompanyChange}
style={{ width: 150 }}
className={styles.monthSelect}
allowClear
placeholder="全部公司"
>
{this.getCompanyOptions()}
</Select>
</div>
{this.renderDangerTable()}
</div>
</div>
</div>
</div>
);
}
}
export default DangerJob;

View File

@ -0,0 +1,578 @@
// HiddenSolve.js - 隐患解决页面组件
import React from 'react';
import { Select } from 'antd';
import styles from './../fullinter.less';
import echarts from 'echarts';
const { Option } = Select;
class HiddenSolve extends React.Component {
constructor(props) {
super(props);
this.echartsInstances = {
hiddenBarChart: null, // 各公司隐患统计柱状图(使用 hiddenList
hiddenRectifyChart: null, // 隐患整改情况柱状图(使用 hiddenRectifyList
};
this.chartResizeHandlers = {};
this.isUnmounted = false;
}
// 获取公司选项(使用 props 传入的 companyData与 TrainingContent 一致)
getCompanyOptions = () => {
const { companyData } = this.props;
if (!companyData || companyData.length === 0) {
return [];
}
return companyData.map((company, index) => (
<Option key={company.ID || index} value={company.ID}>
{company.NAME}
</Option>
));
};
// 根据选中的公司ID获取公司名称用于筛选模拟数据
getSelectedCompanyName = () => {
const { companyData, selectedCompany } = this.props;
if (!selectedCompany || !companyData || companyData.length === 0) {
return null;
}
const selectedCompanyObj = companyData.find((company) => company.ID === selectedCompany);
return selectedCompanyObj ? selectedCompanyObj.NAME : null;
};
waitForElement = (elementId, maxRetries = 10) => {
return new Promise((resolve) => {
let retries = 0;
const checkInterval = setInterval(() => {
const element = document.getElementById(elementId);
if (element || retries >= maxRetries) {
clearInterval(checkInterval);
resolve(!!element);
}
retries++;
}, 50);
});
};
// 图表1: 各公司隐患统计柱状图(使用 hiddenList 数据)
renderHiddenBarChart = async () => {
if (this.isUnmounted) return;
const elementExists = await this.waitForElement('hiddenBarChart');
if (!elementExists || this.isUnmounted) return;
if (this.echartsInstances.hiddenBarChart) {
this.echartsInstances.hiddenBarChart.dispose();
this.echartsInstances.hiddenBarChart = null;
}
const chartDom = document.getElementById('hiddenBarChart');
if (!chartDom) return;
this.echartsInstances.hiddenBarChart = echarts.init(chartDom);
const { hiddenSubData } = this.props;
let hiddenList = hiddenSubData?.hiddenList || [];
if (hiddenList.length === 0) {
this.echartsInstances.hiddenBarChart.setOption({
title: {
text: '各公司累计隐患统计数据',
x: 'center',
y: '25%',
textStyle: { fontSize: 16, color: '#999' },
},
graphic: {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '暂无数据',
fill: '#999',
fontSize: 14,
},
},
});
return;
}
const companyNames = hiddenList.map((item) => item.companyName);
const majorCounts = hiddenList.map((item) => item.majorCount);
const generalCounts = hiddenList.map((item) => item.generalCount);
const option = {
title: {
text: '各公司累计隐患统计数据',
x: 'center',
y: '5%',
textStyle: { fontSize: 16, color: '#000', fontWeight: 'bold' },
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function (params) {
let result = `${params[0].axisValue}<br/>`;
params.forEach((param) => {
result += `${param.marker}${param.seriesName}: ${param.value}<br/>`;
});
return result;
},
},
legend: {
data: ['重大隐患', '一般隐患'],
orient: 'vertical',
right: '3%',
top: '5%',
itemGap: 16,
itemWidth: 18,
itemHeight: 12,
textStyle: { color: '#000', fontSize: 14 },
},
grid: {
left: '8%',
right: '5%',
top: '18%',
bottom: '8%',
containLabel: true,
},
xAxis: [
{
type: 'category',
data: companyNames,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
textStyle: { color: '#000' },
rotate: companyNames.length > 4 ? 15 : 0,
interval: 0,
fontSize: 12,
},
},
],
yAxis: [
{
type: 'value',
show: true,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
show: true,
textStyle: { color: '#000' },
},
splitLine: { show: false },
name: '隐患数量',
nameTextStyle: { fontSize: 12 },
},
],
series: [
{
name: '重大隐患',
type: 'bar',
data: majorCounts,
itemStyle: {
normal: {
color: '#c92a2a', // 红色
},
},
label: {
show: true,
position: 'top',
textStyle: { color: '#c92a2a', fontSize: 12 },
formatter: (params) => `${params.value}`,
},
barWidth: '35%',
},
{
name: '一般隐患',
type: 'bar',
data: generalCounts,
itemStyle: {
normal: {
color: '#4285F4', // 蓝色
},
},
label: {
show: true,
position: 'top',
textStyle: { color: '#4285F4', fontSize: 12 },
formatter: (params) => `${params.value}`,
},
barWidth: '35%',
},
],
};
this.echartsInstances.hiddenBarChart.setOption(option);
this.setupResizeHandler('hiddenBarChart', this.renderHiddenBarChart);
};
// 图表2: 隐患整改情况柱状图(使用 hiddenRectifyList 数据)
renderHiddenRectifyChart = async () => {
if (this.isUnmounted) return;
const elementExists = await this.waitForElement('hiddenRectifyChart');
if (!elementExists || this.isUnmounted) return;
if (this.echartsInstances.hiddenRectifyChart) {
this.echartsInstances.hiddenRectifyChart.dispose();
this.echartsInstances.hiddenRectifyChart = null;
}
const chartDom = document.getElementById('hiddenRectifyChart');
if (!chartDom) return;
this.echartsInstances.hiddenRectifyChart = echarts.init(chartDom);
const { hiddenSubData } = this.props;
let hiddenRectifyList = hiddenSubData?.hiddenRectifyList || [];
if (hiddenRectifyList.length === 0) {
this.echartsInstances.hiddenRectifyChart.setOption({
title: {
text: '当月各公司隐患统计数据',
x: 'center',
y: '25%',
textStyle: { fontSize: 16, color: '#999' },
},
graphic: {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '暂无数据',
fill: '#999',
fontSize: 14,
},
},
});
return;
}
const companyNames = hiddenRectifyList.map((item) => item.companyName);
const majorTotal = hiddenRectifyList.map((item) => item.majorCount);
const majorRectified = hiddenRectifyList.map((item) => (item.majorCount || 0) - (item.majorCountNo || 0));
const generalTotal = hiddenRectifyList.map((item) => item.generalCount);
const generalRectified = hiddenRectifyList.map((item) => (item.generalCount || 0) - (item.generalCountNo || 0));
const option = {
title: {
text: '当月各公司隐患统计数据',
x: 'center',
y: '5%',
textStyle: { fontSize: 16, color: '#000', fontWeight: 'bold' },
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function (params) {
let result = `${params[0].axisValue}<br/>`;
params.forEach((param) => {
result += `${param.marker}${param.seriesName}: ${param.value}<br/>`;
});
return result;
},
},
legend: {
data: ['重大隐患量', '重大隐患未整改量', '一般隐患量', '一般隐患未整改量'],
orient: 'vertical',
right: '3%',
top: '5%',
itemGap: 12,
itemWidth: 18,
itemHeight: 12,
textStyle: { color: '#000', fontSize: 12 },
},
grid: {
left: '8%',
right: '5%',
top: '18%',
bottom: '8%',
containLabel: true,
},
xAxis: [
{
type: 'category',
data: companyNames,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
textStyle: { color: '#000' },
rotate: companyNames.length > 4 ? 15 : 0,
interval: 0,
fontSize: 12,
},
},
],
yAxis: [
{
type: 'value',
show: true,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
show: true,
textStyle: { color: '#000' },
},
splitLine: { show: false },
name: '隐患数量',
nameTextStyle: { fontSize: 12 },
},
],
series: [
{
name: '重大隐患量',
type: 'bar',
data: majorTotal,
itemStyle: {
normal: {
color: '#c92a2a', // 红色
},
},
label: {
show: true,
position: 'top',
textStyle: { color: '#c92a2a', fontSize: 11 },
formatter: (params) => `${params.value}`,
},
barWidth: '20%',
},
{
name: '重大隐患未整改量',
type: 'bar',
data: majorRectified,
itemStyle: {
normal: {
color: '#ffa94d', // 橙色
},
},
label: {
show: true,
position: 'top',
textStyle: { color: '#ffa94d', fontSize: 11 },
formatter: (params) => `${params.value}`,
},
barWidth: '20%',
},
{
name: '一般隐患量',
type: 'bar',
data: generalTotal,
itemStyle: {
normal: {
color: '#4285F4', // 蓝色
},
},
label: {
show: true,
position: 'top',
textStyle: { color: '#4285F4', fontSize: 11 },
formatter: (params) => `${params.value}`,
},
barWidth: '20%',
},
{
name: '一般隐患未整改量',
type: 'bar',
data: generalRectified,
itemStyle: {
normal: {
color: '#ffe066', // 黄色
},
},
label: {
show: true,
position: 'top',
textStyle: { color: '#d4a000', fontSize: 11 },
formatter: (params) => `${params.value}`,
},
barWidth: '20%',
},
],
};
this.echartsInstances.hiddenRectifyChart.setOption(option);
this.setupResizeHandler('hiddenRectifyChart', this.renderHiddenRectifyChart);
};
// 排名列表组件(支持公司筛选)
renderRankingList = () => {
const { hiddenSubData, selectedCompany, onCompanyChange, companyData } = this.props;
let hiddenRanking = hiddenSubData?.hiddenRanking || [];
// 根据选中的公司筛选数据
const selectedCompanyName = this.getSelectedCompanyName();
if (selectedCompanyName) {
hiddenRanking = hiddenRanking.filter((item) => item.companyName === selectedCompanyName);
}
// if (hiddenRanking.length === 0) {
// return (
// <div style={{ textAlign: 'center', padding: '50px', color: '#999' }}>
// {selectedCompanyName ? `${selectedCompanyName} 暂无隐患排名数据` : '暂无隐患排名数据'}
// </div>
// );
// }
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div
style={{
textAlign: 'center',
padding: '20px 0 0 0',
fontWeight: 'bold',
fontSize: '17px',
color: '#000',
}}
>
隐患次数排名列表
</div>
<div style={{ textAlign: 'right', padding: '10px 20px' }}>
<span style={{ marginRight: '8px', fontSize: '14px', color: '#000' }}>选择公司</span>
<Select
value={selectedCompany || undefined}
onChange={onCompanyChange}
style={{ width: 150 }}
allowClear
placeholder="全部公司"
>
{this.getCompanyOptions()}
</Select>
</div>
{/* 公司筛选器 - 与 TrainingContent 样式保持一致 */}
<div style={{ flex: 1, overflow: 'auto', padding: '0 20px 20px 20px' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ backgroundColor: '#f5f5f5', borderBottom: '2px solid #e8e8e8' }}>
<th style={{ padding: '12px 8px', textAlign: 'center', fontWeight: 'bold', color: '#000' }}>序号</th>
<th style={{ padding: '12px 8px', textAlign: 'center', fontWeight: 'bold', color: '#000' }}>
隐患名称
</th>
<th style={{ padding: '12px 8px', textAlign: 'center', fontWeight: 'bold', color: '#000' }}>数量</th>
</tr>
</thead>
<tbody>
{hiddenRanking.length > 0 ? (
hiddenRanking.map((item, index) => (
<tr
key={index}
style={{
borderBottom: '1px solid #e8e8e8',
backgroundColor: index % 2 === 0 ? '#fafafa' : '#fff',
}}
>
<td style={{ padding: '10px 8px', textAlign: 'center', color: '#000' }}>
<span
style={{
padding: '10px 8px',
textAlign: 'center',
color: '#000',
fontWeight: 'bold',
}}
>
{index + 1}
</span>
</td>
<td style={{ padding: '10px 8px', textAlign: 'center', color: '#000' }}>{item.hiddenName}</td>
<td style={{ padding: '10px 8px', textAlign: 'center', color: '#000' }}>
<span style={{ color: '#4285F4', fontWeight: 'bold' }}>{item.qty}</span>
</td>
</tr>
))
) : (
<div style={{ textAlign: 'center', padding: '50px', color: '#999' }}>{'暂无隐患排名数据'}</div>
)}
</tbody>
</table>
</div>
</div>
);
};
setupResizeHandler = (chartName, renderMethod) => {
const resizeHandler = () => {
if (this.echartsInstances[chartName] && !this.isUnmounted) {
this.echartsInstances[chartName].resize();
}
};
this.chartResizeHandlers[chartName] = resizeHandler;
window.addEventListener('resize', resizeHandler);
};
initAllCharts = () => {
if (this.isUnmounted) return;
setTimeout(() => {
if (this.isUnmounted) return;
this.renderHiddenBarChart();
this.renderHiddenRectifyChart();
}, 100);
};
disposeAllCharts = () => {
Object.keys(this.echartsInstances).forEach((key) => {
if (this.echartsInstances[key]) {
try {
this.echartsInstances[key].dispose();
} catch (e) {
console.warn(`Dispose chart ${key} error:`, e);
}
this.echartsInstances[key] = null;
}
});
Object.keys(this.chartResizeHandlers).forEach((key) => {
if (this.chartResizeHandlers[key]) {
window.removeEventListener('resize', this.chartResizeHandlers[key]);
}
});
this.chartResizeHandlers = {};
};
componentDidMount() {
this.isUnmounted = false;
this.initAllCharts();
}
componentDidUpdate(prevProps) {
// 当 hiddenSubData 或 selectedCompany 变化时重新渲染图表
if (
prevProps.hiddenSubData !== this.props.hiddenSubData ||
prevProps.selectedCompany !== this.props.selectedCompany
) {
this.renderHiddenBarChart();
this.renderHiddenRectifyChart();
}
}
componentWillUnmount() {
this.isUnmounted = true;
this.disposeAllCharts();
}
render() {
const { selectedCompany, onCompanyChange } = this.props;
return (
<div className={styles.trainingContentWrapper}>
<div className={styles.trainingGrid}>
{/* 第一行 - 柱状图left+ 排名列表right */}
<div className={styles.trainingRow}>
<div className={styles.trainingCard} style={{ flex: '0 0 70%' }}>
{/* 添加公司筛选器(左上角,与 TrainingContent 样式一致) */}
<div id="hiddenBarChart" className={styles.trainingChartContainer}></div>
</div>
<div className={styles.trainingCard} style={{ flex: '0 0 30%' }}>
{this.renderRankingList()}
</div>
</div>
{/* 第二行 - 隐患整改情况柱状图 */}
<div className={styles.trainingRow}>
<div className={styles.trainingCard}>
<div id="hiddenRectifyChart" className={styles.trainingChartContainer}></div>
</div>
</div>
</div>
</div>
);
}
}
export default HiddenSolve;

View File

@ -1,13 +1,18 @@
// HomeContent.js
import React, { useEffect, useRef } from 'react';
import { Row, Col, Icon, Carousel } from 'antd';
import { Row, Col, Icon, Modal, Button } from 'antd';
import Slider from 'react-slick';
import styles from './../fullinter.less';
import echarts from 'echarts';
import configc from '../../config';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
import { extendRule } from '../../utils/common';
class HomeContent extends React.Component {
constructor(props) {
super(props);
this.carouselRef = React.createRef();
this.sliderRef = React.createRef();
this.echartsInstances = {
riskLevel: null,
safeCheckChart: null,
@ -16,6 +21,7 @@ class HomeContent extends React.Component {
};
this.chartResizeHandlers = {};
this.isUnmounted = false;
this.autoplayTimer = null;
}
waitForElement = (elementId, maxRetries = 10) => {
@ -111,46 +117,6 @@ class HomeContent extends React.Component {
window.addEventListener('resize', resizeHandler);
};
transformDat = (originalData, barTopColor, num) => {
if (!originalData || !Array.isArray(originalData) || originalData.length === 0) {
return { companyNames: [], series: [], legendData: [] };
}
let allTypes = [];
if (num == 1) {
allTypes = [...new Set(originalData.flatMap((item) => item.details?.map((detail) => detail.jobName)))];
} else {
allTypes = [...new Set(originalData.flatMap((item) => item.details?.map((detail) => detail.name)))];
}
allTypes = allTypes.filter(Boolean);
const companyNames = originalData?.map((item) => item.company);
const series = allTypes?.map((typeName, index) => ({
name: typeName,
type: 'bar',
itemStyle: {
normal: {
color: function (params) {
if (num == 1) {
return barTopColor[index];
} else {
return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: barTopColor[index][0] },
{ offset: 1, color: barTopColor[index][1] },
]);
}
},
},
},
data: originalData?.map((company) => {
const detail = company.details?.find((d) => (num == 1 ? d.jobName : d.name) === typeName);
return detail ? detail.qty : 0;
}),
}));
return { companyNames, series, legendData: allTypes };
};
safeCheckChart = async () => {
if (this.isUnmounted) return;
const elementExists = await this.waitForElement('safeCheckChart');
@ -164,48 +130,130 @@ class HomeContent extends React.Component {
let safeCheckCharts = document.getElementById('safeCheckChart');
if (!safeCheckCharts) return;
const barTopColor = ['#02c3f1', '#53e568', '#a154e9'];
const jobData = this.props.jobTodayTop3 || [];
const hiddenSummary = this.props.hiddenSummary || {};
if (jobData.length === 0) return;
// 检查是否有有效数据
const hasData =
hiddenSummary.monthMajorQty !== undefined ||
hiddenSummary.unfinishMonthMajorQty !== undefined ||
hiddenSummary.monthGeneralQty !== undefined ||
hiddenSummary.unfinishMonthGeneralQty !== undefined;
if (!hasData) {
this.echartsInstances.safeCheckChart = echarts.init(safeCheckCharts);
this.echartsInstances.safeCheckChart.setOption({
title: {
text: '当月隐患统计',
x: 'center',
y: '25%',
textStyle: { fontSize: 16, color: '#fff' },
},
graphic: {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '暂无数据',
fill: '#999',
fontSize: 14,
},
},
});
return;
}
// 定义图例和数据
const legendData = ['重大隐患数', '重大隐患未整改数', '一般隐患数', '一般隐患未整改数'];
const seriesData = [
{ name: '重大隐患数', value: hiddenSummary.monthMajorQty || 0, color: '#c92a2a' },
{ name: '重大隐患未整改数', value: hiddenSummary.unfinishMonthMajorQty || 0, color: '#ffa94d' },
{ name: '一般隐患数', value: hiddenSummary.monthGeneralQty || 0, color: '#ffe066' },
{ name: '一般隐患未整改数', value: hiddenSummary.unfinishMonthGeneralQty || 0, color: '#4285F4' },
];
this.echartsInstances.safeCheckChart = echarts.init(safeCheckCharts);
let xdata = this.transformDat(jobData, barTopColor, 1);
this.echartsInstances.safeCheckChart.setOption({
title: {
text: '当日工作票排名前三家的数据',
textStyle: { fontSize: 16, color: '#fff' },
text: '当月隐患统计',
x: 'center',
y: '5%',
textStyle: { fontSize: 16, color: '#000' },
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function (params) {
if (!params || params.length === 0) return '';
let result = `${params[0].axisValue}<br/>`;
params.forEach((param) => {
result += `${param.marker}${param.seriesName}: ${param.value}<br/>`;
});
return result;
},
},
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: {
data: xdata.legendData,
align: 'right',
right: 10,
itemGap: 16,
itemWidth: 18,
data: legendData,
right: 0,
top: '10%',
itemGap: 12,
itemWidth: 16,
itemHeight: 10,
textStyle: { color: '#fff', fontSize: 14 },
textStyle: { color: '#000', fontSize: 12 },
},
color: seriesData.map((item) => item.color),
grid: {
left: '5%',
right: '8%',
top: '28%',
bottom: '5%',
containLabel: true,
},
color: barTopColor,
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: [
{
type: 'category',
data: xdata.companyNames,
axisLine: { lineStyle: { color: '#3eb2e8' } },
axisLabel: { textStyle: { color: '#fff' } },
data: ['当月隐患'],
axisLine: { lineStyle: { color: '#c4c6c9' } },
axisLabel: { textStyle: { color: '#000', fontSize: 14 } },
axisTick: { show: false },
},
],
yAxis: [
{
type: 'value',
name: '数量',
nameTextStyle: { color: '#000' },
axisLine: { show: false },
axisLabel: { textStyle: { color: '#fff' } },
splitLine: { lineStyle: { color: '#4784e8' } },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { textStyle: { color: '#000' } },
},
],
series: xdata.series,
series: seriesData.map((item) => ({
name: item.name,
type: 'bar',
data: [item.value],
itemStyle: {
normal: {
color: item.color,
barBorderRadius: [8, 8, 0, 0],
},
emphasis: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
label: {
show: true,
position: 'top',
textStyle: { color: '#000', fontSize: 12 },
formatter: '{c}',
},
barWidth: '10%',
barGap: '30%',
barCategoryGap: '20%',
})),
});
const resizeHandler = () => {
@ -230,9 +278,33 @@ class HomeContent extends React.Component {
const dangerOperationCharts = document.getElementById('dangerOperationChart');
if (!dangerOperationCharts) return;
const linkData = this.props.linkSum || [];
if (linkData.length === 0) return;
const linkData = this.props.jobTodayQty || [];
// 如果没有数据,显示“暂无数据”提示
if (linkData.length === 0) {
this.echartsInstances.dangerOperation = echarts.init(dangerOperationCharts);
this.echartsInstances.dangerOperation.setOption({
title: {
text: '当日工作票的统计数量',
x: 'center',
y: '25%',
textStyle: { fontSize: 16, color: '#999' },
},
graphic: {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '暂无数据',
fill: '#999',
fontSize: 14,
},
},
});
return;
}
// 有数据时正常渲染图表
this.echartsInstances.dangerOperation = echarts.init(dangerOperationCharts);
const xAxisData = linkData.map((item) => item.name);
const seriesData = linkData.map((item) => item.qty);
@ -253,10 +325,10 @@ class HomeContent extends React.Component {
formatter: function (params) {
const color = params[0].color;
return `<div style="display: flex; align-items: center;">
<span style="display: inline-block; width: 12px; height: 12px; background-color: ${color}; margin-right: 8px;"></span>
<span>${params[0].name}:</span>
<span style="font-weight: bold; margin-left: 8px; font-size: 16px;">${params[0].value}</span>
</div>`;
<span style="display: inline-block; width: 12px; height: 12px; background-color: ${color}; margin-right: 8px;"></span>
<span>${params[0].name}:</span>
<span style="font-weight: bold; margin-left: 8px; font-size: 16px;">${params[0].value}</span>
</div>`;
},
backgroundColor: 'rgba(255, 255, 255, 0.5)',
borderColor: '#FFFFFF',
@ -268,7 +340,7 @@ class HomeContent extends React.Component {
{
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#000' } },
axisLine: { lineStyle: { color: '#c4c6c9' } },
axisTick: { show: false },
axisLabel: { textStyle: { color: '#000' }, rotate: 30, interval: 0 },
},
@ -374,17 +446,17 @@ class HomeContent extends React.Component {
},
legend: {
data: ['培训人次', '培训场次'],
orient: 'vertical', // 垂直排列
right: 0, // 水平居中
top: 'middle', // 垂直居中
itemGap: 16, // 图例项间隔
orient: 'vertical',
right: 0,
top: 'middle',
itemGap: 16,
itemWidth: 18,
itemHeight: 12,
textStyle: { color: '#000', fontSize: 14 },
},
grid: {
left: '5%',
right: '5%', // 为右侧垂直图例留出空间
right: '5%',
top: '18%',
bottom: '5%',
containLabel: true,
@ -393,8 +465,8 @@ class HomeContent extends React.Component {
{
type: 'category',
data: listNAME,
axisLine: { show: false }, // 隐藏x轴线
axisTick: { show: false }, // 隐藏x轴刻度线
axisLine: { lineStyle: { color: '#c4c6c9' } },
axisTick: { show: false },
axisLabel: {
textStyle: { color: '#000' },
rotate: listNAME.length > 4 ? 15 : 0,
@ -405,16 +477,16 @@ class HomeContent extends React.Component {
yAxis: [
{
type: 'value',
show: true, // 显示y轴
axisLine: { show: false }, // 隐藏y轴线
axisTick: { show: false }, // 隐藏y轴刻度线
show: true,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
show: true, // 显示数值标签
show: true,
textStyle: { color: '#000' },
},
splitLine: { show: false }, // 隐藏横向网格线
name: '', // 不显示单位名称
nameTextStyle: { show: false }, // 隐藏单位文字
splitLine: { show: false },
name: '',
nameTextStyle: { show: false },
},
],
series: [
@ -424,8 +496,8 @@ class HomeContent extends React.Component {
data: monthPersonCount,
itemStyle: {
normal: {
color: '#4285F4', // 蓝色
barBorderRadius: 12, // 柱体圆角
color: '#4285F4',
barBorderRadius: 12,
},
},
label: {
@ -442,8 +514,8 @@ class HomeContent extends React.Component {
data: monthRecordCount,
itemStyle: {
normal: {
color: '#ffe066', // 黄色
barBorderRadius: 12, // 柱体圆角(上左、上右、下右、下左)
color: '#ffe066',
barBorderRadius: 12,
},
},
label: {
@ -468,6 +540,28 @@ class HomeContent extends React.Component {
window.addEventListener('resize', resizeHandler);
};
// 启动自动轮播
startAutoplay = (intervalTime) => {
this.stopAutoplay();
if (!intervalTime || intervalTime <= 0) return;
this.autoplayTimer = setInterval(() => {
if (this.sliderRef.current && !this.isUnmounted) {
const nextIndex = (this.props.currentMediaIndex + 1) % (this.props.trainingData?.listVideoImg?.length || 1);
this.props.onCarouselChange?.(nextIndex);
this.sliderRef.current.slickGoTo(nextIndex);
}
}, intervalTime);
};
// 停止自动轮播
stopAutoplay = () => {
if (this.autoplayTimer) {
clearInterval(this.autoplayTimer);
this.autoplayTimer = null;
}
};
initAllCharts = () => {
if (this.isUnmounted) return;
setTimeout(() => {
@ -500,26 +594,52 @@ class HomeContent extends React.Component {
};
handlePrev = () => {
if (this.carouselRef.current) {
this.carouselRef.current.prev();
if (this.sliderRef.current) {
this.sliderRef.current.slickPrev();
const nextIndex = this.props.currentMediaIndex - 1;
if (nextIndex >= 0) {
this.props.onCarouselChange?.(nextIndex);
}
// 鼠标点击时重启自动播放计时器
this.restartAutoplay();
}
};
handleNext = () => {
if (this.carouselRef.current) {
this.carouselRef.current.next();
if (this.sliderRef.current) {
this.sliderRef.current.slickNext();
const nextIndex = this.props.currentMediaIndex + 1;
const total = this.props.trainingData?.listVideoImg?.length || 0;
if (nextIndex < total) {
this.props.onCarouselChange?.(nextIndex);
}
// 鼠标点击时重启自动播放计时器
this.restartAutoplay();
}
};
handleCarouselChange = (current) => {
this.props.onCarouselChange?.(current);
// 重启自动播放
restartAutoplay = () => {
const playSet = this.props.trainingData?.playSet || {};
const isAutoplay = playSet.IMG_ISRE !== undefined ? playSet.IMG_ISRE : true;
const autoplaySpeed = playSet.IMG_TIMESPAN ? playSet.IMG_TIMESPAN * 1000 : 5000;
if (isAutoplay && autoplaySpeed > 0) {
this.startAutoplay(autoplaySpeed);
}
};
handleDotClick = (index) => {
this.props.onDotClick?.(index);
if (this.carouselRef.current) {
this.carouselRef.current.goTo(index);
if (this.sliderRef.current) {
this.sliderRef.current.slickGoTo(index);
}
// 点击圆点时重启自动播放
this.restartAutoplay();
};
handleCarouselChange = (current) => {
this.props.onCarouselChange?.(current);
};
componentDidMount() {
@ -528,12 +648,20 @@ class HomeContent extends React.Component {
}
componentDidUpdate(prevProps) {
// 当 trainingData 变化时,重新初始化自动播放
if (
prevProps.trainingData?.playSet?.IMG_ISRE !== this.props.trainingData?.playSet?.IMG_ISRE ||
prevProps.trainingData?.playSet?.IMG_TIMESPAN !== this.props.trainingData?.playSet?.IMG_TIMESPAN
) {
this.restartAutoplay();
}
if (
prevProps.riskTypeRate !== this.props.riskTypeRate ||
prevProps.jobTodayTop3 !== this.props.jobTodayTop3 ||
prevProps.linkSum !== this.props.linkSum ||
prevProps.hiddenSummary !== this.props.hiddenSummary ||
prevProps.jobTodayQty !== this.props.jobTodayQty ||
prevProps.taskTop3 !== this.props.taskTop3 ||
prevProps.trainingData !== this.props.trainingData // 新增
prevProps.trainingData !== this.props.trainingData
) {
this.disposeAllCharts();
this.initAllCharts();
@ -542,18 +670,170 @@ class HomeContent extends React.Component {
componentWillUnmount() {
this.isUnmounted = true;
this.stopAutoplay();
this.disposeAllCharts();
}
// 新增:下载附件方法
handleDownload = (file) => {
const fileUrl = configc.picServerHost + file.Nav_ImgFile.FILE_PATH;
const link = document.createElement('a');
link.href = fileUrl;
link.download = file.Nav_ImgFile.FILE_NAME;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// 新增:预览附件方法
handlePreview = (file) => {
const fileUrl = configc.picServerHost + file.Nav_ImgFile.FILE_PATH;
window.open(fileUrl, '_blank');
};
// 新增:渲染公告弹窗内容
renderAnnouncementModal = () => {
const { currentAnnouncement, announcementDetailLoading } = this.props;
if (announcementDetailLoading) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<Icon type="loading" style={{ fontSize: '32px' }} />
</div>
);
}
if (!currentAnnouncement) return null;
const { TITLE, ABSTRACT, START, END, CONTENT, Nav_Files = [] } = currentAnnouncement;
return (
<div>
{/* 标题 */}
<div style={{ textAlign: 'center', fontSize: '16px', color: '#333', marginBottom: '16px', fontWeight: 'bold' }}>
{TITLE}
</div>
{/* 摘要 */}
<div
style={{
fontSize: '14px',
color: '#676767',
wordBreak: 'break-all',
wordWrap: 'break-word',
marginBottom: '12px',
lineHeight: '1.5',
}}
>
{ABSTRACT}
</div>
{/* 日期 */}
<div style={{ fontSize: '12px', color: '#999', marginBottom: '16px', textAlign: 'center' }}>
{START} {END}
</div>
{/* 分割线 */}
<div style={{ borderTop: '1px solid #e8e8e8', marginBottom: '16px' }}></div>
{/* 正文内容 */}
<div
className={styles.announcementContent}
dangerouslySetInnerHTML={{ __html: CONTENT || '' }}
style={{
fontSize: '14px',
lineHeight: '1.6',
color: '#333',
marginBottom: '16px',
}}
/>
{/* 附件列表 */}
{Nav_Files && Nav_Files.length > 0 && (
<div style={{ marginTop: '16px' }}>
<div style={{ fontWeight: 'bold', marginBottom: '12px', fontSize: '14px', color: '#333' }}>
附件 ({Nav_Files.length})
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{Nav_Files.map((file, index) => (
<div
key={file.ID || index}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '8px 12px',
backgroundColor: '#f5f5f5',
borderRadius: '4px',
}}
>
<span style={{ fontSize: '14px', color: '#333', flex: 1 }}>
<Icon type="paper-clip" style={{ marginRight: '8px', color: '#1890ff' }} />
{file.Nav_ImgFile?.FILE_NAME || '未知文件'}
</span>
<div>
<Button
type="link"
size="small"
onClick={() => this.handlePreview(file)}
style={{ padding: '0 8px' }}
>
<Icon type="eye" />
预览
</Button>
<Button
type="link"
size="small"
onClick={() => this.handleDownload(file)}
style={{ padding: '0 8px' }}
>
<Icon type="download" />
下载
</Button>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
};
render() {
const {
riskTypeRate = [],
riskTotal = [],
mediaList = [],
announcementList = [],
currentMediaIndex = 0,
hiddenSummary = {},
trainingData = {}, // 新增 props
onAnnouncementClick,
announcementModalVisible,
onAnnouncementModalClose,
} = this.props;
// 获取播放配置,提供默认值
const playSet = trainingData.playSet || {};
const videoConfig = {
autoPlay: playSet.V_ISAUTO !== undefined ? playSet.V_ISAUTO : true,
loop: playSet.V_ISRE !== undefined ? playSet.V_ISRE : true,
muted: playSet.V_ISSILENT !== undefined ? playSet.V_ISSILENT : true,
controls: playSet.V_ISSHOWCONTROL !== undefined ? playSet.V_ISSHOWCONTROL : false,
};
const isAutoplay = playSet.IMG_ISRE !== undefined ? playSet.IMG_ISRE : true;
const autoplaySpeed = playSet.IMG_TIMESPAN ? playSet.IMG_TIMESPAN * 1000 : 5000;
const slickSettings = {
dots: false,
arrows: false, // 🔑 隐藏默认箭头 ← 添加这一行
infinite: true,
speed: 500,
slidesToShow: 1,
slidesToScroll: 1,
autoplay: isAutoplay,
autoplaySpeed: autoplaySpeed,
fade: true,
pauseOnHover: true,
afterChange: this.handleCarouselChange,
};
return (
<div className={styles.homeContentWrapper}>
<Row className={styles.homeContentRow} gutter={0}>
@ -581,12 +861,18 @@ class HomeContent extends React.Component {
<Row className={styles.riskRow}>
<Col span={8} className={styles.hiddenStatsCol}>
<div className={styles.hiddenTitle}>年度隐患数据</div>
{riskTotal.map((item, index) => (
<div key={index} className={styles.hiddenStatItem}>
<div className={styles.hiddenStatName}>{item.name}</div>
<div className={styles.hiddenStatValue}>{item.value}</div>
</div>
))}
<div className={styles.hiddenStatItem}>
<div className={styles.hiddenStatName}>年度重大隐患数</div>
<div className={styles.hiddenStatValue}>{hiddenSummary.majorQty}</div>
</div>
<div className={styles.hiddenStatItem}>
<div className={styles.hiddenStatName}>年度一般隐患数量</div>
<div className={styles.hiddenStatValue}>{hiddenSummary.generalQty}</div>
</div>
<div className={styles.hiddenStatItem}>
<div className={styles.hiddenStatName}>未整改隐患数量</div>
<div className={styles.hiddenStatValue}>{hiddenSummary.unfinishQty}</div>
</div>
</Col>
<Col span={16} className={styles.chartCol}>
<div id="safeCheckChart" className={styles.chartContainer}></div>
@ -598,47 +884,55 @@ class HomeContent extends React.Component {
{/* 中间区域 */}
<Col span={10} className={styles.middleCol}>
<div className={styles.sloganCard}>
<div className={styles.sloganText}>安全方针以人为本关注健康依法治企安全发展</div>
<div className={styles.sloganSubText}>安全理念一切风险皆可控一切事故皆可防</div>
<div className={styles.sloganContent} dangerouslySetInnerHTML={{ __html: trainingData.TITLE }} />
</div>
<div className={styles.carouselCard}>
<div className={styles.carouselWrapper}>
<Carousel
ref={this.carouselRef}
autoplay
autoplaySpeed={5000}
effect="fade"
dots={false}
pauseOnHover={true}
afterChange={this.handleCarouselChange}
className={styles.carousel}
>
{mediaList.map((item, index) => (
<div key={index} className={styles.carouselItem}>
{item.type === 'video' ? (
<video src={item.url} className={styles.mediaVideo} autoPlay muted loop playsInline />
) : (
<img src={item.url} alt={`轮播图 ${index + 1}`} className={styles.mediaImage} />
)}
</div>
))}
</Carousel>
<div className={styles.customArrowLeft} onClick={this.handlePrev}>
<Icon type="left" style={{ fontSize: '24px', color: 'rgba(0, 0, 0, 0.5)' }} />
</div>
<div className={styles.customArrowRight} onClick={this.handleNext}>
<Icon type="right" style={{ fontSize: '24px', color: 'rgba(0, 0, 0, 0.5)' }} />
</div>
<div className={styles.customDots}>
{mediaList.map((_, index) => (
<span
key={index}
className={`${styles.dot} ${currentMediaIndex === index ? styles.activeDot : ''}`}
onClick={() => this.handleDotClick(index)}
{trainingData.listVideoImg && trainingData.listVideoImg[0] ? (
trainingData.listVideoImg[0].TYPE == 10 ? (
<video
src={configc.picServerHost + trainingData.listVideoImg[0].FILE_PATH}
className={styles.mediaVideo}
autoplay={videoConfig.autoPlay ? 'autoplay' : undefined}
loop={videoConfig.loop ? 'loop' : undefined}
muted={videoConfig.muted ? 'muted' : undefined}
controls={videoConfig.controls ? 'controls' : undefined}
playsInline
/>
))}
</div>
) : (
<div>
<Slider ref={this.sliderRef} {...slickSettings} className={styles.carousel}>
{trainingData.listVideoImg?.map((item, index) => (
<div key={index} className={styles.carouselItem}>
<img
src={configc.picServerHost + item.FILE_PATH}
alt={`轮播图 ${index + 1}`}
className={styles.mediaImage}
/>
</div>
))}
</Slider>
<div className={styles.customArrowLeft} onClick={this.handlePrev}>
<Icon type="left" style={{ fontSize: '24px', color: 'rgba(0, 0, 0, 0.5)' }} />
</div>
<div className={styles.customArrowRight} onClick={this.handleNext}>
<Icon type="right" style={{ fontSize: '24px', color: 'rgba(0, 0, 0, 0.5)' }} />
</div>
<div className={styles.customDots}>
{trainingData.listVideoImg?.map((_, index) => (
<span
key={index}
className={`${styles.dot} ${currentMediaIndex === index ? styles.activeDot : ''}`}
onClick={() => this.handleDotClick(index)}
/>
))}
</div>
</div>
)
) : (
<div></div>
)}
</div>
</div>
@ -648,23 +942,21 @@ class HomeContent extends React.Component {
<Icon type="sound" className={styles.announcementIcon} />
<span>公司公告</span>
</div>
<span className={styles.announcementCount}> {announcementList.length} 条公告</span>
<span className={styles.announcementCount}> {trainingData.listAnnourcement?.length || 0} 条公告</span>
</div>
<div className={styles.announcementList}>
{announcementList.length > 0 ? (
{trainingData.listAnnourcement?.length > 0 ? (
<ul className={styles.announcementUl}>
{announcementList.map((item, index) => (
{trainingData.listAnnourcement.map((item, index) => (
<li
key={item.id || index}
className={styles.announcementItem}
onClick={() => item.url && window.open(item.url)}
onClick={() => onAnnouncementClick && onAnnouncementClick(item)}
>
<span className={styles.announcementItemTitle} title={item.title}>
{item.title}
</span>
<span className={styles.announcementItemTime}>
{item.publishTime || item.createTime || item.date}
<span className={styles.announcementItemTitle} title={item.TITLE}>
{item.TITLE}
</span>
<span className={styles.announcementItemTime}>{item.START}</span>
</li>
))}
</ul>
@ -684,14 +976,13 @@ class HomeContent extends React.Component {
<Row className={styles.riskRow}>
<Col span={8} className={styles.trainingStatsCol}>
<div className={styles.trainingTitle}>年度培训数据</div>
{this.props.trainingData?.listNAME?.map((name, index) => (
{trainingData?.listNAME?.map((name, index) => (
<div key={index} className={styles.trainingStatItem}>
<div className={styles.trainingStatName}>{name}</div>
<div className={styles.trainingStatValue}>{this.props.trainingData?.YearCount?.[index] || 0}</div>
<div className={styles.trainingStatValue}>{trainingData?.YearCount?.[index] || 0}</div>
</div>
))}
{/* 如果 listNAME 为空,显示默认提示 */}
{(!this.props.trainingData?.listNAME || this.props.trainingData.listNAME.length === 0) && (
{(!trainingData?.listNAME || trainingData.listNAME.length === 0) && (
<div style={{ textAlign: 'center', color: '#999', padding: '20px 0' }}>暂无培训数据</div>
)}
</Col>
@ -708,6 +999,21 @@ class HomeContent extends React.Component {
</div>
</Col>
</Row>
{/* 公告详情弹窗 */}
<Modal
title="公告详情"
visible={announcementModalVisible}
onCancel={onAnnouncementModalClose}
footer={[
<Button key="close" onClick={onAnnouncementModalClose}>
关闭
</Button>,
]}
width="80%"
bodyStyle={{ padding: '20px', maxHeight: '70vh', overflowY: 'auto' }}
>
{this.renderAnnouncementModal()}
</Modal>
</div>
);
}

View File

@ -0,0 +1,515 @@
// RiskControl.js - 风险管控页面组件
import React from 'react';
import { Table } from 'antd';
import styles from './../fullinter.less';
import echarts from 'echarts';
class RiskControl extends React.Component {
constructor(props) {
super(props);
this.echartsInstances = {
stackBarChart: null, // 堆叠柱状图
typeBarChart: null, // 风险类别柱状图
};
this.chartResizeHandlers = {};
this.isUnmounted = false;
}
waitForElement = (elementId, maxRetries = 10) => {
return new Promise((resolve) => {
let retries = 0;
const checkInterval = setInterval(() => {
const element = document.getElementById(elementId);
if (element || retries >= maxRetries) {
clearInterval(checkInterval);
resolve(!!element);
}
retries++;
}, 50);
});
};
// 堆叠柱状图: 各公司风险统计(与 trainingContent 样式一致)
renderStackBarChart = async () => {
if (this.isUnmounted) return;
const elementExists = await this.waitForElement('stackBarChart');
if (!elementExists || this.isUnmounted) return;
if (this.echartsInstances.stackBarChart) {
this.echartsInstances.stackBarChart.dispose();
this.echartsInstances.stackBarChart = null;
}
const chartDom = document.getElementById('stackBarChart');
if (!chartDom) return;
this.echartsInstances.stackBarChart = echarts.init(chartDom);
const { riskSubData } = this.props;
const riskList = riskSubData?.riskList || [];
if (riskList.length === 0) {
this.echartsInstances.stackBarChart.setOption({
title: {
text: '各家公司的风险统计情况',
x: 'center',
y: '25%',
textStyle: { fontSize: 16, color: '#999' },
},
graphic: {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '暂无数据',
fill: '#999',
fontSize: 14,
},
},
});
return;
}
const companyNames = riskList.map((item) => item.companyName);
const majorCounts = riskList.map((item) => item.majorCount);
const largerCounts = riskList.map((item) => item.largerCount);
const generalCounts = riskList.map((item) => item.generalCount);
const lowCounts = riskList.map((item) => item.lowCount);
const option = {
title: {
text: '各家公司的风险统计情况',
x: 'center',
y: '5%',
textStyle: { fontSize: 16, color: '#000', fontWeight: 'bold' },
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function (params) {
let result = `${params[0].axisValue}<br/>`;
params.forEach((param) => {
result += `${param.marker}${param.seriesName}: ${param.value}<br/>`;
});
return result;
},
},
legend: {
data: ['重大风险', '较大风险', '一般风险', '低风险'],
orient: 'vertical',
right: '3%',
top: '5%',
itemGap: 16,
itemWidth: 18,
itemHeight: 12,
textStyle: { color: '#000', fontSize: 14 },
},
grid: {
left: '8%',
right: '5%',
top: '18%',
bottom: '8%',
containLabel: true,
},
xAxis: [
{
type: 'category',
data: companyNames,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
textStyle: { color: '#000' },
rotate: companyNames.length > 4 ? 15 : 0,
interval: 0,
fontSize: 12,
},
},
],
yAxis: [
{
type: 'value',
show: true,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
show: true,
textStyle: { color: '#000' },
},
splitLine: { show: false },
name: '风险数量',
nameTextStyle: { fontSize: 12 },
},
],
series: [
{
name: '重大风险',
type: 'bar',
stack: 'total',
data: majorCounts,
itemStyle: { normal: { color: '#c92a2a' } },
label: {
show: true,
position: 'inside',
textStyle: { color: '#fff', fontSize: 11 },
formatter: (params) => (params.value > 0 ? params.value : ''),
},
barWidth: '35%',
},
{
name: '较大风险',
type: 'bar',
stack: 'total',
data: largerCounts,
itemStyle: { normal: { color: '#ffa94d' } },
label: {
show: true,
position: 'inside',
textStyle: { color: '#fff', fontSize: 11 },
formatter: (params) => (params.value > 0 ? params.value : ''),
},
barWidth: '35%',
},
{
name: '一般风险',
type: 'bar',
stack: 'total',
data: generalCounts,
itemStyle: { normal: { color: '#ffe066' } },
label: {
show: true,
position: 'inside',
textStyle: { color: '#fff', fontSize: 11 },
formatter: (params) => (params.value > 0 ? params.value : ''),
},
barWidth: '35%',
},
{
name: '低风险',
type: 'bar',
stack: 'total',
data: lowCounts,
itemStyle: { normal: { color: '#4285F4' } },
label: {
show: true,
position: 'inside',
textStyle: { color: '#fff', fontSize: 11 },
formatter: (params) => (params.value > 0 ? params.value : ''),
},
barWidth: '35%',
},
],
};
this.echartsInstances.stackBarChart.setOption(option);
this.setupResizeHandler('stackBarChart', this.renderStackBarChart);
};
// 风险类别柱状图
renderTypeBarChart = async () => {
if (this.isUnmounted) return;
const elementExists = await this.waitForElement('typeBarChart');
if (!elementExists || this.isUnmounted) return;
if (this.echartsInstances.typeBarChart) {
this.echartsInstances.typeBarChart.dispose();
this.echartsInstances.typeBarChart = null;
}
const chartDom = document.getElementById('typeBarChart');
if (!chartDom) return;
this.echartsInstances.typeBarChart = echarts.init(chartDom);
const { riskSubData } = this.props;
const riskTypeList = riskSubData?.riskTypeList || [];
if (riskTypeList.length === 0) {
this.echartsInstances.typeBarChart.setOption({
title: {
text: '风险类别统计情况',
x: 'center',
y: '25%',
textStyle: { fontSize: 16, color: '#999' },
},
graphic: {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '暂无数据',
fill: '#999',
fontSize: 14,
},
},
});
return;
}
const typeNames = riskTypeList.map((item) => item.typeName);
const quantities = riskTypeList.map((item) => item.qty);
const option = {
title: {
text: '风险类别统计情况',
x: 'center',
y: '5%',
textStyle: { fontSize: 16, color: '#000', fontWeight: 'bold' },
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function (params) {
let result = `${params[0].axisValue}<br/>`;
params.forEach((param) => {
result += `${param.marker}${param.seriesName}: ${param.value}<br/>`;
});
return result;
},
},
grid: {
left: '8%',
right: '5%',
top: '18%',
bottom: '8%',
containLabel: true,
},
xAxis: [
{
type: 'category',
data: typeNames,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
textStyle: { color: '#000' },
// rotate: typeNames.length > 4 ? 15 : 0,
interval: 0,
fontSize: 12,
},
},
],
yAxis: [
{
type: 'value',
show: true,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
show: true,
textStyle: { color: '#000' },
},
splitLine: { show: false },
},
],
series: [
{
name: '风险数量',
type: 'bar',
data: quantities,
itemStyle: {
normal: {
color: '#4285F4', // 与 trainingContent 一致的蓝色
},
},
label: {
show: true,
position: 'top',
textStyle: { color: '#4285F4', fontSize: 12 },
formatter: (params) => `${params.value}`,
},
barWidth: '50%',
},
],
};
this.echartsInstances.typeBarChart.setOption(option);
this.setupResizeHandler('typeBarChart', this.renderTypeBarChart);
};
// 表格: 各公司风险统计明细表
renderRiskTable = () => {
const { riskSubData } = this.props;
const riskList = riskSubData?.riskList || [];
if (riskList.length === 0) {
return <div style={{ textAlign: 'center', padding: '50px', color: '#999' }}>暂无风险数据</div>;
}
// 表格列配置(与 trainingContent 样式保持一致)
const columns = [
{
title: '公司名称',
dataIndex: 'companyName',
key: 'companyName',
align: 'center',
width: 120,
render: (text) => <strong>{text}</strong>,
},
{
title: '重大风险',
dataIndex: 'majorCount',
key: 'majorCount',
align: 'center',
width: 100,
render: (text) => <span style={{ color: '#c92a2a', fontWeight: 'bold' }}>{text}</span>,
},
{
title: '较大风险',
dataIndex: 'largerCount',
key: 'largerCount',
align: 'center',
width: 100,
render: (text) => <span style={{ color: '#ffa94d', fontWeight: 'bold' }}>{text}</span>,
},
{
title: '一般风险',
dataIndex: 'generalCount',
key: 'generalCount',
align: 'center',
width: 100,
render: (text) => <span style={{ color: '#ffe066', fontWeight: 'bold' }}>{text}</span>,
},
{
title: '低风险',
dataIndex: 'lowCount',
key: 'lowCount',
align: 'center',
width: 100,
render: (text) => <span style={{ color: '#4285F4', fontWeight: 'bold' }}>{text}</span>,
},
{
title: '小计',
dataIndex: 'totalCount',
key: 'totalCount',
align: 'center',
width: 100,
render: (text) => <strong style={{ color: '#000', fontSize: '14px' }}>{text}</strong>,
},
];
const tableData = riskList.map((item, index) => ({
key: index,
companyName: item.companyName,
majorCount: item.majorCount,
largerCount: item.largerCount,
generalCount: item.generalCount,
lowCount: item.lowCount,
totalCount: item.totalCount,
}));
// 合计行
const summary = {
majorTotal: riskList.reduce((sum, item) => sum + (item.majorCount || 0), 0),
largerTotal: riskList.reduce((sum, item) => sum + (item.largerCount || 0), 0),
generalTotal: riskList.reduce((sum, item) => sum + (item.generalCount || 0), 0),
lowTotal: riskList.reduce((sum, item) => sum + (item.lowCount || 0), 0),
totalAll: riskList.reduce((sum, item) => sum + (item.totalCount || 0), 0),
};
const scrollConfig = columns.length > 10 ? { x: columns.length * 100, y: 380 } : {};
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div
style={{
textAlign: 'center',
padding: '20px 0 0 0',
fontWeight: 'bold',
fontSize: '17px',
color: '#000',
}}
>
各家公司的风险统计情况
</div>
<div style={{ flex: 1, overflow: 'auto', padding: '10px' }}>
<Table
columns={columns}
dataSource={tableData}
pagination={false}
scroll={scrollConfig}
size="small"
bordered
className={styles.certificateTable}
/>
</div>
</div>
);
};
setupResizeHandler = (chartName, renderMethod) => {
const resizeHandler = () => {
if (this.echartsInstances[chartName] && !this.isUnmounted) {
this.echartsInstances[chartName].resize();
}
};
this.chartResizeHandlers[chartName] = resizeHandler;
window.addEventListener('resize', resizeHandler);
};
initAllCharts = () => {
if (this.isUnmounted) return;
setTimeout(() => {
if (this.isUnmounted) return;
this.renderStackBarChart();
this.renderTypeBarChart();
}, 100);
};
disposeAllCharts = () => {
Object.keys(this.echartsInstances).forEach((key) => {
if (this.echartsInstances[key]) {
try {
this.echartsInstances[key].dispose();
} catch (e) {
console.warn(`Dispose chart ${key} error:`, e);
}
this.echartsInstances[key] = null;
}
});
Object.keys(this.chartResizeHandlers).forEach((key) => {
if (this.chartResizeHandlers[key]) {
window.removeEventListener('resize', this.chartResizeHandlers[key]);
}
});
this.chartResizeHandlers = {};
};
componentDidMount() {
this.isUnmounted = false;
this.initAllCharts();
}
componentDidUpdate(prevProps) {
if (prevProps.riskSubData !== this.props.riskSubData) {
this.renderStackBarChart();
this.renderTypeBarChart();
}
}
componentWillUnmount() {
this.isUnmounted = true;
this.disposeAllCharts();
}
render() {
return (
<div className={styles.trainingContentWrapper}>
<div className={styles.trainingGrid}>
{/* 第一行 - 表格 + 堆叠柱状图 */}
<div className={styles.trainingRow}>
<div className={styles.trainingCard}>{this.renderRiskTable()}</div>
<div className={styles.trainingCard}>
<div id="stackBarChart" className={styles.trainingChartContainer}></div>
</div>
</div>
{/* 第二行 - 风险类别柱状图 */}
<div className={styles.trainingRow}>
<div className={styles.trainingCard}>
<div id="typeBarChart" className={styles.trainingChartContainer}></div>
</div>
</div>
</div>
</div>
);
}
}
export default RiskControl;

View File

@ -226,7 +226,7 @@ class TrainingContent extends React.Component {
if (companyNames.length === 0) {
this.echartsInstances.chart2.setOption({
title: {
text: '本年度各公司培训分析',
text: '各公司月度培训分析',
x: 'center',
y: '25%',
textStyle: { fontSize: 16, color: '#999' },
@ -247,7 +247,7 @@ class TrainingContent extends React.Component {
const option = {
title: {
text: '本年度各公司培训分析',
text: '各公司月度培训分析',
x: 'center',
y: '5%',
textStyle: { fontSize: 16, color: '#000', fontWeight: 'bold' },
@ -693,7 +693,7 @@ class TrainingContent extends React.Component {
color: '#000',
}}
>
特种作业操作证统计
各公司证件统计
</div>
<div style={{ flex: 1, overflow: 'auto' }}>{this.renderCertificateTable()}</div>
</div>

View File

@ -2,14 +2,18 @@
import React from 'react';
import { connect } from 'dva';
import { withRouter } from 'dva/router';
import { Icon, Row, Col } from 'antd';
import { Icon, Row, Col, message } from 'antd';
import styles from './fullinter.less';
import logo from '../assets/layout/headerno-logo-new.png';
import debounce from 'lodash.debounce';
import storage from '../utils/storage';
import { initFilter } from '../utils/common';
import { initFilter, extendRule } from '../utils/common';
import HomeContent from './FullOther/HomeContent';
import TrainingContent from './FullOther/TrainingContent'; // 新增导入
import TrainingContent from './FullOther/TrainingContent';
import RiskControl from './FullOther/RiskControl';
import ClassBuild from './FullOther/ClassBuild';
import DangerJob from './FullOther/DangerJob';
import HiddenSolve from './FullOther/HiddenSolve';
const getScale = () => {
const width = 1920,
@ -25,13 +29,9 @@ class FullScreen extends React.Component {
this.state = {
nowDate: '',
riskTypeRate: [],
riskTotal: [
{ name: '年度重大隐患数', value: 1 },
{ name: '年度一般隐患数量', value: 89 },
{ name: '未整改隐患数量', value: 18 },
],
linkSum: [],
jobTodayTop3: [],
riskTotal: [],
jobTodayQty: [],
hiddenSummary: [],
taskTop3: [],
scale: getScale(),
configBanner: ['首页', '风险管控', '隐患治理', '班组建设', '危险作业', '安全培训'],
@ -39,14 +39,17 @@ class FullScreen extends React.Component {
currentMediaIndex: 0,
announcementList: [],
activeTab: '首页',
// 新增:培训数据
// 安全培训相关数据
trainingData: {
listNAME: [],
YearCount: [],
MonthRecordCount: [],
MonthPersonCount: [],
listAnnourcement: [],
TITLE: '',
listVideoImg: [],
playSet: [],
},
// 新增:安全培训页面专用数据
trainingSubData: {
listNAME: [],
YearCount: [],
@ -61,12 +64,29 @@ class FullScreen extends React.Component {
},
trainingSubIDCard: [],
trainingSubBSType: {},
companyData: [],
// 新增:当前选择的月份
selectedMonth: new Date().getMonth() + 1, // 默认当前月份1-12
selectedCompany: '',
trainingCompanyData: [], // 安全培训专用公司数据
selectedTrainingMonth: new Date().getMonth() + 1,
selectedTrainingCompany: '',
// 隐患治理相关数据
hiddenSubData: {}, // 隐患治理数据
hiddenCompanyData: [], // 隐患治理专用公司数据
selectedHiddenCompany: '', // 隐患治理选中的公司
// 其他页面数据
riskSubData: {},
classSubData: {},
dangerSubData: {},
dangerCompanyData: [],
selectedDangerCompany: '',
announcementDetail: null,
announcementDetailLoading: false,
announcementModalVisible: false,
currentAnnouncement: null,
};
this.isUnmounted = false;
this.dataTimer = null; // 新增:用于数据定时请求
}
setScale = debounce(() => {
@ -81,15 +101,56 @@ class FullScreen extends React.Component {
document.exitFullscreen?.();
}
};
startDataRefreshTimer = () => {
// 清除已有定时器
if (this.dataTimer) {
clearInterval(this.dataTimer);
}
// 设置新的定时器每5分钟执行一次
this.dataTimer = setInterval(
() => {
if (!this.isUnmounted) {
if (this.state.activeTab === '首页') {
this.getHomeDataArray();
this.getYearPXData();
}
// 安全培训 - 参考原有逻辑
else if (this.state.activeTab === '安全培训') {
this.getTrainingCompanyData(); // 先获取公司列表
this.getHomeSESubYearData();
this.getHomeSESubYearMonthData();
this.getHomeSESubIDCardData();
}
// 隐患治理 - 参考安全培训的调用顺序
else if (this.state.activeTab === '隐患治理') {
this.getHiddenCompanyData(); // 先获取公司列表(与安全培训独立)
}
// 风险管控
else if (this.state.activeTab === '风险管控') {
this.getRiskSubData();
}
// 班组建设
else if (this.state.activeTab === '班组建设') {
this.getClassSubData();
}
// 危险作业
else if (this.state.activeTab === '危险作业') {
this.getDangerCompanyData();
}
}
},
5 * 60 * 1000
); // 5分钟 = 300000毫秒
};
componentDidMount() {
this.isUnmounted = false;
window.addEventListener('resize', this.setScale);
this.getHomeDataArray();
this.getYearPXData();
this.loadMediaFiles();
this.getAnnouncementData();
// 启动定时刷新
this.startDataRefreshTimer();
this.timer = setInterval(() => {
if (!this.isUnmounted) this.setState({ nowDate: this.getDate() });
}, 1000);
@ -97,24 +158,56 @@ class FullScreen extends React.Component {
document.addEventListener('fullscreenchange', this.handleFullscreenChange);
}
getAnnouncementData = () => {
this.setState({
announcementList: [
{ id: 1, title: '关于2024年安全生产月活动的通知', publishTime: '2024-06-01', url: '' },
{ id: 2, title: '公司第三季度安全培训安排', publishTime: '2024-05-28', url: '' },
{ id: 3, title: '关于开展安全隐患排查整治工作的通知', publishTime: '2024-05-20', url: '' },
{ id: 4, title: '安全生产标准化建设阶段性总结', publishTime: '2024-05-15', url: '' },
{ id: 5, title: '关于表彰2024年第一季度安全生产先进单位的决定', publishTime: '2024-05-10', url: '' },
],
getAnnouncementDetail = (announcement) => {
return new Promise((resolve, reject) => {
const orgId = storage('lacal').getItem('webOrgId')?.val;
const json = initFilter(orgId);
extendRule(json, 'ID', 1, announcement.ID);
json.Include = ['Nav_Orgs', 'Nav_Orgs.Nav_OrgSub', 'Nav_Files.Nav_ImgFile'];
this.setState({ announcementDetailLoading: true, announcementDetail: null });
this.props.dispatch({
type: 'app/getDataByPost',
payload: json,
url: 'PF/Annourcement/Get',
onComplete: (ret) => {
if (ret && !this.isUnmounted) {
this.setState({
announcementDetail: ret,
announcementDetailLoading: false,
});
resolve(ret);
} else {
message.error('暂无公告详情');
this.setState({ announcementDetailLoading: false });
reject(new Error('获取公告详情失败'));
}
},
onError: (error) => {
this.setState({ announcementDetailLoading: false });
reject(error);
},
});
});
};
loadMediaFiles = () => {
handleAnnouncementClick = async (announcement) => {
try {
const detail = await this.getAnnouncementDetail(announcement);
this.setState({
currentAnnouncement: detail,
announcementModalVisible: true,
});
} catch (error) {
console.error('获取公告详情失败:', error);
}
};
handleAnnouncementModalClose = () => {
this.setState({
mediaList: [
{ type: 'image', url: 'http://10.2.7.18:28028//WZ_Images/static/smyzw@2x.png' },
{ type: 'video', url: 'http://10.2.7.18:28028/WZ_Images/Img_JFSC/welcom/1.mp4' },
],
announcementModalVisible: false,
currentAnnouncement: null,
});
};
@ -129,17 +222,31 @@ class FullScreen extends React.Component {
handleTabClick = (name) => {
this.setState({ activeTab: name });
if (name === '首页') {
this.getHomeDataArray();
this.getYearPXData();
this.loadMediaFiles();
this.getAnnouncementData();
}
// 当点击安全培训tab时获取对应数据
// 安全培训 - 参考原有逻辑
if (name === '安全培训') {
this.getTrainingCompanyData(); // 先获取公司列表
this.getHomeSESubYearData();
this.getHomeSESubYearMonthData();
this.getHomeSESubIDCardData();
// this.getBSTypeMonthData();
this.getCompanyData();
}
// 隐患治理 - 参考安全培训的调用顺序
if (name === '隐患治理') {
this.getHiddenCompanyData(); // 先获取公司列表(与安全培训独立)
}
// 风险管控
if (name === '风险管控') {
this.getRiskSubData();
}
// 班组建设
if (name === '班组建设') {
this.getClassSubData();
}
// 危险作业
if (name === '危险作业') {
this.getDangerCompanyData();
}
};
@ -151,6 +258,9 @@ class FullScreen extends React.Component {
this.isUnmounted = true;
window.removeEventListener('resize', this.setScale);
if (this.timer) clearInterval(this.timer);
// 清除数据刷新定时器
if (this.dataTimer) clearInterval(this.dataTimer);
document.removeEventListener('fullscreenchange', this.handleFullscreenChange);
}
@ -179,14 +289,15 @@ class FullScreen extends React.Component {
if (ret && !this.isUnmounted) {
this.setState({
riskTypeRate: ret.riskTypeRate || [],
linkSum: ret.linkSum || [],
jobTodayTop3: ret.jobTodayTop3 || [],
jobTodayQty: ret.jobTodayQty || [],
hiddenSummary: ret.hiddenSummary || [],
taskTop3: ret.taskTop3 || [],
});
}
},
});
};
getYearPXData = () => {
const orgId = storage('lacal').getItem('webOrgId')?.val;
const json = initFilter(orgId);
@ -202,29 +313,63 @@ class FullScreen extends React.Component {
YearCount: ret.YearCount || [],
MonthRecordCount: ret.MonthRecordCount || [],
MonthPersonCount: ret.MonthPersonCount || [],
listAnnourcement: ret.listAnnourcement || [],
TITLE: ret.TITLE || '',
listVideoImg: ret.listVideoImg || [],
playSet: ret.playSet || [],
},
});
}
},
});
};
// 获取安全培训页面数据
// ==================== 安全培训相关方法 ====================
getTrainingCompanyData = () => {
const orgId = storage('lacal').getItem('webOrgId')?.val;
const json = initFilter(orgId);
this.props.dispatch({
type: 'app/getDataByPost',
payload: json,
url: 'FM/Organization/OrderPaged',
onComplete: (ret) => {
if (ret && !this.isUnmounted) {
// 默认选中邦泰公司
const defaultCompany = ret.find((company) => company.ID === '00500000-0000-0000-0000-000000000000');
this.setState({
trainingCompanyData: ret,
});
if (defaultCompany && !this.state.selectedTrainingCompany) {
this.setState(
{
selectedTrainingCompany: defaultCompany.ID,
},
() => {
this.getBSTypeMonthData(); // 公司设置完成后获取培训类型数据
}
);
} else {
this.getBSTypeMonthData();
}
}
},
});
};
getHomeSESubYearData = () => {
const orgId = storage('lacal').getItem('webOrgId')?.val;
const json = initFilter(orgId);
const currentYear = new Date().getFullYear(); // 获取当前年份
json.Parameter1 = currentYear.toString(); // 设置为当前年份
const currentYear = new Date().getFullYear();
json.Parameter1 = currentYear.toString();
this.props.dispatch({
type: 'app/getDataByPost',
payload: json,
url: 'BI/BIHeadSE/HomeSESubYear',
onComplete: (ret) => {
if (ret && !this.isUnmounted) {
// ret 是数组格式: [{ CName: "邦泰", PCount: 0, RCount: 13 }, ...]
const listNAME = ret.map((item) => item.CName);
const MonthPersonCount = ret.map((item) => item.PCount); // 培训人次
const MonthRecordCount = ret.map((item) => item.RCount); // 培训场次
// YearCount 暂时用 PCount 的总和或保持为空数组
const MonthPersonCount = ret.map((item) => item.PCount);
const MonthRecordCount = ret.map((item) => item.RCount);
const YearCount = ret.map((item) => item.PCount);
this.setState({
@ -239,38 +384,34 @@ class FullScreen extends React.Component {
},
});
};
// 新增:处理月份变化
handleMonthChange = (month) => {
this.setState({ selectedMonth: month }, () => {
// 重新获取月份数据
handleTrainingMonthChange = (month) => {
this.setState({ selectedTrainingMonth: month }, () => {
this.getHomeSESubYearMonthData();
});
};
handleCompanyChange = (company) => {
console.log(company, '11111');
this.setState({ selectedCompany: company }, () => {
// 重新获取月份数据
handleTrainingCompanyChange = (company) => {
this.setState({ selectedTrainingCompany: company }, () => {
this.getBSTypeMonthData();
});
};
// 获取安全培训页面数据
getHomeSESubYearMonthData = () => {
const orgId = storage('lacal').getItem('webOrgId')?.val;
const json = initFilter(orgId);
const currentYear = new Date().getFullYear(); // 获取当前年份
json.Parameter1 = currentYear.toString(); // 设置为当前年份
json.Parameter2 = this.state.selectedMonth.toString(); // 月份
const currentYear = new Date().getFullYear();
json.Parameter1 = currentYear.toString();
json.Parameter2 = this.state.selectedTrainingMonth.toString();
this.props.dispatch({
type: 'app/getDataByPost',
payload: json,
url: 'BI/BIHeadSE/HomeSESubYear',
onComplete: (ret) => {
if (ret && !this.isUnmounted) {
// ret 是数组格式: [{ CName: "邦泰", PCount: 0, RCount: 13 }, ...]
const listNAME = ret.map((item) => item.CName);
const MonthPersonCount = ret.map((item) => item.PCount); // 培训人次
const MonthRecordCount = ret.map((item) => item.RCount); // 培训场次
// YearCount 暂时用 PCount 的总和或保持为空数组
const MonthPersonCount = ret.map((item) => item.PCount);
const MonthRecordCount = ret.map((item) => item.RCount);
const YearCount = ret.map((item) => item.PCount);
this.setState({
@ -285,6 +426,7 @@ class FullScreen extends React.Component {
},
});
};
getHomeSESubIDCardData = () => {
const orgId = storage('lacal').getItem('webOrgId')?.val;
const json = initFilter(orgId);
@ -292,9 +434,9 @@ class FullScreen extends React.Component {
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
const day = String(currentDate.getDate()).padStart(2, '0');
const currentDateStr = `${year}-${month}-${day}`; // 例如2024-06-15
const currentDateStr = `${year}-${month}-${day}`;
json.Parameter1 = currentDateStr; // 设置为当前日期
json.Parameter1 = currentDateStr;
this.props.dispatch({
type: 'app/getDataByPost',
payload: json,
@ -308,6 +450,7 @@ class FullScreen extends React.Component {
},
});
};
getBSTypeMonthData = () => {
const orgId = storage('lacal').getItem('webOrgId')?.val;
const json = initFilter(orgId);
@ -315,11 +458,11 @@ class FullScreen extends React.Component {
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
const day = String(currentDate.getDate()).padStart(2, '0');
const currentDateStr = `${year}-${month}-${day}`; // 例如2024-06-15
const currentDateStr = `${year}-${month}-${day}`;
json.Parameter1 = currentDateStr; // 设置为当前日期
if (this.state.selectedCompany) {
json.Parameter2 = this.state.selectedCompany;
json.Parameter1 = currentDateStr;
if (this.state.selectedTrainingCompany) {
json.Parameter2 = this.state.selectedTrainingCompany;
}
this.props.dispatch({
type: 'app/getDataByPost',
@ -334,7 +477,10 @@ class FullScreen extends React.Component {
},
});
};
getCompanyData = () => {
// ==================== 隐患治理相关方法 ====================
// 获取隐患治理的公司列表(独立于安全培训)
getHiddenCompanyData = () => {
const orgId = storage('lacal').getItem('webOrgId')?.val;
const json = initFilter(orgId);
this.props.dispatch({
@ -343,18 +489,155 @@ class FullScreen extends React.Component {
url: 'FM/Organization/OrderPaged',
onComplete: (ret) => {
if (ret && !this.isUnmounted) {
//加载页面默认为绑泰
// 默认选中邦泰公司
const defaultCompany = ret.find((company) => company.ID === '00500000-0000-0000-0000-000000000000');
this.setState({
companyData: ret,
hiddenCompanyData: ret,
});
// 如果找到了默认公司,设置 selectedCompany
if (defaultCompany && !this.state.selectedCompany) {
if (defaultCompany && !this.state.selectedHiddenCompany) {
this.setState({
selectedCompany: defaultCompany.ID,
selectedHiddenCompany: defaultCompany.ID,
});
}
this.getBSTypeMonthData();
this.getHiddenSubData();
}
},
});
};
// 隐患治理公司筛选变化处理
handleHiddenCompanyChange = (company) => {
this.setState({ selectedHiddenCompany: company }, () => {
this.getHiddenSubData();
});
};
getHiddenSubData = () => {
// 以下是真实的API调用代码如需使用请取消注释
const orgId = storage('lacal').getItem('webOrgId')?.val;
const json = initFilter(orgId);
this.props.dispatch({
type: 'app/getDataByPost',
payload: json,
url: 'BI/BIKanBanController/HiddenManage',
onComplete: (ret) => {
if (ret && !this.isUnmounted) {
let filteredData = ret;
if (this.state.selectedHiddenCompany) {
const selectedCompanyObj = this.state.hiddenCompanyData?.find(
(company) => company.ID === this.state.selectedHiddenCompany
);
const selectedCompanyName = selectedCompanyObj?.NAME;
if (selectedCompanyName) {
// 筛选 hiddenRanking
filteredData.hiddenRanking = filteredData.hiddenRanking.filter(
(item) => item.companyName === selectedCompanyName
);
}
}
this.setState({
hiddenSubData: filteredData,
});
}
},
});
};
// ==================== 其他页面方法 ====================
getRiskSubData = () => {
const orgId = storage('lacal').getItem('webOrgId')?.val;
const json = initFilter(orgId);
this.props.dispatch({
type: 'app/getDataByPost',
payload: json,
url: 'BI/BIKanBanController/RiskManage',
onComplete: (ret) => {
if (ret && !this.isUnmounted) {
this.setState({
riskSubData: ret,
});
}
},
});
};
getClassSubData = () => {
const orgId = storage('lacal').getItem('webOrgId')?.val;
const json = initFilter(orgId);
this.props.dispatch({
type: 'app/getDataByPost',
payload: json,
url: 'BI/BIKanBanController/TeamManage',
onComplete: (ret) => {
if (ret && !this.isUnmounted) {
this.setState({
classSubData: ret,
});
}
},
});
};
getDangerCompanyData = () => {
const orgId = storage('lacal').getItem('webOrgId')?.val;
const json = initFilter(orgId);
this.props.dispatch({
type: 'app/getDataByPost',
payload: json,
url: 'FM/Organization/OrderPaged',
onComplete: (ret) => {
if (ret && !this.isUnmounted) {
const defaultCompany = ret.find((company) => company.ID === '00500000-0000-0000-0000-000000000000');
this.setState({
dangerCompanyData: ret,
});
if (defaultCompany && !this.state.selectedDangerCompany) {
this.setState(
{
selectedDangerCompany: defaultCompany.ID,
},
() => {
this.getDangerSubData();
}
);
} else {
this.getDangerSubData();
}
}
},
});
};
handleDangerCompanyChange = (company) => {
this.setState({ selectedDangerCompany: company }, () => {
this.getDangerSubData();
});
};
getDangerSubData = () => {
const orgId = storage('lacal').getItem('webOrgId')?.val;
const json = initFilter(orgId);
this.props.dispatch({
type: 'app/getDataByPost',
payload: json,
url: 'BI/BIKanBanController/JobManage',
onComplete: (ret) => {
if (ret && !this.isUnmounted) {
let filteredData = ret;
if (this.state.selectedDangerCompany) {
const selectedCompanyObj = this.state.dangerCompanyData?.find(
(company) => company.ID === this.state.selectedDangerCompany
);
const selectedCompanyName = selectedCompanyObj?.NAME;
if (selectedCompanyName) {
filteredData = filteredData.filter((item) => item.companyName === selectedCompanyName);
}
}
this.setState({
dangerSubData: filteredData,
});
}
},
});
@ -363,32 +646,61 @@ class FullScreen extends React.Component {
renderOtherTabContent = () => {
const {
activeTab,
trainingData,
trainingSubData,
trainingSubDataMonth,
selectedMonth,
selectedCompany,
trainingSubBSType,
companyData,
trainingCompanyData,
trainingSubDataMonth,
selectedTrainingMonth,
selectedTrainingCompany,
trainingSubIDCard,
riskSubData,
classSubData,
hiddenSubData,
hiddenCompanyData,
selectedHiddenCompany,
} = this.state;
// 如果是安全培训,显示专门的培训页面
if (activeTab === '安全培训') {
return (
<TrainingContent
trainingData={trainingData}
trainingSubData={trainingSubData}
trainingSubBSType={trainingSubBSType}
companyData={companyData}
companyData={trainingCompanyData}
trainingSubDataMonth={trainingSubDataMonth}
selectedMonth={selectedMonth}
selectedCompany={selectedCompany}
onMonthChange={this.handleMonthChange}
onCompanyChange={this.handleCompanyChange}
selectedMonth={selectedTrainingMonth}
selectedCompany={selectedTrainingCompany}
onMonthChange={this.handleTrainingMonthChange}
onCompanyChange={this.handleTrainingCompanyChange}
trainingSubIDCard={trainingSubIDCard}
/>
);
}
if (activeTab === '风险管控') {
return <RiskControl riskSubData={riskSubData} />;
}
if (activeTab === '隐患治理') {
return (
<HiddenSolve
hiddenSubData={hiddenSubData}
companyData={hiddenCompanyData}
selectedCompany={selectedHiddenCompany}
onCompanyChange={this.handleHiddenCompanyChange}
/>
);
}
if (activeTab === '班组建设') {
return <ClassBuild classSubData={classSubData} />;
}
if (activeTab === '危险作业') {
return (
<DangerJob
dangerSubData={this.state.dangerSubData}
companyData={this.state.dangerCompanyData}
selectedCompany={this.state.selectedDangerCompany}
onCompanyChange={this.handleDangerCompanyChange}
/>
);
}
return (
<div className={styles.otherTabContent}>
<div style={{ textAlign: 'center', color: '#fff' }}>
@ -408,13 +720,16 @@ class FullScreen extends React.Component {
activeTab,
riskTypeRate,
riskTotal,
jobTodayTop3,
linkSum,
hiddenSummary,
jobTodayQty,
taskTop3,
mediaList,
announcementList,
currentMediaIndex,
trainingData,
announcementModalVisible,
currentAnnouncement,
announcementDetailLoading,
} = this.state;
return (
@ -470,15 +785,20 @@ class FullScreen extends React.Component {
<HomeContent
riskTypeRate={riskTypeRate}
riskTotal={riskTotal}
jobTodayTop3={jobTodayTop3}
linkSum={linkSum}
hiddenSummary={hiddenSummary}
jobTodayQty={jobTodayQty}
taskTop3={taskTop3}
mediaList={mediaList}
announcementList={announcementList}
currentMediaIndex={currentMediaIndex}
onCarouselChange={this.handleCarouselChange}
onDotClick={this.handleDotClick}
trainingData={trainingData} // 新增传递
trainingData={trainingData}
onAnnouncementClick={this.handleAnnouncementClick}
announcementModalVisible={announcementModalVisible}
currentAnnouncement={currentAnnouncement}
announcementDetailLoading={announcementDetailLoading}
onAnnouncementModalClose={this.handleAnnouncementModalClose}
/>
) : (
this.renderOtherTabContent()

View File

@ -281,7 +281,7 @@
justify-content: center;
font-weight: bold;
color: #fff;
background-color: #1e2a3a;
background-color: #909399;
margin: 10px 20px;
padding: 5px 10px;
border-radius: 4px;
@ -313,7 +313,7 @@
justify-content: center;
font-weight: bold;
color: #fff;
background-color: #1e2a3a;
background-color: #909399;
margin: 10px 20px;
padding: 5px 10px;
border-radius: 4px;
@ -492,38 +492,59 @@
margin-bottom: 16px;
}
// 轮播图控件
.customArrowLeft,
.customArrowRight {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 40px;
height: 60px;
width: 44px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 10;
transition: all 0.3s ease;
backdrop-filter: blur(4px);
border-radius: 8px;
// 深色半透明背景,确保在任何背景下都可见
background-color: rgba(0, 0, 0, 0.5);
// 添加白色外发光
box-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
// 图标颜色设为白色
i,
.anticon {
color: #ffffff !important;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
&:hover {
background-color: rgba(0, 0, 0, 0.3);
transform: translateY(-50%) scale(1.1);
background-color: rgba(0, 0, 0, 0.7);
transform: translateY(-50%) scale(1.05);
box-shadow: 0 0 12px rgba(255, 255, 255, 0.5);
i,
.anticon {
color: #ffffff !important;
transform: scale(1.1);
}
}
&:active {
transform: translateY(-50%) scale(0.95);
transform: translateY(-50%) scale(0.98);
}
}
.customArrowLeft {
left: 20px;
left: 16px;
border-radius: 0 8px 8px 0;
}
.customArrowRight {
right: 20px;
right: 16px;
border-radius: 8px 0 0 8px;
}
.customDots {
@ -691,19 +712,89 @@
white-space: nowrap;
}
// 奇数行背景色
.ant-table-tbody > tr:nth-child(odd) > td {
background-color: #d4d5e3;
}
// 偶数行背景色
.ant-table-tbody > tr:nth-child(even) > td {
background-color: #d0cedc;
}
// hover 效果
.ant-table-tbody > tr:hover > td {
background-color: #e6f7ff;
}
}
}
// fullinter.less - 在文件末尾添加
// 公告内容富文本样式
.announcementContent {
font-size: 14px;
line-height: 1.6;
color: #333;
// 富文本内容样式
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 10px 0;
font-weight: bold;
}
p {
margin: 8px 0;
}
img {
max-width: 100%;
height: auto;
}
ul,
ol {
padding-left: 20px;
margin: 8px 0;
}
li {
margin: 4px 0;
}
table {
border-collapse: collapse;
width: 100%;
margin: 8px 0;
}
th,
td {
border: 1px solid #ddd;
padding: 6px;
text-align: left;
}
th {
background-color: #f5f5f5;
}
strong,
b {
font-weight: bold;
}
em,
i {
font-style: italic;
}
u {
text-decoration: underline;
}
sub {
vertical-align: sub;
font-size: smaller;
}
sup {
vertical-align: super;
font-size: smaller;
}
}

View File

@ -10651,7 +10651,7 @@
"recompose" "^0.27.1"
"shallowequal" "^1.0.2"
"react-dom@*", "react-dom@^0.14.0 || ^15.0.0 || ^16.0.0", "react-dom@^0.14.0 || ^15.0.0 || ^16.0.0-beta || ^16.0.0", "react-dom@^0.14.0 || ^15.0.0-0 || ^16.0.0", "react-dom@^0.14.0 || ^15.0.0-rc || ^16.0.0-rc || ^16.0.0", "react-dom@^0.14.0 || ^15.0.1 || ^16.0.0", "react-dom@^15.0.0 || ^16.0.0", "react-dom@^15.0.2 || ^16.0.0-rc || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^15.0.2|| ^16.0.0-rc || ^16.0.0", "react-dom@^16.0.0", "react-dom@^16.0.0-0", "react-dom@^16.3.0", "react-dom@^16.4.1", "react-dom@^16.6.3", "react-dom@^16.8.0", "react-dom@>=15.0.0", "react-dom@>=15.x", "react-dom@>=16.0.0", "react-dom@>=16.9.0", "react-dom@>0.14.5", "react-dom@0.14.x || 15.x || 16.x", "react-dom@15.x || ^16.0.0-0", "react-dom@16.8.2":
"react-dom@*", "react-dom@^0.14.0 || ^15.0.0 || ^16.0.0", "react-dom@^0.14.0 || ^15.0.0 || ^16.0.0-beta || ^16.0.0", "react-dom@^0.14.0 || ^15.0.0-0 || ^16.0.0", "react-dom@^0.14.0 || ^15.0.0-rc || ^16.0.0-rc || ^16.0.0", "react-dom@^0.14.0 || ^15.0.1 || ^16.0.0", "react-dom@^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^15.0.0 || ^16.0.0", "react-dom@^15.0.2 || ^16.0.0-rc || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^15.0.2|| ^16.0.0-rc || ^16.0.0", "react-dom@^16.0.0", "react-dom@^16.0.0-0", "react-dom@^16.3.0", "react-dom@^16.4.1", "react-dom@^16.6.3", "react-dom@^16.8.0", "react-dom@>=15.0.0", "react-dom@>=15.x", "react-dom@>=16.0.0", "react-dom@>=16.9.0", "react-dom@>0.14.5", "react-dom@0.14.x || 15.x || 16.x", "react-dom@15.x || ^16.0.0-0", "react-dom@16.8.2":
"integrity" "sha512-cPGfgFfwi+VCZjk73buu14pYkYBR1b/SRMSYqkLDdhSEHnSwcuYTPu6/Bh6ZphJFIk80XLvbSe2azfcRzNF+Xg=="
"resolved" "https://registry.npmmirror.com/react-dom/-/react-dom-16.8.2.tgz"
"version" "16.8.2"
@ -10789,9 +10789,19 @@
"prop-types" "^15.6.1"
"warning" "^4.0.1"
"react-slick@^0.31.0":
"integrity" "sha512-zo6VLT8wuSBJffg/TFPbzrw2dEnfZ/cUKmYsKByh3AgatRv29m2LoFbq5vRMa3R3A4wp4d8gwbJKO2fWZFaI3g=="
"resolved" "https://registry.npmmirror.com/react-slick/-/react-slick-0.31.0.tgz"
"version" "0.31.0"
dependencies:
"classnames" "^2.2.5"
"json2mq" "^0.2.0"
"lodash.debounce" "^4.0.8"
"resize-observer-polyfill" "^1.5.0"
"react-slick@~0.25.2":
"integrity" "sha512-8MNH/NFX/R7zF6W/w+FS5VXNyDusF+XDW1OU0SzODEU7wqYB+ZTGAiNJ++zVNAVqCAHdyCybScaUB+FCZOmBBw=="
"resolved" "https://registry.npmjs.org/react-slick/-/react-slick-0.25.2.tgz"
"resolved" "https://registry.npmmirror.com/react-slick/-/react-slick-0.25.2.tgz"
"version" "0.25.2"
dependencies:
"classnames" "^2.2.5"
@ -10834,7 +10844,7 @@
"resolved" "https://registry.npmmirror.com/react-websocket/-/react-websocket-2.0.1.tgz"
"version" "2.0.1"
"react@*", "react@^0.14 || ^15.0.0 || ^16.0.0-alpha", "react@^0.14.0 || ^15.0.0 || ^16.0.0", "react@^0.14.0 || ^15.0.0 || ^16.0.0-0", "react@^0.14.0 || ^15.0.0 || ^16.0.0-beta || ^16.0.0", "react@^0.14.0 || ^15.0.0-0 || ^16.0.0", "react@^0.14.0 || ^15.0.0-0 || ^16.0.0-0", "react@^0.14.0 || ^15.0.0-rc || ^16.0.0-rc || ^16.0.0", "react@^0.14.0 || ^15.0.1 || ^16.0.0", "react@^15.0.0 || ^16.0.0", "react@^15.0.2 || ^16.0.0-rc || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^15.0.2|| ^16.0.0-rc || ^16.0.0", "react@^16.0.0", "react@^16.0.0-0", "react@^16.3.0", "react@^16.4.1", "react@^16.6.3", "react@^16.8.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@>= 16.3", "react@>=0.13.2 || ^0.14 || ^15.0.0 || >=16.0.0", "react@>=15", "react@>=15.0.0", "react@>=15.x", "react@>=16.0.0", "react@>=16.9.0", "react@>0.14.5", "react@0.13.x || 0.14.x || ^15.0.0-0 || ^16.0.0-0", "react@0.14.x || 15.x || 16.x", "react@15.x || ^16.0.0-0", "react@16.8.2", "react@16.x":
"react@*", "react@^0.14 || ^15.0.0 || ^16.0.0-alpha", "react@^0.14.0 || ^15.0.0 || ^16.0.0", "react@^0.14.0 || ^15.0.0 || ^16.0.0-0", "react@^0.14.0 || ^15.0.0 || ^16.0.0-beta || ^16.0.0", "react@^0.14.0 || ^15.0.0-0 || ^16.0.0", "react@^0.14.0 || ^15.0.0-0 || ^16.0.0-0", "react@^0.14.0 || ^15.0.0-rc || ^16.0.0-rc || ^16.0.0", "react@^0.14.0 || ^15.0.1 || ^16.0.0", "react@^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^15.0.0 || ^16.0.0", "react@^15.0.2 || ^16.0.0-rc || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^15.0.2|| ^16.0.0-rc || ^16.0.0", "react@^16.0.0", "react@^16.0.0-0", "react@^16.3.0", "react@^16.4.1", "react@^16.6.3", "react@^16.8.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@>= 16.3", "react@>=0.13.2 || ^0.14 || ^15.0.0 || >=16.0.0", "react@>=15", "react@>=15.0.0", "react@>=15.x", "react@>=16.0.0", "react@>=16.9.0", "react@>0.14.5", "react@0.13.x || 0.14.x || ^15.0.0-0 || ^16.0.0-0", "react@0.14.x || 15.x || 16.x", "react@15.x || ^16.0.0-0", "react@16.8.2", "react@16.x":
"integrity" "sha512-aB2ctx9uQ9vo09HVknqv3DGRpI7OIGJhCx3Bt0QqoRluEjHSaObJl+nG12GDdYH6sTgE7YiPJ6ZUyMx9kICdXw=="
"resolved" "https://registry.npmmirror.com/react/-/react-16.8.2.tgz"
"version" "16.8.2"