zhangwencui
8 天以前 84bf005a674d3c49e66a67b9a55824dedb0d7120
安全培训模块开发
已添加7个文件
已修改2个文件
2722 ■■■■■ 文件已修改
src/api/safeProduction/safetyTrainingAssessment.js 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/safeProduction/safetyTrainingAssessment/detail.vue 430 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/safeProduction/safetyTrainingAssessment/fileList.vue 567 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/safeProduction/safetyTrainingAssessment/index.vue 448 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/safeProduction/safetyTrainingAssessment/record.vue 546 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/safeProduction/safetyTrainingAssessment/resultDetail.vue 388 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/safeProduction/safetyTrainingAssessment/view.vue 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/safeProduction/safetyTrainingAssessment.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
export function safeTrainingListPage(query) {
  return request({
    url: "/safeTraining/page",
    method: "get",
    params: query,
  });
}
// æ–°å¢žå®‰å…¨åŸ¹è®­è¯„ä¼°
export function safeTrainingAdd(query) {
    return request({
        url: '/safeTraining',
        method: 'post',
        data: query
    })
}
// ä¿®æ”¹å®‰å…¨åŸ¹è®­è¯„ä¼°
export function safeTrainingUpdate(query) {
    return request({
        url: '/safeTraining',
        method: 'put',
        data: query
    })
}
// åˆ é™¤å®‰å…¨åŸ¹è®­è¯„ä¼°
export function safeTrainingDel(ids) {
    return request({
        url: '/safeTraining/' + ids,
        method: 'delete',
        data: ids
    })
}
// å¯¼å‡º
export function safeTrainingExport(query) {
    return request({
        url: '/safeTraining/export',
        method: 'post',
        data: query,
        responseType: 'blob'
    })
}
// æŸ¥è¯¢é™„件列表
export function safeTrainingFileListPage(query) {
  return request({
    url: "/safeTrainingFile/listPage",
    method: "get",
    params: query,
  });
}
// æ·»åР附件
export function safeTrainingFileAdd(query) {
    return request({
        url: '/safeTrainingFile/add',
        method: 'post',
        data: query
    })
}
// åˆ é™¤é™„ä»¶
export function safeTrainingFileDel(ids) {
    return request({
        url: '/safeTrainingFile/del',
        method: 'delete',
        data: ids
    })
}
// ç­¾åˆ°
export function safeTrainingSign(query) {
    return request({
        url: '/safeTraining/sign',
        method: 'post',
        data: query
    })
}
// æŸ¥è¯¢è¯¦æƒ…
export function safeTrainingGet(query) {
    return request({
        url: '/safeTraining/getSafeTraining',
        method: 'get',
        params: query
    })
}
// æäº¤
export function safeTrainingSave(query) {
    return request({
        url: '/safeTraining/saveSafeTraining',
        method: 'post',
        data: query
    })
}
export function safeTrainingDetailListPage(query) {
  return request({
    url: "/safeTrainingDetails/page",
    method: "get",
    params: query,
  });
}
// å¯¼å‡º
export function safeTrainingDetailExport(query) {
    return request({
        url: '/safeTrainingDetails/export',
        method: 'post',
        data: query,
        responseType: 'blob'
    })
}
src/pages.json
@@ -814,6 +814,48 @@
        "navigationBarTitleText": "应急预案详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/safeProduction/safetyTrainingAssessment/index",
      "style": {
        "navigationBarTitleText": "安全培训评估",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/safeProduction/safetyTrainingAssessment/detail",
      "style": {
        "navigationBarTitleText": "培训详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/safeProduction/safetyTrainingAssessment/view",
      "style": {
        "navigationBarTitleText": "培训详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/safeProduction/safetyTrainingAssessment/fileList",
      "style": {
        "navigationBarTitleText": "培训附件",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/safeProduction/safetyTrainingAssessment/resultDetail",
      "style": {
        "navigationBarTitleText": "结果明细",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/safeProduction/safetyTrainingAssessment/record",
      "style": {
        "navigationBarTitleText": "培训记录",
        "navigationStyle": "custom"
      }
    }
  ],
  "subPackages": [
src/pages/index.vue
@@ -331,6 +331,10 @@
      icon: "/static/images/icon/guzhangfenxi@2x.png",
      label: "事故上报",
    },
    {
      icon: "/static/images/icon/guzhangfenxi@2x.png",
      label: "安全培训",
    },
  ]);
  // ååŒåŠžå…¬åŠŸèƒ½æ•°æ®
  const collaborationItems = reactive([
@@ -724,6 +728,12 @@
          url: "/pages/safeProduction/accidentReportingRecord/index",
        });
        break;
      case "安全培训":
        uni.navigateTo({
          url: "/pages/safeProduction/safetyTrainingAssessment/index",
        });
        break;
      default:
        uni.showToast({
          title: `点击了${item.label}`,
src/pages/safeProduction/safetyTrainingAssessment/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,430 @@
<template>
  <view class="danger-investigation-detail">
    <PageHeader :title="isEdit ? '编辑培训' : '新增培训'"
                @back="goBack" />
    <u-form @submit="handleSubmit"
            ref="formRef"
            label-width="110">
      <!-- åŸ¹è®­ä¿¡æ¯ -->
      <u-cell-group title="培训信息">
        <u-form-item label="课程编号"
                     prop="courseCode"
                     border-bottom>
          <u-input v-model="form.courseCode"
                   placeholder="系统自动生成"
                   readonly />
        </u-form-item>
        <u-form-item label="培训日期"
                     prop="trainingDate"
                     required
                     border-bottom>
          <u-input v-model="form.trainingDate"
                   placeholder="请选择培训日期"
                   @click="showTrainingDatePicker"
                   readonly />
          <template #right>
            <up-icon name="arrow-right"
                     @click="showTrainingDatePicker"></up-icon>
          </template>
        </u-form-item>
        <u-form-item label="开始时间"
                     prop="openingTime"
                     required
                     border-bottom>
          <u-input v-model="form.openingTime"
                   placeholder="请选择开始时间"
                   @click="showOpeningTimePicker"
                   readonly />
          <template #right>
            <up-icon name="arrow-right"
                     @click="showOpeningTimePicker"></up-icon>
          </template>
        </u-form-item>
        <u-form-item label="结束时间"
                     prop="endTime"
                     required
                     border-bottom>
          <u-input v-model="form.endTime"
                   placeholder="请选择结束时间"
                   @click="showEndTimePicker"
                   readonly />
          <template #right>
            <up-icon name="arrow-right"
                     @click="showEndTimePicker"></up-icon>
          </template>
        </u-form-item>
        <u-form-item label="培训目标"
                     prop="trainingObjectives"
                     border-bottom>
          <u-input v-model="form.trainingObjectives"
                   placeholder="请输入培训目标" />
        </u-form-item>
        <u-form-item label="参加对象"
                     prop="participants"
                     border-bottom>
          <u-input v-model="form.participants"
                   placeholder="请输入参加对象" />
        </u-form-item>
        <u-form-item label="培训内容"
                     prop="trainingContent"
                     required
                     border-bottom>
          <u-textarea v-model="form.trainingContent"
                      placeholder="请输入培训内容"
                      :maxlength="200"
                      count
                      :autoHeight="true" />
        </u-form-item>
        <u-form-item label="培训讲师"
                     prop="trainingLecturer"
                     required
                     border-bottom>
          <u-input v-model="form.trainingLecturer"
                   placeholder="请输入培训讲师" />
        </u-form-item>
        <u-form-item label="项目学分"
                     prop="projectCredits"
                     border-bottom>
          <u-input v-model="form.projectCredits"
                   placeholder="请输入项目学分"
                   type="number" />
        </u-form-item>
        <u-form-item label="培训方式"
                     prop="trainingMode"
                     border-bottom>
          <u-input v-model="trainingModeName"
                   placeholder="请选择培训方式"
                   @click="showTrainingModeSheet"
                   readonly />
          <template #right>
            <up-icon name="arrow-right"
                     @click="showTrainingModeSheet"></up-icon>
          </template>
        </u-form-item>
        <u-form-item label="培训地点"
                     prop="placeTraining"
                     border-bottom>
          <u-input v-model="form.placeTraining"
                   placeholder="请输入培训地点" />
        </u-form-item>
        <u-form-item label="课时"
                     prop="classHour"
                     required
                     border-bottom>
          <u-input v-model="form.classHour"
                   placeholder="请输入课时"
                   type="number" />
        </u-form-item>
      </u-cell-group>
      <!-- æäº¤æŒ‰é’® -->
      <view class="footer-btns">
        <u-button class="cancel-btn"
                  @click="goBack">取消</u-button>
        <u-button class="sign-btn"
                  type="primary"
                  @click="handleSubmit"
                  :loading="loading">{{ isEdit ? '保存修改' : '提交' }}</u-button>
      </view>
    </u-form>
    <!-- æ—¶é—´é€‰æ‹©å™¨ -->
    <up-datetime-picker :show="trainingDateVisible"
                        mode="date"
                        v-model="nowDate"
                        @confirm="handleTrainingDateConfirm"
                        @cancel="trainingDateVisible = false"
                        title="选择培训日期" />
    <u-datetime-picker :show="openingTimeVisible"
                       mode="time"
                       @confirm="handleOpeningTimeConfirm"
                       @cancel="openingTimeVisible = false"
                       title="选择开始时间" />
    <u-datetime-picker :show="endTimeVisible"
                       mode="time"
                       @confirm="handleEndTimeConfirm"
                       @cancel="endTimeVisible = false"
                       title="选择结束时间" />
    <!-- åŸ¹è®­æ–¹å¼é€‰æ‹©å™¨ -->
    <up-action-sheet :show="trainingModeSheetVisible"
                     :actions="trainingModeOptions"
                     @select="handleTrainingModeSelect"
                     @close="trainingModeSheetVisible = false"
                     title="选择培训方式" />
  </view>
</template>
<script setup>
  // æ›¿æ¢ toast æ–¹æ³•
  defineOptions({ name: "danger-investigation-detail" });
  const showToast = message => {
    uni.showToast({ title: message, icon: "none" });
  };
  import { ref, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import {
    safeTrainingAdd,
    safeTrainingUpdate,
  } from "@/api/safeProduction/safetyTrainingAssessment";
  import { onLoad } from "@dcloudio/uni-app";
  import { useDict } from "@/utils/dict";
  import dayjs from "dayjs";
  // èŽ·å–å­—å…¸æ•°æ®
  const { safe_training_methods } = useDict("safe_training_methods");
  // è¡¨å•数据
  const form = ref({
    courseCode: "", // è¯¾ç¨‹ç¼–号
    trainingDate: "", // åŸ¹è®­æ—¥æœŸ
    openingTime: "", // å¼€å§‹æ—¶é—´
    endTime: "", // ç»“束时间
    trainingObjectives: "", // åŸ¹è®­ç›®æ ‡
    participants: "", // å‚加对象
    trainingContent: "", // åŸ¹è®­å†…容
    trainingLecturer: "", // åŸ¹è®­è®²å¸ˆ
    projectCredits: "", // é¡¹ç›®å­¦åˆ†
    trainingMode: "", // åŸ¹è®­æ–¹å¼
    placeTraining: "", // åŸ¹è®­åœ°ç‚¹
    classHour: "", // è¯¾æ—¶
  });
  // é¡µé¢çŠ¶æ€
  const loading = ref(false);
  const formRef = ref(null);
  const isEdit = ref(false);
  // åŸ¹è®­æ–¹å¼é€‰æ‹©å™¨
  const trainingModeSheetVisible = ref(false);
  const trainingModeName = ref("");
  const trainingModeOptions = ref([]);
  // æ—¶é—´é€‰æ‹©å™¨
  const trainingDateVisible = ref(false);
  const openingTimeVisible = ref(false);
  const endTimeVisible = ref(false);
  const nowDate = ref(new Date());
  const showTrainingDatePicker = () => {
    trainingDateVisible.value = true;
  };
  const showOpeningTimePicker = () => {
    openingTimeVisible.value = true;
  };
  const showEndTimePicker = () => {
    endTimeVisible.value = true;
  };
  const handleTrainingDateConfirm = e => {
    form.value.trainingDate = dayjs(e.value).format("YYYY-MM-DD");
    nowDate.value = e.value;
    trainingDateVisible.value = false;
  };
  const handleOpeningTimeConfirm = e => {
    console.log(e);
    form.value.openingTime = e.value;
    openingTimeVisible.value = false;
  };
  const handleEndTimeConfirm = e => {
    form.value.endTime = e.value;
    endTimeVisible.value = false;
  };
  // æ˜¾ç¤ºåŸ¹è®­æ–¹å¼é€‰æ‹©å™¨
  const showTrainingModeSheet = () => {
    trainingModeSheetVisible.value = true;
  };
  // å¤„理培训方式选择
  const handleTrainingModeSelect = item => {
    form.value.trainingMode = item.value;
    trainingModeName.value = item.name;
    trainingModeSheetVisible.value = false;
  };
  // è¿”回上一页
  const goBack = () => {
    uni.removeStorageSync("safetyTraining");
    uni.navigateBack();
  };
  // æäº¤è¡¨å•
  const handleSubmit = async () => {
    if (!form.value.trainingDate) {
      showToast("请选择培训日期");
      return;
    }
    if (!form.value.openingTime) {
      showToast("请选择开始时间");
      return;
    }
    if (!form.value.endTime) {
      showToast("请选择结束时间");
      return;
    }
    if (!form.value.trainingContent) {
      showToast("请输入培训内容");
      return;
    }
    if (!form.value.trainingLecturer) {
      showToast("请输入培训讲师");
      return;
    }
    if (!form.value.classHour) {
      showToast("请输入课时");
      return;
    }
    if (
      form.value.projectCredits &&
      (isNaN(Number(form.value.projectCredits)) ||
        Number(form.value.projectCredits) <= 0)
    ) {
      showToast("学分必须是大于0的数字");
      return;
    }
    form.value.openingTime = form.value.openingTime + ":00";
    form.value.endTime = form.value.endTime + ":00";
    if (
      form.value.classHour &&
      (isNaN(Number(form.value.classHour)) || Number(form.value.classHour) <= 0)
    ) {
      showToast("课时必须是大于0的数字");
      return;
    }
    try {
      loading.value = true;
      // ä½¿ç”¨å®‰å…¨æµ…拷贝
      const source =
        form.value && typeof form.value === "object" ? form.value : {};
      const submitData = {};
      Object.keys(source).forEach(k => {
        submitData[k] = source[k];
      });
      if (isEdit.value) {
        const { code } = await safeTrainingAdd(submitData);
        if (code === 200) {
          showToast("修改成功");
          setTimeout(() => {
            goBack();
          }, 500);
        } else {
          loading.value = false;
          showToast("修改失败,请重试");
        }
      } else {
        const { code } = await safeTrainingAdd(submitData);
        if (code === 200) {
          showToast("新增成功");
          setTimeout(() => {
            goBack();
          }, 500);
        } else {
          loading.value = false;
          showToast("新增失败,请重试");
        }
      }
    } catch (e) {
      loading.value = false;
      console.error("提交失败:", e);
      showToast("提交失败,请重试");
    }
  };
  onLoad(() => {
    // ç¼–辑培训时,从本地存储获取数据
    const safetyTraining = uni.getStorageSync("safetyTraining");
    if (safetyTraining.id) {
      form.value = safetyTraining;
      nowDate.value = dayjs(form.value.trainingDate).toDate();
      form.value.openingTime = form.value.openingTime
        ? form.value.openingTime.slice(0, 5)
        : "";
      form.value.endTime = form.value.endTime
        ? form.value.endTime.slice(0, 5)
        : "";
      isEdit.value = true;
    } else {
      isEdit.value = false;
      // é»˜è®¤åŸ¹è®­æ—¥æœŸä¸ºä»Šå¤©
      form.value.trainingDate = dayjs().format("YYYY-MM-DD");
    }
  });
  onMounted(() => {
    // åˆå§‹åŒ–培训方式选项
    if (safe_training_methods && Array.isArray(safe_training_methods.value)) {
      trainingModeOptions.value =
        safe_training_methods.value.map(item => ({
          value: item.value,
          name: item.label,
        })) || [];
    } else {
      trainingModeOptions.value = [];
    }
    // è®¾ç½®å·²é€‰å€¼çš„æ˜¾ç¤ºæ–‡æœ¬
    if (form.value.trainingMode) {
      const modeItem = trainingModeOptions.value.find(
        item => String(item.value) === String(form.value.trainingMode)
      );
      trainingModeName.value = modeItem ? modeItem.name : "";
    }
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .danger-investigation-detail {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100px;
  }
  .footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 0.75rem 0;
    box-shadow: 0 -0.125rem 0.5rem rgba(0, 0, 0, 0.05);
    z-index: 1000;
  }
  .cancel-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #666;
    background: #f5f5f5;
    border: 1px solid #ddd;
    width: 45%;
    height: 2.5rem;
    border-radius: 2.5rem;
  }
  .sign-btn {
    font-weight: 500;
    font-size: 1rem;
    color: #fff;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border: none;
    width: 45%;
    height: 2.5rem;
    border-radius: 2.5rem;
  }
</style>
src/pages/safeProduction/safetyTrainingAssessment/fileList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,567 @@
<template>
  <view class="file-list-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="附件管理"
                @back="goBack" />
    <!-- é™„件列表 -->
    <view class="file-list-container">
      <view v-if="fileList.length > 0"
            class="file-list">
        <view v-for="(file, index) in fileList"
              :key="file.id || index"
              class="file-item">
          <!-- æ–‡ä»¶å›¾æ ‡ -->
          <!-- <view class="file-icon"
                :class="getFileIconClass(file.fileType)">
            <up-icon :name="getFileIcon(file.fileType)"
                     size="24"
                     color="#ffffff" />
          </view> -->
          <!-- æ–‡ä»¶ä¿¡æ¯ -->
          <view class="file-info">
            <text class="file-name">{{ file.name }}</text>
            <!-- <text class="file-meta">{{ formatFileSize(file.fileSize) }} Â· {{ file.uploadTime || file.createTime }}</text> -->
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="file-actions">
            <!-- <u-button size="small"
                      type="primary"
                      plain
                      @click="previewFile(file)">预览</u-button> -->
            <u-button size="small"
                      type="info"
                      plain
                      @click="downloadFile(file)">下载并预览</u-button>
            <u-button size="small"
                      type="error"
                      plain
                      @click="confirmDelete(file, index)">删除</u-button>
          </view>
        </view>
      </view>
      <!-- ç©ºçŠ¶æ€ -->
      <view v-else
            class="empty-state">
        <up-icon name="document"
                 size="64"
                 color="#c0c4cc" />
        <text class="empty-text">暂无附件</text>
      </view>
    </view>
    <!-- <a rel="nofollow"
       id="downloadLink"
       href="#"
       style="display:none;">下载文本文件</a> -->
    <!-- ä¸Šä¼ æŒ‰é’® -->
    <view class="upload-button"
          @click="chooseFile">
      <up-icon name="plus"
               size="24"
               color="#ffffff" />
      <text class="upload-text">上传附件</text>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import config from "@/config";
  import { getToken } from "@/utils/auth";
  // import { saveAs } from "file-saver";
  import {
    listRuleFiles,
    delRuleFile,
  } from "@/api/managementMeetings/rulesRegulationsManagement";
  import {
    safeTrainingFileListPage,
    safeTrainingFileAdd,
    safeTrainingFileDel,
  } from "@/api/safeProduction/safetyTrainingAssessment";
  import { blobValidate } from "@/utils/ruoyi";
  // é™„件列表
  const fileList = ref([]);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // const request = axios.create({
  //   baseURL: "URL.com",
  //   adapter: axiosAdapterUniapp,
  // });
  // èŽ·å–æ–‡ä»¶å›¾æ ‡
  const getFileIcon = fileType => {
    const iconMap = {
      doc: "document",
      docx: "document",
      xls: "grid",
      xlsx: "grid",
      pdf: "document",
      ppt: "copy",
      pptx: "copy",
      txt: "document",
      jpg: "image",
      jpeg: "image",
      png: "image",
      gif: "image",
      zip: "folder",
      rar: "folder",
    };
    return iconMap[fileType.toLowerCase()] || "document";
  };
  // èŽ·å–æ–‡ä»¶å›¾æ ‡æ ·å¼ç±»
  const getFileIconClass = fileType => {
    const colorMap = {
      doc: "blue",
      docx: "blue",
      xls: "green",
      xlsx: "green",
      pdf: "red",
      ppt: "orange",
      pptx: "orange",
      txt: "gray",
      jpg: "purple",
      jpeg: "purple",
      png: "purple",
      gif: "purple",
      zip: "yellow",
      rar: "yellow",
    };
    return colorMap[fileType.toLowerCase()] || "gray";
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = bytes => {
    if (bytes === 0) return "0 B";
    const k = 1024;
    const sizes = ["B", "KB", "MB", "GB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  };
  // é€‰æ‹©æ–‡ä»¶
  const chooseFile = () => {
    uni.chooseImage({
      count: 9,
      sizeType: ["original", "compressed"],
      sourceType: ["album", "camera"],
      success: res => {
        console.log(res, "选择图片成功");
        uploadFiles(res.tempFiles);
      },
      fail: err => {
        console.error("选择图片失败:", err);
        showToast("选择文件失败");
      },
    });
    // uni.chooseFile({
    //   count: 9,
    //   extension: [
    //     ".doc",
    //     ".docx",
    //     ".xls",
    //     ".xlsx",
    //     ".pdf",
    //     ".ppt",
    //     ".pptx",
    //     ".txt",
    //     ".jpg",
    //     ".jpeg",
    //     ".png",
    //     ".gif",
    //     ".zip",
    //     ".rar",
    //   ],
    //   success: res => {
    //     console.log(res, "选择文件成功");
    //     uploadFiles(res.tempFiles);
    //   },
    //   fail: err => {
    //     showToast("选择文件失败");
    //   },
    // });
  };
  // ä¸Šä¼ æ–‡ä»¶
  const uploadFiles = tempFiles => {
    console.log(tempFiles, "上传文件1");
    tempFiles.forEach((tempFile, index) => {
      // æ˜¾ç¤ºä¸Šä¼ ä¸­æç¤º
      uni.showLoading({
        title: "上传中...",
        mask: true,
      });
      console.log(tempFile, "上传文件2");
      // 1. ç›´æŽ¥ä½¿ç”¨ uni.uploadFile ä¸Šä¼ æ–‡ä»¶
      uni.uploadFile({
        url: config.baseUrl + "/file/upload",
        filePath: tempFile.path,
        name: "file",
        header: {
          Authorization: "Bearer " + getToken(),
        },
        success: uploadRes => {
          uni.hideLoading();
          console.log(uploadRes, "上传文件3");
          try {
            const res = JSON.parse(uploadRes.data);
            console.log(res, "上传文件4");
            if (res.code === 200) {
              // 2. æå–文件信息
              const fileName = tempFile.name
                ? tempFile.name
                : tempFile.path.split("/").pop();
              // const fileType = fileName.split(".").pop();
              // 3. æž„造保存文件信息的参数
              const saveData = {
                name: fileName,
                safeTrainingId: rulesRegulationsManagementId.value,
                url: res.data.tempPath || "",
              };
              console.log(saveData, "保存文件信息参数");
              // 4. è°ƒç”¨ addRuleFile æŽ¥å£ä¿å­˜æ–‡ä»¶ä¿¡æ¯
              safeTrainingFileAdd(saveData)
                .then(addRes => {
                  if (addRes.code === 200) {
                    // 5. æ·»åŠ åˆ°æ–‡ä»¶åˆ—è¡¨
                    const newFile = {
                      ...addRes.data,
                      uploadTime: new Date().toLocaleString(),
                    };
                    // fileList.value.push(newFile);
                    getFileList();
                    showToast("上传成功");
                  } else {
                    showToast("保存文件信息失败");
                  }
                })
                .catch(err => {
                  console.error("保存文件信息失败:", err);
                  showToast("保存文件信息失败");
                });
            } else {
              showToast("文件上传失败");
            }
          } catch (e) {
            console.error("解析上传结果失败:", e);
            showToast("上传失败");
          }
        },
        fail: err => {
          uni.hideLoading();
          console.error("上传失败:", err);
          showToast("上传失败");
        },
      });
    });
  };
  // ä¸‹è½½æ–‡ä»¶
  const downloadFile = file => {
    var url =
      config.baseUrl +
      "/common/download?fileName=" +
      encodeURIComponent(file.url) +
      "&delete=true";
    console.log(url, "url");
    uni
      .downloadFile({
        url: url,
        responseType: "blob",
        header: { Authorization: "Bearer " + getToken() },
      })
      .then(res => {
        console.log(res, "下载文件");
        let osType = uni.getStorageSync("deviceInfo").osName;
        let filePath = res.tempFilePath;
        if (osType === "ios") {
          uni.openDocument({
            filePath: filePath,
            showMenu: true,
            success: res => {
              resolve(res);
            },
            fail: err => {
              console.log("uni.openDocument--fail");
              reject(err);
            },
          });
        } else {
          uni.saveFile({
            tempFilePath: filePath,
            success: fileRes => {
              uni.showToast({
                icon: "none",
                mask: true,
                title:
                  "文件已保存:Android/data/uni.UNI720216F/apps/__UNI__720216F/" +
                  fileRes.savedFilePath, //保存路径
                duration: 3000,
              });
              setTimeout(() => {
                //打开文档查看
                uni.openDocument({
                  filePath: fileRes.savedFilePath,
                  success: function (res) {
                    resolve(fileRes);
                  },
                });
              }, 3000);
            },
            fail: err => {
              console.log("uni.save--fail");
              reject(err);
            },
          });
        }
        // const isBlob = blobValidate(res.data);
        // if (isBlob) {
        //   const blob = new Blob([res.data], { type: "text/plain" });
        //   const url = URL.createObjectURL(blob);
        //   const downloadLink = document.getElementById("downloadLink");
        //   downloadLink.href = url;
        //   downloadLink.download = file.name;
        //   downloadLink.click();
        //   showToast("下载成功");
        // } else {
        //   showToast("下载失败");
        // }
      })
      .catch(err => {
        console.error("下载失败:", err);
        showToast("下载失败");
      });
  };
  // ç¡®è®¤åˆ é™¤
  const confirmDelete = (file, index) => {
    uni.showModal({
      title: "删除确认",
      content: `确定要删除附件 "${file.name}" å—?`,
      success: res => {
        if (res.confirm) {
          deleteFile(file.id, index);
        }
      },
    });
  };
  // åˆ é™¤æ–‡ä»¶
  const deleteFile = (fileId, index) => {
    uni.showLoading({
      title: "删除中...",
      mask: true,
    });
    safeTrainingFileDel([fileId])
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          // fileList.value.splice(index, 1);
          getFileList();
          showToast("删除成功");
        } else {
          showToast("删除失败");
        }
      })
      .catch(err => {
        uni.hideLoading();
        showToast("删除失败");
      });
  };
  // æ˜¾ç¤ºæç¤º
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  const rulesRegulationsManagementId = ref("");
  // é¡µé¢åŠ è½½æ—¶
  onMounted(() => {
    rulesRegulationsManagementId.value = uni.getStorageSync(
      "safetyTrainingFileId"
    );
    // ä»Ž API èŽ·å–é™„ä»¶åˆ—è¡¨
    getFileList();
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å– rulesRegulationsManagementId
  });
  // èŽ·å–é™„ä»¶åˆ—è¡¨
  const getFileList = () => {
    uni.showLoading({
      title: "加载中...",
      mask: true,
    });
    safeTrainingFileListPage({
      safeTrainingId: rulesRegulationsManagementId.value,
      current: -1,
      size: -1,
    })
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          fileList.value = res.data.records || [];
        } else {
          showToast("获取附件列表失败");
        }
      })
      .catch(err => {
        uni.hideLoading();
        showToast("获取附件列表失败");
      });
  };
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .file-list-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100rpx;
  }
  .file-list-container {
    padding: 20rpx;
  }
  .file-list {
    background: #ffffff;
    border-radius: 8rpx;
    overflow: hidden;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  }
  .file-item {
    display: flex;
    align-items: center;
    padding: 20rpx;
    border-bottom: 1rpx solid #f0f0f0;
    &:last-child {
      border-bottom: none;
    }
  }
  .file-icon {
    width: 56rpx;
    height: 56rpx;
    border-radius: 8rpx;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-right: 20rpx;
    &.blue {
      background: #409eff;
    }
    &.green {
      background: #67c23a;
    }
    &.red {
      background: #f56c6c;
    }
    &.orange {
      background: #e6a23c;
    }
    &.gray {
      background: #909399;
    }
    &.purple {
      background: #909399;
    }
    &.yellow {
      background: #e6a23c;
    }
  }
  .file-info {
    flex: 1;
    min-width: 0;
  }
  .file-name {
    display: block;
    font-size: 16px;
    color: #303133;
    margin-bottom: 8rpx;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .file-meta {
    display: block;
    font-size: 12px;
    color: #909399;
  }
  .file-actions {
    display: flex;
    gap: 12rpx;
  }
  .empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 100rpx 0;
    background: #ffffff;
    border-radius: 8rpx;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  }
  .empty-text {
    font-size: 14px;
    color: #909399;
    margin-top: 20rpx;
  }
  .upload-button {
    position: fixed;
    bottom: 40rpx;
    right: 40rpx;
    width: 130rpx;
    height: 130rpx;
    border-radius: 50%;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4);
    z-index: 1000;
  }
  .upload-text {
    font-size: 10px;
    color: #ffffff;
    margin-top: 4rpx;
  }
  .upload-progress {
    padding: 40rpx 0;
  }
  .upload-progress-text {
    display: block;
    text-align: center;
    margin-top: 20rpx;
    font-size: 14px;
    color: #606266;
  }
