gongchunyi
2026-07-30 5d132317d0ded846812d5009a560be2fd5381478
src/views/safeProduction/safetyTrainingAssessment/index.vue
@@ -2,23 +2,14 @@
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">培训名称:</span>
        <el-input v-model="searchForm.name"
                  style="width: 240px"
                  placeholder="请输入培训名称搜索"
                  @change="handleQuery"
                  clearable
                  :prefix-icon="Search" />
        <span class="search_title ml10">培训类型:</span>
        <el-select v-model="searchForm.type"
                   clearable
                   @change="handleQuery"
                   style="width: 240px">
          <el-option v-for="item in knowledgeTypeOptions"
                     :key="item.value"
                     :label="item.label"
                     :value="item.value" />
        </el-select>
        <span class="search_title">培训日期:</span>
        <el-date-picker v-model="searchForm.trainingDate"
                        value-format="YYYY-MM-DD"
                        format="YYYY-MM-DD"
                        @change="handleQuery"
                        type="date"
                        placeholder="请选择"
                        clearable />
        <el-button type="primary"
                   @click="handleQuery"
                   style="margin-left: 10px">
@@ -28,12 +19,24 @@
      <div>
        <el-button type="primary"
                   @click="openForm('add')">新增培训</el-button>
        <el-button type="primary"
                   @click="opendetail">培训记录</el-button>
        <el-button type="danger"
                   plain
                   @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <el-tabs v-model="searchForm.state"
               @tab-click="tabhandleQuery">
        <el-tab-pane label="未开始"
                     :name="0"></el-tab-pane>
        <el-tab-pane label="进行中"
                     :name="1"></el-tab-pane>
        <el-tab-pane label="已结束"
                     :name="2"></el-tab-pane>
      </el-tabs>
      <!-- state   状态(0:未开始1:进行中;2:已结束)    -->
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
@@ -43,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"
@@ -53,7 +70,8 @@
      <el-form ref="formRef"
               :model="form"
               :rules="rules"
               label-width="120px">
               label-position="top"
               label-width="150px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="培训日期"
@@ -82,6 +100,7 @@
                          prop="openingTime">
              <el-time-picker v-model="form.openingTime"
                              placeholder="请选择"
                              style="width: 100%"
                              value-format="HH:mm:ss"
                              format="HH:mm:ss"
                              clearable />
@@ -92,6 +111,7 @@
                          prop="endTime">
              <el-time-picker v-model="form.endTime"
                              placeholder="请选择"
                              style="width: 100%"
                              value-format="HH:mm:ss"
                              format="HH:mm:ss"
                              clearable />
@@ -109,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>
@@ -125,19 +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"
                        placeholder="请输入课程学分" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="培训方式"
                          prop="trainingMode">
