Browse Source

(+)暂交部门培训记录编辑、导入

master
zmh 2 years ago
parent
commit
791ad75cc8
  1. 35
      web/client/src/sections/humanAffairs/actions/departmentTrain.js
  2. 216
      web/client/src/sections/humanAffairs/containers/departmentTrain/departmentTrainRecord.jsx
  3. 247
      web/client/src/sections/humanAffairs/containers/departmentTrain/editModal.js
  4. 230
      web/client/src/sections/humanAffairs/containers/departmentTrain/importModal.js

35
web/client/src/sections/humanAffairs/actions/departmentTrain.js

@ -1,3 +1,38 @@
'use strict';
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: "修改部门培训记录" }
});
}

216
web/client/src/sections/humanAffairs/containers/departmentTrain/departmentTrainRecord.jsx

@ -1,8 +1,154 @@
import React, { useEffect, useRef, useState, useMemo } from 'react';
import { connect } from 'react-redux';
import moment from 'moment'
import { Table, Pagination, Skeleton } from '@douyinfe/semi-ui';
import EditModal from './editModal'
import ImportModal from './importModal'
import { SkeletonScreen } from "$components";
import '../../style.less'
const DepartmentTrainRecord = (props) => {
const { dispatch, actions } = props
const { dispatch, actions } = props;
const { humanAffairs } = actions;
const [limits, setLimits] = useState()//
const [query, setQuery] = useState({ limit: 10, page: 0 }); //
const [modalV, setModalV] = useState(false);
const [dataToEdit, setDataToEdit] = useState(null);
const [tableData, setTableData] = useState([]);
const [importModalV, setImportModalV] = useState(false);
const page = useRef(query.page);
useEffect(() => {
getDepartmentTrainRecordList();
}, []);
const getDepartmentTrainRecordList = (param) => {
let queryParam = param || query;
dispatch(humanAffairs.getDepartmentTrainRecord({ ...queryParam })).then(r => {
if (r.success) {
setTableData(r.payload?.data?.rows);
setLimits(r.payload?.data?.count);
}
})
}
function handleRow(record, index) {//
if (index % 2 === 0) {
return {
style: {
background: '#FAFCFF',
}
};
} else {
return {};
}
}
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
width: 60,
render: (text, record, index) => index + 1
},
{
title: '部门名称',
dataIndex: 'departmentName',
key: 'departmentName',
width: 150,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '实际培训类型',
dataIndex: 'trainingType',
key: 'trainingType',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '培训时间',
dataIndex: 'trainDate',
key: 'trainDate',
width: 160,
render: (text, record) => <span>{text ? moment(text).format("YYYY-MM-DD HH:mm:ss") : '-'}</span>
}, {
title: '培训内容',
dataIndex: 'trainContent',
key: 'trainContent',
width: 250,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '培训针对人群',
dataIndex: 'trainWho',
key: 'trainWho',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '培训讲师',
dataIndex: 'trainer',
key: 'trainer',
width: 100,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '培训方式',
dataIndex: 'trainMethod',
key: 'trainMethod',
width: 100,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '考核形式',
dataIndex: 'appraisalMethod',
key: 'appraisalMethod',
width: 100,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '培训时长',
dataIndex: 'trainTime',
key: 'trainTime',
width: 100,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '附件',
dataIndex: 'attachPath',
key: 'attachPath',
width: 150,
render: (text, record) => {
if (text) {
let str = text.split('/');
let len = str.length;
let fileName = str[len - 1]
return <div title={fileName} style={{ width: "100%", whiteSpace: "nowrap", overflowWrap: 'break-word', textOverflow: 'ellipsis', overflow: 'hidden', color: "#1890FF" }}>
<a href={'/_file-server/' + text + '?filename=' + encodeURIComponent(fileName)}>{fileName}</a></div>
}
else
return <span>-</span>
}
}, {
title: '操作',
dataIndex: 'action',
width: 90,
fixed: 'right',
render: (text, record) => {
if (record.origin === 'import')
return <div>
<span style={{ color: '#1890FF', cursor: 'pointer' }} onClick={() => onEdit(record)}>编辑</span>&nbsp;&nbsp;
{/* <Popconfirm
title='提示' content="确认删除该部门培训记录信息?" position='topLeft'
onConfirm={() => confirmDelete(record.id)} style={{ width: 330 }}
> <span style={{ color: '#1890FF', cursor: 'pointer' }}>删除</span></Popconfirm> */}
</div>
else
return <span>-</span>
}
}];
const onEdit = (data) => {
setModalV(true);
setDataToEdit(data);
}
const closeAndFetch = () => {
setModalV(false)
getDepartmentTrainRecordList();
}
const scroll = useMemo(() => ({}), []);
return (<div style={{ padding: '0px 12px' }}>
<div style={{ display: 'flex' }}>
<div style={{ color: 'rgba(0,0,0,0.45)', fontSize: 14 }}>培训</div>
@ -16,12 +162,78 @@ const DepartmentTrainRecord = (props) => {
<div style={{ display: 'flex', alignItems: 'baseline' }}>
<div style={{ width: 0, height: 20, borderLeft: '3px solid #0F7EFB', borderTop: '3px solid transparent', borderBottom: '3px solid transparent' }}></div>
<div style={{ fontFamily: "YouSheBiaoTiHei", fontSize: 24, color: '#033C9A', marginLeft: 8 }}>部门培训记录</div>
<div style={{ marginLeft: 6, fontSize: 12, color: '#969799', fontFamily: "DINExp", }}>DEPARTMENT TRAIN RECORD</div>
<div style={{ marginLeft: 6, fontSize: 12, color: '#969799', fontFamily: "DINExp", }}>DEPARTMENT TRAINING RECORD</div>
</div>
</div>
<div style={{ margin: '18px 0px' }}>
<div style={{ display: 'flex', margin: '16px 0px', justifyContent: 'space-between' }}>
<div style={{ display: 'flex' }}>
</div>
<div style={{ display: 'flex', marginRight: 20 }}>
<div style={{ padding: '6px 20px', background: '#0F7EFB', color: '#FFFFFF', fontSize: 14, cursor: "pointer" }}
onClick={() => { setImportModalV(true); }}>
导入
</div>
</div>
</div>
<div style={{ marginTop: 20 }}>
<Skeleton
loading={false}
active={true}
placeholder={SkeletonScreen()}
>
<Table
columns={columns}
dataSource={tableData}
bordered={false}
empty="暂无数据"
pagination={false}
onRow={handleRow}
scroll={scroll}
/>
</Skeleton>
<div style={{
display: "flex",
justifyContent: "space-between",
padding: "20px 20px",
}}>
<div></div>
<div style={{ display: 'flex', }}>
<span style={{ lineHeight: "30px", fontSize: 13, color: 'rgba(0,90,189,0.8)' }}>
{limits}条信息
</span>
<Pagination
total={limits}
showSizeChanger
currentPage={query.page + 1}
pageSizeOpts={[10, 20, 30, 40]}
onChange={(currentPage, pageSize) => {
setQuery({ limit: pageSize, page: currentPage - 1 });
getDepartmentTrainRecordList({ limit: pageSize, page: currentPage - 1 });
page.current = currentPage - 1;
}}
/>
</div>
</div>
</div>
</div>
</div>
{
modalV ? <EditModal
dataToEdit={dataToEdit}
close={() => closeAndFetch()}
onCancel={() => setModalV(false)} /> : ''
}
{
importModalV ? <ImportModal
onCancel={() => {
setImportModalV(false);
getDepartmentTrainRecordList();
}} /> : ''
}
</div>)
}

247
web/client/src/sections/humanAffairs/containers/departmentTrain/editModal.js

@ -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);

230
web/client/src/sections/humanAffairs/containers/departmentTrain/importModal.js

@ -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…
Cancel
Save