Răsfoiți Sursa

Merge branch 'feature-ruidu'

Zimo 1 săptămână în urmă
părinte
comite
4be5ce3f6e
3 a modificat fișierele cu 475 adăugiri și 161 ștergeri
  1. 334 25
      pages/ruiDu/compontents/report-form.vue
  2. 100 136
      pages/ruiDu/index.vue
  3. 41 0
      utils/useDebounceFn.js

+ 334 - 25
pages/ruiDu/compontents/report-form.vue

@@ -1,12 +1,14 @@
 <script setup>
   import { useDataDictStore } from '@/store/modules/dataDict';
   import { onMounted, reactive, ref, computed, getCurrentInstance, watch } from 'vue';
+  import { useDebounceFn } from '@/utils/useDebounceFn.js';
 
   import tpfTimeRange from '@/components/tpf-time-range/tpf-time-range.vue';
   import deviceTransfer from '@/components/device-transfer/index.vue';
 
   import { updateRuiDuReportBatch, getRuiDuReportAttrs } from '@/api/ruiDu.js';
   import config from '@/utils/config';
+  import dayjs from 'dayjs';
 
   import { getTenantId, getAccessToken } from '@/utils/auth.js';
 
@@ -87,6 +89,8 @@
     form.endTime = data[1];
   };
 
+  const dailyFuel = ref(0);
+
   const form = reactive({
     startTime: startDefaultTime.value,
     endTime: endDefaultTime.value,
@@ -98,6 +102,7 @@
     faultDowntime: '',
     malfunction: '',
     attachments: [],
+    reportFuels: [],
   });
 
   const formDataBaseRules = reactive({
@@ -128,6 +133,14 @@
         },
       ],
     },
+    nextPlan: {
+      rules: [
+        {
+          required: true,
+          errorMessage: `请输入下步工作计划`,
+        },
+      ],
+    },
   });
 
   const validate = async () => {
@@ -143,6 +156,14 @@
     // // 处理表单数据
     const formDataCopy = JSON.parse(JSON.stringify(form));
 
+    if (!formDataCopy.dailyFuel && formDataCopy.dailyFuel !== 0) {
+      uni.showToast({
+        title: '请输入当日油耗',
+        icon: 'none',
+      });
+      return;
+    }
+
     const responseData = [];
     // // 处理施工工艺
     form.platformIds.forEach(id => {
@@ -187,13 +208,17 @@
         productionStatus: formDataCopy.productionStatus,
         rdStatus: formDataCopy[id].rdStatus,
         techniqueIds: formDataCopy[id].techniqueIds.map(v => v.toString()),
+        reportFuels: formDataCopy.reportFuels.map(item => ({
+          ...item,
+          customFuel: Number(item.customFuel),
+          reportId: id,
+        })),
+        dailyFuel: Number(formDataCopy.dailyFuel),
       };
 
       responseData.push(data);
     });
 
