yyb
8 天以前 9581c0ae9f0d9a2e92744f3dca78960780b9a2df
src/pages/equipmentManagement/upkeep/add.vue
@@ -1,78 +1,178 @@
<template>
   <view class="upkeep-add">
      <!-- 使用通用页面头部组件 -->
      <PageHeader title="新增保养" @back="goBack" />
      <PageHeader :title="operationType === 'edit' ? '编辑保养计划' : '新增保养计划'" @back="goBack" />
      
      <!-- 表单内容 -->
      <u-form @submit="sendForm" ref="formRef" label-width="110" input-align="right" error-message-align="right">
      <u-form ref="formRef" :model="form" :rules="formRules" label-width="110px">
         <!-- 基本信息 -->
         <u-cell-group title="基本信息">
            <u-form-item label="设备名称" prop="deviceName" required border-bottom>
               <u-input
                  v-model="deviceNameText"
                  placeholder="请选择设备名称"
                  readonly
                  @click="showDevicePicker"
                  clearable
               >
                  <template #suffix>
                     <u-icon name="scan" @click.stop="startScan" class="scan-icon" />
                  </template>
               </u-input>
            </u-form-item>
            <u-form-item label="规格型号" border-bottom>
               <u-input
                  v-model="form.deviceModel"
                  placeholder="请输入规格型号"
                  readonly
                  clearable
               />
            </u-form-item>
            <u-form-item label="保养日期" prop="upkeepDate" required border-bottom>
               <u-input
                  v-model="form.upkeepDate"
                  placeholder="请选择保养日期"
                  readonly
                  @click="showDatePicker"
                  clearable
               />
            </u-form-item>
         </u-cell-group>
         <u-form-item label="设备名称" prop="deviceNameText" required border-bottom>
            <u-input
               v-model="form.deviceNameText"
               placeholder="请选择设备名称"
               readonly
               @click="showDevicePicker"
               clearable
            />
            <template #right>
               <u-icon name="scan" @click="startScan" class="scan-icon" />
            </template>
         </u-form-item>
         <u-form-item label="规格型号" prop="deviceModel" border-bottom>
            <u-input
               v-model="form.deviceModel"
               placeholder="请输入规格型号"
               readonly
               clearable
            />
         </u-form-item>
         <u-form-item label="计划保养日期" prop="maintenancePlanTime" required border-bottom>
            <u-input
               v-model="form.maintenancePlanTime"
               placeholder="请选择计划保养日期"
               readonly
               @click="showDatePicker"
               clearable
            />
            <template #right>
               <u-icon name="arrow-right" @click="showDatePicker" />
            </template>
         </u-form-item>
         <u-form-item label="保养人" prop="maintenancePerson" required border-bottom>
            <u-input
               v-model="form.maintenancePerson"
               placeholder="请选择保养人"
               readonly
               @click="showPersonPicker"
               clearable
            />
            <template #right>
               <u-icon name="arrow-right" @click="showPersonPicker" />
            </template>
         </u-form-item>
         <u-form-item label="保养部位" prop="maintenanceLocation" required border-bottom>
            <u-input
               v-model="form.maintenanceLocation"
               placeholder="请输入保养部位"
               clearable
            />
         </u-form-item>
         <u-form-item label="保养内容" prop="maintenanceItems" required border-bottom>
            <u-input
               v-model="form.maintenanceItems"
               placeholder="请输入保养内容"
               clearable
            />
         </u-form-item>
         <u-form-item label="附件" border-bottom>
            <view class="attachment-upload">
               <view class="upload-buttons">
                  <u-button
                     type="primary"
                     @click="chooseAttachment('camera')"
                     :loading="uploading"
                     :disabled="uploading"
                     :customStyle="{ marginRight: '10px', flex: 1 }"
                  >
                     <u-icon name="camera" size="18" color="#fff" style="margin-right: 5px;"></u-icon>
                     {{ uploading ? '上传中...' : '拍照' }}
                  </u-button>
                  <u-button
                     type="success"
                     @click="chooseAttachment('album')"
                     :loading="uploading"
                     :disabled="uploading"
                     :customStyle="{ flex: 1 }"
                  >
                     <u-icon name="photo" size="18" color="#fff" style="margin-right: 5px;"></u-icon>
                     {{ uploading ? '上传中...' : '相册' }}
                  </u-button>
               </view>
               <view v-if="attachmentList.length" class="attachment-list">
                  <view
                     v-for="(file, index) in attachmentList"
                     :key="file.id || index"
                     class="attachment-item"
                  >
                     <image
                        :src="getFileAccessUrl(file)"
                        mode="aspectFill"
                        class="attachment-preview"
                        @click="previewAttachment(index)"
                     />
                     <view class="attachment-delete" @click="removeAttachment(file, index)">
                        <u-icon name="close" size="12" color="#fff" />
                     </view>
                  </view>
               </view>
               <view v-else class="attachment-empty">暂无附件,可拍照或从相册选择</view>
            </view>
         </u-form-item>
         
         <!-- 提交按钮 -->
         <view class="footer-btns">
            <u-button class="cancel-btn" @click="goBack">取消</u-button>
            <u-button class="save-btn" type="primary" @click="sendForm" :loading="loading">保存</u-button>
            <u-button class="save-btn" @click="sendForm" :loading="loading">保存</u-button>
         </view>
      </u-form>
      <!-- 设备选择器 -->
      <u-popup v-model="showDevice" mode="bottom">
         <u-picker
            v-model="devicePickerValue"
            :columns="deviceColumns"
            @confirm="onDeviceConfirm"
            @cancel="showDevice = false"
         />
      </u-popup>
      <!-- 日期选择器 -->
      <u-popup v-model="showDate" mode="bottom">
         <u-datetime-picker
            v-model="currentDate"
            title="选择日期"
            @confirm="onDateConfirm"
            @cancel="showDate = false"
         />
      </u-popup>
      <up-action-sheet
         :show="showDevice"
         :actions="deviceActions"
         title="选择设备"
         @select="onDeviceConfirm"
         @close="showDevice = false"
      />
      <!-- 保养人选择器 -->
      <up-action-sheet
         :show="showPerson"
         :actions="personActions"
         title="选择保养人"
         @select="onPersonConfirm"
         @close="showPerson = false"
      />
