<template> <div class="content_box"> <FullCalendar :options="calendarOptions" class="calenderBox" ref="FullCalendar"> </FullCalendar> <!-- 新增培训计划 --> <n-modal title="新增培训计划" :mask-closable="false" preset="dialog" :show-icon="false" :style="{ width: '1800px', height: '500px' }" v-model:show="addTrainShow" > <addTrain :trainStartTime="trainStartTime" :trainEndTime="trainEndTime" @refreshTrainData="refreshTrainData"> </addTrain> </n-modal> <!-- 培训详情 --> <n-modal title="培训成果" :mask-closable="false" preset="dialog" :show-icon="false" :style="{ width: '1500px' }" v-model:show="TrainInfo" > <n-form ref="formRef" label-align="right" :label-width="100" :model="trainingData.data" label-placement="left"> <div class="tableName">基本信息</div> <div class="topTable"> <n-space> <n-form-item label="培训讲师:" path="trainTeacher"> <n-input v-model:value="trainingData.data.trainTeacher" style="width: 200px" placeholder="" /> </n-form-item> <n-form-item label="培训主题:" path="trainTopic"> <n-input v-model:value="trainingData.data.trainTopic" style="width: 200px" placeholder="" /> </n-form-item> <n-form-item label="培训地点:" path="trainAdds"> <n-input v-model:value="trainingData.data.trainAdds" style="width: 200px" placeholder="" /> </n-form-item> <n-form-item label="培训类型:" path="trainCategory"> <n-select v-model:value="trainingData.data.trainCategory" style="width: 200px" :options="trainCategoryOptions" placeholder="" clearable /> </n-form-item> </n-space> <n-space> <n-form-item label="开始时间:" path="trainStartTime"> <n-date-picker v-model:value="trainingData.data.trainStartTime" style="width: 200px" placeholder="" type="datetime" clearable /> </n-form-item> <n-form-item label="结束时间:" path="trainEndTime"> <n-date-picker v-model:value="trainingData.data.trainEndTime" style="width: 200px" placeholder="" type="datetime" clearable /> </n-form-item> <n-form-item label="培训方式:" path="trainType"> <n-select v-model:value="trainingData.data.trainType" style="width: 200px" :options="trainTypeOptions" placeholder="" clearable /> </n-form-item> <n-form-item label="培训时长:" path="trainTime"> <n-input-number v-model:value="trainingData.data.trainTime" style="width: 200px" :show-button="false" min="0" placeholder="" /> </n-form-item> </n-space> <n-form-item label="培训内容:" path="trainRemark"> <n-input v-model:value="trainingData.data.trainRemark" type="textarea" :autosize="{ minRows: 2 }" placeholder="" /> </n-form-item> </div> <div class="tableName">培训成果上报</div> <div class="bottomTable"> <n-radio-group v-model:value="trainresult.type"> <n-space> <!-- 人员签到 培训课件 培训现场照片--> <n-radio :value="item.value" v-for="item in radioArr" :key="item.value" @change="changeRadio"> {{ item.label }} </n-radio> </n-space> </n-radio-group> <n-divider /> <n-form-item v-if="trainresult.type == '0'" label="模板下载链接:"> <n-button type="warning" size="tiny" @click="downloadqd">培训签到记录模板下载</n-button> <n-upload accept=".xlsx,.xls" list-type="text" @change="handleChangeupload"> <n-button style="left: 1000px">上传文件</n-button> </n-upload> </n-form-item> <n-data-table v-if="trainresult.type == '0'" :bordered="true" :single-line="false" :max-height="700" :striped="true" :columns="Listcolumns" :data="tableListData" :remote="true" > </n-data-table> <n-upload v-if="trainresult.type == '1'" v-model:file-list="uploadFile1" accept=".ppt,.pptx,.doc,.docx,.pdf" @change="changeFileKJ" > <n-button>点击上传</n-button> </n-upload> <n-upload v-if="trainresult.type == '2'" v-model:file-list="uploadFile2" accept=".jpg,.png,.jpeg,.svg,.gif" list-type="image-card" @change="changeFileXC" > </n-upload> </div> </n-form> <template #action> <n-space style="margin-right: 650px"> <n-button type="primary" @click="UpdataTrainInfo()">保存 </n-button> <n-button @click="cancelTrainInfo()">取消</n-button> </n-space> </template> </n-modal> </div> </template> <script> import { reactive, toRefs, onMounted, ref } from 'vue'; import addTrain from './addTraining.vue'; import { getrainingInfo, updatatrainingInfo, chievementUpload, fileUpload, fileDelete } from '@/services'; import { resetForm, formatDate } from '@/utils/util'; import '@fullcalendar/core/vdom'; // solves problem with Vite import FullCalendar from '@fullcalendar/vue3'; import dayGridPlugin from '@fullcalendar/daygrid'; import timeGridPlugin from '@fullcalendar/timegrid'; import resourceTimelinePlugin from '@fullcalendar/resource-timeline'; import interactionPlugin from '@fullcalendar/interaction'; import { downloadBlob } from '@/utils/util'; import axios from 'axios'; import moment from 'moment'; export default { name: 'calendarOptions', components: { FullCalendar, addTrain, }, setup() { const allData = reactive({ calendarOptions: { // 引入的插件,比如fullcalendar/daygrid,fullcalendar/timegrid引入后才可显示月,周, plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin, resourceTimelinePlugin], droppable: true, //是否可拖拽 eventColor: '', // 全部日历日程背景色 eventTextColor: '#ffffff', //日历中事件的默认文本颜色,优先级低于添加事件时设置的文本色 initialDate: moment().format('YYYY-MM-DD'), // 自定义设置背景颜色时一定要初始化日期时间 initialView: 'dayGridMonth', // 默认为那个视图(月:dayGridMonth,周:timeGridWeek,日:timeGridDay) buttonText: { today: '今天', month: '月', week: '周', day: '日', }, locale: 'zh-cn', editable: true, //是否允许修改事件 eventStartEditable: true, //事件开始时间是否可拖动修改,优先级低于添加事件时设置的值 eventDurationEditable: true, //事件结束时间是否可拖动修改,优先级低于添加事件时设置的值 selectable: true, //是否允许用户通过单击或拖动选择日历中的对象,包括天和时间。 allDaySlot: true, //是否显示allDay headerToolbar: { // 日历头部按钮位置 left: 'title', center: 'prevYear,prev,next,nextYear', right: 'today,dayGridMonth,timeGridWeek,timeGridDay', }, slotLabelFormat: { hour: '2-digit', minute: '2-digit', meridiem: false, hour12: false, // 设置时间为24小时 }, contentHeight: 850, editable: true, // 是否可以进行(拖动、缩放)修改 eventStartEditable: true, // Event日程开始时间可以改变,默认true,如果是false其实就是指日程块不能随意拖动,只能上下拉伸改变他的endTime eventDurationEditable: true, // Event日程的开始结束时间距离是否可以改变,默认true,如果是false则表示开始结束时间范围不能拉伸,只能拖拽 selectable: true, // 是否可以选中日历格 selectMirror: true, selectMinDistance: 8, // 选中日历格的最小距离 dayMaxEvents: true, weekends: true, navLinks: true, // 天链接 slotEventOverlap: false, // 相同时间段的多个日程视觉上是否允许重叠,默认true允许 // 事件 eventDrop: EventsDrop, // 日程事件拖拽完成并且时间改变之后触发 // eventResize: this.eventResize, // 日程事件的时间范围大小被改变成功之后触发 select: addTraining, // 选中日历格事件 eventClick: TrainingInfo, //点击日历日程事件 // datesRender: this.handleDatesRender, // dayClick: this.dayClick, //日历点击事件 events: [ { id: '', title: '新员工入职培训 14:30', start: '2022-12-09', end: '2022-12-10', backgroundColor: 'blue', borderColor: 'blue', editable: true, }, ], customButtons: { prev: { // this overrides the prev button text: 'PREV', click: () => { prev(); }, }, next: { // this overrides the next button text: 'NEXT', click: () => { next(); }, }, prevYear: { // this overrides the prevYear button text: 'PREVYEAR', click: () => { prevYear(); }, }, nextYear: { // this overrides the prevYear button text: 'NEXTYEAR', click: () => { nextYear(); }, }, }, }, addTrainShow: false, TrainInfo: false, time1: null, time2: null, trainStartTime: null, //培训开始时间 trainEndTime: null, //培训结束时间 trainingData: { data: { color: '', id: null, trainTeacher: null, trainTopic: null, trainRemark: null, trainAdds: null, trainStartTime: null, trainEndTime: null, trainFiles: [], trainPics: [], trainScoreLogs: [], trainType: '', trainTime: null, trainCategory: '', }, }, trainresult: { type: '0', }, radioArr: [ { value: '0', label: '人员签到记录' }, { value: '1', label: '培训课件' }, { value: '2', label: '现场培训照片' }, ], trainCategoryOptions: [ { value: '0', label: '入职培训' }, { value: '1', label: '过程培训' }, ], trainTypeOptions: [ { value: '0', label: 'PPT' }, { value: '1', label: 'Word' }, { value: '2', label: '实操' }, ], uploadFile1: [], imageXC: [], fileKJ: [], uploadFile2: [], tableListData: [], Listcolumns: [ { title: '参会人员姓名', align: 'center', key: 'userName' }, { title: '岗位', align: 'center', key: 'userJobDesc' }, { title: '职级', align: 'center', key: 'jobLevel' }, { title: '联系方式', align: 'center', key: 'userMobile' }, { title: '评分', align: 'center', key: 'trainScore' }, { title: '评语', align: 'center', key: 'trainComment' }, ], dataAll: [], currentData: { color: '', id: null, trainTeacher: null, trainTopic: null, trainRemark: null, trainAdds: null, trainStartTime: null, trainEndTime: null, trainFiles: [], trainPics: [], trainScoreLogs: [], trainType: '', trainTime: null, trainCategory: '', }, }); const prev = () => { FullCalendar.value.getApi().prev(); const time0 = FullCalendar.value.getApi().getDate(); allData.time1 = moment(time0).startOf('month').format('YYYY-MM-DD 00:00:00'); allData.time2 = moment(time0).endOf('month').format('YYYY-MM-DD 23:59:59'); getPlan(allData.time1, allData.time2); }; const next = () => { FullCalendar.value.getApi().next(); const time0 = FullCalendar.value.getApi().getDate(); allData.time1 = moment(time0).startOf('month').format('YYYY-MM-DD 00:00:00'); allData.time2 = moment(time0).endOf('month').format('YYYY-MM-DD 23:59:59'); getPlan(allData.time1, allData.time2); }; const prevYear = () => { FullCalendar.value.getApi().prevYear(); const time0 = FullCalendar.value.getApi().getDate(); allData.time1 = moment(time0).startOf('month').format('YYYY-MM-DD 00:00:00'); allData.time2 = moment(time0).endOf('month').format('YYYY-MM-DD 23:59:59'); getPlan(allData.time1, allData.time2); }; const nextYear = () => { FullCalendar.value.getApi().nextYear(); const time0 = FullCalendar.value.getApi().getDate(); allData.time1 = moment(time0).startOf('month').format('YYYY-MM-DD 00:00:00'); allData.time2 = moment(time0).endOf('month').format('YYYY-MM-DD 23:59:59'); getPlan(allData.time1, allData.time2); }; const FullCalendar = ref(null); //获取培训信息 const getPlan = async (strtime, endtime) => { const params = { current: 1, data: { startTime: strtime, endTime: endtime, }, size: 999, }; let res = await getrainingInfo(params); if (res.code == 200) { let datas = res.data.records; allData.calendarOptions.events = []; allData.dataAll = datas; datas.forEach((item) => { const flag = item.trainFiles.length > 0 || item.trainPics.length > 0 || item.trainScoreLogs.length > 0; const obj = { id: item.id, title: flag ? item.trainTopic + '(成果已上传)' : item.trainTopic, start: item.trainStartTime !== null ? item.trainStartTime.split(' ')[0] : null, end: item.trainEndTime !== null ? item.trainEndTime.split(' ')[0] : null, backgroundColor: item.color || 'blue', borderColor: item.color || 'blue', editable: !flag, }; allData.calendarOptions.events.push(obj); }); } }; //新增培训弹窗 function addTraining(info) { allData.addTrainShow = true; allData.trainStartTime = formatDate(info.startStr, 'YYYY-MM-DD hh:mm:ss'); allData.trainEndTime = formatDate(info.endStr, 'YYYY-MM-DD hh:mm:ss'); } //保存培训新增信息 function refreshTrainData() { allData.addTrainShow = false; getPlan(allData.time1, allData.time2); } //培训详情弹窗 function TrainingInfo(e) { const id = e.event.id; allData.TrainInfo = true; allData.tableListData = []; allData.uploadFile1 = []; allData.uploadFile2 = []; allData.trainingData.data.trainPics = []; allData.trainingData.data.trainFiles = []; allData.trainingData.data.trainScoreLogs = []; allData.currentData = allData.dataAll.find((item) => item.id * 1 === id * 1); allData.trainingData.data.trainTeacher = allData.currentData.trainTeacher; allData.trainingData.data.trainRemark = allData.currentData.trainRemark; allData.trainingData.data.trainStartTime = Date.parse(allData.currentData.trainStartTime); allData.trainingData.data.trainEndTime = Date.parse(allData.currentData.trainEndTime); allData.trainingData.data.trainAdds = allData.currentData.trainAdds; allData.trainingData.data.trainTopic = allData.currentData.trainTopic; allData.trainingData.data.id = allData.currentData.id; allData.trainingData.data.color = allData.currentData.color; allData.trainingData.data.trainType = allData.currentData.trainType; allData.trainingData.data.trainTime = allData.currentData.trainTime; allData.trainingData.data.trainCategory = allData.currentData.trainCategory; allData.trainingData.data.trainScoreLogs = allData.currentData.trainScoreLogs; allData.tableListData = allData.currentData.trainScoreLogs; if (allData.currentData.trainFiles != null && allData.currentData.trainFiles.length > 0) { allData.currentData.trainFiles.forEach((item) => { let param = { fileNo: item.fileNo, name: item.fileOriginalName, status: 'finished', url: item.fileCloudStorageKey, }; allData.uploadFile1.push(param); allData.fileKJ.push(param); allData.trainingData.data.trainFiles.push(item.fileNo); }); } if (allData.currentData.trainPics != null && allData.currentData.trainPics.length > 0) { allData.currentData.trainPics.forEach((item) => { let param = { fileNo: item.fileNo, name: item.fileOriginalName, status: 'finished', url: item.fileCloudStorageKey, }; allData.uploadFile2.push(param); allData.imageXC.push(param); allData.trainingData.data.trainPics.push(item.fileNo); }); } } // 上报类型类型点击切换 function changeRadio(e) { allData.trainresult.type = e.target.value; } //保存培训成果信息 const UpdataTrainInfo = async () => { let params = { ...allData.trainingData.data }; params.trainStartTime = formatDate(allData.trainingData.data.trainStartTime, 'YYYY-MM-DD hh:mm:ss'); params.trainEndTime = formatDate(allData.trainingData.data.trainEndTime, 'YYYY-MM-DD hh:mm:ss'); let res = await updatatrainingInfo(params); if (res && res.code == 200) { $message.success('操作成功'); getPlan(allData.time1, allData.time2); allData.TrainInfo = false; allData.trainingData.data.trainPics = []; allData.trainingData.data.trainFiles = []; allData.trainingData.data.trainScoreLogs = []; } }; //取消保存培训成果 const cancelTrainInfo = () => { allData.TrainInfo = false; allData.trainingData.data.trainPics = []; allData.trainingData.data.trainFiles = []; allData.trainingData.data.trainScoreLogs = []; }; //拖拽事件获取到的开始事件和结束事件 function EventsDrop(info) { const id = info.event.id; allData.currentData = allData.dataAll.find((item) => item.id * 1 === id * 1); allData.trainingData.data.trainStartTime = Date.parse(info.event.startStr); allData.trainingData.data.trainEndTime = Date.parse(info.event.endStr); allData.trainingData.data.trainAdds = allData.currentData.trainAdds; allData.trainingData.data.trainTopic = allData.currentData.trainTopic; allData.trainingData.data.id = allData.currentData.id; allData.trainingData.data.color = allData.currentData.color; allData.trainingData.data.trainTeacher = allData.currentData.trainTeacher; allData.trainingData.data.trainRemark = allData.currentData.trainRemark; allData.trainingData.data.trainType = allData.currentData.trainType; allData.trainingData.data.trainTime = allData.currentData.trainTime; allData.trainingData.data.trainCategory = allData.currentData.trainCategory; UpdataTrainInfo(); } //上传签到记录 const handleChangeupload = async (file) => { if (file.event) { // 文件上传 let formdata = new FormData(); formdata.append('file', file.file.file); formdata.append('trainId', allData.trainingData.data.id); let config = { headers: { 'Content-Type': 'multipart/form-data' }, }; let res = await chievementUpload(formdata, config); if (res && res.code === 200) { $message.success('上传成功'); if (res.data.length > 0) { let datas = res.data; datas.forEach((item) => { allData.trainingData.data.trainScoreLogs.push(item); }); allData.tableListData = allData.trainingData.data.trainScoreLogs; } } } }; // 课件文件上传和删除 const changeFileKJ = async (file) => { $loadingBar.start(); if (file.event) { // 文件上传 let formdata = new FormData(); formdata.append('files', file.file.file); formdata.append('trainId', allData.trainingData.data.id); let config = { headers: { 'Content-Type': 'multipart/form-data' }, }; let res = await fileUpload(formdata, config); if (res && res.code === 200) { if (res.data.length > 0) { let datas = res.data[0]; allData.trainingData.data.trainFiles.push(datas.fileNo); let param = { fileNo: datas.fileNo, name: datas.fileOriginalName, url: datas.fileCloudStorageKey, status: 'success', }; allData.fileKJ.push(param); } } else { allData.uploadFile1 = []; let param = { fileNo: file.file.id, name: file.file.name, status: 'error', }; allData.uploadFile1.push(param); } $loadingBar.finish(); } else { // 文件删除,根据文件名进行匹配 let fileIndex = null; allData.fileKJ.map((item, index) => { if (file.file.name == item.name) { fileIndex = index; } }); let fileNos = []; fileNos.push(allData.fileKJ[fileIndex].fileNo); let res = await fileDelete(fileNos); if (res && res.code === 200) { allData.trainingData.data.trainFiles.splice(fileIndex, 1); } } }; // 现场图片文件上传和删除 const changeFileXC = async (file) => { $loadingBar.start(); if (file.event) { // 文件上传 let formdata = new FormData(); formdata.append('files', file.file.file); formdata.append('trainId', allData.trainingData.data.id); let config = { headers: { 'Content-Type': 'multipart/form-data' }, }; let res = await fileUpload(formdata, config); if (res && res.code === 200) { if (res.data.length > 0) { let datas = res.data[0]; allData.trainingData.data.trainPics.push(datas.fileNo); let param = { fileNo: datas.fileNo, name: datas.fileOriginalName, url: datas.fileCloudStorageKey, status: 'success', }; allData.imageXC.push(param); } } else { allData.uploadFile2 = []; let param = { fileNo: file.file.id, name: file.file.name, status: 'error', }; allData.uploadFile2.push(param); } $loadingBar.finish(); } else { // 文件删除,根据文件名进行匹配 let fileIndex = null; allData.imageXC.map((item, index) => { if (file.file.name == item.name) { fileIndex = index; } }); let fileNos = []; fileNos.push(allData.imageXC[fileIndex].fileNo); let res = await fileDelete(fileNos); if (res && res.code === 200) { allData.trainingData.data.trainPics.splice(fileIndex, 1); } } }; // 获取人员签到模板 function downloadqd() { const token = localStorage.getItem('tokenXF'); $loadingBar.start(); axios .get('/api/personAchievements/trainplanachievement/downloadPersonnelSignRecordTemplate', { headers: { token: token, }, responseType: 'blob', params: {}, }) .then(function (res) { downloadBlob(res.data, decodeURIComponent(res.headers['content-disposition'].split('filename=')[1])); }); $loadingBar.finish(); } onMounted(() => { const time0 = FullCalendar.value.getApi().currentData.currentDate; allData.time1 = moment(time0).startOf('month').format('YYYY-MM-DD 00:00:00'); allData.time2 = moment(time0).endOf('month').format('YYYY-MM-DD 23:59:59'); getPlan(allData.time1, allData.time2); }); return { ...toRefs(allData), changeRadio, FullCalendar, getPlan, UpdataTrainInfo, EventsDrop, refreshTrainData, handleChangeupload, prev, next, prevYear, downloadqd, changeFileXC, changeFileKJ, cancelTrainInfo, }; }, }; </script> <style lang="less" scoped> .tableName { border-bottom: 2px solid rgb(20, 217, 215); font-size: 16px; font-weight: bold; margin-bottom: 10px; } </style>