Compare commits

...

3 Commits

21 changed files with 1423 additions and 100 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -337,6 +337,13 @@ input:-webkit-autofill:active {
color: #fff; color: #fff;
} }
} }
.ant-picker-cell-disabled {
pointer-events: none;
.ant-picker-cell-inner {
color: rgba(255, 255, 255, 0.25) !important;
background-color: rgba(255, 255, 255, 0.1) !important;
}
}
// Footer/Ranges // Footer/Ranges
.ant-picker-footer { .ant-picker-footer {

View File

@ -157,33 +157,17 @@ const apiurl = {
managePic: service + '/screen/manageHouseImg/get', managePic: service + '/screen/manageHouseImg/get',
wzPage:service + '/rescue/goods/page/query' wzPage:service + '/rescue/goods/page/query'
}, },
zrrgl: { qhfz: {
page: service + "/resPerson/page", lawPage: service + '/SzRuleByLaw/page',
edit: service + "/resPerson/update", systemPage: service + '/SzRegulatoryFramework/page',
delete: service + "/resPerson/del", caseSta:service + '/szCase/statistics/1'
save: service + "/resPerson/insert",
}, },
}, zrz: {
sy: { respPerson: service + '/screen/responsibility/getPerson',
fxya:{ floodPerson: service + '/screen/responsibility/getFxPerson',
page: service + "/resPlanB/list", train:service + '/screen/responsibility/getTraining'
update: service + "/resPlanB/update", }
save: service + "/resPlanB/insert",
delete: service + "/resPlanB/del",
getFile: service + "/resPlanB/file/get"
},
},
gcaqjc: {
gcaqyj: {
yjgzpz:{
page: service + '/osmoticWarnRule/page',
save: service + '/osmoticWarnRule/insert',
edit: service + "/osmoticWarnRule/update",
delete: service + "/osmoticWarnRule/del",
list: service + "/osmoticPressDevice/list"
} }
},
},
} }
export default apiurl export default apiurl

View File

