gongchunyi
2026-07-30 5d132317d0ded846812d5009a560be2fd5381478
fix: 安全培训考核签到修改
已修改1个文件
407 ■■■■■ 文件已修改
src/views/safeProduction/safetyTrainingAssessment/index.vue 407 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safetyTrainingAssessment/index.vue
@@ -46,7 +46,21 @@
                :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"
@@ -115,8 +129,19 @@
          <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>
@@ -131,21 +156,20 @@
          <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">
@@ -184,6 +208,96 @@
          <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>
@@ -229,9 +343,6 @@
          </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 }}
@@ -408,7 +519,7 @@
      { required: true, message: "请输入培训内容", trigger: "blur" },
    ],
    trainingLecturer: [
      { required: true, message: "请输入培训讲师", trigger: "blur" },
      { required: true, message: "请选择培训讲师", trigger: "change" },
    ],
    classHour: [{ required: true, message: "请输入课时", trigger: "blur" }],
  };
@@ -441,7 +552,6 @@
      participants: "", // 参加对象
      trainingContent: "", // 培训内容
      trainingLecturer: "", // 培训讲师
      projectCredits: "", // 项目学分
      trainingMode: "", // 培训方式
      placeTraining: "", // 培训地点
      classHour: "", // 课时
@@ -475,6 +585,60 @@
    );
    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;
@@ -492,11 +656,38 @@
  ]);
  const fileList = ref([]);
  const getTrainingState = row => Number(row?.state);
  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([
@@ -504,7 +695,8 @@
      label: "课程编号",
      prop: "courseCode",
      width: 150,
      showOverflowTooltip: true,
      dataType: "slot",
      slot: "courseCode",
    },
    {
      label: "培训日期",
@@ -550,12 +742,6 @@
      showOverflowTooltip: true,
    },
    {
      label: "项目学分",
      prop: "projectCredits",
      width: 120,
      showOverflowTooltip: true,
    },
    {
      label: "培训方式",
      prop: "trainingMode",
      width: 120,
@@ -577,10 +763,11 @@
      showOverflowTooltip: true,
    },
    {
      label: "报名人数",
      label: "签到情况",
      prop: "nums",
      width: 120,
      showOverflowTooltip: true,
      width: 170,
      dataType: "slot",
      slot: "signStatus",
    },
    {
      dataType: "action",
@@ -592,10 +779,20 @@
        {
          name: "签到",
          type: "text",
          show: isInProgressTraining,
          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: "编辑",
@@ -643,13 +840,97 @@
    },
  ]);
  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({
@@ -754,21 +1035,38 @@
  };
  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);
      });
    });
  };
@@ -986,8 +1284,13 @@
    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;
@@ -1028,7 +1331,6 @@
        participants: "", // 参加对象
        trainingContent: "", // 培训内容
        trainingLecturer: "", // 培训讲师
        projectCredits: "", // 项目学分
        trainingMode: "", // 培训方式
        placeTraining: "", // 培训地点
        classHour: "", // 课时
@@ -1045,7 +1347,6 @@
        participants: row.participants, // 参加对象
        trainingContent: row.trainingContent, // 培训内容
        trainingLecturer: row.trainingLecturer, // 培训讲师
        projectCredits: row.projectCredits, // 项目学分
        trainingMode: row.trainingMode, // 培训方式
        placeTraining: row.placeTraining, // 培训地点
        classHour: row.classHour, // 课时
@@ -1304,6 +1605,40 @@
  :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;