优化报修表单,新增验收人字段,调整状态管理,提升报修详情展示效果。
已添加2个文件
621 ■■■■■ 文件已修改
src/pages/equipmentManagement/repair/acceptance.vue 259 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/detail.vue 362 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/acceptance.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,259 @@
<template>
  <view class="repair-acceptance">
    <PageHeader title="设备报修验收"
                @back="goBack" />
    <view class="section"
          v-if="detail">
      <view class="section-title">报修信息</view>
      <view class="info-item">
        <text class="info-label">设备名称</text>
        <text class="info-value">{{ detail.deviceName || '-' }}</text>
      </view>
      <view class="info-item">
        <text class="info-label">验收人</text>
        <text class="info-value">{{ detail.acceptanceName || '-' }}</text>
      </view>
    </view>
    <u-form ref="formRef"
            :model="form"
            label-width="110">
      <u-cell-group title="验收信息">
        <u-form-item label="验收人"
                     border-bottom>
          <u-input v-model="form.acceptanceName"
                   disabled
                   placeholder="验收人" />
        </u-form-item>
        <u-form-item label="验收时间"
                     required
                     border-bottom>
          <u-input v-model="form.acceptanceTime"
                   placeholder="请选择验收时间"
                   readonly
                   @click="showDatePicker = true" />
          <template #right>
            <u-icon name="arrow-right"
                    @click="showDatePicker = true" />
          </template>
        </u-form-item>
        <u-form-item label="验收备注"
                     required
                     border-bottom>
          <u-textarea v-model="form.acceptanceRemark"
                      placeholder="请输入验收备注"
                      :maxlength="200"
                      count
                      :autoHeight="true" />
        </u-form-item>
      </u-cell-group>
      <view class="footer-btns">
        <u-button class="cancel-btn"
                  @click="goBack">取消</u-button>
        <u-button class="save-btn"
                  type="primary"
                  :loading="loading"
                  @click="submitAcceptance">确认验收</u-button>
      </view>
    </u-form>
    <up-datetime-picker :show="showDatePicker"
                        v-model="pickerDateValue"
                        mode="datetime"
                        @confirm="onDateConfirm"
                        @cancel="showDatePicker = false" />
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import useUserStore from "@/store/modules/user";
  import { getRepairById, acceptRepair } from "@/api/equipmentManagement/repair";
  import dayjs from "dayjs";
  defineOptions({ name: "设备报修验收" });
  const userStore = useUserStore();
  const repairId = ref("");
  const detail = ref(null);
  const loading = ref(false);
  const showDatePicker = ref(false);
  const pickerDateValue = ref(Date.now());
  const form = ref({
    id: "",
    acceptanceName: "",
    acceptanceTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
    acceptanceRemark: "",
  });
  const showToast = message => {
    uni.showToast({ title: message, icon: "none" });
  };
  const getCurrentUserName = () =>
    (userStore.nickName || userStore.name || "").trim();
  const canAccept = acceptanceName => {
    const current = getCurrentUserName();
    const target = (acceptanceName || "").trim();
    return current && target && current === target;
  };
  const loadDetail = async () => {
    if (!repairId.value) {
      showToast("参数错误");
      return;
    }
    try {
      uni.showLoading({ title: "加载中...", mask: true });
      const { code, data } = await getRepairById(repairId.value);
      if (code !== 200) {
        showToast("获取详情失败");
        return;
      }
      detail.value = data;
      if (Number(data.status) !== 3) {
        showToast("当前状态不可验收");
        setTimeout(() => goBack(), 1500);
        return;
      }
      if (!canAccept(data.acceptanceName)) {
        showToast("仅指定验收人可进行验收");
        setTimeout(() => goBack(), 1500);
        return;
      }
      form.value.id = data.id;
      form.value.acceptanceName = data.acceptanceName || "";
      form.value.acceptanceTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
    } catch (e) {
      showToast("获取详情失败");
    } finally {
      uni.hideLoading();
    }
  };
  const onDateConfirm = e => {
    form.value.acceptanceTime = dayjs(e.value).format("YYYY-MM-DD HH:mm:ss");
    pickerDateValue.value = e.value;
    showDatePicker.value = false;
  };
  const submitAcceptance = async () => {
    if (!form.value.acceptanceTime?.trim()) {
      showToast("请选择验收时间");
      return;
    }
    if (!form.value.acceptanceRemark?.trim()) {
      showToast("请输入验收备注");
      return;
    }
    try {
      loading.value = true;
      const { code, msg } = await acceptRepair({
        id: form.value.id,
        acceptanceTime: form.value.acceptanceTime,
        acceptanceRemark: form.value.acceptanceRemark.trim(),
      });
      if (code === 200) {
        showToast("验收成功");
        setTimeout(() => goBack(), 1500);
      } else {
        showToast(msg || "验收失败");
        loading.value = false;
      }
    } catch (e) {
      loading.value = false;
      showToast("验收失败");
    }
  };
  const goBack = () => {
    uni.removeStorageSync("repairId");
    uni.navigateBack();
  };
  onLoad(options => {
    repairId.value = options?.id || uni.getStorageSync("repairId") || "";
    form.value.id = repairId.value;
  });
  onMounted(() => {
    loadDetail();
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .repair-acceptance {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 5rem;
  }
  .section {
    margin: 12px 16px;
    background: #fff;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
  }
  .section-title {
    padding: 14px 16px;
    font-size: 16px;
    font-weight: 600;
    color: #303133;
    border-bottom: 1px solid #f0f0f0;
  }
  .info-item {
    display: flex;
    padding: 12px 16px;
    border-bottom: 1px solid #f8f8f8;
  }
  .info-label {
    width: 90px;
    font-size: 14px;
    color: #606266;
  }
  .info-value {
    flex: 1;
    font-size: 14px;
    color: #303133;
    text-align: right;
  }
  .footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 0.75rem 0;
    box-shadow: 0 -0.125rem 0.5rem rgba(0, 0, 0, 0.05);
    z-index: 1000;
  }
  .cancel-btn {
    font-size: 1rem;
    color: #ffffff;
    width: 6.375rem;
    background: #c7c9cc;
    border-radius: 2.5rem;
  }
  .save-btn {
    font-size: 1rem;
    color: #ffffff;
    width: 14rem;
    background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
    border-radius: 2.5rem;
  }
</style>
src/pages/equipmentManagement/repair/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,362 @@
<template>
  <view class="repair-detail">
    <PageHeader title="设备报修详情"
                @back="goBack" />
    <view v-if="detail"
          class="detail-content">
      <!-- 1. æŠ¥ä¿®ç™»è®° -->
      <view class="section">
        <view class="section-title">
          <text class="section-num">1</text>
          <text>报修登记</text>
        </view>
        <view class="info-grid">
          <view class="info-item">
            <text class="info-label">设备名称</text>
            <text class="info-value">{{ detail.deviceName || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">规格型号</text>
            <text class="info-value">{{ detail.deviceModel || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">报修日期</text>
            <text class="info-value">{{ formatDate(detail.repairTime) || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">报修人</text>
            <text class="info-value">{{ detail.repairName || detail.maintenanceName || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">验收人</text>
            <text class="info-value">{{ detail.acceptanceName || '-' }}</text>
          </view>
          <view class="info-item"
                v-if="Number(detail.status) !== 0">
            <text class="info-label">维修人</text>
            <text class="info-value">{{ detail.maintenancePerson || detail.maintenanceName || '-' }}</text>
          </view>
          <view class="info-item full">
            <text class="info-label">故障现象</text>
            <text class="info-value">{{ detail.remark || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">当前状态</text>
            <view class="info-value">
              <u-tag :type="getStatusTagType(detail.status)"
                     size="small">{{ getStatusText(detail.status) }}</u-tag>
            </view>
          </view>
        </view>
        <view class="image-section">
          <text class="image-title">设备问题图片</text>
          <view v-if="repairImageList.length"
                class="image-list">
            <image v-for="(file, index) in repairImageList"
                   :key="file.id || index"
                   :src="getFileAccessUrl(file)"
                   mode="aspectFill"
                   class="repair-image"
                   @click="previewImage(index)" />
          </view>
          <view v-else
                class="no-image">
            <up-icon name="photo"
                     size="40"
                     color="#c0c4cc" />
            <text>暂无设备问题图片</text>
          </view>
        </view>
      </view>
      <!-- 2. ç»´ä¿®å¤„理 -->
      <view class="section"
            v-if="showMaintenanceSection">
        <view class="section-title">
          <text class="section-num">2</text>
          <text>维修处理</text>
        </view>
        <view class="info-grid">
          <view class="info-item">
            <text class="info-label">维修人</text>
            <text class="info-value">{{ detail.maintenancePerson || detail.maintenanceName || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">维修时间</text>
            <text class="info-value">{{ formatDateTime(detail.maintenanceTime) || '-' }}</text>
          </view>
          <view class="info-item full">
            <text class="info-label">维修结果</text>
            <text class="info-value">{{ detail.maintenanceResult || '-' }}</text>
          </view>
        </view>
      </view>
      <!-- 3. éªŒæ”¶ -->
      <view class="section"
            v-if="showAcceptanceSection">
        <view class="section-title">
          <text class="section-num">3</text>
          <text>验收</text>
        </view>
        <view class="info-grid">
          <view class="info-item">
            <text class="info-label">验收人</text>
            <text class="info-value">{{ detail.acceptanceName || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">验收时间</text>
            <text class="info-value">{{ formatDateTime(detail.acceptanceTime) || '-' }}</text>
          </view>
          <view class="info-item full">
            <text class="info-label">验收备注</text>
            <text class="info-value">{{ detail.acceptanceRemark || '-' }}</text>
          </view>
        </view>
      </view>
    </view>
    <view v-else
          class="loading-wrap">
      <text>加载中...</text>
    </view>
  </view>
</template>
<script setup>
  import { ref, computed, onMounted } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import config from "@/config";
  import {
    getRepairById,
    getRepairFileList,
  } from "@/api/equipmentManagement/repair";
  import dayjs from "dayjs";
  defineOptions({ name: "设备报修详情" });
  const repairId = ref("");
  const detail = ref(null);
  const repairImageList = ref([]);
  const STATUS_MAP = {
    0: "待维修",
    3: "待验收",
    1: "完成",
    2: "维修失败",
  };
  const getStatusText = status => STATUS_MAP[Number(status)] || "-";
  const getStatusTagType = status => {
    const map = { 0: "error", 3: "warning", 1: "success", 2: "error" };
    return map[Number(status)] || "info";
  };
  const showMaintenanceSection = computed(() => Number(detail.value?.status) !== 0);
  const showAcceptanceSection = computed(() => Number(detail.value?.status) === 1);
  const formatDate = dateStr => {
    if (!dateStr) return "";
    return dayjs(dateStr).format("YYYY-MM-DD");
  };
  const formatDateTime = dateStr => {
    if (!dateStr) return "";
    return dayjs(dateStr).format("YYYY-MM-DD HH:mm:ss");
  };
  const normalizeFileUrl = (rawUrl = "") => {
    let fileUrl = rawUrl || "";
    const javaApi = config.baseUrl;
    const localPrefixes = ["wxfile://", "file://", "content://", "blob:", "data:"];
    if (localPrefixes.some(prefix => fileUrl.startsWith(prefix))) {
      return fileUrl;
    }
    if (fileUrl && fileUrl.indexOf("\\") > -1) {
      const lowerPath = fileUrl.toLowerCase();
      const uploadPathIndex = lowerPath.indexOf("uploadpath");
      if (uploadPathIndex > -1) {
        fileUrl = fileUrl.substring(uploadPathIndex).replace(/\\/g, "/");
      } else {
        fileUrl = fileUrl.replace(/\\/g, "/");
      }
    }
    fileUrl = fileUrl.replace(/^\/?uploadPath/, "/profile");
    if (fileUrl && !fileUrl.startsWith("http")) {
      if (!fileUrl.startsWith("/")) fileUrl = "/" + fileUrl;
      fileUrl = javaApi + fileUrl;
    }
    return fileUrl;
  };
  const getFileAccessUrl = (file = {}) => {
    if (file?.link) {
      if (String(file.link).startsWith("http")) return file.link;
      return normalizeFileUrl(file.link);
    }
    return normalizeFileUrl(file?.url || "");
  };
  const previewImage = index => {
    const urls = repairImageList.value
      .map(item => getFileAccessUrl(item))
      .filter(Boolean);
    if (!urls.length) return;
    uni.previewImage({ urls, current: urls[index] || urls[0] });
  };
  const loadDetail = async () => {
    if (!repairId.value) return;
    try {
      uni.showLoading({ title: "加载中...", mask: true });
      const { code, data } = await getRepairById(repairId.value);
      if (code === 200) {
        detail.value = data;
      } else {
        uni.showToast({ title: "获取详情失败", icon: "none" });
      }
      const fileRes = await getRepairFileList(repairId.value);
      if (fileRes?.code === 200) {
        repairImageList.value = Array.isArray(fileRes.data) ? fileRes.data : [];
      }
    } catch (e) {
      uni.showToast({ title: "获取详情失败", icon: "none" });
    } finally {
      uni.hideLoading();
    }
  };
  const goBack = () => {
    uni.navigateBack();
  };
  onLoad(options => {
    repairId.value = options?.id || uni.getStorageSync("repairId") || "";
  });
  onMounted(() => {
    loadDetail();
  });
</script>
<style scoped lang="scss">
  .repair-detail {
    min-height: 100vh;
    background: #f5f6f8;
    padding-bottom: 24px;
  }
  .detail-content {
    padding: 12px 16px;
  }
  .section {
    background: #fff;
    border-radius: 12px;
    margin-bottom: 16px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
  }
  .section-title {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 14px 16px;
    font-size: 16px;
    font-weight: 600;
    color: #303133;
    border-bottom: 1px solid #f0f0f0;
  }
  .section-num {
    width: 22px;
    height: 22px;
    line-height: 22px;
    text-align: center;
    border-radius: 50%;
    background: #2c7be5;
    color: #fff;
    font-size: 12px;
    font-weight: 600;
  }
  .info-grid {
    padding: 8px 16px 12px;
    display: flex;
    flex-wrap: wrap;
  }
  .info-item {
    width: 50%;
    padding: 10px 8px 10px 0;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 4px;
    &.full {
      width: 100%;
    }
  }
  .info-label {
    font-size: 13px;
    color: #909399;
  }
  .info-value {
    font-size: 14px;
    color: #303133;
    word-break: break-all;
  }
  .image-section {
    padding: 0 16px 16px;
    border-top: 1px solid #f5f5f5;
    margin-top: 4px;
    padding-top: 12px;
  }
  .image-title {
    font-size: 13px;
    color: #909399;
    display: block;
    margin-bottom: 10px;
  }
  .image-list {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
  }
  .repair-image {
    width: 80px;
    height: 80px;
    border-radius: 6px;
    background: #f5f5f5;
  }
  .no-image {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 24px;
    color: #c0c4cc;
    font-size: 13px;
    gap: 8px;
    background: #fafafa;
    border-radius: 8px;
  }
  .loading-wrap {
    padding: 40px;
    text-align: center;
    color: #909399;
  }
</style>