first commit

qzc-dev
李神峰 2026-01-21 16:29:13 +08:00
commit 1aa667daf2
77 changed files with 28702 additions and 0 deletions

2
.env.development Normal file
View File

@ -0,0 +1,2 @@
GENERATE_SOURCEMAP=true
PUBLIC_URL=/ssDp

2
.env.production Normal file
View File

@ -0,0 +1,2 @@
GENERATE_SOURCEMAP=false
PUBLIC_URL=/ssDp

31
.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
build.zip
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
build.7z
#ai
.serena/
CLAUDE.md
local-ai/

51
craco.config.js Normal file
View File

@ -0,0 +1,51 @@
const CracoLessPlugin = require('craco-less');
const pxToViewport = require('postcss-px-to-viewport-8-plugin');
module.exports = {
style: {
postcss: {
mode: 'extends',
loaderOptions: {
postcssOptions: {
ident: 'postcss',
plugins: [
pxToViewport({
viewportWidth: 1920, // 设计稿宽度
unitPrecision: 5, // 单位转换后保留的精度
viewportUnit: 'vw', // 希望使用的视口单位
selectorBlackList: [], // 需要忽略的CSS选择器
minPixelValue: 1, // 小于或等于`1px`不转换为视口单位
mediaQuery: false, // 允许在媒体查询中转换`px`
}),
],
},
},
},
},
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {
'@primary-color': '#1890FF',
'@primary-color-hover': '#3B7CFF',
'@error-color': '#F55E55',
'@text-color': '#3B4859',
'@menu-dark-bg': '#0052D9',
'@layout-body-background': '#eeeeee',
'@height-base': '36px',
'@form-vertical-label-padding': '0 0 10px',
'@normal-color': '#EFF1F5',
'@label-color': '#3B4859',
'@card-background': '#fbfbfb',
'@heading-color': '#3B4859',
},
javascriptEnabled: true,
},
},
},
},
],
};

24807
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

66
package.json Normal file
View File

