From 84bf005a674d3c49e66a67b9a55824dedb0d7120 Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期四, 05 二月 2026 16:56:22 +0800
Subject: [PATCH] 安全培训模块开发

---
 src/pages/index.vue                                                |   10 
 src/pages/safeProduction/safetyTrainingAssessment/fileList.vue     |  567 ++++++++++++
 src/api/safeProduction/safetyTrainingAssessment.js                 |  120 ++
 src/pages.json                                                     |   42 
 src/pages/safeProduction/safetyTrainingAssessment/view.vue         |  171 +++
 src/pages/safeProduction/safetyTrainingAssessment/record.vue       |  546 +++++++++++
 src/pages/safeProduction/safetyTrainingAssessment/index.vue        |  448 +++++++++
 src/pages/safeProduction/safetyTrainingAssessment/detail.vue       |  430 +++++++++
 src/pages/safeProduction/safetyTrainingAssessment/resultDetail.vue |  388 ++++++++
 9 files changed, 2,722 insertions(+), 0 deletions(-)

diff --git a/src/api/safeProduction/safetyTrainingAssessment.js b/src/api/safeProduction/safetyTrainingAssessment.js
new file mode 100644
index 0000000..7fdc956
--- /dev/null
+++ b/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'
+    })
+}
\ No newline at end of file
diff --git a/src/pages.json b/src/pages.json
index 2e1cabe..9bf4888 100644
--- a/src/pages.json
+++ b/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": [
diff --git a/src/pages/index.vue b/src/pages/index.vue
index aa08cb3..7f0408c 100644
--- a/src/pages/index.vue
+++ b/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}`,
diff --git a/src/pages/safeProduction/safetyTrainingAssessment/detail.vue b/src/pages/safeProduction/safetyTrainingAssessment/detail.vue
new file mode 100644
index 0000000..d55c892
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/pages/safeProduction/safetyTrainingAssessment/fileList.vue b/src/pages/safeProduction/safetyTrainingAssessment/fileList.vue
new file mode 100644
index 0000000..9797f21
--- /dev/null
+++ b/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("瑙f瀽涓婁紶缁撴灉澶辫触:", 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>
\ No newline at end of file
diff --git a/src/pages/safeProduction/safetyTrainingAssessment/index.vue b/src/pages/safeProduction/safetyTrainingAssessment/index.vue
new file mode 100644
index 0000000..f2107ae
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/pages/safeProduction/safetyTrainingAssessment/record.vue b/src/pages/safeProduction/safetyTrainingAssessment/record.vue
new file mode 100644
index 0000000..825b7d6
--- /dev/null
+++ b/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,
+      // 濡傛灉鏈夊勾浠界瓫閫夛紝娣诲姞骞翠唤鍙傛暟
+      // 杩欓噷闇�瑕佹牴鎹悗绔帴鍙g殑瀹為檯鍙傛暟鍚嶈繘琛岃皟鏁�
+    };
+
+    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>
\ No newline at end of file
diff --git a/src/pages/safeProduction/safetyTrainingAssessment/resultDetail.vue b/src/pages/safeProduction/safetyTrainingAssessment/resultDetail.vue
new file mode 100644
index 0000000..9fd10a1
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/pages/safeProduction/safetyTrainingAssessment/view.vue b/src/pages/safeProduction/safetyTrainingAssessment/view.vue
new file mode 100644
index 0000000..df8b991
--- /dev/null
+++ b/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>
\ No newline at end of file

--
Gitblit v1.9.3