-    console.log('responseData :>> ', responseData);
-
     // 提交表单
     updateRuiDuReportBatch(responseData).then(res => {
       // 提交成功
@@ -235,6 +260,8 @@
     handleEquipmentNames(selectedIds);
   };
 
+  const steps = ref([]);
+
   const formDataFormat = () => {
     // 处理时间范围
     timeRangeFormat();
@@ -244,8 +271,6 @@
       handleEquipmentNames(deviceIds);
     }
 
-    console.log('props.reportData :>> ', props.reportData);
-
     if (props.reportData.platformWell === 1) {
       form.platformIds = props.reportData.platforms?.map(v => v.reportId) ?? [];
     } else {
@@ -282,6 +307,23 @@
     form.faultDowntime = props.reportData.faultDowntime || ''; //故障误工H
     // 附件
     form.attachments = props.reportData.attachments || [];
+
+    // 提取变量,方便阅读
+    const list1 = props.reportData.reportFuels;
+    const list2 = props.reportData.reportedFuels;
+
+    const validList = list1?.length > 0 ? list1 : list2?.length > 0 ? list2 : [];
+
+    form.reportFuels = validList.map(v => ({
+      ...v,
+      // 这里保持你原有的数值处理逻辑
+      customFuel: Number(Number(props.formDisable ? v.customFuel ?? 0 : v.customFuel ?? v.zhbdFuel ?? 0).toFixed(2)),
+    }));
+
+    steps.value = (props.reportData.taskProgresses ?? []).map(v => ({ title: v.rdStatusLabel, desc: v.createTime }));
+
+    initDailyFuel();
+    // form.
     // 展示用的文件列表
     // attachmentsFileList.value =
     //   props.reportData?.attachments?.map(item => ({
@@ -442,10 +484,109 @@
     // 1. 从formData.attachments中移除选中项
     form.attachments.splice(index, 1);
   };
+
+  function copyToPublicAndOpen(sourcePath, fileName) {
+    // 获取 _downloads/ (公共下载目录) 的目录对象
+    plus.io.resolveLocalFileSystemURL(
+      '_downloads/',
+      entryDir => {
+        // 获取源文件对象
+        plus.io.resolveLocalFileSystemURL(
+          sourcePath,
+          entryFile => {
+            // 执行复制操作:将 sourcePath 复制到 _downloads/ 下,并重命名
+            entryFile.copyTo(
+              entryDir,
+              fileName,
+              newEntry => {
+                console.log('文件已复制到公共目录:', newEntry.fullPath);
+
+                uni.showToast({
+                  title: '已保存到下载目录',
+                  icon: 'none',
+                  duration: 3000,
+                });
+
+                // 3. 预览打开
+                // 注意:打开公共目录的文件推荐用 plus.runtime.openFile
+                // uni.openDocument 有时对公共路径支持不好
+                plus.runtime.openFile(
+                  newEntry.fullPath,
+                  {},
+                  e => {
+                    console.log('打开成功');
+                  },
+                  e => {
+                    console.error('打开失败', e);
+                    uni.showToast({ title: '无法打开文件', icon: 'none' });
+                  }
+                );
+              },
+              e => {
+                console.error('复制文件失败:', e);
+                uni.showToast({ title: '保存到公共目录失败', icon: 'none' });
+              }
+            );
+          },
+          e => {
+            console.error('读取源文件失败:', e);
+          }
+        );
+      },
+      e => {
+        console.error('读取下载目录失败:', e);
+      }
+    );
+  }
+
+  function saveTempFileToDownloads(tempPath, fileName) {
+    // 1. 获取系统 Downloads 目录对象
+    // "_downloads/" 是 H5+ API 对安卓公共下载目录的映射
+    plus.io.resolveLocalFileSystemURL(
+      '_downloads/',
+      entryDir => {
+        // 2. 获取临时文件对象
+        plus.io.resolveLocalFileSystemURL(
+          tempPath,
+          entryFile => {
+            // 3. 执行复制:将临时文件复制到 Downloads 目录
+            entryFile.copyTo(
+              entryDir,
+              fileName,
+              newEntry => {
+                console.log('文件路径:' + newEntry.fullPath);
+
+                uni.showToast({
+                  title: '已保存至Downloads',
+                  icon: 'none',
+                });
+
+                // 4. (可选) 打开预览
+                plus.runtime.openFile(newEntry.fullPath);
+              },
+              e => {
+                console.error('复制失败', e);
+                uni.showToast({ title: '保存失败', icon: 'none' });
+              }
+            );
+          },
+          e => {
+            console.error('读取临时文件失败', e);
+          }
+        );
+      },
+      e => {
+        console.error('无法访问下载目录', e);
+        // 这里如果报错,通常是权限没给或者 Android 11+ 读写受限
+      }
+    );
+  }
+
   // 下载文件
   const downloadFile = async file => {
     console.log('🚀 ~ downloadFile ~ file:', file);
     const { filePath: fileUrl, name: fileName } = file;
+
     if (!fileUrl) {
       uni.showToast({ title: t('operation.fileUrlEmpty'), icon: 'none' });
       return;
@@ -460,23 +601,25 @@
         success: res => {
           console.log('🚀 ~ downloadFile ~ res:', res);
           if (res.statusCode === 200) {
-            uni.saveFile({
-              tempFilePath: res.tempFilePath,
-              success: res => {
-                console.log('🚀 ~ downloadFile saveFile ~ res:', res);
-                uni.showToast({
-                  title: t('operation.downloadSuccess'),
-                  icon: 'none',
-                });
-              },
-              fail: err => {
-                console.log('🚀 ~ downloadFile saveFile ~ err:', err);
-                uni.showToast({
-                  title: t('operation.downloadFail'),
-                  icon: 'none',
-                });
-              },
-            });
+            saveTempFileToDownloads(res.tempFilePath, fileName);
+            // uni.saveFile({
+            //   tempFilePath: res.tempFilePath,
+            //   success: res => {
+            //     // console.log('🚀 ~ downloadFile saveFile ~ res:', res);
+            //     uni.showToast({
+            //       title: t('operation.downloadSuccess'),
+            //       icon: 'none',
+            //     });
+            //     copyToPublicAndOpen(res.savedFilePath, fileName);
+            //   },
+            //   fail: err => {
+            //     console.log('🚀 ~ downloadFile saveFile ~ err:', err);
+            //     uni.showToast({
+            //       title: t('operation.downloadFail'),
+            //       icon: 'none',
+            //     });
+            //   },
+            // });
           }
         },
         fail: err => {
@@ -615,10 +758,111 @@
   defineExpose({
     submitForm,
   });
+
+  // 假设你已经定义了 props
+  // const props = defineProps({ reportData: Object });
+
+  // --- 1. 公共工具函数 (保持不变) ---
+  const parseNumber = val => {
+    let num = parseFloat(val);
+    if (isNaN(num)) num = 0;
+    if (num < 0) num = 0;
+    return Number(num.toFixed(2));
+  };
+
+  // --- 2. 防抖逻辑定义 (保持不变) ---
+
+  // 列表变化 -> 算总和
+  const handleListChange = useDebounceFn(() => {
+    let total = 0;
+    form.reportFuels.forEach(item => {
+      const formattedVal = parseNumber(item.customFuel);
+      if (item.customFuel !== formattedVal) {
+        item.customFuel = formattedVal;
+      }
+      total += formattedVal;
+    });
+    // 更新 dailyFuel,这会触发下面的 dailyFuel watcher
+    dailyFuel.value = parseNumber(total);
+  }, 500);
+
+  // dailyFuel 变化 -> 格式化自身 & 同步 form
+  const handleDailyFuelChange = useDebounceFn(() => {
+    const formattedVal = parseNumber(dailyFuel.value);
+
+    if (dailyFuel.value !== formattedVal) {
+      dailyFuel.value = formattedVal;
+    }
+    form.dailyFuel = formattedVal;
+  }, 500);
+
+  // --- 3. 关键修改:初始化逻辑 ---
+
+  const initDailyFuel = () => {
+    // 获取 props 中的值 (转为数字以做判断)
+    const propVal = props.reportData?.dailyFuel;
+    const numPropVal = parseFloat(propVal);
+
+    // 判断规则:如果有值且不是 NaN (根据需求,你也可以加上 > 0 的判断)
+    // 这里假设只要 props 里有有效数字,就以 props 为准
+    const hasPropValue = !isNaN(numPropVal) && propVal !== null && propVal !== '';
+
+    if (hasPropValue) {
+      // 【情况A】Props 有值:直接使用 Props
+      const val = parseNumber(numPropVal);
+      dailyFuel.value = val;
+      form.dailyFuel = val;
+      // // 顺便把列表里的每一项也格式化一下(可选)
+      // form.reportFuels.forEach(item => {
+      //   item.customFuel = parseNumber(item.customFuel);
+      // });
+    } else {
+      // 【情况B】Props 没值:根据列表计算初始值
+      // 这里我们不使用防抖,直接立即计算一次,确保显示正确
+      let total = 0;
+      form.reportFuels.forEach(item => {
+        // 初始化时顺便把列表里的脏数据格式化了
+        const val = parseNumber(item.customFuel);
+        item.customFuel = val;
+        total += val;
+      });
+      const finalTotal = parseNumber(total);
+      dailyFuel.value = finalTotal;
+      form.dailyFuel = finalTotal;
+    }
+  };
+
+  // --- 4. 监听器 (关键修改) ---
+
+  // 监听列表:【注意】这里去掉了 immediate: true
+  // 因为初始化我们已经在上面手动 initDailyFuel() 里做过了
+  // 现在只监听用户后续的“修改”操作
+  // watch(
+  //   () => form.reportFuels,
+  //   () => {
+  //     handleListChange();
+  //   },
+  //   { deep: true } // 只有 deep,没有 immediate
+  // );
+
+  // 监听 dailyFuel
+  watch(
+    () => dailyFuel.value,
+    (newVal, oldVal) => {
+      // 只有当值真的变了,才触发防抖更新
+      // 避免初始化赋值时触发不必要的逻辑
+      if (newVal !== oldVal) {
+        handleDailyFuelChange();
+      }
+    }
+  );
 </script>
 
 <template>
   <scroll-view scroll-y="true" class="report-form">
+    <scroll-view class="steps" scroll-x :scroll-y="false">
+      <uni-steps :options="steps" :active="steps.length - 1" :style="{ width: `${steps.length * 100}px` }" />
+    </scroll-view>
     <uni-forms
       ref="reportFormRef"
       labelWidth="140px"
@@ -690,6 +934,18 @@
             :disabled="true"
             v-model="unselectedEquipmentNames" />
         </uni-forms-item>
+        <uni-forms-item :required="isRequired" class="form-item" label="当日油耗(L):">
+          <uni-easyinput
+            class="digit-item"
+            type="number"
+            :inputBorder="false"
+            :clearable="false"
+            :styles="{ disableColor: '#fff' }"
+            :placeholder="inputPlaceholder"
+            :disabled="formDisable"
+            v-model="dailyFuel" />
+        </uni-forms-item>
+
         <uni-forms-item
           class="form-item"
           :label="`${$t('ruiDu.dailyProductionDynamic')}:`"
@@ -708,7 +964,11 @@
             :maxlength="1000" />
         </uni-forms-item>
         <!-- 下步工作计划 -->
-        <uni-forms-item class="form-item" :label="`${$t('ruiDu.nextWorkPlan')}:`" name="nextPlan">
+        <uni-forms-item
+          class="form-item"
+          :required="isRequired"
+          :label="`${$t('ruiDu.nextWorkPlan')}:`"
+          name="nextPlan">
           <uni-easyinput
             style="text-align: right"
             type="textarea"
@@ -782,8 +1042,8 @@
           </view>
           <view class="file-list item-col" v-else>
             <view v-for="(file, index) in form.attachments" :key="index">
-              {{ file.filename }}
-              <!-- <button @click="downloadFile(file)" type="primary" size="mini" class="file-picker-btn">下载文件</button> -->
+              <span>{{ file.filename }}</span>
+              <button @click="downloadFile(file)" type="primary" size="mini" class="file-picker-btn">下载文件</button>
             </view>
           </view>
         </uni-forms-item>
@@ -905,6 +1165,37 @@
             :type="'textarea'"></uni-easyinput>
         </uni-forms-item>
       </div>
+
+      <div v-for="(fuel, index) in form.reportFuels" :key="index" class="content">
+        <div class="content-title">{{ fuel.deviceCode }}</div>
+        <uni-forms-item class="form-item" label="设备名称:">
+          <view style="text-align: right; width: 100%; padding-right: 10px; box-sizing: border-box">{{
+            fuel.deviceName
+          }}</view>
+        </uni-forms-item>
+        <uni-forms-item class="form-item" label="发生日期:">
+          <view style="text-align: right; width: 100%; padding-right: 10px; box-sizing: border-box">{{
+            dayjs(fuel.queryDate).format('YYYY-MM-DD')
+          }}</view>
+        </uni-forms-item>
+        <uni-forms-item class="form-item" label="中航北斗油耗(L):">
+          <view style="text-align: right; width: 100%; padding-right: 10px; box-sizing: border-box">{{
+            (fuel.zhbdFuel ?? 0).toFixed(2)
+          }}</view>
+        </uni-forms-item>
+        <uni-forms-item class="form-item" label="实际油耗(L):">
+          <uni-easyinput
+            class="digit-item"
+            type="number"
+            :inputBorder="false"
+            :clearable="false"
+            :styles="{ disableColor: '#fff' }"
+            :placeholder="inputPlaceholder"
+            :disabled="formDisable"
+            v-model="fuel.customFuel"
+            @input="handleListChange" />
+        </uni-forms-item>
+      </div>
     </uni-forms>
   </scroll-view>
 
@@ -932,6 +1223,12 @@
     color: #333;
   }
 
+  .steps {
+    :deep(.uni-scroll-view > .uni-scroll-view) {
+      padding: 10px 0 20px 0;
+    }
+  }
+
   :deep(.uni-textarea-textarea:disabled),
   :deep(.uni-input-input:disabled) {
     color: #333;
@@ -995,7 +1292,7 @@
     flex-direction: column;
     align-items: end;
     justify-content: end;
-    max-width: 300px;
+    max-width: 100%;
 
     & > view {
       width: 100%;
@@ -1004,6 +1301,18 @@
       display: flex;
       align-items: center;
       justify-content: space-between;
+
+      & > span {
+        flex: 1;
+        word-break: break-all;
+        overflow-wrap: anywhere;
+        margin-right: 10px;
+      }
+
+      & > .file-picker-btn {
+        flex-shrink: 0;
+        margin-left: 0;
+      }
     }
   }
 

+ 100 - 136
pages/ruiDu/index.vue

@@ -9,18 +9,12 @@
             v-model="orderName"
             :styles="inputStyles"
             :placeholderStyle="placeholderStyle"
-            :placeholder="$t('operation.searchText')"
-          >
+            :placeholder="$t('operation.searchText')">
           </uni-easyinput>
         </uni-col>
         <uni-col :span="5" class="flex-row justify-end">
-          <button
-            class="mini-btn"
-            type="primary"
-            size="mini"
-            @click="searchList"
-          >
-            {{ $t("operation.search") }}
+          <button class="mini-btn" type="primary" size="mini" @click="searchList">
+            {{ $t('operation.search') }}
           </button>
         </uni-col>
       </uni-row>
@@ -31,87 +25,63 @@
           class="item-module flex-row align-center justify-between"
           :class="{
             tobeFilled: item.status == 0,
-          }"
-        >
+          }">
           <!-- 创建时间 -->
           <view class="module-name">
-            {{ item.createTime ? formatDate(item.createTime) : "" }}
+            {{ item.createTime ? formatDate(item.createTime) : '' }}
           </view>
           <!-- 工单状态 -->
           <view
             class="module-status"
             :class="{
               pending: item.status == 0, //待填写
-            }"
-          >
+            }">
             {{ fillStatusDict[item.status] }}
           </view>
         </view>
         <view class="item-content">
           <!-- 带班干部 -->
           <view class="item-title flex-row">
