| | |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total" |
| | | :rowClassName="getRowClass"></PIMTable> |
| | | :rowClassName="getRowClass"> |
| | | <template #courseCode="{ row }"> |
| | | <el-button v-if="row.courseCode" |
| | | type="primary" |
| | | link |
| | | class="course-code-link" |
| | | @click.stop="openCourseDetail(row)"> |
| | | {{ row.courseCode }} |
| | | </el-button> |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #signStatus="{ row }"> |
| | | <span class="sign-summary-text">{{ formatSignSummary(row) }}</span> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | <!-- 新增/编辑知识弹窗 --> |
| | | <el-dialog v-model="dialogVisible" |
| | |
| | | <el-col :span="12"> |
| | | <el-form-item label="参加对象" |
| | | prop="participants"> |
| | | <el-input v-model="form.participants" |
| | | placeholder="请输入参加对象" /> |
| | | <el-select v-model="participantSelection" |
| | | placeholder="请选择参加对象" |
| | | filterable |
| | | multiple |
| | | collapse-tags |
| | | collapse-tags-tooltip |
| | | clearable |
| | | style="width: 100%"> |
| | | <el-option v-for="item in userOptions" |
| | | :key="item.userId" |
| | | :label="getUserLabel(item)" |
| | | :value="getUserLabel(item)" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | <el-col :span="12"> |
| | | <el-form-item label="培训讲师" |
| | | prop="trainingLecturer"> |
| | | <el-input v-model="form.trainingLecturer" |
| | | placeholder="请输入培训讲师" /> |
| | | <el-select v-model="form.trainingLecturer" |
| | | placeholder="请选择培训讲师" |
| | | filterable |
| | | clearable |
| | | style="width: 100%"> |
| | | <el-option v-for="item in userOptions" |
| | | :key="item.userId" |
| | | :label="getUserLabel(item)" |
| | | :value="getUserLabel(item)" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="课程学分" |
| | | prop="projectCredits"> |
| | | <el-input v-model="form.projectCredits" |
| | | type="number" |
| | | min="0" |
| | | placeholder="请输入课程学分" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="培训方式" |
| | | prop="trainingMode"> |
| | |
| | | <el-button type="primary" |
| | | @click="submitForm">确定</el-button> |
| | | <el-button @click="dialogVisible = false">取消</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 课程详情弹窗 --> |
| | | <el-dialog v-model="courseDetailVisible" |
| | | title="课程详情" |
| | | width="860px" |
| | | :close-on-click-modal="false"> |
| | | <div v-loading="courseDetailLoading" |
| | | class="course-detail"> |
| | | <el-descriptions border |
| | | :column="2" |
| | | class="course-detail-descriptions"> |
| | | <el-descriptions-item label="课程编号"> |
| | | {{ formatDetailValue(courseDetail.courseCode) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="状态"> |
| | | <el-tag :type="getTrainingStateTagType(courseDetail.state)"> |
| | | {{ getTrainingStateLabel(courseDetail.state) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="培训日期"> |
| | | {{ formatDetailValue(courseDetail.trainingDate) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="培训时间"> |
| | | {{ formatTrainingTimeRange(courseDetail) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="培训目标"> |
| | | {{ formatDetailValue(courseDetail.trainingObjectives) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="培训内容"> |
| | | {{ formatDetailValue(courseDetail.trainingContent) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="参加对象"> |
| | | {{ formatDetailValue(courseDetail.participants) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="培训讲师"> |
| | | {{ formatDetailValue(courseDetail.trainingLecturer) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="培训方式"> |
| | | {{ formatDetailValue(getTrainingModeLabel(courseDetail.trainingMode)) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="培训地点"> |
| | | {{ formatDetailValue(courseDetail.placeTraining) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="课时"> |
| | | {{ formatDetailValue(courseDetail.classHour) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="签到情况"> |
| | | {{ formatSignSummary(courseDetail) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="附件列表"> |
| | | <el-button type="primary" |
| | | link |
| | | :disabled="!courseDetail.id" |
| | | @click="openFileDialog(courseDetail)"> |
| | | 查看附件 |
| | | </el-button> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | <div class="classtitle course-sign-title">签到情况</div> |
| | | <div class="course-sign-summary"> |
| | | <el-tag type="success">已签到 {{ getSignedCount(courseDetail) }}</el-tag> |
| | | <el-tag type="info">未签到 {{ getUnsignedCount(courseDetail) }}</el-tag> |
| | | </div> |
| | | <el-table :data="courseSignRows" |
| | | border |
| | | fit |
| | | stripe |
| | | empty-text="暂无参加对象" |
| | | class="course-sign-table"> |
| | | <el-table-column prop="name" |
| | | label="姓名" |
| | | min-width="140" /> |
| | | <el-table-column prop="phonenumber" |
| | | label="电话号码" |
| | | min-width="140" /> |
| | | <el-table-column label="签到状态" |
| | | width="120"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.signed ? 'success' : 'info'"> |
| | | {{ scope.row.signed ? "已签到" : "未签到" }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="courseDetailVisible = false">关闭</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="课时:"> |
| | | {{ currentKnowledge.classHour }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="课程学分:"> |
| | | {{ currentKnowledge.projectCredits }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="报名人数:"> |
| | | {{ currentKnowledge.nums }} |
| | |
| | | { required: true, message: "请输入培训内容", trigger: "blur" }, |
| | | ], |
| | | trainingLecturer: [ |
| | | { required: true, message: "请输入培训讲师", trigger: "blur" }, |
| | | { required: true, message: "请选择培训讲师", trigger: "change" }, |
| | | ], |
| | | classHour: [{ required: true, message: "请输入课时", trigger: "blur" }], |
| | | }; |
| | |
| | | participants: "", // 参加对象 |
| | | trainingContent: "", // 培训内容 |
| | | trainingLecturer: "", // 培训讲师 |
| | | projectCredits: "", // 项目学分 |
| | | trainingMode: "", // 培训方式 |
| | | placeTraining: "", // 培训地点 |
| | | classHour: "", // 课时 |
| | |
| | | ); |
| | | return item ? item.label : val; |
| | | }; |
| | | const courseDetailVisible = ref(false); |
| | | const courseDetailLoading = ref(false); |
| | | const courseDetail = ref({}); |
| | | const formatDetailValue = val => |
| | | val === null || val === undefined || val === "" ? "-" : val; |
| | | const getTrainingStateLabel = val => { |
| | | const state = Number(val); |
| | | if (state === 0) return "未开始"; |
| | | if (state === 1) return "进行中"; |
| | | if (state === 2) return "已结束"; |
| | | return "-"; |
| | | }; |
| | | const getTrainingStateTagType = val => { |
| | | const state = Number(val); |
| | | if (state === 0) return "info"; |
| | | if (state === 1) return "success"; |
| | | if (state === 2) return "warning"; |
| | | return "info"; |
| | | }; |
| | | const formatTrainingTimeRange = row => { |
| | | const start = row?.openingTime || ""; |
| | | const end = row?.endTime || ""; |
| | | if (!start && !end) return "-"; |
| | | return `${start || "-"} - ${end || "-"}`; |
| | | }; |
| | | const normalizeCourseDetail = (data, row = {}) => ({ |
| | | ...row, |
| | | ...data, |
| | | nums: data?.nums ?? row?.nums, |
| | | safeTrainingDetailsDtoList: |
| | | data?.safeTrainingDetailsDtoList ?? row?.safeTrainingDetailsDtoList, |
| | | }); |
| | | const openCourseDetail = row => { |
| | | if (!row?.id) { |
| | | return; |
| | | } |
| | | courseDetail.value = normalizeCourseDetail(row); |
| | | courseDetailVisible.value = true; |
| | | courseDetailLoading.value = true; |
| | | safeTrainingGet({ id: row.id }) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | courseDetail.value = normalizeCourseDetail(res.data || {}, row); |
| | | } else { |
| | | proxy.$modal.msgError(res.msg || "查询课程详情失败"); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | proxy.$modal.msgError(err?.msg || "查询课程详情失败"); |
| | | }) |
| | | .finally(() => { |
| | | courseDetailLoading.value = false; |
| | | }); |
| | | }; |
| | | // 切换tab查询 |
| | | const tabhandleQuery = val => { |
| | | searchForm.value.state = val.paneName; |
| | |
| | | ]); |
| | | |
| | | const fileList = ref([]); |
| | | const getTrainingState = row => { |
| | | if (row?.state === null || row?.state === undefined || row?.state === "") { |
| | | return undefined; |
| | | } |
| | | return Number(row.state); |
| | | }; |
| | | const isNotStartedTraining = row => getTrainingState(row) === 0; |
| | | const isInProgressTraining = row => getTrainingState(row) === 1; |
| | | const isEndedTraining = row => getTrainingState(row) === 2; |
| | | const hasKnownTrainingState = row => [0, 1, 2].includes(getTrainingState(row)); |
| | | const signedTrainingIds = ref(new Set()); |
| | | const signingTrainingIds = ref(new Set()); |
| | | const isSignedValue = val => val === true || val === 1 || val === "1"; |
| | | const isCurrentUserSigned = row => |
| | | isSignedValue(row?.signed) || |
| | | isSignedValue(row?.currentUserSigned) || |
| | | signedTrainingIds.value.has(row?.id); |
| | | const isSigningTraining = row => signingTrainingIds.value.has(row?.id); |
| | | const setSigningTraining = (id, signing) => { |
| | | const next = new Set(signingTrainingIds.value); |
| | | if (signing) { |
| | | next.add(id); |
| | | } else { |
| | | next.delete(id); |
| | | } |
| | | signingTrainingIds.value = next; |
| | | }; |
| | | const markTrainingSigned = id => { |
| | | const next = new Set(signedTrainingIds.value); |
| | | next.add(id); |
| | | signedTrainingIds.value = next; |
| | | }; |
| | | |
| | | // 表格列配置 |
| | | const tableColumn = ref([ |
| | |
| | | label: "课程编号", |
| | | prop: "courseCode", |
| | | width: 150, |
| | | showOverflowTooltip: true, |
| | | dataType: "slot", |
| | | slot: "courseCode", |
| | | }, |
| | | { |
| | | label: "培训日期", |
| | |
| | | showOverflowTooltip: true, |
| | | }, |
| | | { |
| | | label: "项目学分", |
| | | prop: "projectCredits", |
| | | width: 120, |
| | | showOverflowTooltip: true, |
| | | }, |
| | | { |
| | | label: "培训方式", |
| | | prop: "trainingMode", |
| | | width: 120, |
| | |
| | | showOverflowTooltip: true, |
| | | }, |
| | | { |
| | | label: "报名人数", |
| | | label: "签到情况", |
| | | prop: "nums", |
| | | width: 120, |
| | | showOverflowTooltip: true, |
| | | width: 170, |
| | | dataType: "slot", |
| | | slot: "signStatus", |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "操作", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 300, |
| | | width: 220, |
| | | operation: [ |
| | | { |
| | | name: "签到", |
| | | type: "text", |
| | | disabled: row => row.state !== 1, |
| | | show: row => isInProgressTraining(row) && !isCurrentUserSigned(row), |
| | | disabled: isSigningTraining, |
| | | loading: isSigningTraining, |
| | | clickFun: row => { |
| | | signIn(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "已签到", |
| | | type: "text", |
| | | show: row => isInProgressTraining(row) && isCurrentUserSigned(row), |
| | | disabled: true, |
| | | color: "#909399", |
| | | clickFun: () => {}, |
| | | }, |
| | | { |
| | | name: "编辑", |
| | | type: "text", |
| | | disabled: row => row.state !== 0, |
| | | show: isNotStartedTraining, |
| | | clickFun: row => { |
| | | openForm("edit", row); |
| | | }, |
| | |
| | | { |
| | | name: "导出", |
| | | type: "text", |
| | | show: isEndedTraining, |
| | | clickFun: row => { |
| | | exportKnowledge(row); |
| | | }, |
| | |
| | | { |
| | | name: "附件", |
| | | type: "text", |
| | | show: hasKnownTrainingState, |
| | | clickFun: row => { |
| | | openFileDialog(row); |
| | | }, |
| | |
| | | { |
| | | name: "结果明细", |
| | | type: "text", |
| | | disabled: row => row.state == 0, |
| | | show: isEndedTraining, |
| | | clickFun: row => { |
| | | viewResultDetail(row); |
| | | }, |
| | |
| | | }, |
| | | ]); |
| | | const userList = ref([]); |
| | | const userOptions = computed(() => userList.value || []); |
| | | const getUserLabel = user => user?.nickName || user?.userName || ""; |
| | | const parseUserNames = val => { |
| | | if (Array.isArray(val)) { |
| | | return val.filter(Boolean); |
| | | } |
| | | return String(val || "") |
| | | .split(/[,,;;]/) |
| | | .map(item => item.trim()) |
| | | .filter(Boolean); |
| | | }; |
| | | const getSignedDetailList = row => |
| | | Array.isArray(row?.safeTrainingDetailsDtoList) |
| | | ? row.safeTrainingDetailsDtoList |
| | | : []; |
| | | const findUserByName = name => |
| | | userList.value.find(user => |
| | | [user?.nickName, user?.userName, getUserLabel(user)] |
| | | .filter(Boolean) |
| | | .includes(name) |
| | | ); |
| | | const isSignedDetailMatched = (detail, name) => |
| | | [detail?.nickName, detail?.userName].filter(Boolean).includes(name); |
| | | const getParticipantSignRows = row => { |
| | | const participantNames = parseUserNames(row?.participants); |
| | | const signedDetails = getSignedDetailList(row); |
| | | const usedSignedIndexes = new Set(); |
| | | const rows = participantNames.map(name => { |
| | | const signedIndex = signedDetails.findIndex( |
| | | (detail, index) => |
| | | !usedSignedIndexes.has(index) && isSignedDetailMatched(detail, name) |
| | | ); |
| | | const signedDetail = signedIndex >= 0 ? signedDetails[signedIndex] : null; |
| | | const user = findUserByName(name); |
| | | if (signedIndex >= 0) { |
| | | usedSignedIndexes.add(signedIndex); |
| | | } |
| | | return { |
| | | name, |
| | | phonenumber: signedDetail?.phonenumber || user?.phonenumber || "-", |
| | | signed: !!signedDetail, |
| | | }; |
| | | }); |
| | | signedDetails.forEach((detail, index) => { |
| | | if (usedSignedIndexes.has(index)) { |
| | | return; |
| | | } |
| | | const name = |
| | | detail?.nickName || |
| | | detail?.userName || |
| | | (detail?.userId ? `用户${detail.userId}` : ""); |
| | | if (!name) { |
| | | return; |
| | | } |
| | | rows.push({ |
| | | name, |
| | | phonenumber: detail?.phonenumber || "-", |
| | | signed: true, |
| | | }); |
| | | }); |
| | | return rows; |
| | | }; |
| | | const getSignedCount = row => { |
| | | if (Array.isArray(row?.safeTrainingDetailsDtoList)) { |
| | | return getParticipantSignRows(row).filter(item => item.signed).length; |
| | | } |
| | | return Number(row?.nums || 0); |
| | | }; |
| | | const getUnsignedCount = row => { |
| | | const participantCount = parseUserNames(row?.participants).length; |
| | | if (!participantCount) { |
| | | return 0; |
| | | } |
| | | return Math.max(participantCount - getSignedCount(row), 0); |
| | | }; |
| | | const formatSignSummary = row => |
| | | `已签到 ${getSignedCount(row)} / 未签到 ${getUnsignedCount(row)}`; |
| | | const courseSignRows = computed(() => getParticipantSignRows(courseDetail.value)); |
| | | const participantSelection = computed({ |
| | | get: () => parseUserNames(form.value.participants), |
| | | set: val => { |
| | | form.value.participants = (val || []).join(","); |
| | | }, |
| | | }); |
| | | // 生命周期 |
| | | onMounted(() => { |
| | | getCurrentFactoryName(); |
| | | getList(); |
| | | startAutoRefresh(); |
| | | userListNoPage().then(res => { |
| | | userList.value = res.data; |
| | | userList.value = Array.isArray(res.data) ? res.data : []; |
| | | }); |
| | | }); |
| | | const endform = ref({ |
| | |
| | | }; |
| | | |
| | | const signIn = row => { |
| | | if (isCurrentUserSigned(row)) { |
| | | proxy.$modal.msgWarning("已签到,请勿重复签到"); |
| | | return; |
| | | } |
| | | ElMessageBox.confirm("确认签到吗?", "提示", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | }).then(() => { |
| | | setSigningTraining(row.id, true); |
| | | safeTrainingSign({ |
| | | safeTrainingId: row.id, |
| | | userId: currentUserId.value, |
| | | }).then(res => { |
| | | if (res.code === 200) { |
| | | markTrainingSigned(row.id); |
| | | row.signed = true; |
| | | row.currentUserSigned = true; |
| | | proxy.$modal.msgSuccess("签到成功"); |
| | | getList(); |
| | | } else { |
| | | if (res.msg?.includes("已签到")) { |
| | | markTrainingSigned(row.id); |
| | | row.signed = true; |
| | | row.currentUserSigned = true; |
| | | } |
| | | proxy.$modal.msgError(res.msg || "签到失败"); |
| | | } |
| | | }).catch(err => { |
| | | proxy.$modal.msgError(err?.msg || "签到失败"); |
| | | }).finally(() => { |
| | | setSigningTraining(row.id, false); |
| | | }); |
| | | }); |
| | | }; |
| | |
| | | safeTrainingListPage({ ...page.value, ...searchForm.value }) |
| | | .then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records; |
| | | page.value.total = res.data.total; |
| | | const records = res.data?.records || []; |
| | | tableData.value = records.map(item => ({ |
| | | ...item, |
| | | currentUserSigned: |
| | | isSignedValue(item.signed) || signedTrainingIds.value.has(item.id), |
| | | })); |
| | | page.value.total = res.data?.total || 0; |
| | | }) |
| | | .catch(err => { |
| | | tableLoading.value = false; |
| | |
| | | participants: "", // 参加对象 |
| | | trainingContent: "", // 培训内容 |
| | | trainingLecturer: "", // 培训讲师 |
| | | projectCredits: "", // 项目学分 |
| | | trainingMode: "", // 培训方式 |
| | | placeTraining: "", // 培训地点 |
| | | classHour: "", // 课时 |
| | |
| | | participants: row.participants, // 参加对象 |
| | | trainingContent: row.trainingContent, // 培训内容 |
| | | trainingLecturer: row.trainingLecturer, // 培训讲师 |
| | | projectCredits: row.projectCredits, // 项目学分 |
| | | trainingMode: row.trainingMode, // 培训方式 |
| | | placeTraining: row.placeTraining, // 培训地点 |
| | | classHour: row.classHour, // 课时 |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .search_form { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | flex-wrap: wrap; |
| | | gap: 14px 18px; |
| | | min-height: 44px; |
| | | margin-bottom: 18px; |
| | | padding-bottom: 10px; |
| | | } |
| | | |
| | | .search_form > div { |
| | | display: flex; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .search_title { |
| | | margin-right: 4px; |
| | | font-weight: 600; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .table_list { |
| | | padding-top: 4px; |
| | | } |
| | | |
| | | :deep(.el-tabs__header) { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .auto-refresh-info { |
| | | margin-bottom: 15px; |
| | | } |
| | |
| | | :deep(.danger-row td) { |
| | | color: #e95a66 !important; |
| | | } |
| | | |
| | | .course-code-link { |
| | | padding: 0; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .sign-summary-text { |
| | | color: #303133; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .course-detail { |
| | | min-height: 220px; |
| | | padding: 4px 0; |
| | | } |
| | | |
| | | .course-detail-descriptions { |
| | | width: 100%; |
| | | } |
| | | |
| | | .course-sign-title { |
| | | margin-top: 22px; |
| | | } |
| | | |
| | | .course-sign-summary { |
| | | display: flex; |
| | | gap: 10px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .course-sign-table { |
| | | width: 100%; |
| | | } |
| | | |
| | | .classtitle { |
| | | font-size: 16px; |
| | | font-weight: 600; |