From 02167990874e1ac989a4baf08c8ac6e50d276cf2 Mon Sep 17 00:00:00 2001
From: buhuazhen <hua100783@gmail.com>
Date: 星期五, 13 三月 2026 13:30:02 +0800
Subject: [PATCH] feat: 新增巡检任务管理功能

---
 src/pages/index.vue                                           |   18 
 src/pages.json                                                |   11 
 src/pages/inspectionManagement/components/qrCodeDia.vue       |  132 ++++
 src/pages/inspectionManagement/components/viewQrCodeFiles.vue |  169 +++++
 src/pages/inspectionManagement/components/viewFiles.vue       |  296 ++++++++++
 src/pages/inspectionManagement/components/formDia.vue         |  549 ++++++++++++++++++
 src/pages/inspectionManagement/index.vue                      |  545 ++++++++++++++++++
 7 files changed, 1,718 insertions(+), 2 deletions(-)

diff --git a/src/pages.json b/src/pages.json
index 09239ca..a6d3cf9 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -613,6 +613,15 @@
       }
     },
     {
+      "path": "pages/inspectionManagement/index",
+      "style": {
+        "navigationBarTitleText": "宸℃浠诲姟绠$悊",
+        "navigationStyle": "custom",
+        "enablePullDownRefresh": true,
+        "backgroundColor": "#f8f8f8"
+      }
+    },
+    {
       "path": "pages/equipmentManagement/faultAnalysis/index",
       "style": {
         "navigationBarTitleText": "鏁呴殰鍒嗘瀽杩芥函",
@@ -1148,4 +1157,4 @@
     "navigationBarTitleText": "RuoYi",
     "navigationBarBackgroundColor": "#FFFFFF"
   }
-}
\ No newline at end of file
+}
diff --git a/src/pages/index.vue b/src/pages/index.vue
index f3d80c5..cb356ea 100644
--- a/src/pages/index.vue
+++ b/src/pages/index.vue
@@ -435,6 +435,10 @@
       icon: "/static/images/icon/xunjianshangchuan@2x.png",
       label: "宸℃绠$悊",
     },
+    {
+      icon: "/static/images/icon/xunjianshangchuan@2x.png",
+      label: "宸℃浠诲姟绠$悊",
+    }
   ]);
 
   // 澶勭悊甯哥敤鍔熻兘鐐瑰嚮
@@ -657,6 +661,11 @@
       case "宸℃绠$悊":
         uni.navigateTo({
           url: "/pages/inspectionUpload/index",
+        });
+        break;
+      case "宸℃浠诲姟绠$悊":
+        uni.navigateTo({
+          url: "/pages/inspectionManagement/index",
         });
         break;
       case "鍒嗘瀽杩芥函":
@@ -1033,10 +1042,17 @@
       { icon: "/static/images/icon/shbeibaoxiu@2x.png", label: "璁惧鎶ヤ慨" },
       { icon: "/static/images/icon/shbeibaoyang@2x.png", label: "璁惧淇濆吇" },
       { icon: "/static/images/icon/xunjianshangchuan@2x.png", label: "宸℃绠$悊" },
+      { icon: "/static/images/icon/xunjianshangchuan@2x.png", label: "宸℃浠诲姟绠$悊" },
     ];
     const filteredEquipment = originalEquipment.filter(item => {
       return allowedMenuTitles.has(item.label);
     });
+    if (filteredEquipment.some(i => i.label === "宸℃绠$悊")) {
+      const task = originalEquipment.find(i => i.label === "宸℃浠诲姟绠$悊");
+      if (task && !filteredEquipment.some(i => i.label === "宸℃浠诲姟绠$悊")) {
+        filteredEquipment.push(task);
+      }
+    }
     equipmentItems.splice(0, equipmentItems.length, ...filteredEquipment);
   };
 
@@ -1796,4 +1812,4 @@
       box-shadow: 0 0.375rem 1.25rem rgba(0, 0, 0, 0.4);
     }
   }