-            <span class="item-title-width"
-              >{{ $t("ruiDu.shiftLeader") }}:</span
-            >
+            <span class="item-title-width">{{ $t('ruiDu.shiftLeader') }}:</span>
             <span>{{ item.responsiblePersonNames }}</span>
           </view>
           <!-- 日报名称 -->
           <view class="item-title flex-row">
-            <span class="item-title-width"
-              >{{ $t("ruiDu.reportName") }}:</span
-            >
+            <span class="item-title-width">{{ $t('ruiDu.reportName') }}:</span>
             <span>{{ item.reportName }}</span>
           </view>
           <!-- 项目 -->
           <view class="item-title flex-row">
-            <span class="item-title-width"
-              >{{ $t("ruiDu.project") }}:</span
-            >
+            <span class="item-title-width">{{ $t('ruiDu.project') }}:</span>
             <span>{{ item.contractName }}</span>
           </view>
           <!-- 任务 -->
           <view class="item-title flex-row">
-            <span class="item-title-width"
-              >{{ $t("ruiDu.task") }}:</span
-            >
+            <span class="item-title-width">{{ $t('ruiDu.task') }}:</span>
             <span>{{ item.taskName }}</span>
           </view>
 
           <!-- 创建时间 -->
           <view class="item-title flex-row">