@ -0,0 +1,66 @@
{
"name": "wswl-web",
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/charts": "^1.4.2",
"@ant-design/graphs": "^1.4.0",
"@ant-design/icons": "^5.3.6",
"@ant-design/pro-form": "^1.49.6",
"@craco/craco": "^7.0.0",
"@rematch/core": "^2.2.0",
"@turf/turf": "^6.5.0",
"@types/crypto-js": "^4.1.1",
"@types/geojson": "^7946.0.8",
"@types/leaflet": "^1.7.11",
"@types/node": "^16.11.45",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-react": "^1.0.6",
"antd": "^4.21.7",
"clone": "^2.1.2",
"clsx": "^1.2.1",
"copy-to-clipboard": "^3.3.3",
"craco-less": "2.1.0-alpha.0",
"crypto-js": "^4.1.1",
"echarts": "^4.9.0",
"echarts-for-react": "^3.0.2",
"http-proxy-middleware": "^2.0.6",
"moment": "^2.29.4",
"react": "^18.2.0",
"react-audio-player": "^0.17.0",
"react-cookies": "^0.1.1",
"react-dom": "^18.2.0",
"react-redux": "^8.0.2",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"redux": "^4.2.0",
"typescript": "^4.7.4"
},
"scripts": {
"start": "craco start",
"build": "craco build"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"postcss-px-to-viewport-8-plugin": "^1.2.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

59
public/index.html Normal file
View File

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="标题" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>双石水库矩阵化管理</title>
<!-- <script src="%PUBLIC_URL%/iframe/socket.io.js"></script> -->
<style>
.lf {
float: left;
}
.rf {
float: right;
}
.clearFloat:after {
content: "";
display: block;
clear: both;
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

View File

@ -0,0 +1,21 @@
import { Select } from "antd"
const NormalSelect = function (prop) {
return (
<>
<Select {...prop}
getPopupContainer={triggerNode => triggerNode.parentNode || document.body}
style={prop.style||{width:'100%'}}
showSearch
allowClear={prop.allowClear===false?false:true}
optionFilterProp="children"
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
/>
</>
)
}
export default NormalSelect

View File

@ -0,0 +1,86 @@
.ant-table-thead>tr>th {
border-bottom: none !important;
padding: 6px 6px;
background: #ECF2F9 !important;
color: #333 !important;
font-weight: 600 !important;
&::before {
width: 0 !important;
}
/*&:first-child {
border-top-left-radius: 16px !important;
}
&:last-child {
border-top-right-radius: 16px !important;
}*/
}
.ant-table {
background: transparent !important;
}
.ant-table-tbody>tr>td {
//border-bottom: none !important;
background: #fff;
//box-shadow: 3px 2px 3px 2px #E8ECF1;
padding: 6px 6px;
/* &:first-child {
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
&:last-child {
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
}*/
}
.ant-table table,.ant-table.ant-table-bordered > .ant-table-container,
.ant-table.ant-table-bordered > .ant-table-container > .ant-table-content > table > tbody > tr > td{
//border-spacing: 0 8px !important;
//border:0;
}
.ant-radio-group {
.ant-radio-button-wrapper:first-child {
// border-radius: 8px 0 0 8px !important
}
.ant-radio-button-wrapper:last-child {
// border-radius: 0 8px 8px 0 !important
}
.ant-radio-button-wrapper {
border: 1px solid @primary-color !important;
&:hover {
background-color: #D9EBFF;
}
}
.ant-radio-button-wrapper {
color: #3B7CFF;
}
}
.ant-checkbox-checked {
.ant-checkbox-inner {
background-color: #3b7cff !important;
border-color: #3b7cff !important;
}
&::after {
border: none !important;
}
}
.ant-checkbox-inner {
border-radius: 4px !important;
border-width: 1.4px !important;
border-color: #C7CBD3 !important;
}

View File

@ -0,0 +1,142 @@
import React from 'react';
import { Modal, message } from 'antd';
import { createCrudService } from './_';
import apiurl from '../../service/apiurl';
class BasicCrudModal extends React.Component {
constructor(props) {
super(props);
this.state = {
record: null,
open: false,
mode: null,
};
}
showEdit(record) {
record = record || {};
this.setState({ record, open: true, mode: 'edit' });
}
showSave(record) {
record = record || {};
this.setState({ record, open: true, mode: 'save' });
}
showView(record) {
record = record || {};
this.setState({ record, open: true, mode: 'view' });
}
onEdit = (path, values) => {
createCrudService(path).edit(values).then((result) => {
if (result?.code === 200) {
message.success('修改成功');
this.setState({ open: false });
if (this.props.onCrudSuccess) {
this.props.onCrudSuccess();
}
} else if (result?.code === 400) {
message.error(result?.description);
}
else {
message.error(result?.msg||'修改失败');
}
})
}
onSave = (path,values) => {
createCrudService(path).save(values).then((result) => {
if (result?.code === 200) {
message.success('保存成功');
this.setState({ open: false });
if (this.props.onCrudSuccess) {
this.props.onCrudSuccess();
}
} else if (result?.code === 400) {
message.error(result?.description||'修改失败');
}
else {
message.error(result?.msg||'保存失败');
}
})
}
onDelete = (path, values) => {
createCrudService(path).del(values).then((result) => {
if (result?.code === 200) {
message.success('删除成功');
this.setState({ open: false });
if (this.props.onCrudSuccess) {
this.props.onCrudSuccess();
}
} else {
message.error(result?.msg||'删除失败');
}
})
}
onDeleteGet = (path,values) => {
createCrudService(path).delGet(values).then((result) => {
if (result?.code === 200) {
message.success('删除成功');
this.setState({ open: false });
if (this.props.onCrudSuccess) {
this.props.onCrudSuccess();
}
} else {
message.error(result?.msg||'删除失败');
}
})
}
whichMod = (value) => {
if(value === 'save'){
return '新增'
}else if (value === 'add' || value === ''){
return '新增'
}else if (value === 'edit'){
return '修改'
}else if (value === 'view'){
return '查看'
}else if (value === 'show' || value === 'review'){
return ''
}else if (value === 'zg'){
return '整改详情'
}
else {
return '新增'
}
}
render() {
const { record, open, mode } = this.state;
const { width, title, style, wrapClassName, formProps, component: Component,title1 } = this.props;
if (!record || !open) {
return null;
}
return (
<Modal
width={width || 600}
style={style || { top: 10 }}
wrapClassName={wrapClassName}
open={true}
title={title1 ||`${this.whichMod(mode)}${title}`}
footer={null}
destroyOnClose
onCancel={() => { this.setState({ open: false }) }}
>
<Component
mode={mode}
close={()=>{this.setState({ open: false })}}
onCrudSuccess={this.props.onCrudSuccess}
record={record}
onEdit={this.onEdit}
onSave={this.onSave}
formProps={formProps}
/>
</Modal>
);
}
}
export default BasicCrudModal;

View File

@ -0,0 +1,137 @@
import React from 'react';
import { Button, Popconfirm } from 'antd';
import { EditOutlined, DeleteOutlined, UndoOutlined,PlusCircleOutlined, FileSearchOutlined } from '@ant-design/icons';
export function CrudOpRender_icon({ command, edit, del, restore, add, view }) {
return (
<div style={{ display: 'inline' }}>
{
add ? (
<Button style={{ marginRight: 4 }} type="link" size="small" icon={<PlusCircleOutlined />} onClick={command('add')} title="增加"></Button>
) : null
}
{
edit ? (
<Button style={{ marginRight: 4 }} type="link" size="small" icon={<EditOutlined />} onClick={command('edit')} title="编辑"></Button>
) : null
}
{
view ? (
<Button style={{ marginRight: 4 }} type="link" size="small" icon={<FileSearchOutlined />} onClick={command('view')} title="查看"></Button>
) : null
}
{
restore ? (
<Popconfirm title="确定要恢复吗?" onConfirm={command('restore')}>
<Button style={{ marginRight: 4 }} type="link" size="small" icon={<UndoOutlined />} title="恢复"></Button>
</Popconfirm>
) : null
}
{
del ? (
<Popconfirm title="确定要删除吗?" onConfirm={command('del')}>
<Button style={{ marginRight: 4 }} type="link" icon={<DeleteOutlined />} size="small" title='删除'></Button>
</Popconfirm>
) : null
}
</div>
)
}
export function CrudOpRender_text({ command,picReview, edit,detail, dispatch,del, restore, add,similarAdd, view,zg, cjxgl,review,download,record }) {
return (
<div style={{ display: 'inline' }}>
{
record ? (
<Button style={{ marginRight: 4 }} type="link" size="small" onClick={command('record')} title="培训记录">培训记录</Button>
) : null
}
{
add ? (
<Button style={{ marginRight: 4 }} type="link" size="small" onClick={command('add')} title="增加">增加</Button>
) : null
}
{
detail ? (
<Button style={{ marginRight: 4 }} type="link" size="small" onClick={command('view')} title="详情">详情</Button>
) : null
}
{
similarAdd ? (
<Button style={{ marginRight: 4 }} type="link" size="small" onClick={command('similarAdd')} title="复制">复制</Button>
) : null
}
{
edit ? (
<Button style={{ marginRight: 4 }} type="link" size="small" onClick={command('edit')} title="编辑">编辑</Button>
) : null
}
{
view ? (
<Button style={{ marginRight: 4 }} type="link" size="small" onClick={command('view')} title="查看">查看</Button>
) : null
}
{
cjxgl ? (
<Button style={{ marginRight: 4 }} type="link" size="small" onClick={command('cjxgl')} title="采集项管理">采集项管理</Button>
) : null
}
{
review ? (
<Button style={{ marginRight: 4 }} type="link" size="small" onClick={command('review')} title="预览">预览</Button>
) : null
}
{
picReview ? (
<Button style={{ marginRight: 4 }} type="link" size="small" onClick={command('picReview')} title="预览">预览</Button>
) : null
}
{
zg ? (
<Button style={{ marginRight: 4 }} type="link" size="small" onClick={command('edit')} title="整改">整改</Button>
) : null
}
{
download ? (
<Button style={{ marginRight: 4 }} type="link" size="small" onClick={command('download')} title="下载">下载</Button>
) : null
}
{
restore ? (
<Popconfirm title="确定要恢复吗?" onConfirm={command('restore')}>
<Button style={{ marginRight: 4 }} type="link" size="small" title="恢复">恢复</Button>
</Popconfirm>
) : null
}
{
dispatch ? (
<Popconfirm title="确定要下发吗?" onConfirm={command('dispatch')}>
<Button style={{ marginRight: 4 }} type="link" size="small" title="下发">下发</Button>
</Popconfirm>
) : null
}
{
del ? (
<Popconfirm title="确定要删除吗?" onConfirm={command('del')}>
<Button style={{ marginRight: 4,color:'red' }} type="link" size="small" title='删除'>删除</Button>
</Popconfirm>
) : null
}
</div>
)
}
export function CrudOpRender_YJPZ({ command, edit, del, restore, add, view }) {
return (
<div style={{ display: 'inline' }}>
{
edit ? (
<Button style={{ marginRight: 4 }} type="link" size="small" onClick={command('edit')} title="预警配置">预警配置</Button>
) : null
}
</div>
)
}

View File

@ -0,0 +1,38 @@
export const DEF_INPUT_LEN = 464;
export const DEF_INPUT_LEN_QUARTS = 92;
export const DEF_LAYOUT = {
labelCol: { span: 8 },
wrapperCol: { span: 16 },
};
export const DEF_TAIL_LAYOUT = {
wrapperCol: { offset: 8, span: 16 },
};
//复制过来
export const formItemLayout = {
labelCol: { span: 6 },
wrapperCol: { span: 14 },
};
export const tailItemLayout = {
wrapperCol: { span: 17, offset: 7 },
};
export const formItemLayoutTight = {
labelCol: { span: 3 },
wrapperCol: { span: 19 },
};
export const tailItemLayoutTight = {
wrapperCol: { span: 20, offset: 4 },
};
export const itemLayoutPercent = {
labelCol: { span: 0 },
wrapperCol: { span: 24 },
};
export const btnItemLayout = {
wrapperCol: { span: 4, offset: 20 },
};

101
src/components/crud/_.js Normal file
View File

@ -0,0 +1,101 @@
import {httppost, httpPostFile, httpget } from "../../utils/request";
export async function paginate(url, params = {}) {
const {pageNumber, pageSize, ...ret} = params;
const pam = {
pageSo: {
pageSize,
pageNumber
},
...ret
}
const result = await httppost(url, pam);
if (result && result.data) {
const {data} = result;
return {
list: data.records.map((m, index) => ({inx: (pageNumber - 1) * pageSize + index + 1, ...m})),
totalRow: data.total
};
}
return {list: [], totalRow: 0};
}
export function createCrudService(urlSet) {
const find = async (params) => {
return paginate(urlSet, params);
}
const save = async (params) => {
const resData = await httppost(urlSet, params) || {};
return resData;
}
const edit = async (params) => {
const resData = await httppost(urlSet, params) || {};
return resData;
}
const del = async (params) => {
const resData = await httppost(urlSet, params) || {};
return resData;
}
const delGet = async (params) => {
const resData = await httpget(urlSet, params) || {};
return resData;
}
const list = async (params) => {
const {pageNumber, pageSize, sort, search} = params;
const {data} = await httppost(urlSet, params) || {};
if (data) {
return {
list: data.map((m, index) => ({inx: (pageNumber - 1) * pageSize + index + 1, ...m})),
totalRow: data?.length
};
}
return {list: data, totalRow: data?.length};
}
const downLoad = async (params) => {
fetch(urlSet, {
method: 'POST',
body: JSON.stringify(params),
// responseType : 'blob',
headers: {
'Content-Type': 'application/json',
}
}).then(function (response) {
console.log('response', response)
return response.blob();
}).then(function (blob) {
const link = document.createElement('a')
link.style.display = 'none'
// link.download = '到处文件'
link.href = URL.createObjectURL(blob)
document.body.appendChild(link)
link.click()
// 释放的 URL 对象以及移除 a 标签
URL.revokeObjectURL(link.href)
document.body.removeChild(link)
});
}
const upLoad = async (params) => {
const {data} = await httpPostFile(urlSet, params) || {};
return data;
}
return {
find: find,
list: list,
save: save,
edit: edit,
del: del,
upLoad: upLoad,
downLoad: downLoad,
}
}

View File

@ -0,0 +1,88 @@
import { useState, useRef, useEffect } from 'react';
import { ListProps, TableProps } from 'antd';
import { PageResult, SearchOption } from '../../service/def';
import { camel2UnderLine } from "../../utils/utils";
export type PageTableContext<T> = {
tableProps: TableProps<T>,
listProps?: ListProps<T>,
search: (params?: SearchOption) => void,
refresh: () => void
}
//最基础版本的pageTable
function usePageTable<T>(
service: (params?: SearchOption) => Promise<PageResult<T>>,
options: SearchOption = {}
): PageTableContext<T> {
const [ state, setState ] = useState(()=>({
data: [],
total: 0,
loading: false,
pageSize: options.pageSize ?? 10,
pageNumber: options.pageNumber ?? 1,
sortField: options.sortField,
sortOrder: options.sortOrder,
search: options.search || {},
}))
const search = async(opt: SearchOption = {})=>{
setState(s => ({ ...s, loading: true }));
const pageParams = {
pageNumber: opt?.pageNumber ?? state.pageNumber,
pageSize: opt?.pageSize ?? state.pageSize,
sortField: opt?.sortField ?? state.sortField,
sortOrder: opt?.sortOrder ?? state.sortOrder,
search: opt?.search ?? state.search,
};
const { search, ...params } = pageParams;
const data:any = await service({ ...search, ...params })
////给每个数据加一个key
data.list.forEach((item:any,index:number) => {
item.dataIndex = new Date().getTime()+'_'+index
});
setState({
...pageParams,
data: data.list,
total: data.totalRow,
loading: false,
})
}
const handleTableChange = (pagination: any, filters: any, sort: any) => {
const { field, order } = sort;
if (field && order) {
const sortOrder = order === 'ascend' ? 'asc' : 'desc';
const _field = camel2UnderLine(field);
search({ pageNumber: pagination.current, pageSize: state.pageSize, sortOrder, sortField: _field });
return;
}
search({ pageNumber: pagination.current, pageSize: pagination.pageSize });
};
return {
tableProps: {
size: 'small',
bordered: true,
dataSource: state.data,
pagination: {
showTotal: (totalRow) => `${totalRow}`,
showSizeChanger: true,
showQuickJumper: true,
pageSize: state.pageSize,
current: state.pageNumber,
total: state.total,
pageSizeOptions: ['10', '15', '20', '50', '100'],
hideOnSinglePage: true,
},
loading: state.loading,
onChange: handleTableChange,
},
search: (params) => search({ ...params, pageNumber: 1 }),
refresh: search,
}
}
export default usePageTable

191
src/index.less Normal file
View File

@ -0,0 +1,191 @@
@import '~antd/dist/antd.less';
html{
font-size: 16px;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-y: hidden;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
#root {
min-width: 1280px;
}
.flex-grow-1 {
flex-grow: 1;
}
::-webkit-scrollbar {
width: 4px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #eee;
}
::-webkit-scrollbar-thumb {
background: #ECF2F9;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #ECF2F9;
}
.ant-card-body{//此处不加.ant-card-body为导致首页的滚动条样式受到影响
::-webkit-scrollbar {
width: 8px !important;
height: 8px !important;
// background: rgba(250, 15, 15, ) !important;
cursor: pointer;
}
::-webkit-scrollbar-corner {
//background-color: gray!important;
}
}
.ant-table-content::-webkit-scrollbar-thumb,.ant-table-body::-webkit-scrollbar-thumb {
background: #C1C1C1;//#ECF2F9;
}
.ant-table-content::-webkit-scrollbar-thumb,.ant-table-body::-webkit-scrollbar-thumb:hover {
background: #C1C1C1;//#ECF2F9;
}
.ant-modal-title{
font-size:16px;
font-weight:bold;
}
.adcdTreeSelectorBox{
width: 230px;
height:calc( 100vh - 145px );
margin:0 20px 0 0;
background: #fff;
//box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
position:relative;
border: 1px solid #d9d9d9;
box-sizing: border-box;
.treeBox{
//height:calc( 100vh - 158px );
overflow: hidden auto;
padding: 8px 0 0 2px;
border-top: 1px solid #d9d9d9;
}
.ant-input-group .ant-input,.ant-btn{
border:0;
}
.ant-input-affix-wrapper{
border:0;
//border-right: 2px solid #d9d9d9;
}
.checkboxBox{
position:absolute;
top:34px;
width:100%;
height:30px;
background:#fff;
border-top: 1px solid #d9d9d9;
padding-top:4px;
}
}
.adcdTreeTableBox{
width:calc( 100vw - 602px );
height:calc( 100vh - 143px );
background: #fff;
//box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
}
.CrudAdcdTreeTableBox{
width:calc( 100vw - 602px );
height:calc( 100vh - 143px );
background: #fff;
//box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
}
.radarVideoModal{
.ant-modal-content{
width: 100%;
height: 100%;
.ant-modal-body{
width: 100%;
height: calc( 100% - 120px );
padding: 0 30px;
}
.ant-tabs-tabpane{
height: calc( 76vh - 120px );
}
}
}
.toolbarBox{
.ant-form-item{
padding: 5px 0;
}
}
.flex{
display: flex;
}
.flexwarp{
flex-wrap: wrap;
}
.flexcc{
justify-content: center;
align-items: center;
}
.flexsbc{
justify-content: space-between;
align-items: center;
}
.flexsac{
justify-content: space-around;
align-items: center;
}
.flexac{
align-items: center;
}
.flexfc{
flex-direction: column;
}
.ant-table-tbody{
.ant-table-measure-row{
td{
padding: 0 !important;
}
}
// tr:nth-child(odd) {
// td{
// background-color: #f5f8fe !important;
// }
// }
}
.pageBox{
height:calc( 100vh - 90px );
overflow-y: scroll;
background-color: #fff !important;
padding:0;
margin:0 0 0 5px;
.ant-table-container{
border-left:1px solid #d9d9d9 !important;
}
}
.formItemHidden{
display: none;
}

25
src/index.tsx Normal file
View File

@ -0,0 +1,25 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/lib/locale/zh_CN';
import 'moment/locale/zh-cn';
import { Provider } from 'react-redux';
import { HashRouter } from 'react-router-dom';
import { store } from './models/store';
import AppRouters from './views/AppRouters';
import './index.less';
import './components/ant_override.less'
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<Provider store={store}>
<HashRouter>
<ConfigProvider locale={zhCN}>
<AppRouters />
</ConfigProvider>
</HashRouter>
</Provider>
);

336
src/models/_/index.ts Normal file
View File

@ -0,0 +1,336 @@
import { Position } from "geojson";
export type PageResult<T> = {
list: T[];
totalRow: number;
}
export type SearchOption = {
pageSize?: number;
pageNumber?: number;
search?: { [key: string]: any };
sortField?: string;
sortOrder?: 'asc' | 'desc';
}
export type MenuItem = {
id: string;
title: string;
path?: string;
redirect?: string;
icon?: string;
children?: MenuItem[];
isRead?:any
}
export type CCTVObject = {
id: number;
indexCode: string;
stcd: string;
}
export type SZObject = {
gateCode: string; // 水闸工程代码 string
gateName: string; // 水闸名称 string
cctvs: CCTVObject[]; // 视频点列表 array object
ctrlType: 0 | 1; // 是否可控 0 否 1 是 integer
gaorNum: number; //闸孔数量 number
lgtd: number; // 经度 number
lttd: number; // 纬度 number
chanName?: string; // 所在渠道 string
adNamee?: string; // 所属行政位置 string
admDep?: string; // 归口管理部门1水利部门2电力部门3农业部门4 林业部门5城建部门6航运部门7环保部门9其他部门 string
bnch?: string; // 桩号 string
collDate?: any; // 属性采集时间 object
compDate?: string; // 建成时间 string
desTotInsCap?: string; // 设计装机总容量(MW) string
dsfl?: number; // 设计流量(m³/s) number
endLat?: number; // 终点纬度 number
endLong?: number; // 终点经度 number
engGrad?: string; // 工程等别12Ⅱ3Ⅲ4Ⅳ5 string
engManName?: string; // 管理单位 string
engStat?: string; // 工程建设情况0未建1在建2已建 string
gateSize?: string; // 闸门尺寸(m) string
gateTp?: string; // 闸门类型 string
hdgrTp?: string; // 启闭设备类型1卷扬式2螺杆式3凹轮式4涡轮式5丝杆式 string
inEle?: number; // 进口高程(m) number
insPow?: number; // 装机功率(KW) number
lockDisc?: number; // 最大过闸流量(m³/s) number
note?: string; // 备注 string
outEle?: number; // 出口高程(m) number
pwrTp?: string; // 动力类型1手动,2电动3手电两用 string
runStat?: string; // 运行情况1在用良好2在用故障3停用 string
startDate?: any; // 开工时间 object
startLat?: number; // 起点纬度 number
startLong?: number; // 起点经度 number
stfl?: number; // 实达流量(m³/s) number
updDate?: any; // 属性更新时间 object
updserDate?: string; // 更新或维修时间 string
updserInvst?: number; // 更新或维修投资 number
updserRsn?: string; // 更新或维修原因 string
wagaType?: string; // 水闸类别1分洪闸2节制闸3排退水闸4引水闸5 挡潮闸6 船闸9其他 string
wagaUse?: string; // 水闸用途 string
wainWasoType?: string; // 取水水源类型1水库2湖泊3河流4其他 string
_z1: number;
_z2: number;
_kd: number;
_ll: number;
_tm: string;
_status: 0 | 1;
}
export type ZMInfo = {
id: string;
name: string;
kdMax: number; // 也许有同一个闸的不同闸门的开度不一样?
}
export type ZMRuntime = {
id: string; // eqpno
gtophgt?: number;
gtq?: number;
maxhgt?: number;
boot?: boolean;
shut?: boolean;
signal?: boolean;
power?: boolean;
}
export type ZmSocketPack = {
stcd: string,
eqpno: string,
tm: string;
gtophgt?: number;
gtq?: number;
maxhgt?: number;
boot?: boolean;
shut?: boolean;
signal?: boolean;
power?: boolean;
}
export type demoZmInfo = {
stcd: string;
stnm: string;
qqnm: string;
type: string;
status: '在线' | '离线';
time: string;
}
/*
export type DemoOpLog = {
id: number,
name: string,
optm: string,
op: string,
userName: string,
cmd: string
};
*/
export type OpLogListItem = {
id: number,
username: string,
userid: number,
tm: number;
cmd: string;
ip: string;
gateName: string;
eqpno: string;
};
export type DemoStatus = {
id: number,
name: string,
qd: string,
s1: number;
s2: number;
s3: number;
s4: number;
s5: number;
s6: number;
};
export type DemoWarn = {
id: number,
name: string,
qd: string,
z1: number;
z2: number;
q: number;
optm: string,
op: string,
warntm: string,
warn: string
}
export type DemoCamera = {
regionIndexCode: string;
deviceName: string;
cameraList: {
indexCode: string;
channelName: string;
}[];
}
export type SzRealListItem = {
gateCode: string;
gateName: string;
chanName?: string;
stcd: string;
eqpno: string;
z1?: number;
z2?: number;
hq?: number;
status?: {
"eqpno": string,
"shut": boolean, //闸门下降中
"autoCtrl": boolean,
"gtophgt": number, //当前开度
"change": boolean,
"fault": boolean, //闸门故障
"fullOpen": boolean, //闸门全开位置
"allowCtrl": boolean,
"stcd": string,
"fullClose": boolean, //闸门全关位置
"lastCmd": string,
"tm": string, //上报时间同时这个时间若超过10分钟则认定闸门离线如若闸门故障或者电源故障是true这个值也是报警时间
"power": boolean, //电源故障
"boot": boolean, //闸门上升中
"signal": boolean,
"maxhgt": number,
"minhgt": number
}
}
export type SafetyListItem = {
"gateCode": string; //修改接口要用到的stcd
"gateName": string;
"chanName": string;
"maxhgt": number; //上限位
"minhgt": number; //下限位
eqpno: string;
}
//////////////////////////////////////////////////
export type LayerName =
'BouaLayer' |
'RivlLayer' |
'TerrainLayer' |
'GQLayer' |
'ZQLayer' |
'SZLayer' |
'ControledSZLayer';
export type RecordId = number | string;
export type LayerVisibleSettings = { [key: string]: boolean | undefined };
export type HighlightSetting = {
[key: string]: RecordId[] | undefined;
};
export type LayerSettings = {
dom?: boolean;
highlight?: HighlightSetting;
};
export type CameraTarget = {
center?: Position,
bound?: [Position, Position],
zoom?: number,
pitch?: number,
}
export type FeatureTipInfo = {
text: string;
left: number;
top: number;
}
// [{ id, type, data, lgtd, lttd, elev }]
/*export type FeaturePopInfo<T = any> = {
id: RecordId;
type: string;
pos: Position;
offsetPop?: boolean;
data: T;
}*/
export type FeaturePopInfo<T = any> = {
id: RecordId;
type: string;
data: T;
// @ts-ignore
lgtd?: number;
// @ts-ignore
lgtd?: number;
elev?: string;
}
export type CameraInfo = {
pos: Position;
heading: number;
pitch: number;
};
export type MapCtrlState = {
layerVisible: LayerVisibleSettings;
layerSetting: LayerSettings;
cameraTarget?: CameraTarget;
}
export type InfoDlg = {
layerId: string;
properties: any;
}
export type MarkerInfo = {
id: string | number;
lgtd: number;
lttd: number;
elev?: number;
}
export type RuntimeState = {
layerSetting: any;
layerSettingLoop: any;
mouseCoords?: Position;
featureTip?: FeatureTipInfo;
featurePops?: FeaturePopInfo[];
infoDlg?: InfoDlg,
markers?: { [layerName: string]: MarkerInfo[] | undefined; },
viewTick?: number,
coordsText?: string,
isReadObject?:Object,
}
export type ZmCtrlState = {
zmobj?: SZObject;
zmInfos?: ZMInfo[]; // 各栅门的基础信息 // todo del
kdMax?: number; // 最大开度 todo del
selectedId?: string; // 选中的闸门
runtime?: { [key: string]: ZMRuntime };
rz1?: number; // 闸前水位
rz2?: number; // 渠道水位
rz3?: number; // 闸后水位
}

135
src/models/auth/_.ts Normal file
View File

@ -0,0 +1,135 @@
import { message } from 'antd'
import { wait } from '../../utils/common'
import { httpget, httppost } from '../../utils/request'
import { MenuItem } from '../_'
// import { apiPaths } from '../_/apipath'
// import apiurl from '../../service/apiurl'
import { store } from '../store'
// @ts-ignore
import cookie from 'react-cookies'
export type LoginUser = {
id: number
loginName: string
name: string
roleList: string[]
tokenInfo: {
tokenValue: string
}
}
export type AuthState = {
user: LoginUser | null | -1
menu: MenuItem[]
}
export const USER_SESSION_KEY = '__usereinfo__'
export function removeLoginInfo() {
let keysToKeep = ["checked", "loginNamePwd"];
let keysToRemove = [];
// 遍历 localStorage
for (let i = 0; i < localStorage.length; i++) {
let key: any = localStorage.key(i);
if (!keysToKeep.includes(key)) {
keysToRemove.push(key);
}
}
for (let i = 0; i < keysToRemove.length; i++) {
localStorage.removeItem(keysToRemove[i]);
}
/*localStorage.removeItem(USER_SESSION_KEY);
localStorage.removeItem('TOKEN');*/
// localStorage.clear()
sessionStorage.clear()
cookie.remove('Admin-Token')
}
export function setLoginInfo(userInfo: string, token: string) {
sessionStorage.setItem(USER_SESSION_KEY, userInfo)
localStorage.setItem('TOKEN', token)
}
export function getUserFromSession(): LoginUser | null {
const strUser = sessionStorage.getItem(USER_SESSION_KEY)
if (!strUser) {
return null
}
try {
const obj: LoginUser = JSON.parse(strUser)
if (obj.id && obj.tokenInfo.tokenValue) {
return obj
}
} catch (e) {}
return null
}
export async function regByToken(
token: string
): Promise<LoginUser | undefined> {
const result: LoginUser | null = await httppost(
// apiPaths.auth.registerByToken,
{ token }
)
if (!result) {
message.error('登陆失败')
return
}
setLoginInfo(JSON.stringify(result), result.tokenInfo?.tokenValue)
return result
}
function idgen() {
let id = 1
return () => `${id++}`
}
export async function loadMenu(): Promise<MenuItem[]> {
await wait(200)
const id = idgen()
return [
{ id: id(), title: '首页', path: '/mgr/home', icon: 'yzt' },
]
}
export function defaultHomePage() {
return '/mgr/home'
}
export async function currentBreadcrumbs() {
let url = window.location.href
let index = url.lastIndexOf('/')
let str = url.substring(index + 1, url.length)
let menuData: any = await loadMenu()
//console.log("menuData",menuData);
let breadcrumbsArray: any = []
menuData.map(function (item: any) {
if (item.children && item.children.length > 0) {
item.children.map(function (item1: any) {
if (item1.children && item1.children.length > 0) {
item1.children.map(function (item2: any) {
let index = item2.path.lastIndexOf('/')
if (str === item2.path.substring(index + 1, url.length)) {
breadcrumbsArray = [item.title, item1.title, item2.title]
}
})
} else {
let index = item1.path.lastIndexOf('/')
if (str === item1.path.substring(index + 1, url.length)) {
breadcrumbsArray = [item.title, item1.title]
}
}
})
}
})
return breadcrumbsArray
}

48
src/models/auth/index.ts Normal file
View File

@ -0,0 +1,48 @@
import { createModel } from "@rematch/core";
import { RootModel } from "..";
import { MenuItem } from "../_";
import { AuthState, getUserFromSession, loadMenu, LoginUser, regByToken, removeLoginInfo } from "./_";
export const auth = createModel<RootModel>()({
state: {
user: getUserFromSession(),
menu: [],
} as AuthState,
reducers: {
setUser(state, user: LoginUser): AuthState {
return { ...state, user };
},
setMenu(state, menu: MenuItem[]): AuthState {
return { ...state, menu };
},
},
effects: {
async regByToken(token: string): Promise<boolean> {
const result = await regByToken(token);
if (result) {
this.setUser(result);
return true;
}
this.setUser(-1);
return false;
},
logout() {
removeLoginInfo();
this.setUser(null);
},
async loadMenu() {
const data = await loadMenu();
this.setMenu(data);
}
}
});

32
src/models/auth/menu.ts Normal file
View File

@ -0,0 +1,32 @@
import { MenuItem } from "../_";
export function findMenu(menus: MenuItem[], pathname: string) {
if (!menus) {
return [];
}
for (const m1 of menus) {
if (m1.path === pathname) {
return [m1.id];
}
if (m1.children && m1.children.length) {
for (const m2 of m1.children) {
if (m2.path === pathname) {
return [m1.id, m2.id];
}
if (m2.children && m2.children.length) {
for (const m3 of m2.children) {
if (m3.path === pathname) {
return [m1.id, m2.id, m3.id];
}
}
}
}
}
}
return [];
}

10
src/models/index.ts Normal file
View File

@ -0,0 +1,10 @@
import { Models } from "@rematch/core"
import { auth } from "./auth";
export interface RootModel extends Models<RootModel> {
auth: typeof auth;
}
export const models: RootModel = {
auth,
};

12
src/models/store.ts Normal file
View File

@ -0,0 +1,12 @@
import { init, RematchDispatch, RematchRootState } from '@rematch/core'
import { models, RootModel } from './index'
export const store = init({
models,
})
export type Store = typeof store
export type Dispatch = RematchDispatch<RootModel>
export type RootState = RematchRootState<RootModel>
export default store

7
src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
/// <reference types="react-scripts" />
declare module '*.module.less' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.less'
declare module 'sm-crypto'

19
src/service/apiurl.js Normal file
View File

@ -0,0 +1,19 @@
const service = '/gunshiApp/ss'
const apiurl = {
login: {
login: service + '/login',
info: service + '/getInfo',
router: service + '/getRouters',
role: service + '/system/menu/list'
},
//核对完都删了重新组织
zrrgl: {
page: service + "/resPerson/page",
edit: service + "/resPerson/update",
delete: service + "/resPerson/del",
save:service + "/resPerson/insert",
},
}
export default apiurl

147
src/service/def.ts Normal file
View File

@ -0,0 +1,147 @@
import React from 'react';
export type PageResult<T> = {
list: T[];
totalRow: number;
}
export type SearchOption = {
pageSize?: number;
pageNumber?: number;
pageNum?: number;
search?: { [key: string]: any };
sortField?: string;
sortOrder?: 'asc' | 'desc';
stcd?:string
}
export type IdObject = {
id: string | number;
}
export type modeType = 'add' | 'edit' | 'view';
export type TreeNode = {
id: number;
key: React.Key;
parentId?: number;
parentName?: string;
title: string;
type?: number;
disabled?: boolean;
children: TreeNode[];
}
export type LoginUser = {
name: string;
token: string;
}
export type FileParam = {
id: number;
tablename: string;
}
// 附件类型定义
export type Attach = {
id?: number;
filepath: string;
usetype?: number;
filename?: string;
tablename?: string;
tableid?: string;
orderNum?: number;
filetime?: Date;
lng?: number;
lat?: number;
createById?: number;
createBy?: string;
createTime?: Date;
remark?: string;
del?: number;
}
export type ProjectLog = {
code: string;
name: string;
status: '筹备阶段' | '已开工' | '已竣工';
planStm: Date;
planEtm: Date;
actualStm: Date;
actualEtm: Date;
proprietor?: string;
operator?: string;
level:number;
projMgr: string;
projMgrName: string;
fieldMgr: string;
jldw: string;
sjdw: string;
zbwj: string[];
tbwj: string[];
zbtzs: string[];
contracts: string[];
attachments?: string[];
}
export type ScheduleProps = {
wbs: true,
level: '单项工程',
status: '未开工' | '已开工' | '已完工',
phase: '[C]项目实时阶段',
} | {
task: true;
type: string;
schedule: string;
calendar: string;
org?: string;
personel?: string;
};
export type Schedule = {
id: string;
name: string;
stm1: Date;
etm1: Date;
stm2?: Date;
etm2?: Date;
progress: number;
props: ScheduleProps;
remark?: string;
_indent: number;
children?: Schedule[] | undefined; // undefine为task否则就是wbs
}
export type WorkFlow = {
id?: string;
title: string;
versions?: string;
projectNo?: string;
status: string;
level: string;
categoryId: string;
categoryName?: string;
fromType: string;
workFlowName: string;
applyName: string;
userName: string;
applyDate: Date;
workflowId: string;
businessId: string;
createDate: Date;
deptId: string;
deptName: string;
startTime?: Date;
endTime?: Date;
del: number;
}

17
src/service/login.js Normal file
View File

@ -0,0 +1,17 @@
import { httppost, ry_httpget } from "../utils/request"
import apiUrl from '../service/apiurl'
export const login = async (params) => {
const res = await httppost(apiUrl.login.login, params)
return res
}
export const info = async (params) => {
const res = await ry_httpget(apiUrl.login.info, params)
return res
}
export const router = async (params) => {
const res = await ry_httpget(apiUrl.login.router, params)
return res
}

11
src/setupProxy.js Normal file
View File

@ -0,0 +1,11 @@
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
app.use(
'/gunshiApp/ss/',
createProxyMiddleware({
target: 'http://223.75.53.141:83/',//正式环境
changeOrigin: true,
})
);
};