@ -3,7 +3,7 @@ import centerOfMass from '@turf/center-of-mass';
import bbox from '@turf/bbox'; import bbox from '@turf/bbox';
import turfLength from '@turf/length'; import turfLength from '@turf/length';
import along from '@turf/along'; import along from '@turf/along';
import { config } from '@/config';
const class2type = {}; const class2type = {};
const { toString } = class2type; const { toString } = class2type;
'Boolean Number String Function Array Date RegExp Object Error'.split(' ').forEach((name) => { 'Boolean Number String Function Array Date RegExp Object Error'.split(' ').forEach((name) => {
@ -614,3 +614,16 @@ export const myFiltrate = (data,params)=>{
return query.substring(iStart, iEnd); return query.substring(iStart, iEnd);
} }
export const download = (url) => {
let downloadLink = document.createElement("a");
// downloadLink.href = `${process.env.REACT_APP_API_URL}/gunshiApp/ss/personnelPlan/file/download/${params}`;
downloadLink.href = config.ip +url;
// downloadLink.download = `${params.fileName}`;
downloadLink.style.display = "none";
// 将链接添加到页面中
document.body.appendChild(downloadLink);
// 模拟点击事件,开始下载
downloadLink.click();
}

View File

@ -6,6 +6,7 @@ import { treeList, srcData, videoList, ysyToken } from './http'
import VideoControler from "@/components/VideoCom/VideoControler" import VideoControler from "@/components/VideoCom/VideoControler"
import { httppost } from "@/utils/request" import { httppost } from "@/utils/request"
import apiurl from "@/service/apiurl" import apiurl from "@/service/apiurl"
import { Spin } from "antd"
const VideoList = () => { const VideoList = () => {
@ -15,6 +16,7 @@ const VideoList = () => {
const [size, setSize] = useState(1) const [size, setSize] = useState(1)
const [treeListData, setTreeData] = useState([]) const [treeListData, setTreeData] = useState([])
const [selectList, setSelectList] = useState() const [selectList, setSelectList] = useState()
const [loading, setLoading] = useState(true)
const selectedKeys = async (list, node) => { const selectedKeys = async (list, node) => {
console.log(node, 'node'); console.log(node, 'node');
@ -74,6 +76,7 @@ const VideoList = () => {
} }
const getTreeData = async () => { const getTreeData = async () => {
try {
const res = await treeList() const res = await treeList()
const res1 = await videoList() const res1 = await videoList()
const arr = res1.data.filter(item => item.menuId).map((item, index) => { const arr = res1.data.filter(item => item.menuId).map((item, index) => {
@ -86,6 +89,11 @@ const VideoList = () => {
const arr1 = [...arr, ...res.data] const arr1 = [...arr, ...res.data]
console.log("before", arr1); console.log("before", arr1);
setTreeData(buildTree(arr1)) setTreeData(buildTree(arr1))
} catch (error) {
console.error(error)
} finally {
setLoading(false)
}
} }
function buildTree(data, parentId = "0") { function buildTree(data, parentId = "0") {
const tree = []; const tree = [];
@ -137,6 +145,12 @@ const VideoList = () => {
}, []) }, [])
return ( return (
<div className='flex videoList'> <div className='flex videoList'>
{loading ? (
<div style={{ width: '100%', height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Spin tip="加载中..." />
</div>
) : (
<>
<div className={['treeRight', (selectList && selectList.type == 1) ? 'ptz-visible' : ''].join(' ')}> <div className={['treeRight', (selectList && selectList.type == 1) ? 'ptz-visible' : ''].join(' ')}>
<TreeData size={size} selectedKeys={selectedKeys} treeListData={treeListData} videoArr={videoArr} /> <TreeData size={size} selectedKeys={selectedKeys} treeListData={treeListData} videoArr={videoArr} />
{ {
@ -151,6 +165,8 @@ const VideoList = () => {
} }
</div> </div>
<div className='treeLeft'><SplitScreen count={count} videoArr={videoArr} clickIndex={clickIndex} getType={getType} /></div> <div className='treeLeft'><SplitScreen count={count} videoArr={videoArr} clickIndex={clickIndex} getType={getType} /></div>
</>
)}
</div> </div>
) )
} }

View File

@ -0,0 +1,164 @@
import React, { useState } from 'react';
import selectedBg from '@/assets/images/modal/selected.png';
import skbg from '@/assets/images/card/skbg.png';
import gth from '@/assets/images/card/gth.png';
import syl from '@/assets/images/card/syl.png';
import wy from '@/assets/images/card/wy.png';
import './index.less';
const WarningSection = ({ type = 'monitor' }) => {
const [activeTab, setActiveTab] = useState('water'); // 'water' or 'safety' for monitor; 'flood' or 'weather' for forecast
// Mock Data
const waterWarnings = [
{
id: 1,
name: '双石水库',
limitLevel: '108.89',
currentLevel: '109.09',
exceedance: '0.20',
time: '2025-10-28 09:00:00'
}
];
const safetyMonitorItems = [
{ label: '渗压监测', value: 2, icon: syl },
{ label: '渗流监测', value: 1, icon: syl },
{ label: '位移监测', value: 0, icon: wy }
];
const floodWarnings = [
{
id: 1,
message: '根据上游流量站实测和产汇流预测结果未来24小时预计总入库564.2万m³平均入库流量65.2m³/s达到橙色预警级别。',
time: '2025-10-28 08:10:00'
}
];
const weatherWarnings = [
{
id: 1,
title: '赤壁市气象台发布暴雨红色预警[Ⅰ级/特别...',
time: '2025-07-28 06:35:30',
content: '赤壁市2025年08月10日07时33分08秒发布暴雨红色预警信号过去3小时最大降水出现在XXXXX为71毫米。受强降雨云团持续影响预计未来3小时上述地区及周边乡镇仍将有50-80毫米的降雨累计雨量可达150毫米以上局地阵风7-9级。城乡积涝、山区山洪、地质灾害、中小河流洪水风险极高请特别加强防范。'
}
];
const renderContent = () => {
if (type === 'monitor') {
if (activeTab === 'water') {
return (
<div className="warning-list">
{waterWarnings.map((item) => (
<div key={item.id} className="warning-item">
<div className="item-header">
<img src={gth} alt="warning" className="warning-icon" />
<div className="item-title" style={{ backgroundImage: `url(${skbg})` }}>{item.name}</div>
</div>
<div className="item-content">
<p>水库汛限水位 {item.limitLevel} m</p>
<p>实时监测水位 {item.currentLevel} m超出汛限水位 <span className="highlight"> {item.exceedance} m</span></p>
<div className="item-time">{item.time}</div>
</div>
<div className="divider" />
</div>
))}
</div>
);
} else {
return (
<div className="safety-monitor-view">
{safetyMonitorItems.map((item, index) => (
<div key={index} className="monitor-item">
<div className="monitor-value" style={{ color: item.value > 0 ? '#ff4d4f' : '#fff' }}>{item.value}</div>
<img src={item.icon} alt={item.label} className="monitor-icon" />
<div className="monitor-label">{item.label}</div>
</div>
))}
</div>
);
}
} else {
// Forecast Warning
if (activeTab === 'flood') {
return (
<div className="warning-list">
{floodWarnings.map(item => (
<div key={item.id} className="warning-item">
<div className="item-content full-width">
<div className="warning-text-wrapper">
<img src={gth} alt="warning" className="warning-icon-inline" />
<span className="warning-text">{item.message}</span>
</div>
<div className="item-time" style={{marginLeft:25,marginTop:0}}>预报时间{item.time}</div>
</div>
<div className="divider" />
</div>
))}
</div>
)
} else {
return (
<div className="warning-list">
{weatherWarnings.map(item => (
<div key={item.id} className="warning-item">
<div className="item-header weather-header">
<img src={gth} alt="rainstorm" className="weather-icon" />
<div className="item-title" title={item.title} style={{ backgroundImage: `url(${skbg})`,paddingLeft:38 }}>{item.title}</div>
</div>
<div className="item-time weather-time">{item.time}</div>
<div className="item-content weather-content">
{item.content}
</div>
</div>
))}
</div>
)
}
}
};
const getTabs = () => {
if (type === 'monitor') {
return [
{ key: 'water', label: `水库水情 (${waterWarnings.length})` },
{ key: 'safety', label: `安全监测 (${safetyMonitorItems.reduce((acc, cur) => acc + cur.value, 0)})` } // Sum of values or just count of items? Design shows (3) which matches items count 3. Let's use 3.
];
} else {
return [
{ key: 'flood', label: `洪水预报 (${floodWarnings.length})` },
{ key: 'weather', label: `气象预报 (${weatherWarnings.length})` }
];
}
};
// Reset active tab when type changes
React.useEffect(() => {
setActiveTab(type === 'monitor' ? 'water' : 'flood');
}, [type]);
const tabs = getTabs();
return (
<div className="warning-section">
{/* Tabs */}
<div className="tabs-container">
{tabs.map(tab => (
<div
key={tab.key}
className={`tab-item ${activeTab === tab.key ? 'active' : ''}`}
onClick={() => setActiveTab(tab.key)}
style={activeTab === tab.key ? { backgroundImage: `url(${selectedBg})` } : {}}
>
{tab.label}
</div>
))}
</div>
{/* Content */}
{renderContent()}
</div>
);
};
export default WarningSection;

View File

@ -0,0 +1,184 @@
.warning-section {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow: hidden;
.tabs-container {
display: flex;
justify-content: space-around;
margin-bottom: 10px;
padding: 0 10px;
.tab-item {
flex: 1;
text-align: center;
padding: 5px 0;
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
background-size: 100% 100%;
background-repeat: no-repeat;
transition: all 0.3s;
margin: 0 5px;
&.active {
color: #fff;
text-shadow: 0 0 10px #00a0e9;
border: none;
}
&:hover {
color: #fff;
}
}
}
.warning-list {
flex: 1;
overflow-y: auto;
padding: 0 10px;
max-height: 180px;
.warning-item {
margin-bottom: 15px;
.item-header {
display: flex;
align-items: center;
margin-bottom: 8px;
.warning-icon {
width: 24px;
height: 24px;
margin-right: 10px;
flex-shrink: 0;
}
.weather-icon {
width: 40px;
height: 30px;
margin-right: 10px;
flex-shrink: 0;
}
.item-title {
width: 180px;
font-size: 16px;
color: #fff;
padding: 2px 20px 2px 20px;
background-size: 100% 100%;
background-repeat: no-repeat;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&.weather-header {
.item-title {
width: 100%;
}
}
}
.item-content {
padding-left: 34px; /* Align with title (icon width + margin) */
&.full-width {
padding-left: 0;
}
&.weather-content {
padding-left: 0;
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
line-height: 1.5;
text-indent: 2em;
}
.warning-text-wrapper {
display: flex;
align-items: flex-start;
margin-bottom: 5px;
.warning-icon-inline {
width: 20px;
height: 20px;
margin-right: 8px;
flex-shrink: 0;
margin-top: 2px;
}
.warning-text {
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
line-height: 1.5;
}
}
p {
margin: 0 0 5px 0;
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
line-height: 1.5;
.highlight {
color: #ff4d4f; /* Red for warning values */
font-weight: bold;
}
}
.item-time {
margin-top: 5px;
font-size: 14px;
color: rgba(255, 255, 255);
}
.weather-time {
padding-left: 50px;
margin-bottom: 5px;
margin-top: -5px;
}
}
.divider {
margin-top: 10px;
height: 1px;
background: repeating-linear-gradient(to right, rgba(0, 160, 233, 0.5) 0, rgba(0, 160, 233, 0.5) 5px, transparent 5px, transparent 10px);
}
}
}
.safety-monitor-view {
display: flex;
justify-content: space-around;
align-items: center;
padding: 20px 10px;
height: 100%;
.monitor-item {
display: flex;
flex-direction: column;
align-items: center;
.monitor-value {
font-size: 24px;
font-weight: bold;
}
.monitor-icon {
width: 80px;
height: 60px;
margin-bottom: 10px;
object-fit: contain;
}
.monitor-label {
font-size: 14px;
color: #fff;
}
}
}
}

View File

@ -1,17 +1,24 @@
import React from 'react'; import React from 'react';
import CommonCard from '../../UI/CommonCard'; import CommonCard from '../../UI/CommonCard';
import ThreeDots from '../../UI/ThreeDots'; import ThreeDots from '../../UI/ThreeDots';
import WarningSection from './components/WarningSection';
import './index.less'; import './index.less';
const WarningToggles = () => { const WarningToggles = ({ activeType, onToggle }) => {
return ( return (
<div className="warning-toggles"> <div className="warning-toggles">
<div className="toggle-item active"> <div
className={`toggle-item ${activeType === 'monitor' ? 'active' : ''}`}
onClick={() => onToggle('monitor')}
>
<span>监测预警</span> <span>监测预警</span>
<span className="badge">4</span> <span className="badge">4</span>
</div> </div>
<div className="toggle-item"> <div
className={`toggle-item ${activeType === 'forecast' ? 'active' : ''}`}
onClick={() => onToggle('forecast')}
>
<span>预报预警</span> <span>预报预警</span>
<span className="badge bg-red">2</span> <span className="badge bg-red">2</span>
</div> </div>
@ -20,6 +27,8 @@ const WarningToggles = () => {
} }
const SiYu = () => { const SiYu = () => {
const [warningType, setWarningType] = React.useState('monitor');
return ( return (
<div className="siyu-view"> <div className="siyu-view">
<div className="side-panel left"> <div className="side-panel left">
@ -35,9 +44,9 @@ const SiYu = () => {
<CommonCard <CommonCard
title="预警" title="预警"
className="panel-card card-1" className="panel-card card-1"
headerExtra={<WarningToggles />} headerExtra={<WarningToggles activeType={warningType} onToggle={setWarningType} />}
> >
<div className="placeholder-content">内容填充区域</div> <WarningSection type={warningType} />
</CommonCard> </CommonCard>
<CommonCard <CommonCard
title="实时水雨情" title="实时水雨情"

View File

@ -11,7 +11,44 @@
} }
.right { .right {
.card-1 { flex: 1; } .card-1 {
flex: 1;
.warning-toggles {
display: flex;
align-items: center;
.toggle-item {
display: flex;
align-items: center;
margin-left: 15px;
font-size: 14px;
color: rgba(255, 255, 255,0.7);
cursor: pointer;
transition: all 0.3s;
&.active {
color: #fff;
text-shadow: 0 0 10px #00a0e9;
}
.badge {
display: inline-block;
min-width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
background: #ff4d4f; /* Default red */
border-radius: 7px;
font-size: 12px;
color: #fff;
margin-left: 5px;
padding: 0 4px;
}
}
}
}
.card-2 { flex: 1; } .card-2 { flex: 1; }
.card-3 { flex: 1; } .card-3 { flex: 1; }
} }

View File

@ -0,0 +1,212 @@
import React, { useState,useEffect } from 'react';
import { Image } from 'antd';
import arrowIcon from '@/assets/images/card/arrow.png';
import selectedBg from '@/assets/images/modal/selected.png';
import resperson from '@/assets/images/card/resperson.png';
import smallCard from '@/assets/images/card/smallCard.png';
import qys from '@/assets/images/business/qys.png';
import PdfView from '@/views/Home/components/UI/PdfView';
import apiurl from '@/service/apiurl';
import { httpget } from '@/utils/request';
import { download } from '@/utils/tools';
import './index.less';
const ImplementResponsibility = () => {
const types = {
1:"行政责任人",
2:"主管部门责任人",
3:"管理单位责任人",
4:"巡查责任人",
5:"技术责任人",
}
const [activeTab, setActiveTab] = useState('dam'); // 'dam' or 'flood'
const [pdfInfo, setPdfInfo] = useState({ visible: false, title: '', fileId: '' });
const [imagePreview, setImagePreview] = useState({ visible: false, src: '' });
const [respData, setRespData] = useState([])
const [floodData, setFloodData] = useState([])
const [trainData, setTrainData] = useState({})
const [trainFileData, setTrainFileData] = useState({})
// 获取大坝安全责任人
const getRespData = async () => {
try {
const { code, data } = await httpget(apiurl.sz.zrz.respPerson)
if (code == 200 && data.length > 0) {
const list = data.map(item => ({name:item.name,phone:item.contactInfo,role:types[item.type]}))
setRespData(list)
}
} catch (error) {
console.log(error);
}
}
// 获取防汛安全责任人
const getFloodData = async () => {
try {
const { code, data } = await httpget(apiurl.sz.zrz.floodPerson)
if (code == 200 && data.length > 0) {
const list = data.map(item => ({name:item.name,phone:item.contactInfo,role:types[item.type]}))
setFloodData(list)
}
} catch (error) {
console.log(error);
}
}
// 获取岗位培训
const getTrainData = async () => {
try {
const { code, data } = await httpget(apiurl.sz.zrz.train)
if (code == 200) {
setTrainData(data)
setTrainFileData(data?.latestPersonnelPlan?.files[0])
}
} catch (error) {
console.log(error);
}
}
const {trainingCount,hasTraining,hasNoTraining,totalTraining,latestPersonnelPlan} = trainData||{}
const trainingStats = [
{ label: '培训计划', value: trainingCount??'-' },
{ label: '已开展', value: hasTraining??'-' },
{ label: '未开展', value: hasNoTraining??'-' },
{ label: '参训总人次', value: totalTraining??'-' }
];
const handleTrainingClick = () => {
if (!trainFileData || !trainFileData.fileName) return;
const fileName = trainFileData.fileName;
const fileId = trainFileData.fileId;
const extension = fileName.split('.').pop().toLowerCase();
const downloadUrl = `/gunshiApp/ss/personnelPlan/file/download/${fileId}`;
if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(extension)) {
setImagePreview({
visible: true,
src: downloadUrl
});
} else if (extension === 'pdf') {
setPdfInfo({
visible: true,
title: fileName,
fileId: fileId
});
} else {
download(downloadUrl)
// window.location.href = downloadUrl;
}
};
const currentPersonList = activeTab === 'dam' ? respData : floodData;
useEffect(() => {
if (activeTab === 'dam') {
getRespData()
} else {
getFloodData()
}
}, [activeTab])
useEffect(() => {
getTrainData()
}, [])
return (
<div className="implement-responsibility">
{/* Top Tabs */}
<div className="tabs-container">
<div
className={`tab-item ${activeTab === 'dam' ? 'active' : ''}`}
onClick={() => setActiveTab('dam')}
style={activeTab === 'dam' ? { backgroundImage: `url(${selectedBg})` } : {}}
>
大坝安全责任人
</div>
<div
className={`tab-item ${activeTab === 'flood' ? 'active' : ''}`}
onClick={() => setActiveTab('flood')}
style={activeTab === 'flood' ? { backgroundImage: `url(${selectedBg})` } : {}}
>
防汛 三个责任人
</div>
</div>
{/* Person List */}
<div className="person-list">
{currentPersonList.map((person, index) => (
<div key={index} className="person-item">
<img src={resperson} alt="avatar" className="avatar" />
<span className="name">{person.name}</span>
<span className="phone">{person.phone}</span>
<div
className="role-label"
style={{ backgroundImage: `url(${smallCard})` }}
title={person.role}
>
{person.role}
</div>
</div>
))}
</div>
{/* Job Training Section */}
<div className="section-header">
<div className="title-wrapper">
<img src={arrowIcon} alt="arrow" className="arrow-icon" />
<span>岗位培训</span>
</div>
</div>
{/* Training Stats */}
<div className="training-stats">
{trainingStats.map((stat, index) => (
<div key={index} className="stat-item">
<span className="stat-value">{stat.value}</span>
<img src={qys} alt="icon" className="stat-icon" />
<span className="stat-label">{stat.label}</span>
</div>
))}
</div>
{/* Latest Training Content */}
<div className="latest-training" style={{ backgroundImage: `url(${smallCard})` }}>
<span className="label">最新培训内容:</span>
<div
className="value-box"
onClick={handleTrainingClick}
title={trainFileData.fileName?.replace(/\.[^/.]+$/, '')}
>
{trainFileData.fileName?.replace(/\.[^/.]+$/, '')}
</div>
</div>
{/* PDF Viewer */}
{pdfInfo.visible && (
<PdfView
visible={pdfInfo.visible}
onClose={() => setPdfInfo({ ...pdfInfo, visible: false })}
title={pdfInfo.title}
fileId={pdfInfo.fileId}
url="/gunshiApp/ss/personnelPlan/file/download/"
/>
)}
{/* Image Preview */}
<div style={{ display: 'none' }}>
<Image
src={imagePreview.src}
preview={{
visible: imagePreview.visible,
src: imagePreview.src,
onVisibleChange: (value) => {
setImagePreview({ ...imagePreview, visible: value });
},
}}
/>
</div>
</div>
);
};
export default ImplementResponsibility;

View File

@ -0,0 +1,173 @@
.implement-responsibility {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
.tabs-container {
display: flex;
justify-content: space-around;
margin-bottom: 10px;
.tab-item {
flex: 1;
text-align: center;
padding: 3px 0;
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
background-size: 100% 100%;
background-repeat: no-repeat;
transition: all 0.3s;
&.active {
color: #fff;
text-shadow: 0 0 10px #00a0e9;
}
&:hover {
color: #fff;
}
}
}
.person-list {
display: flex;
justify-content: space-between;
padding: 0 10px;
margin-bottom: 10px;
.person-item {
display: flex;
flex-direction: column;
align-items: center;
width: 30%;
.avatar {
width: 40px;
height: 45px;
margin-bottom: 5px;
}
.name {
font-size: 14px;
color: #fff;
margin-bottom: 2px;
}
.phone {
font-size: 14px;
color: #fff;
margin-bottom: 5px;
}
.role-label {
width: 100%;
height: 24px;
line-height: 24px;
text-align: center;
color: #fff;
font-size: 14px;
background-size: 100% 100%;
background-repeat: no-repeat;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.title-wrapper {
display: flex;
align-items: center;
.arrow-icon {
width: 20px;
height: 18px;
margin-right: 8px;
object-fit: contain;
}
span {
font-size: 14px;
color: #fff;
text-shadow: 0 0 5px rgba(0, 160, 233, 0.5);
}
}
}
.training-stats {
display: flex;
justify-content: space-between;
padding: 0 10px;
margin-bottom: 15px;
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.stat-value {
font-size: 18px;
color: #00eaff;
font-weight: bold;
margin-bottom: -5px;
z-index: 1;
}
.stat-icon {
width: 100px;
height: 60px;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
color: rgba(255, 255, 255);
}
}
}
.latest-training {
display: flex;
align-items: center;
padding: 0 10px;
margin-top: auto;
margin-bottom: 10px;
background-size: 100% 100%;
background-repeat: no-repeat;
.label {
font-size: 14px;
color: #fff;
margin-right: 10px;
white-space: nowrap;
}
.value-box {
flex: 1;
height: 30px;
line-height: 30px;
padding: 0 10px;
color: #00eaff;
font-size: 14px;
cursor: pointer;
text-decoration: underline;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
color: #fff;
}
}
}
}

View File

@ -0,0 +1,362 @@
import React, { useState,useEffect } from 'react';
import ReactEcharts from 'echarts-for-react';
import arrowIcon from '@/assets/images/card/arrow.png';
import selectedBg from '@/assets/images/modal/selected.png';
import YearSelect from '@/views/Home/components/UI/YearSelect';
import PdfView from '@/views/Home/components/UI/PdfView';
import { FileTextOutlined } from '@ant-design/icons';
import apiurl from '@/service/apiurl';
import { httppost, httpget } from '@/utils/request';
import { Empty } from 'antd';
import './index.less';
import moment from 'moment';
const StrengthenRuleOfLaw = () => {
const params = {
pageSo:{ pageSize: 9999, pageNumber: 1 }
}
const [activeTab, setActiveTab] = useState('laws');
const [lawData, setLawData] = useState([]);
const [systemData, setSystemData] = useState([]);
const [year, setYear] = useState(moment().format('YYYY'));
const [pdfInfo, setPdfInfo] = useState({ visible: false, title: '', fileId: '' });
// 获取法律法规文件
const lawsFiles = async () => {
try {
const { code, data } = await httppost(apiurl.sz.qhfz.lawPage, params)
if (code == 200) {
const list = data?.records.map(item => {
if (item?.files?.length === 0) {
return []
}
return { name: item?.files[0]?.fileName?.replace(/\.pdf$/i, ''), fileId: item?.files[0]?.fileId }
}).flat()
setLawData(list)
}
} catch (error) {
console.log(error);
}
}
// 获取系统文件
const systemFiles = async () => {
try {
const { code, data } = await httppost(apiurl.sz.qhfz.systemPage, params)
if (code == 200) {
const list = data?.records.map(item => {
if (item?.files?.length === 0) {
return []
}
return { name: item?.files[0]?.fileName?.replace(/\.pdf$/i, ''), fileId: item?.files[0]?.fileId }
}).flat()
setSystemData(list)
}
} catch (error) {
console.log(error);
}
}
const [chartData, setChartData] = useState([]);
const [selectedPieItem, setSelectedPieItem] = useState(null);
const [startAngle, setStartAngle] = useState(90);
const typeMapping = {
0: { name: '违建', color: '#1890ff' },
1: { name: '毁林垦荒', color: '#00eaff' },
2: { name: '筑坝拦汊', color: '#3155fb' },
3: { name: '填占库容', color: '#bcebf7' },
4: { name: '违法取水', color: '#00d085' },
5: { name: '其他', color: '#1890ff' },
};
// 获取水政执法数据 0:违建,1:毀林垦荒,2:筑坝拦汊,3:填占库容,4:违法取水,5:其他
const caseList = async (params) => {
try {
const { code, data } = await httppost(apiurl.sz.qhfz.caseSta, params)
if (code == 200) {
if (!data || data.length === 0) {
setChartData([]);
return;
}
const newChartData = data.map(item => {
const typeInfo = typeMapping[item.type] || { name: '未知', color: '#ccc' };
return {
name: typeInfo.name,
value: item.count,
color: typeInfo.color
};
});
setChartData(newChartData);
}
} catch (error) {
console.log(error);
}
}
const total = chartData.reduce((acc, cur) => acc + cur.value, 0);
const getOption = () => {
let centerName = '案件总数';
let centerValue = total;
let centerPercent = '100%';
if (selectedPieItem) {
centerName = selectedPieItem.name;
centerValue = selectedPieItem.value;
centerPercent = selectedPieItem.percent;
} else if (chartData.length === 1) {
centerName = chartData[0].name;
}
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
series: [
{
name: '水政执法-外圈',
type: 'pie',
radius: ['48%', '82%'],
center: ['34%', '50%'],
startAngle: startAngle,
avoidLabelOverlap: false,
label: {
show: true,
position: 'inside',
formatter: '{b}',
color: '#fff',
fontSize: 10
},
itemStyle: {
opacity: 0.4
},
data: chartData.map(item => ({
value: item.value,
name: item.name,
itemStyle: { color: item.color }
}))
},
{
name: '水政执法-内圈',
type: 'pie',
radius: ['50%', '56%'],
center: ['34%', '50%'],
startAngle: startAngle,
avoidLabelOverlap: false,
label: {
show: true,
position: 'center',
formatter: () => `{name|${centerName}}\n{value|${centerValue}}\n{percent|${centerPercent}}`,
rich: {
name: {
fontSize: 12,
color: 'rgba(255,255,255,0.6)',
lineHeight: 16
},
value: {
fontSize: 14,
color: '#fff',
fontWeight: 'bold',
lineHeight: 20
},
percent: {
fontSize: 12,
color: '#fff',
lineHeight: 16
}
}
},
emphasis: {
label: {
show: true,
fontSize: '14',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: chartData.map(item => ({
value: item.value,
name: item.name,
itemStyle: { color: item.color }
}))
}
]
}};
const onChartClick = (params) => {
if (params.componentType !== 'series') return;
const { name, value, dataIndex } = params;
// Toggle selection
if (selectedPieItem && selectedPieItem.name === name) {
setSelectedPieItem(null);
setStartAngle(90);
return;
}
let sumBefore = 0;
for (let i = 0; i < dataIndex; i++) {
sumBefore += chartData[i].value;
}
const offset = (sumBefore + value / 2) / total * 360;
const newStartAngle = 90 + offset;
setStartAngle(newStartAngle);
setSelectedPieItem({
name,
value,
percent: ((value / total) * 100).toFixed(0) + '%'
});
};
const handleItemClick = (item) => {
setPdfInfo({
visible: true,
title: item.name,
fileId: item.fileId
});
};
useEffect(() => {
if (chartData && chartData.length > 0) {
const totalVal = chartData.reduce((acc, cur) => acc + cur.value, 0);
const firstItem = chartData[0];
const value = firstItem.value;
const offset = (value / 2) / totalVal * 360;
const newStartAngle = 90 + offset;
setStartAngle(newStartAngle);
setSelectedPieItem({
name: firstItem.name,
value: value,
percent: ((value / totalVal) * 100).toFixed(0) + '%'
});
} else {
setSelectedPieItem(null);
setStartAngle(90);
}
}, [chartData]);
useEffect(() => {
if (activeTab == 'laws') {
lawsFiles()
} else {
systemFiles()
}
}, [activeTab])
useEffect(() => {
if (year) {
const params = {
stm: moment(year).format('YYYY-01-01 00:00:00'),
etm: moment(year).format('YYYY-12-31 23:59:59'),
}
caseList(params)
}
}, [year])
return (
<div className="strengthen-rule-of-law">
{/* Top Tabs */}
<div className="tabs-container">
<div
className={`tab-item ${activeTab === 'laws' ? 'active' : ''}`}
onClick={() => setActiveTab('laws')}
style={activeTab === 'laws' ? { backgroundImage: `url(${selectedBg})` } : {}}
>
法律法规
</div>
<div
className={`tab-item ${activeTab === 'system' ? 'active' : ''}`}
onClick={() => setActiveTab('system')}
style={activeTab === 'system' ? { backgroundImage: `url(${selectedBg})` } : {}}
>
制度管理
</div>
</div>
{/* List Content */}
<div className="list-content">
{(activeTab === 'laws' ? lawData : systemData).length > 0 ? (
(activeTab === 'laws' ? lawData : systemData).map((item, index) => (
<div key={index} className="list-item" onClick={() => handleItemClick(item)}>
<div className="icon-wrapper">
<FileTextOutlined style={{ color: '#1890ff', fontSize: '16px' }} />
</div>
<span className="text">{item.name}</span>
</div>
))
) : (
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={<span style={{color:'#fff'}}>暂无数据</span>} />
</div>
)}
</div>
{/* Water Administration Law Enforcement */}
<div className="enforcement-section">
<div className="section-header">
<div className="title-wrapper">
<img src={arrowIcon} alt="arrow" className="arrow-icon" />
<span>水政执法</span>
</div>
<YearSelect
value={year}
onChange={setYear}
style={{ width: 100 }}
/>
</div>
<div className="chart-legend-container">
{chartData.length > 0 ? (
<>
<div className="chart-wrapper">
<ReactEcharts
option={getOption()}
style={{ height: '100%', width: '100%' }}
notMerge={true}
onEvents={{
'click': onChartClick
}}
/>
</div>
<div className="legend-wrapper">
{chartData.map((item, index) => (
<div key={index} className="legend-item">
<span className="color-dot" style={{ backgroundColor: item.color }}></span>
<span className="name">{item.name}</span>
<span className="value">{item.value}</span>
</div>
))}
</div>
</>
) : (
<div style={{ width: '100%', height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={<span style={{color:'#fff'}}>暂无数据</span>} />
</div>
)}
</div>
</div>
{/* PDF Viewer */}
{pdfInfo.visible && (
<PdfView
visible={pdfInfo.visible}
onClose={() => setPdfInfo({ ...pdfInfo, visible: false })}
title={pdfInfo.title}
fileId={pdfInfo.fileId}
url="/gunshiApp/ss/projectEvents/file/download/"
/>
)}
</div>
);
};
export default StrengthenRuleOfLaw;

View File

@ -0,0 +1,148 @@
.strengthen-rule-of-law {
width: 100%;
height: 100%;
color: #fff;
display: flex;
flex-direction: column;
.tabs-container {
display: flex;
justify-content: space-between;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
.tab-item {
flex: 1;
text-align: center;
padding: 3px 0;
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
background-size: 100% 100%;
background-repeat: no-repeat;
transition: all 0.3s;
&.active {
color: #fff;
text-shadow: 0 0 10px #00a0e9;
}
&:hover {
color: #fff;
}
}
}
.list-content {
flex: 1;
overflow-y: auto;
margin-bottom: 15px;
max-height: 120px; /* Limit height to show scroll if needed, though items are few */
.list-item {
display: flex;
align-items: center;
padding: 8px 0;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: rgba(255, 255, 255, 0.05);
.text {
color: #00a0e9;
text-decoration: underline;
}
}
.icon-wrapper {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
background: rgba(24, 144, 255, 0.1);
border-radius: 4px;
}
.text {
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
}
}
}
.enforcement-section {
flex: 1;
display: flex;
flex-direction: column;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.title-wrapper {
display: flex;
align-items: center;
.arrow-icon {
width: 20px;
height: 18px;
margin-right: 8px;
object-fit: contain;
}
span {
font-size: 14px;
color: #fff;
text-shadow: 0 0 5px rgba(0, 160, 233, 0.5);
}
}
}
.chart-legend-container {
flex: 1;
display: flex;
align-items: center;
.chart-wrapper {
flex: 1;
height: 100%;
min-height: 140px;
}
.legend-wrapper {
width: 140px;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 10px;
.legend-item {
display: flex;
align-items: center;
margin-bottom: 6px;
font-size: 12px;
.color-dot {
width: 8px;
height: 8px;
margin-right: 8px;
border-radius: 2px;
}
.name {
flex: 1;
color: rgba(255, 255, 255, 0.8);
}
.value {
color: #fff;
font-weight: bold;
}
}
}
}
}
}

View File

@ -2,6 +2,8 @@ import React, { useState, useEffect } from 'react';
import CommonCard from '../../UI/CommonCard'; import CommonCard from '../../UI/CommonCard';
import PerfectSystem from './components/PerfectSystem'; import PerfectSystem from './components/PerfectSystem';
import SoundMechanism from './components/SoundMechanism'; import SoundMechanism from './components/SoundMechanism';
import StrengthenRuleOfLaw from './components/StrengthenRuleOfLaw';
import ImplementResponsibility from './components/ImplementResponsibility';
import { httppost } from '@/utils/request'; import { httppost } from '@/utils/request';
import apiurl from '@/service/apiurl'; import apiurl from '@/service/apiurl';
import './index.less'; import './index.less';
@ -36,10 +38,10 @@ const SiZhi = () => {
<div className="side-panel right"> <div className="side-panel right">
<CommonCard title="强化法治" className="panel-card card-1"> <CommonCard title="强化法治" className="panel-card card-1">
<div className="placeholder-content">内容填充区域</div> <StrengthenRuleOfLaw />
</CommonCard> </CommonCard>
<CommonCard title="落实责任制" className="panel-card card-2"> <CommonCard title="落实责任制" className="panel-card card-2">
<div className="placeholder-content">内容填充区域</div> <ImplementResponsibility />
</CommonCard> </CommonCard>
</div> </div>
</div> </div>

View File

@ -7,7 +7,7 @@ const PdfView = ({ visible, title, onClose, url, fileId }) => {
return ( return (
<CommonModal <CommonModal
visible={visible} visible={visible}
title={title} title={title?.replace(/\.pdf$/i, '')}
onClose={onClose} onClose={onClose}
width="60%" width="60%"
bodyStyle={{ padding: 0, overflow: 'hidden' }} bodyStyle={{ padding: 0, overflow: 'hidden' }}

View File

@ -9,14 +9,18 @@ const YearSelect = ({ value, onChange, className, style, ...props }) => {
onChange(dateString); onChange(dateString);
} }
}; };
const disabledDate = (current) => {
return current && current > moment().endOf('day');
};
return ( return (
<DatePicker <DatePicker
picker="year" picker="year"
disabledDate={disabledDate}
value={value ? moment(value, 'YYYY') : null} value={value ? moment(value, 'YYYY') : null}
onChange={handleChange} onChange={handleChange}
className={`custom-year-select ${className || ''}`} className={`custom-year-select ${className || ''}`}
dropdownClassName="custom-year-select-dropdown" // dropdownClassName="custom-year-select-dropdown"
style={style} style={style}
allowClear={false} allowClear={false}
{...props} {...props}

View File

@ -65,6 +65,14 @@
color: #fff; color: #fff;
} }
.ant-picker-cell-disabled {
pointer-events: none;
.ant-picker-cell-inner {
color: rgba(255, 255, 255, 0.25) !important;
background-color: rgba(255, 255, 255, 0.1) !important;
}
}
.ant-picker-cell-selected .ant-picker-cell-inner { .ant-picker-cell-selected .ant-picker-cell-inner {
background-color: #00a0e9 !important; background-color: #00a0e9 !important;
color: #fff; color: #fff;