peng.peng
2 years ago
93 changed files with 25678 additions and 0 deletions
@ -0,0 +1,19 @@ |
|||||
|
{ |
||||
|
"presets": [ |
||||
|
"@babel/preset-react", |
||||
|
"@babel/preset-env" |
||||
|
|
||||
|
], |
||||
|
"plugins": [ |
||||
|
"@babel/plugin-proposal-class-properties", |
||||
|
"@babel/plugin-proposal-object-rest-spread", |
||||
|
["import", { |
||||
|
"libraryName": "antd", |
||||
|
"libraryDirectory": "es" |
||||
|
|
||||
|
}] |
||||
|
], |
||||
|
"env": { |
||||
|
"development": {} |
||||
|
} |
||||
|
} |
@ -0,0 +1,23 @@ |
|||||
|
{ |
||||
|
"version": "0.2.0", |
||||
|
"configurations": [ |
||||
|
{ |
||||
|
"name": "Server", |
||||
|
"type": "node", |
||||
|
"request": "launch", |
||||
|
"program": "${workspaceRoot}/server.js", |
||||
|
"args": [ |
||||
|
"-u http://127.0.0.1:4000", |
||||
|
//阿里OSS |
||||
|
"--aliOssAccessKey LTAI5tNDfn7UhStYQcn3JBtw", |
||||
|
"--aliOssSecretKey rnoXtDWQA1djJ5Xqcdn1OSEol0lVyv", |
||||
|
"--aliOssBucket test-c371", |
||||
|
"--aliOssRegion oss-cn-hangzhou", |
||||
|
], |
||||
|
"outputCapture": "std", |
||||
|
"env": { |
||||
|
"NODE_ENV": "development" |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
// 将设置放入此文件中以覆盖默认值和用户设置。 |
||||
|
{ |
||||
|
"editor.fontSize": 16, |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
FROM repository.anxinyun.cn/base-images/nodejs12:20.10.12.2 |
||||
|
|
||||
|
COPY . /var/app |
||||
|
|
||||
|
WORKDIR /var/app |
||||
|
|
||||
|
EXPOSE 8080 |
||||
|
|
||||
|
RUN npm cache clean -f |
||||
|
|
||||
|
RUN npm install --registry http://10.8.30.22:7000 --legacy-peer-deps |
||||
|
RUN npm run build |
||||
|
RUN rm -rf client/src |
||||
|
RUN rm -rf node_modules |
||||
|
|
||||
|
RUN npm install --production --registry http://10.8.30.22:7000 |
||||
|
#RUN npm cache clean -f && npm install --production --force --registry http://10.8.30.22:7000 |
||||
|
|
||||
|
CMD ["-u", "http://localhost:8088"] |
||||
|
|
||||
|
ENTRYPOINT [ "node", "server.js" ] |
@ -0,0 +1,25 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html> |
||||
|
|
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<link rel="shortcut icon" href="/assets/images/favicon.ico"> |
||||
|
<link rel="stylesheet" type="text/css" href="/assets/font_sc/iconfont.css"> |
||||
|
</head> |
||||
|
<style> |
||||
|
@font-face { |
||||
|
font-family: YouSheBiaoTiHei; |
||||
|
src: url("/assets/font_sc/YouSheBiaoTiHei-2.ttf"); |
||||
|
} |
||||
|
|
||||
|
@font-face { |
||||
|
font-family: D-DIN-Bold; |
||||
|
src: url("/assets/font_sc/D-DIN-Bold.ttf"); |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<body style="background: transparent"> |
||||
|
<div id='App'></div> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
@ -0,0 +1,28 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html> |
||||
|
|
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<title></title> |
||||
|
<link rel="shortcut icon" href="/assets/images/favicon.ico"> |
||||
|
<link rel="stylesheet" type="text/css" href="/assets/font_sc/iconfont.css"> |
||||
|
</head> |
||||
|
<style> |
||||
|
@font-face { |
||||
|
font-family: YouSheBiaoTiHei; |
||||
|
src: url("/assets/font_sc/YouSheBiaoTiHei-2.ttf"); |
||||
|
} |
||||
|
|
||||
|
@font-face { |
||||
|
font-family: D-DIN-Bold; |
||||
|
src: url("/assets/font_sc/D-DIN-Bold.ttf"); |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<body style="background: #0F1C2A;"> |
||||
|
<div id='App'></div> |
||||
|
<script type="text/javascript" src="http://localhost:5401/client/build/vendor.js"></script> |
||||
|
<script type="text/javascript" src="http://localhost:5401/client/build/app.js"></script> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
@ -0,0 +1,19 @@ |
|||||
|
/** |
||||
|
* User: liuxinyi/liu.xinyi@free-sun.com.cn |
||||
|
* Date: 2016/2/22 |
||||
|
* Time: 15:29 |
||||
|
* |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
|
||||
|
const views = require('koa-view'); |
||||
|
const path = require('path'); |
||||
|
module.exports = { |
||||
|
entry: function (app, router, opt) { |
||||
|
app.use(views(__dirname)); |
||||
|
|
||||
|
router.get('(.*)', async function (ctx){ |
||||
|
await ctx.render(path.join(__dirname, './index')); |
||||
|
}); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,27 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import React, { useEffect } from 'react'; |
||||
|
import Layout from './layout'; |
||||
|
import Auth from './sections/auth'; |
||||
|
import homePage from './sections/homePage'; |
||||
|
|
||||
|
const App = props => { |
||||
|
const { projectName } = props |
||||
|
|
||||
|
useEffect(() => { |
||||
|
document.title = projectName; |
||||
|
}, []) |
||||
|
|
||||
|
return ( |
||||
|
<Layout |
||||
|
title={projectName} |
||||
|
sections={[ |
||||
|
homePage, |
||||
|
Auth |
||||
|
]} |
||||
|
/> |
||||
|
) |
||||
|
|
||||
|
} |
||||
|
|
||||
|
export default App; |
@ -0,0 +1,316 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import React, { Component } from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { Spin, Upload, message, Modal, Card, Button } from 'antd'; |
||||
|
import moment from 'moment'; |
||||
|
import { PlusOutlined, UploadOutlined, CloseOutlined } from '@ant-design/icons'; |
||||
|
|
||||
|
class Uploads extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
this.ApiRoot = FS_API_ROOT |
||||
|
this.state = { |
||||
|
fileUploading: false, |
||||
|
fileList: [], |
||||
|
curPreviewPic: '', |
||||
|
delPicIng: false, |
||||
|
removeFilesList: [] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
dealName = (uploaded) => { |
||||
|
let realName = uploaded.split('/')[2] |
||||
|
let x1 = realName.split('.') |
||||
|
let x2 = x1[0].split('_') |
||||
|
let showName = `${x2[0]}.${x1[1]}` |
||||
|
return showName |
||||
|
} |
||||
|
|
||||
|
// setFileList = (value) => {
|
||||
|
// let defaultFileList = [];
|
||||
|
// defaultFileList = value.map((u, index) => {
|
||||
|
// let fileUrl = `${this.ApiRoot}/${u.url}`;
|
||||
|
// return {
|
||||
|
// uid: -index - 1,
|
||||
|
// name: this.dealName(u.url),
|
||||
|
// status: 'done',
|
||||
|
// storageUrl: u.url,
|
||||
|
// url: fileUrl
|
||||
|
// };
|
||||
|
// });
|
||||
|
// onChange(defaultFileList)
|
||||
|
// this.setState({
|
||||
|
// fileList: defaultFileList
|
||||
|
// });
|
||||
|
// };
|
||||
|
|
||||
|
componentDidMount () { |
||||
|
const { value } = this.props; |
||||
|
if (value) { |
||||
|
this.setState(value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
componentWillReceiveProps (np) { |
||||
|
const { dispatch, value: thisEditData, onChange } = this.props; |
||||
|
const { value: nextEditData } = np; |
||||
|
|
||||
|
const setFileList = () => { |
||||
|
let defaultFileList = []; |
||||
|
defaultFileList = nextEditData.map((u, index) => { |
||||
|
let fileUrl = `${this.ApiRoot}/${u.storageUrl}`; |
||||
|
return { |
||||
|
uid: -index - 1, |
||||
|
name: this.dealName(u.storageUrl), |
||||
|
status: 'done', |
||||
|
storageUrl: u.storageUrl, |
||||
|
url: fileUrl, |
||||
|
size: u.size || -1 |
||||
|
}; |
||||
|
}); |
||||
|
this.setState({ |
||||
|
fileList: defaultFileList |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
if (nextEditData && nextEditData.length) { |
||||
|
if (!thisEditData || !this.state.fileList.length) { |
||||
|
setFileList(); |
||||
|
} else if (nextEditData.length != thisEditData.length) { |
||||
|
setFileList(); |
||||
|
} else { |
||||
|
let repeat = true; |
||||
|
for (let i = 0; i < thisEditData.length; i++) { |
||||
|
if (thisEditData[i] != nextEditData[i]) { |
||||
|
repeat = false; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
if (!repeat) { |
||||
|
setFileList(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// else{
|
||||
|
// this.setState({
|
||||
|
// fileList:[],
|
||||
|
// })
|
||||
|
// }
|
||||
|
} |
||||
|
|
||||
|
render () { |
||||
|
const UploadPath = { |
||||
|
project: ['txt', 'dwg', 'doc', 'docx', 'xls', 'xlsx', 'pdf', 'png', 'jpg', 'rar', 'zip'], |
||||
|
report: ['doc', 'docx', 'xls', 'xlsx', 'pdf'], |
||||
|
data: ['txt', 'xls', 'xlsx'], |
||||
|
image: ['png', 'jpg', 'svg', 'jpeg'], |
||||
|
three: ['js'], |
||||
|
video: ['mp4'] |
||||
|
}; |
||||
|
/** |
||||
|
* uploadType 【string】 主要区别文件上传路径 以及类型 以 web/routes/attachment/index.js 中 UploadPath 的 key 值为准;默认 project; |
||||
|
* disabled 【boolean】 上传是否可用 |
||||
|
* maxFilesNum 【number】 最大上传数量 |
||||
|
* fileTypes 【array[string]】 可允许上传的文件类型; |
||||
|
* maxFileSize 【number】 单个文件最大大小 M |
||||
|
* listType 【antd】 upload 组件的属性 |
||||
|
* onChange 【function】 文件数量变化时候回调 返回文件 |
||||
|
* value 【array[obj]】 编辑数据 [{url:'xxx', [size:999]}] |
||||
|
* onStateChange 【function】 文件状态改变回调函数 上传中 return { uploading:true/false } |
||||
|
*/ |
||||
|
const { |
||||
|
uploadType, |
||||
|
disabled, |
||||
|
maxFilesNum, |
||||
|
fileTypes, |
||||
|
maxFileSize, |
||||
|
listType, |
||||
|
onChange, |
||||
|
value, |
||||
|
showUploadList, |
||||
|
onStateChange |
||||
|
} = this.props; |
||||
|
const { fileList, curPreviewPic, delPicIng, removeFilesList } = this.state; |
||||
|
const that = this; |
||||
|
let uploadType_ = uploadType || 'project'; |
||||
|
let maxFilesNum_ = maxFilesNum || 1; |
||||
|
let defaultFileTypes = fileTypes || UploadPath[uploadType_]; |
||||
|
const uploadProps = { |
||||
|
name: 'checkFile_', |
||||
|
multiple: false, |
||||
|
showUploadList: showUploadList || true, |
||||
|
action: `${this.ApiRoot}/attachments/${uploadType_}`, |
||||
|
listType: listType || 'text', |
||||
|
disabled: disabled, |
||||
|
beforeUpload: (file) => { |
||||
|
if (fileList.length >= maxFilesNum_) { |
||||
|
message.warning(`最多选择${maxFilesNum_}个文件上传`); |
||||
|
return false; |
||||
|
} |
||||
|
if (file.name.length > 60) { |
||||
|
message.warning(`文件名过长(大于60字符),请修改后上传`); |
||||
|
return false; |
||||
|
} |
||||
|
const extNames = file.name.split('.'); |
||||
|
var reg = /^[\.\s\u4e00-\u9fa5a-zA-Z0-9_-]{0,}$/; |
||||
|
if (!reg.exec(file.name)) { |
||||
|
message.warning(`文件名包含除字母、汉字、数字、中划线、下划线之外的字符,请修改后上传`); |
||||
|
return false; |
||||
|
} |
||||
|
let isDAE = false; |
||||
|
if (extNames.length > 0) { |
||||
|
let fileType = extNames[extNames.length - 1].toLowerCase(); |
||||
|
isDAE = defaultFileTypes.some((f) => f == fileType); |
||||
|
} |
||||
|
if (!isDAE) { |
||||
|
message.error(`只能上传 ${defaultFileTypes.join()} 格式的文件!`); |
||||
|
return false; |
||||
|
} |
||||
|
const isLt = file.size / 1024 / 1024 < (maxFileSize || 3); |
||||
|
if (!isLt) { |
||||
|
message.error(`文件必须小于${maxFileSize || 3}MB!`); |
||||
|
return false; |
||||
|
} |
||||
|
this.setState({ |
||||
|
fileUploading: true |
||||
|
}); |
||||
|
if (onStateChange) { |
||||
|
onStateChange({ uploading: true }); |
||||
|
} |
||||
|
}, |
||||
|
onChange (info) { |
||||
|
const status = info.file.status; |
||||
|
if (status === 'uploading') { |
||||
|
that.setState({ |
||||
|
fileList: info.fileList |
||||
|
}); |
||||
|
} |
||||
|
if (status === 'done') { |
||||
|
let { uploaded, url } = info.file.response; |
||||
|
let size = info.file.size; |
||||
|
let nextFileList = fileList; |
||||
|
nextFileList[nextFileList.length - 1] = { |
||||
|
uid: -moment().unix(), |
||||
|
name: that.dealName(uploaded), |
||||
|
status: 'done', |
||||
|
storageUrl: uploaded, |
||||
|
url: url, |
||||
|
size: size |
||||
|
}; |
||||
|
onChange(nextFileList); |
||||
|
that.setState({ |
||||
|
fileUploading: false, |
||||
|
fileList: nextFileList |
||||
|
}); |
||||
|
if (onStateChange) { |
||||
|
onStateChange({ uploading: false }); |
||||
|
} |
||||
|
} else if (status === 'error') { |
||||
|
that.setState({ |
||||
|
fileUploading: false |
||||
|
}); |
||||
|
message.error(`${info.file.name} 上传失败,请重试`); |
||||
|
if (onStateChange) { |
||||
|
onStateChange({ uploading: false }); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
onRemove (file) { |
||||
|
let nextFileList = []; |
||||
|
fileList.map((f, i) => { |
||||
|
if (f.uid != file.uid) { |
||||
|
nextFileList.push(f); |
||||
|
} |
||||
|
}); |
||||
|
let nextRemoveFiles = removeFilesList.concat([file.storageUrl]); |
||||
|
if (curPreviewPic == file.url) { |
||||
|
that.setState({ |
||||
|
curPreviewPic: '' |
||||
|
}); |
||||
|
} |
||||
|
onChange(nextFileList); |
||||
|
that.setState({ |
||||
|
fileList: nextFileList, |
||||
|
removeFilesList: nextRemoveFiles |
||||
|
}); |
||||
|
}, |
||||
|
onPreview (file) { |
||||
|
let filePostfix = file.url.split('.').pop(); |
||||
|
filePostfix = filePostfix.toLowerCase(); |
||||
|
if (UploadPath.image.some((img) => img == filePostfix)) { |
||||
|
that.setState({ |
||||
|
curPreviewPic: file.url |
||||
|
}); |
||||
|
} else { |
||||
|
message.warn('仅支持图片预览'); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
let fileList_ = fileList |
||||
|
// .map(f => {
|
||||
|
// if (f.storageUrl) {
|
||||
|
// let realName = f.storageUrl.split('/').pop()
|
||||
|
// if (f.name != realName) {
|
||||
|
// f.name = realName
|
||||
|
// }
|
||||
|
// }
|
||||
|
// return f
|
||||
|
// })
|
||||
|
|
||||
|
return ( |
||||
|
<div> |
||||
|
<Spin spinning={delPicIng}> |
||||
|
<Upload {...uploadProps} fileList={fileList_}> |
||||
|
{ |
||||
|
disabled ? ( |
||||
|
'' |
||||
|
) : |
||||
|
listType == 'picture-card' ? |
||||
|
( |
||||
|
fileList.length >= maxFilesNum_ ? null : ( |
||||
|
<div style={{}}> |
||||
|
<PlusOutlined /> |
||||
|
<div>上传图片</div> |
||||
|
</div> |
||||
|
) |
||||
|
) : ( |
||||
|
<Button disabled={fileList.length >= maxFilesNum_} icon={<UploadOutlined />}> 文件上传 </Button> |
||||
|
) |
||||
|
} |
||||
|
</Upload> |
||||
|
{ |
||||
|
curPreviewPic ? ( |
||||
|
<Card |
||||
|
bodyStyle={{ |
||||
|
padding: 8 |
||||
|
}} |
||||
|
> |
||||
|
<div style={{ marginBottom: 8 }} > |
||||
|
<span>文件预览</span> |
||||
|
<span |
||||
|
style={{ float: 'right' }} |
||||
|
onClick={() => { this.setState({ curPreviewPic: '' }); }} |
||||
|
> |
||||
|
<CloseOutlined style={{ fontSize: 20 }} /> |
||||
|
</span> |
||||
|
</div> |
||||
|
<img style={{ width: '100%' }} src={curPreviewPic}></img> |
||||
|
</Card> |
||||
|
) : '' |
||||
|
} |
||||
|
</Spin> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps (state) { |
||||
|
const { auth } = state |
||||
|
return { |
||||
|
user: auth.user |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(Uploads); |
@ -0,0 +1,339 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import React, { Component } from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { Spin, Upload, message, Modal, Card, Button } from 'antd'; |
||||
|
import moment from 'moment'; |
||||
|
import { PlusOutlined, UploadOutlined, CloseOutlined } from '@ant-design/icons'; |
||||
|
import { RouteRequest } from '@peace/utils'; |
||||
|
import { RouteTable } from '$utils' |
||||
|
const { confirm } = Modal; |
||||
|
class Uploads extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
this.state = { |
||||
|
fileUploading: false, |
||||
|
fileList: [], |
||||
|
curPreviewPic: '', |
||||
|
delPicIng: false, |
||||
|
removeFilesList: [] |
||||
|
}; |
||||
|
} |
||||
|
dealName = (uploaded) => { |
||||
|
let realName = uploaded.split('/')[2] |
||||
|
let x1 = realName.split('.') |
||||
|
let x2 = x1[0].split('_') |
||||
|
let showName = `${x2[0]}.${x1[1]}` |
||||
|
return showName |
||||
|
} |
||||
|
|
||||
|
// setFileList = (value) => {
|
||||
|
// let defaultFileList = [];
|
||||
|
// defaultFileList = value.map((u, index) => {
|
||||
|
// let fileUrl = `${this.ApiRoot}/${u.url}`;
|
||||
|
// return {
|
||||
|
// uid: -index - 1,
|
||||
|
// name: this.dealName(u.url),
|
||||
|
// status: 'done',
|
||||
|
// storageUrl: u.url,
|
||||
|
// url: fileUrl
|
||||
|
// };
|
||||
|
// });
|
||||
|
// onChange(defaultFileList)
|
||||
|
// this.setState({
|
||||
|
// fileList: defaultFileList
|
||||
|
// });
|
||||
|
// };
|
||||
|
|
||||
|
componentDidMount() { |
||||
|
const { value } = this.props; |
||||
|
if (value) { |
||||
|
// this.setState(value);
|
||||
|
this.setState({ fileList: value }) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
UNSAFE_componentWillReceiveProps(np) { |
||||
|
const { dispatch, value: thisEditData, onChange } = this.props; |
||||
|
const { value: nextEditData } = np; |
||||
|
|
||||
|
const setFileList = () => { |
||||
|
let defaultFileList = []; |
||||
|
defaultFileList = nextEditData.map((u, index) => { |
||||
|
let fileUrl = u.filename; |
||||
|
return { |
||||
|
uid: -index - 1, |
||||
|
name: u.name, |
||||
|
status: 'done', |
||||
|
storageUrl: u.filename, |
||||
|
url: fileUrl, |
||||
|
size: u.size || -1 |
||||
|
}; |
||||
|
}); |
||||
|
this.setState({ |
||||
|
fileList: defaultFileList |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
if (nextEditData && nextEditData.length) { |
||||
|
if (!thisEditData || !this.state.fileList.length) { |
||||
|
setFileList(); |
||||
|
} else if (nextEditData.length != thisEditData.length) { |
||||
|
setFileList(); |
||||
|
} else { |
||||
|
let repeat = true; |
||||
|
for (let i = 0; i < thisEditData.length; i++) { |
||||
|
if (thisEditData[i] != nextEditData[i]) { |
||||
|
repeat = false; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
if (!repeat) { |
||||
|
setFileList(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// else{
|
||||
|
// this.setState({
|
||||
|
// fileList:[],
|
||||
|
// })
|
||||
|
// }
|
||||
|
} |
||||
|
//删除文件
|
||||
|
deleteFile(file) { |
||||
|
if (file.url) { |
||||
|
RouteRequest.delete(RouteTable.cleanUpUploadTrash, { url: file.url }); |
||||
|
}; |
||||
|
} |
||||
|
handleOk = (that, file, fileList, curPreviewPic, removeFilesList) => { |
||||
|
let nextFileList = []; |
||||
|
fileList.map((f, i) => { |
||||
|
if (f.uid != file.uid) { |
||||
|
nextFileList.push(f); |
||||
|
} |
||||
|
}); |
||||
|
that.deleteFile(file); |
||||
|
let nextRemoveFiles = removeFilesList.concat([file.storageUrl]); |
||||
|
if (curPreviewPic == file.url) { |
||||
|
that.setState({ |
||||
|
curPreviewPic: '' |
||||
|
}); |
||||
|
} |
||||
|
that.props.onChange(nextFileList); |
||||
|
that.setState({ |
||||
|
fileList: nextFileList, |
||||
|
removeFilesList: nextRemoveFiles |
||||
|
}); |
||||
|
} |
||||
|
render() { |
||||
|
const UploadPath = { |
||||
|
project: ['txt', 'dwg', 'doc', 'docx', 'xls', 'xlsx', 'pdf', 'png', 'jpg', 'rar', 'zip'], |
||||
|
report: ['doc', 'docx', 'xls', 'xlsx', 'pdf'], |
||||
|
data: ['txt', 'xls', 'xlsx'], |
||||
|
image: ['png', 'jpg', 'svg', 'jpeg'], |
||||
|
three: ['js'], |
||||
|
video: ['mp4'] |
||||
|
}; |
||||
|
/** |
||||
|
* uploadType 【string】 主要区别文件上传路径 以及类型 以 web/routes/attachment/index.js 中 UploadPath 的 key 值为准;默认 project; |
||||
|
* disabled 【boolean】 上传是否可用 |
||||
|
* maxFilesNum 【number】 最大上传数量 |
||||
|
* fileTypes 【array[string]】 可允许上传的文件类型; |
||||
|
* maxFileSize 【number】 单个文件最大大小 M |
||||
|
* listType 【antd】 upload 组件的属性 |
||||
|
* onChange 【function】 文件数量变化时候回调 返回文件 |
||||
|
* value 【array[obj]】 编辑数据 [{url:'xxx', [size:999]}] |
||||
|
* onStateChange 【function】 文件状态改变回调函数 上传中 return { uploading:true/false } |
||||
|
*/ |
||||
|
const { |
||||
|
uploadType, |
||||
|
disabled, |
||||
|
maxFilesNum, |
||||
|
fileTypes, |
||||
|
maxFileSize, |
||||
|
listType, |
||||
|
onChange, |
||||
|
value, |
||||
|
showUploadList, |
||||
|
onStateChange, |
||||
|
addNew |
||||
|
} = this.props; |
||||
|
const { fileList, curPreviewPic, delPicIng, removeFilesList } = this.state; |
||||
|
const that = this; |
||||
|
let uploadType_ = uploadType || 'project'; |
||||
|
let maxFilesNum_ = maxFilesNum || 1; |
||||
|
let defaultFileTypes = fileTypes || UploadPath[uploadType_]; |
||||
|
const uploadProps = { |
||||
|
name: 'checkFile_', |
||||
|
multiple: false, |
||||
|
showUploadList: showUploadList || true, |
||||
|
action: "/_upload/new?type=project", |
||||
|
listType: listType || 'text', |
||||
|
disabled: disabled, |
||||
|
beforeUpload: (file) => { |
||||
|
if (fileList.length >= maxFilesNum_) { |
||||
|
message.warning(`最多选择${maxFilesNum_}个文件上传`); |
||||
|
return false; |
||||
|
} |
||||
|
if (file.name.length > 60) { |
||||
|
message.warning(`文件名过长(大于60字符),请修改后上传`); |
||||
|
return false; |
||||
|
} |
||||
|
const extNames = file.name.split('.'); |
||||
|
var reg = /^[\.\s\u4e00-\u9fa5a-zA-Z0-9_-]{0,}$/; |
||||
|
// if (!reg.exec(file.name)) {
|
||||
|
// message.warning(`文件名包含除字母、汉字、数字、中划线、下划线之外的字符,请修改后上传`);
|
||||
|
// return false;
|
||||
|
// }
|
||||
|
let isDAE = false; |
||||
|
if (extNames.length > 0) { |
||||
|
let fileType = extNames[extNames.length - 1].toLowerCase(); |
||||
|
isDAE = defaultFileTypes.some((f) => f == fileType); |
||||
|
} |
||||
|
if (!isDAE) { |
||||
|
message.error(`只能上传 ${defaultFileTypes.join()} 格式的文件!`); |
||||
|
return false; |
||||
|
} |
||||
|
const isLt = file.size / 1024 / 1024 < (maxFileSize || 3); |
||||
|
if (!isLt) { |
||||
|
message.error(`文件必须小于${maxFileSize || 3}MB!`); |
||||
|
return false; |
||||
|
} |
||||
|
this.setState({ |
||||
|
fileUploading: true |
||||
|
}); |
||||
|
if (onStateChange) { |
||||
|
onStateChange({ uploading: true }); |
||||
|
} |
||||
|
}, |
||||
|
onChange(info) { |
||||
|
const status = info.file.status; |
||||
|
if (status === 'uploading') { |
||||
|
that.setState({ |
||||
|
fileList: info.fileList |
||||
|
}); |
||||
|
} |
||||
|
if (status === 'done') { |
||||
|
let { filename, realName } = info.file.response; |
||||
|
let size = info.file.size; |
||||
|
let nextFileList = fileList; |
||||
|
nextFileList[nextFileList.length - 1] = { |
||||
|
uid: -moment().unix(), |
||||
|
name: info.file.name, |
||||
|
status: 'done', |
||||
|
storageUrl: filename, |
||||
|
url: filename, |
||||
|
size: size, |
||||
|
realName: realName |
||||
|
}; |
||||
|
onChange(nextFileList); |
||||
|
that.setState({ |
||||
|
fileUploading: false, |
||||
|
fileList: nextFileList |
||||
|
}); |
||||
|
if (onStateChange) { |
||||
|
onStateChange({ uploading: false }); |
||||
|
} |
||||
|
} else if (status === 'error') { |
||||
|
that.setState({ |
||||
|
fileUploading: false |
||||
|
}); |
||||
|
message.error(`${info.file.name} 上传失败,请重试`); |
||||
|
if (onStateChange) { |
||||
|
onStateChange({ uploading: false }); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
onRemove(file) { |
||||
|
// if (confirm('请确认是否删除此文件?删除后将不可恢复!') === true) {
|
||||
|
if (addNew) { |
||||
|
that.handleOk(that, file, fileList, curPreviewPic, removeFilesList); |
||||
|
} else { |
||||
|
confirm({ |
||||
|
title: '请确认是否删除此文件?删除后将不可恢复!', |
||||
|
onOk() { |
||||
|
that.handleOk(that, file, fileList, curPreviewPic, removeFilesList); |
||||
|
}, |
||||
|
onCancel() { }, |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
onPreview(file) { |
||||
|
let filePostfix = file.url.split('.').pop(); |
||||
|
filePostfix = filePostfix.toLowerCase(); |
||||
|
if (UploadPath.image.some((img) => img == filePostfix)) { |
||||
|
that.setState({ |
||||
|
curPreviewPic: file.url |
||||
|
}); |
||||
|
} else { |
||||
|
message.warn('仅支持图片预览'); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
let fileList_ = fileList |
||||
|
// .map(f => {
|
||||
|
// if (f.storageUrl) {
|
||||
|
// let realName = f.storageUrl.split('/').pop()
|
||||
|
// if (f.name != realName) {
|
||||
|
// f.name = realName
|
||||
|
// }
|
||||
|
// }
|
||||
|
// return f
|
||||
|
// })
|
||||
|
|
||||
|
return ( |
||||
|
<div> |
||||
|
<Spin spinning={delPicIng}> |
||||
|
<Upload {...uploadProps} fileList={fileList_}> |
||||
|
{ |
||||
|
disabled ? ( |
||||
|
'' |
||||
|
) : |
||||
|
listType == 'picture-card' ? |
||||
|
( |
||||
|
fileList.length >= maxFilesNum_ ? null : ( |
||||
|
<div style={{}}> |
||||
|
<PlusOutlined /> |
||||
|
<div>上传图片</div> |
||||
|
</div> |
||||
|
) |
||||
|
) : ( |
||||
|
<Button disabled={fileList.length >= maxFilesNum_} icon={<UploadOutlined />}> 文件上传 </Button> |
||||
|
) |
||||
|
} |
||||
|
</Upload> |
||||
|
{ |
||||
|
curPreviewPic ? ( |
||||
|
<Card |
||||
|
bodyStyle={{ |
||||
|
padding: 8 |
||||
|
}} |
||||
|
> |
||||
|
<div style={{ marginBottom: 8 }} > |
||||
|
<span>文件预览</span> |
||||
|
<span |
||||
|
style={{ float: 'right' }} |
||||
|
onClick={() => { this.setState({ curPreviewPic: '' }); }} |
||||
|
> |
||||
|
<CloseOutlined style={{ fontSize: 20 }} /> |
||||
|
</span> |
||||
|
</div> |
||||
|
<img style={{ width: '100%' }} src={curPreviewPic}></img> |
||||
|
</Card> |
||||
|
) : '' |
||||
|
} |
||||
|
</Spin> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps(state) { |
||||
|
const { auth } = state |
||||
|
return { |
||||
|
user: auth.user |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(Uploads); |
@ -0,0 +1,391 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import React, { Component } from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { Spin, Upload, message, Modal, Card, Button } from 'antd'; |
||||
|
import moment from 'moment'; |
||||
|
import { PlusOutlined, UploadOutlined, CloseOutlined } from '@ant-design/icons'; |
||||
|
|
||||
|
class Uploads extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
this.ApiRoot = localStorage.getItem('tyApiRoot') |
||||
|
this.qnDomain = localStorage.getItem('qnDomain'); |
||||
|
this.aliAdmin = localStorage.getItem('aliAdmin'); |
||||
|
this.state = { |
||||
|
fileUploading: false, |
||||
|
fileList: [], |
||||
|
curPreviewPic: '', |
||||
|
curPreviewVideo: '', |
||||
|
delPicIng: false, |
||||
|
removeFilesList: [] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
dealName = (uploaded) => { |
||||
|
let realName = uploaded.split('/')[2] |
||||
|
// let x1 = realName.split('.')
|
||||
|
// let postfix = x1.pop()
|
||||
|
// let allName = x1.join('.')
|
||||
|
// let x2 = allName.split('_')
|
||||
|
// let showName = `${x2[0]}.${postfix}`
|
||||
|
return realName |
||||
|
} |
||||
|
|
||||
|
// setFileList = (value) => {
|
||||
|
// let defaultFileList = [];
|
||||
|
// defaultFileList = value.map((u, index) => {
|
||||
|
// let fileUrl = `${this.ApiRoot}/${u.url}`;
|
||||
|
// return {
|
||||
|
// uid: -index - 1,
|
||||
|
// name: this.dealName(u.url),
|
||||
|
// status: 'done',
|
||||
|
// storageUrl: u.url,
|
||||
|
// url: fileUrl
|
||||
|
// };
|
||||
|
// });
|
||||
|
// onChange(defaultFileList)
|
||||
|
// this.setState({
|
||||
|
// fileList: defaultFileList
|
||||
|
// });
|
||||
|
// };
|
||||
|
|
||||
|
setFileList = (nextEditData, isQiniu, isAli) => { |
||||
|
let defaultFileList = []; |
||||
|
if (nextEditData.length) { |
||||
|
defaultFileList = nextEditData.map((u, index) => { |
||||
|
let fileUrl = |
||||
|
isQiniu ? `/_file-server/${u.storageUrl}` |
||||
|
: isAli ? `/_file-ali-server/${u.storageUrl}` |
||||
|
: `${this.ApiRoot}/${u.storageUrl}`; |
||||
|
|
||||
|
return { |
||||
|
uid: -index - 1, |
||||
|
name: this.dealName(u.storageUrl), |
||||
|
status: 'done', |
||||
|
storageUrl: u.storageUrl, |
||||
|
url: fileUrl, |
||||
|
size: u.size || -1 |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
this.setState({ |
||||
|
fileList: defaultFileList |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
componentDidMount() { |
||||
|
const { value, defaultValue, isQiniu, isAli } = this.props; |
||||
|
if (defaultValue) { |
||||
|
this.setFileList(defaultValue, isQiniu, isAli) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
UNSAFE_componentWillReceiveProps(np) { |
||||
|
const { dispatch, value: thisEditData, onChange } = this.props; |
||||
|
const { value: nextEditData, isQiniu, isAli } = np; |
||||
|
// this.setFileList(nextEditData, isQiniu)
|
||||
|
// const setFileList = () => {
|
||||
|
// let defaultFileList = [];
|
||||
|
// defaultFileList = nextEditData.map((u, index) => {
|
||||
|
// let fileUrl = isQiniu ? `/_file-server/${u.storageUrl}` : `${this.ApiRoot}/${u.storageUrl}`;
|
||||
|
// return {
|
||||
|
// uid: -index - 1,
|
||||
|
// name: this.dealName(u.storageUrl),
|
||||
|
// status: 'done',
|
||||
|
// storageUrl: u.storageUrl,
|
||||
|
// url: fileUrl,
|
||||
|
// size: u.size || -1
|
||||
|
// };
|
||||
|
// });
|
||||
|
// this.setState({
|
||||
|
// fileList: defaultFileList
|
||||
|
// });
|
||||
|
// };
|
||||
|
if (nextEditData && nextEditData.length) { |
||||
|
if (!thisEditData || !this.state.fileList.length) { |
||||
|
this.setFileList(nextEditData, isQiniu, isAli); |
||||
|
} else if (nextEditData.length != thisEditData.length) { |
||||
|
this.setFileList(nextEditData, isQiniu, isAli); |
||||
|
} else { |
||||
|
let repeat = true; |
||||
|
for (let i = 0; i < thisEditData.length; i++) { |
||||
|
if (thisEditData[i] != nextEditData[i]) { |
||||
|
repeat = false; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
if (!repeat) { |
||||
|
this.setFileList(nextEditData, isQiniu, isAli); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// else{
|
||||
|
// this.setState({
|
||||
|
// fileList:[],
|
||||
|
// })
|
||||
|
// }
|
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const UploadPath = { |
||||
|
project: ['txt', 'dwg', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'pdf', 'pptx', 'png', 'jpg', 'svg', 'jpeg', 'rar', 'zip', 'jpeg', 'mp4'], |
||||
|
report: ['doc', 'docx', 'xls', 'xlsx', 'csv', 'pdf'], |
||||
|
data: ['txt', 'xls', 'xlsx', 'csv'], |
||||
|
image: ['png', 'jpg', 'svg', 'jpeg'], |
||||
|
three: ['js'], |
||||
|
video: ['mp4'] |
||||
|
}; |
||||
|
/** |
||||
|
* uploadType 【string】 主要区别文件上传路径 以及类型 以 web/routes/attachment/index.js 中 UploadPath 的 key 值为准;默认 project; |
||||
|
* disabled 【boolean】 上传是否可用 |
||||
|
* maxFilesNum 【number】 最大上传数量 |
||||
|
* fileTypes 【array[string]】 可允许上传的文件类型; |
||||
|
* maxFileSize 【number】 单个文件最大大小 M |
||||
|
* listType 【antd】 upload 组件的属性 |
||||
|
* onChange 【function】 文件数量变化时候回调 返回文件 |
||||
|
* value 【array[obj]】 编辑数据 [{url:'xxx', [size:999]}] |
||||
|
* onStateChange 【function】 文件状态改变回调函数 上传中 return { uploading:true/false } |
||||
|
*/ |
||||
|
const { |
||||
|
uploadType, |
||||
|
disabled, |
||||
|
maxFilesNum, |
||||
|
fileTypes, |
||||
|
maxFileSize, |
||||
|
listType, |
||||
|
onChange = () => { }, |
||||
|
value, |
||||
|
showUploadList, |
||||
|
onStateChange, |
||||
|
isQiniu, |
||||
|
isAli, |
||||
|
} = this.props; |
||||
|
const { fileList, curPreviewPic, curPreviewVideo, delPicIng, removeFilesList } = this.state; |
||||
|
const that = this; |
||||
|
let uploadType_ = uploadType || 'project'; |
||||
|
let maxFilesNum_ = maxFilesNum || 1; |
||||
|
let defaultFileTypes = fileTypes || UploadPath[uploadType_]; |
||||
|
// debugger
|
||||
|
const uploadProps = { |
||||
|
name: 'checkFile_', |
||||
|
multiple: false, |
||||
|
showUploadList: showUploadList || true, |
||||
|
action: |
||||
|
isQiniu ? `/_upload/attachments/${uploadType_}` |
||||
|
: isAli ? `/_upload/attachments/ali/${uploadType_}` |
||||
|
: `${this.ApiRoot}/attachments/${uploadType_}`, |
||||
|
listType: listType || 'text', |
||||
|
disabled: disabled, |
||||
|
beforeUpload: (file) => { |
||||
|
if (fileList.length >= maxFilesNum_) { |
||||
|
message.warning(`最多选择${maxFilesNum_}个文件上传`); |
||||
|
return false; |
||||
|
} |
||||
|
if (file.name.length > 60) { |
||||
|
message.warning(`文件名过长(大于60字符),请修改后上传`); |
||||
|
return false; |
||||
|
} |
||||
|
const extNames = file.name.split('.'); |
||||
|
// var reg = /^[\.\s\u4e00-\u9fa5a-zA-Z0-9_-]{0,}$/;
|
||||
|
// if (!reg.exec(file.name)) {
|
||||
|
// message.warning(`文件名包含除字母、汉字、数字、中划线、下划线之外的字符,请修改后上传`);
|
||||
|
// return false;
|
||||
|
// }
|
||||
|
let isDAE = false; |
||||
|
if (extNames.length > 0) { |
||||
|
let fileType = extNames[extNames.length - 1].toLowerCase(); |
||||
|
isDAE = defaultFileTypes.some((f) => f == fileType); |
||||
|
} |
||||
|
if (!isDAE) { |
||||
|
message.error(`只能上传 ${defaultFileTypes.join()} 格式的文件!`); |
||||
|
return false; |
||||
|
} |
||||
|
const isLt = file.size / 1024 / 1024 < (maxFileSize || 3); |
||||
|
if (!isLt) { |
||||
|
message.error(`文件必须小于${maxFileSize || 3}MB!`); |
||||
|
return false; |
||||
|
} |
||||
|
this.setState({ |
||||
|
fileUploading: true |
||||
|
}); |
||||
|
if (onStateChange) { |
||||
|
onStateChange({ uploading: true }); |
||||
|
} |
||||
|
}, |
||||
|
onChange(info) { |
||||
|
const status = info.file.status; |
||||
|
if (status === 'uploading') { |
||||
|
that.setState({ |
||||
|
fileList: info.fileList |
||||
|
}); |
||||
|
} |
||||
|
if (status === 'done') { |
||||
|
let { uploaded, url } = info.file.response; |
||||
|
let size = info.file.size; |
||||
|
let nextFileList = fileList; |
||||
|
nextFileList[nextFileList.length - 1] = { |
||||
|
uid: -moment().unix(), |
||||
|
name: that.dealName(uploaded), |
||||
|
status: 'done', |
||||
|
storageUrl: uploaded, |
||||
|
url: |
||||
|
isQiniu ? '/_file-server/' + uploaded : |
||||
|
isAli ? `/_file-ali-server/${uploaded}` : |
||||
|
url, |
||||
|
size: size |
||||
|
}; |
||||
|
onChange(nextFileList); |
||||
|
that.setState({ |
||||
|
fileUploading: false, |
||||
|
fileList: nextFileList |
||||
|
}); |
||||
|
if (onStateChange) { |
||||
|
onStateChange({ uploading: false }); |
||||
|
} |
||||
|
} else if (status === 'error') { |
||||
|
that.setState({ |
||||
|
fileUploading: false |
||||
|
}); |
||||
|
message.error(`${info.file.name} 上传失败,请重试`); |
||||
|
if (onStateChange) { |
||||
|
onStateChange({ uploading: false }); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
onRemove(file) { |
||||
|
let nextFileList = []; |
||||
|
fileList.map((f, i) => { |
||||
|
if (f.uid != file.uid) { |
||||
|
nextFileList.push(f); |
||||
|
} |
||||
|
}); |
||||
|
let nextRemoveFiles = removeFilesList.concat([file.storageUrl]); |
||||
|
if (curPreviewPic == file.url) { |
||||
|
that.setState({ |
||||
|
curPreviewPic: '' |
||||
|
}); |
||||
|
} |
||||
|
if (curPreviewVideo == file.url) { |
||||
|
that.setState({ |
||||
|
curPreviewVideo: '' |
||||
|
}); |
||||
|
} |
||||
|
onChange(nextFileList); |
||||
|
that.setState({ |
||||
|
fileList: nextFileList, |
||||
|
removeFilesList: nextRemoveFiles |
||||
|
}); |
||||
|
}, |
||||
|
onPreview(file) { |
||||
|
let filePostfix = file.url.split('.').pop(); |
||||
|
filePostfix = filePostfix.toLowerCase(); |
||||
|
if (UploadPath.image.some((img) => img == filePostfix)) { |
||||
|
that.setState({ |
||||
|
curPreviewPic: file.url |
||||
|
}); |
||||
|
} else if (UploadPath.video.some((img) => img == filePostfix)) { |
||||
|
that.setState({ |
||||
|
curPreviewVideo: file.url |
||||
|
}); |
||||
|
} else { |
||||
|
//message.warn('仅支持图片预览');
|
||||
|
preview(file.storageUrl) |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const preview = (url) => { |
||||
|
let link = isQiniu ? encodeURI(`${this.qnDomain}/${url}`) : |
||||
|
isAli ? encodeURI(`${this.aliAdmin}/${url}`) : '' |
||||
|
if (link) |
||||
|
if (url.indexOf("pdf") !== -1 || url.indexOf("csv") !== -1) { |
||||
|
window.open(link) |
||||
|
} else { |
||||
|
window.open(`https://view.officeapps.live.com/op/view.aspx?src=${link}`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let fileList_ = fileList |
||||
|
// .map(f => {
|
||||
|
// if (f.storageUrl) {
|
||||
|
// let realName = f.storageUrl.split('/').pop()
|
||||
|
// if (f.name != realName) {
|
||||
|
// f.name = realName
|
||||
|
// }
|
||||
|
// }
|
||||
|
// return f
|
||||
|
// })
|
||||
|
//下载文件
|
||||
|
const handleDownload = (file) => { |
||||
|
saveAs(file) |
||||
|
}; |
||||
|
const saveAs = (file) => { |
||||
|
const link = document.createElement('a'); |
||||
|
link.href = file.url; |
||||
|
link.download = file.name; |
||||
|
link.style.display = 'none'; |
||||
|
link.click(); |
||||
|
} |
||||
|
//自定义下载
|
||||
|
return ( |
||||
|
<div> |
||||
|
<Spin spinning={delPicIng}> |
||||
|
<Upload {...uploadProps} fileList={fileList_} showUploadList={{ showDownloadIcon: true }} onDownload={handleDownload}> |
||||
|
{ |
||||
|
disabled ? ( |
||||
|
'' |
||||
|
) : |
||||
|
listType == 'picture-card' ? |
||||
|
( |
||||
|
fileList.length >= maxFilesNum_ ? null : ( |
||||
|
<div style={{}}> |
||||
|
<PlusOutlined /> |
||||
|
<div>添加附件</div> |
||||
|
</div> |
||||
|
) |
||||
|
) : ( |
||||
|
<Button disabled={fileList.length >= maxFilesNum_} icon={<UploadOutlined />}> 文件上传 </Button> |
||||
|
) |
||||
|
} |
||||
|
</Upload> |
||||
|
{ |
||||
|
curPreviewPic ? ( |
||||
|
<Card bodyStyle={{ padding: 8 }}> |
||||
|
<div style={{ marginBottom: 8 }} > |
||||
|
<span>图片预览</span> |
||||
|
<span style={{ float: 'right' }} onClick={() => { this.setState({ curPreviewPic: '' }) }}> |
||||
|
<CloseOutlined style={{ fontSize: 20 }} /> |
||||
|
</span> |
||||
|
</div> |
||||
|
<img style={{ width: '100%' }} src={curPreviewPic} /> |
||||
|
</Card> |
||||
|
) : '' |
||||
|
} |
||||
|
{ |
||||
|
curPreviewVideo ? (<Card bodyStyle={{ padding: 8 }}> |
||||
|
<div style={{ marginBottom: 8 }} > |
||||
|
<span>视频预览</span> |
||||
|
<span style={{ float: 'right' }} onClick={() => { this.setState({ curPreviewVideo: '' }) }}> |
||||
|
<CloseOutlined style={{ fontSize: 20 }} /> |
||||
|
</span> |
||||
|
</div> |
||||
|
<video controls style={{ width: '100%' }}> |
||||
|
<source src={curPreviewVideo} type="video/mp4"></source> |
||||
|
</video> |
||||
|
</Card>) : '' |
||||
|
} |
||||
|
</Spin> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps(state) { |
||||
|
const { auth } = state |
||||
|
return { |
||||
|
user: auth.user |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(Uploads); |
@ -0,0 +1,51 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import React, { Component } from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { Button, Popover, Icon } from 'antd'; |
||||
|
import { EllipsisOutlined } from '@ant-design/icons'; |
||||
|
|
||||
|
class ButtonGroup extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
this.state = { |
||||
|
|
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
content = () => { |
||||
|
<Button></Button> |
||||
|
} |
||||
|
|
||||
|
render_ = () => { |
||||
|
const { children } = this.props |
||||
|
return ( |
||||
|
<div style={{ cursor: 'pointer' }}> |
||||
|
<Popover placement="bottomRight" content={children} arrowPointAtCenter> |
||||
|
<EllipsisOutlined style={{ fontSize: 20, fontWeight: 'bolder' }} /> |
||||
|
</Popover> |
||||
|
</div > |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { children } = this.props |
||||
|
if (children) { |
||||
|
if (Array.isArray(children)) { |
||||
|
if (children.some(c => c)) { |
||||
|
return this.render_() |
||||
|
} |
||||
|
} else { |
||||
|
return this.render_() |
||||
|
} |
||||
|
} |
||||
|
return '' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps(state) { |
||||
|
return { |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(ButtonGroup); |
@ -0,0 +1,683 @@ |
|||||
|
/** |
||||
|
* Created by Xumeng 2020/04/22. |
||||
|
*/ |
||||
|
|
||||
|
import React, { useState, useEffect } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { |
||||
|
Row, Col, Space, Button, message, notification, Form, Input, Tooltip, |
||||
|
} from 'antd'; |
||||
|
import moment from 'moment'; |
||||
|
import XLSX from 'xlsx'; |
||||
|
import { fromJS } from 'immutable'; |
||||
|
import { Request } from '@peace/utils'; |
||||
|
import FileSaver from 'file-saver'; |
||||
|
import './index.less'; |
||||
|
|
||||
|
// 通用前端导入导出组件 使用方法查看底部propTypes
|
||||
|
function ExportAndImport (props) { |
||||
|
const [form] = Form.useForm(); |
||||
|
const [exportLoading, setExportLoading] = useState(false); |
||||
|
const [importLoading, setImportLoading] = useState(false); |
||||
|
const { |
||||
|
importDataCallback, onImportSucess, handelData, importMethod = 'post', |
||||
|
} = props; |
||||
|
|
||||
|
useEffect(() => () => { |
||||
|
// 只有unmount 时调用
|
||||
|
notification.close('import-notification'); |
||||
|
}); |
||||
|
const importExcel = (file, type) => { |
||||
|
setImportLoading(true); |
||||
|
// 获取上传的文件对象
|
||||
|
const { files } = file.target; |
||||
|
// 判断xls、xlsx格式
|
||||
|
if (files[0].type.indexOf('sheet') > -1 || files[0].type.indexOf('ms-excel') > -1) { |
||||
|
// 通过FileReader对象读取文件
|
||||
|
const fileReader = new FileReader(); |
||||
|
fileReader.onload = (event) => { |
||||
|
try { |
||||
|
const { importRequest = true, importUrl, importQuery } = props; |
||||
|
if (importRequest && !importUrl) { |
||||
|
message.error('获取导入接口失败!'); |
||||
|
form.resetFields(); |
||||
|
return; |
||||
|
} |
||||
|
const { result } = event.target; |
||||
|
// 以二进制流方式读取得到整份excel表格对象
|
||||
|
const workbook = XLSX.read(result, { type: 'binary', cellDates: true }); |
||||
|
let data = []; // 存储获取到的数据
|
||||
|
// 遍历每张工作表进行读取(这里默认只读取第一张表)
|
||||
|
for (const sheet in workbook.Sheets) { |
||||
|
if (workbook.Sheets.hasOwnProperty(sheet)) { |
||||
|
// 利用 sheet_to_json 方法将 excel 转成 json 数据
|
||||
|
data = data.concat(XLSX.utils.sheet_to_json(workbook.Sheets[sheet])); |
||||
|
break; // 如果只取第一张表,就取消注释这行
|
||||
|
} |
||||
|
} |
||||
|
if (data.length > 10000) { |
||||
|
message.error('一次最多导入10000条数据,请分批导入!'); |
||||
|
form.resetFields(); |
||||
|
setImportLoading(false); |
||||
|
return; |
||||
|
} |
||||
|
if (importRequest) { |
||||
|
message.success('获取文件数据成功,开始处理导入...'); |
||||
|
const importData = handelData ? handelData(data) : data; |
||||
|
Request[importMethod](importUrl, { data: importData }, importQuery || {}).then((res) => { |
||||
|
message.success('导入数据成功'); |
||||
|
form.resetFields(); |
||||
|
notification.close('import-notification'); |
||||
|
setImportLoading(false); |
||||
|
onImportSucess && onImportSucess(); |
||||
|
}, (err) => { |
||||
|
if (err.status === 500) { |
||||
|
message.error('数据导入出错,导入失败'); |
||||
|
} else if (err.status === 400) { |
||||
|
message.error(err.body.message || '数据验证出错,请检查数据格式是否正确'); |
||||
|
} else { |
||||
|
message.error('导入失败'); |
||||
|
} |
||||
|
form.resetFields(); |
||||
|
setImportLoading(false); |
||||
|
}); |
||||
|
} else { |
||||
|
form.resetFields(); |
||||
|
setImportLoading(false); |
||||
|
importDataCallback && importDataCallback(data, type); |
||||
|
notification.close('import-notification'); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
console.log(e); |
||||
|
// 这里可以抛出文件类型错误不正确的相关提示
|
||||
|
message.error('文件格式不正确!'); |
||||
|
setImportLoading(false); |
||||
|
form.resetFields(); |
||||
|
} |
||||
|
}; |
||||
|
// fileReader.onloadend = (event) => {
|
||||
|
// console.log(event)
|
||||
|
// }
|
||||
|
// 以二进制方式打开文件
|
||||
|
fileReader.readAsBinaryString(files[0]); |
||||
|
} else { |
||||
|
message.error('文件格式不正确!'); |
||||
|
form.resetFields(); |
||||
|
setImportLoading(false); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const loop = (data, keypath, values) => { // deal with array
|
||||
|
const dkey = keypath.slice(0, 1)[0]; |
||||
|
console.log(dkey); |
||||
|
if (dkey) { |
||||
|
const dvalue = data[dkey]; |
||||
|
const otherKeypath = keypath.slice(1); |
||||
|
if (Array.isArray(dvalue)) { |
||||
|
if (otherKeypath.length) { |
||||
|
const immutableData = fromJS(data); |
||||
|
for (let index = 0; index < dvalue.length; index++) { |
||||
|
const tmp = immutableData.getIn([dkey, index]).toJS(); |
||||
|
loop(tmp, otherKeypath, values); |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
values.push(dvalue); |
||||
|
} |
||||
|
} |
||||
|
return values; |
||||
|
}; |
||||
|
const getColumnData = (opts) => { |
||||
|
const { |
||||
|
data, keypath, render, spliter, rawdata, |
||||
|
} = opts; |
||||
|
let v = null; |
||||
|
const outer = data[keypath[0]]; |
||||
|
|
||||
|
if (Array.isArray(outer)) { |
||||
|
const values = loop(data, keypath, []); |
||||
|
v = rawdata ? values : values.join(spliter || ','); |
||||
|
} else { |
||||
|
v = fromJS(data).getIn(keypath); |
||||
|
} |
||||
|
// 处理render
|
||||
|
if (render && typeof render === 'function') { |
||||
|
v = render(outer, data); |
||||
|
} |
||||
|
return v; |
||||
|
}; |
||||
|
const getDataSource = (attrs, filterData) => { |
||||
|
// let token = JSON.parse(sessionStorage.getItem('user')).token;
|
||||
|
const dataSource = filterData.map((item) => { |
||||
|
const record = {}; |
||||
|
attrs.forEach((attr) => { |
||||
|
const { |
||||
|
key, dataIndex, render, child, |
||||
|
} = attr; |
||||
|
if (child) { |
||||
|
record[key] = getDataSource(child, item[key]); |
||||
|
} else { |
||||
|
const v = getColumnData({ |
||||
|
data: item, |
||||
|
keypath: dataIndex || [key], |
||||
|
render: render || null, |
||||
|
}); |
||||
|
record[key] = v; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return record; |
||||
|
}); |
||||
|
return dataSource; |
||||
|
}; |
||||
|
// 暂时只处理两层
|
||||
|
const getFlatData = (attrs, filterData, dataToAoa, deep = 0) => { |
||||
|
filterData.map((item) => { |
||||
|
let cur = dataToAoa[deep]; |
||||
|
if (!cur) { |
||||
|
cur = dataToAoa[deep] = []; |
||||
|
} |
||||
|
attrs.map((attr, index) => { |
||||
|
const { key, child } = attr; |
||||
|
if (child) { |
||||
|
if (Array.isArray(item[key])) { |
||||
|
// getFlatData(child,item[key],dataToAoa,deep)
|
||||
|
|
||||
|
item[key].map((s, i) => { |
||||
|
if (i == 0) { |
||||
|
child.map((c) => { |
||||
|
cur.push(s[c.key]); |
||||
|
}); |
||||
|
} else { |
||||
|
deep++; |
||||
|
const childCur = dataToAoa[deep] = []; |
||||
|
pushNull(childCur, index); |
||||
|
child.map((c) => { |
||||
|
childCur.push(s[c.key]); |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} else { |
||||
|
cur.push(item[key]); |
||||
|
} |
||||
|
}); |
||||
|
deep++; |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const getHeader = (headers, excelHeader, deep, perOffset) => { |
||||
|
let offset = 0; |
||||
|
let cur = excelHeader[deep]; |
||||
|
if (!cur) { |
||||
|
cur = excelHeader[deep] = []; |
||||
|
} |
||||
|
pushNull(cur, perOffset - cur.length); |
||||
|
for (let i = 0; i < headers.length; i++) { |
||||
|
const head = headers[i]; |
||||
|
cur.push(head.name); |
||||
|
if (head.hasOwnProperty('child') && Array.isArray(head.child) && head.child.length > 0) { |
||||
|
const childOffset = getHeader(head.child, excelHeader, deep + 1, cur.length - 1); |
||||
|
pushNull(cur, childOffset - 1); |
||||
|
offset += childOffset; |
||||
|
} else { |
||||
|
offset++; |
||||
|
} |
||||
|
} |
||||
|
return offset; |
||||
|
}; |
||||
|
|
||||
|
const pushNull = (arr, count) => { |
||||
|
for (let i = 0; i < count; i++) { |
||||
|
arr.push(null); |
||||
|
} |
||||
|
}; |
||||
|
const fillNull = (arr) => { |
||||
|
const max = Math.max(...(arr.map((a) => a.length))); |
||||
|
arr.filter((e) => e.length < max).forEach((e) => pushNull(e, max - e.length)); |
||||
|
}; |
||||
|
const doMerges = (arr) => { |
||||
|
// 要么横向合并 要么纵向合并
|
||||
|
const deep = arr.length; |
||||
|
const merges = []; |
||||
|
for (let y = 0; y < deep; y++) { |
||||
|
// 先处理横向合并
|
||||
|
const row = arr[y]; |
||||
|
let colSpan = 0; |
||||
|
for (let x = 0; x < row.length; x++) { |
||||
|
if (row[x] === null) { |
||||
|
colSpan++; |
||||
|
if (((x + 1) === row.length) && (colSpan > 0 && x > colSpan)) { |
||||
|
merges.push({ s: { r: y, c: x - colSpan }, e: { r: y, c: x } }); |
||||
|
} |
||||
|
} else if (colSpan > 0 && x > colSpan) { |
||||
|
merges.push({ s: { r: y, c: x - colSpan - 1 }, e: { r: y, c: x - 1 } }); |
||||
|
colSpan = 0; |
||||
|
} else { |
||||
|
colSpan = 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// 再处理纵向合并
|
||||
|
const colLength = arr[0].length; |
||||
|
for (let x = 0; x < colLength; x++) { |
||||
|
let rowSpan = 0; |
||||
|
for (let y = 0; y < deep; y++) { |
||||
|
if (arr[y][x] != null) { |
||||
|
rowSpan = 0; |
||||
|
} else { |
||||
|
rowSpan++; |
||||
|
} |
||||
|
} |
||||
|
if (rowSpan > 0) { |
||||
|
merges.push({ s: { r: deep - rowSpan - 1, c: x }, e: { r: deep - 1, c: x } }); |
||||
|
} |
||||
|
} |
||||
|
return merges; |
||||
|
}; |
||||
|
// 内容暂只出了纵向合并
|
||||
|
const doContetMerges = (arr, headerLength) => { |
||||
|
const deep = arr.length; |
||||
|
const merges = []; |
||||
|
// 处理纵向合并
|
||||
|
const colLength = arr[0].length; |
||||
|
for (let x = 0; x < colLength; x++) { |
||||
|
let rowSpan = 0; |
||||
|
const mergY = 0; |
||||
|
for (let y = 0; y < deep; y++) { |
||||
|
if (rowSpan > 0) { |
||||
|
// 如果还有null 继续加
|
||||
|
if (arr[y][x] === null) { |
||||
|
rowSpan += 1; |
||||
|
} else { |
||||
|
// 不为null 增加merge
|
||||
|
merges.push({ s: { r: headerLength + (y - rowSpan - 1), c: x }, e: { r: headerLength + y - 1, c: x } }); |
||||
|
rowSpan = 0; |
||||
|
} |
||||
|
} else if (arr[y][x] === null) { |
||||
|
rowSpan += 1; |
||||
|
} |
||||
|
} |
||||
|
if (rowSpan > 0) { |
||||
|
merges.push({ s: { r: headerLength + (deep - rowSpan - 1), c: x }, e: { r: headerLength + deep - 1, c: x } }); |
||||
|
rowSpan = 0; |
||||
|
} |
||||
|
} |
||||
|
return merges; |
||||
|
}; |
||||
|
const exportMergeExcel = async () => { |
||||
|
setExportLoading(true); |
||||
|
const { |
||||
|
column, data, fileName, exportUrl, exportQuery, exportBody, requestType, header, showYearMouth, |
||||
|
} = props || {}; |
||||
|
|
||||
|
let resultData = []; |
||||
|
if (exportUrl) { |
||||
|
resultData = requestType == 'post' ? await Request.post(exportUrl, exportBody || {}, exportQuery || {}).then((data) => { |
||||
|
// 数据接口返回的结果 如果是对象 必须把返回数组放入rows
|
||||
|
if (typeof data === 'object' && data.rows) { |
||||
|
return data.rows; |
||||
|
} |
||||
|
return data; |
||||
|
}, (err) => { |
||||
|
message.error('获取数据失败,导出失败!'); |
||||
|
}) : await Request.get(exportUrl, exportQuery || {}).then((data) => { |
||||
|
if (typeof data === 'object' && data.rows) { |
||||
|
return data.rows; |
||||
|
} |
||||
|
return data; |
||||
|
}, (err) => { |
||||
|
message.error('获取数据失败,导出失败!'); |
||||
|
}); |
||||
|
if (!resultData) { |
||||
|
return; |
||||
|
} |
||||
|
} else { |
||||
|
resultData = data; |
||||
|
} |
||||
|
const excelHeader = []; |
||||
|
getHeader(column, excelHeader, 0, 0); |
||||
|
fillNull(excelHeader); |
||||
|
|
||||
|
// console.log(excelHeader);
|
||||
|
|
||||
|
const loopData = getDataSource(column, resultData); |
||||
|
// console.log(loopData)
|
||||
|
|
||||
|
const dataToAoa = []; |
||||
|
getFlatData(column, loopData, dataToAoa, 0); |
||||
|
fillNull(dataToAoa); |
||||
|
// console.log(dataToAoa);
|
||||
|
|
||||
|
const aoa = [].concat(excelHeader, dataToAoa); |
||||
|
// console.log(aoa)
|
||||
|
|
||||
|
const headerMerges = doMerges(excelHeader); |
||||
|
const contentMerages = doContetMerges(dataToAoa, excelHeader.length); |
||||
|
const merges = [].concat(headerMerges, contentMerages); |
||||
|
// console.log(contentMerages)
|
||||
|
|
||||
|
// let opts = {
|
||||
|
// defaultCellStyle: {
|
||||
|
// font: { name: "宋体", sz: 11, color: { auto: 1 } },
|
||||
|
// border: {
|
||||
|
// color: { auto: 1 }
|
||||
|
// },
|
||||
|
// alignment: {
|
||||
|
// /// 自动换行
|
||||
|
// wrapText: 1,
|
||||
|
// // 居中
|
||||
|
// horizontal: "center",
|
||||
|
// vertical: "center",
|
||||
|
// indent: 0
|
||||
|
// }
|
||||
|
// }
|
||||
|
// }
|
||||
|
const sheet = XLSX.utils.aoa_to_sheet(aoa); |
||||
|
// let newSheet = {};
|
||||
|
// for (let [key, value] of Object.entries(sheet)) {
|
||||
|
// if(key == '!ref'){
|
||||
|
// newSheet[key] = value
|
||||
|
// }else if(typeof value === 'object'){
|
||||
|
// newSheet[key] = {
|
||||
|
// ...value,
|
||||
|
// s: opts.defaultCellStyle
|
||||
|
// }
|
||||
|
// }
|
||||
|
// }
|
||||
|
const wpx = column.map((c) => ({ |
||||
|
wpx: Number.parseInt(c.wpx) ? Number.parseInt(c.wpx) : 100, |
||||
|
})); |
||||
|
sheet['!cols'] = wpx; |
||||
|
sheet['!merges'] = merges; |
||||
|
|
||||
|
// 构建 workbook 对象
|
||||
|
const workbook = XLSX.utils.book_new(); |
||||
|
|
||||
|
const time = moment().format('YYYY-MM-DD'); |
||||
|
|
||||
|
XLSX.utils.book_append_sheet(workbook, sheet, 'mySheet'); |
||||
|
// 导出 Excel
|
||||
|
XLSX.writeFile(workbook, fileName ? `${fileName}-${time}.xlsx` : '导出数据.xlsx'); |
||||
|
setExportLoading(false); |
||||
|
// message.success(`成功导出了 ${loopData.length || 0} 条数据`);
|
||||
|
}; |
||||
|
|
||||
|
const exportProExcel = async () => { |
||||
|
setExportLoading(true); |
||||
|
const { |
||||
|
column, data, fileName, exportUrl, exportQuery, exportBody, requestType, showYearMouth, |
||||
|
} = props || {}; |
||||
|
let resultData = []; |
||||
|
if (exportUrl) { |
||||
|
resultData = requestType == 'post' ? await Request.post(exportUrl, exportBody || {}, exportQuery || {}).then((data) => { |
||||
|
// 数据接口返回的结果 如果是对象 必须把返回数组放入rows
|
||||
|
if (typeof data === 'object') { |
||||
|
return data.data ? data.data : data.rows; |
||||
|
} |
||||
|
return data; |
||||
|
}, (err) => { |
||||
|
message.error('获取数据失败,导出失败!'); |
||||
|
}) : await Request.get(exportUrl, exportQuery || {}).then((data) => { |
||||
|
if (showYearMouth) { |
||||
|
|
||||
|
} |
||||
|
if (typeof data === 'object' && data.rows) { |
||||
|
return data.rows; |
||||
|
} |
||||
|
return data; |
||||
|
}, (err) => { |
||||
|
message.error('获取数据失败,导出失败!'); |
||||
|
}); |
||||
|
if (!resultData) { |
||||
|
return; |
||||
|
} |
||||
|
} else { |
||||
|
resultData = data; |
||||
|
} |
||||
|
|
||||
|
const loopData = getDataSource(column, resultData); |
||||
|
|
||||
|
let content = ''; |
||||
|
let header = '<tr>'; |
||||
|
// header += `<th><div>序号</div></th>`;
|
||||
|
column.map((colum) => { |
||||
|
header += `<th><div>${colum.name}</div></th>`; |
||||
|
}); |
||||
|
header += '</tr>'; |
||||
|
loopData.map((data) => { |
||||
|
content += '<tr>'; |
||||
|
column.map((c) => { |
||||
|
if (c.style) { |
||||
|
content += `<th style="${c.style}"><div>${data[c.dataIndex || c.key]}</div></th>`; |
||||
|
} else { |
||||
|
content += `<th><div>${data[c.dataIndex || c.key]}</div></th>`; |
||||
|
} |
||||
|
}); |
||||
|
content += '</tr>'; |
||||
|
}); |
||||
|
|
||||
|
const exportTable = `\uFEFF
|
||||
|
<table style='text-alagin:center' border="1"> |
||||
|
${header} |
||||
|
${content} |
||||
|
</table> |
||||
|
`;
|
||||
|
const time = moment().format('YYYY-MM-DD'); |
||||
|
const tempStrs = new Blob([exportTable], { type: 'text/xls' }); |
||||
|
FileSaver.saveAs(tempStrs, fileName ? `${fileName}-${time}.xls` : '导出数据.xls'); |
||||
|
setExportLoading(false); |
||||
|
// message.success(`成功导出了 ${loopData.length || 0} 条数据`);
|
||||
|
}; |
||||
|
|
||||
|
const exportExcel = async () => { |
||||
|
setExportLoading(true); |
||||
|
const { |
||||
|
column, data, fileName, exportUrl, exportQuery, exportBody, requestType, |
||||
|
} = props || {}; |
||||
|
const _headers = column |
||||
|
.map((item, i) => ({ key: item.key, title: item.name, position: String.fromCharCode(65 + i) + 1 })) |
||||
|
.reduce((prev, next) => ({ ...prev, [next.position]: { key: next.key, v: next.title } }), {}); |
||||
|
let resultData = []; |
||||
|
if (exportUrl) { |
||||
|
resultData = requestType == 'post' ? await Request.post(exportUrl, exportBody || {}, exportQuery || {}).then((data) => { |
||||
|
// 数据接口返回的结果 如果是对象 必须把返回数组放入rows
|
||||
|
|
||||
|
if (typeof data === 'object' && (data.rows || data.data)) { |
||||
|
return data.data ? data.data : data.rows; |
||||
|
} |
||||
|
return data; |
||||
|
}, (err) => { |
||||
|
message.error('获取数据失败,导出失败!'); |
||||
|
}) : await Request.get(exportUrl, exportQuery || {}).then((data) => { |
||||
|
if (typeof data === 'object' && data.rows) { |
||||
|
return data.rows; |
||||
|
} |
||||
|
return data; |
||||
|
}, (err) => { |
||||
|
message.error('获取数据失败,导出失败!'); |
||||
|
}); |
||||
|
if (!resultData) { |
||||
|
return; |
||||
|
} |
||||
|
} else { |
||||
|
resultData = data; |
||||
|
} |
||||
|
|
||||
|
const loopDate = getDataSource(column, resultData); |
||||
|
|
||||
|
const wpx = column.map((c) => ({ |
||||
|
wpx: Number.parseInt(c.wpx) ? Number.parseInt(c.wpx) : 100, |
||||
|
})); |
||||
|
if (!(loopDate.length > 0)) { |
||||
|
setExportLoading(false); |
||||
|
return; |
||||
|
} |
||||
|
const _data = loopDate |
||||
|
.map((item, i) => column.map((key, j) => ({ content: item[key.key], position: String.fromCharCode(65 + j) + (i + 2) }))) |
||||
|
// 对刚才的结果进行降维处理(二维数组变成一维数组)
|
||||
|
.reduce((prev, next) => prev.concat(next)) |
||||
|
// 转换成 worksheet 需要的结构
|
||||
|
.reduce((prev, next) => ({ ...prev, [next.position]: { v: next.content } }), {}); |
||||
|
|
||||
|
// 合并 column 和 data
|
||||
|
const output = { ..._headers, ..._data }; |
||||
|
// 获取所有单元格的位置
|
||||
|
const outputPos = Object.keys(output); |
||||
|
// 计算出范围 ,["A1",..., "H2"]
|
||||
|
const ref = `${outputPos[0]}:${outputPos[outputPos.length - 1]}`; |
||||
|
|
||||
|
// 构建 workbook 对象
|
||||
|
const workbook = { |
||||
|
SheetNames: ['mySheet'], |
||||
|
Sheets: { |
||||
|
mySheet: { |
||||
|
|
||||
|
...output, |
||||
|
'!ref': ref, |
||||
|
'!cols': wpx, |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
const time = moment().format('YYYY-MM-DD'); |
||||
|
// 导出 Excel
|
||||
|
XLSX.writeFile(workbook, fileName ? `${fileName}-${time}.xlsx` : '导出数据.xlsx'); |
||||
|
setExportLoading(false); |
||||
|
// message.success(`成功导出了 ${loopDate.length || 0} 条数据`);
|
||||
|
}; |
||||
|
|
||||
|
const exportTemplete = async () => { |
||||
|
const { importTemColumn, importTemData, fileName } = props || {}; |
||||
|
const _headers = importTemColumn |
||||
|
.map((item, i) => { |
||||
|
let group = 0; // 用于处理Z1的时候,重计算AA1
|
||||
|
if (parseInt(i / 26) > group) { |
||||
|
group = parseInt(i / 26); |
||||
|
} |
||||
|
if (group > 0) { // AA1 BA1 CA1
|
||||
|
const position = String.fromCharCode(65 + (group - 1)); |
||||
|
return { key: item.key, title: item.name, position: position + String.fromCharCode(65 + (i % 26)) + 1 }; |
||||
|
} return { key: item.key, title: item.name, position: String.fromCharCode(65 + i) + 1 }; |
||||
|
}) |
||||
|
.reduce((prev, next) => ({ ...prev, [next.position]: { key: next.key, v: next.title } }), {}); |
||||
|
|
||||
|
const loopDate = getDataSource(importTemColumn, importTemData); |
||||
|
|
||||
|
const wpx = importTemColumn.map((c) => ({ |
||||
|
wpx: Number.parseInt(c.wpx) ? Number.parseInt(c.wpx) : 100, |
||||
|
})); |
||||
|
const _data = loopDate.length ? loopDate |
||||
|
.map((item, i) => importTemColumn.map((key, j) => ({ content: item[key.key], position: String.fromCharCode(65 + j) + (i + 2) }))) |
||||
|
// 对刚才的结果进行降维处理(二维数组变成一维数组)
|
||||
|
.reduce((prev, next) => prev.concat(next)) |
||||
|
// 转换成 worksheet 需要的结构
|
||||
|
.reduce((prev, next) => ({ ...prev, [next.position]: { v: next.content } }), {}) : []; |
||||
|
// 合并 column 和 data
|
||||
|
const output = { ..._headers, ..._data }; |
||||
|
// 获取所有单元格的位置
|
||||
|
const outputPos = Object.keys(output); |
||||
|
// 计算出范围 ,["A1",..., "H2"]
|
||||
|
const ref = `${outputPos[0]}:${outputPos[outputPos.length - 1]}`; |
||||
|
|
||||
|
// 构建 workbook 对象
|
||||
|
const workbook = { |
||||
|
SheetNames: ['mySheet'], |
||||
|
Sheets: { |
||||
|
mySheet: { |
||||
|
|
||||
|
...output, |
||||
|
'!ref': ref, |
||||
|
'!cols': wpx, |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
// 导出 Excel
|
||||
|
XLSX.writeFile(workbook, fileName ? `${fileName}-导入模板.xlsx` : '导入模板.xlsx'); |
||||
|
}; |
||||
|
const tips = (type) => { |
||||
|
const { tips, templeteBth = true } = props; |
||||
|
const description = ( |
||||
|
<div className="export-import"> |
||||
|
{tips && tips} |
||||
|
<Row gutter={16}> |
||||
|
<Col span={12}> |
||||
|
<Form form={form} initialValues={{}}> |
||||
|
<Form.Item name="import-file"> |
||||
|
<Input className="file-uploader" type="file" accept=".xlsx, .xls" onChange={(e) => importExcel(e, type)} /> |
||||
|
<Button style={props.btnStyle} className={props.btnClass} loading={importLoading}> |
||||
|
选择文件 |
||||
|
</Button> |
||||
|
</Form.Item> |
||||
|
</Form> |
||||
|
</Col> |
||||
|
{templeteBth && ( |
||||
|
<Col span={12}> |
||||
|
<Button style={props.btnStyle} className={props.btnClass} onClick={exportTemplete}> |
||||
|
模板下载 |
||||
|
</Button> |
||||
|
</Col> |
||||
|
)} |
||||
|
</Row> |
||||
|
</div> |
||||
|
); |
||||
|
|
||||
|
notification.info({ |
||||
|
message: '支持 .xlsx、.xls 格式的文件', |
||||
|
description, |
||||
|
key: 'import-notification', |
||||
|
duration: null, |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<Space> |
||||
|
{ |
||||
|
props.import && ( |
||||
|
<Button style={props.btnStyle} className={props.btnClass} loading={importLoading} onClick={tips}> |
||||
|
{props.importBtnName || '导入'} |
||||
|
</Button> |
||||
|
) |
||||
|
} |
||||
|
{ |
||||
|
props.export && ( |
||||
|
<Tooltip placement="top" title={props.exportBtnTips || '默认导出所有数据'}> |
||||
|
<Button style={props.btnStyle} className={props.btnClass} loading={exportLoading} onClick={props.exportType === 'pro' ? exportProExcel : exportExcel}> |
||||
|
{props.exportBtnName || '导出'} |
||||
|
</Button> |
||||
|
</Tooltip> |
||||
|
) |
||||
|
} |
||||
|
</Space> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
ExportAndImport.propTypes = { |
||||
|
export: PropTypes.bool, // 是否显示导出按钮
|
||||
|
exportBtnName: PropTypes.string, // 导出按钮文字
|
||||
|
importBtnName: PropTypes.string, // 导入按钮文字
|
||||
|
import: PropTypes.bool, // 是否显示导入按钮
|
||||
|
variedImport: PropTypes.bool, // 是否显示多样导入
|
||||
|
variedImportDisable: PropTypes.bool, // 多样导入禁用
|
||||
|
variedImportBtnName: PropTypes.string, // 多样导入文字
|
||||
|
column: PropTypes.array, // 导出显示的header数组 兼容antd column 可直接拿table的column使用 注:column每列的属性wpx设置导出的execl每列的宽度值 默认 100
|
||||
|
data: PropTypes.array, // 导出的数据 兼容antd table 数组嵌套处理
|
||||
|
exportUrl: PropTypes.string, // 导出数据从接口获取的url地址 返回的数据1、数组必须支持column的设置 ,2、如果是对象,数组需放在rows属性上
|
||||
|
exportBody: PropTypes.object, // 导出数据接口body参数
|
||||
|
exportQuery: PropTypes.object, // 导出数据从接口获取的url地址上的参数
|
||||
|
exportBtnTips: PropTypes.string, // 导出按钮tips文字提示
|
||||
|
importUrl: PropTypes.string, // 导入接口url
|
||||
|
importQuery: PropTypes.object, // 导入接口url地址上的参数
|
||||
|
btnClass: PropTypes.string, // 按钮className
|
||||
|
btnStyle: PropTypes.object, // 按钮style
|
||||
|
tips: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), // 上传文件提示的信息
|
||||
|
onImportSucess: PropTypes.func, // 上传成功后 返回处理函数
|
||||
|
importTemColumn: PropTypes.array, // 导入模板设置 头部字段数组
|
||||
|
importTemData: PropTypes.array, // 导入模板默认数据
|
||||
|
requestType: PropTypes.string, // 请求类型
|
||||
|
importDataCallback: PropTypes.func, // 上传后数据返回
|
||||
|
templeteBth: PropTypes.bool, // 模板按钮
|
||||
|
importRequest: PropTypes.bool, // 请求导入接口,false时搭配importDataCallback,
|
||||
|
exportType: PropTypes.string, // 导出执行的函数名
|
||||
|
}; |
||||
|
|
||||
|
export default ExportAndImport; |
@ -0,0 +1,13 @@ |
|||||
|
.export-import { |
||||
|
.file-uploader { |
||||
|
position: absolute; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
outline: none; |
||||
|
opacity: 0; |
||||
|
background-color: transparent; |
||||
|
z-index: 10; |
||||
|
} |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
import Upload from './Upload'; |
||||
|
import Uploads from './Uploads'; |
||||
|
import NoResource from './no-resource'; |
||||
|
import ExportAndImport from './export'; |
||||
|
import ButtonGroup from './buttonGroup'; |
||||
|
import UploadLocal from './UploadLocal'; |
||||
|
|
||||
|
export { |
||||
|
Upload, |
||||
|
Uploads, |
||||
|
NoResource, |
||||
|
ExportAndImport, |
||||
|
ButtonGroup, |
||||
|
UploadLocal |
||||
|
}; |
@ -0,0 +1,21 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import React from 'react'; |
||||
|
import { Result} from 'antd'; |
||||
|
import { MehOutlined } from '@ant-design/icons'; |
||||
|
class NoResource extends React.Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
} |
||||
|
render() { |
||||
|
const title = this.props.title ? this.props.title : "抱歉,没有可访问的资源!" |
||||
|
return ( |
||||
|
<Result |
||||
|
icon={<MehOutlined />} |
||||
|
title={title} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default NoResource; |
@ -0,0 +1,7 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import React from 'react'; |
||||
|
import { render } from 'react-dom'; |
||||
|
import App from './app'; |
||||
|
|
||||
|
render((<App projectName="政务数据资源中心" />), document.getElementById('App')); |
@ -0,0 +1,27 @@ |
|||||
|
'use strict'; |
||||
|
import { RouteRequest } from '@peace/utils'; |
||||
|
import { RouteTable } from '$utils' |
||||
|
|
||||
|
export const INIT_LAYOUT = 'INIT_LAYOUT'; |
||||
|
export function initLayout (title, copyright, sections, actions) { |
||||
|
return { |
||||
|
type: INIT_LAYOUT, |
||||
|
payload: { |
||||
|
title, |
||||
|
copyright, |
||||
|
sections, |
||||
|
actions |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export const RESIZE = 'RESIZE'; |
||||
|
export function resize (clientHeight, clientWidth) { |
||||
|
return { |
||||
|
type: RESIZE, |
||||
|
payload: { |
||||
|
clientHeight, |
||||
|
clientWidth |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,15 @@ |
|||||
|
'use strict'; |
||||
|
import React from 'react'; |
||||
|
import style from './style.css'; |
||||
|
|
||||
|
export default class Footer extends React.Component { |
||||
|
render() { |
||||
|
const {footerProps} = this.props; |
||||
|
|
||||
|
return ( |
||||
|
<div className={style.footer} {...footerProps}> |
||||
|
{this.props.children} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,5 @@ |
|||||
|
.footer { |
||||
|
text-align: center; |
||||
|
font-size: 12px; |
||||
|
color: #999; |
||||
|
} |
@ -0,0 +1,100 @@ |
|||||
|
'use strict'; |
||||
|
import React from 'react'; |
||||
|
import { Menu } from 'antd'; |
||||
|
import { Link } from 'react-router-dom'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import styles from './style.css'; |
||||
|
import { |
||||
|
MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined, LogoutOutlined |
||||
|
} from '@ant-design/icons'; |
||||
|
import ResetPasswordModal from '../../../sections/memberManagement/components/resetPassword'; |
||||
|
const Header = props => { |
||||
|
const { dispatch, history, user, pathname, toggleCollapsed, collapsed, actions } = props |
||||
|
|
||||
|
const onFinish = async (values) => { |
||||
|
const dataToSave = { ...values } |
||||
|
return dispatch( |
||||
|
actions.memberManagement.modifyUser(user.id, dataToSave, values?.msg || ''), |
||||
|
).then((res) => { |
||||
|
if (res.success) { |
||||
|
return true; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const handelClick = item => { |
||||
|
if (item.key == 'logout') { |
||||
|
dispatch(actions.auth.logout(user)); |
||||
|
history.push(`/signin`); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let current = pathname; |
||||
|
if (pathname == '/' || pathname == '') { |
||||
|
current = 'default'; |
||||
|
} else if (pathname.charAt(0) == '/') { |
||||
|
current = pathname.substring(1); |
||||
|
} |
||||
|
|
||||
|
if (current.indexOf('/') != -1) { |
||||
|
current = current.substring(0, current.indexOf('/')); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div className={styles.header}> |
||||
|
<div className={styles['header-fold']}> |
||||
|
<span onClick={toggleCollapsed} style={{ marginRight: 20 }}> |
||||
|
{ |
||||
|
collapsed ? |
||||
|
<MenuUnfoldOutlined /> : <MenuFoldOutlined /> |
||||
|
} |
||||
|
</span> |
||||
|
<div className={styles['header-title']} style={{}}> |
||||
|
<img src='/assets/images/favicon.ico' style={{ margin: '0 12px 4px 12px', height: 42, borderRadius: 4 }} />政务数据资源中心 |
||||
|
</div> |
||||
|
</div> |
||||
|
<div id="nav" className={styles['header-nav']}> |
||||
|
<Menu |
||||
|
mode='horizontal' |
||||
|
selectedKeys={[current]} |
||||
|
style={{ border: 0, background: '#1890ff' }} |
||||
|
onClick={handelClick} |
||||
|
theme={'light'} |
||||
|
items={[{ |
||||
|
label: <span style={{ color: 'aliceblue' }}>{user.displayName}</span>, |
||||
|
key: "user", |
||||
|
icon: <img className={styles['header-nav-user-img']} src={`/assets/images/avatar/5.png`} />, |
||||
|
children: [ |
||||
|
{ |
||||
|
icon: <UserOutlined />, |
||||
|
label: <ResetPasswordModal |
||||
|
editData={user} |
||||
|
triggerRender={<a>修改密码</a>} |
||||
|
title="修改密码" |
||||
|
onFinish={onFinish} |
||||
|
key="resetPassword" |
||||
|
/>, |
||||
|
key: 'resetPassword' |
||||
|
}, |
||||
|
{ |
||||
|
label: '退出', key: 'logout', icon: <LogoutOutlined /> |
||||
|
}, |
||||
|
], |
||||
|
}]} |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
function mapStateToProps(state) { |
||||
|
const { global, auth } = state; |
||||
|
return { |
||||
|
actions: global.actions, |
||||
|
user: auth.user |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(Header); |
@ -0,0 +1,41 @@ |
|||||
|
.header { |
||||
|
position: relative; |
||||
|
height: 65px; |
||||
|
min-width: 520px; |
||||
|
background-color: #1890ff; |
||||
|
} |
||||
|
|
||||
|
.header-fold { |
||||
|
float: left; |
||||
|
padding-left: 32px; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
.header-title { |
||||
|
line-height: 60px; |
||||
|
display: inline-block; |
||||
|
font-size: 20px; |
||||
|
color: #fff; |
||||
|
text-shadow: 0 4px 3px rgba(54, 77, 108, 0.20); |
||||
|
} |
||||
|
|
||||
|
.header-nav { |
||||
|
float: right; |
||||
|
} |
||||
|
|
||||
|
.header-nav-notification { |
||||
|
/* color : #666; */ |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
.header-nav-user-img-wrapper { |
||||
|
display: inline; |
||||
|
margin: 14px 8px; |
||||
|
} |
||||
|
|
||||
|
.header-nav-user-img { |
||||
|
width: 36px; |
||||
|
height: 36px; |
||||
|
position: relative; |
||||
|
bottom: 2px; |
||||
|
} |
@ -0,0 +1,95 @@ |
|||||
|
import React, { Component, useEffect, useState } from 'react'; |
||||
|
import { Menu } from 'antd'; |
||||
|
const JumpUrls = [ |
||||
|
{ url: '/risk/hiddenrectification_approval', keys: 'riskHiddenrectification_approval' }, |
||||
|
{ url: '/safetymanage/hiddenrectification_approval', keys: 'hiddenrectification_approval' }, |
||||
|
{ url: '/metadataManagement/latestMetadata', keys: 'latestMetadata' }, |
||||
|
] |
||||
|
const Sider = (props) => { |
||||
|
const [items, setItems] = useState([]) |
||||
|
const [selectedKeys, setSelectedKeys] = useState([]) |
||||
|
const [openKeys, setOpenKeys] = useState([]) |
||||
|
const { pathname } = props; |
||||
|
useEffect(() => { |
||||
|
let jumpurlObj = JumpUrls.find(s => s.url == pathname && selectedKeys != s.keys) |
||||
|
if (jumpurlObj) { |
||||
|
localStorage.setItem('governmentDataResourceCenter_selected_sider', JSON.stringify([jumpurlObj.keys])) |
||||
|
setSelectedKeys(jumpurlObj.keys) |
||||
|
} |
||||
|
if (pathname.indexOf('metadataManagement/latestMetadata') < 0) |
||||
|
sessionStorage.removeItem('jumpSelectedKey'); |
||||
|
if (pathname.indexOf('metadataManagement/businessMetadata') < 0) |
||||
|
sessionStorage.removeItem('jumpBusinessSelectedKey'); |
||||
|
}, [pathname]) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const { sections, dispatch, user } = props; |
||||
|
let items = sections.reduce((p, c) => { |
||||
|
if (typeof c.getNavItem == 'function') { |
||||
|
let item = c.getNavItem(user, dispatch); |
||||
|
if (item != null) { |
||||
|
if (Array.isArray(item)) { |
||||
|
p = p.concat(item); |
||||
|
} else { |
||||
|
p.push(item); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return p; |
||||
|
}, []); |
||||
|
setItems(items) |
||||
|
|
||||
|
let selectedKeys = [] |
||||
|
let openKeys = [] |
||||
|
const lastSelectedKeys = localStorage.getItem('governmentDataResourceCenter_selected_sider') |
||||
|
if (lastSelectedKeys) { |
||||
|
selectedKeys = JSON.parse(lastSelectedKeys) |
||||
|
} |
||||
|
const lastOpenKeys = localStorage.getItem('governmentDataResourceCenter_open_sider') |
||||
|
if (lastOpenKeys) { |
||||
|
openKeys = JSON.parse(lastOpenKeys) |
||||
|
} |
||||
|
if (!selectedKeys.length && !openKeys.length) { |
||||
|
let firstItem = items[0] || null |
||||
|
|
||||
|
if (firstItem) { |
||||
|
let children = firstItem.props.children |
||||
|
if (Array.isArray(children)) { |
||||
|
selectedKeys = [children[0].key] |
||||
|
openKeys = [firstItem.key] |
||||
|
} else if (children.key) { |
||||
|
selectedKeys = [children.key] |
||||
|
openKeys = [firstItem.key] |
||||
|
} else { |
||||
|
selectedKeys = [firstItem.key] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
localStorage.setItem('governmentDataResourceCenter_selected_sider', JSON.stringify(selectedKeys)) |
||||
|
setSelectedKeys(selectedKeys) |
||||
|
localStorage.setItem('governmentDataResourceCenter_open_sider', JSON.stringify(openKeys)) |
||||
|
setOpenKeys(openKeys) |
||||
|
}, []) |
||||
|
|
||||
|
|
||||
|
return ( |
||||
|
<Menu id="sider" mode="inline" |
||||
|
theme={'light'} |
||||
|
selectedKeys={selectedKeys} |
||||
|
openKeys={openKeys} |
||||
|
onSelect={(e) => { |
||||
|
const { selectedKeys } = e; |
||||
|
setSelectedKeys(selectedKeys) |
||||
|
localStorage.setItem('governmentDataResourceCenter_selected_sider', JSON.stringify(selectedKeys)) |
||||
|
}} |
||||
|
onOpenChange={(openKeys) => { |
||||
|
setOpenKeys(openKeys) |
||||
|
localStorage.setItem('governmentDataResourceCenter_open_sider', JSON.stringify(openKeys)) |
||||
|
}} |
||||
|
> |
||||
|
{items} |
||||
|
</Menu> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Sider; |
@ -0,0 +1,6 @@ |
|||||
|
'use strict'; |
||||
|
import Layout from './layout'; |
||||
|
import NoMatch from './no-match'; |
||||
|
|
||||
|
export { Layout }; |
||||
|
export { NoMatch }; |
@ -0,0 +1,48 @@ |
|||||
|
import React from 'react'; |
||||
|
import { Breadcrumb } from 'antd'; |
||||
|
import withBreadcrumbs from 'react-router-breadcrumbs-hoc'; |
||||
|
import { Link } from 'react-router-dom'; |
||||
|
|
||||
|
const extRoutes = [{ path: '/project-monitor/things/struct/:id/configuration/station', breadcrumb: '测点' }]; |
||||
|
|
||||
|
function Breadcrumbs(props) { |
||||
|
const excludePaths = [ |
||||
|
'/', |
||||
|
'/metadataManagement/latestMetadata/detail', |
||||
|
'/metadataAcquisition/adapter/detail', |
||||
|
]; |
||||
|
|
||||
|
const { routes } = props; |
||||
|
|
||||
|
const Bread = withBreadcrumbs(routes.concat(extRoutes), { excludePaths })(({ breadcrumbs }) => ( |
||||
|
<Breadcrumb separator="/" style={{ height: 25 }}> |
||||
|
{ |
||||
|
breadcrumbs.map((bc, index) => ( |
||||
|
<Breadcrumb.Item key={index}> |
||||
|
{ |
||||
|
bc.component |
||||
|
? ( |
||||
|
<Link |
||||
|
to={{ |
||||
|
pathname: bc.match.url, |
||||
|
state: bc.match.params ? bc.match.params : {}, |
||||
|
query: bc.location.query ? bc.location.query : {}, |
||||
|
}} |
||||
|
> |
||||
|
{bc.breadcrumb} |
||||
|
</Link> |
||||
|
) |
||||
|
: bc.breadcrumb |
||||
|
} |
||||
|
</Breadcrumb.Item> |
||||
|
)) |
||||
|
} |
||||
|
</Breadcrumb> |
||||
|
)); |
||||
|
|
||||
|
return ( |
||||
|
<Bread /> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default Breadcrumbs; |
@ -0,0 +1,132 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import './index.less'; |
||||
|
import React, { useState, useEffect } from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { message, Layout } from 'antd'; |
||||
|
import Sider from '../../components/sider'; |
||||
|
import Header from '../../components/header'; |
||||
|
import Footer from '../../components/footer'; |
||||
|
import Breadcrumbs from './breadcrumb'; |
||||
|
import { resize } from '../../actions/global'; |
||||
|
import * as NProgress from 'nprogress'; |
||||
|
import PerfectScrollbar from 'perfect-scrollbar'; |
||||
|
|
||||
|
NProgress.configure({ |
||||
|
template: ` |
||||
|
<div class="bar" style="height:2px" role="bar"> |
||||
|
<div class="peg"></div> |
||||
|
</div> |
||||
|
<div class="spinner" role="spinner"> |
||||
|
<div class="spinner-icon"></div> |
||||
|
</div> |
||||
|
` |
||||
|
}); |
||||
|
|
||||
|
const headerHeight = 64 |
||||
|
const footerHeight = 0 |
||||
|
let scrollbar |
||||
|
|
||||
|
const LayoutContainer = props => { |
||||
|
const { dispatch, msg, user, copyright, children, sections, clientWidth, clientHeight, location, match, routes, history } = props |
||||
|
const [collapsed, setCollapsed] = useState(false) |
||||
|
|
||||
|
NProgress.start(); |
||||
|
|
||||
|
const resize_ = (collapsed) => { |
||||
|
const extraHeight = headerHeight + footerHeight; |
||||
|
dispatch(resize( |
||||
|
document.body.clientHeight - extraHeight - 12, |
||||
|
document.body.clientWidth - (collapsed ? 120 : 220) |
||||
|
)); |
||||
|
} |
||||
|
|
||||
|
useEffect(() => { |
||||
|
resize_(collapsed) |
||||
|
scrollbar = new PerfectScrollbar('#page-content', { suppressScrollX: true }); |
||||
|
}, []) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
NProgress.done(); |
||||
|
if (!user || !user.authorized) { |
||||
|
history.push('/signin'); |
||||
|
} |
||||
|
if (msg) { |
||||
|
message.destroy(); |
||||
|
if (msg.done) { |
||||
|
message.success(msg.done); |
||||
|
} |
||||
|
if (msg.error) { |
||||
|
message.error(msg.error); |
||||
|
} |
||||
|
} |
||||
|
const dom = document.getElementById('page-content'); |
||||
|
if (dom) { |
||||
|
scrollbar.update(); |
||||
|
dom.scrollTop = 0; |
||||
|
} |
||||
|
}) |
||||
|
// console.log(FS_API_ROOT);
|
||||
|
return ( |
||||
|
<Layout id="layout"> |
||||
|
<Layout.Header style={{ padding: 0 }}> |
||||
|
<Header |
||||
|
user={user} |
||||
|
pathname={location.pathname} |
||||
|
toggleCollapsed={() => { |
||||
|
setCollapsed(!collapsed); |
||||
|
resize_(!collapsed) |
||||
|
}} |
||||
|
collapsed={collapsed} |
||||
|
history={history} |
||||
|
/> |
||||
|
</Layout.Header> |
||||
|
<Layout> |
||||
|
<Layout.Sider trigger={null} collapsible collapsed={collapsed} theme={'light'}> |
||||
|
<Sider |
||||
|
sections={sections} |
||||
|
dispatch={dispatch} |
||||
|
user={user} |
||||
|
pathname={location.pathname} |
||||
|
collapsed={collapsed} |
||||
|
/> |
||||
|
</Layout.Sider> |
||||
|
<Layout.Content id="page-content" style={{ |
||||
|
position: 'relative', |
||||
|
margin: '12px 12px 0px', |
||||
|
padding: '8px', |
||||
|
height: clientHeight, |
||||
|
background: '#fff' |
||||
|
}}> |
||||
|
<div style={{ minWidth: 520 }}> |
||||
|
<div style={{ padding: '0px 16px 4px', borderBottom: '1px solid #e8e8e8' }}> |
||||
|
<Breadcrumbs routes={routes} /> |
||||
|
</div> |
||||
|
<div style={{ padding: '12px 12px 0px 12px' }}> |
||||
|
{children} |
||||
|
</div> |
||||
|
</div> |
||||
|
</Layout.Content> |
||||
|
{/* <Layout.Footer {...footerProps}> |
||||
|
{copyright} |
||||
|
</Layout.Footer> */} |
||||
|
</Layout> |
||||
|
</Layout> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps(state) { |
||||
|
const { global, auth, ajaxResponse } = state; |
||||
|
return { |
||||
|
title: global.title, |
||||
|
copyright: global.copyright, |
||||
|
sections: global.sections, |
||||
|
actions: global.actions, |
||||
|
clientWidth: global.clientWidth, |
||||
|
clientHeight: global.clientHeight, |
||||
|
msg: ajaxResponse.msg, |
||||
|
user: auth.user, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(LayoutContainer); |
@ -0,0 +1,3 @@ |
|||||
|
@import '~perfect-scrollbar/css/perfect-scrollbar.css'; |
||||
|
@import '~nprogress/nprogress.css'; |
||||
|
@import '~simplebar-react/dist/simplebar.min.css'; |
@ -0,0 +1,18 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import React from 'react'; |
||||
|
import moment from 'moment' |
||||
|
|
||||
|
const NoMatch = props => { |
||||
|
return ( |
||||
|
<div style={{ textAlign: 'center', padding: 120 }}> |
||||
|
<p style={{ fontSize: 80, lineHeight: 1.5 }}>404</p> |
||||
|
<p style={{ fontSize: 32, lineHeight: 2 }}>PAGE NOT FOUND</p> |
||||
|
<p>很遗憾,您暂时无法访问该页面。</p> |
||||
|
<p>请检查您访问的链接地址是否正确。</p> |
||||
|
<p style={{ marginTop: 80 }}>Copyright © {moment().year()} 飞尚</p> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default NoMatch; |
@ -0,0 +1,179 @@ |
|||||
|
'use strict'; |
||||
|
import React, { useEffect, useState } from 'react'; |
||||
|
import moment from 'moment'; |
||||
|
import configStore from './store'; |
||||
|
import { Provider } from 'react-redux'; |
||||
|
import { createBrowserHistory } from 'history'; |
||||
|
import { ConnectedRouter } from 'connected-react-router' |
||||
|
import { Layout, NoMatch } from './containers'; |
||||
|
import { Switch, Route } from "react-router-dom"; |
||||
|
import { ConfigProvider } from 'antd'; |
||||
|
import * as layoutActions from './actions/global'; |
||||
|
import zhCN from 'antd/lib/locale/zh_CN'; |
||||
|
import { basicReducer } from '@peace/utils'; |
||||
|
import 'moment/locale/zh-cn'; |
||||
|
import 'antd/dist/antd.less'; |
||||
|
|
||||
|
moment.locale('zh-cn'); |
||||
|
|
||||
|
const { initLayout } = layoutActions; |
||||
|
|
||||
|
const Root = props => { |
||||
|
const { sections, title, copyright } = props; |
||||
|
const [history, setHistory] = useState(null) |
||||
|
const [store, setStore] = useState(null) |
||||
|
const [outerRoutes, setOuterRoutes] = useState([]) |
||||
|
const [combineRoutes, setCombineRoutes] = useState([]) |
||||
|
const [innnerRoutes, setInnerRoutes] = useState([]) |
||||
|
|
||||
|
const flatRoutes = (routes) => { |
||||
|
const combineRoutes = []; |
||||
|
|
||||
|
function flat(routes, parentRoute) { |
||||
|
routes.forEach((route, i) => { |
||||
|
const obj = { |
||||
|
path: route.path, |
||||
|
breadcrumb: route.breadcrumb, |
||||
|
component: route.component || null, |
||||
|
authCode: route.authCode || '', |
||||
|
key: route.key, |
||||
|
}; |
||||
|
if (!route.path.startsWith('/')) { |
||||
|
console.error(`路由配置需以 "/" 开始:${route.path}`); |
||||
|
} |
||||
|
if (route.path.length > 1 && route.path[route.path.length] == '/') { |
||||
|
console.error(`除根路由路由配置不可以以 "/" 结束:${route.path}`); |
||||
|
} |
||||
|
if (parentRoute && parentRoute != '/') { |
||||
|
obj.path = parentRoute + route.path; |
||||
|
} |
||||
|
if (route.exact === false) { |
||||
|
obj.exact = false; |
||||
|
} |
||||
|
if (route.hasOwnProperty('childRoutes')) { |
||||
|
combineRoutes.push(obj); |
||||
|
flat(route.childRoutes, obj.path); |
||||
|
} else { |
||||
|
combineRoutes.push(obj); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
flat(routes); |
||||
|
return combineRoutes; |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
const initReducer = (reducers, reducerName, action) => { |
||||
|
let reducerParams = {} |
||||
|
const { actionType, initReducer, reducer } = action()() |
||||
|
if (initReducer || reducer) { |
||||
|
if (reducer) { |
||||
|
if (reducer.name) { |
||||
|
reducerName = reducer.name |
||||
|
} |
||||
|
if (reducer.params) { |
||||
|
reducerParams = reducer.params |
||||
|
} |
||||
|
} else { |
||||
|
reducerName = `${reducerName}Rslt` |
||||
|
} |
||||
|
reducers[reducerName] = function (state, action) { |
||||
|
return basicReducer(state, action, Object.assign({ actionType: actionType }, reducerParams)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
useEffect(() => { |
||||
|
let innerRoutes = [] |
||||
|
let outerRoutes = [] |
||||
|
let reducers = {} |
||||
|
let actions = { |
||||
|
layout: layoutActions |
||||
|
} |
||||
|
|
||||
|
for (let s of sections) { |
||||
|
if (!s.key) console.warn('请给你的section添加一个key值,section name:' + s.name); |
||||
|
for (let r of s.routes) { |
||||
|
if (r.type == 'inner' || r.type == 'home') { |
||||
|
innerRoutes.push(r.route) |
||||
|
} else if (r.type == 'outer') { |
||||
|
outerRoutes.push(r.route) |
||||
|
} |
||||
|
} |
||||
|
if (s.reducers) { |
||||
|
reducers = { ...reducers, ...s.reducers } |
||||
|
} |
||||
|
if (s.actions) { |
||||
|
actions = { ...actions, [s.key]: s.actions } |
||||
|
if (s.key != 'auth') { |
||||
|
for (let ak in s.actions) { |
||||
|
let actions = s.actions[ak] |
||||
|
if (actions && typeof actions == 'object') { |
||||
|
for (let actionName in actions) { |
||||
|
initReducer(reducers, actionName, actions[actionName]) |
||||
|
} |
||||
|
} else if (typeof actions == 'function') { |
||||
|
initReducer(reducers, ak, actions) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let history = createBrowserHistory(); |
||||
|
let store = configStore(reducers, history); |
||||
|
store.dispatch(initLayout(title, copyright, sections, actions)); |
||||
|
store.dispatch(actions.auth.initAuth()); |
||||
|
|
||||
|
const combineRoutes = flatRoutes(innerRoutes); |
||||
|
|
||||
|
setInnerRoutes(combineRoutes) |
||||
|
setHistory(history) |
||||
|
setStore(store) |
||||
|
setOuterRoutes(outerRoutes.map(route => ( |
||||
|
<Route |
||||
|
key={route.key} |
||||
|
exact |
||||
|
path={route.path} |
||||
|
component={route.component} |
||||
|
/> |
||||
|
))) |
||||
|
setCombineRoutes(combineRoutes.map(route => ( |
||||
|
<Route |
||||
|
key={route.key} |
||||
|
exact={Object.prototype.hasOwnProperty.call(route, 'exact') ? route.exact : true} |
||||
|
path={route.path} |
||||
|
component={route.component} |
||||
|
/> |
||||
|
))) |
||||
|
}, []) |
||||
|
|
||||
|
return ( |
||||
|
store ? |
||||
|
<ConfigProvider locale={zhCN}> |
||||
|
<Provider store={store}> |
||||
|
<ConnectedRouter history={history}> |
||||
|
<div> |
||||
|
<Switch> |
||||
|
{outerRoutes} |
||||
|
<Layout |
||||
|
history={history} |
||||
|
routes={innnerRoutes} |
||||
|
> |
||||
|
{combineRoutes} |
||||
|
</Layout> |
||||
|
<Route |
||||
|
path={'*'} |
||||
|
component={NoMatch} |
||||
|
/> |
||||
|
</Switch> |
||||
|
</div> |
||||
|
</ConnectedRouter> |
||||
|
</Provider> |
||||
|
</ConfigProvider> |
||||
|
: '' |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Root; |
@ -0,0 +1,28 @@ |
|||||
|
/** |
||||
|
* Created by liu.xinyi |
||||
|
* on 2016/4/1. |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
const initState = { |
||||
|
msg: null |
||||
|
}; |
||||
|
|
||||
|
import Immutable from 'immutable'; |
||||
|
|
||||
|
/** |
||||
|
* 全局ajax响应处理: |
||||
|
* 判断action中是否有done字段,如果有,则修改store中的msg.done |
||||
|
* 判断action中是否有error字段,如果有,则修改store中msg.error |
||||
|
* 在layout中根据msg的值,呈现提示信息。 |
||||
|
*/ |
||||
|
export default function ajaxResponse(state = initState, action) { |
||||
|
if (action.done) { |
||||
|
return Immutable.fromJS(state).set('msg', {done: action.done}).toJS(); |
||||
|
} |
||||
|
|
||||
|
if (action.error) { |
||||
|
return Immutable.fromJS(state).set('msg', {error: action.error}).toJS(); |
||||
|
} |
||||
|
|
||||
|
return {msg: null}; |
||||
|
}; |
@ -0,0 +1,36 @@ |
|||||
|
'use strict'; |
||||
|
import Immutable from 'immutable'; |
||||
|
import { INIT_LAYOUT, RESIZE } from '../actions/global'; |
||||
|
|
||||
|
function global (state = { |
||||
|
title: '', |
||||
|
copyright: '', |
||||
|
sections: [], |
||||
|
actions: {}, |
||||
|
plugins: {}, |
||||
|
clientHeight: 768, |
||||
|
clientWidth: 1024, |
||||
|
}, action) { |
||||
|
const payload = action.payload; |
||||
|
switch (action.type) { |
||||
|
case RESIZE: |
||||
|
return Immutable.fromJS(state).merge({ |
||||
|
clientHeight: payload.clientHeight, |
||||
|
clientWidth: payload.clientWidth |
||||
|
}).toJS(); |
||||
|
case INIT_LAYOUT: |
||||
|
return { |
||||
|
title: payload.title, |
||||
|
copyright: payload.copyright, |
||||
|
sections: payload.sections, |
||||
|
actions: payload.actions, |
||||
|
plugins: payload.plugins, |
||||
|
clientHeight: state.clientHeight, |
||||
|
clientWidth: state.clientWidth, |
||||
|
}; |
||||
|
default: |
||||
|
return state; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default global; |
@ -0,0 +1,15 @@ |
|||||
|
/** |
||||
|
* User: liuxinyi/liu.xinyi@free-sun.com.cn |
||||
|
* Date: 2016/1/13 |
||||
|
* Time: 17:52 |
||||
|
* |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
|
||||
|
import global from './global'; |
||||
|
import ajaxResponse from './ajaxResponse'; |
||||
|
|
||||
|
export default { |
||||
|
global, |
||||
|
ajaxResponse |
||||
|
}; |
@ -0,0 +1,16 @@ |
|||||
|
/** |
||||
|
* User: liuxinyi/liu.xinyi@free-sun.com.cn |
||||
|
* Date: 2016/1/13 |
||||
|
* Time: 17:51 |
||||
|
* |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
|
||||
|
let store = null; |
||||
|
if(process.env.NODE_ENV == 'production'){ |
||||
|
store = require('./store.prod').default; |
||||
|
}else { |
||||
|
store = require('./store.dev').default; |
||||
|
} |
||||
|
|
||||
|
export default store; |
@ -0,0 +1,30 @@ |
|||||
|
/** |
||||
|
* Created by liu.xinyi |
||||
|
* on 2016/4/8. |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; |
||||
|
import reduxThunk from 'redux-thunk'; |
||||
|
import { connectRouter, routerMiddleware } from 'connected-react-router'; |
||||
|
import innerReducers from '../reducers'; |
||||
|
|
||||
|
function configStore(reducers, history) { |
||||
|
const reducer = Object.assign({}, innerReducers, reducers, { |
||||
|
router: connectRouter(history) |
||||
|
}); |
||||
|
|
||||
|
const composeEnhancers = |
||||
|
typeof window === 'object' && |
||||
|
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? |
||||
|
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ |
||||
|
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
|
||||
|
}) : compose; |
||||
|
|
||||
|
const enhancers = composeEnhancers( |
||||
|
applyMiddleware(routerMiddleware(history), reduxThunk) |
||||
|
); |
||||
|
|
||||
|
return createStore(combineReducers(reducer), {}, enhancers); |
||||
|
} |
||||
|
|
||||
|
export default configStore; |
@ -0,0 +1,20 @@ |
|||||
|
/** |
||||
|
* Created by liu.xinyi |
||||
|
* on 2016/4/8. |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
|
||||
|
import { createStore, combineReducers, applyMiddleware } from 'redux'; |
||||
|
import reduxThunk from 'redux-thunk'; |
||||
|
import { connectRouter, routerMiddleware } from 'connected-react-router'; |
||||
|
import innerReducers from '../reducers'; |
||||
|
|
||||
|
function configStore(reducers, history){ |
||||
|
const reducer = Object.assign({}, innerReducers, reducers, { |
||||
|
router: connectRouter(history) |
||||
|
}); |
||||
|
|
||||
|
return createStore(combineReducers(reducer), {}, applyMiddleware(routerMiddleware(history), reduxThunk)); |
||||
|
} |
||||
|
|
||||
|
export default configStore; |
@ -0,0 +1,62 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import { ApiTable } from '$utils' |
||||
|
import { Request } from '@peace/utils' |
||||
|
|
||||
|
export const INIT_AUTH = 'INIT_AUTH'; |
||||
|
export function initAuth() { |
||||
|
const user = JSON.parse(sessionStorage.getItem('user')) || {}; |
||||
|
return { |
||||
|
type: INIT_AUTH, |
||||
|
payload: { |
||||
|
user: user |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export const REQUEST_LOGIN = 'REQUEST_LOGIN'; |
||||
|
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; |
||||
|
export const LOGIN_ERROR = 'LOGIN_ERROR'; |
||||
|
export function login({ username, password, phone, code }) { |
||||
|
return dispatch => { |
||||
|
dispatch({ type: REQUEST_LOGIN }); |
||||
|
|
||||
|
return Request.post(ApiTable.login, { username, password, phone, code }) |
||||
|
.then(user => { |
||||
|
sessionStorage.setItem('user', JSON.stringify(user)); |
||||
|
dispatch({ |
||||
|
type: LOGIN_SUCCESS, |
||||
|
payload: { user: user }, |
||||
|
}); |
||||
|
}, error => { |
||||
|
let { body } = error.response; |
||||
|
dispatch({ |
||||
|
type: LOGIN_ERROR, |
||||
|
payload: { |
||||
|
error: body && body.message ? body.message : '登录失败' |
||||
|
} |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const LOGOUT = 'LOGOUT'; |
||||
|
export function logout(user) { |
||||
|
const token = user.token; |
||||
|
const url = ApiTable.logout; |
||||
|
sessionStorage.removeItem('user'); |
||||
|
localStorage.removeItem('governmentDataResourceCenter_selected_sider') |
||||
|
localStorage.removeItem('governmentDataResourceCenter_open_sider') |
||||
|
Request.put(url, { |
||||
|
token: token |
||||
|
}); |
||||
|
return { |
||||
|
type: LOGOUT |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default { |
||||
|
initAuth, |
||||
|
login, |
||||
|
logout |
||||
|
} |
@ -0,0 +1,10 @@ |
|||||
|
/** |
||||
|
* Created by liu.xinyi |
||||
|
* on 2016/4/1. |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
import auth from './auth'; |
||||
|
|
||||
|
export default { |
||||
|
...auth |
||||
|
}; |
@ -0,0 +1,4 @@ |
|||||
|
'use strict'; |
||||
|
import Login from './login'; |
||||
|
|
||||
|
export { Login }; |
@ -0,0 +1,158 @@ |
|||||
|
'use strict'; |
||||
|
import React, { useState, useEffect, useRef } from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { push } from 'react-router-redux'; |
||||
|
import SHA1 from 'crypto-js/sha1'; |
||||
|
import Hex from 'crypto-js/enc-hex'; |
||||
|
import { ApiTable } from '$utils' |
||||
|
import { Request } from '@peace/utils' |
||||
|
import { Button, Input, Form, Row, Col, message, Tabs } from 'antd'; |
||||
|
import { login, LOGIN_ERROR } from '../actions/auth'; |
||||
|
import { ExclamationCircleOutlined } from '@ant-design/icons'; |
||||
|
import { Uploads } from '$components' |
||||
|
import '../style.less'; |
||||
|
|
||||
|
const FormItem = Form.Item; |
||||
|
|
||||
|
let codCountDownInterval = null |
||||
|
const Login = props => { |
||||
|
const { dispatch, user, error, isRequesting } = props |
||||
|
const [username, setUserName] = useState('') |
||||
|
const [password, setPassword] = useState('') |
||||
|
const [phone, setPhone] = useState('') |
||||
|
const [code, setCode] = useState('') |
||||
|
const [inputChanged, setInputChanged] = useState(false) |
||||
|
const [curTabKey, setCurTabKey] = useState(1) |
||||
|
const [codSending, setCodSending] = useState(false) |
||||
|
const [codCountDown, setCodeCountDown] = useState(60) |
||||
|
const codCountDownRef = useRef(0) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
sessionStorage.removeItem('user'); |
||||
|
localStorage.removeItem('governmentDataResourceCenter_selected_sider') |
||||
|
localStorage.removeItem('governmentDataResourceCenter_open_sider') |
||||
|
}, []) |
||||
|
|
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (user && user.authorized) { |
||||
|
user?.role == '数据消费者' ? dispatch(push('/metadataManagement/latestMetadata')) : dispatch(push('/homePage')); |
||||
|
} |
||||
|
}, [user]) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (codSending) { |
||||
|
setCodeCountDown(59) |
||||
|
codCountDownRef.current = 59 |
||||
|
codCountDownInterval = setInterval(() => { |
||||
|
codCountDownRef.current -= 1 |
||||
|
if (codCountDownRef.current == 0) { |
||||
|
setCodSending(false) |
||||
|
setCodeCountDown(60) |
||||
|
clearInterval(codCountDownInterval) |
||||
|
codCountDownInterval = null |
||||
|
} else { |
||||
|
setCodeCountDown(codCountDownRef.current) |
||||
|
} |
||||
|
}, 1000); |
||||
|
} else { |
||||
|
if (codCountDownInterval) { |
||||
|
clearInterval(codCountDownInterval) |
||||
|
codCountDownInterval = null |
||||
|
setCodeCountDown(60) |
||||
|
} |
||||
|
} |
||||
|
}, [codSending]) |
||||
|
|
||||
|
const doLogin = () => { |
||||
|
if (curTabKey == 1) { |
||||
|
if (!username || !password) |
||||
|
dispatch({ |
||||
|
type: LOGIN_ERROR, |
||||
|
payload: { error: '请输入账号名和密码' } |
||||
|
}); |
||||
|
setInputChanged(false) |
||||
|
dispatch(login({ username, password })); |
||||
|
} else { |
||||
|
if (!phone || !code) |
||||
|
dispatch({ |
||||
|
type: LOGIN_ERROR, |
||||
|
payload: { error: '请输入手机号和验证码' } |
||||
|
}); |
||||
|
dispatch(login({ phone, code })); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const enterHandler = e => { |
||||
|
if (e.key === 'Enter') { |
||||
|
doLogin() |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<div className='login'> |
||||
|
<div className='left'></div> |
||||
|
<div className='right'> |
||||
|
<div className='loginBox'> |
||||
|
<div className='_title'>欢迎登录系统</div> |
||||
|
<div className='_divider'></div> |
||||
|
<Form onKeyDown={enterHandler}> |
||||
|
|
||||
|
<FormItem> |
||||
|
<Input |
||||
|
style={{ marginTop: 30 }} |
||||
|
placeholder='请输入账号' |
||||
|
className='loginInp' |
||||
|
type="text" |
||||
|
value={username} |
||||
|
// maxlength={11}
|
||||
|
onChange={e => { |
||||
|
setUserName(e.target.value) |
||||
|
setInputChanged(true) |
||||
|
}} |
||||
|
/> |
||||
|
</FormItem> |
||||
|
<FormItem> |
||||
|
<Input |
||||
|
style={{ marginTop: 30 }} |
||||
|
placeholder='请输入密码' |
||||
|
className='loginInp' |
||||
|
type="password" |
||||
|
value={password} |
||||
|
onChange={e => { |
||||
|
setPassword(e.target.value) |
||||
|
setInputChanged(true) |
||||
|
}} |
||||
|
/> |
||||
|
</FormItem> |
||||
|
</Form> |
||||
|
<Row style={{ |
||||
|
paddingLeft: '10%' |
||||
|
}}> |
||||
|
{ |
||||
|
inputChanged || !error ? |
||||
|
<span style={{ |
||||
|
visibility: 'hidden' |
||||
|
}}>-</span> : |
||||
|
<span> |
||||
|
<ExclamationCircleOutlined style={{ color: 'red' }} />{error} |
||||
|
</span> |
||||
|
} |
||||
|
</Row> |
||||
|
<Button style={{ borderRadius: 28 }} type="primary" className='loginBtn' loading={isRequesting} onClick={doLogin}>登录</Button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps(state) { |
||||
|
const { auth } = state; |
||||
|
return { |
||||
|
user: auth.user, |
||||
|
error: auth.error, |
||||
|
isRequesting: auth.isRequesting |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(Login); |
@ -0,0 +1,12 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import routes from './routes'; |
||||
|
import reducers from './reducers'; |
||||
|
import actions from './actions'; |
||||
|
|
||||
|
export default { |
||||
|
key: 'auth', |
||||
|
reducers: reducers, |
||||
|
routes: routes, |
||||
|
actions: actions |
||||
|
}; |
@ -0,0 +1,40 @@ |
|||||
|
'use strict'; |
||||
|
import * as actionTypes from '../actions/auth'; |
||||
|
import Immutable from 'immutable'; |
||||
|
|
||||
|
const initState = { |
||||
|
user: {}, |
||||
|
isRequesting: false, |
||||
|
error: null |
||||
|
}; |
||||
|
|
||||
|
function auth(state = initState, action) { |
||||
|
const payload = action.payload; |
||||
|
switch (action.type){ |
||||
|
case actionTypes.INIT_AUTH: |
||||
|
return Immutable.fromJS(state).set('user', payload.user).toJS(); |
||||
|
case actionTypes.REQUEST_LOGIN: |
||||
|
return Immutable.fromJS(state).merge({ |
||||
|
isRequesting: true, |
||||
|
error: null |
||||
|
}).toJS(); |
||||
|
case actionTypes.LOGIN_SUCCESS: |
||||
|
return Immutable.fromJS(state).merge({ |
||||
|
isRequesting: false, |
||||
|
user: payload.user |
||||
|
}).toJS(); |
||||
|
case actionTypes.LOGIN_ERROR: |
||||
|
return Immutable.fromJS(state).merge({ |
||||
|
isRequesting: false, |
||||
|
error: payload.error |
||||
|
}).toJS(); |
||||
|
case actionTypes.LOGOUT: |
||||
|
return Immutable.fromJS(state).merge({ |
||||
|
user: null |
||||
|
}).toJS(); |
||||
|
default: |
||||
|
return state; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default auth; |
@ -0,0 +1,6 @@ |
|||||
|
'use strict'; |
||||
|
import auth from './auth' |
||||
|
|
||||
|
export default { |
||||
|
auth |
||||
|
}; |
@ -0,0 +1,12 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import { Login } from './containers'; |
||||
|
|
||||
|
export default [{ |
||||
|
type: 'outer', |
||||
|
route: { |
||||
|
key:'signin', |
||||
|
path: "/signin", |
||||
|
component: Login |
||||
|
} |
||||
|
}]; |
@ -0,0 +1,112 @@ |
|||||
|
.login { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
background-image: url('/assets/images/login_bg.png'); |
||||
|
background-size: 100% 100%; |
||||
|
|
||||
|
.left { |
||||
|
|
||||
|
background-size: 100% 100%; |
||||
|
background-repeat: no-repeat; |
||||
|
width: 55%; |
||||
|
height: 100%; |
||||
|
float: left; |
||||
|
top: 0px; |
||||
|
left: 0px; |
||||
|
} |
||||
|
|
||||
|
.right { |
||||
|
width: 45%; |
||||
|
height: 100%; |
||||
|
// background-color: #000066; |
||||
|
float: left; |
||||
|
right: 0px; |
||||
|
bottom: 0px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media screen and (max-height:1440px) { |
||||
|
.loginBox { |
||||
|
top: 25%; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media screen and (max-height: 768px) { |
||||
|
.loginBox { |
||||
|
top: 20%; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media screen and (max-height: 630px) { |
||||
|
.loginBox { |
||||
|
top: 10%; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.loginBox { |
||||
|
height: 50%; |
||||
|
width: 25%; |
||||
|
position: absolute; |
||||
|
right: 16.5%; |
||||
|
z-index: 20; |
||||
|
background: #fff; |
||||
|
text-align: center; |
||||
|
padding-top: 30px; |
||||
|
|
||||
|
@media screen and (min-height:1080px) { |
||||
|
height: 40%; |
||||
|
} |
||||
|
|
||||
|
._title { |
||||
|
font-family: SourceHanSansCN-Medium; |
||||
|
font-weight: 500; |
||||
|
font-size: 24px; |
||||
|
color: rgba(0, 0, 0, 0.75); |
||||
|
letter-spacing: 0; |
||||
|
} |
||||
|
|
||||
|
._divider { |
||||
|
width: 58px; |
||||
|
height: 4px; |
||||
|
background: #4263F7; |
||||
|
display: inline-block; |
||||
|
} |
||||
|
|
||||
|
h1 { |
||||
|
color: #fff; |
||||
|
font-size: 58px; |
||||
|
} |
||||
|
|
||||
|
.loginFormTit { |
||||
|
width: 20%; |
||||
|
font-size: 18px; |
||||
|
color: rgb(255, 255, 255); |
||||
|
// margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.loginInp { |
||||
|
width: 80%; |
||||
|
height: 50px; |
||||
|
background: #ffffff; |
||||
|
border: 1px solid #C2C2C2; |
||||
|
border-radius: 5px; |
||||
|
} |
||||
|
|
||||
|
.loginBtn { |
||||
|
width: 80%; |
||||
|
height: 50px; |
||||
|
margin-top: 40px; |
||||
|
border-radius: 5px; |
||||
|
font-size: 16px; |
||||
|
background: #4263F7; |
||||
|
border-color: #4263F7; |
||||
|
} |
||||
|
|
||||
|
.loginBtn:hover { |
||||
|
background: #4263F7; |
||||
|
border-color: #4263F7; |
||||
|
} |
||||
|
} |
@ -0,0 +1,15 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import { basicAction } from '@peace/utils' |
||||
|
import { ApiTable } from '$utils' |
||||
|
|
||||
|
// export function getMembers(orgId) {
|
||||
|
// return dispatch => basicAction({
|
||||
|
// type: 'get',
|
||||
|
// dispatch: dispatch,
|
||||
|
// actionType: 'GET_MEMBERS',
|
||||
|
// url: `${ApiTable.getEnterprisesMembers.replace('{enterpriseId}', orgId)}`,
|
||||
|
// msg: { error: '获取用户列表失败' },
|
||||
|
// reducer: { name: 'members' }
|
||||
|
// });
|
||||
|
// }
|
@ -0,0 +1,7 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import * as example from './example' |
||||
|
|
||||
|
export default { |
||||
|
...example, |
||||
|
} |
@ -0,0 +1,49 @@ |
|||||
|
import React, { useEffect, useState } from 'react' |
||||
|
import CarouselList from './public/carousel-list'; |
||||
|
import { Tooltip } from 'antd'; |
||||
|
import { ApiTable, useFsRequest } from '$utils'; |
||||
|
import moment from 'moment'; |
||||
|
function AbnormalMonitoring(props) { |
||||
|
|
||||
|
const { data: logs = {} } = useFsRequest({ |
||||
|
url: ApiTable.getLogs, |
||||
|
query: { |
||||
|
logState: false, |
||||
|
startTime: moment().subtract(7, 'days').format('YYYY-MM-DD HH:mm:ss'), |
||||
|
endTime: moment().format('YYYY-MM-DD HH:mm:ss') |
||||
|
}, |
||||
|
pollingInterval: 1000 * 60 |
||||
|
}); |
||||
|
|
||||
|
const dataSource = logs?.rows ? logs?.rows?.map(s => { |
||||
|
return [ |
||||
|
<div style={{ color: '#fff' }}> |
||||
|
<Tooltip placement="top" title={s?.acquisitionTask?.taskName}> |
||||
|
{s?.acquisitionTask?.taskName?.length > 20 ? s?.acquisitionTask?.taskNamesubstring(0, 20) + '...' : s?.acquisitionTask?.taskName} |
||||
|
</Tooltip> |
||||
|
</div>, |
||||
|
moment(s?.startTime).format('YYYY-MM-DD HH:mm:ss'), |
||||
|
moment(s?.endTime).valueOf() - moment(s?.startTime).valueOf() + '毫秒', |
||||
|
<div style={{ color: 'rgba(245, 27, 27, 1)' }}> |
||||
|
<Tooltip placement="top" title={s?.details}> |
||||
|
{s?.details?.length > 20 ? s?.details.substring(0, 20) + '...' : s?.details} |
||||
|
</Tooltip> |
||||
|
</div> |
||||
|
] |
||||
|
}) : [] |
||||
|
return <div style={{ height: 149, border: '1px solid #50c9d74d', backgroundImage: 'linear-gradient(180deg, rgba(0, 32, 74, 0) 3%, rgba(80, 201, 247, 0.1) 100%)' }}> |
||||
|
<div className='center-card-title' style={{ marginBottom: 6 }}><div className='_icon_left' />异常监控<div className='_icon_right' /></div> |
||||
|
<CarouselList |
||||
|
header={['任务名称', '采集时间', '耗时', '异常日志']} |
||||
|
data={dataSource} |
||||
|
rowNum={2} |
||||
|
height={100} |
||||
|
multiellipsis |
||||
|
marginTop={-50} |
||||
|
/> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
export default AbnormalMonitoring; |
||||
|
|
||||
|
|
@ -0,0 +1,36 @@ |
|||||
|
import React from 'react' |
||||
|
import Box from './public/table-card'; |
||||
|
import { useFsRequest } from '$utils'; |
||||
|
import { mathRound } from './util' |
||||
|
function AccessData() { |
||||
|
|
||||
|
const { data: accessdata = [] } = useFsRequest({ |
||||
|
url: 'homepage/accessdata', |
||||
|
pollingInterval: 1000 * 60, |
||||
|
cacheKey: 'accessdata', |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
const renderBody = () => { |
||||
|
return <div className='access_data'> |
||||
|
<div className='_img'></div> |
||||
|
<div className='data_unit'> |
||||
|
数据单位<div className='data_number'>{!accessdata?.projects ? '-' : accessdata?.projects?.split(',')?.length}</div>个 |
||||
|
</div> |
||||
|
<div className='data_today'> |
||||
|
今日数据<div className='data_number'>{!accessdata?.res?.stat?.today ? '-' : accessdata?.res?.stat?.today > 1000 ? mathRound(accessdata?.res?.stat?.today) : accessdata?.res?.stat?.today}</div>{accessdata?.res?.stat?.today > 1000 ? '万条' : '条'} |
||||
|
</div> |
||||
|
<div className='data_total'> |
||||
|
数据总量<div className='data_number'>{accessdata?.res?.stat?.datas ? Math.round(accessdata?.res?.stat?.datas / 10000) : '-'}</div>万条 |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
return <Box title={"接入数据统计"} > |
||||
|
{renderBody()} |
||||
|
</Box> |
||||
|
} |
||||
|
|
||||
|
export default AccessData; |
||||
|
|
||||
|
|
@ -0,0 +1,52 @@ |
|||||
|
import React from 'react' |
||||
|
import Box from './public/table-card'; |
||||
|
import CarouselList from './public/carousel-list'; |
||||
|
import { Tooltip } from 'antd'; |
||||
|
import moment from 'moment'; |
||||
|
import NoData from './public/noData'; |
||||
|
import { useFsRequest } from '$utils'; |
||||
|
|
||||
|
function AlarmList(props) { |
||||
|
const { cardContentHeight } = props; |
||||
|
const { data: alarms = [] } = useFsRequest({ |
||||
|
url: 'homepage/alarms', |
||||
|
pollingInterval: 1000 * 60, |
||||
|
cacheKey: 'alarms', |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
const data = alarms.map(s => { |
||||
|
return [ |
||||
|
s.content, |
||||
|
s.level == 1 ? '一级' : s.level == 2 ? '二级' : s.level == 3 ? '三级' : '四级', |
||||
|
moment(s.time).format('YYYY-MM-DD HH:mm:ss') |
||||
|
] |
||||
|
}) |
||||
|
|
||||
|
const renderBody = () => { |
||||
|
return <CarouselList |
||||
|
header={['预警内容', '预警等级', '预警时间']} |
||||
|
data={data?.map(s => { |
||||
|
return [ |
||||
|
<Tooltip placement="top" title={s[0]}> |
||||
|
{s[0].length > 20 ? s[0]?.substring(0, 20) + '...' : s[0]} |
||||
|
</Tooltip>, |
||||
|
<div style={{ color: s[1] == '一级' ? 'rgba(245, 27, 27, 1)' : s[1] == '二级' ? '#FF7900' : s[1] == '三级' ? '#FFCD00' : '#00DA9F' }}>{s[1]}</div>, |
||||
|
s[2] |
||||
|
] |
||||
|
})} |
||||
|
rowNum={6} |
||||
|
height={cardContentHeight} |
||||
|
multiellipsis |
||||
|
columnWidth={[180, 80, 150]} |
||||
|
/> |
||||
|
} |
||||
|
|
||||
|
return <Box title={"预警列表"}> |
||||
|
{alarms?.length > 0 ? renderBody() : <NoData />} |
||||
|
</Box> |
||||
|
} |
||||
|
|
||||
|
export default AlarmList; |
||||
|
|
||||
|
|
@ -0,0 +1,19 @@ |
|||||
|
import React from 'react' |
||||
|
import './style.less' |
||||
|
|
||||
|
function CenterTop(props) { |
||||
|
|
||||
|
|
||||
|
return <div className='_top'> |
||||
|
<div className='center_top_data'> |
||||
|
<div className='_center_card1'>共享交换</div> |
||||
|
<div className='_center_card2'>数据监控</div> |
||||
|
<div className='_center_card3'>数据治理</div> |
||||
|
<div className='_center_card4'>数据采集</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
export default CenterTop; |
||||
|
|
||||
|
|
@ -0,0 +1,54 @@ |
|||||
|
import React from 'react' |
||||
|
import Box from './public/table-card'; |
||||
|
import { useFsRequest } from '$utils'; |
||||
|
import { mathRound } from './util'; |
||||
|
function DataShare(props) { |
||||
|
|
||||
|
const { data: dataTotal = {} } = useFsRequest({ |
||||
|
url: 'homepage/datatotal/top5', |
||||
|
pollingInterval: 1000 * 60, |
||||
|
cacheKey: 'datatotal', |
||||
|
}); |
||||
|
|
||||
|
const { data: restfulInfo = {} } = useFsRequest({ |
||||
|
url: 'homepage/restful/info', |
||||
|
pollingInterval: 1000 * 60, |
||||
|
cacheKey: 'restfulInfo', |
||||
|
}); |
||||
|
|
||||
|
const renderItem = (s) => { |
||||
|
return <div className='_item_content'> |
||||
|
<div className={'_item_icon' + s.key} /> |
||||
|
<div className='_item_text'> |
||||
|
{s.title} |
||||
|
<div className='number_container'> |
||||
|
<span className='_number'>{s.data}</span>{s.unit} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
const leftData = [ |
||||
|
{ key: '1', data: mathRound(dataTotal?.total), unit: '万条', title: '共享库数据总量' }, |
||||
|
{ key: '2', data: restfulInfo?.total, unit: '次', title: '访问接口总次数' }, |
||||
|
{ key: '3', data: restfulInfo?.totalUser, unit: '个', title: '访问接口用户总数' }] |
||||
|
const rightData = [ |
||||
|
{ key: '2', data: restfulInfo?.todayTotal, unit: '次', title: '接口访问次数' }, |
||||
|
{ key: '3', data: restfulInfo?.todayUser, unit: '个', title: '访问接口用户总数' }] |
||||
|
|
||||
|
return <Box title={"数据共享"} > |
||||
|
<div className='data_share'> |
||||
|
<div className='_left_content'> |
||||
|
{leftData.map(s => renderItem(s))} |
||||
|
</div> |
||||
|
<div className='_right_content'> |
||||
|
<div className='_today_text'>今日</div> |
||||
|
{rightData.map(s => renderItem(s))} |
||||
|
</div> |
||||
|
</div> |
||||
|
</Box> |
||||
|
} |
||||
|
|
||||
|
export default DataShare; |
||||
|
|
||||
|
|
@ -0,0 +1,262 @@ |
|||||
|
import React, { useEffect, useState } from 'react' |
||||
|
import Box from './public/table-card'; |
||||
|
import ReactEcharts from 'echarts-for-react'; |
||||
|
import './style.less'; |
||||
|
import { useFsRequest } from '$utils'; |
||||
|
import { mathRound } from './util'; |
||||
|
import NoData from './public/noData'; |
||||
|
function DataTop5(props) { |
||||
|
const { cardContentHeight } = props; |
||||
|
const { data: dataTotal = {} } = useFsRequest({ |
||||
|
url: 'homepage/datatotal/top5', |
||||
|
pollingInterval: 1000 * 60, |
||||
|
cacheKey: 'datatotal', |
||||
|
}); |
||||
|
|
||||
|
const renderBody = () => { |
||||
|
let chartData = dataTotal?.top5?.map(x => { |
||||
|
return { |
||||
|
name: x?.dataSource?.resourceCatalog?.name, |
||||
|
value: mathRound(x.dbRecordCount), |
||||
|
} |
||||
|
}) || [] |
||||
|
|
||||
|
let options = { |
||||
|
xAxis: { |
||||
|
splitLine: { |
||||
|
show: false, |
||||
|
}, |
||||
|
axisLabel: { |
||||
|
show: false, |
||||
|
}, |
||||
|
axisTick: { |
||||
|
show: false, |
||||
|
}, |
||||
|
splitArea: { show: false }, |
||||
|
axisLine: { |
||||
|
show: false, |
||||
|
}, |
||||
|
}, |
||||
|
tooltip: { |
||||
|
confine: true, |
||||
|
trigger: 'axis', |
||||
|
axisPointer: { |
||||
|
type: 'shadow', |
||||
|
}, |
||||
|
backgroundColor: 'rgba(13,30,44, 0.7)', |
||||
|
borderColor: 'rgba(3, 65, 118, 0.8)', |
||||
|
textStyle: { |
||||
|
color: '#fff', |
||||
|
}, |
||||
|
formatter: function (params) { |
||||
|
var name = params[0].name |
||||
|
if (name.length > 20) { |
||||
|
name = name.replace(/(.{20})/g, '$1<br>') // 每 30 个字符添加一个换行符
|
||||
|
} |
||||
|
var content = name |
||||
|
|
||||
|
return content + ' : <b>' + params[0].value + '</b>万条' |
||||
|
} |
||||
|
}, |
||||
|
grid: { |
||||
|
top: 13, |
||||
|
bottom: -10, |
||||
|
left: '5%', |
||||
|
}, |
||||
|
yAxis: { |
||||
|
inverse: true, |
||||
|
axisLine: { |
||||
|
show: false, |
||||
|
}, |
||||
|
axisTick: { |
||||
|
show: false, |
||||
|
}, |
||||
|
axisLabel: { |
||||
|
textStyle: { |
||||
|
color: '#fff', |
||||
|
padding: [-5, 0, 35, 18], |
||||
|
}, |
||||
|
formatter(value, index) { |
||||
|
let str = '', num = 'TOP' + (index + 1) |
||||
|
let valueHandle = value.length > 10 ? value.substring(0, 10) + '...' : value |
||||
|
if (index === 0) { |
||||
|
str = '{a| ' + num + '}{title| ' + valueHandle + '}' |
||||
|
} else if (index === 1) { |
||||
|
str = '{b| ' + num + '}{title| ' + valueHandle + '}' |
||||
|
} else if (index === 2) { |
||||
|
str = '{c| ' + num + '}{title| ' + valueHandle + '}' |
||||
|
} else { |
||||
|
str = '{d| ' + num + '}{title| ' + valueHandle + '}' |
||||
|
} |
||||
|
return str |
||||
|
}, |
||||
|
rich: { |
||||
|
a: { |
||||
|
borderColor: '#EE6F7C', |
||||
|
borderWidth: 1, |
||||
|
borderRadius: [0, 10, 10, 0], |
||||
|
padding: [3.5, 10, 1, -13], |
||||
|
backgroundColor: 'rgba(238, 111, 124, 0.8)', |
||||
|
}, |
||||
|
b: { |
||||
|
borderColor: '#FFCF5F', |
||||
|
borderWidth: 1, |
||||
|
borderRadius: [0, 10, 10, 0], |
||||
|
padding: [3.5, 10, 1, -13], |
||||
|
backgroundColor: 'rgba(255, 207, 95, 0.7)', |
||||
|
}, |
||||
|
c: { |
||||
|
borderColor: '#00E8FF', |
||||
|
borderWidth: 1, |
||||
|
borderRadius: [0, 10, 10, 0], |
||||
|
padding: [3.5, 10, 1, -13], |
||||
|
backgroundColor: 'rgba(0, 232, 255, 0.7)', |
||||
|
}, |
||||
|
d: { |
||||
|
borderColor: '#1A90FF', |
||||
|
borderWidth: 1, |
||||
|
borderRadius: [0, 10, 10, 0], |
||||
|
padding: [3.5, 10, 1, -13], |
||||
|
backgroundColor: 'rgba(26, 144, 255, 0.7)', |
||||
|
}, |
||||
|
title: { |
||||
|
padding: [0, 0, 0, 3], |
||||
|
}, |
||||
|
}, |
||||
|
align: 'left', |
||||
|
}, |
||||
|
data: chartData.map((item) => item.name), |
||||
|
}, |
||||
|
series: [ |
||||
|
{ |
||||
|
type: 'pictorialBar', |
||||
|
symbol: 'rect', |
||||
|
symbolRotate: 30, |
||||
|
symbolRepeat: 'fixed', |
||||
|
symbolClip: true, |
||||
|
symbolOffset: [0, -1.5], |
||||
|
symbolSize: [2, 12], |
||||
|
symbolMargin: '3', |
||||
|
itemStyle: { |
||||
|
normal: { |
||||
|
color: '#000726', |
||||
|
}, |
||||
|
}, |
||||
|
label: { |
||||
|
show: true, |
||||
|
color: '#C8F0FF', |
||||
|
fontFamily: 'Bebas', |
||||
|
fontSize: 12, |
||||
|
offset: [-9, 1], |
||||
|
position: 'right', |
||||
|
formatter(params) { |
||||
|
let result = '' |
||||
|
switch (params.dataIndex) { |
||||
|
case 0: |
||||
|
result = '{img|}{index0|' + params.value + '}{unit|}' |
||||
|
break |
||||
|
case 1: |
||||
|
result = '{img|}{index1|' + params.value + '}{unit|}' |
||||
|
break |
||||
|
case 2: |
||||
|
result = '{img|}{index2|' + params.value + '}{unit|}' |
||||
|
break |
||||
|
default: |
||||
|
result = '{img|}{index3|' + params.value + '}{unit|}' |
||||
|
break |
||||
|
} |
||||
|
return result |
||||
|
}, |
||||
|
rich: { |
||||
|
img: { |
||||
|
height: 18, |
||||
|
width: 20, |
||||
|
// backgroundColor: { image: arrow },这个图片自己切,这里上传不了(加了一个尾巴的形状)
|
||||
|
}, |
||||
|
unit: { |
||||
|
color: '#C8F0FF', |
||||
|
fontSize: 11, |
||||
|
}, |
||||
|
index0: { |
||||
|
color: '#FFF', |
||||
|
fontFamily: 'Bebas', |
||||
|
padding: [-2, 2, 0, 0], |
||||
|
fontWeight: 'bold', |
||||
|
fontSize: 16, |
||||
|
}, |
||||
|
index1: { |
||||
|
color: '#FFF', |
||||
|
fontFamily: 'Bebas', |
||||
|
padding: [-2, 2, 0, 0], |
||||
|
fontWeight: 'bold', |
||||
|
fontSize: 16, |
||||
|
}, |
||||
|
index2: { |
||||
|
color: '#FFF', |
||||
|
fontFamily: 'Bebas', |
||||
|
padding: [-2, 2, 0, 0], |
||||
|
fontWeight: 'bold', |
||||
|
fontSize: 16, |
||||
|
}, |
||||
|
index3: { |
||||
|
color: '#FFF', |
||||
|
fontFamily: 'Bebas', |
||||
|
padding: [-2, 2, 0, 0], |
||||
|
fontWeight: 'bold', |
||||
|
fontSize: 16, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
symbolBoundingData: Math.max(...chartData.map((item) => item.value)) * 1.3, |
||||
|
data: chartData.map((item) => item.value), |
||||
|
z: 2, |
||||
|
}, |
||||
|
{ |
||||
|
type: 'bar', |
||||
|
barWidth: 10, |
||||
|
data: chartData.map((item) => item.value), |
||||
|
itemStyle: { |
||||
|
normal: { |
||||
|
color: '#54DEFA', |
||||
|
}, |
||||
|
}, |
||||
|
z: 1, |
||||
|
}, |
||||
|
{ |
||||
|
type: 'bar', |
||||
|
barGap: '-125%', // 设置外框粗细
|
||||
|
data: chartData.map((items) => Math.max(...chartData.map((item) => item.value)) * 1.3), |
||||
|
barWidth: 15, |
||||
|
itemStyle: { |
||||
|
color: 'none', |
||||
|
borderColor: '#979797', |
||||
|
}, |
||||
|
z: 0, |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
|
||||
|
return <ReactEcharts |
||||
|
option={options} |
||||
|
notMerge |
||||
|
lazyUpdate |
||||
|
style={{ height: cardContentHeight }} |
||||
|
/> |
||||
|
} |
||||
|
|
||||
|
return <Box title={"数据量TOP5单位"} bodyPaddingTop={1} > |
||||
|
{ |
||||
|
dataTotal?.top5?.length > 0 ? |
||||
|
<> |
||||
|
<div className='data_top5_unit'>数据量:万条</div> |
||||
|
{renderBody()} |
||||
|
</> |
||||
|
: <NoData /> |
||||
|
} |
||||
|
|
||||
|
</Box> |
||||
|
} |
||||
|
|
||||
|
export default DataTop5; |
||||
|
|
||||
|
|
@ -0,0 +1,42 @@ |
|||||
|
import React, { useEffect, useState } from 'react' |
||||
|
import Box from './public/table-card'; |
||||
|
import NoData from './public/noData'; |
||||
|
import './style.less'; |
||||
|
import { ApiTable, useFsRequest } from '$utils'; |
||||
|
function HotspotData(props) { |
||||
|
|
||||
|
const { data: restfulInfo = {} } = useFsRequest({ |
||||
|
url: 'homepage/restful/info', |
||||
|
pollingInterval: 1000 * 60, |
||||
|
cacheKey: 'restfulInfo', |
||||
|
}); |
||||
|
|
||||
|
const top3 = restfulInfo?.top3 |
||||
|
return <Box title={"热点数据"} bodyPaddingTop={25} > |
||||
|
{top3?.length > 0 ? |
||||
|
<div className='hotspot_data_container'> |
||||
|
<div className='_img'></div> |
||||
|
<div className='_top1'> |
||||
|
<span className='hotspot_title' title={top3[0].name}>{top3[0].name?.length > 8 ? top3[0].name.substring(0, 8) + '...' : top3[0].name}</span> |
||||
|
<div className='hotspot_data_number'>{top3[0].count}</div> |
||||
|
</div> |
||||
|
<div className='_top2'> |
||||
|
{top3?.length > 2 && <> |
||||
|
<span className='hotspot_title' title={top3[2].name}>{top3[2].name?.length > 8 ? top3[2].name.substring(0, 8) + '...' : top3[2].name}</span> |
||||
|
<div className='hotspot_data_number'>{top3[2].count}</div> |
||||
|
</>} |
||||
|
</div> |
||||
|
<div className='_top3'> |
||||
|
{top3?.length > 1 && <> |
||||
|
<span className='hotspot_title' title={top3[1].name}>{top3[1].name?.length > 8 ? top3[1].name.substring(0, 8) + '...' : top3[1].name}</span> |
||||
|
<div className='hotspot_data_number'>{top3[1].count}</div> |
||||
|
</>} |
||||
|
</div> |
||||
|
</div> : <NoData /> |
||||
|
} |
||||
|
</Box> |
||||
|
} |
||||
|
|
||||
|
export default HotspotData; |
||||
|
|
||||
|
|
@ -0,0 +1,39 @@ |
|||||
|
import React, { useEffect, useState } from 'react' |
||||
|
import Box from './public/table-card'; |
||||
|
import { ApiTable, useFsRequest } from '$utils'; |
||||
|
import './style.less'; |
||||
|
function NodeResource(props) { |
||||
|
const { data: cluters = {} } = useFsRequest({ |
||||
|
url: 'homepage/datatotal/cluters', |
||||
|
pollingInterval: 1000 * 20, |
||||
|
}); |
||||
|
|
||||
|
const renderBody = () => { |
||||
|
return <div className='node-resource-container'> |
||||
|
<div className='_item'> |
||||
|
<div className='_noderesource_data'>{cluters?.disk}<span className='_percent'>%</span></div> |
||||
|
<div className='_noderesource_title'>硬盘</div> |
||||
|
<div className='disk_icon' /> |
||||
|
</div> |
||||
|
<div className='_item'> |
||||
|
<div className='_noderesource_data'>{cluters?.memory}<span className='_percent'>%</span></div> |
||||
|
<div className='_noderesource_title'>内存</div> |
||||
|
<div className='memory_icon' /> |
||||
|
</div> |
||||
|
<div className='_item'> |
||||
|
<div className='_noderesource_data'>{cluters?.cpu}<span className='_percent'>%</span></div> |
||||
|
<div className='_noderesource_title'>CPU</div> |
||||
|
<div className='cpu_icon' /> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
} |
||||
|
|
||||
|
return <Box title={"节点资源"} > |
||||
|
{renderBody()} |
||||
|
</Box> |
||||
|
} |
||||
|
|
||||
|
export default NodeResource; |
||||
|
|
||||
|
|
@ -0,0 +1,33 @@ |
|||||
|
/* 轮播列表组件 */ |
||||
|
import React from 'react'; |
||||
|
import ScrollBoard from './scrollBoard'; |
||||
|
import NoData from './noData'; |
||||
|
import './index.less'; |
||||
|
function CarouselList(props) { |
||||
|
const { |
||||
|
header = [], data = [], rowNum = 4, height, columnWidth, multiellipsis, waitTime = 2000, marginTop, ...restProps |
||||
|
} = props; |
||||
|
|
||||
|
const config = { |
||||
|
header, |
||||
|
rowNum, |
||||
|
headerBGC: 'rgba(81, 200, 247, 0.2)', |
||||
|
oddRowBGC: 'transparent', |
||||
|
evenRowBGC: 'transparent', |
||||
|
headerHeight: 30, |
||||
|
data, |
||||
|
waitTime, |
||||
|
columnWidth: columnWidth || [], |
||||
|
}; |
||||
|
|
||||
|
return data.length > 0 ? ( |
||||
|
<ScrollBoard |
||||
|
config={config} |
||||
|
style={{ height }} |
||||
|
className={multiellipsis ? 'scroll-board-multi' : 'scroll-board'} |
||||
|
{...restProps} |
||||
|
/> |
||||
|
) : <NoData marginTop={marginTop || 0} />; |
||||
|
} |
||||
|
|
||||
|
export default CarouselList; |
@ -0,0 +1,80 @@ |
|||||
|
.opcityBackground { |
||||
|
background-color: rgba(8, 27, 55, 0.6); |
||||
|
} |
||||
|
|
||||
|
.card-title { |
||||
|
// background: linear-gradient(to bottom, #fafafb, #92cbff); |
||||
|
// background-clip: border-box; |
||||
|
// -webkit-background-clip: text; |
||||
|
color: #fff; |
||||
|
font-size: 22px; |
||||
|
font-family: YouSheBiaoTiHei; |
||||
|
padding-left: 20px; |
||||
|
// font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
/* 滚动列表 */ |
||||
|
.scroll-board { |
||||
|
width: 533px; |
||||
|
height: 220px; |
||||
|
margin-top: 10px; |
||||
|
margin-left: 6px; |
||||
|
|
||||
|
.header { |
||||
|
height: 30px; |
||||
|
border-top: 1px solid #0047ba; |
||||
|
border-bottom: 1px solid #0047ba; |
||||
|
|
||||
|
.header-item { |
||||
|
// background: rgba(12, 49, 110, 0.3); |
||||
|
margin-right: 10px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.rows { |
||||
|
.row-item { |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
.row-item:hover { |
||||
|
background: linear-gradient(270deg, rgba(17, 183, 247, 0) 0%, rgba(17, 183, 247, 0.85) 100%); |
||||
|
color: #9ac8fc; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.scroll-board-multi { |
||||
|
padding: 5px 0px 5px; |
||||
|
color: rgba(204, 228, 255, 1) !important; |
||||
|
|
||||
|
.header { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
font-size: 12px !important; |
||||
|
color: rgba(204, 228, 255, 1) !important; |
||||
|
// border-bottom: 1px solid #124C79 !important; |
||||
|
} |
||||
|
|
||||
|
.rows { |
||||
|
color: rgba(204, 228, 255, 1) !important; |
||||
|
|
||||
|
.row-item { |
||||
|
border-bottom: 1px solid #124C79 !important; |
||||
|
} |
||||
|
|
||||
|
.row-item:hover { |
||||
|
background: linear-gradient(270deg, rgba(17, 183, 247, 0) 0%, rgba(17, 183, 247, 0.85) 100%); |
||||
|
color: #9ac8fc; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
._sorrow { |
||||
|
display: inline-block; |
||||
|
width: 15px; |
||||
|
height: 15px; |
||||
|
background: url('/assets/images/homePage/bigscreen/sorrow.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
margin-left: 13px; |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
/* 公共模块暂无数据组件 */ |
||||
|
import React from 'react'; |
||||
|
import { Empty } from 'antd'; |
||||
|
|
||||
|
function NoData({ height = 180, marginTop = 0 }) { |
||||
|
return ( |
||||
|
<Empty |
||||
|
image="/assets/images/homePage/bigscreen/empty.png" |
||||
|
imageStyle={{ |
||||
|
height, |
||||
|
marginTop |
||||
|
}} |
||||
|
description={false} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default NoData; |
@ -0,0 +1,469 @@ |
|||||
|
import React, { |
||||
|
useEffect, useState, useRef, useMemo, forwardRef, |
||||
|
} from 'react'; |
||||
|
|
||||
|
import PropTypes from 'prop-types'; |
||||
|
|
||||
|
import classnames from 'classnames'; |
||||
|
|
||||
|
import { deepMerge } from '@jiaminghi/charts/lib/util/index'; |
||||
|
|
||||
|
import { deepClone } from '@jiaminghi/c-render/lib/plugin/util'; |
||||
|
|
||||
|
import { useAutoResize, co } from '@jiaminghi/data-view-react'; |
||||
|
|
||||
|
import './style.less'; |
||||
|
|
||||
|
const defaultConfig = { |
||||
|
/** |
||||
|
* @description Board header |
||||
|
* @type {Array<String>} |
||||
|
* @default header = [] |
||||
|
* @example header = ['column1', 'column2', 'column3'] |
||||
|
*/ |
||||
|
header: [], |
||||
|
/** |
||||
|
* @description Board data |
||||
|
* @type {Array<Array>} |
||||
|
* @default data = [] |
||||
|
*/ |
||||
|
data: [], |
||||
|
/** |
||||
|
* @description Row num |
||||
|
* @type {Number} |
||||
|
* @default rowNum = 5 |
||||
|
*/ |
||||
|
rowNum: 5, |
||||
|
/** |
||||
|
* @description Header background color |
||||
|
* @type {String} |
||||
|
* @default headerBGC = '#00BAFF' |
||||
|
*/ |
||||
|
headerBGC: '#00BAFF', |
||||
|
/** |
||||
|
* @description Odd row background color |
||||
|
* @type {String} |
||||
|
* @default oddRowBGC = '#003B51' |
||||
|
*/ |
||||
|
oddRowBGC: '#003B51', |
||||
|
/** |
||||
|
* @description Even row background color |
||||
|
* @type {String} |
||||
|
* @default evenRowBGC = '#003B51' |
||||
|
*/ |
||||
|
evenRowBGC: '#0A2732', |
||||
|
/** |
||||
|
* @description Scroll wait time |
||||
|
* @type {Number} |
||||
|
* @default waitTime = 2000 |
||||
|
*/ |
||||
|
waitTime: 2000, |
||||
|
/** |
||||
|
* @description Header height |
||||
|
* @type {Number} |
||||
|
* @default headerHeight = 35 |
||||
|
*/ |
||||
|
headerHeight: 35, |
||||
|
/** |
||||
|
* @description Column width |
||||
|
* @type {Array<Number>} |
||||
|
* @default columnWidth = [] |
||||
|
*/ |
||||
|
columnWidth: [], |
||||
|
/** |
||||
|
* @description Column align |
||||
|
* @type {Array<String>} |
||||
|
* @default align = [] |
||||
|
* @example align = ['left', 'center', 'right'] |
||||
|
*/ |
||||
|
align: [], |
||||
|
/** |
||||
|
* @description Show index |
||||
|
* @type {Boolean} |
||||
|
* @default index = false |
||||
|
*/ |
||||
|
index: false, |
||||
|
/** |
||||
|
* @description index Header |
||||
|
* @type {String} |
||||
|
* @default indexHeader = '#' |
||||
|
*/ |
||||
|
indexHeader: '#', |
||||
|
/** |
||||
|
* @description Carousel type |
||||
|
* @type {String} |
||||
|
* @default carousel = 'single' |
||||
|
* @example carousel = 'single' | 'page' |
||||
|
*/ |
||||
|
carousel: 'single', |
||||
|
/** |
||||
|
* @description Pause scroll when mouse hovered |
||||
|
* @type {Boolean} |
||||
|
* @default hoverPause = true |
||||
|
* @example hoverPause = true | false |
||||
|
*/ |
||||
|
hoverPause: true, |
||||
|
}; |
||||
|
|
||||
|
function calcHeaderData({ header, index, indexHeader }) { |
||||
|
if (!header.length) { |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
header = [...header]; |
||||
|
|
||||
|
if (index) header.unshift(indexHeader); |
||||
|
|
||||
|
return header; |
||||
|
} |
||||
|
|
||||
|
function calcRows({ |
||||
|
data, index, headerBGC, rowNum, |
||||
|
}) { |
||||
|
if (index) { |
||||
|
data = data.map((row, i) => { |
||||
|
row = [...row]; |
||||
|
|
||||
|
const indexTag = `<span class="index" style="background-color: ${headerBGC};">${i |
||||
|
+ 1}</span>`; |
||||
|
|
||||
|
row.unshift(indexTag); |
||||
|
|
||||
|
return row; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
data = data.map((ceils, i) => ({ ceils, rowIndex: i })); |
||||
|
|
||||
|
const rowLength = data.length; |
||||
|
|
||||
|
if (rowLength > rowNum && rowLength < 2 * rowNum) { |
||||
|
data = [...data, ...data]; |
||||
|
} |
||||
|
|
||||
|
return data.map((d, i) => ({ ...d, scroll: i })); |
||||
|
} |
||||
|
|
||||
|
function calcAligns(mergedConfig, header) { |
||||
|
const columnNum = header.length; |
||||
|
|
||||
|
const aligns = new Array(columnNum).fill('left'); |
||||
|
|
||||
|
const { align } = mergedConfig; |
||||
|
|
||||
|
return deepMerge(aligns, align); |
||||
|
} |
||||
|
|
||||
|
const ScrollBoard = forwardRef(({ |
||||
|
onClick, config = {}, className, style, onMouseOver, |
||||
|
}, ref) => { |
||||
|
const { width, height, domRef } = useAutoResize(ref); |
||||
|
|
||||
|
const [state, setState] = useState({ |
||||
|
mergedConfig: null, |
||||
|
|
||||
|
header: [], |
||||
|
|
||||
|
rows: [], |
||||
|
|
||||
|
rowsShow: [], |
||||
|
|
||||
|
widths: [], |
||||
|
|
||||
|
heights: [], |
||||
|
|
||||
|
aligns: [], |
||||
|
}); |
||||
|
|
||||
|
const { |
||||
|
mergedConfig, header, rows, widths, heights, aligns, rowsShow, |
||||
|
} = state; |
||||
|
|
||||
|
const stateRef = useRef({ |
||||
|
...state, |
||||
|
rowsData: [], |
||||
|
avgHeight: 0, |
||||
|
animationIndex: 0, |
||||
|
}); |
||||
|
|
||||
|
Object.assign(stateRef.current, state); |
||||
|
|
||||
|
function onResize() { |
||||
|
if (!mergedConfig) return; |
||||
|
|
||||
|
const widths = calcWidths(mergedConfig, stateRef.current.rowsData); |
||||
|
|
||||
|
const heights = calcHeights(mergedConfig, header); |
||||
|
|
||||
|
const data = { widths, heights }; |
||||
|
|
||||
|
Object.assign(stateRef.current, data); |
||||
|
setState((state) => ({ ...state, ...data })); |
||||
|
} |
||||
|
const [init, setInit] = useState(true); |
||||
|
|
||||
|
function calcData() { |
||||
|
// const mergedConfig = deepMerge(
|
||||
|
// deepClone(defaultConfig, true),
|
||||
|
// config || {},
|
||||
|
// );
|
||||
|
const mergedConfig = { |
||||
|
...defaultConfig, |
||||
|
...config, |
||||
|
}; |
||||
|
|
||||
|
const header = calcHeaderData(mergedConfig); |
||||
|
|
||||
|
const rows = calcRows(mergedConfig); |
||||
|
|
||||
|
const widths = calcWidths(mergedConfig, stateRef.current.rowsData); |
||||
|
|
||||
|
const heights = calcHeights(mergedConfig, header); |
||||
|
|
||||
|
const aligns = calcAligns(mergedConfig, header); |
||||
|
|
||||
|
const data = { |
||||
|
mergedConfig, |
||||
|
header, |
||||
|
rows, |
||||
|
widths, |
||||
|
aligns, |
||||
|
heights: init ? heights : state.heights.concat(heights), |
||||
|
rowsShow: init ? rows : state.rowsShow, |
||||
|
}; |
||||
|
setInit(false); |
||||
|
Object.assign(stateRef.current, data, { |
||||
|
rowsData: rows, |
||||
|
animationIndex: stateRef.current.animationIndex, |
||||
|
}); |
||||
|
|
||||
|
setState((state) => ({ ...state, ...data })); |
||||
|
} |
||||
|
|
||||
|
function calcWidths({ columnWidth, header }, rowsData) { |
||||
|
const usedWidth = columnWidth.reduce((all, w) => all + w, 0); |
||||
|
|
||||
|
let columnNum = 0; |
||||
|
if (rowsData[0]) { |
||||
|
columnNum = rowsData[0].ceils.length; |
||||
|
} else if (header.length) { |
||||
|
columnNum = header.length; |
||||
|
} |
||||
|
|
||||
|
const avgWidth = (width - usedWidth) / (columnNum - columnWidth.length); |
||||
|
|
||||
|
const widths = new Array(columnNum).fill(avgWidth); |
||||
|
|
||||
|
return deepMerge(widths, columnWidth); |
||||
|
} |
||||
|
|
||||
|
function calcHeights({ headerHeight, rowNum, data }, header) { |
||||
|
let allHeight = height; |
||||
|
|
||||
|
if (header.length) allHeight -= headerHeight; |
||||
|
|
||||
|
const avgHeight = allHeight / rowNum; |
||||
|
|
||||
|
Object.assign(stateRef.current, { avgHeight }); |
||||
|
|
||||
|
return new Array(data.length).fill(avgHeight); |
||||
|
} |
||||
|
|
||||
|
function* animation(start = false) { |
||||
|
let { |
||||
|
avgHeight, |
||||
|
animationIndex, |
||||
|
mergedConfig: { waitTime, carousel, rowNum }, |
||||
|
rowsData, |
||||
|
} = stateRef.current; |
||||
|
|
||||
|
const rowLength = rowsData.length; |
||||
|
|
||||
|
if (start) yield new Promise((resolve) => setTimeout(resolve, waitTime)); |
||||
|
|
||||
|
const animationNum = carousel === 'single' ? 1 : rowNum; |
||||
|
|
||||
|
let rows = rowsData.slice(animationIndex); |
||||
|
rows.push(...rowsData.slice(0, animationIndex)); |
||||
|
rows = rows.slice(0, carousel === 'page' ? rowNum * 2 : rowNum + 1); |
||||
|
|
||||
|
const heights = new Array(rowLength).fill(avgHeight); |
||||
|
setState((state) => ({ |
||||
|
...state, rows, heights, rowsShow: rows, |
||||
|
})); |
||||
|
|
||||
|
yield new Promise((resolve) => setTimeout(resolve, 300)); |
||||
|
|
||||
|
animationIndex += animationNum; |
||||
|
|
||||
|
const back = animationIndex - rowLength; |
||||
|
if (back >= 0) animationIndex = back; |
||||
|
|
||||
|
const newHeights = [...heights]; |
||||
|
newHeights.splice(0, animationNum, ...new Array(animationNum).fill(0)); |
||||
|
|
||||
|
Object.assign(stateRef.current, { animationIndex }); |
||||
|
setState((state) => ({ ...state, heights: newHeights })); |
||||
|
} |
||||
|
|
||||
|
function emitEvent(handle, ri, ci, row, ceil) { |
||||
|
const { ceils, rowIndex } = row; |
||||
|
|
||||
|
handle && handle({ |
||||
|
row: ceils, ceil, rowIndex, columnIndex: ci, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function handleHover(enter, ri, ci, row, ceil) { |
||||
|
if (enter) emitEvent(onMouseOver, ri, ci, row, ceil); |
||||
|
|
||||
|
if (!mergedConfig.hoverPause) return; |
||||
|
|
||||
|
const { pause, resume } = task.current; |
||||
|
|
||||
|
enter && pause && resume ? pause() : resume && resume(); |
||||
|
} |
||||
|
|
||||
|
// updateRows(rows, animationIndex) {
|
||||
|
// const { mergedConfig, animationHandler, animation } = this
|
||||
|
// this.mergedConfig = {
|
||||
|
// ...mergedConfig,
|
||||
|
// data: [...rows]
|
||||
|
// }
|
||||
|
// this.needCalc = true
|
||||
|
// if (typeof animationIndex === 'number') this.animationIndex = animationIndex
|
||||
|
// if (!animationHandler) animation(true)
|
||||
|
// }
|
||||
|
|
||||
|
const getBackgroundColor = (rowIndex) => mergedConfig[rowIndex % 2 === 0 ? 'evenRowBGC' : 'oddRowBGC']; |
||||
|
|
||||
|
const task = useRef({}); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
calcData(); |
||||
|
|
||||
|
let start = true; |
||||
|
|
||||
|
function* loop() { |
||||
|
while (true) { |
||||
|
yield* animation(start); |
||||
|
|
||||
|
start = false; |
||||
|
|
||||
|
const { waitTime } = stateRef.current.mergedConfig; |
||||
|
|
||||
|
yield new Promise((resolve) => setTimeout(resolve, waitTime - 300)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const { |
||||
|
mergedConfig: { rowNum }, |
||||
|
rows: rowsData, |
||||
|
} = stateRef.current; |
||||
|
|
||||
|
const rowLength = rowsData.length; |
||||
|
|
||||
|
if (rowNum >= rowLength) { |
||||
|
setState((prestate) => ({ |
||||
|
...prestate, rowsShow: state.rows, |
||||
|
})); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
task.current = co(loop); |
||||
|
|
||||
|
return task.current.end; |
||||
|
}, [config, domRef.current]); |
||||
|
|
||||
|
useEffect(onResize, [width, height, domRef.current]); |
||||
|
|
||||
|
const classNames = useMemo(() => classnames('dv-scroll-board', className), [ |
||||
|
className, |
||||
|
]); |
||||
|
|
||||
|
return ( |
||||
|
<div className={classNames} style={style} ref={domRef}> |
||||
|
{!!header.length && !!mergedConfig && ( |
||||
|
<div |
||||
|
className="header" |
||||
|
style={{ backgroundColor: `${mergedConfig.headerBGC}` }} |
||||
|
> |
||||
|
{header.map((headerItem, i) => ( |
||||
|
<div |
||||
|
className="header-item" |
||||
|
key={`${headerItem}-${i}`} |
||||
|
style={{ |
||||
|
height: `${mergedConfig.headerHeight}px`, |
||||
|
lineHeight: `${mergedConfig.headerHeight}px`, |
||||
|
width: `${widths[i]}px`, |
||||
|
}} |
||||
|
align={aligns[i]} |
||||
|
dangerouslySetInnerHTML={{ __html: headerItem }} |
||||
|
/> |
||||
|
))} |
||||
|
</div> |
||||
|
)} |
||||
|
|
||||
|
{!!mergedConfig && ( |
||||
|
<div |
||||
|
className="rows" |
||||
|
style={{ |
||||
|
height: `${height |
||||
|
- (header.length ? mergedConfig.headerHeight : 0)}px`,
|
||||
|
}} |
||||
|
> |
||||
|
{rowsShow.map((row, ri) => ( |
||||
|
<div |
||||
|
className="row-item" |
||||
|
key={`${row.toString()}-${row.scroll}`} |
||||
|
style={{ |
||||
|
height: `${heights[ri]}px`, |
||||
|
lineHeight: `${heights[ri]}px`, |
||||
|
backgroundColor: `${getBackgroundColor(row.rowIndex)}`, |
||||
|
}} |
||||
|
> |
||||
|
{row.ceils.map((ceil, ci) => { |
||||
|
if (typeof (ceil) === 'string') { |
||||
|
return ( |
||||
|
<div |
||||
|
className="ceil" |
||||
|
key={`${ceil}-${ri}-${ci}`} |
||||
|
style={{ width: `${widths[ci]}px` }} |
||||
|
align={aligns[ci]} |
||||
|
dangerouslySetInnerHTML={{ __html: ceil }} |
||||
|
onClick={() => emitEvent(onClick, ri, ci, row, ceil)} |
||||
|
onMouseEnter={() => handleHover(true, ri, ci, row, ceil)} |
||||
|
onMouseLeave={() => handleHover(false)} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
return ( |
||||
|
<div |
||||
|
className="ceil" |
||||
|
style={{ width: `${widths[ci]}px` }} |
||||
|
align={aligns[ci]} |
||||
|
key={`${ri}-${ci}`} |
||||
|
onMouseEnter={() => handleHover(true, ri, ci, row, ceil)} |
||||
|
onMouseLeave={() => handleHover(false)} |
||||
|
> |
||||
|
{ceil} |
||||
|
</div> |
||||
|
); |
||||
|
})} |
||||
|
</div> |
||||
|
))} |
||||
|
</div> |
||||
|
)} |
||||
|
</div> |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
ScrollBoard.propTypes = { |
||||
|
config: PropTypes.object, |
||||
|
onClick: PropTypes.func, |
||||
|
onMouseOver: PropTypes.func, |
||||
|
className: PropTypes.string, |
||||
|
style: PropTypes.object, |
||||
|
}; |
||||
|
|
||||
|
export default ScrollBoard; |
@ -0,0 +1,44 @@ |
|||||
|
.dv-scroll-board { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
color: #fff; |
||||
|
|
||||
|
.text { |
||||
|
padding: 0 10px; |
||||
|
box-sizing: border-box; |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
|
||||
|
.header { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
font-size: 15px; |
||||
|
|
||||
|
.header-item { |
||||
|
.text; |
||||
|
transition: all 0.3s; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.rows { |
||||
|
overflow: hidden; |
||||
|
|
||||
|
.row-item { |
||||
|
display: flex; |
||||
|
font-size: 14px; |
||||
|
transition: all 0.3s; |
||||
|
} |
||||
|
|
||||
|
.ceil { |
||||
|
.text; |
||||
|
} |
||||
|
|
||||
|
.index { |
||||
|
border-radius: 3px; |
||||
|
padding: 0px 3px; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,34 @@ |
|||||
|
'use strict' |
||||
|
|
||||
|
import React from 'react' |
||||
|
import './index.less' |
||||
|
class Box extends React.Component { |
||||
|
render() { |
||||
|
const { title, height = '100%', children, bodyPaddingTop = 1, titlePaddingTop, margin, overflow } = this.props |
||||
|
|
||||
|
const headerbg = { |
||||
|
background: 'url(/assets/images/homePage/bigscreen/headertitlebg.png) no-repeat', |
||||
|
backgroundSize: '100% 100%', |
||||
|
} |
||||
|
return ( |
||||
|
<div style={{ height, width: '100%', margin: margin || "0px 0px 28px" }}> |
||||
|
<div style={{ |
||||
|
height: height, listStyle: 'none', overflow: overflow || 'hidden', |
||||
|
backgroundImage: 'linear-gradient(180deg, #00204a00 3%, #50c9f71a 100%)', |
||||
|
}}> |
||||
|
<div style={{ height: 42, paddingLeft: 24, paddingTop: '4px', wordBreak: 'keep-all', whiteSpace: 'nowrap', width: '100%', ...headerbg }}> |
||||
|
<span className='card-title'>{title}</span><div className='_sorrow' /> |
||||
|
</div> |
||||
|
<div |
||||
|
style={{ |
||||
|
width: '100%', height: 2, |
||||
|
marginTop: titlePaddingTop || 10, marginBottom: bodyPaddingTop || 25, |
||||
|
}} /> |
||||
|
{children} |
||||
|
</div> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
export default Box |
||||
|
|
@ -0,0 +1,373 @@ |
|||||
|
@card-height: calc(100% - 42px - 13px); //左右卡片内容高度定义 目前卡片为等高 |
||||
|
|
||||
|
//节点资源 |
||||
|
.node-resource-container { |
||||
|
display: flex; |
||||
|
height: @card-height; |
||||
|
width: 100%; |
||||
|
align-items: center; |
||||
|
|
||||
|
._item { |
||||
|
width: 33%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
|
||||
|
._noderesource_data { |
||||
|
font-family: D-DINExp-Bold; |
||||
|
font-weight: 600; |
||||
|
font-size: 24px; |
||||
|
color: #FFFFFF; |
||||
|
line-height: 43.2px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
|
||||
|
._percent { |
||||
|
opacity: 0.8; |
||||
|
font-family: PingFangSC-Regular; |
||||
|
font-weight: 400; |
||||
|
font-size: 12px; |
||||
|
color: #C8F0FF; |
||||
|
text-align: left; |
||||
|
line-height: 24px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
._noderesource_title { |
||||
|
font-family: YouSheBiaoTiHei; |
||||
|
font-size: 20px; |
||||
|
color: #D8F0FF; |
||||
|
letter-spacing: 1.54px; |
||||
|
text-align: center; |
||||
|
text-shadow: 0 0 10px rgba(0, 145, 255, 0.5); |
||||
|
margin-bottom: 17px; |
||||
|
} |
||||
|
|
||||
|
.disk_icon { |
||||
|
width: 68.73px; |
||||
|
height: 62.77px; |
||||
|
background: url('/assets/images/homePage/bigscreen/disk.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
} |
||||
|
|
||||
|
.cpu_icon { |
||||
|
width: 68.73px; |
||||
|
height: 62.77px; |
||||
|
background: url('/assets/images/homePage/bigscreen/cpu.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
} |
||||
|
|
||||
|
.memory_icon { |
||||
|
width: 68.73px; |
||||
|
height: 62.77px; |
||||
|
background: url('/assets/images/homePage/bigscreen/memory.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//接入数据统计 |
||||
|
.access_data { |
||||
|
display: flex; |
||||
|
height: @card-height; |
||||
|
width: 100%; |
||||
|
justify-content: center; |
||||
|
|
||||
|
font-family: PingFangSC-Regular; |
||||
|
font-weight: 400; |
||||
|
font-size: 14px; |
||||
|
color: #FFFFFF; |
||||
|
|
||||
|
._img { |
||||
|
width: 230px; |
||||
|
height: 95%; |
||||
|
background: url('/assets/images/homePage/bigscreen/accessdata.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
} |
||||
|
|
||||
|
.data_unit { |
||||
|
position: absolute; |
||||
|
top: 27%; |
||||
|
right: 21%; |
||||
|
|
||||
|
.data_number { |
||||
|
line-height: 25px; |
||||
|
font-family: D-DINExp-Bold; |
||||
|
font-weight: 700; |
||||
|
font-size: 20px; |
||||
|
color: #3E86FF; |
||||
|
letter-spacing: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.data_today { |
||||
|
position: absolute; |
||||
|
bottom: 13%; |
||||
|
right: 79%; |
||||
|
text-align: right; |
||||
|
|
||||
|
.data_number { |
||||
|
line-height: 25px; |
||||
|
font-family: D-DINExp-Bold; |
||||
|
font-weight: 700; |
||||
|
font-size: 20px; |
||||
|
color: #00F6E4; |
||||
|
letter-spacing: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.data_total { |
||||
|
position: absolute; |
||||
|
bottom: 13%; |
||||
|
left: 79%; |
||||
|
|
||||
|
.data_number { |
||||
|
line-height: 25px; |
||||
|
font-family: D-DINExp-Bold; |
||||
|
font-weight: 700; |
||||
|
font-size: 20px; |
||||
|
color: #FFDC4E; |
||||
|
letter-spacing: 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.data_top5_unit { |
||||
|
position: absolute; |
||||
|
right: 4%; |
||||
|
top: 18%; |
||||
|
font-family: PingFangSC-Regular; |
||||
|
font-weight: 400; |
||||
|
font-size: 12px; |
||||
|
color: #C8F0FF; |
||||
|
} |
||||
|
|
||||
|
.hotspot_data_container { |
||||
|
display: flex; |
||||
|
height: @card-height; |
||||
|
width: 100%; |
||||
|
justify-content: center; |
||||
|
|
||||
|
font-family: PingFangSC-Regular; |
||||
|
font-weight: 400; |
||||
|
font-size: 14px; |
||||
|
color: #FFFFFF; |
||||
|
|
||||
|
._img { |
||||
|
width: 203px; |
||||
|
height: 80%; |
||||
|
background: url('/assets/images/homePage/bigscreen/hotspotdatabg.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
} |
||||
|
|
||||
|
.hotspot_title { |
||||
|
padding: 2px; |
||||
|
padding-left: 6px; |
||||
|
padding-right: 6px; |
||||
|
background: rgba(77, 241, 227, 0.08); |
||||
|
border: 1px solid rgba(77, 241, 227, 0.1); |
||||
|
box-shadow: inset 0 0 20px 0 rgba(28, 185, 196, 0.23); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
.hotspot_data_number { |
||||
|
font-family: D-DINExp-Bold; |
||||
|
font-weight: 700; |
||||
|
font-size: 18px; |
||||
|
color: #FFFFFF; |
||||
|
} |
||||
|
|
||||
|
._top1 { |
||||
|
position: absolute; |
||||
|
top: 25%; |
||||
|
right: 63%; |
||||
|
text-align: right; |
||||
|
} |
||||
|
|
||||
|
._top2 { |
||||
|
position: absolute; |
||||
|
bottom: 22%; |
||||
|
right: 67%; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
._top3 { |
||||
|
position: absolute; |
||||
|
bottom: 34%; |
||||
|
left: 73%; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//数据共享 |
||||
|
.data_share { |
||||
|
display: flex; |
||||
|
height: @card-height; |
||||
|
|
||||
|
._left_content { |
||||
|
width: 50%; |
||||
|
height: 90%; |
||||
|
padding-left: 30px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
|
||||
|
._right_content { |
||||
|
._today_text { |
||||
|
font-family: YouSheBiaoTiHei; |
||||
|
font-size: 24px; |
||||
|
color: #FFFFFF; |
||||
|
letter-spacing: 0.5px; |
||||
|
position: absolute; |
||||
|
right: 6%; |
||||
|
top: 21%; |
||||
|
} |
||||
|
|
||||
|
padding-top: 23px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
width: 47%; |
||||
|
height: 95%; |
||||
|
background: url(/assets/images/homePage/bigscreen/todaybg.png); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
justify-content: space-evenly; |
||||
|
align-items: center; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
._item_content { |
||||
|
display: flex; |
||||
|
|
||||
|
|
||||
|
._item_icon1 { |
||||
|
width: 52px; |
||||
|
height: 52px; |
||||
|
background: url('/assets/images/homePage/bigscreen/1.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
} |
||||
|
|
||||
|
._item_icon2 { |
||||
|
width: 52px; |
||||
|
height: 52px; |
||||
|
background: url('/assets/images/homePage/bigscreen/2.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
} |
||||
|
|
||||
|
._item_icon3 { |
||||
|
width: 52px; |
||||
|
height: 52px; |
||||
|
background: url('/assets/images/homePage/bigscreen/3.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
} |
||||
|
|
||||
|
._item_text { |
||||
|
color: #C8F0FF; |
||||
|
padding-left: 6px; |
||||
|
|
||||
|
.number_container { |
||||
|
._number { |
||||
|
font-family: D-DINExp-Bold; |
||||
|
font-weight: 700; |
||||
|
font-size: 22px; |
||||
|
color: #FFFFFF; |
||||
|
} |
||||
|
|
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-evenly; |
||||
|
width: 112px; |
||||
|
height: 28px; |
||||
|
background-image: linear-gradient(227deg, #3196AB 0%, #2091cd00 100%); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//大屏中间上部分 |
||||
|
.center_top_data { |
||||
|
|
||||
|
._center_card1 { |
||||
|
width: 353px; |
||||
|
height: 74px; |
||||
|
font-family: YouSheBiaoTiHei; |
||||
|
font-size: 22px; |
||||
|
color: #FFFFFF; |
||||
|
letter-spacing: 0.46px; |
||||
|
text-align: center; |
||||
|
position: absolute; |
||||
|
top: -3%; |
||||
|
left: 32%; |
||||
|
background: url(/assets/images/homePage/bigscreen/centerdatabg1.png); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
._center_card2 { |
||||
|
width: 146px; |
||||
|
height: 35px; |
||||
|
font-family: YouSheBiaoTiHei; |
||||
|
font-size: 16px; |
||||
|
color: #35D0FF; |
||||
|
letter-spacing: 0.46px; |
||||
|
text-align: center; |
||||
|
position: absolute; |
||||
|
top: 26%; |
||||
|
left: 42%; |
||||
|
background: url(/assets/images/homePage/bigscreen/centerdatabg2.png); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
._center_card3 { |
||||
|
width: 146px; |
||||
|
height: 35px; |
||||
|
font-family: YouSheBiaoTiHei; |
||||
|
font-size: 16px; |
||||
|
color: #35D0FF; |
||||
|
letter-spacing: 0.46px; |
||||
|
text-align: center; |
||||
|
position: absolute; |
||||
|
top: 52%; |
||||
|
left: 42%; |
||||
|
background: url(/assets/images/homePage/bigscreen/centerdatabg2.png); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
._center_card4 { |
||||
|
width: 146px; |
||||
|
height: 35px; |
||||
|
font-family: YouSheBiaoTiHei; |
||||
|
font-size: 16px; |
||||
|
color: #35D0FF; |
||||
|
letter-spacing: 0.46px; |
||||
|
text-align: center; |
||||
|
position: absolute; |
||||
|
top: 74%; |
||||
|
left: 42%; |
||||
|
background: url(/assets/images/homePage/bigscreen/centerdatabg2.png); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
} |
@ -0,0 +1,3 @@ |
|||||
|
export const mathRound = (number) => { |
||||
|
return number ? Math.round(number / 1000) / 10 : 0 |
||||
|
} |
@ -0,0 +1,74 @@ |
|||||
|
import React, { useEffect, useState } from 'react' |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { push } from 'react-router-redux'; |
||||
|
import AccessData from '../components/accessData' |
||||
|
import AlarmList from '../components/alarmList' |
||||
|
import DataShare from '../components/dataShare' |
||||
|
import DataTop5 from '../components/dataTop5' |
||||
|
import HotspotData from '../components/hotspotData' |
||||
|
import NodeResource from '../components/nodeResource' |
||||
|
import AbnormalMonitoring from '../components/abnormalMonitoring' |
||||
|
import CenterTop from '../components/centerTop' |
||||
|
import './style.less' |
||||
|
|
||||
|
function homePage(props) { |
||||
|
const { dispatch } = props; |
||||
|
const childStyle = { height: '32%', color: '#fff', marginBottom: 17 } |
||||
|
const cardHeight = document.body.clientHeight * 0.896 * 0.32 |
||||
|
const cardContentHeight = cardHeight - 42 - 13 |
||||
|
return <div className='homepage'> |
||||
|
<div className='_title'> |
||||
|
<div onClick={() => { dispatch(push('/metadataManagement/latestMetadata')) }} className='_exit' ><div className='_icon' />进入后台</div> |
||||
|
</div> |
||||
|
<div className='homepage-left homepage-left-left'> |
||||
|
<div className="list"> |
||||
|
<div className='child' style={childStyle}> |
||||
|
<AccessData /> |
||||
|
</div> |
||||
|
<div className='child' style={childStyle}> |
||||
|
<NodeResource /> |
||||
|
</div> |
||||
|
<div className='child' style={childStyle}> |
||||
|
<AlarmList cardContentHeight={cardContentHeight} /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div className='homepage-center'> |
||||
|
<CenterTop /> |
||||
|
<div className="list"> |
||||
|
<div className='child-top'> |
||||
|
<AbnormalMonitoring /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div className='homepage-left homepage-left-right'> |
||||
|
<div className="list"> |
||||
|
<div className='child-right' style={childStyle}> |
||||
|
<DataShare /> |
||||
|
</div> |
||||
|
<div className='child-right' style={childStyle}> |
||||
|
<DataTop5 cardContentHeight={cardContentHeight} /> |
||||
|
</div> |
||||
|
<div className='child-right' style={childStyle}> |
||||
|
<HotspotData /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
} |
||||
|
|
||||
|
function mapStateToProps(state) { |
||||
|
const { |
||||
|
auth, global |
||||
|
} = state; |
||||
|
return { |
||||
|
clientHeight: global.clientHeight, |
||||
|
actions: global.actions, |
||||
|
|
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(homePage); |
||||
|
|
||||
|
|
@ -0,0 +1,6 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import homePage from './homePage'; |
||||
|
|
||||
|
|
||||
|
export default homePage ; |
@ -0,0 +1,224 @@ |
|||||
|
.homepage { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
background: url('/assets/images/homePage/bigscreen/bg.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
._title { |
||||
|
width: 100%; |
||||
|
height: 88px; |
||||
|
background: url('/assets/images/homePage/bigscreen/top.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
} |
||||
|
|
||||
|
._exit { |
||||
|
position: absolute; |
||||
|
right: 60px; |
||||
|
top: 38px; |
||||
|
cursor: pointer; |
||||
|
color: #C8F0FF; |
||||
|
display: flex; |
||||
|
|
||||
|
._icon { |
||||
|
display: inline-block; |
||||
|
width: 28px; |
||||
|
height: 28px; |
||||
|
background: url('/assets/images/homePage/bigscreen/exit.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
margin-right: 3px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.homepage-left { |
||||
|
width: 21.8%; |
||||
|
height: 89.6%; |
||||
|
position: absolute; |
||||
|
top: 8.2%; |
||||
|
z-index: 300; |
||||
|
} |
||||
|
|
||||
|
.homepage-center { |
||||
|
width: 49.16%; |
||||
|
height: 89.6%; |
||||
|
position: absolute; |
||||
|
bottom: 2.4%; |
||||
|
left: 25.5%; |
||||
|
padding-left: 16px; |
||||
|
padding-right: 16px; |
||||
|
z-index: 400; |
||||
|
|
||||
|
._top { |
||||
|
margin-top: 5%; |
||||
|
height: calc(100% - 200px); |
||||
|
background: url('/assets/images/homePage/bigscreen/centerbg.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
position: relative; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.homepage-left-left { |
||||
|
left: 48px; |
||||
|
} |
||||
|
|
||||
|
.homepage-left-right { |
||||
|
right: 48px; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
.list { |
||||
|
list-style: none; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.list .child { |
||||
|
box-sizing: border-box; |
||||
|
opacity: 0; |
||||
|
transform: translateX(-300px); |
||||
|
animation: show .5s forwards; |
||||
|
} |
||||
|
|
||||
|
.list .child.show { |
||||
|
animation-delay: 0s !important; |
||||
|
} |
||||
|
|
||||
|
.list .child.hide { |
||||
|
opacity: 1; |
||||
|
transform: translateX(0); |
||||
|
animation-name: hide; |
||||
|
animation-delay: 0s; |
||||
|
} |
||||
|
|
||||
|
/*animation-delay*/ |
||||
|
.list .child:not(.hide):nth-child(5n + 1) { |
||||
|
animation-delay: .3s; |
||||
|
} |
||||
|
|
||||
|
.list .child:not(.hide):nth-child(5n + 2) { |
||||
|
animation-delay: .6s; |
||||
|
} |
||||
|
|
||||
|
.list .child:not(.hide):nth-child(5n + 3) { |
||||
|
animation-delay: .9s; |
||||
|
} |
||||
|
|
||||
|
.list .child:not(.hide):nth-child(5n + 4) { |
||||
|
animation-delay: 1.2s; |
||||
|
} |
||||
|
|
||||
|
.list .child:not(.hide):nth-child(5n + 5) { |
||||
|
animation-delay: 1.5s; |
||||
|
} |
||||
|
|
||||
|
.list .child-right { |
||||
|
box-sizing: border-box; |
||||
|
opacity: 0; |
||||
|
transform: translateX(300px); |
||||
|
animation: show .5s forwards; |
||||
|
} |
||||
|
|
||||
|
.list .child-right.show { |
||||
|
animation-delay: 0s !important; |
||||
|
} |
||||
|
|
||||
|
.list .child-right.hide { |
||||
|
opacity: 1; |
||||
|
transform: translateX(0); |
||||
|
animation-name: hide; |
||||
|
animation-delay: 0s; |
||||
|
} |
||||
|
|
||||
|
/*animation-delay*/ |
||||
|
.list .child-right:not(.hide):nth-child(5n + 1) { |
||||
|
animation-delay: .3s; |
||||
|
} |
||||
|
|
||||
|
.list .child-right:not(.hide):nth-child(5n + 2) { |
||||
|
animation-delay: .6s; |
||||
|
} |
||||
|
|
||||
|
.list .child-right:not(.hide):nth-child(5n + 3) { |
||||
|
animation-delay: .9s; |
||||
|
} |
||||
|
|
||||
|
.list .child-right:not(.hide):nth-child(5n + 4) { |
||||
|
animation-delay: 1.2s; |
||||
|
} |
||||
|
|
||||
|
.list .child-right:not(.hide):nth-child(5n + 5) { |
||||
|
animation-delay: 1.5s; |
||||
|
} |
||||
|
|
||||
|
.list .child-top { |
||||
|
box-sizing: border-box; |
||||
|
opacity: 0; |
||||
|
transform: translateY(300px); |
||||
|
animation: show 1s forwards; |
||||
|
} |
||||
|
|
||||
|
.list .child-top.show { |
||||
|
animation-delay: 0s !important; |
||||
|
} |
||||
|
|
||||
|
.list .child-top.hide { |
||||
|
opacity: 1; |
||||
|
transform: translateY(0); |
||||
|
animation-name: hide; |
||||
|
animation-delay: 0s; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@keyframes show { |
||||
|
to { |
||||
|
opacity: 1; |
||||
|
transform: translateX(0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes hide { |
||||
|
to { |
||||
|
opacity: 0; |
||||
|
transform: translateX(100px); |
||||
|
max-height: 0; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.center-card-title { |
||||
|
height: 31px; |
||||
|
font-family: YouSheBiaoTiHei; |
||||
|
font-size: 24px; |
||||
|
color: #FFFFFF; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
margin-top: 5px; |
||||
|
|
||||
|
._icon_left { |
||||
|
width: 32px; |
||||
|
height: 17px; |
||||
|
background: url('/assets/images/homePage/bigscreen/center-left.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
margin-right: 11px; |
||||
|
margin-left: 10px; |
||||
|
} |
||||
|
|
||||
|
._icon_right { |
||||
|
width: 32px; |
||||
|
height: 17px; |
||||
|
background: url('/assets/images/homePage/bigscreen/center-right.png'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
margin-right: 11px; |
||||
|
margin-left: 10px; |
||||
|
} |
||||
|
} |
@ -0,0 +1,15 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import reducers from './reducers'; |
||||
|
import routes from './routes'; |
||||
|
import actions from './actions'; |
||||
|
import { getNavItem } from './nav-item'; |
||||
|
|
||||
|
export default { |
||||
|
key: 'homePage', |
||||
|
name: '首页', |
||||
|
reducers: reducers, |
||||
|
routes: routes, |
||||
|
actions: actions, |
||||
|
getNavItem: getNavItem |
||||
|
}; |
@ -0,0 +1,11 @@ |
|||||
|
import React from 'react'; |
||||
|
import { Link } from 'react-router-dom'; |
||||
|
import { Menu } from 'antd'; |
||||
|
import { HomeOutlined } from '@ant-design/icons'; |
||||
|
export function getNavItem(user) { |
||||
|
return ( |
||||
|
user?.role == '系统管理员' && <Menu.Item key="homePage" icon={<HomeOutlined />}> |
||||
|
<Link to="/homePage">数据监控平台</Link> |
||||
|
</Menu.Item> |
||||
|
); |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
} |
@ -0,0 +1,13 @@ |
|||||
|
'use strict'; |
||||
|
import homePage from './containers/index'; |
||||
|
|
||||
|
export default [{ |
||||
|
type: 'outer', |
||||
|
route: { |
||||
|
path: '/homePage', |
||||
|
key: 'homePage', |
||||
|
breadcrumb: '数据监控平台', |
||||
|
// 不设置 component 则面包屑禁止跳转
|
||||
|
component: homePage |
||||
|
} |
||||
|
}]; |
@ -0,0 +1,10 @@ |
|||||
|
|
||||
|
@icon-url: "/assets/fonticon/iconfont"; |
||||
|
|
||||
|
.tree-transfer .ant-transfer-list-body { |
||||
|
overflow: auto !important; |
||||
|
} |
||||
|
|
||||
|
.ant-pro-table-search { |
||||
|
background-color: @component-background !important; |
||||
|
} |
@ -0,0 +1,16 @@ |
|||||
|
@import "~antd/lib/style/themes/default.less"; |
||||
|
@primary-color : @blue-6; |
||||
|
@link-color : @primary-color; |
||||
|
@secondary-color : fade(@primary-color, 20%); |
||||
|
@btn-primary-bg : @primary-color; |
||||
|
@select-item-selected-option-color: @primary-color; |
||||
|
@processing-color : @primary-color; |
||||
|
@select-item-selected-bg : @background-color-base; |
||||
|
@skeleton-color : @primary-color; |
||||
|
@btn-primary-bg : @primary-color; |
||||
|
|
||||
|
@component-background: transparent; |
||||
|
|
||||
|
:root { |
||||
|
--PC: @primary-color; |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
export const AuthorizationCode = { |
||||
|
|
||||
|
}; |
@ -0,0 +1,13 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
export default class Func { |
||||
|
static isAuthorized(authcode) { |
||||
|
if (JSON.parse(sessionStorage.getItem('user'))) { |
||||
|
const { resources } = JSON.parse(sessionStorage.getItem('user')); |
||||
|
return resources.includes(authcode); |
||||
|
}else{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
@ -0,0 +1,30 @@ |
|||||
|
import React, { useEffect } from 'react'; |
||||
|
import { useRequest } from 'ahooks'; |
||||
|
import { Request } from '@peace/utils'; |
||||
|
|
||||
|
export const useFsRequest = ({ ...props }) => { |
||||
|
const { |
||||
|
method = 'get', header = null, body = {}, query = {}, root = null, url, ...rest |
||||
|
} = props; |
||||
|
return useRequest(() => { |
||||
|
if (method === 'post') { |
||||
|
return Request.post(url, body, query, root, header); |
||||
|
} |
||||
|
if (method === 'put') { |
||||
|
return Request.put(url, body, query, root, header); |
||||
|
} |
||||
|
if (method === 'delete') { |
||||
|
return Request.delete(url, query, root, header); |
||||
|
} |
||||
|
return Request.get(url, query, root, header); |
||||
|
}, { |
||||
|
loadingDelay: 500, |
||||
|
...rest, |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
|
||||
|
export default { |
||||
|
useFsRequest, |
||||
|
}; |
@ -0,0 +1,12 @@ |
|||||
|
'use strict'; |
||||
|
import { AuthorizationCode } from './authCode'; |
||||
|
import { ApiTable, RouteTable, } from './webapi' |
||||
|
import Func from './func'; |
||||
|
import { useFsRequest } from './hooks'; |
||||
|
|
||||
|
export { |
||||
|
AuthorizationCode, |
||||
|
Func, |
||||
|
ApiTable, RouteTable, |
||||
|
useFsRequest, |
||||
|
} |
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
@ -0,0 +1,172 @@ |
|||||
|
'use strict'; |
||||
|
import request from 'superagent'; |
||||
|
|
||||
|
export const ApiTable = { |
||||
|
login: 'login', |
||||
|
logout: 'logout', |
||||
|
validatePhone: 'validate/phone', |
||||
|
//标签管理
|
||||
|
getTags: 'tags', |
||||
|
postTagSets: 'tag-sets', |
||||
|
putTagSets: 'tag-sets/{id}', |
||||
|
delTagSets: 'tag-sets/{id}', |
||||
|
postTags: 'tags', |
||||
|
putTags: 'tags/{id}', |
||||
|
delTags: 'tags/{id}', |
||||
|
//元数据管理-模型管理
|
||||
|
getMetaModelList: 'meta/models', |
||||
|
addMetaModel: 'meta/model', |
||||
|
modifyMetaModel: 'meta/model/{id}', |
||||
|
//最新元数据-资源目录
|
||||
|
getResourceCatalog: 'resource-catalog', |
||||
|
postResourceCatalog: 'resource-catalog', |
||||
|
putResourceCatalog: 'resource-catalog/{id}', |
||||
|
delResourceCatalog: 'resource-catalog/{id}', |
||||
|
listStructuredData: 'listStructuredData', |
||||
|
publishingServices: 'publishing/services', |
||||
|
|
||||
|
//最新元数据-元数据列表查询
|
||||
|
getMetadataDatabases: 'metadata/databases', |
||||
|
getMetadataFiles: 'metadata/files', |
||||
|
getMetadataRestapis: 'metadata/restapis', |
||||
|
getMetadataModels: 'metadata/models', |
||||
|
//库表元数据增删改、等配置
|
||||
|
postMetadataDatabases: 'metadata/databases', |
||||
|
putMetadataDatabases: 'metadata/databases/{id}', |
||||
|
delMetadataDatabases: 'metadata/databases/{id}', |
||||
|
getMetadataDatabasesById: 'metadata/databases/{id}', |
||||
|
postTagMetadata: 'tag/metadata', |
||||
|
getTagMetadata: 'tag/metadata/{id}', |
||||
|
getMetadataResourceApplications: 'resource-consumption/applications', |
||||
|
postMetadataResourceApplications: 'resource-consumption/applications', |
||||
|
//文件元数据增删改
|
||||
|
postMetadataFiles: 'metadata/files', |
||||
|
putMetadataFiles: 'metadata/files/{id}', |
||||
|
delMetadataFiles: 'metadata/files/{id}', |
||||
|
//接口元数据增删改
|
||||
|
postMetadataRestapis: 'metadata/restapis', |
||||
|
putMetadataRestapis: 'metadata/restapis/{id}', |
||||
|
delMetadataRestapis: 'metadata/restapis/{id}', |
||||
|
|
||||
|
//业务元数据管理
|
||||
|
getBusinessMetadataDatabases: 'business/metadata/databases', |
||||
|
postBusinessMetadataDatabases: 'business/metadata/databases', |
||||
|
putBusinessMetadataDatabases: 'business/metadata/databases/{id}', |
||||
|
delBusinessMetadataDatabases: 'business/metadata/databases/{id}', |
||||
|
getBusinessMetadataFiles: 'business/metadata/files', |
||||
|
postBusinessMetadataFiles: 'business/metadata/files', |
||||
|
putBusinessMetadataFiles: 'business/metadata/files/{id}', |
||||
|
delBusinessMetadataFiles: 'business/metadata/files/{id}', |
||||
|
getBusinessMetadataRestapis: 'business/metadata/restapis', |
||||
|
postBusinessMetadataRestapis: 'business/metadata/restapis', |
||||
|
putBusinessMetadataRestapis: 'business/metadata/restapis/{id}', |
||||
|
delBusinessMetadataRestapis: 'business/metadata/restapis/{id}', |
||||
|
|
||||
|
//元数据采集-数据源管理
|
||||
|
pgCheckConnect: 'adapter/check/connect', |
||||
|
addDataSource: 'meta/acq/dataSource', |
||||
|
getAdapters: 'meta/acq/adapters', |
||||
|
getDataSources: 'meta/acq/dataSources', |
||||
|
modifyDataSource: 'acq/dataSource/{id}', |
||||
|
|
||||
|
//元数据采集-采集任务管理
|
||||
|
addTask: 'meta/acq/task', |
||||
|
getTasks: 'meta/acq/tasks', |
||||
|
modifyTask: 'acq/task/{id}', |
||||
|
runTask: 'run/acq/task', |
||||
|
|
||||
|
//采集日志
|
||||
|
getLogs: "meta/acq/logs", |
||||
|
|
||||
|
//资源消费
|
||||
|
approveList: 'resource/approve', |
||||
|
//用户管理
|
||||
|
getUserList: 'meta/members', |
||||
|
addUser: 'meta/member', |
||||
|
modifyUser: 'meta/member/{id}', |
||||
|
|
||||
|
//元数据检索
|
||||
|
searchMetadata: "meta/data/search", |
||||
|
|
||||
|
//数据质量
|
||||
|
standardDocFolders: 'standard-doc-folders', |
||||
|
standardDocs: 'standard-docs', |
||||
|
postFolderFile: 'postFolderFile', |
||||
|
businessRules: 'business-rules', |
||||
|
delBusinessRules: 'business-rules/{id}', |
||||
|
regularBasis: 'regular-basis', |
||||
|
fetchFiles: 'fetchFiles', |
||||
|
getQualityInspection: 'quality-inspection', |
||||
|
|
||||
|
//数据安全规范上传
|
||||
|
specifications: 'data-security/specifications', |
||||
|
delSpecifications: 'data-security/specifications/{fileIds}', |
||||
|
|
||||
|
//元数据检索
|
||||
|
searchMetadata: "meta/data/search", |
||||
|
|
||||
|
//备份恢复
|
||||
|
getBackupsList: 'meta/backups', |
||||
|
addBackups: 'meta/backups', |
||||
|
modifyBackups: 'meta/backups/{id}', |
||||
|
restoreBackups: 'backups/restore', |
||||
|
|
||||
|
//REST服务
|
||||
|
serviceManagement: 'service-management', |
||||
|
delServiceManagement: 'service-management/{id}', |
||||
|
lookField: 'lookField', |
||||
|
|
||||
|
}; |
||||
|
|
||||
|
export const RouteTable = { |
||||
|
fileUpload: '/_upload/new', |
||||
|
cleanUpUploadTrash: '/_upload/cleanup', |
||||
|
}; |
||||
|
|
||||
|
const resultHandler = (resolve, reject) => (err, res) => { |
||||
|
if (err) { |
||||
|
if (err.status == 401) { |
||||
|
// 退出到登录页
|
||||
|
const user = JSON.parse(sessionStorage.getItem('user')); |
||||
|
sessionStorage.clear(); |
||||
|
window.document.location.replace('/login'); |
||||
|
reject('unauth'); |
||||
|
} else { |
||||
|
reject({ |
||||
|
status: err.status || 0, |
||||
|
body: err.response ? err.response.body : err.message |
||||
|
}); |
||||
|
} |
||||
|
reject({ |
||||
|
status: err.status || 0, |
||||
|
body: err.response ? err.response.body : err.message |
||||
|
}); |
||||
|
} else { |
||||
|
resolve(res.body); |
||||
|
} |
||||
|
}; |
||||
|
export const buildRoute = (url) => { |
||||
|
const user = JSON.parse(sessionStorage.getItem('user')); |
||||
|
if (user == null) { |
||||
|
return url; |
||||
|
} |
||||
|
let connector = url.indexOf('?') === -1 ? '?' : '&'; |
||||
|
return `${url}${connector}token=${user.token}`; |
||||
|
}; |
||||
|
export class RouteRequest { |
||||
|
static get = (url, query) => |
||||
|
new Promise((resolve, reject) => { |
||||
|
request.get(buildRoute(url)).query(query).end(resultHandler(resolve, reject)); |
||||
|
}); |
||||
|
|
||||
|
static post = (url, data, query) => |
||||
|
new Promise((resolve, reject) => { |
||||
|
request.post(buildRoute(url)).query(query).send(data).end(resultHandler(resolve, reject)); |
||||
|
}); |
||||
|
|
||||
|
static delete = (url, data, query) => |
||||
|
new Promise((resolve, reject) => { |
||||
|
request.delete(buildRoute(url)).query(query).send(data).end(resultHandler(resolve, reject)); |
||||
|
}); |
||||
|
} |
||||
|
|
@ -0,0 +1,145 @@ |
|||||
|
/** |
||||
|
* Created by rain on 2016/1/25. |
||||
|
*/ |
||||
|
|
||||
|
'use strict'; |
||||
|
/*jslint node:true*/ |
||||
|
const path = require('path'); |
||||
|
/*这种以CommonJS的同步形式去引入其它模块的方式代码更加简洁:获取组件*/ |
||||
|
const os = require('os'); |
||||
|
const moment = require('moment'); |
||||
|
const args = require('args'); |
||||
|
const dev = process.env.NODE_ENV == 'development'; |
||||
|
dev && console.log('\x1B[33m%s\x1b[0m', '请遵循并及时更新 readme.md,维护良好的开发环境,媛猿有责'); |
||||
|
// // 启动参数
|
||||
|
args.option(['p', 'port'], '启动端口'); |
||||
|
args.option(['u', 'api-url'], 'webapi的URL'); |
||||
|
args.option('qnak', 'qiniuAccessKey'); |
||||
|
args.option('qnsk', 'qiniuSecretKey'); |
||||
|
args.option('qnbkt', 'qiniuBucket'); |
||||
|
args.option('qndmn', 'qiniuDomain'); |
||||
|
const flags = dev ? args.parse(process.argv) : {}; |
||||
|
|
||||
|
const FS_UNIAPP_API = process.env.FS_UNIAPP_API || flags.apiUrl; |
||||
|
|
||||
|
const ANXINCLOUD_QINIU_ACCESSKEY = process.env.ANXINCLOUD_QINIU_ACCESSKEY || flags.qnak; |
||||
|
const ANXINCLOUD_QINIU_SECRETKEY = process.env.ANXINCLOUD_QINIU_SECRETKEY || flags.qnsk; |
||||
|
const ANXINCLOUD_QINIU_BUCKET_RESOURCE = process.env.ANXINCLOUD_QINIU_BUCKET_RESOURCE || flags.qnbkt; |
||||
|
const ANXINCLOUD_QINIU_DOMAIN_QNDMN_RESOURCE = process.env.ANXINCLOUD_QINIU_DOMAIN_QNDMN_RESOURCE || flags.qndmn; |
||||
|
|
||||
|
//阿里OSS
|
||||
|
const ALI_OSS_ACCESSKEY = process.env.ALI_OSS_ACCESSKEY || flags.aliOssAccessKey; |
||||
|
const ALI_OSS_SECRETKET = process.env.ALI_OSS_SECRETKET || flags.aliOssSecretKey; |
||||
|
const ALI_OSS_BUCKET = process.env.ALI_OSS_BUCKET || flags.aliOssBucket; |
||||
|
const ALI_OSS_REGION = process.env.ALI_OSS_REGION || flags.aliOssRegion; |
||||
|
|
||||
|
if ( |
||||
|
!FS_UNIAPP_API |
||||
|
// || !ANXINCLOUD_QINIU_ACCESSKEY
|
||||
|
// || !ANXINCLOUD_QINIU_SECRETKEY
|
||||
|
// || !ANXINCLOUD_QINIU_BUCKET_RESOURCE
|
||||
|
// || !ANXINCLOUD_QINIU_DOMAIN_QNDMN_RESOURCE
|
||||
|
) { |
||||
|
console.log('缺少启动参数,异常退出'); |
||||
|
args.showHelp(); |
||||
|
process.exit(-1); |
||||
|
} |
||||
|
|
||||
|
const product = { |
||||
|
port: flags.port || 8080, |
||||
|
staticDirs: [path.join(__dirname, './client')], |
||||
|
mws: [ |
||||
|
{ |
||||
|
entry: require('./middlewares/proxy').entry, |
||||
|
opts: { |
||||
|
host: FS_UNIAPP_API, |
||||
|
match: /^\/_api\//, |
||||
|
} |
||||
|
}, |
||||
|
// {
|
||||
|
// entry: require('./middlewares/attachment').entry,
|
||||
|
// opts: {
|
||||
|
// qiniu: {
|
||||
|
// accessKey: ANXINCLOUD_QINIU_ACCESSKEY,
|
||||
|
// secretKey: ANXINCLOUD_QINIU_SECRETKEY,
|
||||
|
// bucket: ANXINCLOUD_QINIU_BUCKET_RESOURCE,
|
||||
|
// domain: ANXINCLOUD_QINIU_DOMAIN_QNDMN_RESOURCE
|
||||
|
// },
|
||||
|
// maxSize: 104857600, // 100M
|
||||
|
// uploadPath: 'other'
|
||||
|
// }
|
||||
|
// },
|
||||
|
{ |
||||
|
entry: require('./routes').entry, |
||||
|
opts: { |
||||
|
apiUrl: FS_UNIAPP_API, |
||||
|
staticRoot: './client', |
||||
|
qiniu: { |
||||
|
fetchUrl: '/_file-server', |
||||
|
domain: ANXINCLOUD_QINIU_DOMAIN_QNDMN_RESOURCE |
||||
|
}, |
||||
|
aliOss: { |
||||
|
fetchUrl: '/_file-ali-server', |
||||
|
accessKey: ALI_OSS_ACCESSKEY, |
||||
|
secretKey: ALI_OSS_SECRETKET, |
||||
|
bucket: ALI_OSS_BUCKET, |
||||
|
region: ALI_OSS_REGION |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
entry: require('./client').entry,// 静态信息
|
||||
|
opts: {} |
||||
|
} |
||||
|
], |
||||
|
logger: { |
||||
|
level: 'debug', |
||||
|
json: false, |
||||
|
filename: path.join(__dirname, 'log', 'runtime.txt'), |
||||
|
colorize: true, |
||||
|
maxsize: 1024 * 1024 * 5, |
||||
|
rotationFormat: false, |
||||
|
zippedArchive: true, |
||||
|
maxFiles: 10, |
||||
|
prettyPrint: true, |
||||
|
label: '', |
||||
|
timestamp: () => moment().format('YYYY-MM-DD HH:mm:ss.SSS'), |
||||
|
eol: os.EOL, |
||||
|
tailable: true, |
||||
|
depth: null, |
||||
|
showLevel: true, |
||||
|
maxRetries: 1 |
||||
|
} |
||||
|
}; |
||||
|
if (product.frontParams) { |
||||
|
const patt = /[^A-Z|_]+/ |
||||
|
for (let k in product.frontParams) { |
||||
|
if (!k.startsWith('FS_') || patt.test(k)) { |
||||
|
console.warn(`参数 ${k} 当以大写字母和下划线组成 且以 FS_ 开头`); |
||||
|
process.exit(-1); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let config; |
||||
|
if (dev) { |
||||
|
config = { |
||||
|
port: product.port, |
||||
|
staticDirs: product.staticDirs, |
||||
|
frontParams: product.frontParams, |
||||
|
mws: product.mws.concat([ |
||||
|
{ |
||||
|
entry: require('./middlewares/webpack-dev').entry, |
||||
|
opts: { |
||||
|
frontParams: product.frontParams, |
||||
|
} |
||||
|
} |
||||
|
]), |
||||
|
logger: product.logger |
||||
|
} |
||||
|
config.logger.filename = path.join(__dirname, 'log', 'development.txt'); |
||||
|
} else { |
||||
|
config = product; |
||||
|
} |
||||
|
|
||||
|
module.exports = config;//区分开发和发布
|
@ -0,0 +1,15 @@ |
|||||
|
{ |
||||
|
|
||||
|
"compilerOptions": { |
||||
|
"target": "es6", |
||||
|
"module": "commonjs", |
||||
|
"allowSyntheticDefaultImports": true |
||||
|
}, |
||||
|
"exclude": [ |
||||
|
"node_modules", |
||||
|
"bower_components", |
||||
|
"jspm_packages", |
||||
|
"tmp", |
||||
|
"temp" |
||||
|
] |
||||
|
} |
@ -0,0 +1,23 @@ |
|||||
|
/** |
||||
|
* Created by PengLing on 2018/1/2. |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
|
||||
|
const Attachment = require('fs-attachment'); |
||||
|
|
||||
|
module.exports = { |
||||
|
entry: function (app, router, opts) { |
||||
|
const attachment = new Attachment(opts); |
||||
|
|
||||
|
app.fs = app.fs || {}; |
||||
|
app.fs.attachment = attachment; |
||||
|
|
||||
|
app.fs.logger.log('debug', 'init fs.attachment and inject it into app(app.fs.attachment) and runtime ctx(ctx.fs.attachment)'); |
||||
|
|
||||
|
return async (ctx, next) => { |
||||
|
ctx.fs = ctx.fs || {}; |
||||
|
ctx.fs.attachment = attachment; |
||||
|
await next(); |
||||
|
}; |
||||
|
} |
||||
|
}; |
@ -0,0 +1,16 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
const proxy = require('koa-proxy'); |
||||
|
const convert = require('koa-convert'); |
||||
|
|
||||
|
module.exports = { |
||||
|
entry: function (app, router, opts) { |
||||
|
app.use(convert(proxy({ |
||||
|
host: opts.host, |
||||
|
match: opts.match, |
||||
|
map: function (path) { |
||||
|
return path.replace(opts.match, ''); |
||||
|
} |
||||
|
}))); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,53 @@ |
|||||
|
'use strict'; |
||||
|
const express = require('express') |
||||
|
const webpack = require('webpack'); |
||||
|
const devConfig = require('../webpack.config'); |
||||
|
const middleware = require('webpack-dev-middleware'); |
||||
|
const proxy = require('koa-better-http-proxy'); |
||||
|
const url = require('url'); |
||||
|
|
||||
|
module.exports = { |
||||
|
entry: function (app, router, opts) { |
||||
|
devConfig.plugins.push( |
||||
|
new webpack.DefinePlugin({ |
||||
|
'process.env.NODE_ENV': JSON.stringify('development'), |
||||
|
...(() => { |
||||
|
let nextParams = {} |
||||
|
for (let k in opts.frontParams) { |
||||
|
nextParams[k] = JSON.stringify(opts.frontParams[k]) |
||||
|
} |
||||
|
return nextParams |
||||
|
})() |
||||
|
}), |
||||
|
) |
||||
|
|
||||
|
const compiler = webpack(devConfig); |
||||
|
|
||||
|
app.use(proxy('http://localhost:5501', { |
||||
|
filter: function (ctx) { |
||||
|
return /\/build/.test(url.parse(ctx.url).path); |
||||
|
}, |
||||
|
proxyReqPathResolver: function (ctx) { |
||||
|
return 'client' + url.parse(ctx.url).path; |
||||
|
} |
||||
|
})); |
||||
|
|
||||
|
app.use(proxy('http://localhost:5501', { |
||||
|
filter: function (ctx) { |
||||
|
return /\/$/.test(url.parse(ctx.url).path); |
||||
|
}, |
||||
|
proxyReqPathResolver: function (ctx) { |
||||
|
return 'client/build/index.html'; |
||||
|
} |
||||
|
})); |
||||
|
|
||||
|
const server = express(); |
||||
|
server.use(middleware(compiler)); |
||||
|
//server.use(require("webpack-hot-middleware")(compiler));
|
||||
|
server.listen('5401', function (err) { |
||||
|
if (err) { |
||||
|
console.log(err); |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
}; |
@ -0,0 +1,101 @@ |
|||||
|
{ |
||||
|
"name": "fs-anxincloud-4.0", |
||||
|
"version": "1.0.0", |
||||
|
"description": "anxincloud-4.0", |
||||
|
"main": "server.js", |
||||
|
"scripts": { |
||||
|
"test": "mocha", |
||||
|
"start": "cross-env NODE_ENV=development npm run start-params", |
||||
|
"start-params": "node server -p 5500 -u http://localhost:4400", |
||||
|
"deploy": "export NODE_ENV=production && npm run build && node server", |
||||
|
"build-dev": "set NODE_ENV=development&&webpack --config webpack.config.js", |
||||
|
"build": "set NODE_ENV=production&&webpack --config webpack.config.prod.js" |
||||
|
}, |
||||
|
"keywords": [ |
||||
|
"app" |
||||
|
], |
||||
|
"author": "", |
||||
|
"license": "ISC", |
||||
|
"devDependencies": { |
||||
|
"@babel/core": "^7.14.6", |
||||
|
"@babel/plugin-proposal-class-properties": "^7.14.5", |
||||
|
"@babel/plugin-proposal-object-rest-spread": "^7.14.7", |
||||
|
"@babel/plugin-transform-runtime": "^7.14.5", |
||||
|
"@babel/polyfill": "^7.12.1", |
||||
|
"@babel/preset-env": "^7.14.7", |
||||
|
"@babel/preset-react": "^7.14.5", |
||||
|
"babel-loader": "^8.2.2", |
||||
|
"babel-plugin-import": "^1.13.3", |
||||
|
"babel-polyfill": "^6.26.0", |
||||
|
"connected-react-router": "^6.8.0", |
||||
|
"css-loader": "^3.5.0", |
||||
|
"express": "^4.17.1", |
||||
|
"file-loader": "^6.0.0", |
||||
|
"html-webpack-plugin": "^4.5.0", |
||||
|
"immutable": "^4.0.0-rc.12", |
||||
|
"less": "^3.12.2", |
||||
|
"less-loader": "^7.0.2", |
||||
|
"natty-fetch": "^2.5.3", |
||||
|
"nprogress": "^0.2.0", |
||||
|
"path-to-regexp": "^2.4.0", |
||||
|
"perfect-scrollbar": "^1.5.5", |
||||
|
"react": "^17.0.0", |
||||
|
"react-copy-to-clipboard": "^5.0.1", |
||||
|
"react-dnd": "^10.0.2", |
||||
|
"react-dnd-html5-backend": "^10.0.2", |
||||
|
"react-dom": "^17.0.0", |
||||
|
"react-if": "^2.2.1", |
||||
|
"react-jsonschema-form": "^1.8.1", |
||||
|
"react-quill": "^1.3.5", |
||||
|
"react-redux": "^7.2.1", |
||||
|
"react-router-dom": "^5.2.0", |
||||
|
"react-router-redux": "^4.0.8", |
||||
|
"redux": "^4.0.5", |
||||
|
"redux-thunk": "^2.3.0", |
||||
|
"redux-undo": "^1.0.1", |
||||
|
"style-loader": "^2.0.0", |
||||
|
"webpack": "^5.3.2", |
||||
|
"webpack-bundle-analyzer": "^4.1.0", |
||||
|
"webpack-cli": "^4.2.0", |
||||
|
"webpack-dev-middleware": "^4.0.2", |
||||
|
"webpack-hot-middleware": "^2.25.0" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@ant-design/icons": "^4.6.2", |
||||
|
"@ant-design/pro-form": "^1.34.0", |
||||
|
"@ant-design/pro-table": "^2.48.0", |
||||
|
"@antv/g6": "^4.2.5", |
||||
|
"@douyinfe/semi-ui": "^2.28.1", |
||||
|
"@fs/attachment": "^1.0.0", |
||||
|
"@jiaminghi/data-view-react": "^1.2.5", |
||||
|
"@peace/components": "0.0.35", |
||||
|
"@peace/utils": "0.0.37", |
||||
|
"ahooks": "^3.7.4", |
||||
|
"ali-oss": "^6.17.1", |
||||
|
"antd": "^4.24.5", |
||||
|
"args": "^5.0.1", |
||||
|
"cron-js-parser": "^1.0.17", |
||||
|
"cron-parser": "^4.8.1", |
||||
|
"cross-env": "^7.0.3", |
||||
|
"crypto-js": "^4.1.1", |
||||
|
"echarts": "^5.4.1", |
||||
|
"echarts-for-react": "^3.0.2", |
||||
|
"file-saver": "^2.0.5", |
||||
|
"fs-attachment": "^1.0.0", |
||||
|
"fs-web-server-scaffold": "^1.0.6", |
||||
|
"jszip": "^3.10.1", |
||||
|
"koa-better-http-proxy": "^0.2.5", |
||||
|
"koa-proxy": "^1.0.0-alpha.3", |
||||
|
"koa-view": "^2.1.4", |
||||
|
"moment": "^2.22.0", |
||||
|
"npm": "^7.20.6", |
||||
|
"path": "^0.12.7", |
||||
|
"react-router-breadcrumbs-hoc": "^4.0.1", |
||||
|
"simplebar-react": "^3.2.4", |
||||
|
"superagent": "^6.1.0", |
||||
|
"uuid": "^8.3.1", |
||||
|
"webpack-dev-server": "^3.11.2", |
||||
|
"xlsx": "^0.16.9", |
||||
|
"xmlhttprequest": "^1.8.0" |
||||
|
} |
||||
|
} |
@ -0,0 +1,231 @@ |
|||||
|
创建时间:2021/08/19 |
||||
|
项目名称: |
||||
|
## 1. 文档维护: |
||||
|
|
||||
|
- 文档相关内容若有更改,请及时更新文档,以备后来者查询; |
||||
|
|
||||
|
## 2. 项目开发: |
||||
|
|
||||
|
- 请遵循此文档约定的目录结构与约定 |
||||
|
|
||||
|
```js |
||||
|
|-- .babelrc |
||||
|
|-- color.js |
||||
|
|-- config.js |
||||
|
|-- Dockerfile |
||||
|
|-- jsconfig.json |
||||
|
|-- package.json |
||||
|
|-- readme.md |
||||
|
|-- server.js |
||||
|
|-- webpack.config.js |
||||
|
|-- webpack.config.prod.js |
||||
|
|-- .vscode |
||||
|
| |-- launch.json |
||||
|
| |-- settings.json |
||||
|
|-- client |
||||
|
| |-- index.ejs |
||||
|
| |-- index.html // 当前 html 文件 |
||||
|
| |-- index.js |
||||
|
| |-- assets // 资源文件 |
||||
|
| | |-- color.less |
||||
|
| | |-- files |
||||
|
| | |-- fonticon |
||||
|
| | |-- font_sc |
||||
|
| | |-- images |
||||
|
| | |-- avatar |
||||
|
| | |-- loginUi |
||||
|
| |-- src // 项目代码 |
||||
|
| |-- app.js // 由此开始并加载模块 |
||||
|
| |-- index.js |
||||
|
| |-- components // 公用组件 |
||||
|
| | |-- index.js // 由此导出组件 |
||||
|
| | |-- Upload |
||||
|
| | |-- index.js |
||||
|
| |-- layout // 项目布局以及初始化等操作 |
||||
|
| | |-- index.js |
||||
|
| | |-- actions |
||||
|
| | | |-- global.js |
||||
|
| | |-- components |
||||
|
| | | |-- footer |
||||
|
| | | | |-- index.js |
||||
|
| | | | |-- style.css |
||||
|
| | | |-- header |
||||
|
| | | | |-- index.js |
||||
|
| | | | |-- style.css |
||||
|
| | | |-- sider |
||||
|
| | | |-- index.js |
||||
|
| | |-- containers |
||||
|
| | | |-- index.js |
||||
|
| | | |-- layout |
||||
|
| | | | |-- breadcrumb.js // 面包屑组件 |
||||
|
| | | | |-- index.js |
||||
|
| | | | |-- index.less |
||||
|
| | | |-- no-match |
||||
|
| | | |-- index.js |
||||
|
| | |-- reducers |
||||
|
| | | |-- ajaxResponse.js |
||||
|
| | | |-- global.js // 全局数据,主要包含屏幕可视宽高、所有的 action 等 |
||||
|
| | | |-- index.js |
||||
|
| | |-- store |
||||
|
| | |-- index.js |
||||
|
| | |-- store.dev.js |
||||
|
| | |-- store.prod.js |
||||
|
| |-- sections // 各功能模块 |
||||
|
| | |-- auth // 比较特别的 Auth 模块,目前 action、reducer 依然采用原始写法;包含登录、忘记密码等项目基本功能页面 |
||||
|
| | | |-- index.js |
||||
|
| | | |-- routes.js |
||||
|
| | | |-- actions |
||||
|
| | | | |-- auth.js |
||||
|
| | | | |-- index.js |
||||
|
| | | |-- components |
||||
|
| | | |-- containers |
||||
|
| | | | |-- index.js |
||||
|
| | | | |-- login.js |
||||
|
| | | |-- reducers |
||||
|
| | | | |-- auth.js |
||||
|
| | | | |-- index.js |
||||
|
| | | |-- __tests__ |
||||
|
| | |-- example // 示例模块,一般的功能模块应遵循此结构 |
||||
|
| | |-- index.js // 由此导出该模块信息,应包括一个 key 值,actions 等 |
||||
|
| | |-- nav-item.js // 用于生成菜单项,此文件内可以进行权限判断 |
||||
|
| | |-- routes.js // 路由文件 |
||||
|
| | |-- style.less // 样式文件,若样式并不是非常多,每个模块一个样式文件即可 |
||||
|
| | |-- actions |
||||
|
| | | |-- example.js // 具体的 action 操作 |
||||
|
| | | |-- index.js // 由此导出该项目的 action |
||||
|
| | |-- components // 组件 |
||||
|
| | |-- containers // 容器,此文件夹内应只包括该模块第一层级的页面 |
||||
|
| | | |-- example.js |
||||
|
| | | |-- index.js |
||||
|
| | |-- reducers // 若采用封装后的 action 写法,则 reducer 可不写 |
||||
|
| | |-- index.js |
||||
|
| |-- styles // 待初始化的主题样式 |
||||
|
| | |-- antd.less |
||||
|
| | |-- theme.less |
||||
|
| |-- themes // 初始化后的主题样式文件 |
||||
|
| | |-- dark.json |
||||
|
| | |-- light.json |
||||
|
| | |-- theme.json |
||||
|
| |-- utils // |
||||
|
| |-- authCode.js |
||||
|
| |-- func.js // 常用函数 |
||||
|
| |-- index.js |
||||
|
| |-- region.js |
||||
|
| |-- webapi.js // api 路由 |
||||
|
|-- log |
||||
|
|-- middlewares |
||||
|
| |-- proxy.js |
||||
|
| |-- webpack-dev.js |
||||
|
|-- routes |
||||
|
| |-- index.js |
||||
|
| |-- attachment |
||||
|
|-- typings |
||||
|
|-- node |
||||
|
| |-- node.d.ts |
||||
|
|-- react |
||||
|
|-- react.d.ts |
||||
|
``` |
||||
|
|
||||
|
- 封装后一般 action 写法: |
||||
|
|
||||
|
`@peace/utils 的 actionHelp 中有详细注释` |
||||
|
|
||||
|
``` js |
||||
|
'use strict'; |
||||
|
|
||||
|
import { basicAction } from '@peace/utils' |
||||
|
import { ApiTable } from '$utils' |
||||
|
|
||||
|
export function getMembers(orgId) { |
||||
|
return dispatch => basicAction({ |
||||
|
type: 'get', |
||||
|
dispatch: dispatch, |
||||
|
actionType: 'GET_MEMBERS', |
||||
|
url: `${ApiTable.getEnterprisesMembers.replace('{enterpriseId}', orgId)}`, |
||||
|
msg: { error: '获取用户列表失败' }, |
||||
|
reducer: { name: 'members' } |
||||
|
}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
1. 若 type=post,则可以使用 data 属性发送对象格式数据; |
||||
|
|
||||
|
2. reducer.name 会作为该 action 对应的 reducer 的名字,从 state 里可以解构此变量,获得该 action 异步或其他操作获得的数据; |
||||
|
|
||||
|
3. msg 可以发送 `{ option:'获取用户列表' }` ,则 actionHelp 会自动将其处理为失败和成功两种情况; |
||||
|
|
||||
|
若单独写 success 或 error 的 key,则只在成功或失败的时候进行提示; |
||||
|
|
||||
|
4. 后续可以优化:type=get 时候, |
||||
|
|
||||
|
使用 query 属性将数据传递,在 @peace/utils 的 actionHelp 中将其添加到路由后面;eg. `{ enterpriseId: orgId }` |
||||
|
|
||||
|
使用 replace 属性传递对象数据,对象数据中将被替换的值为key,替换的值为 value,然后再 actionHelp 中更改路由;eg. `{ "{enterpriseId}": orgId}` |
||||
|
|
||||
|
5. 最终取得的 reducer 中的数据格式一般为: |
||||
|
``` js |
||||
|
{ |
||||
|
data: xxx, // 接口返回的数据格式 |
||||
|
isRequesting: false, // 请求状态 |
||||
|
success: true, // 以此判断请求是否成功,不用再以 payload.type 判断 |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- actions 的引用 |
||||
|
|
||||
|
从 reducer 的 state.global.actions 里引用具体 action |
||||
|
|
||||
|
```js |
||||
|
const Example = (props) => { |
||||
|
const { dispatch, actions, user, loading } = props |
||||
|
|
||||
|
useEffect(() => { |
||||
|
dispatch(actions.example.getMembers(user.orgId)) |
||||
|
}, []) |
||||
|
|
||||
|
return ( |
||||
|
<Spin tip="biubiubiu~" spinning={loading}> |
||||
|
example |
||||
|
</Spin> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps(state) { |
||||
|
const { auth, global, members } = state; |
||||
|
return { |
||||
|
loading: members.isRequesting, |
||||
|
user: auth.user, |
||||
|
actions: global.actions, |
||||
|
members: members.data |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(Example); |
||||
|
``` |
||||
|
|
||||
|
- 一般路由配置 |
||||
|
```js |
||||
|
'use strict'; |
||||
|
import { Example, } from './containers'; |
||||
|
|
||||
|
export default [{ |
||||
|
type: 'inner', // 是否在layout 内,如果为outer,则看不到 header、footer、sider等布局,比如登陆页面 |
||||
|
route: { |
||||
|
path: '/example', |
||||
|
key: 'example', |
||||
|
breadcrumb: '栗子', |
||||
|
// 不设置 component 则面包屑禁止跳转 |
||||
|
childRoutes: [{ |
||||
|
path: '/e1', // 自路由不必复写父路由内容,会自动拼接; 则此处组件的实际路由为 /example/e1 |
||||
|
key: 'e1', |
||||
|
component: Example, |
||||
|
breadcrumb: '棒子', |
||||
|
}] |
||||
|
} |
||||
|
}]; |
||||
|
``` |
||||
|
- cross-env 的使用限制 |
||||
|
|
||||
|
cross-env 可以统一不同操作系统下环境变量的导出方式,不用再在 windows 下写 set;linux 下写 export; 可以统一以 cross-env NODE_ENV=DEV 代替; |
||||
|
|
||||
|
但是这样的话就不能在同一条运行的命令中使用 && 切割,因为会把命令切割为两个环境,则最终拿不到我们设置的变量; |
@ -0,0 +1,240 @@ |
|||||
|
'use strict'; |
||||
|
const request = require('superagent'); |
||||
|
const parse = require('async-busboy'); |
||||
|
const path = require('path') |
||||
|
const fs = require('fs'); |
||||
|
const OSS = require('ali-oss'); |
||||
|
const uuid = require('uuid'); |
||||
|
|
||||
|
const UploadPath = { |
||||
|
project: ['.txt', '.dwg', '.doc', '.docx', '.xls', '.xlsx', ".csv", '.pdf', '.pptx', '.png', '.jpg', '.svg', '.rar', '.zip', '.jpeg', '.mp4'], |
||||
|
report: ['.doc', '.docx', '.xls', '.xlsx', ".csv", '.pdf'], |
||||
|
data: ['.txt', '.xls', '.xlsx', ".csv"], |
||||
|
image: ['.png', '.jpg', '.svg'], |
||||
|
three: ['.js'], |
||||
|
video: ['.mp4'] |
||||
|
}; |
||||
|
const ext = { |
||||
|
project: ['.txt', '.dwg', '.doc', '.docx', '.xls', '.xlsx', ".csv", '.pdf', '.pptx', '.png', '.jpg', '.gif', '.svg', '.rar', '.zip', '.jpeg', '.mp4'], |
||||
|
report: [".doc", ".docx", ".xls", ".xlsx", ".pdf"], |
||||
|
data: [".txt", ".xls", ".xlsx"], |
||||
|
image: [".png", ".jpg", ".svg"], |
||||
|
three: [".js"], |
||||
|
video: [".mp4"], |
||||
|
bpmn: [".bpmn", ".bpmn20.xml", ".zip", ".bar"], |
||||
|
app: [".apk"] |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
entry: function (app, router, opts) { |
||||
|
let download_ = async function (ctx, next) { |
||||
|
const { fetchUrl } = opts.qiniu; |
||||
|
const { fetchUrl: aliFetchUrl, bucket, region } = opts.aliOss |
||||
|
if (ctx.path && ctx.path.includes(fetchUrl)) { |
||||
|
try { |
||||
|
const { filename } = ctx.request.query; |
||||
|
const fkey = decodeURI(ctx.path.slice(fetchUrl.length + 1)).replace(/\.json$/, '.js'); |
||||
|
if (ctx.path) { |
||||
|
const extNames = ctx.path.split('.'); |
||||
|
app.fs.logger.log('info', 'extNames', extNames); |
||||
|
if (extNames.length > 0) { |
||||
|
let fileType = extNames[extNames.length - 1].toLowerCase(); |
||||
|
if (fileType === 'pdf') { |
||||
|
ctx.type = 'application/pdf'; |
||||
|
app.fs.logger.log('info', 'application/pdf', fileType); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
const publicDownloadUrl = await app.fs.attachment.download(fkey); |
||||
|
ctx.status = 200; |
||||
|
if (filename) ctx.attachment(filename); |
||||
|
ctx.body = request.get(publicDownloadUrl); |
||||
|
} catch (err) { |
||||
|
ctx.fs.logger.error(err); |
||||
|
ctx.status = 404; |
||||
|
ctx.body = { error: 'file not found.' }; |
||||
|
} |
||||
|
} else if (ctx.path && ctx.path.includes(aliFetchUrl)) { |
||||
|
const { filename } = ctx.request.query; |
||||
|
const fkey = decodeURI(ctx.path.slice(aliFetchUrl.length + 1)).replace(/\.json$/, '.js'); |
||||
|
if (ctx.path) { |
||||
|
const extNames = ctx.path.split('.'); |
||||
|
app.fs.logger.log('info', 'extNames', extNames); |
||||
|
if (extNames.length > 0) { |
||||
|
let fileType = extNames[extNames.length - 1].toLowerCase(); |
||||
|
if (fileType === 'pdf') { |
||||
|
ctx.type = 'application/pdf'; |
||||
|
app.fs.logger.log('info', 'application/pdf', fileType); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
const publicDownloadUrl = `http://${bucket}.${region}.aliyuncs.com/${encodeURIComponent(fkey)}` |
||||
|
ctx.status = 200; |
||||
|
ctx.body = request.get(publicDownloadUrl); |
||||
|
} else { |
||||
|
await next(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
let upload = async function (ctx, next) { |
||||
|
try { |
||||
|
const { files } = await parse(ctx.req); |
||||
|
const file = files[0]; |
||||
|
const extname = path.extname(file.filename).toLowerCase(); |
||||
|
const fileType = ctx.query.type || "image"; |
||||
|
const fileFolder = ctx.query.fileFolder || 'common'; |
||||
|
if (ext[fileType].indexOf(extname) < 0) { |
||||
|
ctx.status = 400; |
||||
|
ctx.body = JSON.stringify({ name: 'UploadFailed', message: '文件格式无效' }); |
||||
|
return; |
||||
|
} |
||||
|
const date = new Date().toLocaleDateString(); |
||||
|
const time = new Date().getTime(); |
||||
|
let fileName = time + '_' + file.filename; |
||||
|
let saveFile = path.join(__dirname, '../../', `/client/assets/files/${fileFolder}`, fileName); |
||||
|
const pathUrl = `./client/assets/files/${fileFolder}`; |
||||
|
|
||||
|
const res1 = fs.existsSync(`./client/assets/files/${fileFolder}`); |
||||
|
!res1 && fs.mkdirSync(`./client/assets/files/${fileFolder}`); |
||||
|
const res = fs.existsSync(pathUrl); |
||||
|
!res && fs.mkdirSync(pathUrl); |
||||
|
let stream = fs.createWriteStream(saveFile); |
||||
|
fs.createReadStream(file.path).pipe(stream); |
||||
|
stream.on('error', function (err) { |
||||
|
app.fs.logger.log('error', '[Upload Heatmap]', err); |
||||
|
}); |
||||
|
ctx.status = 200; |
||||
|
ctx.body = { realName: fileName, filename: path.join(`/assets/files/${fileFolder}`, fileName), name: 'UploadSuccess', message: '上传成功' }; |
||||
|
} catch (err) { |
||||
|
ctx.status = 500; |
||||
|
ctx.fs.logger.error(err); |
||||
|
ctx.body = { err: 'upload error.' }; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let remove = async function (ctx, next) { |
||||
|
try { |
||||
|
const fkeys = ctx.request.body; |
||||
|
let removeUrl = path.join(__dirname, '../../', './client', fkeys.url); |
||||
|
const res = fs.existsSync(removeUrl); |
||||
|
if (!res) { |
||||
|
ctx.status = 400; |
||||
|
ctx.body = JSON.stringify({ name: 'DeleteFailed', message: '文件地址不存在' }); |
||||
|
return; |
||||
|
} |
||||
|
fs.unlink(removeUrl, function (error) { |
||||
|
if (error) { |
||||
|
console.log(error); |
||||
|
} |
||||
|
}) |
||||
|
ctx.status = 200; |
||||
|
ctx.body = { name: 'DeleteSuccess.', message: '删除成功' }; |
||||
|
} catch (err) { |
||||
|
ctx.status = 500; |
||||
|
ctx.fs.logger.error(err); |
||||
|
ctx.body = { err: 'upload cleanup error.' }; |
||||
|
} |
||||
|
} |
||||
|
let upload_ = async function (ctx, next) { |
||||
|
let fkey = null; |
||||
|
try { |
||||
|
const { p } = ctx.params; |
||||
|
const { files } = await parse(ctx.req); |
||||
|
const file = files[0]; |
||||
|
const extname = path.extname(file.filename).toLowerCase(); |
||||
|
if (!UploadPath[p]) { |
||||
|
ctx.status = 400; |
||||
|
ctx.body = JSON.stringify({ error: '附件存放的文件夹名称无效' }); |
||||
|
return; |
||||
|
} else if (UploadPath[p].indexOf(extname) < 0) { |
||||
|
ctx.status = 400; |
||||
|
ctx.body = JSON.stringify({ error: '文件格式无效' }); |
||||
|
return; |
||||
|
} else { |
||||
|
const fileInfo = await ctx.app.fs.attachment.upload(file, { uploadPath: p }); |
||||
|
fkey = fileInfo.key; |
||||
|
ctx.body = { uploaded: fkey }; |
||||
|
} |
||||
|
} catch (err) { |
||||
|
ctx.status = 500; |
||||
|
ctx.fs.logger.error(err); |
||||
|
ctx.body = { err: 'upload error.' }; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const uploadAliOSS = async (ctx,) => { |
||||
|
// 这个是上传到阿里
|
||||
|
try { |
||||
|
const { aliOss } = opts |
||||
|
const { p = 'default' } = ctx.params; |
||||
|
const { files } = await parse(ctx.req); |
||||
|
const file = files[0]; |
||||
|
const filename = file.filename || path.basename(file); |
||||
|
|
||||
|
const client = new OSS({ |
||||
|
// yourRegion填写Bucket所在地域.以华东1(杭州)为例,Region填写为oss-cn-hangzhou.
|
||||
|
region: aliOss.region, |
||||
|
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高.强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户.
|
||||
|
accessKeyId: aliOss.accessKey, |
||||
|
accessKeySecret: aliOss.secretKey, |
||||
|
// 填写Bucket名称,例如examplebucket.
|
||||
|
bucket: aliOss.bucket, |
||||
|
}); |
||||
|
|
||||
|
let uploadPath = path.posix.join(p, uuid.v4(), filename); |
||||
|
let result = await client.putStream( |
||||
|
uploadPath, |
||||
|
file, |
||||
|
// { contentLength: size }
|
||||
|
); |
||||
|
|
||||
|
ctx.status = 200; |
||||
|
ctx.body = { |
||||
|
key: result.name, |
||||
|
uploaded: result.name, |
||||
|
url: result.url, |
||||
|
}; |
||||
|
} catch (error) { |
||||
|
ctx.status = 400; |
||||
|
ctx.fs.logger.error(error); |
||||
|
ctx.body = { err: 'upload error.' }; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const downloadFromAli = async (ctx) => { |
||||
|
try { |
||||
|
const { aliOss } = opts |
||||
|
const { path, filename } = ctx.query |
||||
|
const client = new OSS({ |
||||
|
// yourRegion填写Bucket所在地域.以华东1(杭州)为例,Region填写为oss-cn-hangzhou.
|
||||
|
region: aliOss.region, |
||||
|
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高.强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户.
|
||||
|
accessKeyId: aliOss.accessKey, |
||||
|
accessKeySecret: aliOss.secretKey, |
||||
|
// 填写Bucket名称,例如examplebucket.
|
||||
|
bucket: aliOss.bucket, |
||||
|
}); |
||||
|
|
||||
|
const filename_ = filename || path.split('/').pop() |
||||
|
|
||||
|
const result = await client.get(path); |
||||
|
ctx.status = 200; |
||||
|
ctx.set('Content-Type', 'application/x-xls'); |
||||
|
ctx.set('Content-disposition', 'attachment; filename=' + filename_); |
||||
|
ctx.body = result.content; |
||||
|
} catch (error) { |
||||
|
ctx.status = 400; |
||||
|
ctx.fs.logger.error(error); |
||||
|
ctx.body = { err: 'download error.' }; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
router.use(download_); |
||||
|
router.post('/_upload/new', upload); |
||||
|
router.delete('/_upload/cleanup', remove); |
||||
|
router.post('/_upload/attachments/ali/:p', uploadAliOSS); |
||||
|
router.get('/_download/attachments/ali', downloadFromAli); |
||||
|
router.post('/_upload/attachments/:p', upload_); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,20 @@ |
|||||
|
/** |
||||
|
* Created by liu.xinyi |
||||
|
* on 2016/7/7. |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
const path = require('path'); |
||||
|
const fs = require('fs'); |
||||
|
|
||||
|
module.exports = { |
||||
|
entry: function (app, router, opts) { |
||||
|
fs.readdirSync(__dirname).forEach(function (dir) { |
||||
|
if(fs.lstatSync(path.join(__dirname, dir)).isDirectory()){ |
||||
|
fs.readdirSync(path.join(__dirname, dir)).forEach(function (api) { |
||||
|
require(`./${dir}/${api}`).entry(app, router, opts); |
||||
|
app.fs.logger.log('info', '[Router]', 'Inject api:', dir + '/' + path.basename(api, '.js')); |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,8 @@ |
|||||
|
'use strict'; |
||||
|
/*jslint node:true*/ |
||||
|
//from koa
|
||||
|
|
||||
|
const scaffold = require('fs-web-server-scaffold'); |
||||
|
const config = require('./config.js'); |
||||
|
|
||||
|
module.exports = scaffold(config); |
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,72 @@ |
|||||
|
var path = require('path'); |
||||
|
var webpack = require('webpack'); |
||||
|
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; |
||||
|
|
||||
|
const PATHS = { |
||||
|
app: path.join(__dirname, 'client/src'), |
||||
|
build: path.join(__dirname, 'client/build') |
||||
|
}; |
||||
|
module.exports = { |
||||
|
mode: "development", |
||||
|
devtool: 'source-map', |
||||
|
devServer: { |
||||
|
historyApiFallback: true |
||||
|
}, |
||||
|
entry: { |
||||
|
app: ["@babel/polyfill", PATHS.app] |
||||
|
}, |
||||
|
output: { |
||||
|
publicPath: '/client/build/', |
||||
|
path: PATHS.build, |
||||
|
filename: '[name].js' |
||||
|
}, |
||||
|
resolve: { |
||||
|
modules: [path.resolve(__dirname, 'client/src'), path.resolve(__dirname, 'node_modules')], |
||||
|
extensions: ['.js', '.jsx'], |
||||
|
alias: { |
||||
|
crypto: false, |
||||
|
$utils: path.resolve(__dirname, 'client/src/utils/'), |
||||
|
$components: path.resolve(__dirname, 'client/src/components/'), |
||||
|
} |
||||
|
}, |
||||
|
plugins: [ |
||||
|
// DefinePlugin 已在 middlewares 的 webpack-dev 中重新加载
|
||||
|
// new webpack.DefinePlugin({
|
||||
|
// 'process.env.NODE_ENV': JSON.stringify('development'),
|
||||
|
// }),
|
||||
|
new webpack.HotModuleReplacementPlugin(), |
||||
|
new BundleAnalyzerPlugin({ |
||||
|
analyzerPort: 8500, |
||||
|
}), |
||||
|
], |
||||
|
module: { |
||||
|
rules: [{ |
||||
|
test: /\.css$/, |
||||
|
use: ['style-loader', { |
||||
|
loader: 'css-loader', |
||||
|
options: { |
||||
|
modules: true |
||||
|
} |
||||
|
}] |
||||
|
}, |
||||
|
{ |
||||
|
test: /\.less$/, |
||||
|
use: ['style-loader', 'css-loader', { |
||||
|
loader: 'less-loader', |
||||
|
options: { |
||||
|
lessOptions: { |
||||
|
javascriptEnabled: true |
||||
|
} |
||||
|
} |
||||
|
}] |
||||
|
}, |
||||
|
{ |
||||
|
test: /\.(js|jsx)$/, |
||||
|
use: 'babel-loader', |
||||
|
include: [PATHS.app, path.resolve(__dirname, 'node_modules', '@peace')], |
||||
|
}, { |
||||
|
test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/, |
||||
|
loader: "file-loader" |
||||
|
}] |
||||
|
} |
||||
|
}; |
@ -0,0 +1,76 @@ |
|||||
|
var path = require('path'); |
||||
|
var webpack = require('webpack'); |
||||
|
var HtmlWebpackPlugin = require('html-webpack-plugin'); |
||||
|
|
||||
|
const PATHS = { |
||||
|
app: path.join(__dirname, 'client/src'), |
||||
|
build: path.join(__dirname, 'client/build') |
||||
|
}; |
||||
|
|
||||
|
module.exports = { |
||||
|
mode: "production", |
||||
|
entry: { |
||||
|
app: ["babel-polyfill", PATHS.app] |
||||
|
}, |
||||
|
output: { |
||||
|
path: PATHS.build, |
||||
|
publicPath: '/build', |
||||
|
filename: '[name].[hash:5].js' |
||||
|
}, |
||||
|
resolve: { |
||||
|
modules: [path.resolve(__dirname, 'client/src'), path.resolve(__dirname, 'node_modules')], |
||||
|
extensions: ['.js', '.jsx'], |
||||
|
alias: { |
||||
|
crypto: false, |
||||
|
$utils: path.resolve(__dirname, 'client/src/utils/'), |
||||
|
$components: path.resolve(__dirname, 'client/src/components/'), |
||||
|
} |
||||
|
}, |
||||
|
plugins: [ |
||||
|
new HtmlWebpackPlugin({ |
||||
|
filename: '../index.html', |
||||
|
template: './client/index.ejs' |
||||
|
}) |
||||
|
], |
||||
|
optimization: { |
||||
|
splitChunks: { |
||||
|
cacheGroups: { |
||||
|
commons: { |
||||
|
test: /[\\/]node_modules[\\/]/, |
||||
|
name: "vendors", |
||||
|
chunks: "all" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
module: { |
||||
|
rules: [{ |
||||
|
test: /\.css$/, |
||||
|
use: ['style-loader', { |
||||
|
loader: 'css-loader', |
||||
|
options: { |
||||
|
modules: true |
||||
|
} |
||||
|
}] |
||||
|
}, |
||||
|
{ |
||||
|
test: /\.less$/, |
||||
|
use: ['style-loader', 'css-loader', |
||||
|
{ |
||||
|
loader: 'less-loader', options: { |
||||
|
lessOptions: { |
||||
|
javascriptEnabled: true |
||||
|
} |
||||
|
} |
||||
|
}] |
||||
|
}, |
||||
|
{ |
||||
|
test: /\.jsx?$/, |
||||
|
use: 'babel-loader', |
||||
|
include: [PATHS.app, path.resolve(__dirname, 'node_modules', '@peace')], |
||||
|
},{ |
||||
|
test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/, |
||||
|
loader: "file-loader" |
||||
|
}] |
||||
|
} |
||||
|
}; |
Loading…
Reference in new issue