71
src/utils/common.ts Normal file
View File

@ -0,0 +1,71 @@
export function wait(t?: number, data?: any): Promise<any> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(data);
}, t || 1000);
})
}
export function getBasePath(): string {
if (process.env.PUBLIC_URL.startsWith('http')) {
return process.env.PUBLIC_URL;
} else {
return window.location.origin + process.env.PUBLIC_URL;
}
}
export function parseGeoJSONFeature(data: any): any {
if (typeof data.lgtd === 'number' && typeof data.lttd === 'number') {
return {
type: "Feature",
properties: data,
geometry: {
type: "Point",
coordinates: [data.lgtd, data.lttd]
}
};
} else if (data.geometry) {
let geometryObj = data.geometry;
if (typeof geometryObj === 'string') {
try {
geometryObj = JSON.parse(geometryObj);
} catch (e) {
geometryObj = null;
}
}
if (geometryObj?.type) {
return {
type: "Feature",
properties: data,
geometry: geometryObj
}
}
} else {
return {
type: "Feature",
properties: data,
};
}
}
export function parseGeoJSON(data: any): any {
const ret: any = {
type: "FeatureCollection",
crs: {
type: "name",
properties: {
name: "urn:ogc:def:crs:OGC:1.3:CRS84"
}
},
features: []
};
if (Array.isArray(data)) {
ret.features = data.map(parseGeoJSONFeature);
}
return ret;
}

