zmh
2 years ago
4 changed files with 728 additions and 4 deletions
@ -1,3 +1,38 @@ |
|||||
'use strict'; |
'use strict'; |
||||
|
|
||||
import { ApiTable, basicAction } from '$utils' |
import { ApiTable, basicAction } from '$utils' |
||||
|
|
||||
|
export function importDepartmentTrainRecord(data) { |
||||
|
return dispatch => basicAction({ |
||||
|
type: 'post', |
||||
|
dispatch: dispatch, |
||||
|
actionType: 'IMPORT_DEPARTMENT_TRAIN_RECORD', |
||||
|
url: ApiTable.importDepartmentTrainRecord, |
||||
|
data: data, |
||||
|
msg: { option: '导入部门培训记录' }, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export function getDepartmentTrainRecord(query) {//查询
|
||||
|
return (dispatch) => basicAction({ |
||||
|
type: "get", |
||||
|
dispatch: dispatch, |
||||
|
actionType: "GET_DEPARTMENT_TRAIN_RECORD_LIST", |
||||
|
query: query, |
||||
|
url: `${ApiTable.getDepartmentTrainRecord}`, |
||||
|
msg: { option: "查询部门培训记录列表" }, |
||||
|
reducer: { name: "departmentTrainRecord", params: { noClear: true } }, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export function modifyDepartmentTrainRecord(data) { |
||||
|
return (dispatch) => |
||||
|
basicAction({ |
||||
|
type: "put", |
||||
|
dispatch: dispatch, |
||||
|
data, |
||||
|
actionType: "PUT_DEPARTMENT_TRAIN_RECORD", |
||||
|
url: `${ApiTable.modifyDepartmentTrainRecord}`, |
||||
|
msg: { option: "修改部门培训记录" } |
||||
|
}); |
||||
|
} |
@ -0,0 +1,247 @@ |
|||||
|
import React, { useEffect, useRef, useState } from 'react'; |
||||
|
import { connect } from "react-redux"; |
||||
|
import { Select, Modal, Form, Button, Toast } from "@douyinfe/semi-ui"; |
||||
|
import { IconUpload } from '@douyinfe/semi-icons'; |
||||
|
const EditModal = (props) => { |
||||
|
const { dispatch, actions, user, onCancel, dataToEdit, apiRoot, close } = props; |
||||
|
const { humanAffairs } = actions; |
||||
|
const form = useRef();//表单
|
||||
|
const [options, setOptions] = useState([]); |
||||
|
const [attachPath, setAttachPath] = useState(); |
||||
|
const [attachName, setAttachName] = useState(); |
||||
|
const [fileName, setFileName] = useState(); |
||||
|
const [fileSize, setFileSize] = useState(); |
||||
|
const [fileType, setFileType] = useState(); |
||||
|
const [validateStatus, setValidateStatus] = useState('default'); |
||||
|
//初始化
|
||||
|
useEffect(() => { |
||||
|
let optionData = []; |
||||
|
if (user.allDepartment && user.allDepartment.departments) { |
||||
|
optionData = user.allDepartment.departments.filter(d => !d.delete).map(d => <Select.Option value={d.name} key={d.id}> |
||||
|
{d.name} |
||||
|
</Select.Option>) |
||||
|
setOptions(optionData); |
||||
|
} |
||||
|
if (dataToEdit?.attachPath) { |
||||
|
setAttachPath(dataToEdit?.attachPath); |
||||
|
let str = dataToEdit.attachPath.split('/'); |
||||
|
let len = str.length; |
||||
|
setAttachName(str[len - 1]); |
||||
|
|
||||
|
setFileName(dataToEdit?.fileName); |
||||
|
setFileSize(dataToEdit?.fileSize); |
||||
|
setFileType(dataToEdit?.fileType); |
||||
|
} |
||||
|
}, []); |
||||
|
|
||||
|
const handleOk = () => { |
||||
|
form.current.validate().then((values) => { |
||||
|
if (dataToEdit) { |
||||
|
values.attachPath = attachPath; |
||||
|
if (!values.trainWho) { |
||||
|
values.trainWho = null; |
||||
|
} |
||||
|
if (!values.trainer) { |
||||
|
values.trainer = null; |
||||
|
} |
||||
|
dispatch(humanAffairs.modifyDepartmentTrainRecord({ id: dataToEdit.id, fileName, fileSize, fileType, ...values })).then((res) => { |
||||
|
if (res.success) { |
||||
|
close(); |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
const uploadOnChange = (fileList, currentFile, event) => { |
||||
|
if (fileList.currentFile.status == "success") { |
||||
|
setAttachPath(fileList.currentFile?.response?.key); |
||||
|
let index = fileList.currentFile.name.lastIndexOf('.'); |
||||
|
setFileName(fileList.currentFile.name.substring(0, index)); |
||||
|
setFileType(fileList.currentFile.name.substring(index)); |
||||
|
setFileSize(fileList.currentFile.size); |
||||
|
} |
||||
|
if (fileList.fileList.length == 0) { |
||||
|
setAttachPath(null); |
||||
|
setFileName(null); |
||||
|
setFileType(null); |
||||
|
setFileSize(null); |
||||
|
} |
||||
|
} |
||||
|
const validate = (val, values) => { |
||||
|
if (!val) { |
||||
|
setValidateStatus('error'); |
||||
|
return <span>不能为空</span>; |
||||
|
} else if (val && val.trim() === '') { |
||||
|
setValidateStatus('error'); |
||||
|
return <span>不能为空格</span>; |
||||
|
} else if (val && val.length > 20) { |
||||
|
setValidateStatus('error'); |
||||
|
return <span>最多20个字符</span>; |
||||
|
} else { |
||||
|
setValidateStatus('success'); |
||||
|
return ''; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const validateContent = (val, values) => { |
||||
|
if (!val) { |
||||
|
setValidateStatus('error'); |
||||
|
return <span>不能为空</span>; |
||||
|
} else if (val && val.trim() === '') { |
||||
|
setValidateStatus('error'); |
||||
|
return <span>不能为空格</span>; |
||||
|
} else if (val && val.length > 200) { |
||||
|
setValidateStatus('error'); |
||||
|
return <span>最多200个字符</span>; |
||||
|
} else { |
||||
|
setValidateStatus('success'); |
||||
|
return ''; |
||||
|
} |
||||
|
}; |
||||
|
return ( |
||||
|
<Modal title={dataToEdit?.id ? '修改部门培训' : '新增部门培训'} |
||||
|
visible={true} |
||||
|
destroyOnClose |
||||
|
okText='保存' width={600} |
||||
|
onOk={handleOk} |
||||
|
onCancel={onCancel}> |
||||
|
<Form getFormApi={(formApi) => (form.current = formApi)} |
||||
|
labelPosition={'left'} labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> |
||||
|
<Form.Select |
||||
|
initValue={dataToEdit?.departmentName || null} |
||||
|
label="部门名称" |
||||
|
field='departmentName' |
||||
|
showClear |
||||
|
rules={[{ required: true, message: '请选择部门名称' }]} |
||||
|
placeholder='请选择部门名称' |
||||
|
filter |
||||
|
style={{ width: '100%' }} |
||||
|
disabled={dataToEdit?.id ? true : false} |
||||
|
> |
||||
|
{options} |
||||
|
</Form.Select> |
||||
|
<Form.Input |
||||
|
field="trainingType" |
||||
|
label="培训类型" |
||||
|
style={{ width: '100%' }} |
||||
|
initValue={dataToEdit?.trainingType || ""} |
||||
|
placeholder="请输入培训类型" |
||||
|
showClear |
||||
|
validate={validate} |
||||
|
validateStatus={validateStatus} |
||||
|
/> |
||||
|
<Form.DatePicker |
||||
|
field='trainDate' |
||||
|
type='dateTime' |
||||
|
label="培训时间" |
||||
|
initValue={dataToEdit?.trainDate || new Date()} |
||||
|
placeholder="请选择培训时间" |
||||
|
style={{ width: '100%' }} |
||||
|
rules={[{ required: true, message: "培训时间不可空" }]} |
||||
|
disabled={dataToEdit?.id ? true : false} |
||||
|
/> |
||||
|
<Form.Input |
||||
|
field="trainContent" |
||||
|
label="培训内容" |
||||
|
style={{ width: '100%' }} |
||||
|
initValue={dataToEdit?.trainContent || ""} |
||||
|
placeholder="请输入培训内容" |
||||
|
showClear |
||||
|
|
||||
|
validate={validateContent} |
||||
|
validateStatus={validateStatus} |
||||
|
// rules={[{
|
||||
|
// required: true, message: "培训内容不可空"
|
||||
|
// }, {
|
||||
|
// max: 200, message: "最多200个字符"
|
||||
|
// }]}
|
||||
|
/> |
||||
|
<Form.Input |
||||
|
field="trainWho" |
||||
|
label='针对人群' |
||||
|
style={{ width: '100%' }} |
||||
|
initValue={dataToEdit?.trainWho || ""} |
||||
|
placeholder="请输入培训针对人群" |
||||
|
showClear |
||||
|
/> |
||||
|
<Form.Input |
||||
|
field="trainer" |
||||
|
label='培训讲师' |
||||
|
style={{ width: '100%' }} |
||||
|
initValue={dataToEdit?.trainer || ""} |
||||
|
placeholder="请输入培训讲师" |
||||
|
showClear |
||||
|
/> |
||||
|
<Form.Input |
||||
|
field="trainMethod" |
||||
|
label='培训方式' |
||||
|
style={{ width: '100%' }} |
||||
|
initValue={dataToEdit?.trainMethod || ""} |
||||
|
placeholder="请输入培训方式" |
||||
|
showClear |
||||
|
validate={validate} |
||||
|
validateStatus={validateStatus} |
||||
|
/> |
||||
|
<Form.Input |
||||
|
field="appraisalMethod" |
||||
|
label='考核形式' |
||||
|
style={{ width: '100%' }} |
||||
|
initValue={dataToEdit?.appraisalMethod || ""} |
||||
|
placeholder="请输入考核形式" |
||||
|
showClear |
||||
|
validate={validate} |
||||
|
validateStatus={validateStatus} |
||||
|
// rules={[{
|
||||
|
// required: true, message: "考核形式不可空"
|
||||
|
// }, {
|
||||
|
// max: 20, message: "最多20个字符"
|
||||
|
// }]}
|
||||
|
/> |
||||
|
<Form.Input |
||||
|
field="trainTime" |
||||
|
label='培训时长' |
||||
|
style={{ width: '100%' }} |
||||
|
initValue={dataToEdit?.trainTime || ""} |
||||
|
placeholder="请输入培训时长" |
||||
|
showClear |
||||
|
validate={validate} |
||||
|
validateStatus={validateStatus} |
||||
|
/> |
||||
|
<Form.Upload |
||||
|
field='attachPath' |
||||
|
label='附件' |
||||
|
action={apiRoot + '/attachments/departmentTraining?token=' + user.token} |
||||
|
initValue={ |
||||
|
dataToEdit?.attachPath ? [ |
||||
|
{ |
||||
|
uid: '1', |
||||
|
name: attachName, |
||||
|
status: 'success', |
||||
|
preview: false, |
||||
|
url: '/_file-server/' + dataToEdit.attachPath, |
||||
|
} |
||||
|
] : [] |
||||
|
} |
||||
|
maxSize={204800} limit={1} accept='.zip,.rar,.7z' |
||||
|
onSizeError={(file, fileList) => Toast.error(`${file.name} 超过200M`)} |
||||
|
onChange={uploadOnChange} |
||||
|
> |
||||
|
<Button icon={<IconUpload />} theme="light"> |
||||
|
上传 zip, rar, 7z |
||||
|
</Button> |
||||
|
</Form.Upload> |
||||
|
</Form> |
||||
|
</Modal > |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps(state) { |
||||
|
const { auth, global } = state; |
||||
|
return { |
||||
|
user: auth.user, |
||||
|
actions: global.actions, |
||||
|
apiRoot: global.apiRoot |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(EditModal); |
@ -0,0 +1,230 @@ |
|||||
|
'use strict'; |
||||
|
import React, { useState, useEffect } from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { Modal, Form, Button, Notification } from '@douyinfe/semi-ui'; |
||||
|
import { IconUpload } from '@douyinfe/semi-icons'; |
||||
|
import XLSX from 'xlsx'; |
||||
|
import moment from 'moment'; |
||||
|
|
||||
|
//下载模板和上传文件读取
|
||||
|
const ImportModal = props => { |
||||
|
const { dispatch, actions, user, onCancel } = props |
||||
|
const { humanAffairs } = actions; |
||||
|
const [msg, setMsg] = useState('') |
||||
|
const [loading, setLoading] = useState('') |
||||
|
const [postData, setPostData] = useState([]) |
||||
|
//初始化
|
||||
|
useEffect(() => { |
||||
|
|
||||
|
}, []); |
||||
|
|
||||
|
const confirm = () => { |
||||
|
if (postData.length) { |
||||
|
setLoading(true) |
||||
|
dispatch(humanAffairs.importDepartmentTrainRecord(postData)).then(res => { |
||||
|
if (res.success) { |
||||
|
onCancel() |
||||
|
} |
||||
|
setLoading(false) |
||||
|
}) |
||||
|
} else { |
||||
|
Notification.warning({ content: '没有数据可以提交,请上传数据文件', duration: 2 }) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const download = () => { |
||||
|
const head = [["部门", "实际培训类型", "培训时间", "培训内容", "培训针对人群", "培训讲师", "培训方式", "考核形式", "培训时长"]]; |
||||
|
let sheetName = '部门培训台账'; |
||||
|
let workbook = { SheetNames: [sheetName], Sheets: {} }; |
||||
|
workbook.Sheets[sheetName] = XLSX.utils.aoa_to_sheet(head);//json转excel
|
||||
|
workbook.Sheets[sheetName]['!cols'] = [ |
||||
|
{ wch: 12 }, { wch: 12 }, { wch: 15 }, { wch: 20 }, { wch: 12 }, |
||||
|
{ wch: 10 }, { wch: 10 }, { wch: 10 }, { wch: 10 }]; |
||||
|
let wopts = { bookType: 'xlsx', type: 'buffer' };// 生成excel的配置项
|
||||
|
XLSX.writeFile(workbook, '部门培训记录导入模板.xlsx', wopts); |
||||
|
} |
||||
|
const error = (msg) => { |
||||
|
setMsg(msg) |
||||
|
onError({ message: msg }) |
||||
|
} |
||||
|
|
||||
|
const getFileBlob = (url) => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
let request = new XMLHttpRequest() |
||||
|
request.open("GET", url, true) |
||||
|
request.responseType = "blob" |
||||
|
request.onreadystatechange = e => { |
||||
|
if (request.readyState == 4) { |
||||
|
if (request.status == 200) { |
||||
|
if (window.FileReader) { |
||||
|
let reader = new FileReader(); |
||||
|
reader.readAsBinaryString(request.response); |
||||
|
reader.onload = event => { |
||||
|
try { |
||||
|
const { result } = event.target; |
||||
|
// 以二进制流方式读取得到整份excel表格对象
|
||||
|
const workbook = XLSX.read(result, { |
||||
|
type: "binary", |
||||
|
cellDates: true,//设为true,将天数的时间戳转为时间格式
|
||||
|
codepage: 936//解决了乱码问题
|
||||
|
}); |
||||
|
let data = []; // 存储获取到的数据
|
||||
|
// 遍历每张工作表进行读取(这里默认只读取第一张表)
|
||||
|
for (const sheet in workbook.Sheets) { |
||||
|
if (workbook.Sheets.hasOwnProperty(sheet)) { |
||||
|
data = data.concat(XLSX.utils.sheet_to_json(workbook.Sheets[sheet])); |
||||
|
} |
||||
|
} |
||||
|
resolve(data);//导出数据
|
||||
|
} catch (e) { |
||||
|
reject("失败"); |
||||
|
error(`文件解析失败,请检查编码格式`) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
request.send(); |
||||
|
}) |
||||
|
} |
||||
|
const judgeNull = (value) => { |
||||
|
return value ? String(value).trim().replace(/\s*/g, "") : null; |
||||
|
} |
||||
|
|
||||
|
const changeTime = (v) => { |
||||
|
//注意的点:xlsx将excel中的时间内容解析后,会小一天
|
||||
|
//如2020/8/1,xlsx会解析成 Fri Jul 31 2020 23:59:17 GMT+0800 小了43秒
|
||||
|
let a = new Date(v); |
||||
|
a.setTime(a.getTime() + 43 * 1000); |
||||
|
return a; |
||||
|
} |
||||
|
return ( |
||||
|
<Modal |
||||
|
title="导入" visible={true} |
||||
|
onOk={confirm} width={620} |
||||
|
confirmLoading={loading} |
||||
|
onCancel={() => { |
||||
|
setMsg('') |
||||
|
setLoading(false) |
||||
|
setPostData([]) |
||||
|
onCancel() |
||||
|
}} |
||||
|
> |
||||
|
<div style={{ borderBottom: '1px solid #DCDEE0', margin: '0px -24px' }}></div> |
||||
|
<Form> |
||||
|
<Form.Upload |
||||
|
label={'部门培训记录'} labelPosition='left' |
||||
|
action={'/'} accept={'.xlsx'} |
||||
|
maxSize={102400} limit={1} |
||||
|
onRemove={(currentFile, fileList, fileItem) => { |
||||
|
setMsg(''); |
||||
|
setPostData([]); |
||||
|
}} |
||||
|
customRequest={(data) => { |
||||
|
const { file, onSuccess, onError } = data |
||||
|
getFileBlob(file.url).then(res => { |
||||
|
if (res.length > 1000) { |
||||
|
error('一次性上传数据行数应小于1000行,请分批上传') |
||||
|
return |
||||
|
} |
||||
|
if (!res.length) { |
||||
|
error('请填写至少一行数据') |
||||
|
return |
||||
|
} |
||||
|
let postData = []; |
||||
|
let ymdhms = /([0-9]{3}[1-9]|[0-9][1-9][0-9]2022-01-07 19:29:33|[0-9]2022-01-07 19:29:33[1-9][0-9]|[1-9][0-9]{3})-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8])))([ ])([0-1]?[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])/; |
||||
|
for (let i = 0; i < res.length; i++) { |
||||
|
let d = res[i]; |
||||
|
let departmentName = judgeNull(d['部门']); |
||||
|
let trainingType = judgeNull(d['实际培训类型']); |
||||
|
let trainDate = d['培训时间']; |
||||
|
let trainContent = judgeNull(d['培训内容']); |
||||
|
let trainWho = judgeNull(d['培训针对人群']); |
||||
|
let trainer = judgeNull(d['培训讲师']); |
||||
|
let trainMethod = judgeNull(d['培训方式']); |
||||
|
let appraisalMethod = judgeNull(d['考核形式']); |
||||
|
let trainTime = judgeNull(d['培训时长']); |
||||
|
if (!departmentName) { |
||||
|
error(`第${i + 2}行【部门】为空,请填写`) |
||||
|
return |
||||
|
} |
||||
|
if (user.allDepartment && user.allDepartment.departments) { |
||||
|
let dept = user.allDepartment.departments.find(d => !d.delete && d.name === departmentName); |
||||
|
if (dept) { |
||||
|
departmentName = dept.name; |
||||
|
} else { |
||||
|
error(`第${i + 2}行【部门】数据有误,请确认后重新填写`) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
if (!trainingType) { |
||||
|
error(`第${i + 2}行【实际培训类型】为空,请填写`) |
||||
|
return |
||||
|
} |
||||
|
if (!trainDate) { |
||||
|
error(`第${i + 2}行【培训时间】为空,请填写`) |
||||
|
return |
||||
|
} |
||||
|
if (trainDate instanceof Date) { |
||||
|
trainDate = moment(trainDate).format("YYYY-MM-DD HH:mm:ss"); |
||||
|
if (!ymdhms.test(trainDate)) { |
||||
|
error(`第${i + 2}行【培训时间】填写错误,请填写yyyy/mm/dd hh:mm格式`) |
||||
|
return |
||||
|
} |
||||
|
} else { |
||||
|
error(`第${i + 2}行【培训时间】填写错误,请确认后重新填写`) |
||||
|
return |
||||
|
} |
||||
|
trainDate = changeTime(trainDate); |
||||
|
|
||||
|
if (!trainContent) { |
||||
|
error(`第${i + 2}行【培训内容】为空,请填写`) |
||||
|
return |
||||
|
} |
||||
|
if (!trainMethod) { |
||||
|
error(`第${i + 2}行【培训方式】为空,请填写`) |
||||
|
return |
||||
|
} |
||||
|
if (!appraisalMethod) { |
||||
|
error(`第${i + 2}行【考核形式】为空,请填写`) |
||||
|
return |
||||
|
} |
||||
|
if (!trainTime) { |
||||
|
error(`第${i + 2}行【培训时长】为空,请填写`) |
||||
|
return |
||||
|
} |
||||
|
postData.push({ |
||||
|
departmentName, trainingType, trainDate, trainContent, |
||||
|
trainWho: trainWho || '', trainer: trainer || '', |
||||
|
trainMethod, appraisalMethod, trainTime, origin: 'import' |
||||
|
}) |
||||
|
} |
||||
|
setPostData(postData) |
||||
|
let msg = '文件解析完成,点击确定按钮上传保存!' |
||||
|
setMsg(msg) |
||||
|
onSuccess({ message: msg }) |
||||
|
}) |
||||
|
}}> |
||||
|
<Button icon={<IconUpload />} theme="light"> |
||||
|
请选择文件 |
||||
|
</Button> |
||||
|
</Form.Upload> |
||||
|
<span>{msg}</span> |
||||
|
<div style={{ color: '#ccc' }}>最大不超过200M,导入文件需与 |
||||
|
<span onClick={() => download()} style={{ cursor: 'pointer', color: '#0066FF' }}>导入模板</span> |
||||
|
一致</div> |
||||
|
</Form> |
||||
|
</Modal > |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps(state) { |
||||
|
const { auth, global } = state; |
||||
|
return { |
||||
|
user: auth.user, |
||||
|
actions: global.actions, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(ImportModal); |
Loading…
Reference in new issue