-            <span class="item-title-width">{{ $t("operation.createTime") }}:</span>
-            <span>{{
-              item.createTime ? formatTime(item.createTime) : ""
-            }}</span>
+            <span class="item-title-width">{{ $t('operation.createTime') }}:</span>
+            <span>{{ item.createTime ? formatTime(item.createTime) : '' }}</span>
           </view>
           <!-- 填写时间 -->
           <view class="item-title flex-row">
-            <span class="item-title-width"
-              >{{ $t("operation.fillTime") }}:</span
-            >
-            <span>{{
-              item.fillTime ? formatTime(item.fillTime) : ""
-            }}</span>
+            <span class="item-title-width">{{ $t('operation.fillTime') }}:</span>
+            <span>{{ item.fillTime ? formatTime(item.fillTime) : '' }}</span>
           </view>
-          
         </view>
         <view class="item-btn flex-row align-center justify-end">
           <!--  状态:0(待填写),1(已完成),2(填写中),3(忽略) -->
 
           <!-- 查看 -->
-          <button
-            type="primary"
-            plain="true"
-            @click="navigatorDetail(item)"
-          >
-            {{ $t("operation.view") }}
+          <button type="primary" plain="true" @click="navigatorDetail(item)">
+            {{ $t('operation.view') }}
           </button>
           <!-- 填写 -->