146
src/utils/request.js Normal file
View File

@ -0,0 +1,146 @@
import * as tools from './tools';
import { message } from 'antd';
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
const error = new Error(response.statusText);
error.response = response;
throw error;
}
function getToken() {
return localStorage.getItem('access_token');
}
function request(url, options,type) {
const opt = { ...options };
opt.headers = opt.headers || {};
opt.headers.Accept = 'application/json';
// opt.credentials = opt.credentials || 'include';
return fetch(url, opt)
.then(checkStatus)
.then(response => type=='blob'?response.blob():response.json())
.then(data => ({ data })) // { data: 接口实际返回的数据 }
.catch(err => ({ err })); // { err: 错误信息 }
}
async function send(url, options) {
try {
const res = await request(url, options);
if (!('data' in res) && 'err' in res) {
//message.error(getErrCode(res.err.message));
return null;
}
const { code } = res.data;
if (code === 401) {
//debugger;
// window.location.href = '/mgr/home';// /mgr/home /login
// window.location.href = '/xfflood/#/login';
window.location.hash = '#/login';
}
return res.data;
} catch (e) {
//message.error(e);
}
return {};
}
export function httpget(url, data = {}) {
const params = [];
for (const k in data) {
const v = data[k];
if (tools.strIsEmpty(v)) { continue; }
params.push(`${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
}
const strparams = params.join('&');
if (params.length > 0) {
if (url.indexOf('?') > 0) {
url += `&${strparams}`;
} else {
url += `?${strparams}`;
}
}
const options = {
method: 'GET',
headers: {
"gs-token": localStorage.getItem('access_token'),
},
};
return send(url, options);
}
//若依get请求
export function ry_httpget(url, data = {}) {
const params = [];
for (const k in data) {
const v = data[k];
if (tools.strIsEmpty(v)) { continue; }
params.push(`${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
}
const strparams = params.join('&');
if (params.length > 0) {
if (url.indexOf('?') > 0) {
url += `&${strparams}`;
} else {
url += `?${strparams}`;
}
}
const options = {
method: 'GET',
headers: {
"cookie":'Token=' + localStorage.getItem('access_token'),
"authorization":"Bearer" + ' ' + localStorage.getItem('access_token')
},
};
return send(url, options);
}
export function httppost(url, data = {}) {
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
"gs-token": localStorage.getItem('access_token'),
},
body: JSON.stringify(data),
};
return send(url, options);
}
export function httpPostFile(url, data = {}) {
const form = new FormData();
for (const k in data) {
const v = data[k];
if (tools.strIsEmpty(v)) { continue; }
form.append(k, data[k]);
console.log(k,data[k])
}
const token = getToken();
if (token) {
form.append('token', token);
}
const options = {
method: 'POST',
headers: {
// 'Content-Type': 'multipart/form-data',
"Content-Type": "application/json"
},
body: form,
};
return send(url, options);
}