<up-datetime-picker
         :show="showDate"
         v-model="pickerDateValue"
         @confirm="onDateConfirm"
         @cancel="showDate = false"
         mode="date"
      />
   </view>
</template>
<script setup>
// 替换 Vant 的 toast
// import { showToast } from 'vant';
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onShow, onUnload } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
import config from '@/config';
import { getToken } from '@/utils/auth';
import { getDeviceLedger } from '@/api/equipmentManagement/ledger';
import {
   addUpkeep,
   editUpkeep,
   getUpkeepById,
   listMaintenanceTaskFiles,
   delMaintenanceTaskFile,
} from '@/api/equipmentManagement/upkeep';
import { userListNoPageByTenantId } from '@/api/system/user';
import useUserStore from '@/store/modules/user';
import dayjs from "dayjs";
import { formatDateToYMD } from '@/utils/ruoyi';
// 替换 toast 方法
const userStore = useUserStore();
defineOptions({
   name: "设备保养计划表单",
});
const showToast = (message) => {
  uni.showToast({
    title: message,
@@ -80,30 +180,44 @@
  })
}
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
import { getDeviceLedger } from '@/api/equipmentManagement/ledger';
import { addUpkeep, editUpkeep, getUpkeepById } from '@/api/equipmentManagement/upkeep';
import dayjs from "dayjs";
import { showToast } from 'vant';
defineOptions({
   name: "设备保养计划表单",
});
const normalizeId = (raw) => {
   if (raw === null || raw === undefined) return undefined;
   const val = String(raw).trim();
   if (!val || val === 'undefined' || val === 'null') return undefined;
   return val;
};
// 表单引用
const formRef = ref(null);
const operationType = ref('add');
const loading = ref(false);
const uploading = ref(false);
const attachmentList = ref([]);
const showDevice = ref(false);
const devicePickerValue = ref([]);
const showPerson = ref(false);
const showDate = ref(false);
const pickerDateValue = ref(Date.now());
const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]);
// 设备选项
const deviceOptions = ref([]);
const deviceNameText = ref('');
// 保养人选项
const userOptions = ref([]);
// 转换为 action-sheet 需要的格式
const deviceActions = computed(() => {
   return deviceOptions.value.map(item => ({
      text: item.deviceName,
      value: item.id,
      data: item
   }));
});
const personActions = computed(() => {
   return userOptions.value.map(item => ({
      name: item.nickName,
      value: item.userId,
   }));
});
// 扫码相关状态
const isScanning = ref(false);
@@ -113,6 +227,9 @@
const formRules = {
   deviceLedgerId: [{ required: true, trigger: "change", message: "请选择设备名称" }],
   maintenancePlanTime: [{ required: true, trigger: "change", message: "请选择计划保养日期" }],
   maintenancePerson: [{ required: true, trigger: "change", message: "请选择保养人" }],
   maintenanceLocation: [{ required: true, trigger: "blur", message: "请输入保养部位" }],
   maintenanceItems: [{ required: true, trigger: "blur", message: "请输入保养内容" }],
};
// 使用 ref 声明表单数据
@@ -120,14 +237,10 @@
   deviceLedgerId: undefined, // 设备ID
   deviceModel: undefined, // 规格型号
   maintenancePlanTime: dayjs().format("YYYY-MM-DD"), // 计划保养日期
});
// 设备选择器列
const deviceColumns = computed(() => {
   return deviceOptions.value.map(item => ({
      text: item.deviceName,
      value: item.id
   }));
   maintenancePerson: userStore.nickName || undefined, // 保养人
   maintenanceLocation: undefined, // 保养部位
   maintenanceItems: undefined, // 保养内容
   tempFileIds: [], // 本次上传附件的临时文件ID,保存时提交
});
// 加载设备列表
@@ -140,13 +253,202 @@
   }
};
// 设置设备规格型号
const setDeviceModel = (id) => {
   const option = deviceOptions.value.find((item) => item.id === id);
   if (option) {
      form.value.deviceModel = option.deviceModel;
      deviceNameText.value = option.deviceName;
// 加载保养人列表
const loadUserOptions = async () => {
   try {
      const { data } = await userListNoPageByTenantId();
      userOptions.value = data || [];
   } catch (e) {
      showToast('获取保养人列表失败');
   }
};
// 附件相关
const extractTempFileId = (uploadedFile) => {
   if (!uploadedFile) return undefined;
   const data = Array.isArray(uploadedFile) ? uploadedFile[0] : uploadedFile;
   return data?.tempId ?? data?.tempFileId ?? data?.id;
};
const syncTempFileIds = () => {
   form.value.tempFileIds = attachmentList.value
      .filter((item) => item.isTempFile)
      .map((item) => item.tempId ?? item.tempFileId)
      .filter((v) => v !== undefined && v !== null && v !== '');
};
const getFileAccessUrl = (file = {}) => {
   if (file?._localPreviewUrl) return file._localPreviewUrl;
   const url = file.url || file.tempPath || file.tempFilePath || file.path || '';
   if (!url) return '';
   if (String(url).startsWith('http') || String(url).startsWith('blob:') || String(url).startsWith('file:') || String(url).startsWith('wxfile:')) {
      return url;
   }
   const path = String(url).startsWith('/') ? url : `/${url}`;
   return `${config.fileUrl}${path}`;
};
const fetchAttachmentList = async (id) => {
   if (!id) {
      attachmentList.value = [];
      form.value.tempFileIds = [];
      return;
   }
   try {
      const { code, data } = await listMaintenanceTaskFiles({
         current: 1,
         size: 100,
         deviceMaintenanceId: id,
      });
      if (code === 200) {
         const records = data?.records || [];
         attachmentList.value = records.map((file) => ({
            ...file,
            isTempFile: false,
         }));
      } else {
         attachmentList.value = [];
      }
   } catch (e) {
      attachmentList.value = [];
   }
   syncTempFileIds();
};
const chooseAttachment = (sourceType) => {
   const source = sourceType === 'camera' ? ['camera'] : ['album'];
   const remaining = 9 - attachmentList.value.length;
   if (remaining <= 0) {
      showToast('最多上传9张附件');
      return;
   }
   uni.chooseImage({
      count: Math.min(remaining, 9),
      sizeType: ['original', 'compressed'],
      sourceType: source,
      success: (res) => {
         const files = res.tempFiles || [];
         if (!files.length) return;
         uploadAttachments(files, res.tempFilePaths);
      },
      fail: () => {
         showToast('选择图片失败');
      },
   });
};
const handleUploadSuccess = (response, file) => {
   let uploadedFile = response?.data;
   if (Array.isArray(uploadedFile)) {
      uploadedFile = uploadedFile[0];
   }
   const tempId = extractTempFileId(uploadedFile);
   if (!tempId) {
      showToast('未获取到文件ID');
      return;
   }
   attachmentList.value.push({
      tempId,
      tempFileId: tempId,
      isTempFile: true,
      url: uploadedFile?.tempPath || uploadedFile?.url || uploadedFile?.downloadUrl || file.tempFilePath || file.path,
      tempPath: uploadedFile?.tempPath || '',
      name: uploadedFile?.originalName || uploadedFile?.originalFilename || file.name,
      _localPreviewUrl: file.tempFilePath || file.path || '',
   });
   syncTempFileIds();
};
const uploadAttachments = async (files, tempFilePaths = []) => {
   const token = getToken();
   if (!token) {
      showToast('登录已失效,请重新登录');
      return;
   }
   uploading.value = true;
   try {
      for (let i = 0; i < files.length; i++) {
         const file = files[i];
         const filePath = file.path || file.tempFilePath || tempFilePaths[i];
         if (!filePath) continue;
         await new Promise((resolve, reject) => {
            uni.uploadFile({
               url: `${config.baseUrl}/file/upload`,
               filePath,
               name: 'file',
               header: {
                  Authorization: `Bearer ${token}`,
               },
               success: (uploadRes) => {
                  try {
                     const parsed = JSON.parse(uploadRes.data || '{}');
                     if (uploadRes.statusCode === 200 && parsed.code === 200) {
                        handleUploadSuccess(parsed, {
                           ...file,
                           tempFilePath: filePath,
                           path: filePath,
                           name: file.name || `附件_${Date.now()}_${i}.jpg`,
                        });
                        resolve(parsed);
                     } else {
                        reject(new Error(parsed.msg || '上传失败'));
                     }
                  } catch (err) {
                     reject(new Error('上传响应解析失败'));
                  }
               },
               fail: () => reject(new Error('上传失败')),
            });
         });
      }
      showToast('上传成功');
   } catch (e) {
      showToast(e?.message || '上传失败');
   } finally {
      uploading.value = false;
   }
};
const previewAttachment = (index) => {
   const urls = attachmentList.value
      .map((item) => getFileAccessUrl(item))
      .filter(Boolean);
   if (!urls.length) return;
   uni.previewImage({
      urls,
      current: urls[index] || urls[0],
   });
};
const removeAttachment = (file, index) => {
   if (file?.isTempFile) {
      attachmentList.value.splice(index, 1);
      syncTempFileIds();
      return;
   }
   if (!file?.id) {
      attachmentList.value.splice(index, 1);
      syncTempFileIds();
      return;
   }
   uni.showModal({
      title: '提示',
      content: '确认删除该附件吗?',
      success: async (res) => {
         if (!res.confirm) return;
         try {
            const { code } = await delMaintenanceTaskFile(file.id);
            if (code === 200) {
               attachmentList.value.splice(index, 1);
               showToast('删除成功');
            } else {
               showToast('删除失败');
            }
         } catch (e) {
            showToast('删除失败');
         }
      },
   });
};
// 加载表单数据(编辑模式)
@@ -159,11 +461,15 @@
            form.value.deviceLedgerId = data.deviceLedgerId;
            form.value.deviceModel = data.deviceModel;
            form.value.maintenancePlanTime = dayjs(data.maintenancePlanTime).format("YYYY-MM-DD");
            form.value.maintenancePerson = data.maintenancePerson;
            form.value.maintenanceLocation = data.maintenanceLocation;
            form.value.maintenanceItems = data.maintenanceItems;
            // 设置设备名称显示
            const device = deviceOptions.value.find(item => item.id === data.deviceLedgerId);
            if (device) {
               deviceNameText.value = device.deviceName;
               form.value.deviceNameText = device.deviceName;
            }
            await fetchAttachmentList(id);
         }
      } catch (e) {
         showToast('获取详情失败');
@@ -171,27 +477,9 @@
   } else {
      // 新增模式
      operationType.value = 'add';
      attachmentList.value = [];
      form.value.tempFileIds = [];
   }
};
// 清除表单校验状态
const clearValidate = () => {
   formRef.value?.clearValidate();
};
// 重置表单数据和校验状态
const resetForm = () => {
   form.value = {
      deviceLedgerId: undefined,
      deviceModel: undefined,
      maintenancePlanTime: dayjs().format("YYYY-MM-DD"),
   };
   deviceNameText.value = '';
};
const resetFormAndValidate = () => {
   resetForm();
   clearValidate();
};
// 扫描二维码功能
@@ -222,29 +510,30 @@
   }
   
   isScanning.value = true;
   showToast('扫码成功,3秒后自动填充设备信息');
   showToast('扫码成功');
   
   // 3秒后处理扫码结果
   scanTimer.value = setTimeout(() => {
      processScanResult(scanResult);
      isScanning.value = false;
   }, 3000);
   }, 1000);
};
function getDeviceIdByRegExp(url) {
   // 匹配deviceId=后面的数字
   const reg = /deviceId=(\d+)/;
   const match = url.match(reg);
   // 如果匹配到结果,返回数字类型,否则返回null
   return match ? Number(match[1]) : null;
}
// 处理扫码结果并匹配设备
const processScanResult = (scanResult) => {
   // 在设备列表中查找匹配的设备
   // 假设二维码内容是设备名称或设备编号
   const matchedDevice = deviceOptions.value.find(device =>
      device.deviceName === scanResult ||
      device.deviceCode === scanResult ||
      device.id.toString() === scanResult
   );
   const deviceId = getDeviceIdByRegExp(scanResult);
   const matchedDevice = deviceOptions.value.find(item => item.id == deviceId);
   
   if (matchedDevice) {
      // 找到匹配的设备,自动填充
      form.value.deviceLedgerId = matchedDevice.id;
      deviceNameText.value = matchedDevice.deviceName;
      form.value.deviceNameText = matchedDevice.deviceName;
      form.value.deviceModel = matchedDevice.deviceModel;
      showToast('设备信息已自动填充');
   } else {
@@ -258,12 +547,32 @@
   showDevice.value = true;
};
// 显示保养人选择器
const showPersonPicker = () => {
   if (!userOptions.value.length) {
      showToast('暂无可选保养人');
      return;
   }
   showPerson.value = true;
};
// 确认保养人选择
const onPersonConfirm = (selected) => {
   const user = userOptions.value.find(item => item.userId === selected.value);
   form.value.maintenancePerson = user?.nickName || selected.name || '';
   showPerson.value = false;
};
// 确认设备选择
const onDeviceConfirm = ({ selectedValues, selectedOptions }) => {
   form.value.deviceLedgerId = selectedOptions[0].value;
   devicePickerValue.value = selectedValues;
const onDeviceConfirm = (selected) => {
   // selected 返回的是选中项
   form.value.deviceLedgerId = selected.value;
      form.value.deviceNameText = selected.name;
   const selectedDevice = deviceOptions.value.find(item => item.id === selected.value);
   if (selectedDevice) {
      form.value.deviceModel = selectedDevice.deviceModel;
   }
   showDevice.value = false;
   setDeviceModel(selectedOptions[0].value);
};
// 显示日期选择器
@@ -272,9 +581,8 @@
};
// 确认日期选择
const onDateConfirm = ({ selectedValues }) => {
   form.value.maintenancePlanTime = selectedValues.join('-');
   currentDate.value = selectedValues;
const onDateConfirm = (e) => {
   form.value.maintenancePlanTime = formatDateToYMD(e.value);
   showDate.value = false;
};
@@ -284,8 +592,9 @@
});
onMounted(() => {
   // 页面加载时获取设备列表和参数
   // 页面加载时获取设备列表、保养人列表和参数
   loadDeviceName();
   loadUserOptions();
   getPageParams();
});
@@ -300,25 +609,30 @@
const sendForm = async () => {
   try {
      // 手动验证表单
      await formRef.value?.validate();
      const valid = await formRef.value.validate();
      if (!valid) return;
      
      loading.value = true;
      const id = getPageId();
      
      syncTempFileIds();
      // 准备提交数据
      const submitData = { ...form.value };
      submitData.tempFileIds = form.value.tempFileIds || [];
      // 确保日期格式正确
      if (submitData.maintenancePlanTime && !submitData.maintenancePlanTime.includes(':')) {
         submitData.maintenancePlanTime = submitData.maintenancePlanTime + ' 00:00:00';
      }
      
      const { code } = id
      const result = id
         ? await editUpkeep({ id: id, ...submitData })
         : await addUpkeep(submitData);
      const { code } = result || {};
      
      if (code == 200) {
         showToast(`${id ? "编辑" : "新增"}计划成功`);
         setTimeout(() => {
            uni.removeStorageSync('repairId');
            uni.navigateBack();
         }, 1500);
      } else {
@@ -332,35 +646,36 @@
// 返回上一页
const goBack = () => {
   // 清除存储的id
   uni.removeStorageSync('repairId');
   uni.navigateBack();
};
// 获取页面参数
const getPageParams = () => {
   const pages = getCurrentPages();
   const currentPage = pages[pages.length - 1];
   const options = currentPage.options;
   // 根据是否有id参数来判断是新增还是编辑
   if (options.id) {
      // 编辑模式,获取详情
      loadForm(options.id);
   } else {
      // 新增模式
      loadForm();
   }
};
// 获取页面ID
const getPageId = () => {
   const pages = getCurrentPages();
   const currentPage = pages[pages.length - 1];
   const options = currentPage.options;
   return options.id;
   return normalizeId(uni.getStorageSync('repairId'));
};
// 获取页面参数
const getPageParams = () => {
   const id = getPageId();
   if (id) {
      loadForm(id);
   } else {
      operationType.value = 'add';
      attachmentList.value = [];
      form.value.tempFileIds = [];
      loadForm();
   }
};
onUnload(() => {
   uni.removeStorageSync('repairId');
});
</script>
<style scoped lang="scss">
@import '@/static/scss/form-common.scss';
.upkeep-add {
   min-height: 100vh;
   background: #f8f9fa;
@@ -420,4 +735,50 @@
   margin-left: 8px;
   cursor: pointer;
}
.attachment-upload {
   width: 100%;
}
.upload-buttons {
   display: flex;
   margin-bottom: 12px;
}
.attachment-list {
   display: flex;
   flex-wrap: wrap;
   gap: 10px;
}
.attachment-item {
   position: relative;
   width: 80px;
   height: 80px;
}
.attachment-preview {
   width: 80px;
   height: 80px;
   border-radius: 6px;
   background: #f5f5f5;
}
.attachment-delete {
   position: absolute;
   top: -6px;
   right: -6px;
   width: 18px;
   height: 18px;
   border-radius: 50%;
   background: rgba(0, 0, 0, 0.65);
   display: flex;
   align-items: center;
   justify-content: center;
}
.attachment-empty {
   font-size: 12px;
   color: #909399;
}
</style>