@@ -173,77 +205,277 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <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-dialog v-model="viewDialogVisible"
               title="培训详情"
               title="结果明细"
               width="900px"
               :close-on-click-modal="false">
      <div class="knowledge-detail">
        <el-descriptions :column="2"
                         border>
          <el-descriptions-item label="培训名称"
                                :span="2">
            <span class="detail-title">{{ currentKnowledge.name }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="培训编码">
            {{ currentKnowledge.code }}
          </el-descriptions-item>
          <el-descriptions-item label="培训类型">
            <el-tag type="info">
              <!-- {{ getTypeLabel(currentKnowledge.type) }} -->
        <div class="classtitle">课程详情</div>
        <el-descriptions size="mini"
                         border
                         :column="3">
          <el-descriptions-item label="课程编号:">{{ currentKnowledge.courseCode }}</el-descriptions-item>
          <el-descriptions-item label="培训内容:">{{ currentKnowledge.trainingContent }}</el-descriptions-item>
          <el-descriptions-item label="状态:">
            <el-tag :type="currentKnowledge.status === 0 ? 'success' : (currentKnowledge.status === 1 ? 'success' : 'info')">
              {{ currentKnowledge.status === 0 ? '未开始' : (currentKnowledge.status === 1 ? '进行中' : '已结束') }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="所在位置">
            {{ currentKnowledge.location }}
          <el-descriptions-item label="培训讲师:">
            {{ currentKnowledge.trainingLecturer }}
          </el-descriptions-item>
          <el-descriptions-item label="管控措施">
            {{ currentKnowledge.controlMeasures }}
          <el-descriptions-item label="培训开始时间:">
            {{ currentKnowledge.trainingDate + ' ' + currentKnowledge.openingTime }}
          </el-descriptions-item>
          <el-descriptions-item label="库存数量">
            {{ currentKnowledge.stockQty }}
          <el-descriptions-item label="培训结束时间:">
            {{ currentKnowledge.trainingDate + ' ' + currentKnowledge.endTime }}
          </el-descriptions-item>
          <el-descriptions-item label="管控责任人">
            {{ currentKnowledge.principalUserId }}
          <el-descriptions-item label="培训目标:">
            {{ currentKnowledge.trainingObjectives }}
          </el-descriptions-item>
          <el-descriptions-item label="责任人联系电话">
            {{ currentKnowledge.principalMobile }}
          <el-descriptions-item label="参加对象:">
            {{ currentKnowledge.participants }}
          </el-descriptions-item>
          <el-descriptions-item label="风险等级">
            <el-tag :type="getTypeTagType(currentKnowledge.riskLevel)">
              {{ currentKnowledge.riskLevel }}
          <el-descriptions-item label="培训方式:">
            <el-tag type="primary">
              {{ getTrainingModeLabel(currentKnowledge.trainingMode) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="规格 / 风险描述">
            {{ currentKnowledge.specInfo }}
          <el-descriptions-item label="培训地点:">
            {{ currentKnowledge.placeTraining }}
          </el-descriptions-item>
          <el-descriptions-item label="课时:">
            {{ currentKnowledge.classHour }}
          </el-descriptions-item>
          <el-descriptions-item label="报名人数:">
            {{ currentKnowledge.nums }}
          </el-descriptions-item>
          <el-descriptions-item label="附件列表:">
            <el-button type="primary"
                       size="small"
                       @click="openFileDialog(endform)">附件列表</el-button>
          </el-descriptions-item>
        </el-descriptions>
        <!-- <el-divider style="margin: 20px 0;" /> -->
        <div class="classtitle"
             style="margin-top: 40px;margin-bottom: 30px;">课程评价</div>
        <el-form ref="formRef"
                 :model="form"
                 :rules="rules"
                 label-position="top"
                 label-width="150px">
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="评价人:"
                            prop="courseCode">
                <el-input v-model="endform.assessmentUserName"
                          disabled
                          placeholder="请选择评价人" />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="评价时间:"
                            prop="trainingDate">
                <el-date-picker style="width: 100%"
                                disabled
                                v-model="endform.assessmentDate"
                                value-format="YYYY-MM-DD"
                                format="YYYY-MM-DD"
                                type="date"
                                placeholder="请选择"
                                clearable />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="考核方式:"
                            prop="assessmentMethod">
                <el-input v-model="endform.assessmentMethod"
                          placeholder="请选择考核方式" />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="本次课程综合评价:"
                            prop="comprehensiveAssessment">
                <el-input v-model="endform.comprehensiveAssessment"
                          placeholder="请输入本次课程综合评价" />
              </el-form-item>
            </el-col>
          </el-row>
          <el-form-item label="培训摘要:"
                        prop="trainingAbstract">
            <el-input v-model="endform.trainingAbstract"
                      type="textarea"
                      :rows="2"
                      placeholder="请输入培训摘要" />
          </el-form-item>
          <!-- <el-row :gutter="30">
            <el-col :span="24">
              <el-form-item label="附件材料:"
                            prop="remark">
                <el-upload v-model:file-list="fileList"
                           :action="upload.url"
                           multiple
                           ref="fileUpload"
                           auto-upload
                           :headers="upload.headers"
                           :before-upload="handleBeforeUpload"
                           :on-error="handleUploadError"
                           :on-success="handleUploadSuccess"
                           :on-remove="handleRemove">
                  <el-button type="primary"
                             v-if="operationType !== 'view'">上传</el-button>
                  <template #tip
                            v-if="operationType !== 'view'">
                    <div class="el-upload__tip">
                      文件格式支持
                      doc,docx,xls,xlsx,ppt,pptx,pdf,txt,xml,jpg,jpeg,png,gif,bmp,rar,zip,7z
                    </div>
                  </template>
                </el-upload>
              </el-form-item>
            </el-col>
          </el-row> -->
        </el-form>
        <div class="classtitle"
             style="margin-top: 40px;">考核列表</div>
        <el-table style="margin-top: 20px;"
                  :data="endform.safeTrainingDetailsDtoList"
                  border
                  fit
                  stripe
                  highlight-current-row>
          <el-table-column prop="nickName"
                           label="姓名" />
          <el-table-column prop="phonenumber"
                           label="电话号码" />
          <el-table-column prop="examinationResults"
                           label="考核结果">
            <template #default="scope">
              <el-select v-model="scope.row.examinationResults"
                         placeholder="请选择考核结果">
                <el-option label="合格"
                           value="合格" />
                <el-option label="不合格"
                           value="不合格" />
              </el-select>
            </template>
          </el-table-column>
        </el-table>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary"
                     @click="submitForm2">提交</el-button>
          <el-button @click="viewDialogVisible = false">关闭</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- 附件列表弹窗 -->
    <FileListDialog ref="fileListRef"
                    v-model="fileListDialogVisible"
                    :show-upload-button="true"
                    :show-delete-button="true"
                    :upload-method="handleUpload"
                    :delete-method="handleFileDelete"
                    title="附件列表" />
    <!--  todo 附件预览相关 -->
    <FileList v-if="fileDialogVisible"  v-model:visible="fileDialogVisible" record-type="safe_training" :record-id="recordId"  />
  </div>
</template>
<script setup>
  import { Search } from "@element-plus/icons-vue";
  import FileListDialog from "@/components/Dialog/FileListDialog.vue";
  import {
    onMounted,
    ref,
@@ -265,7 +497,14 @@
    safeTrainingFileListPage,
    safeTrainingFileAdd,
    safeTrainingFileDel,
    safeTrainingSign,
    safeTrainingGet,
    safeTrainingSave,
  } from "@/api/safeProduction/safetyTrainingAssessment.js";
  import useUserStore from "@/store/modules/user";
  import dayjs from "dayjs";
  const userStore = useUserStore();
  const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
  // 表单验证规则
  const rules = {
@@ -280,16 +519,21 @@
      { required: true, message: "请输入培训内容", trigger: "blur" },
    ],
    trainingLecturer: [
      { required: true, message: "请输入培训讲师", trigger: "blur" },
      { required: true, message: "请选择培训讲师", trigger: "change" },
    ],
    classHour: [{ required: true, message: "请输入课时", trigger: "blur" }],
  };
  const upload = reactive({
    // 上传的地址
    url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
    // 设置上传的请求头部
    headers: { Authorization: "Bearer " + getToken() },
  });
  // 响应式数据
  const data = reactive({
    searchForm: {
      name: "",
      type: "",
      trainingDate: "",
      state: 0,
    },
    tableLoading: false,
    page: {
@@ -308,7 +552,6 @@
      participants: "", // 参加对象
      trainingContent: "", // 培训内容
      trainingLecturer: "", // 培训讲师
      projectCredits: "", // 项目学分
      trainingMode: "", // 培训方式
      placeTraining: "", // 培训地点
      classHour: "", // 课时
@@ -342,6 +585,67 @@
    );
    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;
    console.log(searchForm.value.state, "searchForm.value.state");
    handleQuery();
  };
  // 表单引用
  const formRef = ref();
  const riskLevelOptions = ref([
@@ -351,56 +655,96 @@
    { value: "重大风险", label: "重大风险" },
  ]);
  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",
      showOverflowTooltip: true,
      width: 150,
      dataType: "slot",
      slot: "courseCode",
    },
    {
      label: "培训日期",
      prop: "trainingDate",
      width: 120,
      showOverflowTooltip: true,
    },
    {
      label: "开始时间",
      prop: "openingTime",
      width: 120,
      showOverflowTooltip: true,
    },
    {
      label: "结束时间",
      prop: "endTime",
      width: 120,
      showOverflowTooltip: true,
    },
    {
      label: "培训目标",
      prop: "trainingObjectives",
      width: 200,
      showOverflowTooltip: true,
    },
    {
      label: "参加对象",
      prop: "participants",
      width: 200,
      showOverflowTooltip: true,
    },
    {
      label: "培训内容",
      prop: "trainingContent",
      width: 200,
      showOverflowTooltip: true,
    },
    {
      label: "培训讲师",
      prop: "trainingLecturer",
      showOverflowTooltip: true,
    },
    {
      label: "项目学分",
      prop: "projectCredits",
      width: 200,
      showOverflowTooltip: true,
    },
    {
      label: "培训方式",
      prop: "trainingMode",
      width: 120,
      showOverflowTooltip: true,
      formatData: params => {
        return getTrainingModeLabel(params);
@@ -409,39 +753,80 @@
    {
      label: "培训地点",
      prop: "placeTraining",
      width: 200,
      showOverflowTooltip: true,
    },
    {
      label: "课时",
      prop: "classHour",
      width: 120,
      showOverflowTooltip: true,
    },
    {
      label: "签到情况",
      prop: "nums",
      width: 170,
      dataType: "slot",
      slot: "signStatus",
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 200,
      width: 220,
      operation: [
        {
          name: "签到",
          type: "text",
          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",
          show: isNotStartedTraining,
          clickFun: row => {
            openForm("edit", row);
          },
        },
        {
          name: "导出",
          type: "danger",
          type: "text",
          show: isEndedTraining,
          clickFun: row => {
            exportKnowledge(row);
          },
          color: "#C49000",
        },
        {
          name: "附件",
          type: "danger",
          type: "text",
          show: hasKnownTrainingState,
          clickFun: row => {
            downLoadFile(row);
            openFileDialog(row);
          },
          color: "#007AFF",
        },
        {
          name: "结果明细",
          type: "text",
          show: isEndedTraining,
          clickFun: row => {
            viewResultDetail(row);
          },
        },
        // {
@@ -455,14 +840,236 @@
    },
  ]);
  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({
    assessmentUserId: "", //评价人
    assessmentUserName: "", //评价人姓名
    assessmentMethod: "", //考核方式
    assessmentDate: "", //评价时间
    comprehensiveAssessment: "", //综合评价
    trainingAbstract: "", //培训摘要
    safeTrainingFileList: [], //培训附件
    safeTrainingDetailsDtoList: [], //考核结果详情
  });
  const operationType = ref("edit");
  const viewResultDetail = row => {
    // fileList.value = [];
    operationType.value = "edit";
    safeTrainingGet({ id: row.id }).then(res => {
      if (res.code === 200) {
        console.log(res.data, "res.data");
        currentKnowledge.value = JSON.parse(JSON.stringify(res.data));
        currentKnowledge.value.nums = row.nums;
        viewDialogVisible.value = true;
        endform.value = { ...res.data };
        endform.value.assessmentUserName = endform.value.assessmentUserName
          ? endform.value.assessmentUserName
          : currentUserName.value;
        endform.value.assessmentUserId = endform.value.assessmentUserId
          ? endform.value.assessmentUserId
          : currentUserId.value;
        endform.value.assessmentDate = dayjs().format("YYYY-MM-DD");
      } else {
        proxy.$modal.msgError(res.msg || "查询详情失败");
      }
    });
  };
  // 上传前校检
  function handleBeforeUpload(file) {
    proxy.$modal.loading("正在上传文件,请稍候...");
    return true;
  }
  // 上传失败
  function handleUploadError(err) {
    proxy.$modal.msgError("上传文件失败");
    proxy.$modal.closeLoading();
  }
  // 上传成功回调
  function handleUploadSuccess(res, file, uploadFiles) {
    proxy.$modal.closeLoading();
    if (res.code === 200) {
      // 确保 tempFileIds 存在且为数组
      if (!endform.value.safeTrainingFileList) {
        endform.value.safeTrainingFileList = [];
      }
      endform.value.safeTrainingFileList.push({
        id: res.data.tempId,
        fileName: res.data.originalName,
        url: res.data.tempPath,
        safeTrainingId: currentKnowledge.value.id,
      });
      proxy.$modal.msgSuccess("上传成功");
    } else {
      proxy.$modal.msgError(res.msg);
      proxy.$refs.fileUpload.handleRemove(file);
    }
  }
  // 移除文件
  function handleRemove(file) {
    if (operationType.value === "edit") {
      let index = endform.value.safeTrainingFileList.findIndex(
        item => item.fileName === file.name
      );
      if (index !== -1) {
        endform.value.safeTrainingFileList.splice(index, 1);
      }
    }
  }
  const submitForm2 = () => {
    endform.value.safeTrainingDetailsDtoList.forEach((item, index) => {
      if (!item.examinationResults) {
        proxy.$modal.msgError(`请选择${item.nickName}的考核结果`);
        return;
      }
    });
    console.log(endform.value, "endform.value");
    proxy.$modal.loading("正在提交,请稍候...");
    safeTrainingSave(endform.value).then(res => {
      proxy.$modal.closeLoading();
      if (res.code === 200) {
        proxy.$modal.msgSuccess("提交成功");
        getList();
        viewDialogVisible.value = false;
      } else {
        proxy.$modal.msgError(res.msg || "提交失败");
      }
    });
  };
  const opendetail = row => {
    proxy.$router.push({
      path: "/safeProduction/safetyTrainingAssessmentDetail",
    });
  };
  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);
      });
    });
  };
  // 处理用户选择变化
  const handleUserChange = userId => {
@@ -472,22 +1079,17 @@
      form.value.principalMobile = selectedUser.phonenumber;
    }
  };
  /**
   * 下载文件
   *
   * @param row 下载文件的相关信息对象
   */
  const fileListRef = ref(null);
  const fileListDialogVisible = ref(false);
  const currentFileRow = ref(null);
  const downLoadFile = row => {
    currentFileRow.value = row;
    safeTrainingFileListPage({ safeTrainingId: row.id }).then(res => {
      if (fileListRef.value) {
        fileListRef.value.open(res.data.records);
      }
    });
  };
  // 打开附件弹窗
  const recordId =ref(0)
  const fileDialogVisible = ref(false)
  // 打开附件弹框
  const openFileDialog = async (row) => {
    recordId.value = row.id
    fileDialogVisible.value = true
  }
  // 上传附件
  const handleUpload = async () => {
    if (!currentFileRow.value) {
@@ -536,6 +1138,8 @@
              // 重新加载文件列表
              const listRes = await safeTrainingFileListPage({
                safeTrainingId: currentFileRow.value.id,
                current: filePagination.value.current,
                size: filePagination.value.size,
              });
              if (listRes.code === 200 && fileListRef.value) {
                const fileList = (listRes.data?.records || []).map(item => ({
@@ -545,6 +1149,7 @@
                  ...item,
                }));
                fileListRef.value.setList(fileList);
                filePagination.value.total = listRes.data?.total || 0;
              }
              // 返回新文件信息
              resolve({
@@ -572,6 +1177,31 @@
      input.click();
    });
  };
  const filePagination = ref({
    current: 1,
    size: 10,
    total: 0,
  });
  const paginationSearch = async (page, size) => {
    filePagination.value.current = page;
    filePagination.value.size = size;
    const listRes = await safeTrainingFileListPage({
      safeTrainingId: currentFileRow.value.id,
      current: filePagination.value.current,
      size: filePagination.value.size,
    });
    if (listRes.code === 200) {
      const fileList = (listRes.data?.records || []).map(item => ({
        name: item.name,
        url: item.url,
        id: item.id,
        ...item,
      }));
      fileListRef.value.setList(fileList);
      filePagination.value.total = listRes.data?.total || 0;
    }
  };
  // 删除附件
  const handleFileDelete = async row => {
    try {
@@ -582,6 +1212,8 @@
        if (currentFileRow.value && fileListRef.value) {
          const listRes = await safeTrainingFileListPage({
            safeTrainingId: currentFileRow.value.id,
            current: filePagination.value.current,
            size: filePagination.value.size,
          });
          if (listRes.code === 200) {
            const fileList = (listRes.data?.records || []).map(item => ({
@@ -591,6 +1223,7 @@
              ...item,
            }));
            fileListRef.value.setList(fileList);
            filePagination.value.total = listRes.data?.total || 0;
          }
        }
        return true; // 返回 true 表示删除成功,组件会更新列表
@@ -627,7 +1260,7 @@
        const url = window.URL.createObjectURL(blob);
        const link = document.createElement("a");
        link.href = url;
        link.download = `培训记录_${row.courseCode}.xlsx`;
        link.download = `培训记录_${row.courseCode}.docx`;
        // 模拟点击下载
        document.body.appendChild(link);
@@ -651,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;
@@ -663,12 +1301,19 @@
  const pagination = obj => {
    page.value.current = obj.page;
    page.value.size = obj.limit;
    handleQuery();
    getList();
  };
  // 选择变化处理
  const handleSelectionChange = selection => {
    selectedIds.value = selection.map(item => item.id);
  };
  const currentUserId = ref("");
  const currentUserName = ref("");
  const getCurrentFactoryName = async () => {
    let res = await userStore.getInfo();
    currentUserId.value = res.user.userId;
    currentUserName.value = res.user.nickName;
  };
  // 打开表单
@@ -686,7 +1331,6 @@
        participants: "", // 参加对象
        trainingContent: "", // 培训内容
        trainingLecturer: "", // 培训讲师
        projectCredits: "", // 项目学分
        trainingMode: "", // 培训方式
        placeTraining: "", // 培训地点
        classHour: "", // 课时
@@ -703,19 +1347,12 @@
        participants: row.participants, // 参加对象
        trainingContent: row.trainingContent, // 培训内容
        trainingLecturer: row.trainingLecturer, // 培训讲师
        projectCredits: row.projectCredits, // 项目学分
        trainingMode: row.trainingMode, // 培训方式
        placeTraining: row.placeTraining, // 培训地点
        classHour: row.classHour, // 课时
      });
    }
    dialogVisible.value = true;
  };
  // 查看培训详情
  const viewKnowledge = row => {
    currentKnowledge.value = { ...row };
    viewDialogVisible.value = true;
  };
  // 获取类型标签类型
@@ -860,6 +1497,38 @@
</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;
  }
@@ -936,4 +1605,46 @@
  :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;
    color: #303133;
    border-left: 4px solid #409eff;
    padding-left: 12px;
    margin-bottom: 12px;
  }
</style>