583
src/utils/tools.js Normal file
View File

@ -0,0 +1,583 @@
import moment from 'moment';
import centerOfMass from '@turf/center-of-mass';
import bbox from '@turf/bbox';
import turfLength from '@turf/length';
import along from '@turf/along';
const class2type = {};
const { toString } = class2type;
'Boolean Number String Function Array Date RegExp Object Error'.split(' ').forEach((name) => {
class2type[`[object ${name}]`] = name.toLowerCase();
});
/**
* 对象类型
* @param {object} obj
*/
export function objType(obj) {
if (obj == null) {
return `${obj}`;
}
return typeof obj === 'object' || typeof obj === 'function' ? class2type[toString.call(obj)] || 'object' : typeof obj;
}
/**
* 是否数组
* @param {object} obj 需要判断的对象
* @return {boolean} 返回 true false
*/
export function isArray(obj) {
return objType(obj) === 'array';
}
/**
* 将简单结构数据转换成树状结构
* @param {object} setting 设置节点的 idKey\pIdKey\children
* @param {array} sNodes 数据数组
* @return {array} 树状解构数据
*/
export function transformToTreeFormat(setting, sNodes) {
setting = setting || {};
let i;
let l;
const key = setting.idKey || 'id'; // 当前节点的唯一key名
const parentKey = setting.pIdKey || 'pid'; // 指向的父节点唯一key名
const childKey = setting.children || 'children'; // 子元素的key名
if (!key || key === '' || !sNodes) return [];
if (isArray(sNodes)) {
const r = [];
const tmpMap = [];
for (i = 0, l = sNodes.length; i < l; i += 1) {
tmpMap[sNodes[i][key]] = sNodes[i];
}
for (i = 0, l = sNodes.length; i < l; i += 1) {
if (tmpMap[sNodes[i][parentKey]] && sNodes[i][key] !== sNodes[i][parentKey]) {
if (!tmpMap[sNodes[i][parentKey]][childKey]) {
tmpMap[sNodes[i][parentKey]][childKey] = [];
}
tmpMap[sNodes[i][parentKey]][childKey].push(sNodes[i]);
} else {
r.push(sNodes[i]);
}
}
return r;
}
return [sNodes];
}
/**
* 获取上级adcd
* @param {string} adcd 15
* @return {string} 上级行政区划 adcd 15
*/
export function getAdcdPid(adcd) {
if (!adcd || adcd.length !== 15) {
return '';
}
if (adcd === '429021000000000') { // 神农架
return '429021000000000';
} else if (adcd === '420821450000000') { // 屈家岭
return '420800000000000'; // 荆门
} else if (adcd === '420381450000000') { // 武当山
return '420300000000000'; // 十堰
} else if (/00000000000$/.test(adcd)) { // 市
return '420000000000000';
} else if (/000000000$/.test(adcd)) { // 县\区\县级市
return `${adcd.substring(0, 4)}00000000000`;
} else if (/000000$/.test(adcd)) { // 乡\镇
return `${adcd.substring(0, 6)}000000000`;
} else if (/000$/.test(adcd)) { // 行政村
return `${adcd.substring(0, 9)}000000`;
}
return `${adcd.substring(0, 12)}000`; // 自然村
}
/**
* 根据行政区划设定放缩至的级别
* @param {string} adcd 行政区划
*/
export function getAdcdZoom(adcd) {
if (!adcd || adcd.length !== 15) {
return -1;
}
if (adcd === '420821450000000') { // 屈家岭
return 3;
} else if (adcd === '420381450000000') { // 武当山
return 3;
} else if (/00000000000$/.test(adcd)) { // 市
return 2;
} else if (/000000000$/.test(adcd)) { // 县\区\县级市
return 3;
} else if (/000000$/.test(adcd)) { // 乡\镇
return 4;
} else if (/000$/.test(adcd)) { // 行政村
return 5;
}
return 6; // 自然村
}
/**
* int 属性值排序
* @param {object} o1
* @param {object} o2
* @param {string} key
*/
export function orderByInt(o1, o2, key) {
const v1 = o1[key] || 0;
const v2 = o2[key] || 0;
if (parseInt(v1, 10) >= parseInt(v2, 10)) {
return 1;
}
return -1;
}
/**
* 检查用户是否登陆
*/
export function checkUL() {
return true;
// const token = cookies.get('token');
// const uid = cookies.get('uid');
// return token != null && uid != null;
}
export function isEmptyObject(e) {
if (e === undefined || e === null) {
return true;
}
for (const t in e) {
return false;
}
return true;
}
export function strIsEmpty(v) {
// return v === undefined || v == null || v === '';
return v === undefined;
}
export function complexUrlParams(resultObj, key, obj) {
if (!resultObj) {
resultObj = {};
}
if (typeof obj !== 'object') {
resultObj[key] = obj;
return resultObj;
}
for (const subkey in obj) {
complexUrlParams(resultObj, `${key}[${subkey}]`, obj[subkey]);
}
return resultObj;
}
export function arr2map(arrData) {
if (!Array.isArray(arrData)) {
return null;
}
const ret = {};
for (const str of arrData) {
ret[str] = true;
}
return ret;
}
export function map2arr(mapData) {
if (!mapData) {
return null;
}
const ret = [];
for (const key in mapData) {
if (mapData[key]) {
ret.push(key);
}
}
return ret;
}
export function isDebug() {
return window.location.host.indexOf('dev') > 0 || window.location.host.indexOf('localhost') >= 0;
}
export function normalizeSearchTmRange(tm, type) {
if (!tm || !tm[0] || !tm[1]) {
return undefined;
}
if (tm[0] instanceof Date) {
tm[0] = moment(tm[0]);
}
if (tm[1] instanceof Date) {
tm[1] = moment(tm[1]);
}
if (type === 'd') {
return [
tm[0].format('YYYY-MM-DD 00:00:00'),
tm[1].format('YYYY-MM-DD 00:00:00'),
];
}
if (type === 'm') {
return [
tm[0].format('YYYY-MM-01 00:00:00'),
tm[1].format('YYYY-MM-01 00:00:00'),
];
}
if (type === 't') {
return [
tm[0].format('YYYY-MM-01 00:00:00'),
tm[1].format('YYYY-MM-21 00:00:00'),
];
}
if (type === 'y') {
return [
tm[0].format('YYYY-01-01 00:00:00'),
tm[1].format('YYYY-01-01 00:00:00'),
];
}
if (type === 'h') {
return [
tm[0].format('YYYY-MM-DD HH:00:00'),
tm[1].format('YYYY-MM-DD HH:00:00'),
];
}
return [
tm[0].format('YYYY-MM-DD HH:mm:00'),
tm[1].format('YYYY-MM-DD HH:mm:00'),
];
}
export function normalizeRzMinMax_Dangyang(min, max) {
let max50cm = Math.ceil(max * 2);
let min50cm = Math.floor(min * 2);
let av50cm = Math.ceil(max + min);
if (av50cm < 3) {
av50cm = 3;
}
if (min50cm < 0) {
min50cm = 0
}
if (max50cm < av50cm + 3) {
max50cm = av50cm + 3;
}
if (min50cm > av50cm - 3) {
min50cm = av50cm - 3;
}
return {
min: min50cm / 2,
max: max50cm / 2,
}
}
export const MomentTypeFromTmType = {
h: 'hour',
m: 'month',
d: 'day',
}
export function parseGeoJSONFeature(data) {
if (typeof data.lgtd === 'number' && typeof data.lttd === 'number') {
return {
type: "Feature",
properties: data,
geometry: {
type: "Point",
coordinates: [data.lgtd, data.lttd]
}
};
} else if (data.geometry) {
let geometryObj = data.geometry;
if (typeof geometryObj === 'string') {
try {
geometryObj = JSON.parse(geometryObj);
} catch (e) {
geometryObj = null;
}
}
if (geometryObj?.type) {
return {
type: "Feature",
properties: data,
geometry: geometryObj
}
}
}
return null;
}
export function parseGeoJSON(data) {
const ret = {
type: "FeatureCollection",
name: "drpreal",
crs: {
type: "name",
properties: {
name: "urn:ogc:def:crs:OGC:1.3:CRS84"
}
},
features: []
};
if (Array.isArray(data)) {
ret.features = data.map(parseGeoJSONFeature).filter(Boolean);
}
return ret;
}
export function wait(tm) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, tm || 0);
});
}
export function strNumber(val, def) {
if (typeof val === "number") {
return val;
}
return def;
}
export function KrM100Render(val) {
if (typeof val !== "number") {
return '-';
}
return (val * 100).toFixed(2);
}
/**
* 深度优先遍历树
* 一个递归方法
* @params tree: 要转换的树结构数据
* @params list: 保存结果的列表结构数据初始传list = []
* @params parentId: 当前遍历节点的父级节点id初始为null(因为根节点无parentId)
**/
function toListDF(tree, list) {
for (let node of tree) { //遍历最上层
list.push({
id: node.id,
key: node.key,
title: node.title,
type: node?.type,
parentId: node?.parentId
});
//如果有子结点,再遍历子结点
if (node.children.length !== 0) {
toListDF(node.children, list) //递归
}
}
}
/**
* 树转list
*/
export function treeToList(tree){
const list = []; // 结果list
for(let node of tree){
if (node?.children?.length !== 0) { //遍历树的第一层,只有一个根结点
toListDF(node.children, list); //遍历子树,并加入到list中.
}
}
return list;
}
/**
* 将时间戳转换成年月日
*/
export function timeChange(val){
let time = moment(val).format()
let d = new Date(time);
let batchDate = d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds();
return batchDate;
}
function childNodesDeep(nodes, arr) {
if (nodes)
nodes.forEach((ele) => {
arr.push(ele.key);
if (ele.children) {
childNodesDeep(ele.children, arr);
}
});
}
/**
* 获取父节点下所有子节点的key
* @param nodes
* @param key
* @param arr
* @returns {Array}
*/
export function getChildKysByParentKey(nodes, key, arr) {
for (let el of nodes) {
if (el.key === key) {
arr.push(el.key);
if (el.children) {
childNodesDeep(el.children, arr);
}
} else if (el.children) {
getChildKysByParentKey(el.children, key, arr);
}
}
return arr;
}
export function geometryCenter(geometry) {
if (!geometry) {
return null;
}
try {
if (typeof geometry === 'string') {
geometry = JSON.parse(geometry);
}
// todo
if (geometry.type === 'MultiLineString') {
geometry = {
type: 'LineString',
coordinates: geometry.coordinates[0]
}
} else if (geometry.type === 'MultiPolygon') {
geometry = {
type: 'Polygon',
coordinates: geometry.coordinates[0]
}
}
let ptgeom = null;
if (geometry.type === 'Polygon') {
ptgeom = centerOfMass(geometry);
} else if (geometry.type === 'LineString') {
const length = turfLength(geometry);
ptgeom = along(geometry, length * 0.5);
}
return ptgeom?.geometry?.coordinates;
} catch (e) { return null; }
}
export function geometryBound(geometry) {
if (!geometry) {
return null;
}
try {
if (typeof geometry === 'string') {
geometry = JSON.parse(geometry);
}
const result = bbox(geometry);
return result;
} catch (e) { return null; }
}
export const dealValue = (data = [])=>{
let linshiObj = {}
data?.forEach((item)=>{
linshiObj[item.value]=item.label
})
return linshiObj
}
export const ArrToObj = (data = [])=>{
let linshiObj = {}
data?.forEach((item)=>{
linshiObj[item.value]=item.label
})
return linshiObj
}
//前端筛选
export const myFiltrate = (data,params)=>{
const {bgTm,endTm,code,dataSource,ly} = params
const myBgTm = moment(bgTm).format('YYYY-MM-DD')+" 00:00:00"
const myEndTm = moment(endTm).format('YYYY-MM-DD')+" 23:59:59"
const newData = []
data?.map((item,index)=>{
let flag = 0
if(bgTm){
if(item?.tm && moment(bgTm).isBefore(item.tm)&&moment(item.tm).isBefore(myEndTm)){
}else{
flag = flag+1
}
}
if(code){
const str = (item.stnm?item.stnm:"")+(item.stcd?item.stcd:"")+(item.wscd?item.wscd:"")
if(str.indexOf(code)>(-1)){
}else{
flag = flag+1
}
}
if(dataSource){
if(item?.type&&item?.type===dataSource){
}else{
flag = flag+1
}
}
if(ly){
if(item?.wscd&&item?.wscd.indexOf(ly)>0){
}else{
flag = flag+1
}
}
if(flag === 0){
newData.push(item)
}
})
return newData
}
export const GetInterval=(min,max,step=10)=>{
const distance = Math.ceil(max) - Math.floor(min)
console.log(distance);
let s = (distance / step);
console.log(s);
if (s > 5){
s = Math.ceil(s / 5) *5;
return s;
}else{
return Math.ceil(s)
}
}
//获取url里key
export function getParameter(param) {
var query = window.location.hash;
var iLen = param.length;
var iStart = query.indexOf(param);
if (iStart == -1)
return "";
iStart += iLen + 1;
var iEnd = query.indexOf("&", iStart);
if (iEnd == -1)
return query.substring(iStart);
return query.substring(iStart, iEnd);
}