-          <button
-            type="primary"
-            @click="navigatorEdit(item)"
-          >
-            {{ $t("operation.fill") }}
+          <button v-show="item.status === 0" type="primary" @click="navigatorEdit(item)">
+            {{ $t('operation.fill') }}
           </button>
         </view>
       </view>
@@ -120,106 +90,100 @@
 </template>
 
 <script setup>
-import { ref, reactive, nextTick } from "vue";
-import { onShow } from "@dcloudio/uni-app";
-import dayjs from "dayjs";
-import {
-  getRuiDuReportPage,
-} from "@/api/ruiDu";
-import { getUserId, getDeptId } from "@/utils/auth.js";
-import { useDataDictStore } from "@/store/modules/dataDict";
+  import { ref, reactive, nextTick } from 'vue';
+  import { onShow } from '@dcloudio/uni-app';
+  import dayjs from 'dayjs';
+  import { getRuiDuReportPage } from '@/api/ruiDu';
+  import { useDataDictStore } from '@/store/modules/dataDict';
 
-// 获取字典项
-const { getStrDictOptions } = useDataDictStore();
-// 填写状态
-const fillStatusDict = reactive({});
-getStrDictOptions("operation_fill_order_status").map((item) => {
-  fillStatusDict[item.value] = item.label;
-});
-console.log("🚀 ~ getDataDictList ~ fillStatusDict:", fillStatusDict);
+  // 获取字典项
+  const { getStrDictOptions } = useDataDictStore();
+  // 填写状态
+  const fillStatusDict = reactive({});
+  getStrDictOptions('operation_fill_order_status').map(item => {
+    fillStatusDict[item.value] = item.label;
+  });
+  console.log('🚀 ~ getDataDictList ~ fillStatusDict:', fillStatusDict);
 
