已添加11个文件
已修改17个文件
已删除2个文件
6917 ■■■■ 文件已修改
src/api/collaborativeApproval/approvalProcess.js 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInventory.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/procurementLedger.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/CommonUpload.vue 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/approve.vue 812 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/detail.vue 1113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/index.vue 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/add.vue 57 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/add.vue 758 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/attachment.vue 460 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/components/formDia.vue 228 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/index.vue 1474 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/upload.vue 885 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/Qualified.vue 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/Record.vue 292 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/Unqualified.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/index.vue 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/procurementManagement/procurementLedger/detail.vue 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/works.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/baogongtaizhang.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/shengchandingdan.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/shengchanhesuan.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/shengchanjihua.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/shengchanpaichan.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/shengchanshikuang.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/shengchanzhuisu.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/approvalProcess.js
@@ -2,63 +2,72 @@
import request from "@/utils/request";
export function approveProcessListPage(query) {
    return request({
        url: '/approveProcess/list',
        method: 'get',
        params: query,
    })
  return request({
    url: "/approveProcess/list",
    method: "get",
    params: query,
  });
}
export function getDept(query) {
    return request({
        url: '/approveProcess/getDept',
        method: 'get',
        params: query,
    })
  return request({
    url: "/approveProcess/getDept",
    method: "get",
    params: query,
  });
}
export function approveProcessGetInfo(query) {
    return request({
        url: '/approveProcess/get',
        method: 'get',
        params: query,
    })
  return request({
    url: "/approveProcess/get",
    method: "get",
    params: query,
  });
}
// æ–°å¢žå®¡æ‰¹æµç¨‹
export function approveProcessAdd(query) {
    return request({
        url: '/approveProcess/add',
        method: 'post',
        data: query,
    })
  return request({
    url: "/approveProcess/add",
    method: "post",
    data: query,
  });
}
// ä¿®æ”¹å®¡æ‰¹æµç¨‹
export function approveProcessUpdate(query) {
    return request({
        url: '/approveProcess/update',
        method: 'post',
        data: query,
    })
  return request({
    url: "/approveProcess/update",
    method: "post",
    data: query,
  });
}
// æäº¤å®¡æ‰¹
export function updateApproveNode(query) {
    return request({
        url: '/approveNode/updateApproveNode',
        method: 'post',
        data: query,
    })
  return request({
    url: "/approveNode/updateApproveNode",
    method: "post",
    data: query,
  });
}
// åˆ é™¤å®¡æ‰¹æµç¨‹
export function approveProcessDelete(query) {
    return request({
        url: '/approveProcess/deleteIds',
        method: 'delete',
        data: query,
    })
  return request({
    url: "/approveProcess/deleteIds",
    method: "delete",
    data: query,
  });
}
// æŸ¥è¯¢å®¡æ‰¹æµç¨‹
export function approveProcessDetails(query) {
    return request({
        url: '/approveNode/details/' + query,
        method: 'get',
    })
}
  return request({
    url: "/approveNode/details/" + query,
    method: "get",
  });
}
// å®¡æ‰¹è¯¦æƒ…
export function getDeliveryDetailByShippingNo(query) {
  return request({
    url: "/shippingInfo/getDateilByShippingNo",
    method: "get",
    params: query,
  });
}
src/api/inventoryManagement/stockInventory.js
@@ -8,6 +8,15 @@
    });
};
// åˆ†é¡µæŸ¥è¯¢è”合库存记录列表(包含商品信息)
export const getStockInventoryListPageCombined = (params) => {
    return request({
        url: "/stockInventory/pageListCombinedStockInventory",
        method: "get",
        params,
    });
};
// åˆ›å»ºåº“存记录
export const createStockInventory = (params) => {
    return request({
src/api/procurementManagement/procurementLedger.js
@@ -72,6 +72,16 @@
    method: "get",
  });
}
// æŸ¥è¯¢é‡‡è´­è¯¦æƒ…
export function getPurchaseByCode(query) {
  return request({
    url: "/purchase/ledger/getPurchaseByCode",
    method: "get",
    params: query,
  });
}
export function approveProcessGetInfo(query) {
    return request({
        url: '/approveProcess/get',
src/components/CommonUpload.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,164 @@
<template>
  <view class="common-upload">
    <u-upload
      :fileList="internalFileList"
      @afterRead="afterRead"
      @delete="deleteFile"
      :name="name"
      :multiple="multiple"
      :maxCount="maxCount"
      :accept="accept"
      :disabled="disabled"
    ></u-upload>
  </view>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue';
import { getToken } from "@/utils/auth";
import config from "@/config";
const props = defineProps({
  // çˆ¶ç»„件传入的文件列表(对应后端存储的对象列表)
  modelValue: {
    type: Array,
    default: () => []
  },
  // æœ€å¤§ä¸Šä¼ æ•°é‡
  maxCount: {
    type: Number,
    default: 9
  },
  // æ˜¯å¦æ”¯æŒå¤šé€‰
  multiple: {
    type: Boolean,
    default: true
  },
  // æŽ¥å—的文件类型
  accept: {
    type: String,
    default: 'image'
  },
  // ä¸Šä¼ æŽ¥å£å¯¹åº”的参数名
  name: {
    type: String,
    default: 'file'
  },
  // æ˜¯å¦ç¦ç”¨
  disabled: {
    type: Boolean,
    default: false
  }
});
const emit = defineEmits(['update:modelValue']);
// ç”¨äºŽ u-upload æ˜¾ç¤ºçš„内部列表
const internalFileList = ref([]);
// ç›‘听外部 modelValue å˜åŒ–,同步到内部显示列表
watch(() => props.modelValue, (newVal) => {
  if (newVal) {
    internalFileList.value = newVal.map(item => ({
      ...item,
      url: item.url || item.previewURL,
      status: 'success',
      message: ''
    }));
  }
}, { immediate: true, deep: true });
const showToast = (message) => {
  uni.showToast({
    title: message,
    icon: "none",
  });
};
// ä¸Šä¼ é€»è¾‘
const uploadFilePromise = (url) => {
  return new Promise((resolve, reject) => {
    uni.uploadFile({
      url: config.baseUrl + "/common/upload",
      filePath: url,
      name: "files", // æ³¨æ„ï¼šè¿™é‡Œæ ¹æ®åŽŸä»£ç æ˜¯ "files"
      header: {
        Authorization: "Bearer " + getToken(),
      },
      success: (res) => {
        try {
          const data = JSON.parse(res.data);
          if (data.code === 200) {
            // å¦‚果返回的是数组,取第一个元素
            const resultData = Array.isArray(data.data) ? data.data[0] : data.data;
            // å¤„理 url èµ‹å€¼
            if (!resultData.url && resultData.previewURL) {
              resultData.url = resultData.previewURL;
            }
            // å…¼å®¹åŽŸä»£ç ä¸­çš„ name èµ‹å€¼
            if (!resultData.name && resultData.originalFilename) {
              resultData.name = resultData.originalFilename;
            }
            resolve(resultData);
          } else {
            reject(data.msg || "上传失败");
          }
        } catch (e) {
          reject("解析响应失败");
        }
      },
      fail: (err) => {
        reject(err);
      },
    });
  });
};
// ä¸Šä¼ åŽçš„处理
const afterRead = async (event) => {
  let lists = [].concat(event.file);
  let currentModelValue = [...props.modelValue];
  // å…ˆåœ¨å†…部列表中添加占位(上传中状态)
  lists.forEach(item => {
    internalFileList.value.push({
      ...item,
      status: 'uploading',
      message: '上传中'
    });
  });
  for (let i = 0; i < lists.length; i++) {
    try {
      const result = await uploadFilePromise(lists[i].url);
      // æ›´æ–° modelValue
      currentModelValue.push(result);
      emit('update:modelValue', currentModelValue);
    } catch (e) {
      // å¦‚果上传失败,从内部列表中移除刚才添加的项
      const errorIndex = internalFileList.value.findIndex(item => item.status === 'uploading');
      if (errorIndex > -1) {
        internalFileList.value.splice(errorIndex, 1);
      }
      showToast(typeof e === "string" ? e : "上传失败");
    }
  }
};
// åˆ é™¤å¤„理
const deleteFile = (event) => {
  const newList = [...props.modelValue];
  newList.splice(event.index, 1);
  emit('update:modelValue', newList);
};
</script>
<style scoped lang="scss">
.common-upload {
  width: 100%;
}
</style>
src/config.js
@@ -1,7 +1,7 @@
// åº”用全局配置
const config = {
  baseUrl: "http://1.15.17.182:9003",
  fileUrl: "http://1.15.17.182:9002",
  baseUrl: "http://1.15.17.182:9048",
  fileUrl: "http://1.15.17.182:9049",
  // åº”用信息
  appInfo: {
    // åº”用名称
src/pages.json
@@ -761,6 +761,20 @@
      }
    },
    {
      "path": "pages/inspectionUpload/upload",
      "style": {
        "navigationBarTitleText": "上传巡检记录",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/inspectionUpload/attachment",
      "style": {
        "navigationBarTitleText": "查看附件",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/equipmentManagement/faultAnalysis/index",
      "style": {
        "navigationBarTitleText": "故障分析追溯",
src/pages/cooperativeOffice/collaborativeApproval/approve.vue
@@ -1,8 +1,7 @@
<template>
  <view class="approve-page">
    <PageHeader title="审核" @back="goBack" />
    <PageHeader title="审核"
                @back="goBack" />
    <!-- ç”³è¯·ä¿¡æ¯ -->
    <view class="application-info">
      <view class="info-header">
@@ -25,7 +24,6 @@
          <text class="info-label">申请日期</text>
          <text class="info-value">{{ approvalData.approveTime }}</text>
        </view>
        <!-- approveType=2 è¯·å‡ç›¸å…³å­—段 -->
        <template v-if="approvalData.approveType === 2">
          <view class="info-row">
@@ -37,462 +35,472 @@
            <text class="info-value">{{ approvalData.endDate || '-' }}</text>
          </view>
        </template>
        <!-- approveType=3 å‡ºå·®ç›¸å…³å­—段 -->
        <view v-if="approvalData.approveType === 3" class="info-row">
        <view v-if="approvalData.approveType === 3"
              class="info-row">
          <text class="info-label">出差地点</text>
          <text class="info-value">{{ approvalData.location || '-' }}</text>
        </view>
        <!-- approveType=4 æŠ¥é”€ç›¸å…³å­—段 -->
        <view v-if="approvalData.approveType === 4" class="info-row">
        <view v-if="approvalData.approveType === 4"
              class="info-row">
          <text class="info-label">报销金额</text>
          <text class="info-value">{{ approvalData.price ? `Â¥${approvalData.price}` : '-' }}</text>
        </view>
      </view>
    </view>
    <!-- å®¡æ‰¹æµç¨‹ -->
    <view class="approval-process">
      <view class="process-header">
        <text class="process-title">审批流程</text>
      </view>
      <view class="process-steps">
        <view
          v-for="(step, index) in approvalSteps"
          :key="index"
          class="process-step"
          :class="{
        <view v-for="(step, index) in approvalSteps"
              :key="index"
              class="process-step"
              :class="{
            'completed': step.status === 'completed',
            'current': step.status === 'current',
            'pending': step.status === 'pending',
            'rejected': step.status === 'rejected'
          }"
        >
          }">
          <view class="step-indicator">
            <view class="step-dot">
              <text v-if="step.status === 'completed'" class="step-icon">✓</text>
              <text v-else-if="step.status === 'rejected'" class="step-icon">✗</text>
              <text v-else class="step-number">{{ index + 1 }}</text>
              <text v-if="step.status === 'completed'"
                    class="step-icon">✓</text>
              <text v-else-if="step.status === 'rejected'"
                    class="step-icon">✗</text>
              <text v-else
                    class="step-number">{{ index + 1 }}</text>
            </view>
            <view v-if="index < approvalSteps.length - 1" class="step-line"></view>
            <view v-if="index < approvalSteps.length - 1"
                  class="step-line"></view>
          </view>
          <view class="step-content">
            <view class="step-info">
              <text class="step-title">{{ step.title }}</text>
              <text class="step-approver">{{ step.approverName }}</text>
              <text v-if="step.approveTime" class="step-time">{{ step.approveTime }}</text>
              <text v-if="step.approveTime"
                    class="step-time">{{ step.approveTime }}</text>
            </view>
            <view v-if="step.opinion" class="step-opinion">
            <view v-if="step.opinion"
                  class="step-opinion">
              <text class="opinion-label">审批意见:</text>
              <text class="opinion-content">{{ step.opinion }}</text>
            </view>
            <!-- ç­¾åå±•示 -->
            <view v-if="step.urlTem" class="step-opinion" style="margin-top:8px;">
            <view v-if="step.urlTem"
                  class="step-opinion"
                  style="margin-top:8px;">
              <text class="opinion-label">签名:</text>
              <image :src="step.urlTem" mode="widthFix" style="width:180px;border-radius:6px;border:1px solid #eee;" />
              <image :src="step.urlTem"
                     mode="widthFix"
                     style="width:180px;border-radius:6px;border:1px solid #eee;" />
            </view>
          </view>
        </view>
      </view>
    </view>
    <!-- å®¡æ ¸æ„è§è¾“å…¥ -->
    <view v-if="canApprove" class="approval-input">
    <view v-if="canApprove"
          class="approval-input">
      <view class="input-header">
        <text class="input-title">审核意见</text>
      </view>
      <view class="input-content">
        <u-textarea
          v-model="approvalOpinion"
          rows="4"
          placeholder="请输入审核意见"
          maxlength="200"
          count
        />
        <u-textarea v-model="approvalOpinion"
                    rows="4"
                    placeholder="请输入审核意见"
                    maxlength="200"
                    count />
      </view>
    </view>
    <!-- åº•部操作按钮 -->
    <view v-if="canApprove" class="footer-actions">
      <u-button class="reject-btn" @click="handleReject">驳回</u-button>
      <u-button class="approve-btn" @click="handleApprove">通过</u-button>
    <view v-if="canApprove"
          class="footer-actions">
      <u-button class="reject-btn"
                @click="handleReject">驳回</u-button>
      <u-button class="approve-btn"
                @click="handleApprove">通过</u-button>
    </view>
  </view>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { approveProcessGetInfo, approveProcessDetails, updateApproveNode } from '@/api/collaborativeApproval/approvalProcess'
import useUserStore from '@/store/modules/user'
const showToast = (message) => {
    uni.showToast({
        title: message,
        icon: 'none'
    })
}
import PageHeader from "@/components/PageHeader.vue";
  import { ref, onMounted, computed } from "vue";
  import {
    approveProcessGetInfo,
    approveProcessDetails,
    updateApproveNode,
  } from "@/api/collaborativeApproval/approvalProcess";
  import useUserStore from "@/store/modules/user";
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  import PageHeader from "@/components/PageHeader.vue";
const userStore = useUserStore()
const approvalData = ref({})
const approvalSteps = ref([])
const approvalOpinion = ref('')
const approveId = ref('')
  const userStore = useUserStore();
  const approvalData = ref({});
  const approvalSteps = ref([]);
  const approvalOpinion = ref("");
  const approveId = ref("");
// ä»Žè¯¦æƒ…接口字段对齐 canApprove:仅当有 isShen çš„节点时可审批
const canApprove = computed(() => {
  return approvalSteps.value.some(step => step.isShen === true)
})
  // ä»Žè¯¦æƒ…接口字段对齐 canApprove:仅当有 isShen çš„节点时可审批
  const canApprove = computed(() => {
    return approvalSteps.value.some(step => step.isShen === true);
  });
onMounted(() => {
  approveId.value = uni.getStorageSync('approveId')
  if (approveId.value) {
    loadApprovalData()
  }
})
const loadApprovalData = () => {
  // åŸºæœ¬ç”³è¯·ä¿¡æ¯
  approveProcessGetInfo({ id: approveId.value }).then(res => {
    approvalData.value = res.data || {}
  })
  // å®¡æ‰¹èŠ‚ç‚¹è¯¦æƒ…
  approveProcessDetails(approveId.value).then(res => {
    const list = Array.isArray(res.data) ? res.data : []
    // ä¿å­˜åŽŸå§‹èŠ‚ç‚¹æ•°æ®ä¾›æäº¤ä½¿ç”¨
    activities.value = list
    approvalSteps.value = list.map((it, idx) => {
      // èŠ‚ç‚¹çŠ¶æ€æ˜ å°„ï¼š1=通过,2=不通过,否则看是否当前(isShen),再默认为待处理
      let status = 'pending'
      if (it.approveNodeStatus === 1) status = 'completed'
      else if (it.approveNodeStatus === 2) status = 'rejected'
      else if (it.isShen) status = 'current'
      return {
        title: `第${idx + 1}步审批`,
        approverName: it.approveNodeUser || '未知用户',
        status,
        approveTime: it.approveTime || null,
        opinion: it.approveNodeReason || '',
        urlTem: it.urlTem || '',
        isShen: !!it.isShen
      }
    })
  })
}
const goBack = () => {
  uni.removeStorageSync('approveId');
  uni.navigateBack()
}
const submitForm = (status) => {
  // å¯é€‰ï¼šæ ¡éªŒå®¡æ ¸æ„è§
  if (!approvalOpinion.value?.trim()) {
    showToast('请输入审核意见')
    return
  }
  // æ‰¾åˆ°å½“前可审批节点
  const filteredActivities = activities.value.filter(activity => activity.isShen)
  if (!filteredActivities.length) {
    showToast('当前无可审批节点')
    return
  }
  // å†™å…¥çŠ¶æ€å’Œæ„è§
  filteredActivities[0].approveNodeStatus = status
  filteredActivities[0].approveNodeReason = approvalOpinion.value || ''
  // è®¡ç®—是否为最后一步
  const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length - 1
  // è°ƒç”¨åŽç«¯
  updateApproveNode({ ...filteredActivities[0], isLast }).then(() => {
    const msg = status === 1 ? '审批通过' : '审批已驳回'
    showToast(msg)
    // æç¤ºåŽè¿”回上一个页面
    setTimeout(() => {
      goBack() // å†…部是 uni.navigateBack()
    }, 800)
  })
}
const handleApprove = () => {
  uni.showModal({
    title: '确认操作',
    content: '确定要通过此审批吗?',
    success: (res) => {
      if (res.confirm) submitForm(1)
  onMounted(() => {
    approveId.value = uni.getStorageSync("approveId");
    if (approveId.value) {
      loadApprovalData();
    }
  })
}
  });
const handleReject = () => {
  uni.showModal({
    title: '确认操作',
    content: '确定要驳回此审批吗?',
    success: (res) => {
      if (res.confirm) submitForm(2)
  const loadApprovalData = () => {
    // åŸºæœ¬ç”³è¯·ä¿¡æ¯
    approveProcessGetInfo({ id: approveId.value }).then(res => {
      approvalData.value = res.data || {};
    });
    // å®¡æ‰¹èŠ‚ç‚¹è¯¦æƒ…
    approveProcessDetails(approveId.value).then(res => {
      const list = Array.isArray(res.data) ? res.data : [];
      // ä¿å­˜åŽŸå§‹èŠ‚ç‚¹æ•°æ®ä¾›æäº¤ä½¿ç”¨
      activities.value = list;
      approvalSteps.value = list.map((it, idx) => {
        // èŠ‚ç‚¹çŠ¶æ€æ˜ å°„ï¼š1=通过,2=不通过,否则看是否当前(isShen),再默认为待处理
        let status = "pending";
        if (it.approveNodeStatus === 1) status = "completed";
        else if (it.approveNodeStatus === 2) status = "rejected";
        else if (it.isShen) status = "current";
        return {
          title: `第${idx + 1}步审批`,
          approverName: it.approveNodeUser || "未知用户",
          status,
          approveTime: it.approveTime || null,
          opinion: it.approveNodeReason || "",
          urlTem: it.urlTem || "",
          isShen: !!it.isShen,
        };
      });
    });
  };
  const goBack = () => {
    uni.removeStorageSync("approveId");
    uni.navigateBack();
  };
  const submitForm = status => {
    // å¯é€‰ï¼šæ ¡éªŒå®¡æ ¸æ„è§
    if (!approvalOpinion.value?.trim()) {
      showToast("请输入审核意见");
      return;
    }
  })
}
// åŽŸå§‹èŠ‚ç‚¹æ•°æ®ï¼ˆç”¨äºŽæäº¤é€»è¾‘ï¼‰
const activities = ref([])
    // æ‰¾åˆ°å½“前可审批节点
    const filteredActivities = activities.value.filter(
      activity => activity.isShen
    );
    if (!filteredActivities.length) {
      showToast("当前无可审批节点");
      return;
    }
    // å†™å…¥çŠ¶æ€å’Œæ„è§
    filteredActivities[0].approveNodeStatus = status;
    filteredActivities[0].approveNodeReason = approvalOpinion.value || "";
    // è®¡ç®—是否为最后一步
    const isLast =
      activities.value.findIndex(a => a.isShen) === activities.value.length - 1;
    // è°ƒç”¨åŽç«¯
    updateApproveNode({ ...filteredActivities[0], isLast }).then(() => {
      const msg = status === 1 ? "审批通过" : "审批已驳回";
      showToast(msg);
      // æç¤ºåŽè¿”回上一个页面
      setTimeout(() => {
        goBack(); // å†…部是 uni.navigateBack()
      }, 800);
    });
  };
  const handleApprove = () => {
    uni.showModal({
      title: "确认操作",
      content: "确定要通过此审批吗?",
      success: res => {
        if (res.confirm) submitForm(1);
      },
    });
  };
  const handleReject = () => {
    uni.showModal({
      title: "确认操作",
      content: "确定要驳回此审批吗?",
      success: res => {
        if (res.confirm) submitForm(2);
      },
    });
  };
  // åŽŸå§‹èŠ‚ç‚¹æ•°æ®ï¼ˆç”¨äºŽæäº¤é€»è¾‘ï¼‰
  const activities = ref([]);
</script>
<style scoped lang="scss">
.approve-page {
  min-height: 100vh;
  background: #f8f9fa;
  padding-bottom: 80px;
}
.header {
  display: flex;
  align-items: center;
  background: #fff;
  padding: 16px 20px;
  border-bottom: 1px solid #f0f0f0;
  position: sticky;
  top: 0;
  z-index: 100;
}
.title {
  flex: 1;
  text-align: center;
  font-size: 18px;
  font-weight: 600;
  color: #333;
}
.application-info {
  background: #fff;
  margin: 16px;
  border-radius: 12px;
  overflow: hidden;
}
.info-header {
  padding: 16px;
  border-bottom: 1px solid #f0f0f0;
  background: #f8f9fa;
}
.info-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
.info-content {
  padding: 16px;
}
.info-row {
  display: flex;
  align-items: center;
  margin-bottom: 12px;
  &:last-child {
    margin-bottom: 0;
  .approve-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 80px;
  }
}
.info-label {
  font-size: 14px;
  color: #666;
  width: 80px;
  flex-shrink: 0;
}
  .header {
    display: flex;
    align-items: center;
    background: #fff;
    padding: 16px 20px;
    border-bottom: 1px solid #f0f0f0;
    position: sticky;
    top: 0;
    z-index: 100;
  }
.info-value {
  font-size: 14px;
  color: #333;
  flex: 1;
}
  .title {
    flex: 1;
    text-align: center;
    font-size: 18px;
    font-weight: 600;
    color: #333;
  }
.approval-process {
  background: #fff;
  margin: 16px;
  border-radius: 12px;
  overflow: hidden;
}
  .application-info {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
.process-header {
  padding: 16px;
  border-bottom: 1px solid #f0f0f0;
  background: #f8f9fa;
}
  .info-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
.process-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
  .info-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
.process-steps {
  padding: 20px;
}
  .info-content {
    padding: 16px;
  }
.process-step {
  display: flex;
  position: relative;
  margin-bottom: 24px;
  &:last-child {
    margin-bottom: 0;
    .step-line {
      display: none;
  .info-row {
    display: flex;
    align-items: center;
    margin-bottom: 12px;
    &:last-child {
      margin-bottom: 0;
    }
  }
}
.step-indicator {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-right: 16px;
}
  .info-label {
    font-size: 14px;
    color: #666;
    width: 80px;
    flex-shrink: 0;
  }
.step-dot {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 600;
  position: relative;
  z-index: 2;
}
  .info-value {
    font-size: 14px;
    color: #333;
    flex: 1;
  }
.process-step.completed .step-dot {
  background: #52c41a;
  color: #fff;
}
  .approval-process {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
.process-step.current .step-dot {
  background: #1890ff;
  color: #fff;
  animation: pulse 2s infinite;
}
  .process-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
.process-step.pending .step-dot {
  background: #d9d9d9;
  color: #999;
}
  .process-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
.step-line {
  width: 2px;
  height: 40px;
  background: #d9d9d9;
  margin-top: 8px;
}
  .process-steps {
    padding: 20px;
  }
.process-step.completed .step-line {
  background: #52c41a;
}
  .process-step {
    display: flex;
    position: relative;
    margin-bottom: 24px;
.process-step.rejected .step-dot {
  background: #ff4d4f;
  color: #fff;
}
.process-step.rejected .step-line {
  background: #ff4d4f;
}
    &:last-child {
      margin-bottom: 0;
.step-content {
  flex: 1;
  padding-top: 4px;
}
      .step-line {
        display: none;
      }
    }
  }
.step-info {
  margin-bottom: 8px;
}
  .step-indicator {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-right: 16px;
  }
.step-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
  display: block;
  margin-bottom: 4px;
}
  .step-dot {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    font-weight: 600;
    position: relative;
    z-index: 2;
  }
.step-approver {
  font-size: 14px;
  color: #666;
  display: block;
  margin-bottom: 4px;
}
  .process-step.completed .step-dot {
    background: #52c41a;
    color: #fff;
  }
.step-time {
  font-size: 12px;
  color: #999;
  display: block;
}
  .process-step.current .step-dot {
    background: #1890ff;
    color: #fff;
    animation: pulse 2s infinite;
  }
.step-opinion {
  background: #f8f9fa;
  padding: 12px;
  border-radius: 8px;
  border-left: 4px solid #52c41a;
}
  .process-step.pending .step-dot {
    background: #d9d9d9;
    color: #999;
  }
.opinion-label {
  font-size: 12px;
  color: #666;
  display: block;
  margin-bottom: 4px;
}
  .step-line {
    width: 2px;
    height: 40px;
    background: #d9d9d9;
    margin-top: 8px;
  }
.opinion-content {
  font-size: 14px;
  color: #333;
  line-height: 1.5;
}
  .process-step.completed .step-line {
    background: #52c41a;
  }
.approval-input {
  background: #fff;
  margin: 16px;
  border-radius: 12px;
  overflow: hidden;
}
  .process-step.rejected .step-dot {
    background: #ff4d4f;
    color: #fff;
  }
  .process-step.rejected .step-line {
    background: #ff4d4f;
  }
.input-header {
  padding: 16px;
  border-bottom: 1px solid #f0f0f0;
  background: #f8f9fa;
}
  .step-content {
    flex: 1;
    padding-top: 4px;
  }
.input-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
  .step-info {
    margin-bottom: 8px;
  }
.input-content {
  padding: 16px;
}
  .step-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
    display: block;
    margin-bottom: 4px;
  }
.footer-actions {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  background: #fff;
  display: flex;
  justify-content: space-around;
  align-items: center;
  padding: 16px;
  box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
  z-index: 1000;
}
  .step-approver {
    font-size: 14px;
    color: #666;
    display: block;
    margin-bottom: 4px;
  }
.reject-btn {
  .step-time {
    font-size: 12px;
    color: #999;
    display: block;
  }
  .step-opinion {
    background: #f8f9fa;
    padding: 12px;
    border-radius: 8px;
    border-left: 4px solid #52c41a;
  }
  .opinion-label {
    font-size: 12px;
    color: #666;
    display: block;
    margin-bottom: 4px;
  }
  .opinion-content {
    font-size: 14px;
    color: #333;
    line-height: 1.5;
  }
  .approval-input {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
  .input-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
  .input-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .input-content {
    padding: 16px;
  }
  .footer-actions {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 16px;
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
    z-index: 1000;
  }
  .reject-btn {
    width: 120px;
    background: #ff4d4f;
    color: #fff;
@@ -503,47 +511,47 @@
    background: #52c41a;
    color: #fff;
  }
  /* é€‚配u-button样式 */
  :deep(.u-button) {
    border-radius: 6px;
  }
@keyframes pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7);
  @keyframes pulse {
    0% {
      box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7);
    }
    70% {
      box-shadow: 0 0 0 10px rgba(24, 144, 255, 0);
    }
    100% {
      box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
    }
  }
  70% {
    box-shadow: 0 0 0 10px rgba(24, 144, 255, 0);
  .signature-section {
    background: #fff;
    padding: 12px 16px 16px;
    border-top: 1px solid #f0f0f0;
  }
  100% {
    box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
  .signature-header {
    margin-bottom: 8px;
  }
}
.signature-section {
  background: #fff;
  padding: 12px 16px 16px;
  border-top: 1px solid #f0f0f0;
}
.signature-header {
  margin-bottom: 8px;
}
.signature-title {
  font-size: 14px;
  font-weight: 600;
  color: #333;
}
.signature-box {
  width: 100%;
  height: 180px;
  background: #fff;
  border: 1px dashed #d9d9d9;
  border-radius: 8px;
  overflow: hidden;
}
.signature-actions {
  margin-top: 8px;
  display: flex;
  justify-content: flex-end;
}
  .signature-title {
    font-size: 14px;
    font-weight: 600;
    color: #333;
  }
  .signature-box {
    width: 100%;
    height: 180px;
    background: #fff;
    border: 1px dashed #d9d9d9;
    border-radius: 8px;
    overflow: hidden;
  }
  .signature-actions {
    margin-top: 8px;
    display: flex;
    justify-content: flex-end;
  }
</style>
src/pages/cooperativeOffice/collaborativeApproval/detail.vue
@@ -1,6 +1,6 @@
<template>
  <view class="account-detail">
    <PageHeader title="审批流程"
    <PageHeader :title="operationType === 'detail' ? '详情' : '审批流程'"
                @back="goBack" />
    <!-- è¡¨å•区域 -->
    <u-form ref="formRef"
@@ -8,38 +8,39 @@
            :rules="rules"
            :model="form"
            label-width="140rpx">
      <u-form-item prop="approveReason"
                   label="流程编号">
        <u-input v-model="form.approveId"
                 disabled
                 placeholder="自动编号" />
      </u-form-item>
      <u-form-item prop="approveReason"
                   :label="approveType === 5 ? '采购事由' : '申请事由'"
                   required>
        <u-input v-model="form.approveReason"
                 type="textarea"
                 rows="2"
                 auto-height
                 maxlength="200"
                 :placeholder="approveType === 5 ? '请输入采购事由' : '请输入申请事由'"
                 show-word-limit />
      </u-form-item>
      <u-form-item prop="approveDeptName"
                   label="申请部门"
                   required>
        <!-- <u-input v-model="form.approveDeptName"
      <template v-if="operationType !== 'detail'">
        <u-form-item prop="approveReason"
                     label="流程编号">
          <u-input v-model="form.approveId"
                   disabled
                   placeholder="自动编号" />
        </u-form-item>
        <u-form-item prop="approveReason"
                     :label="approveType === 5 ? '采购事由' : '申请事由'"
                     required>
          <u-input v-model="form.approveReason"
                   type="textarea"
                   rows="2"
                   auto-height
                   maxlength="200"
                   :placeholder="approveType === 5 ? '请输入采购事由' : '请输入申请事由'"
                   show-word-limit />
        </u-form-item>
        <u-form-item prop="approveDeptName"
                     label="申请部门"
                     required>
          <!-- <u-input v-model="form.approveDeptName"
                 placeholder="请选择申请部门" /> -->
        <u-input v-model="form.approveDeptName"
                 readonly
                 placeholder="请选择申请部门"
                 @click="showPicker = true" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showPicker = true"></up-icon>
        </template>
      </u-form-item>
      <u-form-item prop="approveUser"
          <u-input v-model="form.approveDeptName"
                   readonly
                   placeholder="请选择申请部门"
                   @click="showPicker = true" />
          <template #right>
            <up-icon name="arrow-right"
                     @click="showPicker = true"></up-icon>
          </template>
        </u-form-item>
        <!-- <u-form-item prop="approveUser"
                   label="申请人"
                   required>
        <u-input v-model="form.approveUserName"
@@ -57,141 +58,277 @@
          <up-icon name="arrow-right"
                   @click="showDatePicker"></up-icon>
        </template>
      </u-form-item>
      <!-- approveType=2 è¯·å‡ç›¸å…³å­—段 -->
      <template v-if="approveType === 2">
        <u-form-item prop="startDate"
                     label="开始时间"
      </u-form-item> -->
        <!-- approveType=2 è¯·å‡ç›¸å…³å­—段 -->
        <template v-if="approveType === 2">
          <u-form-item prop="startDate"
                       label="开始时间"
                       required>
            <u-input v-model="form.startDate"
                     readonly
                     placeholder="请假开始时间"
                     @click="showStartDatePicker" />
            <template #right>
              <up-icon name="arrow-right"
                       @click="showStartDatePicker"></up-icon>
            </template>
          </u-form-item>
          <u-form-item prop="endDate"
                       label="结束时间"
                       required>
            <u-input v-model="form.endDate"
                     readonly
                     placeholder="请假结束时间"
                     @click="showEndDatePicker" />
            <template #right>
              <up-icon name="arrow-right"
                       @click="showEndDatePicker"></up-icon>
            </template>
          </u-form-item>
        </template>
        <!-- approveType=3 å‡ºå·®ç›¸å…³å­—段 -->
        <u-form-item v-if="approveType === 3"
                     prop="location"
                     label="出差地点"
                     required>
          <u-input v-model="form.startDate"
                   readonly
                   placeholder="请假开始时间"
                   @click="showStartDatePicker" />
          <template #right>
            <up-icon name="arrow-right"
                     @click="showStartDatePicker"></up-icon>
          </template>
          <u-input v-model="form.location"
                   placeholder="请输入出差地点"
                   clearable />
        </u-form-item>
        <u-form-item prop="endDate"
                     label="结束时间"
        <!-- approveType=4 æŠ¥é”€ç›¸å…³å­—段 -->
        <u-form-item v-if="approveType === 4"
                     prop="price"
                     label="报销金额"
                     required>
          <u-input v-model="form.endDate"
                   readonly
                   placeholder="请假结束时间"
                   @click="showEndDatePicker" />
          <template #right>
            <up-icon name="arrow-right"
                     @click="showEndDatePicker"></up-icon>
          </template>
          <u-input v-model="form.price"
                   type="number"
                   placeholder="请输入报销金额"
                   clearable />
        </u-form-item>
      </template>
      <!-- approveType=3 å‡ºå·®ç›¸å…³å­—段 -->
      <u-form-item v-if="approveType === 3"
                   prop="location"
                   label="出差地点"
                   required>
        <u-input v-model="form.location"
                 placeholder="请输入出差地点"
                 clearable />
      </u-form-item>
      <!-- approveType=4 æŠ¥é”€ç›¸å…³å­—段 -->
      <u-form-item v-if="approveType === 4"
                   prop="price"
                   label="报销金额"
                   required>
        <u-input v-model="form.price"
                 type="number"
                 placeholder="请输入报销金额"
                 clearable />
      <!-- æŠ¥ä»·å®¡æ‰¹è¯¦æƒ… -->
      <view v-if="isQuotationApproval"
            style="margin: 20rpx 0;">
        <u-divider text="报价详情"
                   text-size="28rpx"
                   color="#2979ff"></u-divider>
        <u-skeleton :loading="quotationLoading"
                    rows="3"
                    animated>
          <view v-if="!currentQuotation || !currentQuotation.quotationNo"
                style="padding: 40rpx; text-align: center; color: #999;">
            æœªæŸ¥è¯¢åˆ°å¯¹åº”报价详情
          </view>
          <view v-else>
            <u-cell-group :border="false">
              <u-cell title="报价单号"
                      :value="currentQuotation.quotationNo"></u-cell>
              <u-cell title="客户名称"
                      :value="currentQuotation.customer"></u-cell>
              <u-cell title="业务员"
                      :value="currentQuotation.salesperson"></u-cell>
              <u-cell title="报价日期"
                      :value="currentQuotation.quotationDate"></u-cell>
              <u-cell title="有效期至"
                      :value="currentQuotation.validDate"></u-cell>
              <u-cell title="付款方式"
                      :value="currentQuotation.paymentMethod"></u-cell>
              <u-cell title="报价总额">
                <template #value>
                  <text style="font-size: 32rpx; color: #e6a23c; font-weight: bold;">
                    Â¥{{ Number(currentQuotation.totalAmount ?? 0).toFixed(2) }}
                  </text>
                </template>
              </u-cell>
            </u-cell-group>
            <view style="margin-top: 20rpx; padding: 0 30rpx;">
              <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">产品明细</view>
              <view v-for="(item, index) in (currentQuotation.products || [])"
                    :key="index"
                    style="background: #f8f8f8; border-radius: 8rpx; padding: 20rpx; margin-bottom: 10rpx;">
                <view style="display: flex; justify-content: space-between;">
                  <text style="font-weight: bold;">{{ item.product }}</text>
                  <text style="color: #e6a23c;">Â¥{{ Number(item.unitPrice ?? 0).toFixed(2) }}</text>
                </view>
                <view style="font-size: 24rpx; color: #666; margin-top: 10rpx;">
                  è§„æ ¼: {{ item.specification }} | å•位: {{ item.unit }}
                </view>
              </view>
            </view>
            <view v-if="currentQuotation.remark"
                  style="margin-top: 20rpx; padding: 0 30rpx;">
              <view style="font-size: 28rpx; font-weight: bold;">备注</view>
              <view style="font-size: 26rpx; color: #666; margin-top: 10rpx;">{{ currentQuotation.remark }}</view>
            </view>
          </view>
        </u-skeleton>
      </view>
      <!-- é‡‡è´­å®¡æ‰¹è¯¦æƒ… -->
      <view v-if="isPurchaseApproval"
            style="margin: 20rpx 0;">
        <u-divider text="采购详情"
                   text-size="28rpx"
                   color="#2979ff"></u-divider>
        <u-skeleton :loading="purchaseLoading"
                    rows="3"
                    animated>
          <view v-if="!currentPurchase || !currentPurchase.purchaseContractNumber"
                style="padding: 40rpx; text-align: center; color: #999;">
            æœªæŸ¥è¯¢åˆ°å¯¹åº”采购详情
          </view>
          <view v-else>
            <u-cell-group :border="false">
              <u-cell title="采购合同号"
                      :value="currentPurchase.purchaseContractNumber"></u-cell>
              <u-cell title="供应商名称"
                      :value="currentPurchase.supplierName"></u-cell>
              <u-cell title="项目名称"
                      :value="currentPurchase.projectName"></u-cell>
              <u-cell title="销售合同号"
                      :value="currentPurchase.salesContractNo"></u-cell>
              <u-cell title="签订日期"
                      :value="currentPurchase.executionDate"></u-cell>
              <u-cell title="录入日期"
                      :value="currentPurchase.entryDate"></u-cell>
              <u-cell title="付款方式"
                      :value="currentPurchase.paymentMethod"></u-cell>
              <u-cell title="合同金额">
                <template #value>
                  <text style="font-size: 32rpx; color: #e6a23c; font-weight: bold;">
                    Â¥{{ Number(currentPurchase.contractAmount ?? 0).toFixed(2) }}
                  </text>
                </template>
              </u-cell>
            </u-cell-group>
            <view style="margin-top: 20rpx; padding: 0 30rpx;">
              <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">产品明细</view>
              <view v-for="(item, index) in (currentPurchase.productData || [])"
                    :key="index"
                    style="background: #f8f8f8; border-radius: 8rpx; padding: 20rpx; margin-bottom: 10rpx;">
                <view style="display: flex; justify-content: space-between;">
                  <text style="font-weight: bold;">{{ item.productCategory }}</text>
                  <text style="color: #e6a23c;">Â¥{{ Number(item.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</text>
                </view>
                <view style="font-size: 24rpx; color: #666; margin-top: 10rpx;">
                  è§„æ ¼: {{ item.specificationModel }} | æ•°é‡: {{ item.quantity }} {{ item.unit }}
                </view>
                <view style="font-size: 24rpx; color: #999; margin-top: 4rpx;">
                  å«ç¨Žå•ä»·: Â¥{{ Number(item.taxInclusiveUnitPrice ?? 0).toFixed(2) }}
                </view>
              </view>
            </view>
          </view>
        </u-skeleton>
      </view>
      <!-- å‘货审批详情 -->
      <view v-if="isDeliveryApproval"
            style="margin: 20rpx 0;">
        <u-divider text="发货详情"
                   text-size="28rpx"
                   color="#2979ff"></u-divider>
        <u-skeleton :loading="deliveryLoading"
                    rows="3"
                    animated>
          <view v-if="!currentDelivery || !currentDelivery.shippingInfo"
                style="padding: 40rpx; text-align: center; color: #999;">
            æœªæŸ¥è¯¢åˆ°å¯¹åº”发货详情
          </view>
          <view v-else>
            <u-cell-group :border="false">
              <u-cell title="销售订单"
                      :value="currentDelivery.shippingInfo.salesContractNo || '--'"></u-cell>
              <u-cell title="发货订单号"
                      :value="currentDelivery.shippingInfo.shippingNo || '--'"></u-cell>
              <u-cell title="客户名称"
                      :value="currentDelivery.shippingInfo.customerName || '--'"></u-cell>
              <u-cell title="发货类型"
                      :value="currentDelivery.shippingInfo.type || '--'"></u-cell>
              <u-cell title="发货日期"
                      :value="currentDelivery.shippingInfo.shippingDate || '--'"></u-cell>
              <u-cell title="审核状态"
                      :value="currentDelivery.shippingInfo.status || '--'"></u-cell>
              <u-cell title="发货车牌号"
                      :value="currentDelivery.shippingInfo.shippingCarNumber || '--'"></u-cell>
              <u-cell title="快递公司"
                      :value="currentDelivery.shippingInfo.expressCompany || '--'"></u-cell>
              <u-cell title="快递单号"
                      :value="currentDelivery.shippingInfo.expressNumber || '--'"></u-cell>
            </u-cell-group>
            <view style="margin-top: 20rpx; padding: 0 30rpx;">
              <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">产品明细</view>
              <view v-for="(item, index) in deliveryProductList"
                    :key="index"
                    style="background: #f8f8f8; border-radius: 8rpx; padding: 20rpx; margin-bottom: 10rpx;">
                <view style="display: flex; justify-content: space-between;">
                  <text style="font-weight: bold;">{{ item.productName }}</text>
                  <text style="color: #2979ff;">数量: {{ item.deliveryQuantity }}</text>
                </view>
                <view style="font-size: 24rpx; color: #666; margin-top: 10rpx;">
                  è§„æ ¼: {{ item.specificationModel }}
                </view>
                <view v-if="item.batchNo"
                      style="font-size: 24rpx; color: #999; margin-top: 4rpx;">
                  æ‰¹å·: {{ item.batchNo }}
                </view>
              </view>
            </view>
            <view v-if="currentDelivery.shippingInfo.storageBlobVOs && currentDelivery.shippingInfo.storageBlobVOs.length"
                  style="margin-top: 20rpx; padding: 0 30rpx;">
              <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">发货图片</view>
              <CommonUpload :model-value="currentDelivery.shippingInfo.storageBlobVOs"
                            disabled />
            </view>
          </view>
        </u-skeleton>
      </view>
      <u-form-item v-if="operationType !== 'detail'"
                   label="图片附件"
                   prop="storageBlobDTOS"
                   border-bottom>
        <CommonUpload v-model="form.storageBlobDTOS" />
      </u-form-item>
    </u-form>
    <!-- é€‰æ‹©å™¨å¼¹çª— -->
    <up-action-sheet :show="showPicker"
                     :actions="productOptions"
                     title="选择部门"
                     @select="onConfirm"
                     @close="showPicker = false" />
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup :show="showDate"
              mode="bottom"
              @close="showDate = false">
      <up-datetime-picker :show="true"
                          v-model="currentDate"
                          @confirm="onDateConfirm"
                          @cancel="showDate = false"
                          mode="date" />
    </up-popup>
    <!-- è¯·å‡å¼€å§‹æ—¶é—´é€‰æ‹©å™¨ -->
    <up-popup :show="showStartDate"
              mode="bottom"
              @close="showStartDate = false">
      <up-datetime-picker :show="true"
                          v-model="startDateValue"
                          @confirm="onStartDateConfirm"
                          @cancel="showStartDate = false"
                          mode="date" />
    </up-popup>
    <!-- è¯·å‡ç»“束时间选择器 -->
    <up-popup :show="showEndDate"
              mode="bottom"
              @close="showEndDate = false">
      <up-datetime-picker :show="true"
                          v-model="endDateValue"
                          @confirm="onEndDateConfirm"
                          @cancel="showEndDate = false"
                          mode="date" />
    </up-popup>
    <!-- å®¡æ ¸æµç¨‹åŒºåŸŸ -->
    <view class="approval-process">
      <view class="approval-header">
        <text class="approval-title">审核流程</text>
        <text class="approval-desc">每个步骤只能选择一个审批人</text>
      </view>
      <view class="approval-steps">
        <view v-for="(step, stepIndex) in approverNodes"
              :key="stepIndex"
              class="approval-step">
          <view class="step-dot"></view>
          <view class="step-title">
            <text>审批人</text>
          </view>
          <view class="approver-container">
            <view v-if="step.nickName"
                  class="approver-item">
              <view class="approver-avatar">
                <text class="avatar-text">{{ step.nickName.charAt(0) }}</text>
                <view class="status-dot"></view>
              </view>
              <view class="approver-info">
                <text class="approver-name">{{ step.nickName }}</text>
              </view>
              <view class="delete-approver-btn"
                    @click="removeApprover(stepIndex)">×</view>
            </view>
            <view v-else
                  class="add-approver-btn"
                  @click="addApprover(stepIndex)">
              <view class="add-circle">+</view>
              <text class="add-label">选择审批人</text>
            </view>
          </view>
          <view class="step-line"
                v-if="stepIndex < approverNodes.length - 1"></view>
          <view class="delete-step-btn"
                v-if="approverNodes.length > 1"
                @click="removeApprovalStep(stepIndex)">删除节点</view>
        </view>
      </view>
      <view class="add-step-btn">
        <u-button icon="plus"
                  plain
                  type="primary"
                  style="width: 100%"
                  @click="addApprovalStep">新增节点</u-button>
      </view>
    </view>
    <template v-if="operationType !== 'detail'">
      <up-action-sheet :show="showPicker"
                       :actions="productOptions"
                       title="选择部门"
                       @select="onConfirm"
                       @close="showPicker = false" />
      <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
      <up-popup :show="showDate"
                mode="bottom"
                @close="showDate = false">
        <up-datetime-picker :show="true"
                            v-model="currentDate"
                            @confirm="onDateConfirm"
                            @cancel="showDate = false"
                            mode="date" />
      </up-popup>
      <!-- è¯·å‡å¼€å§‹æ—¶é—´é€‰æ‹©å™¨ -->
      <up-popup :show="showStartDate"
                mode="bottom"
                @close="showStartDate = false">
        <up-datetime-picker :show="true"
                            v-model="startDateValue"
                            @confirm="onStartDateConfirm"
                            @cancel="showStartDate = false"
                            mode="date" />
      </up-popup>
      <!-- è¯·å‡ç»“束时间选择器 -->
      <up-popup :show="showEndDate"
                mode="bottom"
                @close="showEndDate = false">
        <up-datetime-picker :show="true"
                            v-model="endDateValue"
                            @confirm="onEndDateConfirm"
                            @cancel="showEndDate = false"
                            mode="date" />
      </up-popup>
    </template>
    <!-- åº•部按钮 -->
    <view class="footer-btns">
    <view class="footer-btns"
          v-if="operationType !== 'detail'">
      <u-button class="cancel-btn"
                @click="goBack">取消</u-button>
      <u-button class="save-btn"
@@ -201,8 +338,17 @@
</template>
<script setup>
  import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
  import {
    ref,
    onMounted,
    onUnmounted,
    reactive,
    toRefs,
    computed,
    watch,
  } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import CommonUpload from "@/components/CommonUpload.vue";
  import useUserStore from "@/store/modules/user";
  import { formatDateToYMD } from "@/utils/ruoyi";
  import {
@@ -210,14 +356,16 @@
    approveProcessGetInfo,
    approveProcessAdd,
    approveProcessUpdate,
    getDeliveryDetailByShippingNo,
  } from "@/api/collaborativeApproval/approvalProcess";
  import { getQuotationList } from "@/api/salesManagement/salesQuotation";
  import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger";
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  import { userListNoPageByTenantId } from "@/api/system/user";
  const data = reactive({
    form: {
@@ -229,8 +377,7 @@
      approveDeptId: "",
      approveReason: "",
      checkResult: "",
      tempFileIds: [],
      approverList: [], // æ–°å¢žå­—段,存储所有节点的审批人id
      storageBlobDTOS: [],
      startDate: "",
      endDate: "",
      location: "",
@@ -258,8 +405,6 @@
  const productOptions = ref([]);
  const operationType = ref("");
  const currentApproveStatus = ref("");
  const approverNodes = ref([]);
  const userList = ref([]);
  const formRef = ref(null);
  const message = ref("");
  const showDate = ref(false);
@@ -270,6 +415,19 @@
  const endDateValue = ref(Date.now());
  const userStore = useUserStore();
  const approveType = ref(0);
  const isInitialLoading = ref(false);
  const quotationLoading = ref(false);
  const currentQuotation = ref({});
  const purchaseLoading = ref(false);
  const currentPurchase = ref({});
  const deliveryLoading = ref(false);
  const currentDelivery = ref({});
  const deliveryProductList = ref([]);
  const isQuotationApproval = computed(() => Number(approveType.value) === 6);
  const isPurchaseApproval = computed(() => Number(approveType.value) === 5);
  const isDeliveryApproval = computed(() => Number(approveType.value) === 7);
  const getProductOptions = () => {
    getDept().then(res => {
@@ -279,20 +437,133 @@
      }));
    });
  };
  const fileList = ref([]);
  let nextApproverId = 2;
  const getCurrentinfo = () => {
    userStore.getInfo().then(res => {
      form.value.approveDeptId = res.user.tenantId;
      console.log(res.user.tenantId, "res.user.tenantId");
    });
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const onDateConfirm = e => {
    form.value.approveTime = formatDateToYMD(e.value);
    currentDate.value = formatDateToYMD(e.value);
    showDate.value = false;
  };
  // æ˜¾ç¤ºè¯·å‡å¼€å§‹æ—¶é—´é€‰æ‹©å™¨
  const showStartDatePicker = () => {
    showStartDate.value = true;
  };
  // ç¡®è®¤è¯·å‡å¼€å§‹æ—¶é—´é€‰æ‹©
  const onStartDateConfirm = e => {
    form.value.startDate = formatDateToYMD(e.value);
    showStartDate.value = false;
  };
  const showEndDatePicker = () => {
    showEndDate.value = true;
  };
  // ç¡®è®¤è¯·å‡ç»“束时间选择
  const onEndDateConfirm = e => {
    form.value.endDate = formatDateToYMD(e.value);
    showEndDate.value = false;
  };
  const fetchDetailData = async row => {
    // æŠ¥ä»·å®¡æ‰¹
    if (isQuotationApproval.value) {
      const quotationNo = row?.approveReason;
      if (quotationNo) {
        quotationLoading.value = true;
        getQuotationList({ quotationNo })
          .then(res => {
            const records = res?.data?.records || [];
            currentQuotation.value = records[0] || {};
          })
          .finally(() => {
            quotationLoading.value = false;
          });
      }
    }
    // é‡‡è´­å®¡æ‰¹
    if (isPurchaseApproval.value) {
      const purchaseContractNumber = row?.approveReason;
      if (purchaseContractNumber) {
        purchaseLoading.value = true;
        getPurchaseByCode({ purchaseContractNumber })
          .then(res => {
            currentPurchase.value = res;
          })
          .catch(err => {
            console.error("查询采购详情失败:", err);
          })
          .finally(() => {
            purchaseLoading.value = false;
          });
      }
    }
    // å‘货审批
    if (isDeliveryApproval.value) {
      const deliveryNo = row?.approveReason;
      if (deliveryNo) {
        deliveryLoading.value = true;
        currentDelivery.value = {};
        deliveryProductList.value = [];
        getDeliveryDetailByShippingNo({ shippingNo: deliveryNo })
          .then(res => {
            const detailData = res?.data || res || {};
            currentDelivery.value = detailData;
            deliveryProductList.value =
              detailData.shippingProductDetailDtoList || [];
          })
          .catch(err => {
            console.error("查询发货详情失败:", err);
          })
          .finally(() => {
            deliveryLoading.value = false;
          });
      }
    }
  };
  // ç›‘听审批事由变化,如果是特定审批类型则尝试获取详情
  watch(
    () => form.value.approveReason,
    newVal => {
      if (isInitialLoading.value) return;
      if (
        newVal &&
        (isQuotationApproval.value ||
          isPurchaseApproval.value ||
          isDeliveryApproval.value)
      ) {
        // å»¶è¿Ÿä¸€ä¼šå†è¯·æ±‚,避免输入过程中频繁触发
        debounceFetchDetail();
      }
    }
  );
  let timer = null;
  const debounceFetchDetail = () => {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fetchDetailData(form.value);
    }, 800);
  };
  onMounted(async () => {
    try {
      getProductOptions();
      userListNoPageByTenantId().then(res => {
        userList.value = res.data;
      });
      form.value.approveUser = userStore.id;
      form.value.approveUserName = userStore.nickName;
      form.value.approveTime = getCurrentDate();
@@ -302,57 +573,39 @@
      approveType.value = uni.getStorageSync("approveType") || 0;
      // å¦‚果是编辑模式,从本地存储获取数据
      if (operationType.value === "edit") {
      if (operationType.value === "edit" || operationType.value === "detail") {
        const storedData = uni.getStorageSync("invoiceLedgerEditRow");
        if (storedData) {
          const row = JSON.parse(storedData);
          fileList.value = row.commonFileList || [];
          form.value.tempFileIds = fileList.value.map(file => file.id);
          currentApproveStatus.value = row.approveStatus;
          approveProcessGetInfo({ id: row.approveId, approveReason: "1" }).then(
            res => {
          isInitialLoading.value = true;
          approveProcessGetInfo({ id: row.approveId, approveReason: "1" })
            .then(res => {
              form.value = { ...res.data };
              // åæ˜¾å®¡æ‰¹äºº
              if (res.data && res.data.approveUserIds) {
                const userIds = res.data.approveUserIds.split(",");
                approverNodes.value = userIds.map((userId, idx) => {
                  const userIdNum = parseInt(userId.trim());
                  // ä»ŽuserList中找到对应的用户信息
                  const userInfo = userList.value.find(
                    user => user.userId === userIdNum
                  );
                  return {
                    id: idx + 1,
                    userId: userIdNum,
                    nickName: userInfo ? userInfo.nickName : null,
                  };
                });
                nextApproverId = userIds.length + 1;
              } else {
                // æ–°å¢žæ¨¡å¼ï¼Œåˆå§‹åŒ–一个空的审批节点
                approverNodes.value = [{ id: 1, userId: null, nickName: null }];
                nextApproverId = 2;
              // è®¾ç½®å›¾ç‰‡åˆ—表显示
              const fileData =
                res.data.storageBlobVOS || res.data.commonFileList || [];
              if (fileData.length > 0) {
                form.value.storageBlobDTOS = fileData;
              }
            }
          );
              // èŽ·å–é¢å¤–è¯¦æƒ…
              fetchDetailData(res.data);
            })
            .finally(() => {
              // å»¶è¿Ÿä¸€ä¼šé‡ç½®ï¼Œç¡®ä¿ watch ä¸ä¼šè¢«è§¦å‘
              setTimeout(() => {
                isInitialLoading.value = false;
              }, 100);
            });
        }
      } else {
        // æ–°å¢žæ¨¡å¼ï¼Œåˆå§‹åŒ–一个空的审批节点
        approverNodes.value = [{ id: 1, userId: null }];
      }
      // ç›‘听联系人选择事件
      uni.$on("selectContact", handleSelectContact);
    } catch (error) {
      console.error("获取部门数据失败:", error);
      console.error("获取数据失败:", error);
    }
  });
  onUnmounted(() => {
    // ç§»é™¤äº‹ä»¶ç›‘听
    uni.$off("selectContact", handleSelectContact);
  });
  onUnmounted(() => {});
  const onConfirm = item => {
    // è®¾ç½®é€‰ä¸­çš„部门
@@ -375,13 +628,6 @@
  };
  const submitForm = () => {
    // æ£€æŸ¥æ¯ä¸ªå®¡æ‰¹æ­¥éª¤æ˜¯å¦éƒ½æœ‰å®¡æ‰¹äºº
    const hasEmptyStep = approverNodes.value.some(step => !step.nickName);
    if (hasEmptyStep) {
      showToast("请为每个审批步骤选择审批人");
      return;
    }
    // æ‰‹åŠ¨æ£€æŸ¥å¿…å¡«å­—æ®µï¼Œé˜²æ­¢å› æ•°æ®ç±»åž‹é—®é¢˜å¯¼è‡´çš„æ ¡éªŒå¤±è´¥
    if (!form.value.approveReason || !form.value.approveReason.trim()) {
      showToast("请输入申请事由");
@@ -406,26 +652,8 @@
      .then(valid => {
        if (valid) {
          // è¡¨å•校验通过,可以提交数据
          // æ”¶é›†æ‰€æœ‰èŠ‚ç‚¹çš„å®¡æ‰¹äººid
          console.log("approverNodes---", approverNodes.value);
          form.value.approveUserIds = approverNodes.value
            .map(node => node.userId)
            .join(",");
          form.value.approveType = approveType.value;
          form.value.approveDeptId = Number(form.value.approveDeptId);
          // const submitForm = {
          //   approveDeptId: form.value.approveDeptId,
          //   approveDeptName: form.value.approveDeptName,
          //   approveReason: form.value.approveReason,
          //   approveTime: form.value.approveTime,
          //   approveType: form.value.approveType,
          //   approveUser: form.value.approveUser,
          //   approveUserIds: form.value.approveUserIds,
          //   endDate: form.value.endDate,
          //   startDate: form.value.startDate,
          // };
          // console.log("form.value---", form.value);
          // console.log("submitForm", submitForm);
          if (operationType.value === "add" || currentApproveStatus.value == 3) {
            approveProcessAdd(form.value).then(res => {
@@ -461,77 +689,6 @@
      });
  };
  // å¤„理联系人选择结果
  const handleSelectContact = data => {
    const { stepIndex, contact } = data;
    // å°†é€‰ä¸­çš„联系人设置为对应审批步骤的审批人
    approverNodes.value[stepIndex].userId = contact.userId;
    approverNodes.value[stepIndex].nickName = contact.nickName;
  };
  const addApprover = stepIndex => {
    // è·³è½¬åˆ°è”系人选择页面
    uni.setStorageSync("stepIndex", stepIndex);
    uni.navigateTo({
      url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect",
    });
  };
  const addApprovalStep = () => {
    // æ·»åŠ æ–°çš„å®¡æ‰¹æ­¥éª¤
    approverNodes.value.push({ userId: null, nickName: null });
  };
  const removeApprover = stepIndex => {
    // ç§»é™¤å®¡æ‰¹äºº
    approverNodes.value[stepIndex].userId = null;
    approverNodes.value[stepIndex].nickName = null;
  };
  const removeApprovalStep = stepIndex => {
    // ç¡®ä¿è‡³å°‘保留一个审批步骤
    if (approverNodes.value.length > 1) {
      approverNodes.value.splice(stepIndex, 1);
    } else {
      uni.showToast({
        title: "至少需要一个审批步骤",
        icon: "none",
      });
    }
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const onDateConfirm = e => {
    form.value.approveTime = formatDateToYMD(e.value);
    currentDate.value = formatDateToYMD(e.value);
    showDate.value = false;
  };
  // æ˜¾ç¤ºè¯·å‡å¼€å§‹æ—¶é—´é€‰æ‹©å™¨
  const showStartDatePicker = () => {
    showStartDate.value = true;
  };
  // ç¡®è®¤è¯·å‡å¼€å§‹æ—¶é—´é€‰æ‹©
  const onStartDateConfirm = e => {
    form.value.startDate = formatDateToYMD(e.value);
    showStartDate.value = false;
  };
  const showEndDatePicker = () => {
    showEndDate.value = true;
  };
  // ç¡®è®¤è¯·å‡ç»“束时间选择
  const onEndDateConfirm = e => {
    form.value.endDate = formatDateToYMD(e.value);
    showEndDate.value = false;
  };
  // èŽ·å–å½“å‰æ—¥æœŸå¹¶æ ¼å¼åŒ–ä¸º YYYY-MM-DD
  function getCurrentDate() {
    const today = new Date();
@@ -544,238 +701,8 @@
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .approval-process {
    background: #fff;
    margin: 16px;
    border-radius: 16px;
    padding: 16px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
  }
  .approval-header {
    margin-bottom: 16px;
  }
  .approval-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
    display: block;
    margin-bottom: 4px;
  }
  .approval-desc {
    font-size: 12px;
    color: #999;
  }
  /* æ ·å¼å¢žå¼ºä¸ºâ€œç®€æ´å°åœ†åœˆé£Žæ ¼â€ */
  .approval-steps {
    padding-left: 22px;
    position: relative;
    &::before {
      content: "";
      position: absolute;
      left: 11px;
      top: 40px;
      bottom: 40px;
      width: 2px;
      background: linear-gradient(
        to bottom,
        #e6f7ff 0%,
        #bae7ff 50%,
        #91d5ff 100%
      );
      border-radius: 1px;
    }
  }
  .approval-step {
    position: relative;
    margin-bottom: 24px;
    &::before {
      content: "";
      position: absolute;
      left: -18px;
      top: 14px; // ä»Ž 8px è°ƒæ•´ä¸º 14px,与文字中心对齐
      width: 12px;
      height: 12px;
      background: #fff;
      border: 3px solid #006cfb;
      border-radius: 50%;
      z-index: 2;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }
  }
  .step-title {
    top: 12px;
    margin-bottom: 12px;
    position: relative;
    margin-left: 6px;
  }
  .step-title text {
    font-size: 14px;
    color: #666;
    background: #f0f0f0;
    padding: 4px 12px;
    border-radius: 12px;
    position: relative;
    line-height: 1.4; // ç¡®ä¿æ–‡å­—行高一致
  }
  .approver-item {
    display: flex;
    align-items: center;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
    border-radius: 16px;
    padding: 16px;
    gap: 12px;
    position: relative;
    border: 1px solid #e6f7ff;
    box-shadow: 0 4px 12px rgba(0, 108, 251, 0.08);
    transition: all 0.3s ease;
  }
  .approver-avatar {
    width: 48px;
    height: 48px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
  }
  .avatar-text {
    color: #fff;
    font-size: 18px;
    font-weight: 600;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  }
  .approver-info {
    flex: 1;
    position: relative;
  }
  .approver-name {
    display: block;
    font-size: 16px;
    color: #333;
    font-weight: 500;
    position: relative;
  }
  .approver-dept {
    font-size: 12px;
    color: #999;
    background: rgba(0, 108, 251, 0.05);
    padding: 2px 8px;
    border-radius: 8px;
    display: inline-block;
    position: relative;
    &::before {
      content: "";
      position: absolute;
      left: 4px;
      top: 50%;
      transform: translateY(-50%);
      width: 2px;
      height: 2px;
      background: #006cfb;
      border-radius: 50%;
    }
  }
  .delete-approver-btn {
    font-size: 16px;
    color: #ff4d4f;
    background: linear-gradient(
      135deg,
      rgba(255, 77, 79, 0.1) 0%,
      rgba(255, 77, 79, 0.05) 100%
    );
    width: 28px;
    height: 28px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.3s ease;
    position: relative;
  }
  .add-approver-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%);
    border: 2px dashed #006cfb;
    border-radius: 16px;
    padding: 20px;
    color: #006cfb;
    font-size: 14px;
    position: relative;
    transition: all 0.3s ease;
    &::before {
      content: "";
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      width: 32px;
      height: 32px;
      border: 2px solid #006cfb;
      border-radius: 50%;
      opacity: 0;
      transition: all 0.3s ease;
    }
  }
  .delete-step-btn {
    color: #ff4d4f;
    font-size: 12px;
    background: linear-gradient(
      135deg,
      rgba(255, 77, 79, 0.1) 0%,
      rgba(255, 77, 79, 0.05) 100%
    );
    padding: 6px 12px;
    border-radius: 12px;
    display: inline-block;
    position: relative;
    transition: all 0.3s ease;
    &::before {
      content: "";
      position: absolute;
      left: 6px;
      top: 50%;
      transform: translateY(-50%);
      width: 4px;
      height: 4px;
      background: #ff4d4f;
      border-radius: 50%;
    }
  }
  .step-line {
    display: none; // éšè—åŽŸæ¥çš„çº¿æ¡ï¼Œä½¿ç”¨ä¼ªå…ƒç´ ä»£æ›¿
  }
  .add-step-btn {
    display: flex;
    align-items: center;
    justify-content: center;
  .account-detail {
    background-color: #fff;
  }
  .footer-btns {
    position: fixed;
@@ -809,121 +736,5 @@
    background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
  }
  // åŠ¨ç”»å®šä¹‰
  @keyframes pulse {
    0% {
      transform: scale(1);
      opacity: 1;
    }
    50% {
      transform: scale(1.2);
      opacity: 0.7;
    }
    100% {
      transform: scale(1);
      opacity: 1;
    }
  }
  @keyframes rotate {
    0% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(360deg);
    }
  }
  @keyframes ripple {
    0% {
      transform: translate(-50%, -50%) scale(0.8);
      opacity: 1;
    }
    100% {
      transform: translate(-50%, -50%) scale(1.6);
      opacity: 0;
    }
  }
  /* å¦‚果已有 .step-line,这里更精准定位到左侧与小圆点对齐 */
  .step-line {
    position: absolute;
    left: 4px;
    top: 48px;
    width: 2px;
    height: calc(100% - 48px);
    background: #e5e7eb;
  }
  .approver-container {
    display: flex;
    align-items: center;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
    border-radius: 16px;
    gap: 12px;
    padding: 10px 0;
    background: transparent;
    border: none;
    box-shadow: none;
  }
  .approver-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 10px;
    background: transparent;
    border: none;
    box-shadow: none;
    border-radius: 0;
  }
  .approver-avatar {
    position: relative;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: #f3f4f6;
    border: 2px solid #e5e7eb;
    display: flex;
    align-items: center;
    justify-content: center;
    animation: none; /* ç¦ç”¨æ—‹è½¬ç­‰åŠ¨ç”»ï¼Œå›žå½’ç®€æ´ */
  }
  .avatar-text {
    font-size: 14px;
    color: #374151;
    font-weight: 600;
  }
  .add-approver-btn {
    display: flex;
    align-items: center;
    gap: 8px;
    background: transparent;
    border: none;
    box-shadow: none;
    padding: 0;
  }
  .add-approver-btn .add-circle {
    width: 40px;
    height: 40px;
    border: 2px dashed #a0aec0;
    border-radius: 50%;
    color: #6b7280;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 22px;
    line-height: 1;
  }
  .add-approver-btn .add-label {
    color: #3b82f6;
    font-size: 14px;
  }
</style>
src/pages/cooperativeOffice/collaborativeApproval/index.vue
@@ -97,13 +97,20 @@
              </view>
              <view class="detail-row">
                <view class="actions">
                  <!-- <u-button type="primary"
                  <u-button type="primary"
                            size="small"
                            class="action-btn edit"
                            :disabled="item.approveStatus == 2 || item.approveStatus == 1 || item.approveStatus == 4 || item.approveStatus == 8"
                            v-if="!(item.approveStatus == 2 || item.approveStatus == 1 || item.approveStatus == 4 || item.approveStatus == 8 || item.approveType == 5 || item.approveType == 6 || item.approveType == 7)"
                            @click="handleItemClick(item)">
                    ç¼–辑
                  </u-button> -->
                  </u-button>
                  <u-button type="info"
                            v-if="item.approveType == 5 || item.approveType == 6 || item.approveType == 7"
                            size="small"
                            class="action-btn detail"
                            @click="handleDetailClick(item)">
                    è¯¦æƒ…
                  </u-button>
                  <u-button type="success"
                            size="small"
                            class="action-btn approve"
@@ -123,13 +130,13 @@
      <text>暂无审批数据</text>
    </view>
    <!-- æµ®åŠ¨æ“ä½œæŒ‰é’® -->
    <!-- <view class="fab-button"
    <view class="fab-button"
          v-if="props.approveType != 5 && props.approveType != 6 && props.approveType != 7"
          @click="handleAdd">
      <up-icon name="plus"
               size="24"
               color="#ffffff"></up-icon>
    </view> -->
    </view>
  </view>
</template>
@@ -262,6 +269,17 @@
    });
  };
  // æŸ¥çœ‹è¯¦æƒ…
  const handleDetailClick = item => {
    uni.setStorageSync("invoiceLedgerEditRow", JSON.stringify(item));
    uni.setStorageSync("operationType", "detail");
    uni.setStorageSync("approveId", item.approveId);
    uni.setStorageSync("approveType", props.approveType);
    uni.navigateTo({
      url: "/pages/cooperativeOffice/collaborativeApproval/detail",
    });
  };
  // æ·»åŠ æ–°è®°å½•
  const handleAdd = () => {
    uni.setStorageSync("operationType", "add");
src/pages/equipmentManagement/repair/add.vue
@@ -69,6 +69,20 @@
                   placeholder="请输入报修人"
                   clearable />
        </u-form-item>
        <u-form-item label="维修人"
                     prop="maintenanceName"
                     border-bottom>
          <u-input v-model="form.maintenanceName"
                   placeholder="请输入维修人"
                   clearable />
        </u-form-item>
        <u-form-item label="维修项目"
                     prop="machineryCategory"
                     border-bottom>
          <u-input v-model="form.machineryCategory"
                   placeholder="请输入维修项目"
                   clearable />
        </u-form-item>
        <u-form-item label="故障现象"
                     prop="remark"
                     required
@@ -79,6 +93,11 @@
                      clearable
                      count
                      maxlength="200" />
        </u-form-item>
        <u-form-item label="图片附件"
                     prop="storageBlobDTOs"
                     border-bottom>
          <CommonUpload v-model="form.storageBlobDTOs" />
        </u-form-item>
      </u-cell-group>
      <!-- æäº¤æŒ‰é’® -->
@@ -108,8 +127,9 @@
<script setup>
  import { ref, computed, onMounted, onUnmounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import { onShow, onLoad } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import CommonUpload from "@/components/CommonUpload.vue";
  import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
  import {
    addRepair,
@@ -132,10 +152,18 @@
  // è¡¨å•引用
  const formRef = ref(null);
  const operationType = ref("add");
  const repairId = ref("");
  const loading = ref(false);
  const showDevice = ref(false);
  const showDate = ref(false);
  const pickerDateValue = ref(Date.now());
  onLoad(options => {
    if (options.id) {
      repairId.value = options.id;
    }
    getPageParams();
  });
  // è®¾å¤‡é€‰é¡¹
  const deviceOptions = ref([]);
@@ -169,7 +197,10 @@
    deviceModel: undefined, // è§„格型号
    repairTime: dayjs().format("YYYY-MM-DD"), // æŠ¥ä¿®æ—¥æœŸ
    repairName: undefined, // æŠ¥ä¿®äºº
    maintenanceName: undefined, // ç»´ä¿®äºº
    machineryCategory: undefined, // ç»´ä¿®é¡¹ç›®
    remark: undefined, // æ•…障现象
    storageBlobDTOs: [], // å›¾ç‰‡é™„ä»¶
  });
  // æŠ¥ä¿®çŠ¶æ€é€‰é¡¹
@@ -221,7 +252,10 @@
          form.value.deviceModel = data.deviceModel;
          form.value.repairTime = dayjs(data.repairTime).format("YYYY-MM-DD");
          form.value.repairName = data.repairName;
          form.value.maintenanceName = data.maintenanceName;
          form.value.machineryCategory = data.machineryCategory;
          form.value.remark = data.remark;
          form.value.storageBlobDTOs = data.storageBlobVOs || [];
          repairStatusText.value =
            repairStatusOptions.value.find(item => item.value == data.status)
              ?.name || "";
@@ -328,14 +362,12 @@
  };
  onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶èŽ·å–å‚æ•°
    getPageParams();
    // é¡µé¢æ˜¾ç¤ºæ—¶é€»è¾‘
  });
  onMounted(() => {
    // é¡µé¢åŠ è½½æ—¶èŽ·å–è®¾å¤‡åˆ—è¡¨å’Œå‚æ•°
    // é¡µé¢åŠ è½½æ—¶èŽ·å–è®¾å¤‡åˆ—è¡¨
    loadDeviceName();
    getPageParams();
  });
  // ç»„件卸载时清理定时器
@@ -375,7 +407,6 @@
      // å‡†å¤‡æäº¤æ•°æ®
      const submitData = { ...form.value };
      const { code } = id
        ? await editRepair({ id: id, ...submitData })
        : await addRepair(submitData);
@@ -396,21 +427,15 @@
  // è¿”回上一页
  const goBack = () => {
    uni.removeStorageSync("repairId");
    uni.navigateBack();
  };
  // èŽ·å–é¡µé¢å‚æ•°
  const getPageParams = () => {
    // ä½¿ç”¨uni.getStorageSync获取id
    const id = uni.getStorageSync("repairId");
    // æ ¹æ®æ˜¯å¦æœ‰id参数来判断是新增还是编辑
    if (id) {
    if (repairId.value) {
      // ç¼–辑模式,获取详情
      loadForm(id);
      // å¯é€‰ï¼šèŽ·å–åŽæ¸…é™¤å­˜å‚¨çš„id,避免影响后续操作
      uni.removeStorageSync("repairId");
      loadForm(repairId.value);
    } else {
      // æ–°å¢žæ¨¡å¼
      loadForm();
@@ -419,9 +444,7 @@
  // èŽ·å–é¡µé¢ID
  const getPageId = () => {
    // ä½¿ç”¨uni.getStorageSync获取id
    const id = uni.getStorageSync("repairId");
    return id;
    return repairId.value;
  };
</script>
src/pages/equipmentManagement/repair/index.vue
@@ -58,12 +58,16 @@
              <text class="detail-value">{{ item.repairName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">故障现象</text>
              <text class="detail-value">{{ item.remark || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维修人</text>
              <text class="detail-value">{{ item.maintenanceName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维修项目</text>
              <text class="detail-value">{{ item.machineryCategory || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">故障现象</text>
              <text class="detail-value">{{ item.remark || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维修结果</text>
@@ -208,9 +212,9 @@
  const edit = id => {
    if (!id) return;
    // ä½¿ç”¨uni.setStorageSync存储id
    uni.setStorageSync("repairId", id);
    // uni.setStorageSync("repairId", id);
    uni.navigateTo({
      url: "/pages/equipmentManagement/repair/add",
      url: "/pages/equipmentManagement/repair/add?id=" + id,
    });
  };
src/pages/equipmentManagement/upkeep/add.vue
@@ -1,393 +1,435 @@
<template>
    <view class="upkeep-add">
        <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
        <PageHeader :title="operationType === 'edit' ? '编辑保养计划' : '新增保养计划'" @back="goBack" />
        <!-- è¡¨å•内容 -->
        <u-form ref="formRef" :model="form" :rules="formRules" label-width="110px">
            <!-- åŸºæœ¬ä¿¡æ¯ -->
            <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>
            <!-- æäº¤æŒ‰é’® -->
            <view class="footer-btns">
                <u-button class="cancel-btn" @click="goBack">取消</u-button>
                <u-button class="save-btn" @click="sendForm" :loading="loading">保存</u-button>
            </view>
        </u-form>
        <!-- è®¾å¤‡é€‰æ‹©å™¨ -->
        <up-action-sheet
            :show="showDevice"
            :actions="deviceActions"
            title="选择设备"
            @select="onDeviceConfirm"
            @close="showDevice = false"
        />
<up-datetime-picker
            :show="showDate"
            v-model="pickerDateValue"
            @confirm="onDateConfirm"
            @cancel="showDate = false"
            mode="date"
        />
    </view>
  <view class="upkeep-add">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader :title="operationType === 'edit' ? '编辑保养计划' : '新增保养计划'"
                @back="goBack" />
    <!-- è¡¨å•内容 -->
    <u-form ref="formRef"
            :model="form"
            :rules="formRules"
            label-width="110px">
      <!-- åŸºæœ¬ä¿¡æ¯ -->
      <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"
                   border-bottom>
        <u-input v-model="form.maintenancePerson"
                 placeholder="请输入保养人"
                 clearable />
      </u-form-item>
      <u-form-item label="保养项目"
                   prop="machineryCategory"
                   border-bottom>
        <u-input v-model="form.machineryCategory"
                 placeholder="请输入保养项目"
                 clearable />
      </u-form-item>
      <!-- æäº¤æŒ‰é’® -->
      <view class="footer-btns">
        <u-button class="cancel-btn"
                  @click="goBack">取消</u-button>
        <u-button class="save-btn"
                  @click="sendForm"
                  :loading="loading">保存</u-button>
      </view>
    </u-form>
    <!-- è®¾å¤‡é€‰æ‹©å™¨ -->
    <up-action-sheet :show="showDevice"
                     :actions="deviceActions"
                     title="选择设备"
                     @select="onDeviceConfirm"
                     @close="showDevice = false" />
    <up-datetime-picker :show="showDate"
                        v-model="pickerDateValue"
                        @confirm="onDateConfirm"
                        @cancel="showDate = false"
                        mode="date" />
  </view>
</template>
<script setup>
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 { formatDateToYMD } from '@/utils/ruoyi';
  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 { formatDateToYMD } from "@/utils/ruoyi";
defineOptions({
    name: "设备保养计划表单",
});
const showToast = (message) => {
  uni.showToast({
    title: message,
    icon: 'none'
  })
}
  defineOptions({
    name: "设备保养计划表单",
  });
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
// è¡¨å•引用
const formRef = ref(null);
const operationType = ref('add');
const loading = ref(false);
const showDevice = 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 formRef = ref(null);
  const operationType = ref("add");
  const loading = ref(false);
  const showDevice = 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('');
// è½¬æ¢ä¸º action-sheet éœ€è¦çš„æ ¼å¼
const deviceActions = computed(() => {
    return deviceOptions.value.map(item => ({
        text: item.deviceName,
        value: item.id,
        data: item
    }));
});
  // è®¾å¤‡é€‰é¡¹
  const deviceOptions = ref([]);
  const deviceNameText = ref("");
  // è½¬æ¢ä¸º action-sheet éœ€è¦çš„æ ¼å¼
  const deviceActions = computed(() => {
    return deviceOptions.value.map(item => ({
      text: item.deviceName,
      value: item.id,
      data: item,
    }));
  });
// æ‰«ç ç›¸å…³çŠ¶æ€
const isScanning = ref(false);
const scanTimer = ref(null);
  // æ‰«ç ç›¸å…³çŠ¶æ€
  const isScanning = ref(false);
  const scanTimer = ref(null);
// è¡¨å•验证规则
const formRules = {
    deviceLedgerId: [{ required: true, trigger: "change", message: "请选择设备名称" }],
    maintenancePlanTime: [{ required: true, trigger: "change", message: "请选择计划保养日期" }],
};
  // è¡¨å•验证规则
  const formRules = {
    deviceLedgerId: [
      { required: true, trigger: "change", message: "请选择设备名称" },
    ],
    maintenancePlanTime: [
      { required: true, trigger: "change", message: "请选择计划保养日期" },
    ],
  };
// ä½¿ç”¨ ref å£°æ˜Žè¡¨å•数据
const form = ref({
    deviceLedgerId: undefined, // è®¾å¤‡ID
    deviceModel: undefined, // è§„格型号
    maintenancePlanTime: dayjs().format("YYYY-MM-DD"), // è®¡åˆ’保养日期
});
  // ä½¿ç”¨ ref å£°æ˜Žè¡¨å•数据
  const form = ref({
    deviceLedgerId: undefined, // è®¾å¤‡ID
    deviceModel: undefined, // è§„格型号
    maintenancePlanTime: dayjs().format("YYYY-MM-DD"), // è®¡åˆ’保养日期
    maintenancePerson: undefined, // ä¿å…»äºº
    machineryCategory: undefined, // ä¿å…»é¡¹ç›®
  });
// åŠ è½½è®¾å¤‡åˆ—è¡¨
const loadDeviceName = async () => {
    try {
        const { data } = await getDeviceLedger();
        deviceOptions.value = data || [];
    } catch (e) {
        showToast('获取设备列表失败');
    }
};
  // åŠ è½½è®¾å¤‡åˆ—è¡¨
  const loadDeviceName = async () => {
    try {
      const { data } = await getDeviceLedger();
      deviceOptions.value = data || [];
    } catch (e) {
      showToast("获取设备列表失败");
    }
  };
// åŠ è½½è¡¨å•æ•°æ®ï¼ˆç¼–è¾‘æ¨¡å¼ï¼‰
const loadForm = async (id) => {
    if (id) {
        operationType.value = 'edit';
        try {
            const { code, data } = await getUpkeepById(id);
            if (code == 200) {
                form.value.deviceLedgerId = data.deviceLedgerId;
                form.value.deviceModel = data.deviceModel;
                form.value.maintenancePlanTime = dayjs(data.maintenancePlanTime).format("YYYY-MM-DD");
                // è®¾ç½®è®¾å¤‡åç§°æ˜¾ç¤º
                const device = deviceOptions.value.find(item => item.id === data.deviceLedgerId);
                if (device) {
                    form.value.deviceNameText = device.deviceName;
                }
            }
        } catch (e) {
            showToast('获取详情失败');
        }
    } else {
        // æ–°å¢žæ¨¡å¼
        operationType.value = 'add';
    }
};
  // åŠ è½½è¡¨å•æ•°æ®ï¼ˆç¼–è¾‘æ¨¡å¼ï¼‰
  const loadForm = async id => {
    if (id) {
      operationType.value = "edit";
      try {
        const { code, data } = await getUpkeepById(id);
        if (code == 200) {
          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.machineryCategory = data.machineryCategory;
          // è®¾ç½®è®¾å¤‡åç§°æ˜¾ç¤º
          const device = deviceOptions.value.find(
            item => item.id === data.deviceLedgerId
          );
          if (device) {
            form.value.deviceNameText = device.deviceName;
          }
        }
      } catch (e) {
        showToast("获取详情失败");
      }
    } else {
      // æ–°å¢žæ¨¡å¼
      operationType.value = "add";
    }
  };
// æ‰«æäºŒç»´ç åŠŸèƒ½
const startScan = () => {
    if (isScanning.value) {
        showToast('正在扫描中,请稍候...');
        return;
    }
    // è°ƒç”¨uni-app的扫码API
    uni.scanCode({
        scanType: ['qrCode', 'barCode'],
        success: (res) => {
            handleScanResult(res.result);
        },
        fail: (err) => {
            console.error('扫码失败:', err);
            showToast('扫码失败,请重试');
        }
    });
};
  // æ‰«æäºŒç»´ç åŠŸèƒ½
  const startScan = () => {
    if (isScanning.value) {
      showToast("正在扫描中,请稍候...");
      return;
    }
// å¤„理扫码结果
const handleScanResult = (scanResult) => {
    if (!scanResult) {
        showToast('扫码结果为空');
        return;
    }
    isScanning.value = true;
    showToast('扫码成功');
    // 3秒后处理扫码结果
    scanTimer.value = setTimeout(() => {
        processScanResult(scanResult);
        isScanning.value = false;
    }, 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 deviceId = getDeviceIdByRegExp(scanResult);
    const matchedDevice = deviceOptions.value.find(item => item.id == deviceId);
    if (matchedDevice) {
        // æ‰¾åˆ°åŒ¹é…çš„设备,自动填充
        form.value.deviceLedgerId = matchedDevice.id;
        form.value.deviceNameText = matchedDevice.deviceName;
        form.value.deviceModel = matchedDevice.deviceModel;
        showToast('设备信息已自动填充');
    } else {
        // æœªæ‰¾åˆ°åŒ¹é…çš„设备
        showToast('未找到匹配的设备,请手动选择');
    }
};
    // è°ƒç”¨uni-app的扫码API
    uni.scanCode({
      scanType: ["qrCode", "barCode"],
      success: res => {
        handleScanResult(res.result);
      },
      fail: err => {
        console.error("扫码失败:", err);
        showToast("扫码失败,请重试");
      },
    });
  };
// æ˜¾ç¤ºè®¾å¤‡é€‰æ‹©å™¨
const showDevicePicker = () => {
    showDevice.value = true;
};
  // å¤„理扫码结果
  const handleScanResult = scanResult => {
    if (!scanResult) {
      showToast("扫码结果为空");
      return;
    }
// ç¡®è®¤è®¾å¤‡é€‰æ‹©
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;
};
    isScanning.value = true;
    showToast("扫码成功");
// æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
const showDatePicker = () => {
    showDate.value = true;
};
    // 3秒后处理扫码结果
    scanTimer.value = setTimeout(() => {
      processScanResult(scanResult);
      isScanning.value = false;
    }, 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 deviceId = getDeviceIdByRegExp(scanResult);
    const matchedDevice = deviceOptions.value.find(item => item.id == deviceId);
// ç¡®è®¤æ—¥æœŸé€‰æ‹©
const onDateConfirm = (e) => {
    form.value.maintenancePlanTime = formatDateToYMD(e.value);
    showDate.value = false;
};
    if (matchedDevice) {
      // æ‰¾åˆ°åŒ¹é…çš„设备,自动填充
      form.value.deviceLedgerId = matchedDevice.id;
      form.value.deviceNameText = matchedDevice.deviceName;
      form.value.deviceModel = matchedDevice.deviceModel;
      showToast("设备信息已自动填充");
    } else {
      // æœªæ‰¾åˆ°åŒ¹é…çš„设备
      showToast("未找到匹配的设备,请手动选择");
    }
  };
onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶èŽ·å–å‚æ•°
    getPageParams();
});
  // æ˜¾ç¤ºè®¾å¤‡é€‰æ‹©å™¨
  const showDevicePicker = () => {
    showDevice.value = true;
  };
onMounted(() => {
    // é¡µé¢åŠ è½½æ—¶èŽ·å–è®¾å¤‡åˆ—è¡¨å’Œå‚æ•°
    loadDeviceName();
    getPageParams();
});
  // ç¡®è®¤è®¾å¤‡é€‰æ‹©
  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;
  };
// ç»„件卸载时清理定时器
onUnmounted(() => {
    if (scanTimer.value) {
        clearTimeout(scanTimer.value);
    }
});
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
// æäº¤è¡¨å•
const sendForm = async () => {
    try {
        // æ‰‹åŠ¨éªŒè¯è¡¨å•
        const valid = await formRef.value.validate();
        if (!valid) return;
        loading.value = true;
        const id = getPageId();
        // å‡†å¤‡æäº¤æ•°æ®
        const submitData = { ...form.value };
        // ç¡®ä¿æ—¥æœŸæ ¼å¼æ­£ç¡®
        if (submitData.maintenancePlanTime && !submitData.maintenancePlanTime.includes(':')) {
            submitData.maintenancePlanTime = submitData.maintenancePlanTime + ' 00:00:00';
        }
        const { code } = id
            ? await editUpkeep({ id: id, ...submitData })
            : await addUpkeep(submitData);
        if (code == 200) {
            showToast(`${id ? "编辑" : "新增"}计划成功`);
            setTimeout(() => {
                uni.navigateBack();
            }, 1500);
        } else {
            loading.value = false;
        }
    } catch (e) {
        loading.value = false;
        showToast('表单验证失败');
    }
};
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const onDateConfirm = e => {
    form.value.maintenancePlanTime = formatDateToYMD(e.value);
    showDate.value = false;
  };
// è¿”回上一页
const goBack = () => {
    // æ¸…除存储的id
    uni.removeStorageSync('repairId');
    uni.navigateBack();
};
  onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶èŽ·å–å‚æ•°
    getPageParams();
  });
// èŽ·å–é¡µé¢å‚æ•°
const getPageParams = () => {
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–id
    const id = uni.getStorageSync('repairId');
    // æ ¹æ®æ˜¯å¦æœ‰id参数来判断是新增还是编辑
    if (id) {
        // ç¼–辑模式,获取详情
        loadForm(id);
    } else {
        // æ–°å¢žæ¨¡å¼
        loadForm();
    }
};
  onMounted(() => {
    // é¡µé¢åŠ è½½æ—¶èŽ·å–è®¾å¤‡åˆ—è¡¨å’Œå‚æ•°
    loadDeviceName();
    getPageParams();
  });
// èŽ·å–é¡µé¢ID
const getPageId = () => {
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–id
    return uni.getStorageSync('repairId');
};
  // ç»„件卸载时清理定时器
  onUnmounted(() => {
    if (scanTimer.value) {
      clearTimeout(scanTimer.value);
    }
  });
  // æäº¤è¡¨å•
  const sendForm = async () => {
    try {
      // æ‰‹åŠ¨éªŒè¯è¡¨å•
      const valid = await formRef.value.validate();
      if (!valid) return;
      loading.value = true;
      const id = getPageId();
      // å‡†å¤‡æäº¤æ•°æ®
      const submitData = { ...form.value };
      // ç¡®ä¿æ—¥æœŸæ ¼å¼æ­£ç¡®
      if (
        submitData.maintenancePlanTime &&
        !submitData.maintenancePlanTime.includes(":")
      ) {
        submitData.maintenancePlanTime =
          submitData.maintenancePlanTime + " 00:00:00";
      }
      const { code } = id
        ? await editUpkeep({ id: id, ...submitData })
        : await addUpkeep(submitData);
      if (code == 200) {
        showToast(`${id ? "编辑" : "新增"}计划成功`);
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      } else {
        loading.value = false;
      }
    } catch (e) {
      loading.value = false;
      showToast("表单验证失败");
    }
  };
  // è¿”回上一页
  const goBack = () => {
    // æ¸…除存储的id
    uni.removeStorageSync("repairId");
    uni.navigateBack();
  };
  // èŽ·å–é¡µé¢å‚æ•°
  const getPageParams = () => {
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–id
    const id = uni.getStorageSync("repairId");
    // æ ¹æ®æ˜¯å¦æœ‰id参数来判断是新增还是编辑
    if (id) {
      // ç¼–辑模式,获取详情
      loadForm(id);
    } else {
      // æ–°å¢žæ¨¡å¼
      loadForm();
    }
  };
  // èŽ·å–é¡µé¢ID
  const getPageId = () => {
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–id
    return uni.getStorageSync("repairId");
  };
</script>
<style scoped lang="scss">
@import '@/static/scss/form-common.scss';
.upkeep-add {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 5rem;
}
  @import "@/static/scss/form-common.scss";
  .upkeep-add {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 5rem;
  }
.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;
}
  .footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 0.75rem 0;
    box-shadow: 0 -0.125rem 0.5rem rgba(0, 0, 0, 0.05);
    z-index: 1000;
  }
.cancel-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 6.375rem;
    background: #C7C9CC;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
  .cancel-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #ffffff;
    width: 6.375rem;
    background: #c7c9cc;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
  }
.save-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 14rem;
    background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
  .save-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #ffffff;
    width: 14rem;
    background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
  }
// å“åº”式调整
@media (max-width: 768px) {
    .submit-section {
        padding: 12px;
    }
}
  // å“åº”式调整
  @media (max-width: 768px) {
    .submit-section {
      padding: 12px;
    }
  }
.tip-text {
    padding: 4px 16px 0 16px;
    font-size: 12px;
    color: #888;
}
  .tip-text {
    padding: 4px 16px 0 16px;
    font-size: 12px;
    color: #888;
  }
.scan-icon {
    color: #1989fa;
    font-size: 18px;
    margin-left: 8px;
    cursor: pointer;
}
  .scan-icon {
    color: #1989fa;
    font-size: 18px;
    margin-left: 8px;
    cursor: pointer;
  }
</style>
src/pages/equipmentManagement/upkeep/index.vue
@@ -63,6 +63,14 @@
              <text class="detail-value">{{ formatDateTime(item.createTime) || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">保养人</text>
              <text class="detail-value">{{ item.maintenancePerson || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">保养项目</text>
              <text class="detail-value">{{ item.machineryCategory || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">实际保养人</text>
              <text class="detail-value">{{ item.maintenanceActuallyName || '-' }}</text>
            </view>
src/pages/inspectionUpload/attachment.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,460 @@
<template>
  <view class="attachment-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader :title="`查看附件 - ${taskInfo?.taskName || ''}`" @back="goBack" />
    <!-- é¡µé¢å†…容 -->
    <view class="attachment-content">
      <!-- åˆ†ç±»æ ‡ç­¾é¡µ -->
      <view class="attachment-tabs">
        <view
          class="tab-item"
          :class="{ active: currentViewType === 'before' }"
          @click="switchViewType('before')"
        >
          ç”Ÿäº§å‰ ({{ getAttachmentsByType(0).length }})
        </view>
        <view
          class="tab-item"
          :class="{ active: currentViewType === 'after' }"
          @click="switchViewType('after')"
        >
          ç”Ÿäº§ä¸­ ({{ getAttachmentsByType(1).length }})
        </view>
        <view
          class="tab-item"
          :class="{ active: currentViewType === 'issue' }"
          @click="switchViewType('issue')"
        >
          ç”Ÿäº§åŽ ({{ getAttachmentsByType(2).length }})
        </view>
      </view>
      <!-- å½“前分类的附件列表 -->
      <view class="attachment-list-container">
        <view v-if="getCurrentViewAttachments().length > 0" class="attachment-list">
          <view
            v-for="(file, index) in getCurrentViewAttachments()"
            :key="index"
            class="attachment-item"
            @click="previewAttachment(file)"
          >
            <view class="attachment-preview-container">
              <image
                v-if="isImageFile(file)"
                :src="file.url || file.downloadUrl"
                class="attachment-preview"
                mode="aspectFill"
              />
              <view v-else class="attachment-video-preview">
                <u-icon name="video" size="40" color="#409eff"></u-icon>
                <text class="video-text">视频</text>
              </view>
            </view>
            <view class="attachment-info">
              <text class="attachment-name">{{ file.originalFilename || file.bucketFilename || file.name || '附件' }}</text>
              <text class="attachment-size">{{ formatFileSize(file.byteSize || file.size) }}</text>
            </view>
          </view>
        </view>
        <view v-else class="attachment-empty">
          <u-icon name="folder-open" size="60" color="#ccc"></u-icon>
          <text class="empty-text">该分类暂无附件</text>
        </view>
      </view>
    </view>
    <!-- è§†é¢‘预览弹窗 -->
    <view v-if="showVideoDialog" class="video-modal-overlay" @click="closeVideoPreview">
      <view class="video-modal-container" @click.stop>
        <view class="video-modal-header">
          <text class="video-modal-title">{{ currentVideoFile?.originalFilename || '视频预览' }}</text>
          <view class="close-btn-video" @click="closeVideoPreview">
            <u-icon name="close" size="20" color="#fff"></u-icon>
          </view>
        </view>
        <view class="video-modal-body">
          <video
            v-if="currentVideoFile"
            :src="currentVideoFile.url || currentVideoFile.downloadUrl"
            class="video-player"
            controls
            autoplay
            @error="handleVideoError"
          ></video>
        </view>
      </view>
    </view>
  </view>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
import config from '@/config';
// ä»»åŠ¡ä¿¡æ¯
const taskInfo = ref(null);
// é™„件列表
const attachmentList = ref([]);
// å½“前查看类型
const currentViewType = ref('before'); // 'before', 'after', 'issue'
// è§†é¢‘预览相关状态
const showVideoDialog = ref(false);
const currentVideoFile = ref(null);
// æ–‡ä»¶è®¿é—®åŸºç¡€åŸŸ
const filePreviewBase = config.fileUrl;
// é¡µé¢åŠ è½½
onLoad((options) => {
  if (options.taskInfo) {
    try {
      taskInfo.value = JSON.parse(decodeURIComponent(options.taskInfo));
      loadAttachments();
    } catch (e) {
      console.error('解析任务信息失败:', e);
      uni.showToast({
        title: '加载失败',
        icon: 'error'
      });
    }
  }
});
// åŠ è½½é™„ä»¶æ•°æ®
const loadAttachments = () => {
  const task = taskInfo.value;
  if (!task) return;
  attachmentList.value = [];
  // åŽç«¯åæ˜¾å­—段
  const allList = Array.isArray(task?.commonFileList) ? task.commonFileList : [];
  const beforeList = Array.isArray(task?.commonFileListBefore)
    ? task.commonFileListBefore
    : allList.filter((f) => f?.type === 10);
  const afterList = Array.isArray(task?.commonFileListAfter)
    ? task.commonFileListAfter
    : allList.filter((f) => f?.type === 11);
  const issueList = Array.isArray(task?.commonFileListIssue)
    ? task.commonFileListIssue
    : allList.filter((f) => f?.type === 12);
  const mapToViewFile = (file, viewType) => {
    const u = normalizeFileUrl(file?.url || file?.downloadUrl || '');
    return {
      ...file,
      type: viewType,
      name: file?.name || file?.originalFilename || file?.bucketFilename,
      bucketFilename: file?.bucketFilename || file?.name,
      originalFilename: file?.originalFilename || file?.name,
      url: u,
      downloadUrl: u,
      size: file?.size || file?.byteSize,
    };
  };
  attachmentList.value.push(...beforeList.map((f) => mapToViewFile(f, 0)));
  attachmentList.value.push(...afterList.map((f) => mapToViewFile(f, 1)));
  attachmentList.value.push(...issueList.map((f) => mapToViewFile(f, 2)));
};
// å°†åŽç«¯è¿”回的文件地址规范成可访问URL
const normalizeFileUrl = (rawUrl) => {
  try {
    if (!rawUrl || typeof rawUrl !== 'string') return '';
    const url = rawUrl.trim();
    if (!url) return '';
    if (/^https?:\/\//i.test(url)) return url;
    if (url.startsWith('/')) return `${filePreviewBase}${url}`;
    // Windows path -> web path
    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 `${filePreviewBase}/${relative}`;
      }
      return normalized;
    }
    return `${filePreviewBase}/${url.replace(/^\//, '')}`;
  } catch (e) {
    return rawUrl || '';
  }
};
// è¿”回上一页
const goBack = () => {
  uni.navigateBack();
};
// åˆ‡æ¢æŸ¥çœ‹ç±»åž‹
const switchViewType = (type) => {
  currentViewType.value = type;
};
// æ ¹æ®type获取对应分类的附件
const getAttachmentsByType = (typeValue) => {
  return attachmentList.value.filter((file) => file.type === typeValue) || [];
};
// èŽ·å–å½“å‰æŸ¥çœ‹ç±»åž‹çš„é™„ä»¶
const getCurrentViewAttachments = () => {
  switch (currentViewType.value) {
    case 'before':
      return getAttachmentsByType(0);
    case 'after':
      return getAttachmentsByType(1);
    case 'issue':
      return getAttachmentsByType(2);
    default:
      return [];
  }
};
// åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡æ–‡ä»¶
const isImageFile = (file) => {
  if (file.contentType && file.contentType.startsWith('image/')) {
    return true;
  }
  if (file.type === 'image') return true;
  const name = file.bucketFilename || file.originalFilename || file.name || '';
  const ext = name.split('.').pop()?.toLowerCase();
  return ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext);
};
// é¢„览附件
const previewAttachment = (file) => {
  if (isImageFile(file)) {
    const imageUrls = getCurrentViewAttachments()
      .filter((f) => isImageFile(f))
      .map((f) => f.url || f.downloadUrl);
    uni.previewImage({
      urls: imageUrls,
      current: file.url || file.downloadUrl,
    });
  } else {
    showVideoPreview(file);
  }
};
// æ˜¾ç¤ºè§†é¢‘预览
const showVideoPreview = (file) => {
  currentVideoFile.value = file;
  showVideoDialog.value = true;
};
// å…³é—­è§†é¢‘预览
const closeVideoPreview = () => {
  showVideoDialog.value = false;
  currentVideoFile.value = null;
};
// è§†é¢‘播放错误处理
const handleVideoError = () => {
  uni.showToast({
    title: '视频播放失败',
    icon: 'error',
  });
};
// æ ¼å¼åŒ–文件大小
const formatFileSize = (size) => {
  if (!size) return '';
  if (size < 1024) return size + 'B';
  if (size < 1024 * 1024) return (size / 1024).toFixed(1) + 'KB';
  return (size / (1024 * 1024)).toFixed(1) + 'MB';
};
</script>
<style scoped>
.attachment-page {
  min-height: 100vh;
  background-color: #f5f5f5;
}
.attachment-content {
  padding: 15px;
}
/* æ ‡ç­¾é¡µæ ·å¼ */
.attachment-tabs {
  display: flex;
  background: #fff;
  border-radius: 12px;
  margin-bottom: 15px;
  padding: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.tab-item {
  flex: 1;
  text-align: center;
  padding: 12px 8px;
  font-size: 14px;
  color: #666;
  border-radius: 8px;
  transition: all 0.3s ease;
}
.tab-item.active {
  background: #409eff;
  color: #fff;
  font-weight: 500;
}
/* é™„件列表样式 */
.attachment-list-container {
  background: #fff;
  border-radius: 12px;
  padding: 15px;
  min-height: 400px;
}
.attachment-list {
  display: flex;
  flex-wrap: wrap;
  gap: 15px;
}
.attachment-item {
  width: calc(33.33% - 10px);
  background: #f8f9fa;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  transition: all 0.3s ease;
}
.attachment-item:active {
  transform: scale(0.98);
}
.attachment-preview-container {
  width: 100%;
  height: 120px;
  background: #e9ecef;
  display: flex;
  align-items: center;
  justify-content: center;
}
.attachment-preview {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.attachment-video-preview {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.video-text {
  font-size: 12px;
  color: #666;
}
.attachment-info {
  padding: 10px;
}
.attachment-name {
  font-size: 12px;
  color: #333;
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-bottom: 4px;
}
.attachment-size {
  font-size: 10px;
  color: #999;
}
/* ç©ºçŠ¶æ€æ ·å¼ */
.attachment-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 80px 20px;
  color: #999;
}
.empty-text {
  margin-top: 15px;
  font-size: 14px;
}
/* è§†é¢‘弹窗样式 */
.video-modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.9);
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.video-modal-container {
  width: 100%;
  max-width: 800px;
  background: #000;
  border-radius: 12px;
  overflow: hidden;
}
.video-modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 20px;
  background: #1a1a1a;
}
.video-modal-title {
  font-size: 16px;
  color: #fff;
  font-weight: 500;
}
.close-btn-video {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 50%;
}
.video-modal-body {
  padding: 20px;
}
.video-player {
  width: 100%;
  height: 400px;
  border-radius: 8px;
}
</style>
src/pages/inspectionUpload/components/formDia.vue
@@ -8,53 +8,99 @@
  >
    <view class="popup-content">
      <view class="popup-header">
        <text class="popup-title">上传</text>
        <text class="popup-title">巡检记录上传</text>
      </view>
      
      <view class="upload-container">
        <!-- å¼‚常状态选择 -->
        <view class="form-container">
          <view class="title">生产前</view>
          <u-upload
            :fileList="beforeModelValue"
            @afterRead="afterRead"
            @delete="deleteFile"
            name="before"
            multiple
            :maxCount="10"
            :maxSize="5 * 1024 * 1024"
            accept="image/*"
            :previewFullImage="true"
          ></u-upload>
          <view class="title">巡检状态</view>
          <view class="exception-section">
            <view class="exception-options">
              <view
                class="exception-option"
                :class="{ active: hasException === false }"
                @click="setExceptionStatus(false)"
              >
                <u-icon name="checkmark-circle" size="20" color="#52c41a"></u-icon>
                <text class="option-text">正常</text>
              </view>
              <view
                class="exception-option"
                :class="{ active: hasException === true }"
                @click="setExceptionStatus(true)"
              >
                <u-icon name="close-circle" size="20" color="#ff4d4f"></u-icon>
                <text class="option-text">存在异常</text>
              </view>
            </view>
          </view>
        </view>
        <!-- å¼‚常描述(仅在异常时显示) -->
        <view class="form-container" v-if="hasException === true">
          <view class="title">异常描述</view>
          <u-input
            v-model="exceptionDescription"
            type="textarea"
            :maxlength="500"
            placeholder="请描述异常情况..."
            :customStyle="{ padding: '10px', backgroundColor: '#f5f5f5' }"
          />
        </view>
        
        <view class="form-container">
          <view class="title">生产后</view>
          <u-upload
            :fileList="afterModelValue"
            @afterRead="afterRead"
            @delete="deleteFile"
            name="after"
            multiple
            :maxCount="10"
            :maxSize="5 * 1024 * 1024"
            accept="image/*"
            :previewFullImage="true"
          ></u-upload>
        </view>
        <view class="form-container">
          <view class="title">生产问题</view>
          <u-upload
            :fileList="issueModelValue"
            @afterRead="afterRead"
            @delete="deleteFile"
            name="issue"
            multiple
            :maxCount="10"
            :maxSize="5 * 1024 * 1024"
            accept="image/*"
            :previewFullImage="true"
          ></u-upload>
        <!-- ä¸Šä¼ åŒºåŸŸï¼ˆä»…在异常时显示) -->
        <template v-if="hasException === true">
          <view class="form-container">
            <view class="title">生产前</view>
            <u-upload
              :fileList="beforeModelValue"
              @afterRead="afterRead"
              @delete="deleteFile"
              name="before"
              multiple
              :maxCount="10"
              :maxSize="5 * 1024 * 1024"
              accept="image/*"
              :previewFullImage="true"
            ></u-upload>
          </view>
          <view class="form-container">
            <view class="title">生产后</view>
            <u-upload
              :fileList="afterModelValue"
              @afterRead="afterRead"
              @delete="deleteFile"
              name="after"
              multiple
              :maxCount="10"
              :maxSize="5 * 1024 * 1024"
              accept="image/*"
              :previewFullImage="true"
            ></u-upload>
          </view>
          <view class="form-container">
            <view class="title">生产问题</view>
            <u-upload
              :fileList="issueModelValue"
              @afterRead="afterRead"
              @delete="deleteFile"
              name="issue"
              multiple
              :maxCount="10"
              :maxSize="5 * 1024 * 1024"
              accept="image/*"
              :previewFullImage="true"
            ></u-upload>
          </view>
        </template>
        <!-- æ­£å¸¸çŠ¶æ€æç¤º -->
        <view class="form-container normal-tip" v-if="hasException === false">
          <u-icon name="info-circle" size="40" color="#52c41a"></u-icon>
          <text class="tip-text">设备运行正常,无需上传照片</text>
        </view>
      </view>
      
@@ -79,6 +125,11 @@
const afterModelValue = ref([])
const issueModelValue = ref([])
const infoData = ref(null)
// å¼‚常状态:null=未选择, false=正常, true=异常
const hasException = ref(null)
// å¼‚常描述
const exceptionDescription = ref('')
// è®¡ç®—上传URL
const uploadFileUrl = computed(() => {
@@ -196,9 +247,43 @@
  }
}
// è®¾ç½®å¼‚常状态
const setExceptionStatus = (status) => {
  hasException.value = status
}
// æäº¤è¡¨å•
const submitForm = async () => {
  try {
    // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†å·¡æ£€çŠ¶æ€
    if (hasException.value === null) {
      uni.showToast({
        title: '请选择巡检状态',
        icon: 'none'
      })
      return
    }
    // å¦‚果是异常状态,检查是否有上传文件
    if (hasException.value === true) {
      const totalFiles = beforeModelValue.value.length + afterModelValue.value.length + issueModelValue.value.length
      if (totalFiles === 0) {
        uni.showToast({
          title: '请上传异常照片',
          icon: 'none'
        })
        return
      }
      // æ£€æŸ¥æ˜¯å¦å¡«å†™äº†å¼‚常描述
      if (!exceptionDescription.value.trim()) {
        uni.showToast({
          title: '请填写异常描述',
          icon: 'none'
        })
        return
      }
    }
    let arr = []
    if (beforeModelValue.value.length > 0) {
      arr.push(...beforeModelValue.value.map(item => ({ ...item, statusType: 0 })))
@@ -212,6 +297,8 @@
    
    // æäº¤æ•°æ®
    infoData.value.storageBlobDTO = arr
    infoData.value.hasException = hasException.value
    infoData.value.exceptionDescription = exceptionDescription.value
    await submitInspectionRecord({ ...infoData.value })
    
    uni.showToast({
@@ -238,6 +325,8 @@
  beforeModelValue.value = []
  afterModelValue.value = []
  issueModelValue.value = []
  hasException.value = null
  exceptionDescription.value = ''
}
// å…³é—­å¼¹æ¡†
@@ -311,4 +400,61 @@
  border-top: 1px solid #f0f0f0;
  background-color: #fafafa;
}
// å¼‚常状态选择样式
.exception-section {
  padding: 10px 0;
}
.exception-options {
  display: flex;
  gap: 15px;
}
.exception-option {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 15px 20px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s;
  background-color: #fff;
  &.active {
    border-color: #1890ff;
    background-color: #e6f7ff;
  }
  &:active {
    opacity: 0.8;
  }
}
.option-text {
  font-size: 14px;
  color: #333;
  font-weight: 500;
}
// æ­£å¸¸çŠ¶æ€æç¤ºæ ·å¼
.normal-tip {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  background-color: #f6ffed;
  border: 1px dashed #b7eb8f;
  border-radius: 8px;
  .tip-text {
    margin-top: 15px;
    font-size: 14px;
    color: #52c41a;
  }
}
</style>
src/pages/inspectionUpload/index.vue
@@ -56,6 +56,10 @@
              <text class="detail-value">{{ item.taskId || item.id }}</text>
            </view>
            <view class="detail-item">
              <text class="detail-label">巡检项目</text>
              <text class="detail-value">{{ item.inspectionProject || '无' }}</text>
            </view>
            <view class="detail-item">
              <text class="detail-label">备注</text>
              <text class="detail-value">{{ item.remarks || '无' }}</text>
            </view>
@@ -95,227 +99,6 @@
      <view v-if="taskTableData?.length === 0"
            class="no-data">
        <text>暂无数据</text>
      </view>
    </view>
    <!-- å›¾ç‰‡ä¸Šä¼ å¼¹çª— - åŽŸç”Ÿå®žçŽ° -->
    <view v-if="showUploadDialog"
          class="custom-modal-overlay"
          @click="closeUploadDialog">
      <view class="custom-modal-container"
            @click.stop>
        <view class="upload-popup-content">
          <view class="upload-popup-header">
            <text class="upload-popup-title">上传巡检记录</text>
          </view>
          <view class="upload-popup-body">
            <!-- åˆ†ç±»æ ‡ç­¾é¡µ -->
            <view class="upload-tabs">
              <view class="tab-item"
                    :class="{ active: currentUploadType === 'before' }"
                    @click="switchUploadType('before')">
                ç”Ÿäº§å‰
              </view>
              <view class="tab-item"
                    :class="{ active: currentUploadType === 'after' }"
                    @click="switchUploadType('after')">
                ç”Ÿäº§ä¸­
              </view>
              <view class="tab-item"
                    :class="{ active: currentUploadType === 'issue' }"
                    @click="switchUploadType('issue')">
                ç”Ÿäº§åŽ
              </view>
            </view>
            <!-- å¼‚常状态选择 -->
            <view class="exception-section">
              <text class="section-title">是否存在异常?</text>
              <view class="exception-options">
                <view class="exception-option"
                      :class="{ active: hasException === false }"
                      @click="setExceptionStatus(false)">
                  <u-icon name="checkmark-circle"
                          size="20"
                          color="#52c41a"></u-icon>
                  <text>正常</text>
                </view>
                <view class="exception-option"
                      :class="{ active: hasException === true }"
                      @click="setExceptionStatus(true)">
                  <u-icon name="close-circle"
                          size="20"
                          color="#ff4d4f"></u-icon>
                  <text>存在异常</text>
                </view>
              </view>
            </view>
            <!-- å½“前分类的上传区域 -->
            <view class="simple-upload-area">
              <view class="upload-buttons">
                <u-button type="primary"
                          @click="chooseMedia('image')"
                          :loading="uploading"
                          :disabled="getCurrentFiles().length >= uploadConfig.limit"
                          :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="chooseMedia('video')"
                          :loading="uploading"
                          :disabled="getCurrentFiles().length >= uploadConfig.limit"
                          :customStyle="{ flex: 1 }">
                  <uni-icons type="videocam"
                             name="videocam"
                             size="18"
                             color="#fff"
                             style="margin-right: 5px;"></uni-icons>
                  {{ uploading ? '上传中...' : '拍视频' }}
                </u-button>
              </view>
              <!-- ä¸Šä¼ è¿›åº¦ -->
              <view v-if="uploading"
                    class="upload-progress">
                <u-line-progress :percentage="uploadProgress"
                                 :showText="true"
                                 activeColor="#409eff"></u-line-progress>
              </view>
              <!-- å½“前分类的文件列表 -->
              <view v-if="getCurrentFiles().length > 0"
                    class="file-list">
                <view v-for="(file, index) in getCurrentFiles()"
                      :key="index"
                      class="file-item">
                  <view class="file-preview-container">
                    <image v-if="file.type === 'image' || (file.type !== 'video' && !file.type)"
                           :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                           class="file-preview"
                           mode="aspectFill" />
                    <view v-else-if="file.type === 'video'"
                          class="video-preview">
                      <uni-icons type="videocam"
                                 name="videocam"
                                 size="18"
                                 color="#fff"
                                 style="margin-right: 5px;"></uni-icons>
                      <text class="video-text">视频</text>
                    </view>
                    <!-- åˆ é™¤æŒ‰é’® -->
                    <view class="delete-btn"
                          @click="removeFile(index)">
                      <u-icon name="close"
                              size="12"
                              color="#fff"></u-icon>
                    </view>
                  </view>
                  <view class="file-info">
                    <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? '图片' : '视频')
                      }}</text>
                    <text class="file-size">{{ formatFileSize(file.size) }}</text>
                  </view>
                </view>
              </view>
              <view v-if="getCurrentFiles().length === 0"
                    class="empty-state">
                <text>请选择要上传的{{ getUploadTypeText() }}图片或视频</text>
              </view>
              <!-- ç»Ÿè®¡ä¿¡æ¯ -->
              <view class="upload-summary">
                <text class="summary-text">
                  ç”Ÿäº§å‰: {{ beforeModelValue.length }}个文件 |
                  ç”Ÿäº§ä¸­: {{ afterModelValue.length }}个文件 |
                  ç”Ÿäº§åŽ: {{ issueModelValue.length }}个文件
                </text>
              </view>
            </view>
          </view>
          <view class="upload-popup-footer">
            <u-button @click="closeUploadDialog"
                      :customStyle="{ marginRight: '10px' }">取消</u-button>
            <u-button v-if="hasException === true"
                      type="warning"
                      @click="goToRepair"
                      :customStyle="{ marginRight: '10px' }">
              æ–°å¢žæŠ¥ä¿®
            </u-button>
            <u-button type="primary"
                      @click="submitUpload">提交</u-button>
          </view>
        </view>
      </view>
    </view>
    <!-- æŸ¥çœ‹é™„件弹窗 -->
    <view v-if="showAttachmentDialog"
          class="custom-modal-overlay"
          @click="closeAttachmentDialog">
      <view class="custom-modal-container"
            @click.stop>
        <view class="attachment-popup-content">
          <view class="attachment-popup-header">
            <text class="attachment-popup-title">查看附件 - {{ currentViewTask?.taskName }}</text>
            <view class="close-btn-attachment"
                  @click="closeAttachmentDialog">
              <u-icon name="close"
                      size="16"
                      color="#666"></u-icon>
            </view>
          </view>
          <view class="attachment-popup-body">
            <!-- åˆ†ç±»æ ‡ç­¾é¡µ -->
            <view class="attachment-tabs">
              <view class="tab-item"
                    :class="{ active: currentViewType === 'before' }"
                    @click="switchViewType('before')">
                ç”Ÿäº§å‰ ({{ getAttachmentsByType(0).length }})
              </view>
              <view class="tab-item"
                    :class="{ active: currentViewType === 'after' }"
                    @click="switchViewType('after')">
                ç”Ÿäº§ä¸­ ({{ getAttachmentsByType(1).length }})
              </view>
              <view class="tab-item"
                    :class="{ active: currentViewType === 'issue' }"
                    @click="switchViewType('issue')">
                ç”Ÿäº§åŽ ({{ getAttachmentsByType(2).length }})
              </view>
            </view>
            <!-- å½“前分类的附件列表 -->
            <view class="attachment-content">
              <view v-if="getCurrentViewAttachments().length > 0"
                    class="attachment-list">
                <view v-for="(file, index) in getCurrentViewAttachments()"
                      :key="index"
                      class="attachment-item"
                      @click="previewAttachment(file)">
                  <view class="attachment-preview-container">
                    <image v-if="file.type === 'image' || isImageFile(file)"
                           :src="file.url || file.downloadUrl"
                           class="attachment-preview"
                           mode="aspectFill" />
                    <view v-else
                          class="attachment-video-preview">
                      <u-icon name="video"
                              size="24"
                              color="#409eff"></u-icon>
                      <text class="video-text">视频</text>
                    </view>
                  </view>
                  <view class="attachment-info">
                    <text class="attachment-name">{{ file.originalFilename || file.bucketFilename || file.name || '附件'
                      }}</text>
                    <text class="attachment-size">{{ formatFileSize(file.byteSize || file.size) }}</text>
                  </view>
                </view>
              </view>
              <view v-else
                    class="attachment-empty">
                <text>该分类暂无附件</text>
              </view>
            </view>
          </view>
        </view>
      </view>
    </view>
    <!-- è§†é¢‘预览弹窗 -->
@@ -378,57 +161,9 @@
  const currentScanningTask = ref(null);
  const infoData = ref(null);
  // ä¸Šä¼ ç›¸å…³çŠ¶æ€
  const showUploadDialog = ref(false);
  const uploadFiles = ref([]); // ä¿ç•™ç”¨äºŽå…¼å®¹æ€§
  const uploadStatusType = ref(0);
  const uploading = ref(false);
  const uploadProgress = ref(0);
  const number = ref(0);
  const uploadList = ref([]);
  // ä¸‰ä¸ªåˆ†ç±»çš„上传状态
  const beforeModelValue = ref([]); // ç”Ÿäº§å‰
  const afterModelValue = ref([]); // ç”Ÿäº§ä¸­
  const issueModelValue = ref([]); // ç”Ÿäº§åŽ
  // å½“前激活的上传类型
  const currentUploadType = ref("before"); // 'before', 'after', 'issue'
  // æŸ¥çœ‹é™„件相关状态
  const showAttachmentDialog = ref(false);
  const currentViewTask = ref(null);
  const currentViewType = ref("before"); // 'before', 'after', 'issue'
  const attachmentList = ref([]); // å½“前查看任务的附件列表
  // è§†é¢‘预览相关状态
  const showVideoDialog = ref(false);
  const currentVideoFile = ref(null);
  // å¼‚常状态
  const hasException = ref(null); // null: æœªé€‰æ‹©, true: å­˜åœ¨å¼‚常, false: æ­£å¸¸
  // ä¸Šä¼ é…ç½®
  const uploadConfig = {
    action: "/file/upload",
    limit: 10,
    fileSize: 50, // MB
    fileType: ["jpg", "jpeg", "png", "mp4", "mov"],
    maxVideoDuration: 60, // ç§’
  };
  // è®¡ç®—上传URL
  const uploadFileUrl = computed(() => {
    const baseUrl = config.baseUrl;
    return baseUrl + uploadConfig.action;
  });
  // è®¡ç®—请求头
  const headers = computed(() => {
    const token = getToken();
    return token ? { Authorization: "Bearer " + token } : {};
  });
  // è¯·æ±‚取消标志,用于取消正在进行的请求
  let isRequestCancelled = false;
@@ -489,11 +224,6 @@
  onUnmounted(() => {
    // è®¾ç½®å–消标志,阻止后续的异步操作
    isRequestCancelled = true;
    // å…³é—­ä¸Šä¼ å¼¹çª—
    if (showUploadDialog.value) {
      showUploadDialog.value = false;
    }
  });
  // è¿”回上一页
@@ -653,322 +383,27 @@
    }
  };
  // æ‰“开上传弹窗
  // æ‰“开上传页面
  const openUploadDialog = task => {
    // è®¾ç½®ä»»åŠ¡ä¿¡æ¯åˆ°infoData
    if (task) {
      infoData.value = {
        ...task,
        taskId: task.taskId || task.id,
        storageBlobDTO: [], // åˆå§‹åŒ–文件列表
      };
    }
    // è®¾ç½®ä¸Šä¼ çŠ¶æ€ç±»åž‹ï¼ˆå¯ä»¥æ ¹æ®ä»»åŠ¡ç±»åž‹è®¾ç½®ä¸åŒçš„çŠ¶æ€ï¼‰
    uploadStatusType.value = 0; // é»˜è®¤çŠ¶æ€
    // æ¸…空之前的文件
    uploadFiles.value = [];
    // æ˜¾ç¤ºä¸Šä¼ å¼¹çª—
    showUploadDialog.value = true;
  };
  // å…³é—­ä¸Šä¼ å¼¹çª—
  const closeUploadDialog = () => {
    showUploadDialog.value = false;
    uploadFiles.value = [];
    // æ¸…理三个分类的数据
    beforeModelValue.value = [];
    afterModelValue.value = [];
    issueModelValue.value = [];
    currentUploadType.value = "before";
    hasException.value = null; // é‡ç½®å¼‚常状态
    infoData.value = null; // æ¸…理任务数据
  };
  // åˆ‡æ¢ä¸Šä¼ ç±»åž‹
  const switchUploadType = type => {
    currentUploadType.value = type;
  };
  // èŽ·å–å½“å‰åˆ†ç±»çš„æ–‡ä»¶åˆ—è¡¨
  const getCurrentFiles = () => {
    switch (currentUploadType.value) {
      case "before":
        return beforeModelValue.value || [];
      case "after":
        return afterModelValue.value || [];
      case "issue":
        return issueModelValue.value || [];
      default:
        return [];
    }
  };
  // èŽ·å–ä¸Šä¼ ç±»åž‹æ–‡æœ¬
  const getUploadTypeText = () => {
    switch (currentUploadType.value) {
      case "before":
        return "生产前";
      case "after":
        return "生产中";
      case "issue":
        return "生产后";
      default:
        return "";
    }
  };
  // å¤„理上传文件更新
  const handleUploadUpdate = files => {
    uploadFiles.value = files;
  };
  // è®¾ç½®å¼‚常状态
  const setExceptionStatus = status => {
    hasException.value = status;
  };
  // è·³è½¬åˆ°æ–°å¢žæŠ¥ä¿®é¡µé¢
  const goToRepair = () => {
    try {
      // å­˜å‚¨å½“前任务信息到本地存储,供报修页面使用
      const taskInfo = {
        taskId: infoData.value?.taskId || infoData.value?.id,
        taskName: infoData.value?.taskName,
        inspectionLocation: infoData.value?.inspectionLocation,
        inspector: infoData.value?.inspector,
        // ä¼ é€’当前上传的文件信息
        uploadedFiles: {
          before: beforeModelValue.value,
          after: afterModelValue.value,
          issue: issueModelValue.value,
        },
      };
      uni.setStorageSync("repairTaskInfo", JSON.stringify(taskInfo));
      // è·³è½¬åˆ°æ–°å¢žæŠ¥ä¿®é¡µé¢
      uni.navigateTo({
        url: "/pages/equipmentManagement/repair/add",
      });
      // å…³é—­ä¸Šä¼ å¼¹çª—
      closeUploadDialog();
    } catch (error) {
      console.error("跳转报修页面失败:", error);
      uni.showToast({
        title: "跳转失败,请重试",
        icon: "error",
      });
    }
  };
  // æäº¤ä¸Šä¼ 
  const submitUpload = async () => {
    try {
      // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†å¼‚常状态
      if (hasException.value === null) {
        uni.showToast({
          title: "请选择是否存在异常",
          icon: "none",
        });
        return;
      }
      // æ£€æŸ¥æ˜¯å¦æœ‰ä»»ä½•文件上传
      const totalFiles =
        beforeModelValue.value.length +
        afterModelValue.value.length +
        issueModelValue.value.length;
      if (totalFiles === 0) {
        uni.showToast({
          title: "请先上传文件",
          icon: "none",
        });
        return;
      }
      // æ˜¾ç¤ºæäº¤ä¸­çš„加载提示
      showLoadingToast("提交中...");
      // æŒ‰ç…§æ‚¨çš„逻辑合并所有分类的文件
      let arr = [];
      if (beforeModelValue.value.length > 0) {
        arr.push(...beforeModelValue.value);
      }
      if (afterModelValue.value.length > 0) {
        arr.push(...afterModelValue.value);
      }
      if (issueModelValue.value.length > 0) {
        arr.push(...issueModelValue.value);
      }
      // ä¼ ç»™åŽç«¯çš„临时文件ID列表(tempFileIds)
      // å…¼å®¹ï¼šæœ‰äº›æŽ¥å£å¯èƒ½è¿”回 tempId / tempFileId / id
      let tempFileIds = [];
      if (arr !== null && arr.length > 0) {
        tempFileIds = arr
          .map(item => item?.tempId ?? item?.tempFileId ?? item?.id)
          .filter(v => v !== undefined && v !== null && v !== "");
      }
      // æäº¤æ•°æ®
      infoData.value.storageBlobDTO = arr;
      // æ·»åŠ å¼‚å¸¸çŠ¶æ€ä¿¡æ¯
      infoData.value.hasException = hasException.value;
      infoData.value.tempFileIds = tempFileIds;
      const result = await uploadInspectionTask({ ...infoData.value });
      // æ£€æŸ¥æäº¤ç»“æžœ
      if (result && (result.code === 200 || result.success)) {
        // æäº¤æˆåŠŸ
        closeToast(); // å…³é—­åŠ è½½æç¤º
        uni.showToast({
          title: "提交成功",
          icon: "success",
        });
        // å…³é—­å¼¹çª—
        closeUploadDialog();
        // åˆ·æ–°åˆ—表
        setTimeout(() => {
          reloadPage();
        }, 500);
      } else {
        // æäº¤å¤±è´¥
        closeToast();
        uni.showToast({
          title: result?.msg || result?.message || "提交失败",
          icon: "error",
        });
      }
    } catch (error) {
      console.error("提交上传失败:", error);
      closeToast(); // å…³é—­åŠ è½½æç¤º
      let errorMessage = "提交失败";
      if (error.message) {
        errorMessage = error.message;
      } else if (error.msg) {
        errorMessage = error.msg;
      } else if (typeof error === "string") {
        errorMessage = error;
      }
      uni.showToast({
        title: errorMessage,
        icon: "error",
      });
    }
    // å°†ä»»åŠ¡ä¿¡æ¯ä¼ é€’åˆ°ä¸Šä¼ é¡µé¢
    const taskData = encodeURIComponent(JSON.stringify(task));
    uni.navigateTo({
      url: `/pages/inspectionUpload/upload?taskInfo=${taskData}`,
    });
  };
  // å›¾ç‰‡ä¸Šä¼ (可选择图片上传或者是相机拍照)
  const startUploadForTask = async (task, type) => {
    // ç›´æŽ¥æ‰“开上传弹窗
    // æ‰“开上传页面
    openUploadDialog(task);
  };
  // æŸ¥çœ‹é™„ä»¶
  // æŸ¥çœ‹é™„ä»¶ - è·³è½¬åˆ°é™„件页面
  const viewAttachments = async task => {
    try {
      currentViewTask.value = task;
      currentViewType.value = "before";
      // è§£æžæ–°çš„æ•°æ®ç»“æž„
      attachmentList.value = [];
      // åŽç«¯åæ˜¾å­—段(你提供的数据结构):
      // - commonFileListBefore:生产前(通常 type=10)
      // - commonFileListAfter:生产中(通常 type=11)
      // - commonFileList:可能是全部/兜底(若包含生产后,一般 type=12)
      const allList = Array.isArray(task?.commonFileList)
        ? task.commonFileList
        : [];
      const beforeList = Array.isArray(task?.commonFileListBefore)
        ? task.commonFileListBefore
        : allList.filter(f => f?.type === 10);
      const afterList = Array.isArray(task?.commonFileListAfter)
        ? task.commonFileListAfter
        : allList.filter(f => f?.type === 11);
      // å¦‚果后端后续补了 commonFileListIssue,则优先用;否则从 commonFileList é‡ŒæŒ‰ type=12 å…œåº•
      const issueList = Array.isArray(task?.commonFileListIssue)
        ? task.commonFileListIssue
        : allList.filter(f => f?.type === 12);
      const mapToViewFile = (file, viewType) => {
        const u = normalizeFileUrl(file?.url || file?.downloadUrl || "");
        return {
          ...file,
          // ç”¨äºŽä¸‰æ ‡ç­¾é¡µåˆ†ç»„:0=生产前 1=生产中 2=生产后
          type: viewType,
          name: file?.name || file?.originalFilename || file?.bucketFilename,
          bucketFilename: file?.bucketFilename || file?.name,
          originalFilename: file?.originalFilename || file?.name,
          url: u,
          downloadUrl: u,
          size: file?.size || file?.byteSize,
        };
      };
      attachmentList.value.push(...beforeList.map(f => mapToViewFile(f, 0)));
      attachmentList.value.push(...afterList.map(f => mapToViewFile(f, 1)));
      attachmentList.value.push(...issueList.map(f => mapToViewFile(f, 2)));
      showAttachmentDialog.value = true;
    } catch (error) {
      uni.showToast({
        title: "获取附件失败",
        icon: "error",
      });
    }
  };
  // å…³é—­é™„件查看弹窗
  const closeAttachmentDialog = () => {
    showAttachmentDialog.value = false;
    currentViewTask.value = null;
    attachmentList.value = [];
    currentViewType.value = "before";
  };
  // åˆ‡æ¢æŸ¥çœ‹ç±»åž‹
  const switchViewType = type => {
    currentViewType.value = type;
  };
  // æ ¹æ®type获取对应分类的附件
  const getAttachmentsByType = typeValue => {
    return attachmentList.value.filter(file => file.type === typeValue) || [];
  };
  // èŽ·å–type值
  const getTabType = () => {
    switch (currentUploadType.value) {
      case "before":
        return 10;
      case "after":
        return 11;
      case "issue":
        return 12;
      default:
        return 10;
    }
  };
  // èŽ·å–å½“å‰æŸ¥çœ‹ç±»åž‹çš„é™„ä»¶
  const getCurrentViewAttachments = () => {
    switch (currentViewType.value) {
      case "before":
        return getAttachmentsByType(0);
      case "after":
        return getAttachmentsByType(1);
      case "issue":
        return getAttachmentsByType(2);
      default:
        return [];
    }
    const taskData = encodeURIComponent(JSON.stringify(task));
    uni.navigateTo({
      url: `/pages/inspectionUpload/attachment?taskInfo=${taskData}`,
    });
  };
  // åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡æ–‡ä»¶
@@ -1060,474 +495,7 @@
    });
  };
  // æ‹ç…§/拍视频(真机优先用 chooseMedia;不支持则降级)
  const chooseMedia = type => {
    if (getCurrentFiles().length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能选择${uploadConfig.limit}个文件`,
        icon: "none",
      });
      return;
    }
    const remaining = uploadConfig.limit - getCurrentFiles().length;
    // ä¼˜å…ˆï¼šchooseMedia(支持 image/video)
    if (typeof uni.chooseMedia === "function") {
      uni.chooseMedia({
        count: Math.min(remaining, 1),
        mediaType: [type || "image"],
        sizeType: ["compressed", "original"],
        sourceType: ["camera"],
        success: res => {
          try {
            const files = res?.tempFiles || [];
            if (!files.length) throw new Error("未获取到文件");
            files.forEach((tf, idx) => {
              const filePath = tf.tempFilePath || tf.path || "";
              const fileType = tf.fileType || type || "image";
              const ext = fileType === "video" ? "mp4" : "jpg";
              const file = {
                tempFilePath: filePath,
                path: filePath,
                type: fileType,
                name: `${fileType}_${Date.now()}_${idx}.${ext}`,
                size: tf.size || 0,
                duration: tf.duration || 0,
                createTime: Date.now(),
                uid: Date.now() + Math.random() + idx,
              };
              handleBeforeUpload(file);
            });
          } catch (e) {
            console.error("处理拍摄结果失败:", e);
            uni.showToast({ title: "处理文件失败", icon: "error" });
          }
        },
        fail: err => {
          console.error("拍摄失败:", err);
          uni.showToast({ title: "拍摄失败", icon: "error" });
        },
      });
      return;
    }
    // é™çº§ï¼šchooseImage / chooseVideo
    if (type === "video") {
      chooseVideo();
    } else {
      uni.chooseImage({
        count: 1,
        sizeType: ["compressed", "original"],
        sourceType: ["camera"],
        success: res => {
          const tempFilePath = res?.tempFilePaths?.[0];
          const tempFile = res?.tempFiles?.[0] || {};
          if (!tempFilePath) return;
          handleBeforeUpload({
            tempFilePath,
            path: tempFilePath,
            type: "image",
            name: `photo_${Date.now()}.jpg`,
            size: tempFile.size || 0,
            createTime: Date.now(),
            uid: Date.now() + Math.random(),
          });
        },
      });
    }
  };
  // æ‹ç…§
  const chooseImage = () => {
    if (uploadFiles.value.length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能拍摄${uploadConfig.limit}个文件`,
        icon: "none",
      });
      return;
    }
    uni.chooseMedia({
      count: 1,
      mediaType: ["image", "video"],
      sizeType: ["compressed", "original"],
      sourceType: ["camera"],
      success: res => {
        try {
          if (!res.tempFiles || res.tempFiles.length === 0) {
            throw new Error("未获取到图片文件");
          }
          const tempFilePath = res.tempFiles[0];
          const tempFile =
            res.tempFiles && res.tempFiles[0] ? res.tempFiles[0] : {};
          const file = {
            tempFilePath: tempFilePath,
            path: tempFilePath, // ä¿æŒå…¼å®¹æ€§
            type: "image",
            name: `photo_${Date.now()}.jpg`,
            size: tempFile.size || 0,
            createTime: new Date().getTime(),
            uid: Date.now() + Math.random(),
          };
          handleBeforeUpload(file);
        } catch (error) {
          console.error("处理拍照结果失败:", error);
          uni.showToast({
            title: "处理图片失败",
            icon: "error",
          });
        }
      },
      fail: err => {
        console.error("拍照失败:", err);
        uni.showToast({
          title: "拍照失败: " + (err.errMsg || "未知错误"),
          icon: "error",
        });
      },
    });
  };
  // æ‹è§†é¢‘
  const chooseVideo = () => {
    if (uploadFiles.value.length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能拍摄${uploadConfig.limit}个文件`,
        icon: "none",
      });
      return;
    }
    uni.chooseVideo({
      sourceType: ["camera"],
      maxDuration: uploadConfig.maxVideoDuration,
      camera: "back",
      success: res => {
        try {
          if (!res.tempFilePath) {
            throw new Error("未获取到视频文件");
          }
          const file = {
            tempFilePath: res.tempFilePath,
            path: res.tempFilePath, // ä¿æŒå…¼å®¹æ€§
            type: "video",
            name: `video_${Date.now()}.mp4`,
            size: res.size || 0,
            duration: res.duration || 0,
            createTime: new Date().getTime(),
            uid: Date.now() + Math.random(),
          };
          handleBeforeUpload(file);
        } catch (error) {
          console.error("处理拍视频结果失败:", error);
          uni.showToast({
            title: "处理视频失败",
            icon: "error",
          });
        }
      },
      fail: err => {
        console.error("拍视频失败:", err);
        uni.showToast({
          title: "拍视频失败: " + (err.errMsg || "未知错误"),
          icon: "error",
        });
      },
    });
  };
  // åˆ é™¤æ–‡ä»¶
  const removeFile = index => {
    uni.showModal({
      title: "确认删除",
      content: "确定要删除这个文件吗?",
      success: res => {
        if (res.confirm) {
          // æ ¹æ®å½“前上传类型删除对应分类的文件
          switch (currentUploadType.value) {
            case "before":
              beforeModelValue.value.splice(index, 1);
              break;
            case "after":
              afterModelValue.value.splice(index, 1);
              break;
            case "issue":
              issueModelValue.value.splice(index, 1);
              break;
          }
          uni.showToast({
            title: "删除成功",
            icon: "success",
          });
        }
      },
    });
  };
  // æ£€æŸ¥ç½‘络连接
  const checkNetworkConnection = () => {
    return new Promise(resolve => {
      uni.getNetworkType({
        success: res => {
          if (res.networkType === "none") {
            resolve(false);
          } else {
            resolve(true);
          }
        },
        fail: () => {
          resolve(false);
        },
      });
    });
  };
  // ä¸Šä¼ å‰æ ¡éªŒ
  const handleBeforeUpload = async file => {
    // æ ¡éªŒæ–‡ä»¶ç±»åž‹
    if (
      uploadConfig.fileType &&
      Array.isArray(uploadConfig.fileType) &&
      uploadConfig.fileType.length > 0
    ) {
      const fileName = file.name || "";
      const fileExtension = fileName
        ? fileName.split(".").pop().toLowerCase()
        : "";
      // æ ¹æ®æ–‡ä»¶ç±»åž‹ç¡®å®šæœŸæœ›çš„æ‰©å±•名
      let expectedTypes = [];
      if (file.type === "image") {
        expectedTypes = ["jpg", "jpeg", "png", "gif", "webp"];
      } else if (file.type === "video") {
        expectedTypes = ["mp4", "mov", "avi", "wmv"];
      }
      // æ£€æŸ¥æ–‡ä»¶æ‰©å±•名是否在允许的类型中
      if (fileExtension && expectedTypes.length > 0) {
        const isAllowed = expectedTypes.some(
          type => uploadConfig.fileType.includes(type) && type === fileExtension
        );
        if (!isAllowed) {
          uni.showToast({
            title: `文件格式不支持,请拍摄 ${expectedTypes.join("/")} æ ¼å¼çš„æ–‡ä»¶`,
            icon: "none",
          });
          return false;
        }
      }
    }
    // æ ¡éªŒé€šè¿‡ï¼Œå¼€å§‹ä¸Šä¼ 
    uploadFile(file);
    return true;
  };
  // æ–‡ä»¶ä¸Šä¼ å¤„理(真机走 uni.uploadFile)
  const uploadFile = async file => {
    uploading.value = true;
    uploadProgress.value = 0;
    number.value++; // å¢žåŠ ä¸Šä¼ è®¡æ•°
    // ç¡®ä¿token存在
    const token = getToken();
    if (!token) {
      handleUploadError("用户未登录");
      return;
    }
    const typeValue = getTabType(); // ç”Ÿäº§å‰:10, ç”Ÿäº§ä¸­:11, ç”Ÿäº§åŽ:12
    uploadWithUniUploadFile(
      file,
      file.tempFilePath || file.path || "",
      typeValue,
      token
    );
  };
  // ä½¿ç”¨uni.uploadFile上传(非H5环境或H5回退方案)
  const uploadWithUniUploadFile = (file, filePath, typeValue, token) => {
    if (!filePath) {
      handleUploadError("文件路径不存在");
      return;
    }
    const uploadTask = uni.uploadFile({
      url: uploadFileUrl.value,
      filePath: filePath,
      name: "file",
      formData: {
        type: typeValue,
      },
      header: {
        Authorization: `Bearer ${token}`,
      },
      success: res => {
        try {
          if (res.statusCode === 200) {
            const response = JSON.parse(res.data);
            if (response.code === 200) {
              handleUploadSuccess(response, file);
              uni.showToast({
                title: "上传成功",
                icon: "success",
              });
            } else {
              handleUploadError(response.msg || "服务器返回错误");
            }
          } else {
            handleUploadError(`服务器错误,状态码: ${res.statusCode}`);
          }
        } catch (e) {
          console.error("解析响应失败:", e);
          console.error("原始响应数据:", res.data);
          handleUploadError("响应数据解析失败: " + e.message);
        }
      },
      fail: err => {
        console.error("上传失败:", err.errMsg || err);
        number.value--; // ä¸Šä¼ å¤±è´¥æ—¶å‡å°‘计数
        let errorMessage = "上传失败";
        if (err.errMsg) {
          if (err.errMsg.includes("statusCode: null")) {
            errorMessage = "网络连接失败,请检查网络设置";
          } else if (err.errMsg.includes("timeout")) {
            errorMessage = "上传超时,请重试";
          } else if (err.errMsg.includes("fail")) {
            errorMessage = "上传失败,请检查网络连接";
          } else {
            errorMessage = err.errMsg;
          }
        }
        handleUploadError(errorMessage);
      },
      complete: () => {
        uploading.value = false;
        uploadProgress.value = 0;
      },
    });
    // ç›‘听上传进度
    if (uploadTask && uploadTask.onProgressUpdate) {
      uploadTask.onProgressUpdate(res => {
        uploadProgress.value = res.progress;
      });
    }
  };
  // ä¸Šä¼ å¤±è´¥å¤„理
  const handleUploadError = (message = "上传文件失败", showRetry = false) => {
    uploading.value = false;
    uploadProgress.value = 0;
    if (showRetry) {
      uni.showModal({
        title: "上传失败",
        content: message + ",是否重试?",
        success: res => {
          if (res.confirm) {
            // ç”¨æˆ·é€‰æ‹©é‡è¯•,这里可以重新触发上传
          }
        },
      });
    } else {
      uni.showToast({
        title: message,
        icon: "error",
      });
    }
  };
  // ä¸Šä¼ æˆåŠŸå›žè°ƒ
  const handleUploadSuccess = (res, file) => {
    console.log("上传成功响应:", res);
    // å¤„理不同的数据结构:可能是数组,也可能是单个对象
    let uploadedFile = null;
    uploadedFile = res.data;
    if (!uploadedFile) {
      console.error("无法解析上传响应数据:", res);
      number.value--; // ä¸Šä¼ å¤±è´¥æ—¶å‡å°‘计数
      handleUploadError("上传响应数据格式错误", false);
      return;
    }
    // æ ¹æ®å½“前上传类型设置type字段
    let typeValue = 0; // é»˜è®¤ä¸ºç”Ÿäº§å‰
    switch (currentUploadType.value) {
      case "before":
        typeValue = 0;
        break;
      case "after":
        typeValue = 1;
        break;
      case "issue":
        typeValue = 2;
        break;
    }
    // ç¡®ä¿ä¸Šä¼ çš„æ–‡ä»¶æ•°æ®å®Œæ•´ï¼ŒåŒ…含id和type
    const fileData = {
      ...file,
      id: uploadedFile.id, // æ·»åŠ æœåŠ¡å™¨è¿”å›žçš„id
      tempId: uploadedFile.tempId ?? uploadedFile.tempFileId ?? uploadedFile.id,
      url:
        uploadedFile.url ||
        uploadedFile.downloadUrl ||
        file.tempFilePath ||
        file.path,
      bucketFilename:
        uploadedFile.bucketFilename || uploadedFile.originalFilename || file.name,
      downloadUrl: uploadedFile.downloadUrl || uploadedFile.url,
      size: uploadedFile.size || uploadedFile.byteSize || file.size,
      createTime: uploadedFile.createTime || new Date().getTime(),
      type: typeValue, // æ·»åŠ ç±»åž‹å­—æ®µï¼š0=生产前, 1=生产中, 2=生产后
    };
    uploadList.value.push(fileData);
    // ç«‹å³æ·»åŠ åˆ°å¯¹åº”çš„åˆ†ç±»ï¼Œä¸ç­‰å¾…æ‰€æœ‰æ–‡ä»¶ä¸Šä¼ å®Œæˆ
    switch (currentUploadType.value) {
      case "before":
        beforeModelValue.value.push(fileData);
        break;
      case "after":
        afterModelValue.value.push(fileData);
        break;
      case "issue":
        issueModelValue.value.push(fileData);
        break;
    }
    // é‡ç½®ä¸Šä¼ åˆ—表(因为已经添加到对应分类了)
    uploadList.value = [];
    number.value = 0;
  };
  // ä¸Šä¼ ç»“束处理(已废弃,现在在handleUploadSuccess中直接处理)
  const uploadedSuccessfully = () => {
    // æ­¤å‡½æ•°å·²ä¸å†ä½¿ç”¨ï¼Œæ–‡ä»¶ä¸Šä¼ æˆåŠŸåŽç«‹å³æ·»åŠ åˆ°å¯¹åº”åˆ†ç±»
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = size => {
    if (!size) return "";
    if (size < 1024) return size + "B";
    if (size < 1024 * 1024) return (size / 1024).toFixed(1) + "KB";
    return (size / (1024 * 1024)).toFixed(1) + "MB";
  };
</script>
<style scoped>
@@ -1725,416 +693,6 @@
    display: flex;
    align-items: center;
    justify-content: center;
  }
  /* ä¸Šä¼ å¼¹çª—样式 */
  .upload-popup-content {
    background: #fff;
    border-radius: 12px;
    width: 100%;
    min-height: 300px;
    max-height: 70vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  }
  .upload-popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 20px;
    border-bottom: 1px solid #eee;
  }
  .upload-popup-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .upload-popup-body {
    flex: 1;
    padding: 20px;
    overflow-y: auto;
  }
  .upload-popup-footer {
    display: flex;
    justify-content: flex-end;
    padding: 15px 20px;
    border-top: 1px solid #eee;
    gap: 10px;
  }
  /* ç®€åŒ–上传组件样式 */
  .simple-upload-area {
    padding: 15px;
  }
  .upload-buttons {
    display: flex;
    gap: 10px;
    margin-bottom: 15px;
  }
  .file-list {
    margin-top: 15px;
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
  }
  .file-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    background: #fff;
    border-radius: 12px;
    padding: 8px;
    border: 1px solid #e9ecef;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    transition: all 0.3s ease;
    width: calc(50% - 6px);
    min-width: 120px;
  }
  .file-item:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
  }
  .file-preview-container {
    position: relative;
    margin-bottom: 8px;
  }
  .file-preview {
    width: 80px;
    height: 80px;
    border-radius: 8px;
    object-fit: cover;
    border: 2px solid #f0f0f0;
  }
  .video-preview {
    width: 80px;
    height: 80px;
    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
    border-radius: 8px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 2px solid #f0f0f0;
  }
  .video-text {
    font-size: 12px;
    color: #666;
    margin-top: 4px;
  }
  .delete-btn {
    position: absolute;
    top: -6px;
    right: -6px;
    width: 20px;
    height: 20px;
    background: #ff4757;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    box-shadow: 0 2px 4px rgba(255, 71, 87, 0.3);
    transition: all 0.3s ease;
  }
  .delete-btn:hover {
    background: #ff3742;
    transform: scale(1.1);
  }
  .file-info {
    text-align: center;
    width: 100%;
  }
  .file-name {
    font-size: 12px;
    color: #333;
    font-weight: 500;
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100px;
  }
  .file-size {
    font-size: 10px;
    color: #999;
    margin-top: 2px;
    display: block;
  }
  .empty-state {
    text-align: center;
    padding: 40px 20px;
    color: #999;
    font-size: 14px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 2px dashed #ddd;
  }
  .upload-progress {
    margin: 15px 0;
    padding: 0 10px;
  }
  /* ä¸Šä¼ æ ‡ç­¾é¡µæ ·å¼ */
  .upload-tabs {
    display: flex;
    background: #f8f9fa;
    border-radius: 8px;
    margin-bottom: 15px;
    padding: 4px;
  }
  .tab-item {
    flex: 1;
    text-align: center;
    padding: 8px 12px;
    font-size: 14px;
    color: #666;
    border-radius: 6px;
    transition: all 0.3s ease;
    cursor: pointer;
  }
  .tab-item.active {
    background: #409eff;
    color: #fff;
    font-weight: 500;
  }
  .tab-item:hover:not(.active) {
    background: #e9ecef;
    color: #333;
  }
  /* å¼‚常状态选择样式 */
  .exception-section {
    margin-bottom: 20px;
    padding: 15px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 1px solid #e9ecef;
  }
  .section-title {
    display: block;
    font-size: 14px;
    font-weight: 600;
    color: #333;
    margin-bottom: 12px;
  }
  .exception-options {
    display: flex;
    gap: 12px;
  }
  .exception-option {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 12px 16px;
    background: #fff;
    border: 2px solid #e9ecef;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.3s ease;
    font-size: 14px;
    color: #666;
  }
  .exception-option.active {
    border-color: #409eff;
    background: #f0f8ff;
    color: #409eff;
    font-weight: 500;
  }
  .exception-option:hover:not(.active) {
    border-color: #d9d9d9;
    background: #fafafa;
  }
  /* ç»Ÿè®¡ä¿¡æ¯æ ·å¼ */
  .upload-summary {
    margin-top: 15px;
    padding: 10px;
    background: #f8f9fa;
    border-radius: 6px;
    border-left: 3px solid #409eff;
  }
  .summary-text {
    font-size: 12px;
    color: #666;
    line-height: 1.4;
  }
  /* æŸ¥çœ‹é™„件弹窗样式 */
  .attachment-popup-content {
    background: #fff;
    border-radius: 12px;
    width: 100%;
    min-height: 400px;
    max-height: 70vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  }
  .attachment-popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 20px;
    border-bottom: 1px solid #eee;
    background: #f8f9fa;
  }
  .attachment-popup-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .close-btn-attachment {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: #f5f5f5;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: all 0.3s ease;
  }
  .close-btn-attachment:hover {
    background: #e9ecef;
    transform: scale(1.1);
  }
  .attachment-popup-body {
    flex: 1;
    padding: 15px 20px;
    overflow-y: auto;
  }
  .attachment-tabs {
    display: flex;
    background: #f8f9fa;
    border-radius: 8px;
    margin-bottom: 15px;
    padding: 4px;
  }
  .attachment-content {
    min-height: 200px;
  }
  .attachment-list {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
  }
  .attachment-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    background: #fff;
    border-radius: 12px;
    padding: 8px;
    border: 1px solid #e9ecef;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    transition: all 0.3s ease;
    width: calc(33.33% - 8px);
    min-width: 100px;
    cursor: pointer;
  }
  .attachment-item:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
  }
  .attachment-preview-container {
    margin-bottom: 8px;
  }
  .attachment-preview {
    width: 80px;
    height: 80px;
    border-radius: 8px;
    object-fit: cover;
    border: 2px solid #f0f0f0;
  }
  .attachment-video-preview {
    width: 80px;
    height: 80px;
    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
    border-radius: 8px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 2px solid #f0f0f0;
  }
  .attachment-info {
    text-align: center;
    width: 100%;
  }
  .attachment-name {
    font-size: 12px;
    color: #333;
    font-weight: 500;
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 80px;
  }
  .attachment-size {
    font-size: 10px;
    color: #999;
    margin-top: 2px;
    display: block;
  }
  .attachment-empty {
    text-align: center;
    padding: 60px 20px;
    color: #999;
    font-size: 14px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 2px dashed #ddd;
  }
  /* è§†é¢‘预览弹窗样式 */
src/pages/inspectionUpload/upload.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,885 @@
<template>
  <view class="inspection-upload-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="上传巡检记录" @back="goBack" />
    <!-- é¡µé¢å†…容 -->
    <view class="upload-content">
      <!-- ä»»åŠ¡ä¿¡æ¯å¡ç‰‡ -->
      <view class="task-info-card" v-if="taskInfo">
        <view class="task-info-header">
          <text class="task-name">{{ taskInfo.taskName }}</text>
        </view>
        <view class="task-info-body">
          <view class="info-item">
            <text class="info-label">任务ID</text>
            <text class="info-value">{{ taskInfo.taskId || taskInfo.id }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">巡检位置</text>
            <text class="info-value">{{ taskInfo.inspectionLocation || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">执行人</text>
            <text class="info-value">{{ taskInfo.inspector || '-' }}</text>
          </view>
        </view>
      </view>
      <!-- å¼‚常状态选择 -->
      <view class="section-card">
        <view class="section-title">巡检状态</view>
        <view class="exception-options">
          <view
            class="exception-option"
            :class="{ active: hasException === false }"
            @click="setExceptionStatus(false)"
          >
            <u-icon name="checkmark-circle" size="20" color="#52c41a"></u-icon>
            <text class="option-text">正常</text>
          </view>
          <view
            class="exception-option"
            :class="{ active: hasException === true }"
            @click="setExceptionStatus(true)"
          >
            <u-icon name="close-circle" size="20" color="#ff4d4f"></u-icon>
            <text class="option-text">存在异常</text>
          </view>
        </view>
      </view>
      <!-- å¼‚常描述(仅在异常时显示) -->
      <view class="section-card" v-if="hasException === true">
        <view class="section-title">异常描述</view>
        <textarea
          v-model="abnormalDescription"
          class="exception-textarea"
          maxlength="500"
          placeholder="请描述异常情况..."
        />
      </view>
      <!-- åˆ†ç±»æ ‡ç­¾é¡µï¼ˆä»…在异常时显示) -->
      <view class="section-card" v-if="hasException === true">
        <view class="upload-tabs">
          <view
            class="tab-item"
            :class="{ active: currentUploadType === 'before' }"
            @click="switchUploadType('before')"
          >
            ç”Ÿäº§å‰
          </view>
          <view
            class="tab-item"
            :class="{ active: currentUploadType === 'after' }"
            @click="switchUploadType('after')"
          >
            ç”Ÿäº§ä¸­
          </view>
          <view
            class="tab-item"
            :class="{ active: currentUploadType === 'issue' }"
            @click="switchUploadType('issue')"
          >
            ç”Ÿäº§åŽ
          </view>
        </view>
        <!-- å½“前分类的上传区域 -->
        <view class="upload-area">
          <view class="upload-buttons">
            <u-button
              type="primary"
              @click="chooseMedia('image')"
              :loading="uploading"
              :disabled="getCurrentFiles().length >= uploadConfig.limit"
              :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="chooseMedia('video')"
              :loading="uploading"
              :disabled="getCurrentFiles().length >= uploadConfig.limit"
              :customStyle="{ flex: 1 }"
            >
              <uni-icons type="videocam" size="18" color="#fff" style="margin-right: 5px"></uni-icons>
              {{ uploading ? '上传中...' : '拍视频' }}
            </u-button>
          </view>
          <!-- ä¸Šä¼ è¿›åº¦ -->
          <view v-if="uploading" class="upload-progress">
            <u-line-progress :percentage="uploadProgress" :showText="true" activeColor="#409eff"></u-line-progress>
          </view>
          <!-- å½“前分类的文件列表 -->
          <view v-if="getCurrentFiles().length > 0" class="file-list">
            <view v-for="(file, index) in getCurrentFiles()" :key="index" class="file-item">
              <view class="file-preview-container">
                <image
                  v-if="file.type === 'image' || (file.type !== 'video' && !file.type)"
                  :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                  class="file-preview"
                  mode="aspectFill"
                />
                <view v-else-if="file.type === 'video'" class="video-preview">
                  <uni-icons type="videocam" size="18" color="#fff" style="margin-right: 5px"></uni-icons>
                  <text class="video-text">视频</text>
                </view>
                <!-- åˆ é™¤æŒ‰é’® -->
                <view class="delete-btn" @click="removeFile(index)">
                  <u-icon name="close" size="12" color="#fff"></u-icon>
                </view>
              </view>
              <view class="file-info">
                <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? '图片' : '视频') }}</text>
                <text class="file-size">{{ formatFileSize(file.size) }}</text>
              </view>
            </view>
          </view>
          <view v-if="getCurrentFiles().length === 0" class="empty-state">
            <text>请选择要上传的{{ getUploadTypeText() }}图片或视频</text>
          </view>
        </view>
        <!-- ç»Ÿè®¡ä¿¡æ¯ -->
        <view class="upload-summary">
          <text class="summary-text">
            ç”Ÿäº§å‰: {{ beforeModelValue.length }}个文件 |
            ç”Ÿäº§ä¸­: {{ afterModelValue.length }}个文件 |
            ç”Ÿäº§åŽ: {{ issueModelValue.length }}个文件
          </text>
        </view>
      </view>
      <!-- æ­£å¸¸çŠ¶æ€æç¤º -->
      <view class="normal-tip-card" v-if="hasException === false">
        <u-icon name="info-circle" size="60" color="#52c41a"></u-icon>
        <text class="tip-text">设备运行正常,无需上传照片</text>
      </view>
    </view>
    <!-- åº•部按钮 -->
    <view class="footer-buttons">
      <u-button @click="goBack" :customStyle="{ marginRight: '10px' }">取消</u-button>
      <u-button v-if="hasException === true" type="warning" @click="goToRepair" :customStyle="{ marginRight: '10px' }">
        æ–°å¢žæŠ¥ä¿®
      </u-button>
      <u-button type="primary" @click="submitUpload">提交</u-button>
    </view>
  </view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
import { uploadInspectionTask } from '@/api/inspectionManagement';
import { getToken } from '@/utils/auth';
import config from '@/config';
// ä»»åŠ¡ä¿¡æ¯
const taskInfo = ref(null);
// ä¸Šä¼ ç›¸å…³çŠ¶æ€
const uploading = ref(false);
const uploadProgress = ref(0);
// ä¸‰ä¸ªåˆ†ç±»çš„上传状态
const beforeModelValue = ref([]); // ç”Ÿäº§å‰
const afterModelValue = ref([]); // ç”Ÿäº§ä¸­
const issueModelValue = ref([]); // ç”Ÿäº§åŽ
// å½“前激活的上传类型
const currentUploadType = ref('before'); // 'before', 'after', 'issue'
// å¼‚常状态
const hasException = ref(null); // null: æœªé€‰æ‹©, true: å­˜åœ¨å¼‚常, false: æ­£å¸¸
// å¼‚常描述
const abnormalDescription = ref('');
// ä¸Šä¼ é…ç½®
const uploadConfig = {
  action: '/file/upload',
  limit: 10,
  fileSize: 50, // MB
  fileType: ['jpg', 'jpeg', 'png', 'mp4', 'mov'],
  maxVideoDuration: 60, // ç§’
};
// è®¡ç®—上传URL
const uploadFileUrl = computed(() => {
  const baseUrl = config.baseUrl;
  return baseUrl + uploadConfig.action;
});
// é¡µé¢åŠ è½½
onLoad((options) => {
  if (options.taskInfo) {
    try {
      taskInfo.value = JSON.parse(decodeURIComponent(options.taskInfo));
    } catch (e) {
      console.error('解析任务信息失败:', e);
    }
  }
});
// è¿”回上一页
const goBack = () => {
  uni.navigateBack();
};
// åˆ‡æ¢ä¸Šä¼ ç±»åž‹
const switchUploadType = (type) => {
  currentUploadType.value = type;
};
// èŽ·å–å½“å‰åˆ†ç±»çš„æ–‡ä»¶åˆ—è¡¨
const getCurrentFiles = () => {
  switch (currentUploadType.value) {
    case 'before':
      return beforeModelValue.value || [];
    case 'after':
      return afterModelValue.value || [];
    case 'issue':
      return issueModelValue.value || [];
    default:
      return [];
  }
};
// èŽ·å–ä¸Šä¼ ç±»åž‹æ–‡æœ¬
const getUploadTypeText = () => {
  switch (currentUploadType.value) {
    case 'before':
      return '生产前';
    case 'after':
      return '生产中';
    case 'issue':
      return '生产后';
    default:
      return '';
  }
};
// è®¾ç½®å¼‚常状态
const setExceptionStatus = (status) => {
  hasException.value = status;
};
// è·³è½¬åˆ°æ–°å¢žæŠ¥ä¿®é¡µé¢
const goToRepair = () => {
  try {
    const taskData = {
      taskId: taskInfo.value?.taskId || taskInfo.value?.id,
      taskName: taskInfo.value?.taskName,
      inspectionLocation: taskInfo.value?.inspectionLocation,
      inspector: taskInfo.value?.inspector,
      uploadedFiles: {
        before: beforeModelValue.value,
        after: afterModelValue.value,
        issue: issueModelValue.value,
      },
    };
    uni.setStorageSync('repairTaskInfo', JSON.stringify(taskData));
    uni.navigateTo({
      url: '/pages/equipmentManagement/repair/add',
    });
  } catch (error) {
    console.error('跳转报修页面失败:', error);
    uni.showToast({
      title: '跳转失败,请重试',
      icon: 'error',
    });
  }
};
// æäº¤ä¸Šä¼ 
const submitUpload = async () => {
  try {
    // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†å¼‚常状态
    if (hasException.value === null) {
      uni.showToast({
        title: '请选择巡检状态',
        icon: 'none',
      });
      return;
    }
    // å¦‚果是异常状态,检查是否有上传文件和描述
    if (hasException.value === true) {
      const totalFiles = beforeModelValue.value.length + afterModelValue.value.length + issueModelValue.value.length;
      if (totalFiles === 0) {
        uni.showToast({
          title: '请上传异常照片',
          icon: 'none',
        });
        return;
      }
      // æ£€æŸ¥æ˜¯å¦å¡«å†™äº†å¼‚常描述
      if (!abnormalDescription.value.trim()) {
        uni.showToast({
          title: '请填写异常描述',
          icon: 'none',
        });
        return;
      }
    }
    // æ˜¾ç¤ºæäº¤ä¸­çš„加载提示
    uni.showLoading({
      title: '提交中...',
      mask: true,
    });
    // æŒ‰ç…§é€»è¾‘合并所有分类的文件
    let arr = [];
    if (beforeModelValue.value.length > 0) {
      arr.push(...beforeModelValue.value);
    }
    if (afterModelValue.value.length > 0) {
      arr.push(...afterModelValue.value);
    }
    if (issueModelValue.value.length > 0) {
      arr.push(...issueModelValue.value);
    }
    // ä¼ ç»™åŽç«¯çš„临时文件ID列表
    let tempFileIds = [];
    if (arr !== null && arr.length > 0) {
      tempFileIds = arr
        .map((item) => item?.tempId ?? item?.tempFileId ?? item?.id)
        .filter((v) => v !== undefined && v !== null && v !== '');
    }
    // æäº¤æ•°æ®
    const submitData = {
      ...taskInfo.value,
      storageBlobDTO: arr,
      hasException: hasException.value,
      abnormalDescription: abnormalDescription.value,
      tempFileIds: tempFileIds,
    };
    const result = await uploadInspectionTask(submitData);
    // æ£€æŸ¥æäº¤ç»“æžœ
    if (result && (result.code === 200 || result.success)) {
      uni.hideLoading();
      uni.showToast({
        title: '提交成功',
        icon: 'success',
      });
      // è¿”回列表页并刷新
      setTimeout(() => {
        uni.navigateBack();
      }, 500);
    } else {
      uni.hideLoading();
      uni.showToast({
        title: result?.msg || result?.message || '提交失败',
        icon: 'error',
      });
    }
  } catch (error) {
    console.error('提交上传失败:', error);
    uni.hideLoading();
    uni.showToast({
      title: error?.message || '提交失败',
      icon: 'error',
    });
  }
};
// æ ¼å¼åŒ–文件大小
const formatFileSize = (size) => {
  if (!size) return '0 B';
  const units = ['B', 'KB', 'MB', 'GB'];
  let index = 0;
  let fileSize = size;
  while (fileSize >= 1024 && index < units.length - 1) {
    fileSize /= 1024;
    index++;
  }
  return `${fileSize.toFixed(2)} ${units[index]}`;
};
// æ‹ç…§/拍视频
const chooseMedia = (type) => {
  if (getCurrentFiles().length >= uploadConfig.limit) {
    uni.showToast({
      title: `最多只能选择${uploadConfig.limit}个文件`,
      icon: 'none',
    });
    return;
  }
  const remaining = uploadConfig.limit - getCurrentFiles().length;
  // ä¼˜å…ˆä½¿ç”¨ chooseMedia
  if (typeof uni.chooseMedia === 'function') {
    uni.chooseMedia({
      count: Math.min(remaining, 1),
      mediaType: [type || 'image'],
      sizeType: ['compressed', 'original'],
      sourceType: ['camera'],
      success: (res) => {
        try {
          const files = res?.tempFiles || [];
          if (!files.length) throw new Error('未获取到文件');
          files.forEach((tf, idx) => {
            const filePath = tf.tempFilePath || tf.path || '';
            const fileType = tf.fileType || type || 'image';
            const ext = fileType === 'video' ? 'mp4' : 'jpg';
            const file = {
              tempFilePath: filePath,
              path: filePath,
              type: fileType,
              name: `${fileType}_${Date.now()}_${idx}.${ext}`,
              size: tf.size || 0,
              duration: tf.duration || 0,
              createTime: Date.now(),
            };
            uploadFile(file);
          });
        } catch (err) {
          uni.showToast({ title: err.message || '处理文件失败', icon: 'none' });
        }
      },
      fail: (err) => {
        console.error('选择媒体失败:', err);
        uni.showToast({ title: '选择失败', icon: 'none' });
      },
    });
  } else {
    // é™çº§æ–¹æ¡ˆ
    if (type === 'video') {
      uni.chooseVideo({
        sourceType: ['camera'],
        success: (res) => {
          const file = {
            tempFilePath: res.tempFilePath,
            path: res.tempFilePath,
            type: 'video',
            name: `video_${Date.now()}.mp4`,
            size: res.size || 0,
            duration: res.duration || 0,
            createTime: Date.now(),
          };
          uploadFile(file);
        },
        fail: () => {
          uni.showToast({ title: '选择视频失败', icon: 'none' });
        },
      });
    } else {
      uni.chooseImage({
        count: Math.min(remaining, 9),
        sizeType: ['compressed'],
        sourceType: ['camera'],
        success: (res) => {
          const list = res.tempFilePaths || res.tempFiles || [];
          list.forEach((src, idx) => {
            const path = typeof src === 'string' ? src : src.path;
            const file = {
              tempFilePath: path,
              path: path,
              type: 'image',
              name: `image_${Date.now()}_${idx}.jpg`,
              size: 0,
              createTime: Date.now(),
            };
            uploadFile(file);
          });
        },
        fail: () => {
          uni.showToast({ title: '选择图片失败', icon: 'none' });
        },
      });
    }
  }
};
// ä¸Šä¼ å•个文件
const uploadFile = (file) => {
  const token = getToken();
  if (!token) {
    uni.showToast({ title: '用户未登录', icon: 'none' });
    return;
  }
  uploading.value = true;
  uploadProgress.value = 0;
  const uploadTask = uni.uploadFile({
    url: uploadFileUrl.value,
    filePath: file.tempFilePath,
    name: 'file',
    header: {
      Authorization: `Bearer ${token}`,
    },
    formData: {
      type: getTabType(),
    },
    success: (res) => {
      try {
        const data = JSON.parse(res.data);
        if (data.code === 200) {
          const uploadedFile = {
            ...file,
            url: data.data.url,
            tempId: data.data.tempId || data.data.id,
            status: 'success',
          };
          // æ ¹æ®å½“前类型添加到对应数组
          if (currentUploadType.value === 'before') {
            beforeModelValue.value.push(uploadedFile);
          } else if (currentUploadType.value === 'after') {
            afterModelValue.value.push(uploadedFile);
          } else if (currentUploadType.value === 'issue') {
            issueModelValue.value.push(uploadedFile);
          }
          uni.showToast({ title: '上传成功', icon: 'success' });
        } else {
          uni.showToast({ title: data.msg || '上传失败', icon: 'none' });
        }
      } catch (e) {
        uni.showToast({ title: '解析响应失败', icon: 'none' });
      }
    },
    fail: (err) => {
      console.error('上传失败:', err);
      uni.showToast({ title: '上传失败', icon: 'none' });
    },
    complete: () => {
      uploading.value = false;
    },
  });
  // ç›‘听上传进度
  uploadTask.onProgressUpdate((res) => {
    uploadProgress.value = res.progress;
  });
};
// èŽ·å–type值
const getTabType = () => {
  switch (currentUploadType.value) {
    case 'before':
      return 10;
    case 'after':
      return 11;
    case 'issue':
      return 12;
    default:
      return 10;
  }
};
// åˆ é™¤æ–‡ä»¶
const removeFile = (index) => {
  const files = getCurrentFiles();
  files.splice(index, 1);
};
</script>
<style scoped>
.inspection-upload-page {
  min-height: 100vh;
  background-color: #f5f5f5;
  padding-bottom: 80px;
}
.upload-content {
  padding: 15px;
}
/* ä»»åŠ¡ä¿¡æ¯å¡ç‰‡ */
.task-info-card {
  background: #fff;
  border-radius: 12px;
  padding: 15px;
  margin-bottom: 15px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.task-info-header {
  margin-bottom: 12px;
  padding-bottom: 12px;
  border-bottom: 1px solid #f0f0f0;
}
.task-name {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
.task-info-body {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.info-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.info-label {
  font-size: 13px;
  color: #999;
}
.info-value {
  font-size: 13px;
  color: #666;
}
/* é€šç”¨å¡ç‰‡æ ·å¼ */
.section-card {
  background: #fff;
  border-radius: 12px;
  padding: 15px;
  margin-bottom: 15px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.section-title {
  font-size: 14px;
  font-weight: 600;
  color: #333;
  margin-bottom: 12px;
}
/* å¼‚常状态选择 */
.exception-options {
  display: flex;
  gap: 12px;
}
.exception-option {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 14px 16px;
  background: #f8f9fa;
  border: 2px solid #e9ecef;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s ease;
}
.exception-option.active {
  border-color: #409eff;
  background: #f0f8ff;
}
.option-text {
  font-size: 14px;
  color: #333;
  font-weight: 500;
}
/* å¼‚常描述 */
.exception-textarea {
  width: 100%;
  min-height: 100px;
  padding: 12px;
  background: #f8f9fa;
  border: 1px solid #e9ecef;
  border-radius: 8px;
  font-size: 14px;
  color: #333;
  resize: none;
  box-sizing: border-box;
}
.exception-textarea:focus {
  outline: none;
  border-color: #409eff;
  background: #fff;
}
/* åˆ†ç±»æ ‡ç­¾é¡µ */
.upload-tabs {
  display: flex;
  gap: 10px;
  margin-bottom: 15px;
}
.tab-item {
  flex: 1;
  padding: 10px;
  text-align: center;
  background: #f5f5f5;
  border-radius: 6px;
  font-size: 13px;
  color: #666;
  cursor: pointer;
  transition: all 0.3s;
}
.tab-item.active {
  background: #409eff;
  color: #fff;
}
/* ä¸Šä¼ åŒºåŸŸ */
.upload-area {
  padding: 10px 0;
}
.upload-buttons {
  display: flex;
  gap: 10px;
  margin-bottom: 15px;
}
.upload-progress {
  margin-bottom: 15px;
}
/* æ–‡ä»¶åˆ—表 */
.file-list {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.file-item {
  width: calc(33.33% - 7px);
}
.file-preview-container {
  position: relative;
  width: 100%;
  aspect-ratio: 1;
  border-radius: 8px;
  overflow: hidden;
  background: #f5f5f5;
}
.file-preview {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.video-preview {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: #333;
}
.video-text {
  font-size: 12px;
  color: #fff;
  margin-top: 5px;
}
.delete-btn {
  position: absolute;
  top: 5px;
  right: 5px;
  width: 22px;
  height: 22px;
  background: rgba(0, 0, 0, 0.5);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.file-info {
  margin-top: 5px;
}
.file-name {
  display: block;
  font-size: 11px;
  color: #666;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.file-size {
  display: block;
  font-size: 10px;
  color: #999;
  margin-top: 2px;
}
.empty-state {
  text-align: center;
  padding: 30px;
  color: #999;
  font-size: 13px;
}
/* ç»Ÿè®¡ä¿¡æ¯ */
.upload-summary {
  margin-top: 15px;
  padding: 10px;
  background: #f8f9fa;
  border-radius: 6px;
  border-left: 3px solid #409eff;
}
.summary-text {
  font-size: 12px;
  color: #666;
}
/* æ­£å¸¸çŠ¶æ€æç¤º */
.normal-tip-card {
  background: #f6ffed;
  border: 2px dashed #b7eb8f;
  border-radius: 12px;
  padding: 50px 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  margin-bottom: 15px;
}
.normal-tip-card .tip-text {
  margin-top: 15px;
  font-size: 16px;
  color: #52c41a;
  font-weight: 500;
}
/* åº•部按钮 */
.footer-buttons {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  padding: 15px;
  background: #fff;
  border-top: 1px solid #f0f0f0;
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
}
</style>
src/pages/inventoryManagement/stockManagement/Qualified.vue
ÎļþÒÑɾ³ý
src/pages/inventoryManagement/stockManagement/Record.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,292 @@
<template>
  <view class="record-container">
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input
            class="search-text"
            placeholder="请输入产品大类"
            v-model="searchForm.productName"
            @confirm="handleQuery"
            clearable
          />
        </view>
        <view class="filter-button" @click="handleQuery">
          <up-icon name="search" size="24" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <scroll-view scroll-y class="ledger-list" v-if="tableData.length > 0" @scrolltolower="loadMore">
      <view v-for="item in tableData" :key="item.id" class="ledger-item">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
            </view>
            <text class="item-id">{{ item.productName }}</text>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">规格型号</text>
            <text class="detail-value">{{ item.model }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">单位</text>
            <text class="detail-value">{{ item.unit }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">批号</text>
            <text class="detail-value">{{ item.batchNo }}</text>
          </view>
          <view class="quantity-section">
            <view class="quantity-box qualified">
              <text class="q-label">合格库存</text>
              <text class="q-value">{{ item.qualifiedQuantity }}</text>
            </view>
            <view class="quantity-box unqualified">
              <text class="q-label">不合格库存</text>
              <text class="q-value">{{ item.unQualifiedQuantity }}</text>
            </view>
          </view>
          <view class="quantity-section">
            <view class="quantity-box locked">
              <text class="q-label">合格冻结</text>
              <text class="q-value">{{ item.qualifiedLockedQuantity }}</text>
            </view>
            <view class="quantity-box locked">
              <text class="q-label">不合格冻结</text>
              <text class="q-value">{{ item.unQualifiedLockedQuantity }}</text>
            </view>
          </view>
          <view class="detail-row">
            <text class="detail-label">库存预警</text>
            <text class="detail-value">{{ item.warnNum }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">备注</text>
            <text class="detail-value">{{ item.remark || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">更新时间</text>
            <text class="detail-value">{{ item.updateTime }}</text>
          </view>
        </view>
      </view>
      <up-loadmore :status="loadStatus" />
    </scroll-view>
    <view v-else-if="!loading" class="no-data">
      <up-empty mode="data" text="暂无库存数据"></up-empty>
    </view>
  </view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { getStockInventoryListPageCombined } from "@/api/inventoryManagement/stockInventory.js";
const props = defineProps({
  productId: {
    type: Number,
    required: true
  }
});
const tableData = ref([]);
const loading = ref(false);
const loadStatus = ref('loadmore');
const page = reactive({ current: 1, size: 10 });
const total = ref(0);
const searchForm = reactive({
  productName: '',
  topParentProductId: props.productId
});
const handleQuery = () => {
  page.current = 1;
  tableData.value = [];
  getList();
};
const getList = () => {
  if (loading.value) return;
  loading.value = true;
  loadStatus.value = 'loading';
  getStockInventoryListPageCombined({
    ...searchForm,
    current: page.current,
    size: page.size
  }).then(res => {
    loading.value = false;
    const records = res.data.records || [];
    tableData.value = page.current === 1 ? records : [...tableData.value, ...records];
    total.value = res.data.total;
    loadStatus.value = tableData.value.length >= total.value ? 'nomore' : 'loadmore';
  }).catch(() => {
    loading.value = false;
    loadStatus.value = 'loadmore';
  });
};
const loadMore = () => {
  if (loadStatus.value === 'loadmore') {
    page.current++;
    getList();
  }
};
onMounted(() => {
  getList();
});
</script>
<style scoped lang="scss">
.record-container {
  height: 100%;
  display: flex;
  flex-direction: column;
  background-color: #f5f7fa;
}
.search-section {
  padding: 20rpx;
  background-color: #ffffff;
  position: sticky;
  top: 0;
  z-index: 10;
}
.search-bar {
  display: flex;
  align-items: center;
  background-color: #f2f2f2;
  border-radius: 40rpx;
  padding: 0 30rpx;
  height: 80rpx;
}
.search-input {
  flex: 1;
}
.search-text {
  font-size: 28rpx;
}
.filter-button {
  padding-left: 20rpx;
}
.ledger-list {
  flex: 1;
  padding: 20rpx;
  box-sizing: border-box;
}
.ledger-item {
  background-color: #ffffff;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.item-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20rpx;
}
.item-left {
  display: flex;
  align-items: center;
}
.document-icon {
  width: 40rpx;
  height: 40rpx;
  background: linear-gradient(135deg, #2979ff, #1565c0);
  border-radius: 8rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 16rpx;
}
.item-id {
  font-size: 30rpx;
  font-weight: bold;
  color: #303133;
}
.item-details {
  .detail-row {
    display: flex;
    justify-content: space-between;
    margin-bottom: 16rpx;
    font-size: 26rpx;
    .detail-label {
      color: #909399;
    }
    .detail-value {
      color: #303133;
      font-weight: 500;
    }
  }
}
.quantity-section {
  display: flex;
  gap: 20rpx;
  margin: 20rpx 0;
  .quantity-box {
    flex: 1;
    padding: 16rpx;
    border-radius: 8rpx;
    display: flex;
    flex-direction: column;
    align-items: center;
    .q-label {
      font-size: 22rpx;
      margin-bottom: 8rpx;
    }
    .q-value {
      font-size: 32rpx;
      font-weight: bold;
    }
    &.qualified {
      background-color: #ecf5ff;
      color: #409eff;
    }
    &.unqualified {
      background-color: #fef0f0;
      color: #f56c6c;
    }
    &.locked {
      background-color: #f4f4f5;
      color: #909399;
    }
  }
}
.no-data {
  padding-top: 200rpx;
}
</style>
src/pages/inventoryManagement/stockManagement/Unqualified.vue
ÎļþÒÑɾ³ý
src/pages/inventoryManagement/stockManagement/index.vue
@@ -1,57 +1,104 @@
<template>
  <view class="app-container">
    <PageHeader title="库存管理" @back="goBack" />
    <up-tabs :list="tabs" @click="handleTabClick" :current="activeTab"/>
    <swiper class="swiper-box" :current="activeTab" @change="handleSwiperChange">
      <swiper-item class="swiper-item">
        <qualified-record />
      </swiper-item>
      <swiper-item class="swiper-item">
        <unqualified-record />
      </swiper-item>
    </swiper>
    <PageHeader title="库存管理"
                @back="goBack" />
    <view v-if="loading"
          class="loading-state">
      <up-loading-icon text="加载中..."></up-loading-icon>
    </view>
    <template v-else>
      <up-tabs :list="tabs"
               @click="handleTabClick"
               :current="activeTab" />
      <swiper class="swiper-box"
              :current="activeTab"
              @change="handleSwiperChange">
        <swiper-item class="swiper-item"
                     v-for="tab in products"
                     :key="tab.id">
          <record :product-id="tab.id"
                  v-if="activeTab === products.indexOf(tab) || initializedTabs.includes(tab.id)" />
        </swiper-item>
      </swiper>
    </template>
  </view>
</template>
<script setup>
import { ref } from 'vue';
import PageHeader from "@/components/PageHeader.vue";
import QualifiedRecord from "./Qualified.vue";
import UnqualifiedRecord from "./Unqualified.vue";
  import { ref, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import Record from "./Record.vue";
  import { productTreeList } from "@/api/basicData/product.js";
const activeTab = ref(0);
const tabs = ref([
  { name: '合格库存' },
  { name: '不合格库存' }
]);
  const activeTab = ref(0);
  const tabs = ref([]);
  const products = ref([]);
  const loading = ref(false);
  const initializedTabs = ref([]);
const handleTabClick = (item) => {
  activeTab.value = item.index;
};
  const handleTabClick = item => {
    activeTab.value = item.index;
    if (!initializedTabs.value.includes(products.value[item.index].id)) {
      initializedTabs.value.push(products.value[item.index].id);
    }
  };
const handleSwiperChange = (e) => {
  activeTab.value = e.detail.current;
};
  const handleSwiperChange = e => {
    const index = e.detail.current;
    activeTab.value = index;
    if (!initializedTabs.value.includes(products.value[index].id)) {
      initializedTabs.value.push(products.value[index].id);
    }
  };
const goBack = () => {
  uni.navigateBack();
};
  const fetchProducts = async () => {
    loading.value = true;
    try {
      const res = await productTreeList();
      // è¿‡æ»¤æ ¹èŠ‚ç‚¹äº§å“
      products.value = res
        .filter(item => item.parentId === null)
        .map(({ id, productName }) => ({ id, productName }));
      tabs.value = products.value.map(p => ({ name: p.productName }));
      if (products.value.length > 0) {
        activeTab.value = 0;
        initializedTabs.value = [products.value[0].id];
      }
    } finally {
      loading.value = false;
    }
  };
  const goBack = () => {
    uni.navigateBack();
  };
  onMounted(() => {
    fetchProducts();
  });
</script>
<style scoped lang="scss">
.app-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
  background-color: #f8f9fa;
}
.swiper-box {
  flex: 1;
}
.swiper-item {
  height: 100%;
}
:deep(.up-tabs) {
  background-color: #fff;
}
  .app-container {
    display: flex;
    flex-direction: column;
    height: 100vh;
    background-color: #f8f9fa;
  }
  .loading-state {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .swiper-box {
    flex: 1;
  }
  .swiper-item {
    height: 100%;
  }
  :deep(.up-tabs) {
    background-color: #fff;
  }
</style>
src/pages/procurementManagement/procurementLedger/detail.vue
@@ -31,8 +31,7 @@
      </up-form-item>
      <up-form-item label="供应商名称"
                    prop="supplierName"
                    required
                    >
                    required>
        <up-input v-model="form.supplierName"
                  readonly
                  :disabled="isReadOnly"
@@ -82,55 +81,6 @@
                  placeholder="请输入"
                  disabled />
      </up-form-item>
      <view class="approval-process">
        <view class="approval-header">
          <text class="approval-title">审核流程</text>
          <text class="approval-desc">每个步骤只能选择一个审批人</text>
        </view>
        <view class="approval-steps">
          <view v-for="(step, stepIndex) in approverNodes"
                :key="stepIndex"
                class="approval-step">
            <view class="step-dot"></view>
            <view class="step-title">
              <text>审批人</text>
            </view>
            <view class="approver-container">
              <view v-if="step.nickName"
                    class="approver-item">
                <view class="approver-avatar">
                  <text class="avatar-text">{{ step.nickName.charAt(0) }}</text>
                  <view class="status-dot"></view>
                </view>
                <view class="approver-info">
                  <text class="approver-name">{{ step.nickName }}</text>
                </view>
                <view class="delete-approver-btn"
                      v-if="!isReadOnly"
                      @click="removeApprover(stepIndex)">×</view>
              </view>
              <view v-else-if="!isReadOnly"
                    class="add-approver-btn"
                    @click="addApprover(stepIndex)">
                <view class="add-circle">+</view>
                <text class="add-label">选择审批人</text>
              </view>
            </view>
            <view class="step-line"
                  v-if="stepIndex < approverNodes.length - 1"></view>
            <view class="delete-step-btn"
                  v-if="approverNodes.length > 1 && !isReadOnly"
                  @click="removeApprovalStep(stepIndex)">删除节点</view>
          </view>
        </view>
        <view class="add-step-btn" v-if="!isReadOnly">
          <u-button icon="plus"
                    plain
                    type="primary"
                    style="width: 100%"
                    @click="addApprovalStep">新增节点</u-button>
        </view>
      </view>
      <up-popup :show="showTimePicker"
                mode="bottom"
                @close="showTimePicker = false">
src/pages/works.vue
@@ -570,23 +570,23 @@
  // ç”Ÿäº§ç®¡æŽ§åŠŸèƒ½æ•°æ®
  const productionItems = reactive([
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      icon: "/static/images/icon/shengchanjihua.svg",
      label: "主生产计划",
    },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      icon: "/static/images/icon/shengchandingdan.svg",
      label: "生产订单",
    },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      icon: "/static/images/icon/shengchanzhuisu.svg",
      label: "生产追溯",
    },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      icon: "/static/images/icon/shengchanshikuang.svg",
      label: "工序生产实况",
    },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      icon: "/static/images/icon/shengchanpaichan.svg",
      label: "生产排产",
    },
@@ -595,17 +595,12 @@
      label: "生产报工",
    },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      icon: "/static/images/icon/baogongtaizhang.svg",
      label: "报工台账",
    },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      icon: "/static/images/icon/shengchanhesuan.svg",
      label: "生产核算",
    },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      label: "工序实况",
    },
  ]);
src/static/images/icon/baogongtaizhang.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_141_34784"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_141_34784)"><path d="M22.914785281249998,14.7584486125C19.94918328125,14.5709972125,17.63954128125,12.1115937125,17.63853528125,9.140073812499999C17.63853528125,7.8441987125,18.08040928125,6.6541986125,18.81540928125,5.7021988625C16.85716058125,5.1570737325,16.802909881250002,5.6943240125,15.13253498125,6.5203237125C13.82265998125,6.7941990125,13.91541008125,5.0013237625,12.260785081249999,5.8281989124999996C13.102535281249999,7.6236989125,13.792035081249999,8.1040738125,13.94516038125,8.736698412500001C13.753535281249999,8.7576985125,13.60566088125,8.8758237125,13.60566088125,9.0201988125C13.60566088125,9.1636982125,13.753535281249999,9.282698912499999,13.94516138125,9.3028235125C13.93991188125,9.3308234125,13.93641088125,9.3570738125,13.92853638125,9.3894486125C13.799910981250001,9.8951988125,12.595911481249999,10.705448112500001,11.571286181249999,11.464948612499999L11.571286181249999,18.7930733125C11.571286181249999,19.3863233125,11.16791058125,19.865823312499998,10.670036281249999,19.865823312499998L7.51128649725,19.865823312499998C7.19803667125,22.8863223125,8.09316146125,25.6539483125,11.87228678125,25.7878223125C15.48691178125,25.9138223125,15.94628718125,25.9138223125,15.94628718125,25.9138223125C15.94628718125,25.9138223125,16.40566158125,25.9138223125,20.021161281250002,25.7878223125C26.02016028125,25.5751973125,24.75928728125,18.7221973125,22.914785281249998,14.7584486125ZM18.59141028125,17.703697312499997L18.59141028125,19.0109473125L16.719785681250002,19.0109473125L16.719785681250002,19.546446312500002L18.57128528125,19.546446312500002L18.57828428125,20.8598213125L16.71978378125,20.8545703125L16.71978378125,22.4269463125L15.25503448125,22.4199463125L15.25503448125,20.8545723125L13.40003438125,20.8545723125L13.40003438125,19.547323312499998L15.25503448125,19.547323312499998L15.25503448125,19.0118233125L13.40003438125,19.0118233125L13.40003438125,17.704573312500003L15.04503438125,17.697574312500002L12.55653478125,14.2518244125L14.20153378125,14.2518244125L16.04865928125,16.6685753125L17.693658281250002,14.2308264125L19.33778628125,14.2308264125L16.934161181249998,17.6975763125L18.59141028125,17.703697312499997Z" fill="#CFCFCF" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M22.914785281249998,14.7584486125C19.94918328125,14.5709972125,17.63954128125,12.1115937125,17.63853528125,9.140073812499999C17.63853528125,7.8441987125,18.08040928125,6.6541986125,18.81540928125,5.7021988625C16.85716058125,5.1570737325,16.802909881250002,5.6943240125,15.13253498125,6.5203237125C13.82265998125,6.7941990125,13.91541008125,5.0013237625,12.260785081249999,5.8281989124999996C13.102535281249999,7.6236989125,13.792035081249999,8.1040738125,13.94516038125,8.736698412500001C13.753535281249999,8.7576985125,13.60566088125,8.8758237125,13.60566088125,9.0201988125C13.60566088125,9.1636982125,13.753535281249999,9.282698912499999,13.94516138125,9.3028235125C13.93991188125,9.3308234125,13.93641088125,9.3570738125,13.92853638125,9.3894486125C13.799910981250001,9.8951988125,12.595911481249999,10.705448112500001,11.571286181249999,11.464948612499999L11.571286181249999,18.7930733125C11.571286181249999,19.3863233125,11.16791058125,19.865823312499998,10.670036281249999,19.865823312499998L7.51128649725,19.865823312499998C7.19803667125,22.8863223125,8.09316146125,25.6539483125,11.87228678125,25.7878223125C15.48691178125,25.9138223125,15.94628718125,25.9138223125,15.94628718125,25.9138223125C15.94628718125,25.9138223125,16.40566158125,25.9138223125,20.021161281250002,25.7878223125C26.02016028125,25.5751973125,24.75928728125,18.7221973125,22.914785281249998,14.7584486125ZM18.59141028125,17.703697312499997L18.59141028125,19.0109473125L16.719785681250002,19.0109473125L16.719785681250002,19.546446312500002L18.57128528125,19.546446312500002L18.57828428125,20.8598213125L16.71978378125,20.8545703125L16.71978378125,22.4269463125L15.25503448125,22.4199463125L15.25503448125,20.8545723125L13.40003438125,20.8545723125L13.40003438125,19.547323312499998L15.25503448125,19.547323312499998L15.25503448125,19.0118233125L13.40003438125,19.0118233125L13.40003438125,17.704573312500003L15.04503438125,17.697574312500002L12.55653478125,14.2518244125L14.20153378125,14.2518244125L16.04865928125,16.6685753125L17.693658281250002,14.2308264125L19.33778628125,14.2308264125L16.934161181249998,17.6975763125L18.59141028125,17.703697312499997Z" fill="#FF9533" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M10.38227634375,2.37548828125L0.92615223375,2.37548828125C0.42740228375,2.37548828125,0.02490234375,2.85586330125,0.02490234375,3.4464882812499997L0.02490234375,18.43261328125C0.02490234375,19.02323728125,0.42740231375,19.50186328125,0.92615223375,19.50186328125L10.38315134375,19.50186328125C10.88102634375,19.50186328125,11.28440134375,19.02323728125,11.28440134375,18.43261328125L11.28440134375,3.4464882812499997C11.28352534375,2.85586330125,10.88102534375,2.37548828125,10.38227634375,2.37548828125ZM3.62815214375,17.89798828125C3.62815214375,18.19286328125,3.42690204375,18.43261328125,3.17840214375,18.43261328125L1.82565224375,18.43261328125C1.57802724375,18.43261328125,1.37590234375,18.191988281249998,1.37590234375,17.89798828125L1.37590234375,16.29148728125C1.37590234375,15.99573828125,1.57715224375,15.75511128125,1.82565224375,15.75511128125L3.17665244375,15.75511128125C3.42602734375,15.75511128125,3.62815214375,15.99573728125,3.62815214375,16.29148728125L3.62815214375,17.89798828125ZM3.62815214375,14.15123828125C3.62815214375,14.44698828125,3.42690204375,14.68673828125,3.17840214375,14.68673828125L1.82565224375,14.68673828125C1.57802724375,14.68673828125,1.37590234375,14.44698828125,1.37590234375,14.15123828125L1.37590234375,12.54561328125C1.37590234375,12.25073818125,1.57715224375,12.01186368125,1.82565224375,12.01186368125L3.17665244375,12.01186368125C3.42602734375,12.01186368125,3.62815214375,12.25073818125,3.62815214375,12.54561328125L3.62815214375,14.15123828125ZM3.62815214375,10.40448858125C3.62815214375,10.70023918125,3.42690204375,10.94086358125,3.17840214375,10.94086358125L1.82565224375,10.94086358125C1.57802724375,10.94086358125,1.37590234375,10.70111368125,1.37590234375,10.40448858125L1.37590234375,8.79886388125C1.37590234375,8.503988781250001,1.57715224375,8.26423888125,1.82565224375,8.26423888125L3.17665244375,8.26423888125C3.42602734375,8.26423888125,3.62815214375,8.503988781250001,3.62815214375,8.79886388125L3.62815214375,10.40448858125ZM6.32927704375,18.43261528125L4.97827724375,18.43261528125C4.72890234375,18.43261528125,4.52852724375,18.19199028125,4.52852724375,17.89798928125L4.52852724375,16.29148928125C4.52852724375,15.99574028125,4.73065234375,15.75511328125,4.97827724375,15.75511328125L6.32927704375,15.75511328125C6.57865194375,15.75511328125,6.77902694375,15.99573828125,6.77902694375,16.29148928125L6.77902694375,17.89798928125L6.77990194375,17.89798928125C6.78077694375,18.19286428125,6.57865194375,18.43261528125,6.32927704375,18.43261528125ZM6.32927704375,14.68674028125L4.97827724375,14.68674028125C4.72890234375,14.68674028125,4.52852724375,14.44698928125,4.52852724375,14.15124028125L4.52852724375,12.54561528125C4.52852724375,12.25074008125,4.73065234375,12.01186558125,4.97827724375,12.01186558125L6.32927704375,12.01186558125C6.57865194375,12.01186558125,6.77902694375,12.25074008125,6.77902694375,12.54561528125L6.77902694375,14.15124028125L6.77990194375,14.15124028125C6.78077694375,14.44698928125,6.57865194375,14.68674028125,6.32927704375,14.68674028125ZM6.32927704375,10.93999008125L4.97827724375,10.93999008125C4.72890234375,10.93999008125,4.52852724375,10.70024018125,4.52852724375,10.40361498125L4.52852724375,8.79798988125C4.52852724375,8.50311518125,4.73065234375,8.26336478125,4.97827724375,8.26336478125L6.32927704375,8.26336478125C6.57865194375,8.26336478125,6.77902694375,8.50311518125,6.77902694375,8.79798988125L6.77902694375,10.40361498125L6.77990194375,10.40361498125C6.78077694375,10.70024018125,6.57865194375,10.93999008125,6.32927704375,10.93999008125ZM9.93252654375,17.89798928125C9.93252654375,18.19286428125,9.73127744375,18.43261528125,9.48277664375,18.43261528125L8.13090134375,18.43261528125C7.88152694375,18.43261528125,7.68027644375,18.19199028125,7.68027644375,17.89798928125L7.68027644375,16.29148928125C7.68027644375,15.99574028125,7.88152594375,15.75511328125,8.13090134375,15.75511328125L9.48190114375,15.75511328125C9.72952654375,15.75511328125,9.93165114375,15.99573828125,9.93165114375,16.29148928125L9.93165114375,17.89798928125L9.93252654375,17.89798928125ZM9.93252654375,14.15124028125C9.93252654375,14.44698928125,9.73127744375,14.68674028125,9.48277664375,14.68674028125L8.13090134375,14.68674028125C7.88152694375,14.68674028125,7.68027644375,14.44698928125,7.68027644375,14.15124028125L7.68027644375,12.54561528125C7.68027644375,12.25074008125,7.88152594375,12.01186558125,8.13090134375,12.01186558125L9.48190114375,12.01186558125C9.72952654375,12.01186558125,9.93165114375,12.25074008125,9.93165114375,12.54561528125L9.93165114375,14.15124028125L9.93252654375,14.15124028125ZM9.93252654375,10.40449048125C9.93252654375,10.70024108125,9.73127744375,10.94086548125,9.48277664375,10.94086548125L8.13090134375,10.94086548125C7.88152694375,10.94086548125,7.68027644375,10.70111558125,7.68027644375,10.40449048125L7.68027644375,8.79886528125C7.68027644375,8.50399068125,7.88152594375,8.26424028125,8.13090134375,8.26424028125L9.48190114375,8.26424028125C9.72952654375,8.26424028125,9.93165114375,8.50399068125,9.93165114375,8.79886528125L9.93165114375,10.40449048125L9.93252654375,10.40449048125ZM9.93252654375,6.65686558125L1.37590234375,6.65686558125L1.37590234375,3.98286338125L9.93165204375,3.98286338125L9.93165204375,6.65686318125L9.93252654375,6.65686558125ZM23.27452634375,4.50086328125C20.71282234375,4.50086328125,18.63615234375,6.57753468125,18.63615234375,9.13923788125C18.63615234375,11.70094208125,20.71282234375,13.77761328125,23.27452634375,13.77761328125C25.83623134375,13.77761328125,27.91290234375,11.70094208125,27.91290234375,9.13923788125C27.91290234375,6.57753468125,25.83623134375,4.50086328125,23.27452634375,4.50086328125ZM26.45340134375,8.56961348125C26.45340134375,8.606363281250001,26.41489934375,8.64398858125,26.37815134375,8.64398858125L25.85314934375,8.64398858125C25.81552534375,8.64398858125,25.77702534375,8.68161388125,25.74027634375,8.719239281250001L25.28965234375,9.20573898125L25.28965234375,9.317738981249999L25.66502734375,10.06761458125C25.70177634375,10.10436438125,25.74027634375,10.14198968125,25.77702734375,10.14198968125L26.00190134375,10.14198968125C26.04236234375,10.14420228125,26.07453734375,10.176757381249999,26.07627634375,10.21723988125L26.07627634375,10.81748958125C26.07627634375,10.85423948125,26.03952634375,10.89186478125,26.00190134375,10.89186478125L25.40252734375,10.89186478125C25.36577834375,10.89186478125,25.28965234375,10.85423948125,25.28965234375,10.81748958125L24.80140334375,9.842739581250001C24.80140334375,9.805114281249999,24.76465234375,9.805114281249999,24.72702834375,9.842739581250001L23.86515234375,10.81748958125C23.86515234375,10.85423948125,23.78990134375,10.89186478125,23.75227734375,10.89186478125L23.15290234375,10.89186478125C23.11527834375,10.89186478125,23.07852934375,10.85423948125,23.07852934375,10.81748958125L23.07852934375,10.21723988125C23.07852934375,10.17961458125,23.11527834375,10.14198968125,23.15290234375,10.14198968125L23.67790434375,10.14198968125C23.71640434375,10.14198968125,23.75227734375,10.10436438125,23.78990334375,10.10436438125L24.46452934375,9.317738981249999L24.46452934375,9.20573898125L24.24052834375,8.719239281250001C24.20202834375,8.68248938125,24.16527734375,8.64398858125,24.12677934375,8.64398858125L23.52565334375,8.64398858125C23.48890534375,8.64398858125,23.45215434375,8.606363281250001,23.45215434375,8.56961348125C23.45215434375,8.606363281250001,23.41452934375,8.64398858125,23.37690334375,8.64398858125L22.51590534375,8.64398858125L21.95152834375,10.89273838125C21.87890434375,11.26723858125,21.76602934375,11.60411358125,21.57703034375,11.79223828125C21.42828034375,11.97948838125,21.12903034375,12.01711368125,20.82802934375,12.01711368125L20.45352934375,12.01711368125L20.45352934375,11.26723768125L20.82802934375,11.26723768125C20.82802934375,11.26723768125,21.12990334375,11.34161278125,21.24190334375,10.89273738125L21.80365334375,8.64398768125L20.90502934375,8.64398768125C20.86652734375,8.64398768125,20.82890334375,8.606362781249999,20.82890334375,8.56961298125L20.82890334375,7.97023778125C20.82890334375,7.93261238125,20.86652734375,7.89498708125,20.90502934375,7.89498708125L21.99177934375,7.89498708125L22.17902934375,7.14598708125C22.29102934375,6.77061228125,22.47915434375,6.54573728125,22.66552934375,6.39611248125C23.37777934375,5.79586268125,24.20290334375,6.05836198125,24.57827934375,6.13361268125L24.57827934375,6.77061228125C24.20290334375,6.69711258125,23.64115534375,6.50811198125,23.22815534375,6.80823758125C23.11615534375,6.88348768125,22.92890534375,7.07161238125,22.89128134375,7.18361238125L22.70490634375,7.89498708125L23.37865634375,7.89498708125C23.41628034375,7.89498708125,23.45390734375,7.93261238125,23.45390734375,7.97023778125C23.45390734375,7.93261238125,23.49065634375,7.89498708125,23.52740634375,7.89498708125L24.50303034375,7.89498708125C24.54153034375,7.89498708125,24.57828134375,7.93261238125,24.61590534375,7.97023778125L24.95278134375,8.64486218125C24.95278134375,8.68248748125,24.99040634375,8.68248748125,25.02803034375,8.64486218125L25.66590734375,7.97023778125C25.66590734375,7.93261238125,25.74115734375,7.89498708125,25.77790634375,7.89498708125L26.37903234375,7.89498708125C26.41578134375,7.89498708125,26.45428234375,7.93261238125,26.45428234375,7.97023778125L26.45428234375,8.56961298125L26.45340134375,8.56961348125Z" fill="#FFDB42" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
src/static/images/icon/shengchandingdan.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_141_34441"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_141_34750"><rect x="7" y="5" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_141_34441)"><g transform="matrix(1,3.7247229744963306e-9,-3.7247229744963306e-9,1,7.449445948992661e-9,-7.449445948992661e-9)"><path d="M8.6032233,27C7.2891606,27,6.33216852,28.2456321,6.67075348,29.5153255L7.2081735,31.5306506C7.6751106,33.2816644,9.260909999999999,34.5,11.0731134,34.5L16.926887999999998,34.5C18.739091000000002,34.5,20.324889,33.2816644,20.791826,31.5306506L21.329247000000002,29.5153253C21.667831,28.2456321,20.710839,27,19.396776,27L8.6032233,27Z" fill="#69FFB4" fill-opacity="1" transform="matrix(1,0,0,-1,0,54)"/><path d="M2,8.000001000000001L2,17.499999000000003C2,20.813709,4.6862917,23.5,8.000000499999999,23.5L20,23.5C23.313709,23.5,26,20.813709,26,17.5L26,8.000001000000001C26,4.6862917,23.313709,2,20,2L8.000001000000001,2C4.6862917,2,2,4.6862917,2,8.000001000000001Z" fill="#01BF34" fill-opacity="1" style="opacity:0.800000011920929;"/></g><g clip-path="url(#master_svg1_141_34750)"><path d="M10.5241249,13.861916515625001C10.4281249,13.975916815625,10.383124800000001,14.121916815625,10.3871248,14.299916315625001C10.3911247,14.477916715625,10.4491248,14.623916615625,10.5581248,14.737916015625C10.6671247,14.851916315625,10.8131247,14.908916515625,10.9961247,14.908916515625L12.3081245,14.908916515625C12.2621245,15.045916515625,12.235124599999999,15.177916515625,12.226124800000001,15.304916415625C12.2171249,15.432916615625,12.2171249,15.564916615625,12.226124800000001,15.700916315625C12.226124800000001,16.229916615625,12.2881246,16.728916015625,12.4111247,17.197916015624997C12.5341249,17.667916015625,12.705124900000001,18.102916015625,12.9241247,18.503916015625002L10.203125,18.503916015625002C9.7111249,18.485916015625,9.30112505,18.312916015625,8.97312498,17.983915015625C8.64512491,17.654915015625,8.472125053,17.245915015625002,8.453125,16.753916015625002L8.453125,8.263916015625C8.471125007,7.771915915625,8.64412498,7.359915975625,8.97312498,7.026916025625C9.30112505,6.693916085625,9.7111249,6.522916078525,10.203125,6.513916015625L17.8181248,6.513916015625C18.3101244,6.522915959325,18.722125,6.693916085625,19.055125,7.026916025625C19.388125000000002,7.359915975625,19.559125,7.771915915625,19.568126,8.263916015625L19.568126,10.533916015625C19.304126,10.405916215625,19.010126,10.314916115625,18.686126,10.260915715625C18.3621254,10.205915915624999,18.0461254,10.183915615625,17.736125899999998,10.192915915625L17.640126199999997,10.192915915625C17.5941267,10.101915815625,17.5331259,10.033916015625,17.455125799999998,9.987916015625C17.3771257,9.941916015625,17.293126100000002,9.919916115625,17.2021255,9.919916115625L10.818125,9.919916115625C10.6811249,9.919916115625,10.5601251,9.965916115625,10.456125,10.056916215625C10.351125,10.147916315625,10.299125,10.279916315625,10.299125,10.452916115625C10.299125,10.625916015625,10.351125,10.760916215624999,10.456125,10.855916015624999C10.561125,10.951916215625001,10.6821251,11.021915915625,10.818125,11.067915915625L14.851124800000001,11.067915915625C14.322125,11.368916015625,13.864124799999999,11.737916015625,13.477124700000001,12.174915815624999C13.0891247,12.612916015625,12.7871246,13.117916115625,12.5681248,13.692915915625L10.996125,13.692915915625C10.7771249,13.690916015625,10.6201251,13.747916215625,10.5241249,13.861916515625001ZM20.327126,13.061916315625C21.024124999999998,13.726916315625001,21.387126000000002,14.574916815625,21.414125,15.604916615625C21.387124999999997,16.643917015625,21.024124999999998,17.509916015625002,20.327126,18.202917015624998C19.630126,18.895918015625,18.766126,19.255917015625002,17.736125,19.282917015625C16.697125399999997,19.255917015625002,15.8311253,18.886917015625002,15.1381254,18.175917015625C14.4451256,17.464916015625,14.085125399999999,16.607917015625,14.0581255,15.605917015625C14.085125399999999,14.621916815625,14.4451256,13.784916915625,15.1381254,13.096917115625C15.8311253,12.408916915625,16.697125399999997,12.050917115625001,17.736125,12.023917215625C18.766125000000002,12.050917115625001,19.630125,12.396917315625,20.327126,13.061916315625ZM20.020125,16.671916015625C20.120126,16.571916015625,20.170125,16.452916115625,20.170125,16.316916515625C20.170125,16.180916815625,20.120125,16.059916515624998,20.020125,15.954916015625C19.920125,15.849916415625,19.801126,15.792916315625,19.665126,15.783916515625L17.8191261,15.783916515625L17.8191261,13.854915615625C17.8191261,13.717915515625,17.7661257,13.599915515625,17.6621265,13.499916115625C17.557127,13.399915715625,17.436126700000003,13.349916415625,17.3001261,13.349916415625C17.1641254,13.349916415625,17.0421257,13.399916615625,16.9381266,13.499916115625C16.833126999999998,13.599916415625,16.7761269,13.718915915625,16.767127000000002,13.854915615625L16.767127000000002,16.315916015625C16.7761269,16.452916115625,16.833126999999998,16.570916015625002,16.9381266,16.670916015625C17.043126100000002,16.770916015624998,17.1641264,16.825915015625,17.3001261,16.834915015625L19.665126,16.834915015625C19.801126,16.826915015624998,19.919126,16.771915015624998,20.020125,16.671916015625Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
src/static/images/icon/shengchanhesuan.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_141_34449"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_141_34776"><rect x="6" y="5" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_141_34449)"><path d="M8.6032233,27C7.2891606,27,6.33216852,28.2456321,6.67075348,29.5153255L7.2081735,31.5306506C7.6751106,33.2816644,9.260909999999999,34.5,11.0731134,34.5L16.926887999999998,34.5C18.739091000000002,34.5,20.324889,33.2816644,20.791826,31.5306506L21.329247000000002,29.5153253C21.667831,28.2456321,20.710839,27,19.396776,27L8.6032233,27Z" fill="#FC99E5" fill-opacity="1" transform="matrix(1,0,0,-1,0,54)"/><path d="M2,8.000001000000001L2,17.499999000000003C2,20.813709,4.6862917,23.5,8.000000499999999,23.5L20,23.5C23.313709,23.5,26,20.813709,26,17.5L26,8.000001000000001C26,4.6862917,23.313709,2,20,2L8.000001000000001,2C4.6862917,2,2,4.6862917,2,8.000001000000001Z" fill="#6234F9" fill-opacity="1" style="opacity:0.800000011920929;"/><g clip-path="url(#master_svg1_141_34776)"><path d="M19.536021859374998,6.0341796875L15.876568759375,6.0341796875C15.067975059375,6.0341796875,14.412474659375,6.6896797375,14.412474659375,7.4982734875L14.412474659375,11.157726287500001C14.412474659375,11.9663200875,15.067975059375,12.621820487499999,15.876568759375,12.621820487499999L19.536021859374998,12.621820487499999C20.344615859375,12.621820487499999,21.000115859375,11.9663200875,21.000115859375,11.157726287500001L21.000115859375,7.4982890875C21.000131859375,6.6896796275,20.344632859375,6.03417956829071,19.536021859374998,6.0341796875ZM19.634476859375,9.7672736875L15.778131459375,9.7672736875C15.535570159375,9.7672736875,15.338896759375,9.5706016875,15.338896759375,9.3280391875C15.338896759375,9.0854770875,15.535568259375,8.888804887500001,15.778131459375,8.888804887500001L19.634475859375,8.888804887500001C19.877036859375,8.888804887500001,20.073709859375,9.0854770875,20.073709859375,9.3280391875C20.073693859375,9.5706016875,19.877037859375,9.7672736875,19.634476859375,9.7672736875ZM19.525537859375,13.4172105875L15.873585659375,13.4172105875C15.066647559375,13.4172105875,14.412490859375,14.0713681875,14.412490859375,14.8783044875L14.412490859375,18.5303366875C14.412490859375,19.3372726875,15.066647559375,19.9914306875,15.873585659375,19.9914306875L19.525553859375,19.9914306875C20.332491859375,19.9914306875,20.986648859375002,19.3372726875,20.986648859375002,18.5303366875L20.986648859375002,14.8783197875C20.986648859375002,14.0713672875,20.332491859375,13.4172105875,19.525537859375,13.4172105875ZM17.698413859375002,14.9017886875L17.699912859375,14.9017886875C17.941976859375,14.9017886875,18.138240859375,15.0980548875,18.138240859375,15.3401164875C18.138240859375,15.5821790875,17.941976859375,15.7784452875,17.699912859375,15.7784452875C17.457850859375,15.7784452875,17.260866859375,15.5821790875,17.260866859375,15.3401164875C17.260866859375,15.0980548875,17.456350859375,14.9017886875,17.698413859375002,14.9017886875ZM17.699912859375,18.526601687499998C17.457850859375,18.526601687499998,17.260866859375,18.3303366875,17.260866859375,18.0882736875C17.260866859375,17.846211687500002,17.456350859375,17.6499456875,17.698413859375002,17.6499456875L17.699912859375,17.6499456875C17.941976859375,17.6499456875,18.138240859375,17.846211687500002,18.138240859375,18.0882736875C18.138240859375,18.3303366875,17.941976859375,18.526601687499998,17.699912859375,18.526601687499998ZM19.623787859375,17.1426626875L15.775351559375,17.1426626875C15.533288959375,17.1426626875,15.337023759375,16.9463986875,15.337023759375,16.7043356875C15.337023759375,16.4622726875,15.533288959375,16.266008687499998,15.775351559375,16.266008687499998L19.623787859375,16.266008687499998C19.865851859375,16.266008687499998,20.062116859375,16.4622726875,20.062116859375,16.7043356875C20.062116859375,16.9463986875,19.865851859375,17.1426626875,19.623787859375,17.1426626875ZM12.146208759375,6.0341796875L8.492740259375001,6.0341796875C7.685443399375,6.0341796875,7.031005859375,6.6886171675,7.031005859375,7.4959140875L7.031005859375,11.1494612875C7.031005859375,11.9567579875,7.685443339375,12.6111950875,8.492740259375001,12.6111950875L12.146209259375,12.6111950875C12.953506459375,12.6111950875,13.607944059375,11.9567579875,13.607944059375,11.1494612875L13.607944059375,7.4959140875C13.607944059375,6.6886172275,12.953506459375,6.0341796875,12.146208759375,6.0341796875ZM12.244490159375001,9.7612421875L10.757990359375,9.7612421875L10.757990359375,11.2477421875C10.757990359375,11.4899139875,10.561646659375,11.6862578875,10.319474959375,11.6862578875C10.077303159374999,11.6862578875,9.880959259375,11.4899139875,9.880959259375,11.2477421875L9.880959259375,9.7612421875L8.394459259375001,9.7612421875C8.152287459375,9.7612421875,7.955943529375,9.5648979875,7.955943529375,9.3227262875C7.955943529375,9.0805546875,8.152287259375,8.884210787499999,8.394459259375001,8.884210787499999L9.880959259375,8.884210787499999L9.880959259375,7.3977107875C9.880959259375,7.1555389875,10.077303159374999,6.9591950775,10.319474959375,6.9591950775C10.561646659375,6.9591950775,10.757990359375,7.1555387875,10.757990359375,7.3977107875L10.757990359375,8.884210787499999L12.244490159375001,8.884210787499999C12.486661959374999,8.884210787499999,12.683006259375,9.0805546875,12.683006259375,9.3227262875C12.683022059375,9.5648979875,12.486661959374999,9.7612421875,12.244490159375001,9.7612421875ZM12.154397059375,13.4631013875L8.521380759374999,13.4631013875C7.718615169375,13.4631013875,7.067833904375,14.1138667875,7.067833904375,14.9166488875L7.067833904375,18.549820687500002C7.067833904375,19.3525866875,7.718615169375,20.003366687499998,8.521380759374999,20.003366687499998L12.154397059375,20.003366687499998C12.957162859375,20.003366687499998,13.607944059375,19.3525856875,13.607944059375,18.549820687500002L13.607944059375,14.9166488875C13.607944059375,14.1138820875,12.957162859375,13.4631013875,12.154397059375,13.4631013875ZM11.999740159375001,17.7785396875C12.170084459375001,17.9488036875,12.170084459375001,18.2248996875,11.999740159375001,18.3951646875C11.914646659375,18.4803366875,11.803005659375,18.5229136875,11.691427659375,18.5229136875C11.579850159374999,18.5229136875,11.468209259375,18.4803366875,11.383115259375,18.3951646875L10.337849859375,17.3498996875L9.292584159375,18.3951646875C9.207490659375,18.4803366875,9.095849559375,18.5229136875,8.984271759375,18.5229136875C8.872693759375,18.5229136875,8.761053059375,18.4803366875,8.675959359375,18.3951646875C8.505615459375,18.2248996875,8.505615459375,17.9488036875,8.675959359375,17.7785396875L9.721224759375,16.733273687500002L8.675959359375,15.6880092875C8.505615459375,15.5177430875,8.505615459375,15.2416486875,8.675959359375,15.0713843875C8.846162559375,14.9010400875,9.122381259375,14.9010400875,9.292584659375,15.0713843875L10.337850359375,16.1166486875L11.383115759375,15.0713843875C11.553318959375,14.9010400875,11.829538359375,14.9010400875,11.999741059375001,15.0713843875C12.170084959375,15.2416486875,12.170084959375,15.5177430875,11.999741059375001,15.6880092875L10.954475159375,16.733273687500002L11.999740159375001,17.7785396875Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
src/static/images/icon/shengchanjihua.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_141_34460"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_141_34770"><rect x="6.5" y="4" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_141_34460)"><path d="M25,21.1513879C25,21.3787005,25.067284852,21.6009274,25.19337524,21.7900629L27.1094005,24.6641006C27.6657977,25.4986968,28.6024919,26,29.6055508,26L37,26C38.656855,26,40,24.656854199999998,40,23C40,21.343146,38.656855,20,37,20L26.1513879,20C25.51549393,20,25,20.51549393,25,21.1513879Z" fill="#FFDB42" fill-opacity="1" transform="matrix(-1,0,0,1,50,0)"/><rect x="2" y="2" width="25" height="21" rx="6" fill="#FF7B00" fill-opacity="1" style="opacity:0.800000011920929;"/><g clip-path="url(#master_svg1_141_34770)"><path d="M19.6875,17.8359375L9.3125,17.8359375C8.59623432,17.8359375,8.015625,17.255328499999997,8.015625,16.5390625L8.015625,9.40625L20.984375,9.40625L20.984375,16.5390625C20.984375,17.255328499999997,20.403765999999997,17.8359375,19.6875,17.8359375ZM16.4453125,11.3515625C16.4453125,10.9934998,16.154999699999998,10.703125,15.796875,10.703125C15.7324848,10.703125,13.2675157,10.703125,13.203125,10.703125C12.8450623,10.703125,12.5546875,10.9934998,12.5546875,11.3515625C12.5546875,11.7096877,12.8450623,12,13.203125,12C13.2675157,12,13.4754062,12,13.5273438,12L14.9863281,12L13.203125,15.5664063C13.203125,15.7454996,13.348312400000001,15.890625,13.5273438,15.890625L14.1757812,15.890625C14.354875100000001,15.890625,14.5,15.7454996,14.5,15.5664063L16.4453125,11.6757813C16.4453125,11.6238437,16.4453125,11.4159532,16.4453125,11.3515625ZM8.015625,8.109375C8.015625,7.3931093,8.59623432,6.8125,9.3125,6.8125L10.6747971,6.8125C10.8093438,6.4365938,11.1598282,6.1640625,11.5820312,6.1640625C12.0042343,6.1640625,12.3547029,6.4365938,12.4892654,6.8125L16.5107498,6.8125C16.6452971,6.4365938,16.9957819,6.1640625,17.417984,6.1640625C17.8401871,6.1640625,18.190656,6.4365938,18.325219,6.8125L19.6875,6.8125C20.403765999999997,6.8125,20.984375,7.3931093,20.984375,8.109375L20.984375,8.7578125L8.015625,8.7578125L8.015625,8.109375ZM17.09375,8.109375L17.7421875,8.109375L17.7421875,6.8125L17.09375,6.8125L17.09375,8.109375ZM11.2578125,8.109375L11.90625,8.109375L11.90625,6.8125L11.2578125,6.8125L11.2578125,8.109375Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
src/static/images/icon/shengchanpaichan.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_141_34407"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_141_34756"><rect x="9" y="7" width="14" height="14" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_141_34407)"><path d="M1,10L1,18C1,20.209139,2.7908611,22,5.000001,22L6.9999995,22C8.6568542,22,10,20.656855,10,19L10,9.0000005C10,7.3431458,8.6568542,6,6.9999995,6L5.0000005,6C2.7908611,6,1,7.7908611,1,10Z" fill="#7BF1FF" fill-opacity="1"/><rect x="5" y="2" width="22" height="24" rx="6" fill="#0066FF" fill-opacity="1" style="opacity:0.800000011920929;"/><g clip-path="url(#master_svg1_141_34756)"><path d="M20.666666,7L11.3333335,7C10.0422223,7,9,8.0422223,9,9.3333335L9,18.666667C9,19.957777999999998,10.0422223,21,11.3333335,21L20.666667,21L19.888889,21L19.904445000000003,20.984445C20.106667,20.984445,20.308888,20.906667,20.464444999999998,20.758889L22.782222,18.441112C22.945556,18.277779000000002,23.015556,18.052222,23,17.834443999999998L23,9.3333335C23,8.0422223,21.957777999999998,7,20.666666,7ZM12.8888891,10.1111112L19.111111,10.1111112C19.538888999999998,10.1111112,19.888889,10.461111299999999,19.888889,10.8888891C19.888889,11.3166666,19.538888999999998,11.666667,19.111111,11.666667L12.8888891,11.666667C12.461111299999999,11.666667,12.1111114,11.3166666,12.1111114,10.8888891C12.1111114,10.4611115,12.4611115,10.1111114,12.8888891,10.1111112ZM12.1111112,14C12.1111112,13.572222199999999,12.461111299999999,13.2222223,12.8888891,13.2222223L19.111111,13.2222223C19.538888999999998,13.2222223,19.888889,13.572222199999999,19.888889,14C19.888889,14.427777800000001,19.538888999999998,14.777778099999999,19.111111,14.777778099999999L12.8888891,14.777778099999999C12.461111299999999,14.777778099999999,12.1111114,14.427778199999999,12.1111112,14ZM20.666666,17.888889L19.888888,17.888889L19.888888,18.666667C19.888888,19.094445,19.538888999999998,19.444446,19.11111,19.444446C18.6833315,19.444446,18.3333321,19.094445999999998,18.3333321,18.666667L18.3333321,17.111112C18.3333321,16.6833344,18.6833315,16.333334,19.11111,16.333334L20.666666,16.333334C21.094443,16.333334,21.444444,16.683333400000002,21.444444,17.111112C21.444444,17.538890000000002,21.094444,17.88889,20.666666,17.888889Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
src/static/images/icon/shengchanshikuang.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_141_42939"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_141_42954"><rect x="6" y="5" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_141_42939)"><path d="M22,22.0000005L22,22.9999995C22,25.2091389,23.7908611,27,26,27L34,27C36.209139,27,38,25.2091389,38,22.9999995L38,22.0000005C38,20.3431458,36.656855,19,35,19L25.0000005,19C23.3431458,19,22,20.3431458,22,22.0000005Z" fill="#FDC3C1" fill-opacity="1" transform="matrix(-1,0,0,1,44,0)"/><rect x="2" y="2" width="24" height="22" rx="6" fill="#FF2B00" fill-opacity="1" style="opacity:0.800000011920929;"/><g clip-path="url(#master_svg1_141_42954)"><path d="M19.5976818125,7.975256428125C19.429908812500003,7.822927328125,19.2089708125,7.739575828125,18.980501812500002,7.739575828125C18.9596658125,7.739575828125,18.9388328125,7.739575828125,18.9180168125,7.741015028125C18.9054158125,7.742438228125,18.759929812499998,7.7511042281249996,18.5325648125,7.7511042281249996C18.1557318125,7.7511042281249996,17.4200373125,7.725233628125,16.7231331125,7.560001828125C15.823278412499999,7.345890628125,14.7847490125,6.354385998125,14.4826178125,6.158966188125C14.3303046125,6.059849266125,14.1550002125,6.010986328125,13.9793429125,6.010986328125C13.8044057125,6.010986328125,13.6290998125,6.059849266125,13.4771390125,6.157543068125C13.4405084125,6.181974528125,12.3530998125,7.3070687281249995,11.2894523125,7.560001828125C10.5929158125,7.725233928125,9.8431994125,7.7511042281249996,9.4667181125,7.7511042281249996C9.2393200125,7.7511042281249996,9.0942183125,7.742438228125,9.0798602125,7.741015028125C9.0600977525,7.739575828125,9.0403350025,7.739575828125,9.0202204625,7.739575828125C8.7913834425,7.739575828125,8.5697414925,7.822911628125,8.4012634725,7.975256428125C8.2184271925,8.140488628125,8.1142578125,8.371819928125,8.1142578125,8.614663628125001L8.1142578125,10.803070528125C8.1142578125,18.892904328125,13.5874648125,19.934664328125002,13.8194995125,19.976283328125C13.8726792125,19.984965328125,13.926195612499999,19.989251328125,13.9797263125,19.989251328125C14.0328917125,19.989251328125,14.0871253125,19.984965328125,14.1395697125,19.976283328125C14.3719735125,19.934649328124998,19.8853928125,18.892889328125,19.8853928125,10.803071028125L19.8853928125,8.614663628125001C19.8853928125,8.371819928125,19.7808728125,8.140504328125001,19.5976818125,7.975256428125ZM17.5744924125,11.104817828125L13.779988812500001,14.776156428125C13.756276612499999,14.814913728125,13.7271771125,14.853767428125,13.6919684125,14.886800728125C13.580604512499999,14.994567828125,13.4333286125,15.046293228125,13.287458412500001,15.043415028125C13.1416373125,15.046293228125,12.9946804125,14.994567828125,12.8833489125,14.886800728125C12.8481564125,14.853767428125,12.8187046125,14.814913728125,12.7949929125,14.776156428125L10.7671649125,12.813325428125001C10.5512478125,12.604953728125,10.5512478125,12.267311128125,10.7671649125,12.057500328125C10.9830506125,11.849193528125,11.3332775125,11.849193528125,11.549195812499999,12.057500328125L13.287475112500001,13.740152828125L16.7928295125,10.349056728125C17.0087471125,10.140685528125001,17.3586063125,10.140685528125001,17.5745096125,10.349056728125C17.7903948125,10.557348228125,17.7903948125,10.896494828125,17.5744924125,11.104817828125Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
src/static/images/icon/shengchanzhuisu.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_141_42929"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_141_42949"><rect x="6" y="8" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_141_42929)"><path d="M6,13.0000005L6,15.590147C6,16.842423,7.1377500000000005,17.7866721,8.368577,17.555892L14,16.5L19.631422999999998,17.555892C20.86225,17.7866721,22,16.842423,22,15.590147L22,13.0000005C22,11.3431458,20.656855,10,19,10L9.0000005,10C7.3431458,10,6,11.3431458,6,13.0000005Z" fill="#FC99E5" fill-opacity="1" transform="matrix(1,0,0,-1,0,20)"/><rect x="2" y="5" width="24" height="22" rx="6" fill="#6234F9" fill-opacity="1" style="opacity:0.800000011920929;"/><g clip-path="url(#master_svg1_141_42949)"><path d="M19.743700671875,9.612548828125L8.276513691875,9.612548828125C7.901513691875,9.612548828125,7.598388671875,9.915673848125,7.598388671875,10.290673968125L7.598388671875,21.709423828125C7.598388671875,22.082861828125,7.901513691875,22.385986828125,8.276513691875,22.385986828125L19.204639671875,22.385986828125L18.021826671875,21.204736828125C17.992138671875,21.175048828125,17.967139671875,21.143798828125,17.943701671875,21.109424828125C17.193701771875,21.618798828125,16.309327171874997,21.892236828125,15.381201771875,21.892236828125C14.159326571874999,21.892236828125,13.010889071874999,21.415673828125,12.146826771875,20.551611828125C11.282764471875,19.687549828125,10.806201971875,18.539112128124998,10.806201971875,17.317236928125C10.806201971875,16.095361728125,11.282764471875,14.946924228124999,12.146826771875,14.082861928125C13.010889071874999,13.218799828125,14.159326571874999,12.742237328125,15.381201771875,12.742237328125C16.603076971874998,12.742237328125,17.751514671875,13.218799828125,18.617138671875,14.082861928125C19.481200671875,14.946924228124999,19.957763671875,16.095361728125,19.957763671875,17.317236928125C19.957763671875,18.417237328124997,19.571825671875,19.456299828124997,18.865575671875,20.282861828125C18.879638671875,20.293799828125,18.892138671875,20.306298828125,18.906200671875,20.318798828125L20.410887671875003,21.823486828125C20.417137671875,21.785985828125,20.421825671875,21.746923828125,20.421825671875,21.707860828125L20.421825671875,10.289111378125C20.420263671875,9.915673848125,20.117137671875,9.612548828125,19.743700671875,9.612548828125ZM12.195263871875,12.225048828125C12.082763671875,12.337548928124999,11.926513671875,12.407861428124999,11.753076571874999,12.407861428124999L9.846826571874999,12.407861428124999C9.501513871875,12.407861428124999,9.221826471875,12.128174028125,9.221826471875,11.782861428124999C9.221826471875,11.610986428124999,9.292138971875,11.454736428124999,9.404638871875001,11.340673928125C9.517138871875,11.226611328125,9.673388971875,11.157861428124999,9.846826571874999,11.157861428124999L11.754639171874999,11.157861428124999C12.099951771875,11.157861428124999,12.379639171874999,11.437548828125,12.379639171874999,11.782861428124999C12.378076571874999,11.956299028125,12.309326671874999,12.112549028125,12.195263871875,12.225048828125ZM17.787451671875,15.867236128125L18.546826671875,15.867236128125C17.995263671875,14.670361528125,16.784326571875,13.837549228124999,15.382763871875,13.837549228124999C13.464014071874999,13.837549228124999,11.901514071874999,15.400049228124999,11.901514071874999,17.318799028125C11.901514071874999,19.239111928125,13.464014071874999,20.800048828125,15.382763871875,20.800048828125C16.682764071875,20.800048828125,17.818701671875,20.082860828125,18.417138671875,19.025049228125L17.787451671875,19.025049228125C17.442139671874997,19.025049228125,17.162451771875,18.745361328125,17.162451771875,18.400049228125L17.162451771875,16.492236128125C17.162451771875,16.146924028125,17.442139671874997,15.867236128125,17.787451671875,15.867236128125ZM15.045264271875,18.398486128125C15.045264271875,18.743798228125,14.765576371875,19.023486128125,14.420264271875,19.023486128125C14.248389271875,19.023486128125,14.092139271875,18.953173628125,13.978076971875,18.840673428125C13.865576771875,18.728173228125,13.795264271875,18.571923228125,13.795264271875,18.398486128125L13.795264271875,16.492236128125C13.795264271875,16.146924028125,14.074952171875001,15.867236128125,14.420264271875,15.867236128125C14.592139271875,15.867236128125,14.748389271875,15.937548628125,14.862451571874999,16.050048828125C14.974951771875,16.162549028125,15.045264271875,16.318799028125,15.045264271875,16.492236128125L15.045264271875,18.398486128125ZM16.787451771875,18.398486128125C16.787451771875,18.743798228125,16.507763871875,19.023486128125,16.162451771875,19.023486128125C15.990576771875,19.023486128125,15.834326771875,18.953173628125,15.720264471875,18.840673428125C15.606202171875,18.728173228125,15.537451771875,18.571923228125,15.537451771875,18.398486128125L15.537451771875,16.492236128125C15.537451771875,16.146924028125,15.817139671875,15.867236128125,16.162451771875,15.867236128125C16.334326771875,15.867236128125,16.490576771875,15.937548628125,16.604639071875,16.050048828125C16.717139271875,16.162549028125,16.787451771875,16.318799028125,16.787451771875,16.492236128125L16.787451771875,18.398486128125Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>