102
src/utils/utils.ts Normal file
View File

@ -0,0 +1,102 @@
import moment from "moment";
export function changeObjectStringToMoment(obj: { [key: string]: any }, fields: string[]): any {
const ret = { ...obj };
for (const f of fields) {
if (f in ret && typeof ret[f] === 'string') {
ret[f] = moment(ret[f]);
}
}
return ret;
}
export function apertureMeter(val?: any): number | undefined {
if (typeof val !== 'number') {
return undefined
}
return val / 1000;
}
export function renAperture(val?: any) {
if (typeof val !== 'number') {
return '-'
}
return (val / 1000).toFixed(3);
}
export async function base64FromFile(file: File): Promise<string | undefined> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const str = reader.result;
if (typeof str === 'string') {
resolve(str);
}
resolve(undefined);
}
reader.onerror = () => {
resolve(undefined);
}
})
}
export const normFile = (e: any) => {
if (Array.isArray(e)) {
return e;
}
return e?.fileList;
};
export function randNum(a: number, b: number) {
const ret = a * Math.random() + b;
return parseFloat(ret.toFixed(2));
}
export function getParameter(param: string) {
var query = window.location.hash;
var iLen = param.length;
var iStart = query.indexOf(param);
if (iStart == -1)
return "";
iStart += iLen + 1;
var iEnd = query.indexOf("&", iStart);
if (iEnd == -1)
return query.substring(iStart);
return query.substring(iStart, iEnd);
}
export function toFixed3(val: any) {
if (typeof val !== 'number') {
return '-';
}
return val.toFixed(3);
}
export function renTm(val: any) {
if (typeof val !== 'number') {
return '-'
}
return moment(val).format('MM-DD HH:mm:ss')
}
// 驼峰命名转下划线命名
export const camel2UnderLine = (str: string) => {
let newStr: string = str.replace(/[A-Z]/g,($0) => {
return "_" + $0.toLocaleLowerCase();
});
if(newStr.slice(0,1) == "_"){
newStr = newStr.slice(1)
}
return newStr;
};
// 验证图片
export const isImg = (file: string) => {
return /\.(gif|jpg|jpeg|png|GIF|JPEG|JPG|PNG)$/.test(file);
};

26
src/views/AppRouters.tsx Normal file
View File