-const orderName = ref("");
-const placeholderStyle = ref("color:#797979;font-weight:500;font-size:16px");
-const inputStyles = reactive({
-  backgroundColor: "#FFFFFF",
-  color: "#797979",
-});
-const paging = ref(null);
-// v-model绑定的这个变量不要在分页请求结束中自己赋值,直接使用即可
-const dataList = ref([]);
+  const orderName = ref('');
+  const placeholderStyle = ref('color:#797979;font-weight:500;font-size:16px');
+  const inputStyles = reactive({
+    backgroundColor: '#FFFFFF',
+    color: '#797979',
+  });
+  const paging = ref(null);
+  // v-model绑定的这个变量不要在分页请求结束中自己赋值,直接使用即可
+  const dataList = ref([]);
 
-// @query所绑定的方法不要自己调用!!需要刷新列表数据时,只需要调用paging.value.reload()即可
-const queryList = (pageNo, pageSize) => {
-  // 此处请求仅为演示,请替换为自己项目中的请求
-  getRuiDuReportPage({
-    pageNo,
-    pageSize,
-  })
-    .then((res) => {
-      // 将请求结果通过complete传给z-paging处理,同时也代表请求结束,这一行必须调用
-      paging.value.complete(res.data.list);
+  // @query所绑定的方法不要自己调用!!需要刷新列表数据时,只需要调用paging.value.reload()即可
+  const queryList = (pageNo, pageSize) => {
+    // 此处请求仅为演示,请替换为自己项目中的请求
+    getRuiDuReportPage({
+      pageNo,
+      pageSize,
     })
-    .catch((res) => {
-      // 如果请求失败写paging.value.complete(false);
-      // 注意,每次都需要在catch中写这句话很麻烦,z-paging提供了方案可以全局统一处理
-      // 在底层的网络请求抛出异常时,写uni.$emit('z-paging-error-emit');即可
-      paging.value.complete(false);
+      .then(res => {
+        // 将请求结果通过complete传给z-paging处理,同时也代表请求结束,这一行必须调用
+        paging.value.complete(res.data.list);
+      })
+      .catch(res => {
+        // 如果请求失败写paging.value.complete(false);
+        // 注意,每次都需要在catch中写这句话很麻烦,z-paging提供了方案可以全局统一处理
+        // 在底层的网络请求抛出异常时,写uni.$emit('z-paging-error-emit');即可
+        paging.value.complete(false);
+      });
+  };
+  const searchList = () => {
+    paging.value.reload();
+  };
+  const navigatorDetail = item => {
+    console.log('item', item);
+    uni.navigateTo({
+      url: '/pages/ruiDu/detail?id=' + item.id,
     });
-};
-const searchList = () => {
-  paging.value.reload();
-};
-const navigatorDetail = (item) => {
-  console.log("item", item);
-  uni.navigateTo({
-    url:
-      "/pages/ruiDu/detail?id=" + item.id,
-  });
-};
-const navigatorEdit = (item) => {
-  console.log("item", item);
-  uni.navigateTo({
-    url:
-      "/pages/ruiDu/edit?id=" + item.id,
-  });
-};
-const formatDate = (time) => {
-  return dayjs(time).format("YYYY-MM-DD");
-};
-const formatTime = (time) => {
-  return dayjs(time).format("YYYY-MM-DD HH:mm:ss");
-};
-
+  };
+  const navigatorEdit = item => {
+    console.log('item', item);
+    uni.navigateTo({
+      url: '/pages/ruiDu/edit?id=' + item.id,
+    });
+  };
+  const formatDate = time => {
+    return dayjs(time).format('YYYY-MM-DD');
+  };
+  const formatTime = time => {
+    return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
+  };
 
-onShow(() => {
-  nextTick(() => {
-    searchList();
+  onShow(() => {
+    nextTick(() => {
+      searchList();
+    });
   });
-});
 </script>
 
 <style lang="scss" scoped>
-@import "@/style/work-order.scss";
-
-.search-row {
-  height: 35px;
-  background: #f3f5f9;
+  @import '@/style/work-order.scss';
 
-  .mini-btn {
+  .search-row {
     height: 35px;
-    line-height: 35px;
-    width: 100%;
-    margin-left: 8px;
+    background: #f3f5f9;
+
+    .mini-btn {
+      height: 35px;
+      line-height: 35px;
+      width: 100%;
+      margin-left: 8px;
+    }
   }
-}
 
-.item {
-  width: 100%;
-  // height: 245px;
-  min-height: 245px;
-  // max-height: fit-content;
-  background: #ffffff;
-  border-radius: 6px;
-  margin-top: 10px;
-}
+  .item {
+    width: 100%;
+    // height: 245px;
+    min-height: 245px;
+    // max-height: fit-content;
+    background: #ffffff;
+    border-radius: 6px;
+    margin-top: 10px;
+  }
 </style>

+ 41 - 0
utils/useDebounceFn.js

@@ -0,0 +1,41 @@
+import { onUnload } from '@dcloudio/uni-app';
+
+/**
+ * 自定义防抖函数钩子
+ * @param {Function} fn 需要防抖执行的函数
+ * @param {Number} delay 延迟时间(毫秒)
+ * @returns {Function} 经过防抖处理的函数
+ */
+export function useDebounceFn(fn, delay = 200) {
+  let timer = null;
+
+  const debounced = (...args) => {
+    // 如果已有定时器,清除它
+    if (timer) clearTimeout(timer);
+
+    // 设置新的定时器
+    timer = setTimeout(() => {
+      fn.apply(this, args);
+    }, delay);
+  };
+
+  // 挂载一个 cancel 方法,以便在外部需要时手动清除
+  debounced.cancel = () => {
+    if (timer) {
+      clearTimeout(timer);
+      timer = null;
+    }
+  };
+
+  // 在组件卸载时自动清理定时器,避免内存泄漏
+  // try-catch 是为了防止在组件外部(非setup环境)使用时报错
+  try {
+    onUnload(() => {
+      debounced.cancel();
+    });
+  } catch (e) {
+    // 如果不是在组件 setup 中调用,忽略 onUnmounted 错误
+  }
+
+  return debounced;
+}