ww664853070 2 years ago
parent
commit
82dd445706
  1. 5
      api/app/lib/controllers/departmentTrain/index.js
  2. 105
      api/app/lib/controllers/resourceRepository/index.js
  3. 2
      api/app/lib/models/training_information.js
  4. 3
      api/app/lib/routes/resourceRepository/index.js
  5. 35
      web/client/src/sections/humanAffairs/actions/departmentTrain.js
  6. 14
      web/client/src/sections/humanAffairs/actions/resourceRepository.js
  7. 218
      web/client/src/sections/humanAffairs/containers/departmentTrain/departmentTrainRecord.jsx
  8. 247
      web/client/src/sections/humanAffairs/containers/departmentTrain/editModal.js
  9. 230
      web/client/src/sections/humanAffairs/containers/departmentTrain/importModal.js
  10. 222
      web/client/src/sections/humanAffairs/containers/resourceRepository.jsx
  11. 1
      web/client/src/utils/webapi.js

5
api/app/lib/controllers/departmentTrain/index.js

@ -1,4 +1,7 @@
'use strict'; 'use strict';
const moment = require("moment/moment");
async function get(ctx) { async function get(ctx) {
try { try {
const { limit, page } = ctx.query; const { limit, page } = ctx.query;
@ -45,7 +48,7 @@ async function modify(ctx) {
if (!existRes) { if (!existRes) {
throw '当前部门培训记录信息不存在'; throw '当前部门培训记录信息不存在';
} }
await models.DeptTraining.update({ ...rest }, { where: { id: id } }); await models.DeptTraining.update({ updateDate: moment(), ...rest }, { where: { id: id } });
ctx.status = 204; ctx.status = 204;
} catch (error) { } catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);

105
api/app/lib/controllers/resourceRepository/index.js

@ -4,16 +4,11 @@ const moment = require('moment')
async function getResourceClassify(ctx) { async function getResourceClassify(ctx) {
try { try {
const { models } = ctx.fs.dc; const { models } = ctx.fs.dc;
// const { limit, offset } = ctx.query;
let rlst = []; let rlst = [];
// const findObj = {}
// if (Number(limit) > 0 && Number(offset) >= 0) {
// findObj.limit = Number(limit);
// findObj.offset = Number(offset);
// }
rlst = [{ rlst = [{
label: "公司培训资料", value: "company", key: "公司培训资料", operation: true, children: [] label: "公司培训资料", value: "company", key: "公司培训资料", operation: true, children: []
}, { label: "部门培训资料", value: 'dept', key: '部门培训资料', operation: false, children: [] }]; }, { label: "部门培训资料", value: 'dept', key: '部门培训资料', operation: false, children: [] }];
const findObj = { order: [["departmentName", "asc"]] }
const filterData = (arrayData, arrIndex, operation) => { const filterData = (arrayData, arrIndex, operation) => {
if (arrayData.length) { if (arrayData.length) {
for (let level of arrayData) { for (let level of arrayData) {
@ -21,17 +16,18 @@ async function getResourceClassify(ctx) {
/** departmentName:2级,trainDate:3级 */ /** departmentName:2级,trainDate:3级 */
if (departmentName) { if (departmentName) {
if (trainDate) { if (trainDate) {
const trainDateLabel = 'object' == typeof trainDate ? moment(trainDate).format('YYYY-MM') : trainDate;
const depKey = rlst[arrIndex].label + '/' + departmentName const depKey = rlst[arrIndex].label + '/' + departmentName
//区分2级是否记录 //区分2级是否记录
if (rlst[arrIndex].children) { if (rlst[arrIndex].children) {
const depIndex = rlst[arrIndex].children.findIndex(r => r.key == depKey); const depIndex = rlst[arrIndex].children.findIndex(r => r.key == depKey);
if (depIndex > -1) { if (depIndex > -1) {
const dateKey = rlst[arrIndex].label + '/' + rlst[arrIndex].children[depIndex].label + '/' + moment(trainDate).format('YYYY-MM'); const dateKey = rlst[arrIndex].label + '/' + rlst[arrIndex].children[depIndex].label + '/' + trainDateLabel;
if (rlst[arrIndex].children[depIndex].children) { if (rlst[arrIndex].children[depIndex].children) {
const dateIndex = rlst[arrIndex].children[depIndex].children.findIndex(r => r.key == dateKey); const dateIndex = rlst[arrIndex].children[depIndex].children.findIndex(r => r.key == dateKey);
if (dateIndex == -1) { if (dateIndex == -1) {
rlst[arrIndex].children[depIndex].children.push({ rlst[arrIndex].children[depIndex].children.push({
label: moment(trainDate).format('YYYY-MM'), label: trainDateLabel,
value: dateKey, value: dateKey,
key: dateKey, key: dateKey,
operation operation
@ -39,7 +35,7 @@ async function getResourceClassify(ctx) {
} }
} else { } else {
rlst[arrIndex].children[depIndex].children = [{ rlst[arrIndex].children[depIndex].children = [{
label: moment(trainDate).format('YYYY-MM'), label: trainDateLabel,
value: dateKey, value: dateKey,
key: dateKey, key: dateKey,
operation operation
@ -52,9 +48,9 @@ async function getResourceClassify(ctx) {
key: depKey, key: depKey,
operation, operation,
children: [{ children: [{
label: moment(trainDate).format('YYYY-MM'), label: trainDateLabel,
value: depKey + '/' + moment(trainDate).format('YYYY-MM'), value: depKey + '/' + trainDateLabel,
key: depKey + '/' + moment(trainDate).format('YYYY-MM'), key: depKey + '/' + trainDateLabel,
operation operation
}] }]
}); });
@ -66,9 +62,9 @@ async function getResourceClassify(ctx) {
key: depKey, key: depKey,
operation, operation,
children: [{ children: [{
label: moment(trainDate).format('YYYY-MM'), label: trainDateLabel,
value: depKey + '/' + moment(trainDate).format('YYYY-MM'), value: depKey + '/' + trainDateLabel,
key: depKey + '/' + moment(trainDate).format('YYYY-MM'), key: depKey + '/' + trainDateLabel,
operation operation
}] }]
}] }]
@ -99,17 +95,16 @@ async function getResourceClassify(ctx) {
} }
} }
} }
const deptTraining = await models.DeptTraining.findAll({});
const deptTraining = await models.DeptTraining.findAll(findObj);
if (deptTraining.length) { if (deptTraining.length) {
filterData(deptTraining, 1, false); filterData(deptTraining, 1, false);
} }
const trainingInformation = await models.TrainingInformation.findAll(findObj);
const trainingInformation = await models.TrainingInformation.findAll({});
if (trainingInformation.length) { if (trainingInformation.length) {
filterData(trainingInformation, 0, true); filterData(trainingInformation, 0, true);
} }
ctx.status = 200; ctx.status = 200;
ctx.body = rlst; ctx.body = rlst;
} catch (error) { } catch (error) {
@ -119,9 +114,79 @@ async function getResourceClassify(ctx) {
message: '查询培训资源储备库分类目录失败' message: '查询培训资源储备库分类目录失败'
} }
} }
}
/**
* query
* limitoffset
* type : 文件分类必填{公司培训资料/部门培训资料}
* departmentName二级目录
* trainDate三级目录
*/
async function getResourceFileList(ctx, next) {
try {
const { models } = ctx.fs.dc;
const { limit, offset, type, departmentName, trainDate } = ctx.query;
let rlst = [];
if (["公司培训资料", "部门培训资料"].includes(type)) {
const findObj = {
attributes: ["id", "fileType", "fileName", "fileSize", "updateDate", "attachPath"],
order: [["departmentName", "desc"]]
};
if (Number(limit) > 0 && Number(offset) >= 0) {
findObj.limit = Number(limit);
findObj.offset = Number(offset);
}
if (departmentName) {
const where = {
departmentName: departmentName
}
if (trainDate) {
//todo
// where.trainDate = trainDate
}
findObj.where = where;
}
if ("公司培训资料" == type) {
rlst = await models.TrainingInformation.findAndCountAll(findObj);
} else {
rlst = await models.DeptTraining.findAndCountAll(findObj);
}
} else {
ctx.throw("培训资料范围为公司或部门");
}
ctx.status = 200;
ctx.body = rlst;
} catch (err) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = { message: err.message || '查询培训资源储备库文件列表失败' }
}
}
async function postResourceClassify(ctx) {
try {
const { models } = ctx.fs.dc;
const { departmentName, trainDate } = ctx.request.body;
if (departmentName && '' != departmentName) {
const where = {
departmentName: departmentName
};
if (trainDate && '' != trainDate) {
where.trainDate = trainDate;
}
const oldData = await models.TrainingInformation.findOne(where);
}
ctx.status = 204;
} catch (err) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = { message: err.message || '查询培训资源储备库文件列表失败' }
}
} }
module.exports = { module.exports = {
getResourceClassify getResourceClassify,
getResourceFileList,
postResourceClassify
} }

2
api/app/lib/models/training_information_record.js → api/app/lib/models/training_information.js

@ -25,7 +25,7 @@ module.exports = dc => {
autoIncrement: false autoIncrement: false
}, },
trainDate: { trainDate: {
type: DataTypes.DATE, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
comment: null, comment: null,

3
api/app/lib/routes/resourceRepository/index.js

@ -6,4 +6,7 @@ module.exports = function (app, router, opts) {
app.fs.api.logAttr['GET/train/trainFiles/resourceRepository/classify'] = { content: '查询培训资源储备库分类目录', visible: false }; app.fs.api.logAttr['GET/train/trainFiles/resourceRepository/classify'] = { content: '查询培训资源储备库分类目录', visible: false };
router.get('/train/trainFiles/resourceRepository/classify', resourceRepository.getResourceClassify); router.get('/train/trainFiles/resourceRepository/classify', resourceRepository.getResourceClassify);
app.fs.api.logAttr['GET/train/trainFiles/resourceRepository/fileList'] = { content: '查询培训资源储备库文件列表', visible: false };
router.get('/train/trainFiles/resourceRepository/fileList', resourceRepository.getResourceFileList);
}; };

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

@ -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: "修改部门培训记录" }
});
}

14
web/client/src/sections/humanAffairs/actions/resourceRepository.js

@ -1,8 +1,7 @@
'use strict'; 'use strict';
import { ApiTable, basicAction } from '$utils' import { ApiTable, basicAction } from '$utils'
export function getResourceClassify() {//查询籍贯列表 export function getResourceClassify() {
return (dispatch) => basicAction({ return (dispatch) => basicAction({
type: "get", type: "get",
dispatch: dispatch, dispatch: dispatch,
@ -13,6 +12,17 @@ export function getResourceClassify() {//查询籍贯列表
reducer: { name: "resourceClassify" }, reducer: { name: "resourceClassify" },
}); });
} }
export function getResourceFileList(query) {
return (dispatch) => basicAction({
type: "get",
dispatch: dispatch,
actionType: "GET_RESOURCE_FILELIST",
query: query,
url: `${ApiTable.getResourceFileList}`,
msg: { option: "查询培训资源储备库文件列表" },
reducer: { name: "resourceFilelist" },
});
}
export function postResourceClassify(data) { export function postResourceClassify(data) {
return (dispatch) => return (dispatch) =>

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

@ -1,8 +1,154 @@
import React, { useEffect, useRef, useState, useMemo } from 'react'; import React, { useEffect, useRef, useState, useMemo } from 'react';
import { connect } from 'react-redux'; 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 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' }}> return (<div style={{ padding: '0px 12px' }}>
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<div style={{ color: 'rgba(0,0,0,0.45)', fontSize: 14 }}>培训</div> <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={{ 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={{ 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={{ 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> </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>) </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);

222
web/client/src/sections/humanAffairs/containers/resourceRepository.jsx

@ -1,21 +1,35 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Button, Col, Row, Input, Tree, Table, Space, Tooltip } from '@douyinfe/semi-ui'; import { Button, Col, Row, Input, Tree, Table, Space, Tooltip, Spin } from '@douyinfe/semi-ui';
import { IconSearch, IconEditStroked, IconMinusCircleStroked, IconPlusCircleStroked } from '@douyinfe/semi-icons'; import { IconSearch, IconEditStroked, IconMinusCircleStroked, IconPlusCircleStroked } from '@douyinfe/semi-icons';
import { getResourceClassify } from '../actions/resourceRepository'; import { getResourceClassify, getResourceFileList } from '../actions/resourceRepository';
import '../style.less' import '../style.less';
const ResourceRepository = (props) => { const ResourceRepository = (props) => {
const { dispatch, actions, clientHeight, resourceClassify } = props; const { dispatch, clientHeight, resourceClassify, resourceFilelist, isRequesting } = props;
const [treeData, setTreeData] = useState([]); const [treeData, setTreeData] = useState([]);
const [currentSelect, setCurrentSelect] = useState();
const [defaultExpandedKey, setDefaultExpandedKey] = useState();
const ref = useRef(); const ref = useRef();
useEffect(() => { useEffect(() => {
dispatch(getResourceClassify()).then(res => { dispatch(getResourceClassify()).then(res => {
const { success, payload } = res; const { success, payload } = res;
if (success) if (success) {
let defaultKey = "";
if (payload.data.length) {
if (payload.data[0].children && payload.data[0].children.length) {
defaultKey = payload.data[0].children[0].key;
setDefaultExpandedKey([payload.data[0].key, payload.data[0].children[0].key]);
} else {
defaultKey = payload.data[0].key;
setDefaultExpandedKey([payload.data[0].key]);
}
}
setCurrentSelect([defaultKey]);
setTreeData(payload.data); setTreeData(payload.data);
getFile(defaultKey);
}
}); });
}, []) }, [])
@ -27,117 +41,25 @@ const ResourceRepository = (props) => {
const columns = [ const columns = [
{ {
title: '文件类型', title: '文件类型',
dataIndex: 'name', dataIndex: 'fileType',
}, },
{ {
title: '文件名称', title: '文件名称',
dataIndex: 'size', dataIndex: 'fileName',
}, },
{ {
title: '大小', title: '大小',
dataIndex: 'owner', dataIndex: 'fileSize',
}, },
{ {
title: '更新时间', title: '更新时间',
dataIndex: 'updateTime', dataIndex: 'updateDate',
}, { }, {
title: '操作', title: '操作',
dataIndex: 'action', dataIndex: 'action',
} }
]; ];
const data = [
// {
// key: '1',
// name: 'Semi Design 稿.fig',
// nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
// size: '2M',
// owner: '',
// updateTime: '2020-02-02 05:13',
// avatarBg: 'grey',
// },
// {
// key: '2',
// name: 'Semi Design 稿',
// nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
// size: '2M',
// owner: '',
// updateTime: '2020-01-17 05:31',
// avatarBg: 'red',
// },
// {
// key: '3',
// name: '',
// nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
// size: '34KB',
// owner: 'Zoey Edwards',
// updateTime: '2020-01-26 11:01',
// avatarBg: 'light-blue',
// },
{
key: '4',
name: 'Semi Design 设计稿.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '5',
name: 'Semi Design 分享演示文稿',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '6',
name: '设计文档',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '7',
name: 'Semi Design 设计稿.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '8',
name: 'Semi Design 分享演示文稿',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '9',
name: '设计文档',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '10',
name: '设计文档',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
}
];
const renderLabel = (label, data) => (<div> const renderLabel = (label, data) => (<div>
<Tooltip content={label}><span >{label.length > 8 ? label.substring(0, 8) + '...' : label}</span></Tooltip> <Tooltip content={label}><span >{label.length > 8 ? label.substring(0, 8) + '...' : label}</span></Tooltip>
{true == data.operation ? {true == data.operation ?
@ -148,7 +70,18 @@ const ResourceRepository = (props) => {
</span> </span>
: ''} : ''}
</div> </div>
) );
const handleSelect = (e) => {
setCurrentSelect(e);
getFile(e);
}
const getFile = (e) => {
const arr = e.split("/");
const query = { type: arr[0] };
if (arr[1]) { query.departmentName = arr[1]; }
if (arr[2]) { query.trainDate = arr[2]; }
dispatch(getResourceFileList(query));
}
return ( return (
<> <>
@ -168,40 +101,49 @@ const ResourceRepository = (props) => {
<div style={{ marginLeft: 6, fontSize: 12, color: '#969799', fontFamily: "DINExp", }}>RESOURCE REPOSITORY</div> <div style={{ marginLeft: 6, fontSize: 12, color: '#969799', fontFamily: "DINExp", }}>RESOURCE REPOSITORY</div>
</div> </div>
</div> </div>
<Row className='resourceRepository'> <Spin spinning={isRequesting}>
{/* 左侧 */} <Row className='resourceRepository'>
<Col className='left' span={6}> {/* 左侧 */}
<Button theme='solid' type='primary' >新建文件夹</Button> <Col className='left' span={6}>
<br /> <Button theme='solid' type='primary' >新建文件夹</Button>
<Input suffix={<IconSearch />} showClear onChange={v => ref.current.search(v)} placeholder="请输入"></Input> <br />
<Tree <Input suffix={<IconSearch />} showClear onChange={v => ref.current.search(v)} placeholder="请输入"></Input>
ref={ref} <Tree
treeData={treeData} ref={ref}
defaultExpandAll defaultExpandAll={true}
filterTreeNode treeData={treeData}
searchRender={false} style={style}
blockNode={false} value={currentSelect}
directory expandedKeys={defaultExpandedKey}
style={style} filterTreeNode={true}
renderLabel={renderLabel} searchRender={false}
/> directory={true}
</Col> showFilteredOnly={true}
{/* 右侧内容 */} renderLabel={renderLabel}
<Col span={18} className='right'> onExpand={(e) => setDefaultExpandedKey(e)}
<Row type="flex" justify="space-around" align="middle"> onSelect={handleSelect}
<Col span={18}> />
<Space> </Col>
<Button theme='solid' type='primary' >上传文件</Button> {/* 右侧内容 */}
<span className="path-lable"><strong>当前文件夹公司培训资源/人力资源部2/2022-11</strong></span> <Col span={18} className='right'>
</Space> <Row type="flex" justify="space-around" align="middle">
</Col> <Col span={18}>
<Col span={6} > <Space>
<Input suffix={<IconSearch />} showClear placeholder="文件名称、更新时间"></Input> <Button theme='solid' type='primary' >上传文件</Button>
</Col> <span className="path-lable"><strong>当前文件夹{currentSelect}</strong></span>
</Row> </Space>
<Table columns={columns} dataSource={data} pagination={false} /> </Col>
</Col> <Col span={6} >
</Row> <Input suffix={<IconSearch />} showClear placeholder="文件名称、更新时间"></Input>
</Col>
</Row>
<Table
columns={columns}
dataSource={resourceFilelist && resourceFilelist.rows || []}
pagination={false} />
</Col>
</Row>
</Spin>
</div> </div>
</div> </div>
</> </>
@ -209,12 +151,14 @@ const ResourceRepository = (props) => {
} }
function mapStateToProps(state) { function mapStateToProps(state) {
const { auth, global, resourceClassify } = state; const { auth, global, resourceClassify, resourceFilelist } = state;
return { return {
user: auth.user, user: auth.user,
actions: global.actions, actions: global.actions,
clientHeight: global.clientHeight, clientHeight: global.clientHeight,
resourceClassify: resourceClassify.data || [] resourceClassify: resourceClassify.data || [],
resourceFilelist: resourceFilelist.data && resourceFilelist.data,
isRequesting: resourceClassify.isRequesting || resourceFilelist.isRequesting
}; };
} }

1
web/client/src/utils/webapi.js

@ -53,6 +53,7 @@ export const ApiTable = {
postResourceClassify: 'train/trainFiles/resourceRepository/classify', postResourceClassify: 'train/trainFiles/resourceRepository/classify',
putResourceClassify: 'train/trainFiles/resourceRepository/classify/{id}', putResourceClassify: 'train/trainFiles/resourceRepository/classify/{id}',
delResourceClassify: 'train/trainFiles/resourceRepository/classify/{id}', delResourceClassify: 'train/trainFiles/resourceRepository/classify/{id}',
getResourceFileList: 'train/trainFiles/resourceRepository/fileList',
}; };
export const RouteTable = { export const RouteTable = {

Loading…
Cancel
Save