-</style>
\ No newline at end of file
+</style>
diff --git a/src/pages/inspectionManagement/components/formDia.vue b/src/pages/inspectionManagement/components/formDia.vue
new file mode 100644
index 0000000..e44d91e
--- /dev/null
+++ b/src/pages/inspectionManagement/components/formDia.vue
@@ -0,0 +1,549 @@
+<template>
+  <u-popup :show="dialogVisitable"
+           mode="center"
+           :round="12"
+           :zIndex="900"
+           @close="cancel">
+    <view class="popup-content">
+      <view class="popup-title">{{ operationType === "add" ? "鏂板宸℃浠诲姟" : "缂栬緫宸℃浠诲姟" }}</view>
+      <view class="form-body">
+        <view class="form-item">
+          <text class="label">璁惧鍚嶇О</text>
+          <picker mode="selector"
+                  :range="deviceOptions"
+                  range-key="deviceName"
+                  :value="deviceIndex"
+                  @change="onDeviceChange">
+            <view class="picker-value">{{ form.taskName || "璇烽�夋嫨璁惧" }}</view>
+          </picker>
+        </view>
+        <view class="form-item">
+          <text class="label">宸℃浜�</text>
+          <view class="picker-value inspector-picker"
+                @click="openInspectorPopup">
+            <text>{{ inspectorText || "璇烽�夋嫨宸℃浜�" }}</text>
+            <u-icon name="arrow-right"
+                    size="14"
+                    color="#999" />
+          </view>
+          <view class="inspector-tags"
+                v-if="form.inspector?.length">
+            <view v-for="userId in form.inspector"
+                  :key="userId"
+                  class="inspector-tag"
+                  @click="toggleInspector(userId)">
+              <text>{{ getUserName(userId) }}</text>
+              <u-icon name="close"
+                      size="11"
+                      color="#1677ff" />
+            </view>
+          </view>
+        </view>
+        <view class="form-item">
+          <text class="label">澶囨敞</text>
+          <u-textarea v-model="form.remarks"
+                      placeholder="璇疯緭鍏ュ娉�"
+                      :height="80"
+                      count />
+        </view>
+        <view class="form-item">
+          <text class="label">浠诲姟棰戠巼</text>
+          <picker mode="selector"
+                  :range="frequencyOptions"
+                  range-key="label"
+                  :value="frequencyIndex"
+                  @change="onFrequencyChange">
+            <view class="picker-value">{{ currentFrequencyLabel || "璇烽�夋嫨浠诲姟棰戠巼" }}</view>
+          </picker>
+        </view>
+        <view class="form-item"
+              v-if="form.frequencyType === 'DAILY'">
+          <text class="label">鏃堕棿</text>
+          <picker mode="time"
+                  :value="form.frequencyDetail || '08:00'"
+                  @change="onDailyTimeChange">
+            <view class="picker-value">{{ form.frequencyDetail || "璇烽�夋嫨鏃堕棿" }}</view>
+          </picker>
+        </view>
+        <view class="form-item"
+              v-if="form.frequencyType === 'WEEKLY'">
+          <text class="label">姣忓懆鏃ユ湡</text>
+          <picker mode="selector"
+                  :range="weekOptions"
+                  range-key="label"
+                  :value="weekIndex"
+                  @change="onWeekChange">
+            <view class="picker-value">{{ currentWeekLabel || "璇烽�夋嫨鏄熸湡" }}</view>
+          </picker>
+        </view>
+        <view class="form-item"
+              v-if="form.frequencyType === 'WEEKLY'">
+          <text class="label">姣忓懆鏃堕棿</text>
+          <picker mode="time"
+                  :value="form.time || '08:00'"
+                  @change="onWeekTimeChange">
+            <view class="picker-value">{{ form.time || "璇烽�夋嫨鏃堕棿" }}</view>
+          </picker>
+        </view>
+        <view class="form-item"
+              v-if="form.frequencyType === 'MONTHLY'">
+          <text class="label">姣忔湀鏃ユ湡涓庢椂闂�</text>
+          <picker mode="date"
+                  fields="day"
+                  :value="monthlyDate"
+                  @change="onMonthlyDateChange">
+            <view class="picker-value">{{ monthlyDate || "璇烽�夋嫨鏃ユ湡" }}</view>
+          </picker>
+          <picker mode="time"
+                  :value="monthlyTime"
+                  @change="onMonthlyTimeChange">
+            <view class="picker-value">{{ monthlyTime || "璇烽�夋嫨鏃堕棿" }}</view>
+          </picker>
+        </view>
+      </view>
+      <view class="popup-footer">
+        <u-button @click="cancel">鍙栨秷</u-button>
+        <u-button type="primary"
+                  @click="submitForm">淇濆瓨</u-button>
+      </view>
+    </view>
+  </u-popup>
+  <u-popup :show="showInspectorPopup"
+           mode="bottom"
+           :round="16"
+           :zIndex="1100"
+           @close="closeInspectorPopup">
+    <view class="inspector-popup">
+      <view class="inspector-header">
+        <text class="inspector-title">閫夋嫨宸℃浜�</text>
+      </view>
+      <scroll-view scroll-y
+                   class="inspector-list">
+        <view v-for="item in userList"
+              :key="item.userId"
+              class="inspector-row"
+              @click="toggleInspector(item.userId)">
+          <text class="inspector-name">{{ item.nickName }}</text>
+          <u-icon v-if="form.inspector.includes(item.userId)"
+                  name="checkmark-circle-fill"
+                  color="#1677ff"
+                  size="20" />
+          <u-icon v-else
+                  name=""
+                  color="#d5d8de"
+                  size="20" />
+        </view>
+      </scroll-view>
+      <view class="inspector-footer">
+        <u-button @click="closeInspectorPopup">鍙栨秷</u-button>
+        <u-button type="primary"
+                  @click="confirmInspector">纭畾</u-button>
+      </view>
+    </view>
+  </u-popup>
+</template>
+
+<script setup>
+  import { computed, reactive, ref } from "vue";
+  import useUserStore from "@/store/modules/user";
+  import { addOrEditTimingTask } from "@/api/inspectionManagement/index.js";
+  import { userListNoPageByTenantId } from "@/api/system/user.js";
+  import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
+
+  const emit = defineEmits(["closeDia"]);
+  const userStore = useUserStore();
+  const dialogVisitable = ref(false);
+  const operationType = ref("add");
+  const deviceOptions = ref([]);
+  const userList = ref([]);
+  const showInspectorPopup = ref(false);
+
+  const defaultForm = () => ({
+    id: undefined,
+    taskId: undefined,
+    taskName: "",
+    inspector: [],
+    inspectorIds: "",
+    remarks: "",
+    frequencyType: "",
+    frequencyDetail: "",
+    week: "",
+    time: "",
+    registrantId: undefined,
+  });
+
+  const form = reactive(defaultForm());
+
+  const frequencyOptions = [
+    { label: "姣忔棩", value: "DAILY" },
+    { label: "姣忓懆", value: "WEEKLY" },
+    { label: "姣忔湀", value: "MONTHLY" },
+  ];
+
+  const weekOptions = [
+    { label: "鍛ㄤ竴", value: "MON" },
+    { label: "鍛ㄤ簩", value: "TUE" },
+    { label: "鍛ㄤ笁", value: "WED" },
+    { label: "鍛ㄥ洓", value: "THU" },
+    { label: "鍛ㄤ簲", value: "FRI" },
+    { label: "鍛ㄥ叚", value: "SAT" },
+    { label: "鍛ㄦ棩", value: "SUN" },
+  ];
+
+  const monthlyDate = ref("");
+  const monthlyTime = ref("");
+
+  const deviceIndex = computed(() => {
+    const index = deviceOptions.value.findIndex(item => item.id === form.taskId);
+    return index >= 0 ? index : 0;
+  });
+
+  const frequencyIndex = computed(() => {
+    const index = frequencyOptions.findIndex(item => item.value === form.frequencyType);
+    return index >= 0 ? index : 0;
+  });
+
+  const weekIndex = computed(() => {
+    const index = weekOptions.findIndex(item => item.value === form.week);
+    return index >= 0 ? index : 0;
+  });
+
+  const currentFrequencyLabel = computed(() => {
+    return frequencyOptions.find(item => item.value === form.frequencyType)?.label || "";
+  });
+
+  const currentWeekLabel = computed(() => {
+    return weekOptions.find(item => item.value === form.week)?.label || "";
+  });
+
+  const inspectorText = computed(() => {
+    if (!form.inspector?.length) return "";
+    const nameMap = new Map(userList.value.map(item => [item.userId, item.nickName]));
+    return form.inspector.map(id => nameMap.get(id)).filter(Boolean).join("銆�");
+  });
+
+  const resetForm = () => {
+    Object.assign(form, defaultForm());
+    monthlyDate.value = "";
+    monthlyTime.value = "";
+    showInspectorPopup.value = false;
+  };
+
+  const loadBaseData = async () => {
+    const [userRes, deviceRes] = await Promise.all([
+      userListNoPageByTenantId(),
+      getDeviceLedger(),
+    ]);
+    userList.value = userRes?.data || [];
+    deviceOptions.value = deviceRes?.data || [];
+  };
+
+  const parseWeeklyDetail = detail => {
+    if (!detail || typeof detail !== "string" || !detail.includes(",")) return;
+    const [week, time] = detail.split(",");
+    form.week = week || "";
+    form.time = time || "";
+  };
+
+  const parseMonthlyDetail = detail => {
+    if (!detail || typeof detail !== "string" || !detail.includes(",")) return;
+    const [day, time] = detail.split(",");
+    if (day) monthlyDate.value = `${new Date().getFullYear()}-${new Date().getMonth() + 1 < 10 ? `0${new Date().getMonth() + 1}` : new Date().getMonth() + 1}-${day.padStart(2, "0")}`;
+    if (time) monthlyTime.value = time;
+  };
+
+  const setDeviceModel = id => {
+    const matched = deviceOptions.value.find(item => item.id === id);
+    if (matched) {
+      form.taskId = matched.id;
+      form.taskName = matched.deviceName;
+    }
+  };
+
+  const openDialog = async (type, row) => {
+    operationType.value = type;
+    dialogVisitable.value = true;
+    resetForm();
+    try {
+      await loadBaseData();
+      if (type === "edit" && row) {
+        Object.assign(form, {
+          ...defaultForm(),
+          ...row,
+          inspector: String(row.inspectorIds || "")
+            .split(",")
+            .map(item => Number(item))
+            .filter(Boolean),
+        });
+        if (form.frequencyType === "WEEKLY") parseWeeklyDetail(form.frequencyDetail);
+        if (form.frequencyType === "MONTHLY") parseMonthlyDetail(form.frequencyDetail);
+      }
+    } catch (error) {
+      uni.showToast({
+        title: "鍒濆鍖栧け璐�",
+        icon: "none",
+      });
+    }
+  };
+
+  const onDeviceChange = e => {
+    const index = Number(e?.detail?.value || 0);
+    const selected = deviceOptions.value[index];
+    if (!selected) return;
+    setDeviceModel(selected.id);
+  };
+
+  const getUserName = userId => {
+    const user = userList.value.find(item => item.userId === userId);
+    return user?.nickName || "";
+  };
+
+  const openInspectorPopup = () => {
+    showInspectorPopup.value = true;
+  };
+
+  const closeInspectorPopup = () => {
+    showInspectorPopup.value = false;
+  };
+
+  const toggleInspector = userId => {
+    if (form.inspector.includes(userId)) {
+      form.inspector = form.inspector.filter(id => id !== userId);
+    } else {
+      form.inspector = [...form.inspector, userId];
+    }
+  };
+
+  const confirmInspector = () => {
+    showInspectorPopup.value = false;
+  };
+
+  const onFrequencyChange = e => {
+    const index = Number(e?.detail?.value || 0);
+    const selected = frequencyOptions[index];
+    form.frequencyType = selected?.value || "";
+    form.frequencyDetail = "";
+    form.week = "";
+    form.time = "";
+    monthlyDate.value = "";
+    monthlyTime.value = "";
+  };
+
+  const onDailyTimeChange = e => {
+    form.frequencyDetail = e?.detail?.value || "";
+  };
+
+  const onWeekChange = e => {
+    const index = Number(e?.detail?.value || 0);
+    const selected = weekOptions[index];
+    form.week = selected?.value || "";
+  };
+
+  const onWeekTimeChange = e => {
+    form.time = e?.detail?.value || "";
+  };
+
+  const onMonthlyDateChange = e => {
+    monthlyDate.value = e?.detail?.value || "";
+  };
+
+  const onMonthlyTimeChange = e => {
+    monthlyTime.value = e?.detail?.value || "";
+  };
+
+  const validateForm = () => {
+    if (!form.taskId) {
+      uni.showToast({ title: "璇烽�夋嫨璁惧", icon: "none" });
+      return false;
+    }
+    if (!form.inspector.length) {
+      uni.showToast({ title: "璇烽�夋嫨宸℃浜�", icon: "none" });
+      return false;
+    }
+    if (!form.frequencyType) {
+      uni.showToast({ title: "璇烽�夋嫨浠诲姟棰戠巼", icon: "none" });
+      return false;
+    }
+    if (form.frequencyType === "DAILY" && !form.frequencyDetail) {
+      uni.showToast({ title: "璇烽�夋嫨鏃堕棿", icon: "none" });
+      return false;
+    }
+    if (form.frequencyType === "WEEKLY" && (!form.week || !form.time)) {
+      uni.showToast({ title: "璇烽�夋嫨姣忓懆鏃ユ湡鍜屾椂闂�", icon: "none" });
+      return false;
+    }
+    if (form.frequencyType === "MONTHLY" && (!monthlyDate.value || !monthlyTime.value)) {
+      uni.showToast({ title: "璇烽�夋嫨姣忔湀鏃ユ湡鍜屾椂闂�", icon: "none" });
+      return false;
+    }
+    return true;
+  };
+
+  const buildFrequencyDetail = () => {
+    if (form.frequencyType === "WEEKLY") return `${form.week},${form.time}`;
+    if (form.frequencyType === "MONTHLY") {
+      const day = monthlyDate.value.split("-")[2] || "";
+      return `${day},${monthlyTime.value}`;
+    }
+    return form.frequencyDetail;
+  };
+
+  const submitForm = async () => {
+    if (!validateForm()) return;
+    try {
+      const userInfo = await userStore.getInfo();
+      const payload = {
+        ...form,
+        inspectorIds: form.inspector.join(","),
+        frequencyDetail: buildFrequencyDetail(),
+        registrantId: userInfo?.user?.userId,
+      };
+      delete payload.inspector;
+      delete payload.week;
+      delete payload.time;
+      await addOrEditTimingTask(payload);
+      uni.showToast({
+        title: "鎻愪氦鎴愬姛",
+        icon: "success",
+      });
+      cancel();
+    } catch (error) {
+      uni.showToast({
+        title: "鎻愪氦澶辫触锛岃閲嶈瘯",
+        icon: "none",
+      });
+    }
+  };
+
+  const cancel = () => {
+    dialogVisitable.value = false;
+    resetForm();
+    emit("closeDia");
+  };
+
+  defineExpose({ openDialog });
+</script>
+
+<style scoped lang="scss">
+  :deep(.uni-picker-container) {
+    z-index: 1200 !important;
+  }
+
+  .popup-content {
+    width: 88vw;
+    max-height: 80vh;
+    background: #fff;
+    border-radius: 20rpx;
+    overflow: hidden;
+  }
+
+  .popup-title {
+    text-align: center;
+    font-size: 32rpx;
+    color: #1f1f1f;
+    font-weight: 600;
+    padding: 24rpx 20rpx;
+    border-bottom: 1rpx solid #f0f0f0;
+  }
+
+  .form-body {
+    padding: 24rpx;
+    max-height: 56vh;
+    overflow-y: auto;
+  }
+
+  .form-item {
+    margin-bottom: 20rpx;
+  }
+
+  .label {
+    display: block;
+    margin-bottom: 10rpx;
+    font-size: 24rpx;
+    color: #666;
+  }
+
+  .picker-value {
+    min-height: 72rpx;
+    background: #f7f8fa;
+    border-radius: 12rpx;
+    padding: 0 20rpx;
+    display: flex;
+    align-items: center;
+    color: #333;
+    font-size: 26rpx;
+  }
+
+  .inspector-picker {
+    justify-content: space-between;
+  }
+
+  .inspector-tags {
+    display: flex;
+    flex-wrap: wrap;
+    margin-top: 10rpx;
+    gap: 10rpx;
+  }
+
+  .inspector-tag {
+    display: flex;
+    align-items: center;
+    gap: 8rpx;
+    background: #edf3ff;
+    color: #1677ff;
+    font-size: 22rpx;
+    border-radius: 999rpx;
+    padding: 6rpx 14rpx;
+  }
+
+  .popup-footer {
+    display: flex;
+    gap: 16rpx;
+    padding: 20rpx 24rpx 24rpx;
+    border-top: 1rpx solid #f0f0f0;
+  }
+
+  .inspector-popup {
+    background: #fff;
+    border-radius: 24rpx 24rpx 0 0;
+    overflow: hidden;
+    max-height: 70vh;
+    padding-bottom: env(safe-area-inset-bottom);
+  }
+
+  .inspector-header {
+    padding: 24rpx;
+    text-align: center;
+    border-bottom: 1rpx solid #f0f0f0;
+  }
+
+  .inspector-title {
+    color: #1f1f1f;
+    font-size: 30rpx;
+    font-weight: 600;
+  }
+
+  .inspector-list {
+    max-height: 46vh;
+    padding: 0 24rpx;
+  }
+
+  .inspector-row {
+    min-height: 82rpx;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    border-bottom: 1rpx solid #f5f5f5;
+  }
+
+  .inspector-name {
+    font-size: 26rpx;
+    color: #333;
+  }
+
+  .inspector-footer {
+    display: flex;
+    gap: 16rpx;
+    padding: 20rpx 24rpx 24rpx;
+  }
+</style>
diff --git a/src/pages/inspectionManagement/components/qrCodeDia.vue b/src/pages/inspectionManagement/components/qrCodeDia.vue
new file mode 100644
index 0000000..136c18c
--- /dev/null
+++ b/src/pages/inspectionManagement/components/qrCodeDia.vue
@@ -0,0 +1,132 @@
+<template>
+  <div>
+    <el-dialog :title="operationType === 'add' ? '鏂板浜岀淮鐮�' : '缂栬緫浜岀淮鐮�'"
+               v-model="dialogVisitable" width="500px" @close="cancel">
+      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="璁惧鍚嶇О" prop="deviceName">
+              <el-input v-model="form.deviceName" placeholder="璇疯緭鍏ヨ澶囧悕绉�" maxlength="30" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="鎵�鍦ㄤ綅缃弿杩�" prop="location">
+              <el-input v-model="form.location" placeholder="璇疯緭鍏ユ墍鍦ㄤ綅缃弿杩�" maxlength="30"/>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <div>
+        <el-button type="primary" @click="submitForm">鐢熸垚骞舵墦鍗颁簩缁寸爜</el-button>
+      </div>
+      <div v-if="isShowQrCode" class="print-section" ref="qrCodeContainer" id="qrCodeContainer">
+        <vue-qrcode :value="qrCodeValue" :width="qrCodeSize"></vue-qrcode>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import useUserStore from "@/store/modules/user.js";
+import {reactive, ref} from "vue";
+import printJS from 'print-js';
+import {addOrEditQrCode} from "@/api/inspectionUpload/index.js";
+
+const { proxy } = getCurrentInstance()
+const emit = defineEmits()
+const userStore = useUserStore()
+const dialogVisitable = ref(false);
+const isShowQrCode = ref(false);
+const operationType = ref('add');
+
+const qrCodeValue = ref('');
+const qrCodeSize = ref(100);
+const data = reactive({
+  form: {
+    deviceName: '',
+    location: '',
+    qrCodeId: '',
+    id: ''
+  },
+  rules: {
+    deviceName: [{ required: true, message: '璇疯緭鍏ヨ澶囧悕绉�', trigger: 'blur' }],
+    location: [{ required: true, message: '璇疯緭鍏ュ湴鐐�', trigger: 'blur' }]
+  }
+})
+const { form, rules } = toRefs(data)
+
+
+// 鎵撳紑寮规
+const openDialog = async (type, row) => {
+  dialogVisitable.value = true
+  qrCodeValue.value = ''
+  isShowQrCode.value = false;
+  if (type === 'edit') {
+    form.value.id = row.id
+    form.value.qrCodeId = row.id
+    form.value.deviceName = row.deviceName
+    form.value.location = row.location
+    // 灏嗚〃鍗曟暟鎹浆涓� JSON 瀛楃涓蹭綔涓轰簩缁寸爜鍐呭
+    qrCodeValue.value = JSON.stringify(form.value);
+    isShowQrCode.value = true;
+  }
+}
+// 鎻愪氦鍚堝苟琛ㄥ崟
+const submitForm = () => {
+  proxy.$refs["formRef"].validate(valid => {
+    if (valid) {
+      addOrEditQrCode(form.value).then((res) => {
+        form.value.qrCodeId = res.data
+      })
+      // 灏嗚〃鍗曟暟鎹浆涓� JSON 瀛楃涓蹭綔涓轰簩缁寸爜鍐呭
+      qrCodeValue.value = JSON.stringify(form.value);
+      isShowQrCode.value = true;
+      showQrCode()
+    }
+  })
+}
+const showQrCode = () => {
+  // 寤惰繜鎵ц鎵撳嵃锛岄伩鍏� DOM 鏇存柊鍓嶅氨璋冪敤鎵撳嵃
+  setTimeout(() => {
+    printJS({
+      printable: 'qrCodeContainer',//椤甸潰
+      type: "html",//鏂囨。绫诲瀷
+      maxWidth: 360,
+      style: `@page {
+                margin:0;
+                size: 400px 75px collapse;
+                margin-top:3px;
+                &:first-of-type{
+                  margin-top:0 !important;
+                }
+              }
+              html{
+                zoom:100%;
+              }
+              @media print{
+                width: 400px;
+                height: 75px;
+                margin:0;
+              }`,
+      targetStyles: ["*"], // 浣跨敤dom鐨勬墍鏈夋牱寮忥紝寰堥噸瑕�
+      font_size: '0.20cm',
+    });
+  }, 300);
+}
+// 鍏抽棴鍚堝苟琛ㄥ崟
+const cancel = () => {
+  proxy.resetForm("formRef")
+  dialogVisitable.value = false
+  emit('closeDia')
+}
+defineExpose({ openDialog })
+</script>
+
+<style scoped>
+.print-section {
+  text-align: center;
+  margin-top: 30px;
+}
+</style>
\ No newline at end of file
diff --git a/src/pages/inspectionManagement/components/viewFiles.vue b/src/pages/inspectionManagement/components/viewFiles.vue
new file mode 100644
index 0000000..f39d9a4
--- /dev/null
+++ b/src/pages/inspectionManagement/components/viewFiles.vue
@@ -0,0 +1,296 @@
+<template>
+  <u-popup :show="dialogVisitable"
+           mode="bottom"
+           :round="16"
+           @close="cancel">
+    <view class="popup-content">
+      <view class="popup-header">
+        <text class="popup-title">鏌ョ湅闄勪欢</text>
+        <view class="close-icon"
+              @click="cancel">
+          <u-icon name="close"
+                  size="14"
+                  color="#666" />
+        </view>
+      </view>
+      <view class="popup-body">
+        <view class="tabs">
+          <view v-for="tab in tabs"
+                :key="tab.key"
+                class="tab-item"
+                :class="{ active: currentType === tab.key }"
+                @click="currentType = tab.key">
+            {{ tab.label }} ({{ getCurrentList(tab.key).length }})
+          </view>
+        </view>
+        <view class="file-list"
+              v-if="getCurrentList(currentType).length">
+          <view v-for="(file, index) in getCurrentList(currentType)"
+                :key="index"
+                class="file-item"
+                @click="previewFile(file)">
+            <image v-if="isImageFile(file)"
+                   :src="file.url"
+                   class="thumb"
+                   mode="aspectFill" />
+            <view v-else
+                  class="video-thumb">
+              <u-icon name="video"
+                      size="28"
+                      color="#1677ff" />
+              <text class="video-text">瑙嗛</text>
+            </view>
+            <text class="name">{{ file.name || "闄勪欢" }}</text>
+          </view>
+        </view>
+        <view v-else
+              class="empty">
+          <text>鏆傛棤闄勪欢</text>
+        </view>
+      </view>
+    </view>
+  </u-popup>
+  <u-popup :show="showVideoPopup"
+           mode="center"
+           :round="10"
+           @close="closeVideoPopup">
+    <view class="video-container">
+      <video :src="videoUrl"
+             controls
+             autoplay
+             class="video-player" />
+    </view>
+  </u-popup>
+</template>
+
+<script setup>
+  import { ref } from "vue";
+  import config from "@/config";
+
+  const dialogVisitable = ref(false);
+  const currentType = ref("before");
+  const showVideoPopup = ref(false);
+  const videoUrl = ref("");
+  const filesMap = ref({
+    before: [],
+    after: [],
+    issue: [],
+  });
+
+  const tabs = [
+    { key: "before", label: "鐢熶骇鍓�" },
+    { key: "after", label: "鐢熶骇涓�" },
+    { key: "issue", label: "鐢熶骇鍚�" },
+  ];
+
+  const normalizeUrl = raw => {
+    if (!raw) return "";
+    const url = String(raw).trim();
+    if (!url) return "";
+    if (/^https?:\/\//i.test(url)) return url;
+    if (url.startsWith("/")) return `${config.fileUrl}${url}`;
+    if (/^[a-zA-Z]:\\/.test(url)) {
+      const normalized = url.replace(/\\/g, "/");
+      const idx = normalized.indexOf("/prod/");
+      if (idx >= 0) {
+        const relative = normalized.slice(idx + "/prod/".length);
+        return `${config.fileUrl}/${relative}`;
+      }
+      return normalized;
+    }
+    return `${config.fileUrl}/${url.replace(/^\//, "")}`;
+  };
+
+  const isImageFile = file => {
+    if (file?.contentType?.startsWith("image/")) return true;
+    const name = String(file?.name || file?.bucketFilename || "").toLowerCase();
+    return /\.(jpg|jpeg|png|gif|bmp|webp)$/.test(name);
+  };
+
+  const normalizeFile = (file, type) => ({
+    ...file,
+    type,
+    url: normalizeUrl(file?.url || file?.downloadUrl),
+    name: file?.originalFilename || file?.bucketFilename || file?.name,
+  });
+
+  const getCurrentList = type => filesMap.value[type] || [];
+
+  const previewFile = file => {
+    if (isImageFile(file)) {
+      const urls = getCurrentList(currentType.value)
+        .filter(item => isImageFile(item))
+        .map(item => item.url);
+      uni.previewImage({
+        urls,
+        current: file.url,
+      });
+      return;
+    }
+    videoUrl.value = file.url;
+    showVideoPopup.value = true;
+  };
+
+  const closeVideoPopup = () => {
+    showVideoPopup.value = false;
+    videoUrl.value = "";
+  };
+
+  const openDialog = row => {
+    const allList = Array.isArray(row?.commonFileList) ? row.commonFileList : [];
+    const beforeList = Array.isArray(row?.commonFileListBefore)
+      ? row.commonFileListBefore
+      : allList.filter(item => item?.type === 10);
+    const afterList = Array.isArray(row?.commonFileListAfter)
+      ? row.commonFileListAfter
+      : allList.filter(item => item?.type === 11);
+    const issueList = Array.isArray(row?.commonFileListIssue)
+      ? row.commonFileListIssue
+      : allList.filter(item => item?.type === 12);
+    filesMap.value = {
+      before: beforeList.map(item => normalizeFile(item, "before")).filter(item => item.url),
+      after: afterList.map(item => normalizeFile(item, "after")).filter(item => item.url),
+      issue: issueList.map(item => normalizeFile(item, "issue")).filter(item => item.url),
+    };
+    currentType.value = "before";
+    dialogVisitable.value = true;
+  };
+
+  const cancel = () => {
+    dialogVisitable.value = false;
+    closeVideoPopup();
+    filesMap.value = {
+      before: [],
+      after: [],
+      issue: [],
+    };
+  };
+
+  defineExpose({ openDialog });
+</script>
+
+<style scoped lang="scss">
+  .popup-content {
+    width: 100vw;
+    max-height: 82vh;
+    background: #fff;
+    border-radius: 24rpx 24rpx 0 0;
+    overflow: hidden;
+    padding-bottom: env(safe-area-inset-bottom);
+  }
+
+  .popup-header {
+    padding: 24rpx 20rpx;
+    border-bottom: 1rpx solid #f0f0f0;
+    text-align: center;
+    position: relative;
+  }
+
+  .popup-title {
+    font-size: 32rpx;
+    color: #1f1f1f;
+    font-weight: 600;
+  }
+
+  .popup-body {
+    padding: 20rpx 20rpx 26rpx;
+    max-height: 68vh;
+    overflow-y: auto;
+  }
+
+  .tabs {
+    display: flex;
+    background: #f4f5f8;
+    border-radius: 12rpx;
+    padding: 6rpx;
+    margin-bottom: 20rpx;
+  }
+
+  .tab-item {
+    flex: 1;
+    text-align: center;
+    padding: 12rpx 0;
+    color: #666;
+    font-size: 24rpx;
+    border-radius: 10rpx;
+  }
+
+  .tab-item.active {
+    background: #1677ff;
+    color: #fff;
+    font-weight: 600;
+  }
+
+  .file-list {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 16rpx;
+  }
+
+  .file-item {
+    background: #fafafa;
+    border-radius: 12rpx;
+    padding: 10rpx;
+  }
+
+  .thumb {
+    width: 100%;
+    height: 180rpx;
+    border-radius: 8rpx;
+  }
+
+  .video-thumb {
+    width: 100%;
+    height: 180rpx;
+    border-radius: 8rpx;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    background: #edf3ff;
+  }
+
+  .video-text {
+    font-size: 22rpx;
+    color: #1677ff;
+    margin-top: 6rpx;
+  }
+
+  .name {
+    margin-top: 8rpx;
+    font-size: 22rpx;
+    color: #333;
+    display: block;
+    word-break: break-all;
+  }
+
+  .empty {
+    text-align: center;
+    color: #999;
+    padding: 40rpx 0;
+  }
+
+  .video-container {
+    width: 94vw;
+    background: #000;
+  }
+
+  .video-player {
+    width: 94vw;
+    height: 55vw;
+  }
+
+  .close-icon {
+    position: absolute;
+    right: 24rpx;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 44rpx;
+    height: 44rpx;
+    border-radius: 50%;
+    background: #f5f5f5;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+</style>
diff --git a/src/pages/inspectionManagement/components/viewQrCodeFiles.vue b/src/pages/inspectionManagement/components/viewQrCodeFiles.vue
new file mode 100644
index 0000000..f8e923a
--- /dev/null
+++ b/src/pages/inspectionManagement/components/viewQrCodeFiles.vue
@@ -0,0 +1,169 @@
+<template>
+  <div>
+    <el-dialog title="鏌ョ湅闄勪欢"
+               v-model="dialogVisitable" width="800px" @close="cancel">
+      <div class="upload-container">
+        <div class="form-container">
+          <div class="title">宸℃闄勪欢</div>
+          <!-- 鍥剧墖鍒楄〃 -->
+          <div style="display: flex; flex-wrap: wrap;">
+            <img v-for="(item, index) in beforeProductionImgs" :key="index"
+                 @click="showMedia(beforeProductionImgs, index, 'image')"
+                 :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt="">
+          </div>
+          
+          <!-- 瑙嗛鍒楄〃 -->
+          <div style="display: flex; flex-wrap: wrap;">
+            <div
+                v-for="(videoUrl, index) in beforeProductionVideos"
+                :key="index"
+                @click="showMedia(beforeProductionVideos, index, 'video')"
+                style="position: relative; margin: 10px; cursor: pointer;"
+            >
+              <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;">
+                <img src="@/assets/images/video.png" alt="鎾斁" style="width: 30px; height: 30px; opacity: 0.8;" />
+              </div>
+              <div style="text-align: center; font-size: 12px; color: #666;">鐐瑰嚮鎾斁</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+    <!-- 缁熶竴濯掍綋鏌ョ湅鍣� -->
+    <div v-if="isMediaViewerVisible" class="media-viewer-overlay" @click.self="closeMediaViewer">
+      <div class="media-viewer-content" @click.stop>
+        <!-- 鍥剧墖 -->
+        <vue-easy-lightbox
+            v-if="mediaType === 'image'"
+            :visible="isMediaViewerVisible"
+            :imgs="mediaList"
+            :index="currentMediaIndex"
+            @hide="closeMediaViewer"
+        ></vue-easy-lightbox>
+        
+        <!-- 瑙嗛 -->
+        <div v-else-if="mediaType === 'video'" style="position: relative;">
+          <Video
+              :src="mediaList[currentMediaIndex]"
+              autoplay
+              controls
+              style="max-width: 90vw; max-height: 80vh;"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+// 鎺у埗寮圭獥鏄剧ず
+import VueEasyLightbox from "vue-easy-lightbox";
+
+const dialogVisitable = ref(false);
+// 鍥剧墖鏁扮粍
+const beforeProductionImgs = ref([]);
+// 瑙嗛鏁扮粍
+const beforeProductionVideos = ref([]);
+// 濯掍綋鏌ョ湅鍣ㄧ姸鎬�
+const isMediaViewerVisible = ref(false);
+const currentMediaIndex = ref(0);
+const mediaList = ref([]); // 瀛樺偍褰撳墠瑕佹煡鐪嬬殑濯掍綋鍒楄〃锛堝惈鍥剧墖鍜岃棰戝璞★級
+const mediaType = ref('image'); // image | video
+
+// 鎵撳紑寮圭獥骞跺姞杞芥暟鎹�
+const openDialog = async (row) => {
+  const { images: beforeImgs, videos: beforeVids } = processItems(row.storageBlobDTO);
+  
+  beforeProductionImgs.value = beforeImgs;
+  beforeProductionVideos.value = beforeVids;
+  dialogVisitable.value = true;
+};
+// 鏄剧ず濯掍綋锛堝浘鐗� or 瑙嗛锛�
+function showMedia(mediaArray, index, type) {
+  mediaList.value = mediaArray;
+  currentMediaIndex.value = index;
+  mediaType.value = type;
+  isMediaViewerVisible.value = true;
+}
+// 鍏抽棴濯掍綋鏌ョ湅鍣�
+function closeMediaViewer() {
+  isMediaViewerVisible.value = false;
+  mediaList.value = [];
+  mediaType.value = 'image';
+}
+// 琛ㄥ崟鍏抽棴鏂规硶
+const cancel = () => {
+  dialogVisitable.value = false;
+};
+// 澶勭悊姣忎竴绫绘暟鎹細鍒嗙鍥剧墖鍜岃棰�
+function processItems(items) {
+  const images = [];
+  const videos = [];
+  items.forEach(item => {
+    if (item.contentType?.startsWith('image/')) {
+      images.push(item.url);
+    } else if (item.contentType?.startsWith('video/')) {
+      videos.push(item.url);
+    }
+  });
+  return { images, videos };
+}
+defineExpose({ openDialog });
+</script>
+
+<style scoped lang="scss">
+.upload-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 20px;
+  border: 1px solid #dcdfe6;
+  box-sizing: border-box;
+  
+  .form-container {
+    flex: 1;
+    width: 100%;
+    margin-bottom: 20px;
+  }
+}
+
+.title {
+  font-size: 14px;
+  color: #165dff;
+  line-height: 20px;
+  font-weight: 600;
+  padding-left: 10px;
+  position: relative;
+  margin: 6px 0;
+  
+  &::before {
+    content: "";
+    position: absolute;
+    left: 0;
+    top: 3px;
+    width: 4px;
+    height: 14px;
+    background-color: #165dff;
+  }
+}
+
+.media-viewer-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.8);
+  z-index: 9999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.media-viewer-content {
+  position: relative;
+  max-width: 90vw;
+  max-height: 90vh;
+  overflow: hidden;
+}
+</style>
\ No newline at end of file
diff --git a/src/pages/inspectionManagement/index.vue b/src/pages/inspectionManagement/index.vue
new file mode 100644
index 0000000..349d1d0
--- /dev/null
+++ b/src/pages/inspectionManagement/index.vue
@@ -0,0 +1,545 @@
+<template>
+  <view class="inspection-management-page">
+    <PageHeader title="宸℃浠诲姟绠$悊"
+                @back="goBack" />
+    <view class="toolbar">
+      <view class="tab-wrap">
+        <view v-for="tab in tabs"
+              :key="tab.name"
+              class="tab-item"
+              :class="{ active: activeTab === tab.name }"
+              @click="switchTab(tab.name)">
+          {{ tab.label }}
+        </view>
+      </view>
+      <view class="search-section">
+        <view class="search-bar">
+          <view class="search-input">
+            <up-input class="search-text"
+                      placeholder="璇疯緭鍏ュ贰妫�浠诲姟鍚嶇О"
+                      v-model="queryParams.taskName"
+                      clearable />
+          </view>
+          <view class="search-button"
+                @click="handleQuery">
+            <up-icon name="search"
+                     size="24"
+                     color="#999"></up-icon>
+          </view>
+        </view>
+      </view>
+      <view class="meta-bar">
+        <text class="meta-text">鍏� {{ total }} 鏉�</text>
+      </view>
+    </view>
+    <view class="list-section">
+      <uni-swipe-action>
+        <uni-swipe-action-item v-for="item in tableData"
+                               :key="item.id"
+                               :right-options="activeTab === 'taskManage' ? swipeOptions : []"
+                               :disabled="activeTab !== 'taskManage'"
+                               @click="onSwipeActionClick($event, item)">
+          <view class="ledger-item">
+            <view class="item-header">
+              <view class="item-left">
+                <view class="document-icon">
+                  <up-icon name="file-text"
+                           size="14"
+                           color="#ffffff"></up-icon>
+                </view>
+                <text class="item-id">{{ item.taskName || "--" }}</text>
+              </view>
+              <view class="item-right">
+                <u-tag :type="getFrequencyTagType(item.frequencyType)"
+                       :text="formatFrequency(item.frequencyType) || '鏈煡棰戞'" />
+              </view>
+            </view>
+            <up-divider></up-divider>
+            <view class="item-details">
+              <view class="detail-row">
+                <text class="detail-label">浠诲姟缂栧彿</text>
+                <text class="detail-value">{{ item.id || "--" }}</text>
+              </view>
+              <view class="detail-row">
+                <text class="detail-label">鎵ц宸℃浜�</text>
+                <view class="tag-wrap"
+                      v-if="item.inspector?.length">
+                  <uni-tag v-for="(person, index) in item.inspector"
+                           :key="index"
+                           :text="person"
+                           type="primary"
+                           size="small"
+                           inverted />
+                </view>
+                <text class="detail-value"
+                      v-else>--</text>
+              </view>
+              <view class="detail-row">
+                <text class="detail-label">寮�濮嬫棩鏈熶笌鏃堕棿</text>
+                <text class="detail-value highlight">{{ formatFrequencyDetail(item.frequencyDetail) }}</text>
+              </view>
+              <view class="detail-row">
+                <text class="detail-label">鐧昏浜�</text>
+                <text class="detail-value">{{ item.registrant || "--" }}</text>
+              </view>
+              <view class="detail-row">
+                <text class="detail-label">鐧昏鏃ユ湡</text>
+                <text class="detail-value">{{ item.createTime || "--" }}</text>
+              </view>
+              <view class="detail-row">
+                <text class="detail-label">澶囨敞</text>
+                <text class="detail-value">{{ item.remarks || "鏃�" }}</text>
+              </view>
+              <up-divider></up-divider>
+              <view class="card-actions">
+                <u-button v-if="activeTab === 'taskManage'"
+                          type="primary"
+                          size="small"
+                          class="action-btn"
+                          @click.stop="handleAdd(item)">缂栬緫</u-button>
+                <u-button v-else
+                          type="success"
+                          size="small"
+                          class="action-btn"
+                          @click.stop="viewFile(item)">鏌ョ湅闄勪欢</u-button>
+              </view>
+            </view>
+          </view>
+        </uni-swipe-action-item>
+      </uni-swipe-action>
+      <uni-load-more :status="loadMoreStatus"></uni-load-more>
+      <view v-if="!loading && tableData.length === 0"
+            class="no-data">
+        <text>鏆傛棤鏁版嵁</text>
+      </view>
+    </view>
+    <view v-if="activeTab === 'taskManage'"
+          class="fab-button"
+          @click="handleAdd()">
+      <up-icon name="plus"
+               size="24"
+               color="#ffffff"></up-icon>
+    </view>
+    <form-dia ref="formDia"
+              @closeDia="handleQuery" />
+    <view-files ref="viewFiles" />
+  </view>
+</template>
+
+<script setup>
+  import { ref, reactive, computed, nextTick } from "vue";
+  import { onShow, onReachBottom, onPullDownRefresh } from "@dcloudio/uni-app";
+  import PageHeader from "@/components/PageHeader.vue";
+  import FormDia from "@/pages/inspectionManagement/components/formDia.vue";
+  import ViewFiles from "@/pages/inspectionManagement/components/viewFiles.vue";
+  import {
+    delTimingTask,
+    inspectionTaskList,
+    timingTaskList,
+  } from "@/api/inspectionManagement/index.js";
+
+  const formDia = ref();
+  const viewFiles = ref();
+  const activeTab = ref("taskManage");
+  const tabs = [
+    { name: "taskManage", label: "瀹氭椂浠诲姟绠$悊" },
+    { name: "task", label: "瀹氭椂浠诲姟璁板綍" },
+  ];
+  const queryParams = reactive({
+    taskName: "",
+  });
+  const pageParams = reactive({
+    current: 1,
+    size: 10,
+  });
+  const total = ref(0);
+  const loading = ref(false);
+  const tableData = ref([]);
+  const swipeOptions = [
+    {
+      text: "鍒犻櫎",
+      style: {
+        backgroundColor: "#ee0a24",
+      },
+    },
+  ];
+
+  const noMore = computed(() => tableData.value.length >= total.value);
+  const loadMoreStatus = computed(() => {
+    if (loading.value) return "loading";
+    if (noMore.value) return "noMore";
+    return "more";
+  });
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const formatFrequency = value => {
+    if (value === "DAILY") return "姣忔棩";
+    if (value === "WEEKLY") return "姣忓懆";
+    if (value === "MONTHLY") return "姣忔湀";
+    if (value === "QUARTERLY") return "瀛e害";
+    return "";
+  };
+
+  const getFrequencyTagType = value => {
+    if (value === "DAILY") return "success";
+    if (value === "WEEKLY") return "primary";
+    if (value === "MONTHLY") return "warning";
+    return "info";
+  };
+
+  const formatFrequencyDetail = value => {
+    if (!value || typeof value !== "string") return "--";
+    return value.replace(
+      /MON|TUE|WED|THU|FRI|SAT|SUN/g,
+      item =>
+        ({
+          MON: "鍛ㄤ竴",
+          TUE: "鍛ㄤ簩",
+          WED: "鍛ㄤ笁",
+          THU: "鍛ㄥ洓",
+          FRI: "鍛ㄤ簲",
+          SAT: "鍛ㄥ叚",
+          SUN: "鍛ㄦ棩",
+        })[item] || item
+    );
+  };
+
+  const normalizeInspector = val => {
+    if (!val) return [];
+    if (Array.isArray(val)) return val.filter(Boolean);
+    if (typeof val === "string") {
+      return val
+        .split(",")
+        .map(item => item.trim())
+        .filter(Boolean);
+    }
+    return [String(val)];
+  };
+
+  const switchTab = tabName => {
+    if (activeTab.value === tabName) return;
+    activeTab.value = tabName;
+    handleQuery();
+  };
+
+  const getList = async () => {
+    if (loading.value) return;
+    loading.value = true;
+    try {
+      const params = {
+        ...queryParams,
+        current: pageParams.current,
+        size: pageParams.size,
+      };
+      const request = activeTab.value === "task" ? inspectionTaskList : timingTaskList;
+      const res = await request(params);
+      const records = res?.data?.records || [];
+      const normalized = records.map(item => ({
+        ...item,
+        inspector: normalizeInspector(item.inspector),
+      }));
+      if (pageParams.current === 1) {
+        tableData.value = normalized;
+      } else {
+        tableData.value = [...tableData.value, ...normalized];
+      }
+      total.value = Number(res?.data?.total || 0);
+    } catch (error) {
+      if (pageParams.current === 1) {
+        tableData.value = [];
+      }
+      uni.showToast({
+        title: "鑾峰彇鏁版嵁澶辫触",
+        icon: "none",
+      });
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const handleQuery = () => {
+    pageParams.current = 1;
+    total.value = 0;
+    getList();
+  };
+
+  const loadMore = () => {
+    if (loading.value || noMore.value) return;
+    pageParams.current += 1;
+    getList();
+  };
+
+  const handleAdd = row => {
+    nextTick(() => {
+      formDia.value?.openDialog(row ? "edit" : "add", row);
+    });
+  };
+
+  const viewFile = row => {
+    nextTick(() => {
+      viewFiles.value?.openDialog(row);
+    });
+  };
+
+  const deleteOne = async row => {
+    if (!row?.id) return;
+    const canDelete = await new Promise(resolve => {
+      uni.showModal({
+        title: "鎻愮ず",
+        content: "鏄惁纭鍒犻櫎璇ュ贰妫�浠诲姟锛�",
+        success: modalRes => resolve(Boolean(modalRes.confirm)),
+        fail: () => resolve(false),
+      });
+    });
+    if (!canDelete) return;
+    try {
+      await delTimingTask([row.id]);
+      uni.showToast({
+        title: "鍒犻櫎鎴愬姛",
+        icon: "success",
+      });
+      handleQuery();
+    } catch (error) {
+      uni.showToast({
+        title: "鍒犻櫎澶辫触",
+        icon: "none",
+      });
+    }
+  };
+
+  const onSwipeActionClick = (event, row) => {
+    if (activeTab.value !== "taskManage") return;
+    if (event?.position !== "right") return;
+    deleteOne(row);
+  };
+
+  onShow(() => {
+    handleQuery();
+  });
+
+  onReachBottom(() => {
+    loadMore();
+  });
+
+  onPullDownRefresh(() => {
+    handleQuery();
+    uni.stopPullDownRefresh();
+  });
+</script>
+
+<style scoped>
+  .inspection-management-page {
+    min-height: 100vh;
+    background: #f6f7fb;
+  }
+
+  .toolbar {
+    padding: 20rpx 24rpx;
+    background: #fff;
+    border-bottom: 1rpx solid #f0f0f0;
+    position: sticky;
+    top: 0;
+    z-index: 10;
+  }
+
+  .tab-wrap {
+    display: flex;
+    background: #f4f5f8;
+    border-radius: 16rpx;
+    padding: 6rpx;
+  }
+
+  .tab-item {
+    flex: 1;
+    text-align: center;
+    padding: 14rpx 0;
+    font-size: 26rpx;
+    color: #666;
+    border-radius: 12rpx;
+  }
+
+  .tab-item.active {
+    background: #1677ff;
+    color: #fff;
+    font-weight: 600;
+  }
+
+  .search-section {
+    margin-top: 20rpx;
+  }
+
+  .search-bar {
+    display: flex;
+    align-items: center;
+    background: #f7f8fa;
+    border-radius: 14rpx;
+    padding: 8rpx 12rpx 8rpx 16rpx;
+    border: 1rpx solid #eef1f5;
+  }
+
+  .search-input {
+    flex: 1;
+    min-width: 0;
+  }
+
+  .search-text {
+    background: transparent !important;
+  }
+
+  :deep(.search-text .u-input__content),
+  :deep(.search-text .up-input__content) {
+    background: transparent !important;
+    padding: 0 !important;
+  }
+
+  .search-button {
+    width: 64rpx;
+    height: 64rpx;
+    border-radius: 12rpx;
+    background: #ffffff;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .meta-bar {
+    margin-top: 16rpx;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background: #f7f9fc;
+    border-radius: 12rpx;
+    padding: 10rpx 16rpx;
+  }
+
+  .meta-text {
+    color: #5c6b8a;
+    font-size: 22rpx;
+  }
+
+  .list-section {
+    padding: 20rpx 24rpx;
+    padding-bottom: calc(132rpx + env(safe-area-inset-bottom));
+  }
+
+  .ledger-item {
+    background: #ffffff;
+    border-radius: 20rpx;
+    margin-bottom: 16rpx;
+    overflow: hidden;
+    box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.05);
+    padding: 0 20rpx;
+  }
+
+  .item-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 22rpx 0;
+  }
+
+  .item-left {
+    display: flex;
+    align-items: center;
+    gap: 10rpx;
+    flex: 1;
+    min-width: 0;
+  }
+
+  .item-right {
+    display: flex;
+    align-items: center;
+  }
+
+  .document-icon {
+    width: 38rpx;
+    height: 38rpx;
+    border-radius: 8rpx;
+    background: #2979ff;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .item-id {
+    font-size: 28rpx;
+    color: #1f1f1f;
+    font-weight: 600;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .item-details {
+    padding: 18rpx 0 20rpx;
+  }
+
+  .detail-row {
+    display: flex;
+    align-items: flex-end;
+    justify-content: space-between;
+    margin-bottom: 12rpx;
+  }
+
+  .detail-label {
+    min-width: 160rpx;
+    color: #777;
+    font-size: 24rpx;
+  }
+
+  .detail-value {
+    flex: 1;
+    color: #333;
+    font-size: 24rpx;
+    text-align: right;
+    word-break: break-all;
+    margin-left: 12rpx;
+  }
+
+  .detail-value.highlight {
+    color: #2979ff;
+    font-weight: 500;
+  }
+
+  .tag-wrap {
+    flex: 1;
+    display: flex;
+    gap: 10rpx;
+    flex-wrap: wrap;
+    justify-content: flex-end;
+  }
+
+  .card-actions {
+    padding-top: 8rpx;
+    display: flex;
+    justify-content: flex-end;
+  }
+
+  .action-btn {
+    min-width: 140rpx;
+  }
+
+  .fab-button {
+    position: fixed;
+    bottom: calc(30px + env(safe-area-inset-bottom));
+    right: 30px;
+    width: 56px;
+    height: 56px;
+    background: #2979ff;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    box-shadow: 0 4px 16px rgba(41, 121, 255, 0.3);
+    z-index: 1000;
+  }
+
+  .no-data {
+    padding: 40rpx 0;
+    text-align: center;
+    color: #999;
+  }
+</style>

--
Gitblit v1.9.3