@ -0,0 +1,26 @@
import React, { lazy, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { Navigate, useRoutes } from 'react-router'
import { Dispatch } from '../models/store'
import LoginPage from './Login/index'
const HomePage = lazy(() => import('./Home'))
const AppRouters: React.FC = () => {
const dispatch = useDispatch<Dispatch>()
useEffect(() => {
;(window as any).__dispatch__ = dispatch
return () => {
delete (window as any).__dispatch__
}
}, [dispatch])
let element = useRoutes([
{ path: '/', element: <LoginPage /> },
{ path: '/home', element: <HomePage /> },
])
return element
}
export default AppRouters

View File

@ -0,0 +1,43 @@
import React from 'react';
import CommonCard from '../../UI/CommonCard';
import YearSelect from '../../UI/YearSelect';
import './index.less';
const SiGuan = () => {
return (
<div className="siguan-view">
<div className="side-panel left">
<CommonCard title="安全鉴定" className="panel-card card-1" >
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
<CommonCard title="库区巡查" className="panel-card card-2" style={{minHeight:120}}>
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
<CommonCard title="闸门监控" className="panel-card card-3">
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
<CommonCard
title="安全隐患"
className="panel-card card-1"
headerExtra={<YearSelect />}
>
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
</div>
<div className="side-panel right">
<CommonCard title="水库划界" className="panel-card card-1">
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
<CommonCard title="白蚁防治" className="panel-card card-2">
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
<CommonCard title="维修养护" className="panel-card card-3">
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
</div>
</div>
);
};
export default SiGuan;

View File

@ -0,0 +1,47 @@
.siguan-view {
width: 100%;
height: 100%;
position: relative;
.side-panel {
position: absolute;
top: 20px;
bottom: 20px;
width: 25%;
min-width: 300px;
display: flex;
flex-direction: column;
z-index: 10;
transition: all 0.3s;
pointer-events: none;
&.left {
left: 20px;
}
&.right {
right: 20px;
}
> * {
pointer-events: auto;
}
}
.panel-card {
flex: 1;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.placeholder-content {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: rgba(255, 255, 255, 0.5);
}
}

View File

@ -0,0 +1,54 @@
import React from 'react';
import CommonCard from '../../UI/CommonCard';
import ThreeDots from '../../UI/ThreeDots';
import './index.less';
const LeftPart = () => {
return (
<div className="left-part">
<CommonCard title="监管全覆盖" className="panel-card card-1">
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
<CommonCard
title="监测全要素"
className="panel-card card-2"
headerExtra={<ThreeDots onClick={() => console.log('监测全要素 clicked')} />}
>
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
</div>
);
};
const RightPart = () => {
return (
<div className="right-part">
<CommonCard
title="管控全天候"
className="panel-card card-1"
headerExtra={<ThreeDots onClick={() => console.log('管控全天候 clicked')} />}
>
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
<CommonCard title="管理全周期" className="panel-card card-3">
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
</div>
);
};
const SiQuan = () => {
return (
<div className="siquan-view">
<div className="side-panel left">
<LeftPart />
</div>
<div className="side-panel right">
<RightPart />
</div>
</div>
);
};
export default SiQuan;

View File

@ -0,0 +1,75 @@
.siquan-view {
width: 100%;
height: 100%;
position: relative; // Establish positioning context
.side-panel {
position: absolute;
top: 20px;
bottom: 20px;
width: 25%; // Or use vw
min-width: 300px;
display: flex;
flex-direction: column;
z-index: 10; // Ensure panels are above map
transition: all 0.3s;
pointer-events: none; // Allow clicks to pass through transparent areas if any
&.left {
left: 20px;
}
&.right {
right: 20px;
}
// Re-enable pointer events for the cards themselves
> * {
pointer-events: auto;
}
}
// Responsive adjustments
@media screen and (max-width: 1366px) {
gap: 10px;
.side-panel {
flex: 0 0 28%;
}
}
.left-part, .right-part {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0; // Padding handled by parent or margin in cards
box-sizing: border-box;
.panel-card {
margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
}
}
.left-part {
.card-1 { flex: 4; }
.card-2 { flex: 5; }
}
.right-part {
.card-1 { flex: 4; }
.card-2 { flex: 3; }
.card-3 { flex: 3; }
}
.placeholder-content {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: rgba(255, 255, 255, 0.5);
}
}

View File

@ -0,0 +1,58 @@
import React from 'react';
import CommonCard from '../../UI/CommonCard';
import ThreeDots from '../../UI/ThreeDots';
import './index.less';
const WarningToggles = () => {
return (
<div className="warning-toggles">
<div className="toggle-item active">
<span>监测预警</span>
<span className="badge">4</span>
</div>
<div className="toggle-item">
<span>预报预警</span>
<span className="badge bg-red">2</span>
</div>
</div>
)
}
const SiYu = () => {
return (
<div className="siyu-view">
<div className="side-panel left">
<CommonCard title="预报" className="panel-card card-1">
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
<CommonCard title="预演" className="panel-card card-2">
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
</div>
<div className="side-panel right">
<CommonCard
title="预警"
className="panel-card card-1"
headerExtra={<WarningToggles />}
>
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
<CommonCard
title="实时水雨情"
className="panel-card card-2"
headerExtra={<ThreeDots onClick={() => console.log('实时水雨情 clicked')} />}
>
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
<CommonCard title="预案" className="panel-card card-3">
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
</div>
</div>
);
};
export default SiYu;

View File

@ -0,0 +1,82 @@
.siyu-view {
width: 100%;
height: 100%;
position: relative;
.side-panel {
position: absolute;
top: 20px;
bottom: 20px;
width: 25%;
min-width: 300px;
display: flex;
flex-direction: column;
z-index: 10;
transition: all 0.3s;
pointer-events: none;
&.left {
left: 20px;
}
&.right {
right: 20px;
}
> * {
pointer-events: auto;
}
}
.panel-card {
flex: 1;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.placeholder-content {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: rgba(255, 255, 255, 0.5);
}
// Warning Toggles Styles
.warning-toggles {
display: flex;
gap: 15px;
font-size: 14px;
.toggle-item {
display: flex;
align-items: center;
color: rgba(255,255,255,0.7);
cursor: pointer;
&.active {
color: #00ffff;
font-weight: bold;
}
.badge {
display: inline-block;
background: #faad14; // Orange for normal warning
color: #fff;
border-radius: 50%;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
font-size: 12px;
margin-left: 5px;
&.bg-red {
background: #f5222d; // Red for urgent
}
}
}
}
}

View File

@ -0,0 +1,29 @@
import React from 'react';
import CommonCard from '../../UI/CommonCard';
import './index.less';
const SiZhi = () => {
return (
<div className="sizhi-view">
<div className="side-panel left">
<CommonCard title="完善体系" className="panel-card card-1">
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
<CommonCard title="健全机制" className="panel-card card-2">
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
</div>
<div className="side-panel right">
<CommonCard title="强化法治" className="panel-card card-1">
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
<CommonCard title="落实责任制" className="panel-card card-2">
<div className="placeholder-content">内容填充区域</div>
</CommonCard>
</div>
</div>
);
};
export default SiZhi;

View File

@ -0,0 +1,46 @@
.sizhi-view {
width: 100%;
height: 100%;
position: relative;
.side-panel {
position: absolute;
top: 20px;
bottom: 20px;
width: 25%;
min-width: 300px;
display: flex;
flex-direction: column;
z-index: 10;
transition: all 0.3s;
pointer-events: none;
&.left {
left: 20px;
}
&.right {
right: 20px;
}
> * {
pointer-events: auto;
}
}
.panel-card {
flex: 1;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.placeholder-content {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: rgba(255, 255, 255, 0.5);
}
}

View File

@ -0,0 +1,77 @@
import React, { useState, useEffect } from 'react';
import moment from 'moment';
import './index.less';
const Header = ({ activeMenu, onMenuChange }) => {
const [time, setTime] = useState(moment());
useEffect(() => {
const timer = setInterval(() => {
setTime(moment());
}, 1000);
return () => clearInterval(timer);
}, []);
const weekMap = {
1: '星期一',
2: '星期二',
3: '星期三',
4: '星期四',
5: '星期五',
6: '星期六',
0: '星期日'
};
const menus = [
{ key: 'siquan', title: '四全管理' },
{ key: 'sizhi', title: '四制体系' },
{ key: 'siyu', title: '四预措施' },
{ key: 'siguan', title: '四管工作' },
];
return (
<div className="dashboard-header">
<div className="left-area">
<div className="time-info">
<img src={require('../../../../../assets/images/head/time.png')} alt="time" className="icon" />
<span className="time-text">{time.format('YYYY-MM-DD HH:mm:ss')} {weekMap[time.day()]}</span>
</div>
</div>
<div className="center-area">
<div className="menu-group left-menus">
{menus.slice(0, 2).map(menu => (
<div
key={menu.key}
className={`menu-item1 ${activeMenu === menu.key ? 'active' : ''}`}
onClick={() => onMenuChange(menu.key)}
>
{menu.title}
</div>
))}
</div>
<div className="header-title">双石水库现代化运行管理矩阵平台</div>
<div className="menu-group right-menus">
{menus.slice(2, 4).map(menu => (
<div
key={menu.key}
className={`menu-item2 ${activeMenu === menu.key ? 'active' : ''}`}
onClick={() => onMenuChange(menu.key)}
>
{menu.title}
</div>
))}
</div>
</div>
<div className="right-area">
<div className="user-info">
<img src={require('../../../../../assets/images/head/user.png')} alt="user" className="icon" />
<span className="user-text">当前用户: 系统管理员</span>
</div>
</div>
</div>
);
};
export default Header;

View File

@ -0,0 +1,110 @@
.dashboard-header {
width: 100%;
height: 80px;
background: url('../../../../../assets/images/head/header.png') no-repeat center top;
background-size: 100% 100%;
display: flex;
justify-content: space-between;
align-items: center; // Center vertically
padding: 0 20px; // Horizontal padding
position: relative;
z-index: 10;
box-sizing: border-box;
.left-area, .right-area {
position: absolute;
top: 0px;
z-index: 2;
height: 40px;
display: flex;
align-items: center;
white-space: nowrap; // Prevent text wrapping
}
.left-area {
left: 20px;
justify-content: flex-start;
}
.right-area {
right: 20px;
justify-content: flex-end;
}
.time-info, .user-info {
display: flex;
align-items: center;
color: #fff;
font-size: 14px;
padding: 5px 15px;
.icon {
width: 16px;
height: 16px;
margin-right: 8px;
}
.time-text, .user-text {
font-family: 'Arial', sans-serif;
font-weight: 500;
}
}
.center-area {
width: 100%; // Take full width
display: flex;
justify-content: center;
align-items: center;
height: 100%;
margin-top: 10px;
.header-title {
font-size: 32px;
color: #fff;
font-weight: bold;
letter-spacing: 2px;
text-shadow: 0 0 10px rgba(0, 200, 255, 0.8);
text-align: center;
white-space: nowrap;
margin: 0 10vw; // Increased spacing to push menus further outward
margin-top: -15px;
}
.menu-group {
display: flex;
gap: 50px;
margin-top: 25px; // Move menus further down
.menu-item1, .menu-item2{
width: 180px; // Approx width from image
height: 40px;
line-height: 40px;
text-align: center;
color: #fff;
font-size: 16px;
cursor: pointer;
background: url('../../../../../assets/images/head/menu1.png') no-repeat center;
background-size: 100% 100%;
transition: all 0.3s;
&:hover {
filter: brightness(1.2);
}
&.active {
background: url('../../../../../assets/images/head/menu2.png') no-repeat center;
background-size: 100% 100%;
}
}
.menu-item2{
background: url('../../../../../assets/images/head/menu3.png') no-repeat center;
background-size: 100% 100%;
&.active {
background: url('../../../../../assets/images/head/menu4.png') no-repeat center;
}
}
}
}
}

View File

@ -0,0 +1,12 @@
import React from 'react';
import './index.less';
const MapContainer = () => {
return (
<div className="map-container-view">
<div className="map-placeholder"></div>
</div>
);
};
export default MapContainer;

View File

@ -0,0 +1,19 @@
.map-container-view {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0; // Map is the background layer
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.2); // Keep slight tint or remove
.map-placeholder {
font-size: 24px;
color: rgba(0, 160, 233, 0.5);
font-weight: bold;
}
}

View File

@ -0,0 +1,18 @@
import React from 'react';
import './index.less';
const CommonCard = ({ title, children, style, className, headerExtra }) => {
return (
<div className={`common-card ${className || ''}`} style={style}>
<div className="card-header">
<span className="card-title">{title}</span>
{headerExtra && <div className="header-extra">{headerExtra}</div>}
</div>
<div className="card-content">
{children}
</div>
</div>
);
};
export default CommonCard;

View File

@ -0,0 +1,40 @@
.common-card {
display: flex;
flex-direction: column;
margin-bottom: 20px;
.card-header {
height: 30px;
background: url('../../../../../assets/images/card/header.png') no-repeat left center;
background-size: 100% 100%;
display: flex;
align-items: center;
justify-content: space-between; // Push extra to right
padding-left: 20px;
padding-right: 10px; // Add right padding
margin-bottom:5px;
.card-title {
color: #fff;
font-size: 16px;
font-weight: bold;
font-style: italic;
// text-shadow: 0 0 5px #00eaff;
}
.header-extra {
display: flex;
align-items: center;
z-index: 2;
}
}
.card-content {
flex: 1;
background: rgba(0, 0, 0, 0.4); // Fallback or overlay
padding: 10px;
// min-height: 200px; // Remove fixed min-height to allow custom sizing
color: #fff;
overflow: hidden; // Prevent overflow
}
}

View File

@ -0,0 +1,21 @@
import React from 'react';
const ThreeDots = ({ onClick, style, className }) => (
<div
className={className}
style={{
cursor: 'pointer',
color: '#fff',
fontSize: '20px',
lineHeight: '10px',
letterSpacing: '2px',
marginRight: '15px',
...style
}}
onClick={onClick}
>
</div>
);
export default ThreeDots;

View File

@ -0,0 +1,22 @@
import React from 'react';
import { Select } from 'antd';
import './index.less';
const { Option } = Select;
const YearSelect = ({ defaultValue = "2025", style, className, ...props }) => (
<Select
defaultValue={defaultValue}
style={{ width: 80, color: '#fff',marginRight:15,marginTop:5, ...style }}
bordered={false}
dropdownClassName="year-select-dropdown"
className={`year-select ${className || ''}`}
{...props}
>
<Option value="2025">2025</Option>
<Option value="2024">2024</Option>
<Option value="2023">2023</Option>
</Select>
);
export default YearSelect;

View File

@ -0,0 +1,23 @@
.year-select {
color: #fff;
.ant-select-selector {
color: #fff !important;
background-color: transparent !important;
}
.ant-select-arrow {
color: #fff;
}
}
// Global styles for dropdown (since it renders in body)
.year-select-dropdown {
background-color: rgba(0, 20, 50, 0.9) !important;
border: 1px solid rgba(0, 160, 233, 0.3);
.ant-select-item {
color: #fff;
&:hover, &.ant-select-item-option-selected {
background-color: rgba(0, 160, 233, 0.3);
}
}
}

41
src/views/Home/index.js Normal file
View File

@ -0,0 +1,41 @@
import React, { useState } from 'react';
import Header from './components/Layouts/Header';
import MapContainer from './components/Map/MapContainer';
import SiQuan from './components/Business/SiQuan';
import SiZhi from './components/Business/SiZhi';
import SiYu from './components/Business/SiYu';
import SiGuan from './components/Business/SiGuan';
import './index.less';
const HomePage = () => {
const [activeMenu, setActiveMenu] = useState('siquan');
const renderContent = () => {
switch (activeMenu) {
case 'siquan':
return <SiQuan />;
case 'sizhi':
return <SiZhi />;
case 'siyu':
return <SiYu />;
case 'siguan':
return <SiGuan />;
default:
return <SiQuan />;
}
};
return (
<div className="home-page">
<Header activeMenu={activeMenu} onMenuChange={setActiveMenu} />
<div className="main-content-wrapper">
<MapContainer />
<div className="content-overlay">
{renderContent()}
</div>
</div>
</div>
)
}
export default HomePage;

52
src/views/Home/index.less Normal file
View File

@ -0,0 +1,52 @@
.home-page {
width: 100vw;
height: 100vh;
background-color: #a2b7cc;
overflow: hidden;
display: flex;
flex-direction: column;
color: #fff;
font-family: "Microsoft YaHei", sans-serif;
.main-content-wrapper {
flex: 1;
overflow: hidden;
padding: 0;
position: relative;
.content-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 5; // Content sits above the map
pointer-events: none; // Let clicks pass through to map by default
// Re-enable pointer events for actual interactive children
> * {
pointer-events: auto;
}
}
}
.content-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10; // Above map
pointer-events: none; // Let clicks pass through to map by default
// But children (cards) must be clickable.
// This will be handled in child components or we can set it here if all children are full-screen overlays
// It's safer to handle in children, but for now, since SiQuan is the child:
> * {
pointer-events: none;
width: 100%;
height: 100%;
}
}
}

BIN
src/views/Login/img/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
src/views/Login/img/bg1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

BIN
src/views/Login/img/bgN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

117
src/views/Login/index.less Normal file
View File

@ -0,0 +1,117 @@
.login-root {
height: 100vh;
background: url(./img/bgTest.png) 0 0 no-repeat;
background-size:100% 100%;
display: flex;
justify-content: end;
.login-inner {
margin-top: 60px;
width: 27%;
margin-right: 196px;
display: flex;
flex-direction: row-reverse;
align-items: stretch;
}
.login-form {
display: flex;
width: 91%;
flex-direction: column;
align-items: center;
// background-color: rgba(255, 255, 255, .3);
}
.loginFormBox{
background-color: rgba(255, 255, 255, .5);
width: 100%;
// height: 60%;
border-radius: 6px;
padding: 10px 20px 40px 10px;
// margin-bottom: 30px;
box-sizing: border-box;
margin-top:150px;
.loginName{
font-family: Adobe Heiti Std;
font-weight: normal;
font-size: 25px;
color: #01295F;
margin: 5% 0 10% 0;
text-align: center;
}
.ant-form-item-label > label{
color:#333;
font-size:18px;
letter-spacing: 1px;
height:35px;
display:none;
}
.ant-form-item-control-input{
width:90%;
height: 20%;
margin:0 auto;
.ant-form-item-control-input-content{
height: 20%;
border-radius: 15px;
.ant-input-affix-wrapper{
height: 100%;
border-radius: 15px;
}
.ant-input{
// height: 100%;
border-radius: 15px;
}
}
}
.ant-input,.ant-input-affix-wrapper{
padding:0 10px;
font-size:16px;
height:60px;
line-height: 38px;
background:#fff;
}
.ant-input-affix-wrapper{
height:42px;
}
.ant-btn-primary{
width:100%;
background:#409EFF;
border-color:#409EFF;
height:44px;
line-height:20px;
border-radius:4px;
margin-top:10px;
}
.ant-btn > span{
font-size:15px;
letter-spacing: 1px;
}
}
.login-title {
font-size: 32px;
font-family: Microsoft YaHei;
font-weight: 600;
color: #307DCC;
// background: linear-gradient(to right,#EFF4F8,#EFF4F8,#EFF4F8);
background: linear-gradient(to right, #EFF4F8,#A4D6FF,#EFF4F8);
margin-bottom: 30px;
}
.ant-input {
line-height: 3;
}
input:-webkit-autofill {
transition: background-color 5000s ease-in-out 0s;
}
input {
-webkit-text-fill-color: #606266; //颜色是设置成你需要的颜色
}
}

140
src/views/Login/index.tsx Normal file
View File

@ -0,0 +1,140 @@
import { Button, Form, Input, message, Checkbox } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router';
import { Dispatch } from '../../models/store';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
import './index.less';
// @ts-ignore
import cookie from 'react-cookies'
import { login, info } from '../../service/login'
const ReactLazyPreload = (importStatement: any) => {
const Component: any = React.lazy(importStatement);
Component.preload = importStatement;
return Component;
};
// const Home = ReactLazyPreload(() =>
// import("../Home")
// );
const LoginPage = () => {
const dispatch = useDispatch<Dispatch>();
const navigate = useNavigate();
const [checked, setChecked] = useState(false);
const formRef = useRef<any>(null)
// useEffect(() => {
// Home.preload();
// }, []);
const doLogin = async (value: any) => {
const params = {
password: value.password,
username: value.username
}
const result = await login(params)
if (result.code == 200) {
localStorage.setItem('access_token', result.token);
cookie.save('Admin-Token', result.token);
localStorage.setItem('expires_in', result.token);
localStorage.setItem('password', params.password);
info().then(res => {
getInfo(res,value)
})
}else{
message.error((result.description).replace('参数错误',''));
}
}
const getInfo=(result1:any,value:any)=>{
if (result1.code === 200) {
if(checked){
localStorage.setItem('loginNamePwd',JSON.stringify(value))
// localStorage.setItem('checked',checked)
}else{
localStorage.removeItem('loginNamePwd')
}
localStorage.setItem('userName', result1.user.nickName);
localStorage.setItem('userId', result1.user.userId);
navigate('/home');
}else{
message.error('获取用户信息失败');
}
}
const setCheckedChange=(e:any)=>{
let ischecked = e.target.checked
localStorage.setItem('checked',JSON.stringify(ischecked));
setChecked(ischecked)
}
useEffect(()=>{
// localStorage.clear()
// sessionStorage.clear()
cookie.remove('Admin-Token')
let loginNamePwd = localStorage.getItem('loginNamePwd')
let cc = localStorage.getItem('checked')
let checked = cc && JSON.parse(cc)
if(loginNamePwd && formRef.current && checked){
formRef.current?.setFieldsValue(JSON.parse(loginNamePwd))
setChecked(Boolean(checked))
// doLogin(JSON.parse(loginNamePwd))
}
},[])
return (
<div className='login-root'>
<div className='login-inner'>
<div className='login-form'>
<div style={{ textAlign: 'center', marginBottom: 2 }}>
{/* <div className='login-title'>咸丰县防汛调度系统</div> */}
</div>
<div className="loginFormBox">
<div className='loginName'></div>
<div>
<Form
name="basic"
ref={formRef}
initialValues={{ remember: true }}
labelCol={{ span: 0 }}
wrapperCol={{ span: 24 }}
onFinish={doLogin}
autoComplete="off"
layout="vertical"
>
<Form.Item
name="username"
rules={[{ required: true, message: '请输入用户名!' }]}
>
<Input
prefix={<UserOutlined className="site-form-item-icon" style={{ fontSize: '32px', color: '#3D92FF' }} />}
placeholder="请输入账号" />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: '请输入密码!' }]}
>
<Input.Password
prefix={<LockOutlined className="site-form-item-icon" style={{ fontSize: '32px', color: '#3D92FF' }} />}
placeholder="请输入密码" />
</Form.Item>
<div style={{ display: 'flex', justifyContent: 'end', margin: '32px' }}>
<Checkbox checked={checked} onClick={setCheckedChange}></Checkbox>
</div>
<div style={{ width: '90%', margin: '0 auto' }}>
<Button type="primary" style={{borderRadius:25,background:'linear-gradient(to right, #9BC1FF,#1E9DFF)'}} size="large" htmlType="submit" shape="round">
</Button>
</div>
</Form>
</div>
</div>
</div>
</div>
</div>
)
}
export default LoginPage

27
tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}