</style>
src/pages/safeProduction/safetyTrainingAssessment/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,448 @@
<template>
  <view class="sales-accoun">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="安全培训评估"
                @back="goBack" />
    <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view @click="selectDate"
              class="search-input">
          <view class="search-text">{{ searchKeyword? searchKeyword : '请选择培训日期' }}</view>
        </view>
        <view class="filter-button"
              @click="clearDate">
          <u-icon name="close-circle"
                  size="24"
                  color="#999"></u-icon>
        </view>
      </view>
      <!-- åŸ¹è®­è®°å½•按钮 -->
      <view class="record-button">
        <u-button type="info"
                  @click="viewTrainingRecord">培训记录</u-button>
      </view>
    </view>
    <!-- æ ‡ç­¾é¡µ -->
    <view class="tabs-section">
      <up-tabs v-model="searchForm.state"
               :list="tabList"
               itemStyle="width: 33%;height: 80rpx;"
               @change="tabhandleQuery">
      </up-tabs>
    </view>
    <!-- åŸ¹è®­è®°å½•列表 -->
    <view class="ledger-list"
          v-if="trainingList.length > 0">
      <view v-for="(item, index) in trainingList"
            :key="index">
        <view class="ledger-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="file-text"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">课程编号:{{ item.courseCode }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">课程编号</text>
              <text class="detail-value">{{ item.courseCode || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">培训日期</text>
              <text class="detail-value">{{ item.trainingDate || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">开始时间</text>
              <text class="detail-value">{{ item.openingTime || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">结束时间</text>
              <text class="detail-value">{{ item.endTime || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">培训目标</text>
              <text class="detail-value">{{ item.trainingObjectives || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">参加对象</text>
              <text class="detail-value">{{ item.participants || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">培训内容</text>
              <text class="detail-value">{{ item.trainingContent || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">培训讲师</text>
              <text class="detail-value">{{ item.trainingLecturer || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">项目学分</text>
              <text class="detail-value">{{ item.projectCredits || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">培训方式</text>
              <text class="detail-value">{{ getTrainingModeLabel(item.trainingMode) || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">培训地点</text>
              <text class="detail-value">{{ item.placeTraining || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">课时</text>
              <text class="detail-value">{{ item.classHour || '-' }}</text>
            </view>
          </view>
          <!-- æŒ‰é’®åŒºåŸŸ -->
          <view class="action-buttons">
            <u-button type="primary"
                      size="small"
                      class="action-btn"
                      :disabled="item.state !== 0"
                      @click="editVisit(item)">
              ç¼–辑
            </u-button>
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click="viewFileList(item)">
              é™„ä»¶
            </u-button>
            <u-button type="error"
                      size="small"
                      class="action-btn"
                      @click="deleteVisit(item)">
              åˆ é™¤
            </u-button>
          </view>
          <view class="action-buttons">
            <u-button type="warning"
                      size="small"
                      class="action-btn"
                      :disabled="item.state !== 1"
                      @click="signIn(item)">
              ç­¾åˆ°
            </u-button>
            <u-button type="success"
                      size="small"
                      class="action-btn"
                      :disabled="item.state === 0"
                      @click="viewResultDetail(item)">
              ç»“果明细
            </u-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else
          class="no-data">
      <text>暂无培训记录</text>
    </view>
    <up-datetime-picker :show="trainingDateVisible"
                        mode="date"
                        @confirm="handleDateConfirm"
                        @cancel="handleDateCancel"
                        title="选择培训日期" />
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <view class="fab-button"
          @click="addVisit">
      <up-icon name="plus"
               size="24"
               color="#ffffff"></up-icon>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted, reactive } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import {
    safeTrainingListPage,
    safeTrainingDel,
    safeTrainingSign,
    safeTrainingGet,
  } from "@/api/safeProduction/safetyTrainingAssessment";
  import useUserStore from "@/store/modules/user";
  import { useDict } from "@/utils/dict";
  import dayjs from "dayjs";
  // æ›¿æ¢ toast æ–¹æ³•
  defineOptions({ name: "safety-training-index" });
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  const userStore = useUserStore();
  // èŽ·å–å­—å…¸æ•°æ®
  const { safe_training_methods } = useDict("safe_training_methods");
  // æœç´¢å…³é”®è¯
  const searchKeyword = ref("");
  // æ—¥æœŸé€‰æ‹©å™¨çŠ¶æ€
  const trainingDateVisible = ref(false);
  const tabList = reactive([
    { name: "未开始", value: 0 },
    { name: "进行中", value: 1 },
    { name: "已结束", value: 2 },
  ]);
  // æœç´¢è¡¨å•
  const searchForm = ref({
    state: 0, // é»˜è®¤æ˜¾ç¤ºå·²ç»“束
    trainingDate: "",
  });
  const tabhandleQuery = val => {
    searchForm.value.state = val.value;
    getList();
  };
  const getTrainingModeLabel = mode => {
    if (!safe_training_methods || !Array.isArray(safe_training_methods.value)) {
      return mode || "-";
    }
    const dictItem = safe_training_methods.value.find(
      item => String(item.value) === String(mode)
    );
    return dictItem ? dictItem.label : mode || "-";
  };
  // åŸ¹è®­è®°å½•数据
  const trainingList = ref([]);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  const viewFileList = item => {
    uni.setStorageSync("safetyTrainingFileId", item.id);
    uni.navigateTo({
      url: "/pages/safeProduction/safetyTrainingAssessment/fileList",
    });
  };
  const currentUserId = ref("");
  // ç­¾åˆ°åŠŸèƒ½
  const signIn = item => {
    uni.showModal({
      title: "提示",
      content: "确认签到吗?",
      success: function (res) {
        if (res.confirm) {
          safeTrainingSign({
            safeTrainingId: item.id,
            userId: currentUserId.value,
          })
            .then(res => {
              if (res.code === 200) {
                uni.showToast({ title: "签到成功", icon: "success" });
                setTimeout(() => {}, 1000);
              } else {
                uni.showToast({ title: res.msg || "签到失败", icon: "none" });
              }
            })
            .catch(() => {
              uni.showToast({ title: "签到失败,请重试", icon: "none" });
            });
        }
      },
    });
  };
  // æŸ¥çœ‹ç»“果明细
  const viewResultDetail = item => {
    uni.setStorageSync("safetyTrainingResultId", item.id);
    uni.setStorageSync("safetyTrainingResultNums", item.nums);
    uni.navigateTo({
      url: "/pages/safeProduction/safetyTrainingAssessment/resultDetail",
    });
  };
  // æŸ¥çœ‹åŸ¹è®­è®°å½•
  const viewTrainingRecord = () => {
    uni.navigateTo({
      url: "/pages/safeProduction/safetyTrainingAssessment/record",
    });
  };
  // æ¸…除日期选择
  const clearDate = () => {
    searchKeyword.value = "";
    searchForm.value.trainingDate = "";
    getList();
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const selectDate = () => {
    trainingDateVisible.value = true;
  };
  // å¤„理日期选择确认
  const handleDateConfirm = e => {
    searchKeyword.value = dayjs(e.value).format("YYYY-MM-DD");
    searchForm.value.trainingDate = dayjs(e.value).format("YYYY-MM-DD");
    trainingDateVisible.value = false;
    getList();
  };
  // å¤„理日期选择取消
  const handleDateCancel = () => {
    trainingDateVisible.value = false;
  };
  // æŸ¥è¯¢åˆ—表
  const getList = () => {
    showLoadingToast("加载中...");
    const params = {
      current: -1,
      size: -1,
      trainingDate: searchForm.value.trainingDate,
      state: searchForm.value.state,
    };
    safeTrainingListPage(params)
      .then(res => {
        trainingList.value = res.records || res.data?.records || [];
        closeToast();
      })
      .catch(() => {
        closeToast();
        showToast("获取数据失败");
      });
  };
  // æ˜¾ç¤ºåŠ è½½æç¤º
  const showLoadingToast = message => {
    uni.showLoading({
      title: message,
      mask: true,
    });
  };
  // å…³é—­æç¤º
  const closeToast = () => {
    uni.hideLoading();
  };
  // æ–°å¢žåŸ¹è®­
  const addVisit = () => {
    uni.setStorageSync("safetyTraining", {});
    uni.navigateTo({
      url: "/pages/safeProduction/safetyTrainingAssessment/detail",
    });
  };
  // ç¼–辑培训
  const editVisit = item => {
    uni.setStorageSync("safetyTraining", item);
    uni.navigateTo({
      url: "/pages/safeProduction/safetyTrainingAssessment/detail",
    });
  };
  // åˆ é™¤åŸ¹è®­
  const deleteVisit = item => {
    uni.showModal({
      title: "删除确认",
      content: `确定要删除该培训记录吗?`,
      success: res => {
        if (res.confirm) {
          deleteClientVisit(item.id);
        }
      },
    });
  };
  // åˆ é™¤åŸ¹è®­è®°å½•
  const deleteClientVisit = id => {
    showLoadingToast("删除中...");
    safeTrainingDel([id])
      .then(() => {
        closeToast();
        showToast("删除成功");
        getList();
      })
      .catch(() => {
        closeToast();
        showToast("删除失败");
      });
  };
  // æŸ¥çœ‹è¯¦æƒ…
  const viewDetail = item => {
    uni.setStorageSync("safetyTraining", item);
    uni.navigateTo({
      url: "/pages/safeProduction/safetyTrainingAssessment/view",
    });
  };
  onMounted(() => {
    userStore.getInfo().then(res => {
      currentUserId.value = res.user.userId;
    });
    // currentUserId
    getList();
  });
  onShow(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  // é¡µé¢ç‰¹å®šçš„æ ·å¼è¦†ç›–
  .sales-accoun {
    min-height: 100vh;
    background: #f8f9fa;
    position: relative;
    padding-bottom: 80px;
  }
  // åŸ¹è®­è®°å½•按钮
  .record-button {
    margin-top: 10px;
    text-align: center;
  }
  // ç‰¹å®šçš„图标样式
  .document-icon {
    background: #667eea; // ä¿æŒé¡µé¢ç‰¹æœ‰çš„背景色
  }
  // ç‰¹æœ‰æ ·å¼
  .visit-status {
    display: flex;
    align-items: center;
  }
  .detail-value {
    word-break: break-all; // ä¿ç•™é¡µé¢ç‰¹æœ‰çš„æ–‡æœ¬æ¢è¡Œæ ·å¼
  }
  // ç‰¹å®šçš„æµ®åŠ¨æŒ‰é’®æ ·å¼
  .fab-button {
    background: #667eea; // ä¿æŒé¡µé¢ç‰¹æœ‰çš„背景色
    box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3); // ä¿æŒé¡µé¢ç‰¹æœ‰çš„阴影效果
  }
  .action-buttons {
    gap: 4px;
  }
  .action-buttons {
    padding: 0 0 10rpx 0;
  }
  .tabs-section {
    background: #fff;
    margin-bottom: 1rem;
    box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.05);
  }
  .search-text {
    // font-size: 24rpx;
    color: #a6a6a6;
    height: 70rpx;
    line-height: 70rpx;
    margin-left: 20rpx;
  }
</style>
src/pages/safeProduction/safetyTrainingAssessment/record.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,546 @@
<template>
  <view class="training-record">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="培训记录"
                @back="goBack" />
    <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="人员名称搜索"
                    v-model="searchForm.searchText"
                    @change="searchName"
                    clearable />
        </view>
        <view class="filter-button"
              @click="searchName">
          <u-icon name="search"
                  size="24"
                  color="#999"></u-icon>
        </view>
      </view>
    </view>
    <!-- äººå‘˜å¡ç‰‡åˆ—表 -->
    <view class="user-card-list"
          v-if="userList.length > 0">
      <view v-for="(user, index) in userList"
            :key="index"
            class="user-card">
        <!-- å¡ç‰‡å¤´éƒ¨ -->
        <view class="card-header"
              @click="toggleUserCard(index)">
          <view class="header-left">
            <text class="user-name">{{ user.nickName }}</text>
            <text class="user-dept">所属:{{ user.deptNames || '-' }}</text>
            <text class="user-dept">联系方式:{{ user.phonenumber || '-' }}</text>
            <!-- åŸ¹è®­ç»Ÿè®¡ä¿¡æ¯ -->
          </view>
          <u-icon :name="expandedUsers[index] ? 'arrow-up' : 'arrow-down'"
                  size="20"
                  color="#999"></u-icon>
        </view>
        <!-- å¡ç‰‡å†…容(折叠部分) -->
        <view class="card-content"
              v-if="expandedUsers[index]">
          <!-- å¹´ä»½ç­›é€‰ -->
          <view class="year-filter-section">
            <!-- <text class="filter-label">年份筛选</text> -->
            <view class="year-options">
              <u-tag v-for="year in yearOptions"
                     :key="year"
                     :text="year"
                     :type="userYearFilters[user.userId] === year.toString() ? 'primary' : 'info'"
                     @click="() => {
                       userYearFilters[user.userId] = year.toString();
                       filterUserCourses(user.userId);
                     }"
                     :class="{ active: userYearFilters[user.userId] === year.toString() }"
                     style="margin-right: 8px; margin-bottom: 8px;"></u-tag>
            </view>
          </view>
          <!-- åŸ¹è®­è¯¾ç¨‹åˆ—表 -->
          <view class="course-list"
                v-if="userCourses[user.userId] && userCourses[user.userId].length > 0">
            <view class="user-stats"
                  v-if="userStats[user.userId]">
              <text class="stat-item">培训次数: {{ userStats[user.userId].total }}</text>
              <text class="stat-item success">合格: {{ userStats[user.userId].qualified }}</text>
              <text class="stat-item danger">不合格: {{ userStats[user.userId].unqualified }}</text>
            </view>
            <view v-for="(course, courseIndex) in userCourses[user.userId]"
                  :key="courseIndex">
              <view class="course-item"
                    v-if="userYearFilters[user.userId] === '全部' || course.trainingDate.includes(userYearFilters[user.userId])">
                <view class="course-header">
                  <text class="course-date">{{ course.trainingDate || '-' }}</text>
                  <u-tag :type="course.examinationResults === '合格' ? 'success' : 'error'">
                    {{ course.examinationResults || '-' }}
                  </u-tag>
                </view>
                <view class="course-info">
                  <text class="info-label">培训内容:</text>
                  <text class="info-value">{{ course.trainingContent || '-' }}</text>
                </view>
                <view class="course-info">
                  <text class="info-label">培训课时:</text>
                  <text class="info-value">{{ course.classHour || '-' }}</text>
                </view>
              </view>
            </view>
            <!-- ç­›é€‰åŽæ— æ•°æ® -->
            <view v-if="userYearFilters[user.userId] === '全部' ? userCourses[user.userId].length === 0 : userCourses[user.userId].filter(c => c.trainingDate.includes(userYearFilters[user.userId])).length === 0"
                  class="empty-course">
              <text class="empty-text">{{ userYearFilters[user.userId] === '全部' ? '暂无培训记录' : '该年份暂无培训记录' }}</text>
            </view>
          </view>
          <!-- ç©ºçŠ¶æ€ -->
          <view v-else
                class="empty-course">
            <text class="empty-text">暂无培训记录</text>
          </view>
        </view>
        <!-- å¯¼å‡ºæŒ‰é’® -->
        <!-- <view class="course-export">
          <u-button type="primary"
                    size="small"
                    @click="exportUserRecord(user.userId)">导出记录</u-button>
        </view> -->
      </view>
    </view>
    <view v-else
          class="empty-state">
      <up-icon name="people"
               size="64"
               color="#c0c4cc"></up-icon>
      <text class="empty-text">暂无人员数据</text>
    </view>
    <!-- ç©ºçŠ¶æ€ -->
    <!-- å¹´ä»½é€‰æ‹©å™¨ -->
    <up-datetime-picker :show="yearPickerVisible"
                        mode="year"
                        @confirm="handleYearConfirm"
                        @cancel="yearPickerVisible = false"
                        title="选择年份" />
  </view>
</template>
<script setup>
  import { ref, onMounted, reactive } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import { safeTrainingDetailListPage } from "@/api/safeProduction/safetyTrainingAssessment";
  import { userListNoPage } from "@/api/system/user.js";
  import { getToken } from "@/utils/auth";
  import config from "@/config";
  // é¡µé¢çŠ¶æ€
  const userList = ref([]);
  const userCourses = ref({}); // å­˜å‚¨æ¯ä¸ªç”¨æˆ·çš„培训课程
  const userStats = ref({}); // å­˜å‚¨æ¯ä¸ªç”¨æˆ·çš„培训统计信息
  const expandedUsers = ref([]); // æŽ§åˆ¶ç”¨æˆ·å¡ç‰‡å±•开状态
  const loading = ref(false);
  const courseLoading = ref({}); // æŽ§åˆ¶æ¯ä¸ªç”¨æˆ·çš„课程加载状态
  const userYearFilters = ref({}); // å­˜å‚¨æ¯ä¸ªç”¨æˆ·çš„年份筛选条件
  const yearOptions = ref([]); // å¹´ä»½é€‰é¡¹ï¼ˆä»Šå¹´å’Œè¿‡åŽ»ä¸‰å¹´ï¼‰
  // æœç´¢è¡¨å•
  const searchForm = reactive({
    searchText: "",
    invoiceDate: "",
  });
  // å¹´ä»½é€‰æ‹©å™¨çŠ¶æ€
  const yearPickerVisible = ref(false);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // ç”Ÿæˆå¹´ä»½é€‰é¡¹
  const generateYearOptions = () => {
    const currentYear = new Date().getFullYear();
    const options = [];
    // æ·»åŠ "全部"选项
    options.push("全部");
    for (let i = 0; i < 4; i++) {
      options.push(currentYear - i);
    }
    yearOptions.value = options;
  };
  // æœç´¢äººå‘˜åç§°
  const searchName = () => {
    getUserList();
  };
  // æ˜¾ç¤ºå¹´ä»½é€‰æ‹©å™¨
  const showYearPicker = () => {
    yearPickerVisible.value = true;
  };
  // å¤„理年份选择确认
  const handleYearConfirm = e => {
    searchForm.invoiceDate = e.value;
    yearPickerVisible.value = false;
  };
  // æŒ‰å¹´ä»½æœç´¢
  const searchDate = () => {
    // éåŽ†æ‰€æœ‰å±•å¼€çš„ç”¨æˆ·å¡ç‰‡ï¼Œé‡æ–°åŠ è½½åŸ¹è®­è®°å½•
    userList.value.forEach((user, index) => {
      if (expandedUsers.value[index]) {
        getUserCourses(user.userId, index);
      }
    });
  };
  // èŽ·å–äººå‘˜åˆ—è¡¨
  const getUserList = () => {
    loading.value = true;
    userListNoPage()
      .then(res => {
        loading.value = false;
        if (res.data && res.data.length > 0) {
          let users = res.data;
          // å¦‚果有搜索关键词,进行筛选
          if (searchForm.searchText) {
            users = users.filter(user =>
              user.nickName.includes(searchForm.searchText)
            );
          }
          userList.value = users;
          // åˆå§‹åŒ–所有卡片为收起状态
          expandedUsers.value = new Array(userList.value.length).fill(false);
        } else {
          userList.value = [];
          expandedUsers.value = [];
        }
      })
      .catch(() => {
        loading.value = false;
        uni.showToast({ title: "获取人员列表失败", icon: "none" });
      });
  };
  // åˆ‡æ¢ç”¨æˆ·å¡ç‰‡å±•开状态
  const toggleUserCard = index => {
    const user = userList.value[index];
    expandedUsers.value[index] = !expandedUsers.value[index];
    // å¦‚果展开卡片,加载培训记录
    if (expandedUsers.value[index]) {
      // åˆå§‹åŒ–年份筛选条件为"全部"
      userYearFilters.value[user.userId] = "全部";
      getUserCourses(user.userId, index);
    }
  };
  // ç­›é€‰ç”¨æˆ·åŸ¹è®­è¯¾ç¨‹
  const filterUserCourses = userId => {
    if (!userYearFilters.value[userId]) return;
    console.log("userYearFilters", userYearFilters.value);
    const year = userYearFilters.value[userId];
    const allCourses = userCourses.value[userId] || [];
    // ç­›é€‰æŒ‡å®šå¹´ä»½çš„培训记录
    let filteredCourses = allCourses;
    if (year !== "全部") {
      filteredCourses = allCourses.filter(
        course => course.trainingDate && course.trainingDate.includes(year)
      );
    }
    console.log("filteredCourses", filteredCourses);
    // æ›´æ–°ç»Ÿè®¡ä¿¡æ¯
    const total = filteredCourses.length;
    const qualified = filteredCourses.filter(
      course => course.examinationResults === "合格"
    ).length;
    const unqualified = filteredCourses.filter(
      course => course.examinationResults === "不合格"
    ).length;
    userStats.value[userId] = {
      total,
      qualified,
      unqualified,
    };
  };
  // èŽ·å–ç”¨æˆ·åŸ¹è®­è¯¾ç¨‹
  const getUserCourses = (userId, index) => {
    courseLoading.value[userId] = true;
    const params = {
      userId,
      // å¦‚果有年份筛选,添加年份参数
      // è¿™é‡Œéœ€è¦æ ¹æ®åŽç«¯æŽ¥å£çš„实际参数名进行调整
    };
    safeTrainingDetailListPage(params)
      .then(res => {
        courseLoading.value[userId] = false;
        if (res.data && res.data.records) {
          let courses = res.data.records;
          // å¦‚果有年份筛选,进行筛选
          if (searchForm.invoiceDate) {
            const year = searchForm.invoiceDate.substring(0, 4);
            courses = courses.filter(
              course => course.trainingDate && course.trainingDate.includes(year)
            );
          }
          userCourses.value[userId] = courses;
          // è®¡ç®—培训统计信息
          const total = courses.length;
          const qualified = courses.filter(
            course => course.examinationResults === "合格"
          ).length;
          const unqualified = courses.filter(
            course => course.examinationResults === "不合格"
          ).length;
          userStats.value[userId] = {
            total,
            qualified,
            unqualified,
          };
        } else {
          userCourses.value[userId] = [];
          userStats.value[userId] = {
            total: 0,
            qualified: 0,
            unqualified: 0,
          };
        }
      })
      .catch(() => {
        courseLoading.value[userId] = false;
        uni.showToast({ title: "获取培训记录失败", icon: "none" });
      });
  };
  // é¡µé¢åŠ è½½
  onMounted(() => {
    // ç”Ÿæˆå¹´ä»½é€‰é¡¹
    generateYearOptions();
    getUserList();
  });
  // é¡µé¢æ˜¾ç¤ºæ—¶åˆ·æ–°
  onShow(() => {
    // å¯ä»¥åœ¨è¿™é‡Œæ·»åŠ åˆ·æ–°é€»è¾‘
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .training-record {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 20px;
  }
  // å¹´ä»½é€‰æ‹©å™¨
  .year-picker {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 15px;
    background: #f8f9fa;
    border-radius: 4px;
    margin-right: 10px;
  }
  .picker-text {
    font-size: 14px;
    color: #333;
  }
  // äººå‘˜å¡ç‰‡åˆ—表
  .user-card-list {
    padding: 10px;
  }
  // äººå‘˜å¡ç‰‡
  .user-card {
    background: #fff;
    border-radius: 8px;
    margin-bottom: 12px;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
    overflow: hidden;
    border: 1px solid #e8e8e8;
  }
  // å¡ç‰‡å¤´éƒ¨
  .card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 16px;
    background: #f5f7fa;
    border-bottom: 1px solid #e8e8e8;
    cursor: pointer;
  }
  .header-left {
    display: flex;
    flex-direction: column;
  }
  .user-name {
    font-size: 16px;
    font-weight: 600;
    color: #333;
    margin-bottom: 20rpx;
  }
  .user-dept {
    font-size: 14px;
    color: #666;
    margin-bottom: 8px;
  }
  // åŸ¹è®­ç»Ÿè®¡ä¿¡æ¯
  .user-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-bottom: 8px;
  }
  .stat-item {
    font-size: 12px;
    color: #555;
    padding: 5px 10px;
    background: #f0f2f5;
    border-radius: 4px;
    border: 1px solid #e0e0e0;
    font-weight: 500;
  }
  .stat-item.success {
    background: #e6f7ff;
    color: #1890ff;
    border-color: #91d5ff;
  }
  .stat-item.danger {
    background: #fff1f0;
    color: #ff4d4f;
    border-color: #ffccc7;
  }
  // å¹´ä»½ç­›é€‰åŒºåŸŸ
  .year-filter-section {
    margin-bottom: 16px;
    padding-bottom: 12px;
    border-bottom: 1px solid #e8e8e8;
  }
  .filter-label {
    font-size: 14px;
    font-weight: 500;
    color: #333;
    margin-bottom: 8px;
    display: block;
  }
  .year-options {
    display: flex;
    flex-wrap: wrap;
  }
  // å¡ç‰‡å†…容
  .card-content {
    padding: 16px;
  }
  // åŸ¹è®­è¯¾ç¨‹åˆ—表
  .course-list {
    margin-bottom: 16px;
  }
  // è¯¾ç¨‹é¡¹
  .course-item {
    background: #fafafa;
    border-radius: 6px;
    padding: 14px;
    margin-bottom: 12px;
    border: 1px solid #e8e8e8;
  }
  // è¯¾ç¨‹å¤´éƒ¨
  .course-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
    padding-bottom: 8px;
    border-bottom: 1px solid #f0f0f0;
  }
  .course-date {
    font-size: 14px;
    font-weight: 600;
    color: #333;
  }
  // è¯¾ç¨‹ä¿¡æ¯
  .course-info {
    display: flex;
    margin-bottom: 8px;
    align-items: flex-start;
  }
  .info-label {
    font-size: 14px;
    color: #666;
    width: 80px;
    flex-shrink: 0;
  }
  .info-value {
    font-size: 14px;
    color: #333;
    flex: 1;
    line-height: 1.4;
  }
  // è¯¾ç¨‹å¯¼å‡ºæŒ‰é’®
  .course-export {
    display: flex;
    justify-content: flex-end;
  }
  // ç©ºçŠ¶æ€
  .empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 60px 0;
  }
  .empty-course {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 30px 0;
    color: #999;
    font-size: 14px;
  }
  .empty-text {
    font-size: 14px;
    color: #999;
    margin-top: 16px;
  }
  :deep(.u-tag--info) {
    background-color: #c1c3c8;
    border-width: 1px;
    border-color: #c1c3c8;
  }
</style>
src/pages/safeProduction/safetyTrainingAssessment/resultDetail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,388 @@
<template>
  <view class="result-detail">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="结果明细"
                @back="goBack" />
    <!-- å†…容区域 -->
    <view class="content">
      <!-- è¯¾ç¨‹è¯¦æƒ… -->
      <view class="section">
        <view class="section-title">课程详情</view>
        <view class="info-list">
          <view class="info-item">
            <text class="info-label">课程编号</text>
            <text class="info-value">{{ currentTraining.courseCode || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">培训内容</text>
            <text class="info-value">{{ currentTraining.trainingContent || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">状态</text>
            <text class="info-value">
              <u-tag :type="currentTraining.state === 0 ? 'success' : (currentTraining.state === 1 ? 'warning' : 'info')">
                {{ currentTraining.state === 0 ? '未开始' : (currentTraining.state === 1 ? '进行中' : '已结束') }}
              </u-tag>
            </text>
          </view>
          <view class="info-item">
            <text class="info-label">培训讲师</text>
            <text class="info-value">{{ currentTraining.trainingLecturer || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">培训开始时间</text>
            <text class="info-value">{{ currentTraining.trainingDate + ' ' + currentTraining.openingTime || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">培训结束时间</text>
            <text class="info-value">{{ currentTraining.trainingDate + ' ' + currentTraining.endTime || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">培训目标</text>
            <text class="info-value">{{ currentTraining.trainingObjectives || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">参加对象</text>
            <text class="info-value">{{ currentTraining.participants || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">培训方式</text>
            <text class="info-value">{{ getTrainingModeLabel(currentTraining.trainingMode) || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">培训地点</text>
            <text class="info-value">{{ currentTraining.placeTraining || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">课时</text>
            <text class="info-value">{{ currentTraining.classHour || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">课程学分</text>
            <text class="info-value">{{ currentTraining.projectCredits || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">报名人数</text>
            <text class="info-value">{{ currentTraining.nums || '-' }}</text>
          </view>
        </view>
      </view>
      <!-- è¯¾ç¨‹è¯„ä»· -->
      <view class="section">
        <view class="section-title">课程评价</view>
        <u-form ref="formRef"
                label-width="90"
                :model="endform">
          <u-form-item label="评价人">
            <u-input v-model="endform.assessmentUserName"
                     disabled
                     placeholder="请选择评价人" />
          </u-form-item>
          <u-form-item label="评价时间">
            <u-input v-model="endform.assessmentDate"
                     disabled
                     placeholder="请选择评价时间" />
          </u-form-item>
          <u-form-item label="考核方式">
            <u-input v-model="endform.assessmentMethod"
                     placeholder="请输入考核方式" />
          </u-form-item>
          <u-form-item label="综合评价">
            <u-input v-model="endform.comprehensiveAssessment"
                     placeholder="请输入本次课程综合评价" />
          </u-form-item>
          <u-form-item label="培训摘要">
            <u-textarea v-model="endform.trainingAbstract"
                        :rows="4"
                        placeholder="请输入培训摘要" />
          </u-form-item>
        </u-form>
      </view>
      <!-- è€ƒæ ¸åˆ—表 -->
      <view class="section">
        <view class="section-title">考核列表</view>
        <view class="assessment-list">
          <view v-for="(item, index) in endform.safeTrainingDetailsDtoList"
                :key="index"
                class="assessment-item">
            <view class="assessment-info">
              <view class="info-row">
                <text class="label">姓名:</text>
                <text class="value">{{ item.nickName || '-' }}</text>
              </view>
              <view class="info-row">
                <text class="label">电话号码:</text>
                <text class="value">{{ item.phonenumber || '-' }}</text>
              </view>
            </view>
            <view class="assessment-result">
              <text class="result-label">考核结果:</text>
              <u-radio-group v-model="endform.safeTrainingDetailsDtoList[index].examinationResults"
                             :key="index"
                             size="default">
                <u-radio label="合格"
                         name="合格"></u-radio>
                <u-radio label="不合格"
                         name="不合格"></u-radio>
              </u-radio-group>
            </view>
          </view>
        </view>
      </view>
      <!-- æäº¤æŒ‰é’® -->
      <view class="submit-btn">
        <u-button type="primary"
                  @click="submitForm"
                  :loading="loading">提交</u-button>
      </view>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import { onLoad } from "@dcloudio/uni-app";
  import { useDict } from "@/utils/dict";
  import dayjs from "dayjs";
  import useUserStore from "@/store/modules/user";
  import {
    safeTrainingGet,
    safeTrainingSave,
  } from "@/api/safeProduction/safetyTrainingAssessment";
  // èŽ·å–å­—å…¸æ•°æ®
  const { safe_training_methods } = useDict("safe_training_methods");
  // é¡µé¢çŠ¶æ€
  const loading = ref(false);
  const currentTraining = ref({});
  const endform = ref({
    assessmentUserId: "",
    assessmentUserName: "",
    assessmentMethod: "",
    assessmentDate: "",
    comprehensiveAssessment: "",
    trainingAbstract: "",
    safeTrainingFileList: [],
    safeTrainingDetailsDtoList: [],
  });
  // èŽ·å–åŸ¹è®­æ–¹å¼æ ‡ç­¾
  const getTrainingModeLabel = val => {
    if (!safe_training_methods || !Array.isArray(safe_training_methods.value)) {
      return val;
    }
    const item = safe_training_methods.value.find(
      i => String(i.value) === String(val)
    );
    return item ? item.label : val;
  };
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æäº¤è¡¨å•
  const submitForm = () => {
    // éªŒè¯è€ƒæ ¸ç»“æžœ
    for (let i = 0; i < endform.value.safeTrainingDetailsDtoList.length; i++) {
      const item = endform.value.safeTrainingDetailsDtoList[i];
      if (!item.examinationResults) {
        uni.showToast({
          title: `请选择${item.nickName}的考核结果`,
          icon: "none",
        });
        return;
      }
    }
    loading.value = true;
    safeTrainingSave(endform.value)
      .then(res => {
        loading.value = false;
        if (res.code === 200) {
          uni.showToast({ title: "提交成功", icon: "success" });
          setTimeout(() => {
            goBack();
          }, 500);
        } else {
          uni.showToast({ title: res.msg || "提交失败", icon: "none" });
        }
      })
      .catch(() => {
        loading.value = false;
        uni.showToast({ title: "提交失败,请重试", icon: "none" });
      });
  };
  // é¡µé¢åŠ è½½
  onLoad(() => {
    const trainingId = uni.getStorageSync("safetyTrainingResultId");
    const trainingNums = uni.getStorageSync("safetyTrainingResultNums");
    if (trainingId) {
      getTrainingDetail(trainingId, trainingNums);
    }
  });
  const userStore = useUserStore();
  const getuserInfo = () => {
    const userInfo = {
      id: "",
      nickName: "",
    };
    userStore.getInfo().then(res => {
      userInfo.id = res.user.userId;
      userInfo.nickName = res.user.nickName;
      endform.value.assessmentUserName = res.user.nickName;
      endform.value.assessmentUserId = res.user.userId;
    });
    return userInfo;
  };
  // èŽ·å–åŸ¹è®­è¯¦æƒ…
  const getTrainingDetail = (id, trainingNums) => {
    loading.value = true;
    safeTrainingGet({ id })
      .then(res => {
        loading.value = false;
        if (res.code === 200) {
          currentTraining.value = res.data;
          currentTraining.value.nums = trainingNums;
          endform.value = { ...res.data };
          // è®¾ç½®é»˜è®¤å€¼
          if (!endform.value.assessmentUserId) {
            getuserInfo();
          }
          endform.value.assessmentDate = endform.value.assessmentDate
            ? dayjs(endform.value.assessmentDate).format("YYYY-MM-DD")
            : dayjs().format("YYYY-MM-DD");
          endform.value.safeTrainingDetailsDtoList =
            endform.value.safeTrainingDetailsDtoList || [];
        } else {
          uni.showToast({ title: "获取详情失败", icon: "none" });
        }
      })
      .catch(() => {
        loading.value = false;
        uni.showToast({ title: "获取详情失败", icon: "none" });
      });
  };
</script>
<style scoped lang="scss">
  .result-detail {
    min-height: 100vh;
    background: #f8f9fa;
  }
  .content {
    padding: 16px;
  }
  .section {
    background: #fff;
    border-radius: 8px;
    padding: 16px;
    margin-bottom: 16px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  }
  .section-title {
    font-size: 16px;
    font-weight: 600;
    margin-bottom: 16px;
    color: #333;
  }
  .info-list {
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  .info-item {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    padding-bottom: 12px;
    border-bottom: 1px solid #f0f0f0;
  }
  .info-item:last-child {
    border-bottom: none;
    padding-bottom: 0;
  }
  .info-label {
    font-size: 14px;
    color: #666;
    width: 100px;
  }
  .info-value {
    flex: 1;
    font-size: 14px;
    color: #333;
    text-align: right;
  }
  .assessment-list {
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  .assessment-item {
    background: #f8f9fa;
    border-radius: 8px;
    padding: 16px;
    border: 1px solid #e8e8e8;
  }
  .assessment-info {
    margin-bottom: 12px;
  }
  .info-row {
    display: flex;
    align-items: center;
    margin-bottom: 8px;
  }
  .info-row:last-child {
    margin-bottom: 0;
  }
  .label {
    font-size: 14px;
    color: #666;
    min-width: 80px;
  }
  .value {
    font-size: 14px;
    color: #333;
    flex: 1;
  }
  .assessment-result {
    display: flex;
    align-items: center;
    padding-top: 8px;
    border-top: 1px solid #e8e8e8;
  }
  .result-label {
    font-size: 14px;
    color: #666;
    min-width: 80px;
  }
  .submit-btn {
    margin-top: 24px;
    margin-bottom: 32px;
  }
</style>
src/pages/safeProduction/safetyTrainingAssessment/view.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,171 @@
<template>
  <view class="danger-investigation-view">
    <PageHeader title="培训详情"
                @back="goBack" />
    <!-- å†…容容器 -->
    <view class="detail-content">
      <!-- åŸ¹è®­ä¿¡æ¯ -->
      <view class="info-section">
        <!-- <view class="section-title">培训信息</view> -->
        <view class="info-grid">
          <view class="info-item">
            <text class="info-label">课程编号</text>
            <text class="info-value">{{ form.courseCode || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">培训日期</text>
            <text class="info-value">{{ form.trainingDate || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">开始时间</text>
            <text class="info-value">{{ form.openingTime || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">结束时间</text>
            <text class="info-value">{{ form.endTime || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">培训目标</text>
            <text class="info-value">{{ form.trainingObjectives || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">参加对象</text>
            <text class="info-value">{{ form.participants || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">培训内容</text>
            <text class="info-value">{{ form.trainingContent || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">培训讲师</text>
            <text class="info-value">{{ form.trainingLecturer || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">项目学分</text>
            <text class="info-value">{{ form.projectCredits || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">培训方式</text>
            <text class="info-value">{{ getTrainingModeLabel(form.trainingMode) || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">培训地点</text>
            <text class="info-value">{{ form.placeTraining || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">课时</text>
            <text class="info-value">{{ form.classHour || '-' }}</text>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import { onLoad } from "@dcloudio/uni-app";
  import { useDict } from "@/utils/dict";
  // æ›¿æ¢ toast æ–¹æ³•
  defineOptions({ name: "safety-training-view" });
  const showToast = message => {
    uni.showToast({ title: message, icon: "none" });
  };
  // èŽ·å–å­—å…¸æ•°æ®
  const { safe_training_methods } = useDict("safe_training_methods");
  // èŽ·å–åŸ¹è®­æ–¹å¼æ ‡ç­¾
  const getTrainingModeLabel = val => {
    if (!safe_training_methods || !Array.isArray(safe_training_methods.value)) {
      return val;
    }
    const item = safe_training_methods.value.find(
      i => String(i.value) === String(val)
    );
    return item ? item.label : val;
  };
  // åŸ¹è®­ä¿¡æ¯
  const form = ref({});
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  onLoad(() => {
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–åŸ¹è®­ä¿¡æ¯
    const safetyTraining = uni.getStorageSync("safetyTraining");
    if (safetyTraining) {
      form.value = safetyTraining;
    }
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .danger-investigation-view {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 2rem;
  }
  .detail-content {
    padding: 20px;
  }
  .info-section {
    background: #ffffff;
    border-radius: 12px;
    padding: 24px;
    margin-bottom: 24px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
  .section-title {
    padding: 1rem;
    font-size: 1rem;
    font-weight: 500;
    color: #303133;
    background: #f5f5f5;
    border-bottom: 1px solid #e4e7ed;
  }
  .info-content {
    padding: 1rem;
  }
  .info-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 20px;
  }
  .info-item {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .info-item:last-child {
    margin-bottom: 0;
  }
  .info-label {
    font-size: 14px;
    color: #909399;
  }
  .info-value {
    font-size: 14px;
    color: #303133;
    word-break: break-all;
  }
  .description-content {
    padding: 1rem;
    font-size: 0.875rem;
    color: #303133;
    line-height: 1.5;
  }
</style>