yyb
2026-05-16 4039f4dce566ffddf444bed260906acdff0b164f
Merge branch 'dev_NEW_pro' into dev_NEW_pro_鹤壁
已添加2个文件
已修改31个文件
已删除2个文件
6020 ■■■■ 文件已修改
src/api/collaborativeApproval/approvalProcess.js 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInventory.js 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/procurementLedger.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/CommonUpload.vue 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/approve.vue 812 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/detail.vue 1113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/index.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/add.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/add.vue 778 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | 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/productionManagement/processRoute/items.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionAccounting/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionOrder/pickingDetail.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionReport/index.vue 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionReporting/ledger.vue 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionTraceability/index.vue 159 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/add.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/detail.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/index.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/add.vue 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/detail.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/index.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/add.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/detail.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/index.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesAccount/goOut.vue 976 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/detail.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/edit.vue 547 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/index.vue 118 ●●●●● 补丁 | 查看 | 原始文档 | 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
@@ -1,61 +1,78 @@
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢åº“存记录列表
export const getStockInventoryListPage = (params) => {
    return request({
        url: "/stockInventory/pagestockInventory",
        method: "get",
        params,
    });
export const getStockInventoryListPage = params => {
  return request({
    url: "/stockInventory/pagestockInventory",
    method: "get",
    params,
  });
};
// åˆ†é¡µæŸ¥è¯¢è”合库存记录列表(包含商品信息)
export const getStockInventoryListPageCombined = params => {
  return request({
    url: "/stockInventory/pageListCombinedStockInventory",
    method: "get",
    params,
  });
};
// åˆ›å»ºåº“存记录
export const createStockInventory = (params) => {
    return request({
        url: "/stockInventory/addstockInventory",
        method: "post",
        data: params,
    });
export const createStockInventory = params => {
  return request({
    url: "/stockInventory/addstockInventory",
    method: "post",
    data: params,
  });
};
// å‡å°‘库存记录
export const subtractStockInventory = (params) => {
    return request({
        url: "/stockInventory/subtractStockInventory",
        method: "post",
        data: params,
    });
export const subtractStockInventory = params => {
  return request({
    url: "/stockInventory/subtractStockInventory",
    method: "post",
    data: params,
  });
};
export const getStockInventoryReportList = (params) => {
    return request({
        url: "/stockInventory/stockInventoryPage",
        method: "get",
        params,
    });
export const getStockInventoryReportList = params => {
  return request({
    url: "/stockInventory/stockInventoryPage",
    method: "get",
    params,
  });
};
export const getStockInventoryInAndOutReportList = (params) => {
    return request({
        url: "/stockInventory/stockInAndOutRecord",
        method: "get",
        params,
    });
export const getStockInventoryInAndOutReportList = params => {
  return request({
    url: "/stockInventory/stockInAndOutRecord",
    method: "get",
    params,
  });
};
// å†»ç»“库存记录
export const frozenStockInventory = (params) => {
    return request({
        url: "/stockInventory/frozenStock",
        method: "post",
        data: params,
    });
export const frozenStockInventory = params => {
  return request({
    url: "/stockInventory/frozenStock",
    method: "post",
    data: params,
  });
};
// è§£å†»åº“存记录
export const thawStockInventory = (params) => {
    return request({
        url: "/stockInventory/thawStock",
        method: "post",
        data: params,
    });
export const thawStockInventory = params => {
  return request({
    url: "/stockInventory/thawStock",
    method: "post",
    data: params,
  });
};
export const getStockInventoryByModelId = productModelId => {
  return request({
    url: "/stockInventory/getByModelId",
    method: "get",
    params: { productModelId },
  });
};
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/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"
@@ -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
@@ -77,9 +77,9 @@
                   clearable />
        </u-form-item>
        <u-form-item label="维修项目"
                     prop="maintenanceProject"
                     prop="machineryCategory"
                     border-bottom>
          <u-input v-model="form.maintenanceProject"
          <u-input v-model="form.machineryCategory"
                   placeholder="请输入维修项目"
                   clearable />
        </u-form-item>
@@ -93,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>
      <!-- æäº¤æŒ‰é’® -->
@@ -122,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,
@@ -146,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([]);
@@ -184,8 +198,9 @@
    repairTime: dayjs().format("YYYY-MM-DD"), // æŠ¥ä¿®æ—¥æœŸ
    repairName: undefined, // æŠ¥ä¿®äºº
    maintenanceName: undefined, // ç»´ä¿®äºº
    maintenanceProject: undefined, // ç»´ä¿®é¡¹ç›®
    machineryCategory: undefined, // ç»´ä¿®é¡¹ç›®
    remark: undefined, // æ•…障现象
    storageBlobDTOs: [], // å›¾ç‰‡é™„ä»¶
  });
  // æŠ¥ä¿®çŠ¶æ€é€‰é¡¹
@@ -238,8 +253,9 @@
          form.value.repairTime = dayjs(data.repairTime).format("YYYY-MM-DD");
          form.value.repairName = data.repairName;
          form.value.maintenanceName = data.maintenanceName;
          form.value.maintenanceProject = data.maintenanceProject;
          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 || "";
@@ -346,14 +362,12 @@
  };
  onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶èŽ·å–å‚æ•°
    getPageParams();
    // é¡µé¢æ˜¾ç¤ºæ—¶é€»è¾‘
  });
  onMounted(() => {
    // é¡µé¢åŠ è½½æ—¶èŽ·å–è®¾å¤‡åˆ—è¡¨å’Œå‚æ•°
    // é¡µé¢åŠ è½½æ—¶èŽ·å–è®¾å¤‡åˆ—è¡¨
    loadDeviceName();
    getPageParams();
  });
  // ç»„件卸载时清理定时器
@@ -393,7 +407,6 @@
      // å‡†å¤‡æäº¤æ•°æ®
      const submitData = { ...form.value };
      const { code } = id
        ? await editRepair({ id: id, ...submitData })
        : await addRepair(submitData);
@@ -414,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();
@@ -437,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
@@ -63,7 +63,7 @@
            </view>
            <view class="detail-row">
              <text class="detail-label">维修项目</text>
              <text class="detail-value">{{ item.maintenanceProject || '-' }}</text>
              <text class="detail-value">{{ item.machineryCategory || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">故障现象</text>
@@ -212,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,413 +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>
            <u-form-item label="保养人" prop="maintenancePerson" border-bottom>
                <u-input
                    v-model="form.maintenancePerson"
                    placeholder="请输入保养人"
                    clearable
                />
            </u-form-item>
            <u-form-item label="保养项目" prop="maintenanceProject" border-bottom>
                <u-input
                    v-model="form.maintenanceProject"
                    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>
  <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"), // è®¡åˆ’保养日期
    maintenancePerson: undefined, // ä¿å…»äºº
    maintenanceProject: undefined, // ä¿å…»é¡¹ç›®
});
  // ä½¿ç”¨ 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");
            form.value.maintenancePerson = data.maintenancePerson;
            form.value.maintenanceProject = data.maintenanceProject;
            // è®¾ç½®è®¾å¤‡åç§°æ˜¾ç¤º
            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
@@ -68,7 +68,7 @@
            </view>
            <view class="detail-row">
              <text class="detail-label">保养项目</text>
              <text class="detail-value">{{ item.maintenanceProject || '-' }}</text>
              <text class="detail-value">{{ item.machineryCategory || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">实际保养人</text>
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/productionManagement/processRoute/items.vue
@@ -63,6 +63,16 @@
                      type="success"
                      size="mini"
                      plain />
              <up-tag v-if="item.type==0"
                      text="计时"
                      type="info"
                      size="mini"
                      plain />
              <up-tag v-else
                      text="计件"
                      type="warning"
                      size="mini"
                      plain />
            </view>
          </view>
          <view class="card-footer"
src/pages/productionManagement/productionAccounting/index.vue
@@ -2,7 +2,6 @@
  <view class="production-accounting">
    <PageHeader title="生产核算"
                @back="goBack" />
    <!-- ç­›é€‰åŒºåŸŸ -->
    <view class="filter-section">
      <view class="date-type-selector">
@@ -13,7 +12,6 @@
                 lineWidth="30"
                 lineHeight="3" />
      </view>
      <view class="date-picker-bar"
            @click="showDatePicker = true">
        <view class="date-display">
@@ -27,7 +25,6 @@
                 color="#999"></up-icon>
      </view>
    </view>
    <!-- æ±‡æ€»åˆ—表 -->
    <view class="summary-section"
          v-if="!showDetail">
@@ -80,7 +77,6 @@
                  text="暂无汇总数据" />
      </view>
    </view>
    <!-- æ˜Žç»†åˆ—表 (点击汇总行后显示) -->
    <view class="detail-section"
          v-else>
@@ -91,7 +87,6 @@
                 color="#2979ff"></up-icon>
        <text class="back-text">返回汇总 ({{ currentUserName }})</text>
      </view>
      <view class="ledger-list"
            v-if="detailList.length > 0">
        <view v-for="(item, index) in detailList"
@@ -113,6 +108,14 @@
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">生产日期</text>
              <text class="detail-value">{{ item.schedulingDate || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">生产人</text>
              <text class="detail-value">{{ item.schedulingUserName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ item.productModelName }}</text>
            </view>
@@ -123,6 +126,10 @@
            <view class="detail-row">
              <text class="detail-label">生产数量</text>
              <text class="detail-value">{{ item.quantity }} {{ item.unit }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">工时(h)</text>
              <text class="detail-value">{{ item.workHour || 0 }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">工时定额</text>
@@ -143,7 +150,6 @@
                  text="暂无明细数据" />
      </view>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-datetime-picker :show="showDatePicker"
                        v-model="pickerValue"
@@ -281,7 +287,9 @@
    salesLedgerProductionAccountingListProductionDetails(params)
      .then(res => {
        const records = res.data.records || [];
        detailList.value = isLoadMore ? [...detailList.value, ...records] : records;
        detailList.value = isLoadMore
          ? [...detailList.value, ...records]
          : records;
        page1.total = res.data.total || 0;
        if (detailList.value.length >= page1.total) {
src/pages/productionManagement/productionOrder/pickingDetail.vue
@@ -1,6 +1,6 @@
<template>
  <view class="picking-detail">
    <PageHeader :title="领料详情"
    <PageHeader title="领料详情"
                @back="goBack" />
    <scroll-view scroll-y
                 class="detail-list"
src/pages/productionManagement/productionReport/index.vue
@@ -40,6 +40,15 @@
                   @click="openProducerPicker"
                   suffix-icon="arrow-down" />
        </u-form-item>
        <!-- å·¥æ—¶ -->
        <u-form-item label="工时"
                     v-if="form.type == 0"
                     prop="workHour">
          <u-input v-model="form.workHour"
                   placeholder="请输入工时"
                   type="number" />
          <text class="param-unit">h</text>
        </u-form-item>
      </view>
      <!-- åŠ¨æ€å‚æ•°åŒºåŸŸ -->
      <view class="form-section"
@@ -135,7 +144,10 @@
  import { addProductMain } from "@/api/productionManagement/productionReporting";
  import { getInfo } from "@/api/login";
  import { userListNoPageByTenantId } from "@/api/system/user";
  import { findProcessParamListOrder } from "@/api/productionManagement/productProcessRoute.js";
  import {
    findProcessParamListOrder,
    listMaterialPickingDetail,
  } from "@/api/productionManagement/productionOrder.js";
  import { getDicts } from "@/api/system/dict/data";
  import { formatDateToYMD, parseTime } from "@/utils/ruoyi";
@@ -147,13 +159,13 @@
    scrapQty: "",
    userName: "",
    workOrderId: "",
    productProcessRouteItemId: "",
    userId: "",
    schedulingUserId: "",
    reportWork: "",
    productMainId: null,
    productionOrderRoutingOperationId: "",
    productionOrderId: "",
    workHour: 0,
    type: null,
    paramGroups: {},
  });
@@ -344,12 +356,11 @@
      userId: form.value.userId,
      userName: form.value.userName,
      productionOperationTaskId: form.value.workOrderId,
      productProcessRouteItemId: form.value.productProcessRouteItemId,
      reportWork: form.value.reportWork,
      productMainId: form.value.productMainId,
      productionOrderRoutingOperationId:
        form.value.productionOrderRoutingOperationId,
      productionOrderId: form.value.productionOrderId,
      workHour: form.value.workHour,
      productionOperationParamList: productionOperationParamList,
    };
@@ -374,7 +385,7 @@
      });
  };
  onLoad(options => {
  onLoad(async options => {
    console.log(options, "options");
    if (!options.orderRow) {
      console.log("从首页跳转,无订单数据");
@@ -389,16 +400,43 @@
      const orderRow = JSON.parse(decodeURIComponent(options.orderRow));
      console.log("构造的orderRow:", orderRow);
      // å‚ç…§ PC ç«¯é€»è¾‘:未领料无法报工
      if (orderRow.productionOrderId) {
        try {
          const res = await listMaterialPickingDetail(orderRow.productionOrderId);
          const records = Array.isArray(res.data)
            ? res.data
            : res.data?.records || [];
          if (res.code === 200 && records.length === 0) {
            uni.showModal({
              title: "提示",
              content: "未领料无法报工",
              showCancel: false,
              success: () => {
                goBack();
              },
            });
            return;
          }
        } catch (error) {
          console.error("查询领料详情失败:", error);
        }
      }
      form.value.planQuantity =
        orderRow.planQuantity != null ? String(orderRow.planQuantity) : "";
      form.value.productProcessRouteItemId =
        orderRow.productProcessRouteItemId || "";
      form.value.workOrderId = orderRow.id || "";
      form.value.reportWork = orderRow.reportWork || "";
      form.value.productMainId = orderRow.productMainId || null;
      form.value.productionOrderRoutingOperationId =
        orderRow.productionOrderRoutingOperationId || "";
      form.value.productionOrderId = orderRow.productionOrderId || "";
      form.value.type = orderRow.type;
      if (orderRow.type == 0) {
        form.value.workHour = orderRow.workHour || 0;
      } else {
        form.value.workHour = 0;
      }
      getInfo().then(res => {
        form.value.userId = res.user.userId;
src/pages/productionManagement/productionReporting/ledger.vue
@@ -49,6 +49,10 @@
                <text class="detail-value highlight">{{ item.nickName || '-' }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">工时(h)</text>
                <text class="detail-value">{{ item.workHour || 0 }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">所属工序</text>
                <view class="detail-value">
                  <up-tag :text="item.process || '-'"
@@ -60,6 +64,10 @@
              <view class="detail-row">
                <text class="detail-label">工单编号</text>
                <text class="detail-value">{{ item.workOrderNo || '-' }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">销售合同号</text>
                <text class="detail-value">{{ item.salesContractNo || '-' }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">产品名称</text>
@@ -91,11 +99,11 @@
                             plain
                             text="参数详情"
                             @click="handleShowParams(item)"></up-button>
                  <up-button type="error"
                  <!-- <up-button type="error"
                             size="small"
                             plain
                             text="删除"
                             @click="handleDelete(item)"></up-button>
                             @click="handleDelete(item)"></up-button> -->
                </view>
              </view>
            </view>
@@ -120,16 +128,20 @@
                :key="idx"
                class="detail-item">
            <view class="detail-row">
              <text class="detail-label">投入产品</text>
              <text class="detail-value font-bold">{{ input.productName }}</text>
              <text class="detail-label">报工单号</text>
              <text class="detail-value">{{ input.productNo || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ input.model }}</text>
              <text class="detail-label">投入产品名称</text>
              <text class="detail-value font-bold">{{ input.productName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">投入产品型号</text>
              <text class="detail-value">{{ input.model || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">投入数量</text>
              <text class="detail-value highlight">{{ input.quantity }} {{ input.unit }}</text>
              <text class="detail-value highlight">{{ input.quantity || 0 }} {{ input.unit || '' }}</text>
            </view>
            <up-divider></up-divider>
          </view>
src/pages/productionManagement/productionTraceability/index.vue
@@ -25,45 +25,45 @@
      <!-- åŸºç¡€ä¿¡æ¯ -->
      <view class="info-card">
        <view class="card-title">基础信息</view>
        <view class="info-grid">
          <view class="info-item">
            <text class="label">生产订单号</text>
            <text class="value">{{ rowData.productionOrderDto.npsNo || '-' }}</text>
        <view class="base-info">
          <view class="info-row">
            <text class="label">生产订单号:</text>
            <text class="value">{{ rowData.productionOrderDto?.npsNo || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="label">产品名称</text>
            <text class="value">{{ rowData.productionOrderDto.productName || '-' }}</text>
          <view class="info-row">
            <text class="label">产品名称:</text>
            <text class="value">{{ rowData.productionOrderDto?.productName || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="label">产品规格</text>
            <text class="value">{{ rowData.productionOrderDto.model || '-' }}</text>
          <view class="info-row">
            <text class="label">规格型号:</text>
            <text class="value">{{ rowData.productionOrderDto?.model || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="label">计划数量</text>
            <text class="value">{{ rowData.productionOrderDto.quantity || 0 }} {{ rowData.productionOrderDto.unit || '' }}</text>
          <view class="info-row">
            <text class="label">计划数量:</text>
            <text class="value">{{ rowData.productionOrderDto?.quantity || 0 }} {{ rowData.productionOrderDto?.unit }}</text>
          </view>
          <view class="info-item">
            <text class="label">当前状态</text>
            <up-tag :text="getStatusText(rowData.productionOrderDto.status)"
                    style="width:100rpx"
                    :type="getStatusType(rowData.productionOrderDto.status)"
                    size="mini" />
          <view class="info-row">
            <text class="label">当前状态:</text>
            <view class="value">
              <up-tag :text="getStatusText(rowData.productionOrderDto?.status)"
                      :type="getStatusType(rowData.productionOrderDto?.status)"
                      size="mini" />
            </view>
          </view>
          <view class="info-item">
            <text class="label">客户名称</text>
            <text class="value">{{ rowData.productionOrderDto.customerName || '-' }}</text>
          <view class="info-row">
            <text class="label">客户名称:</text>
            <text class="value">{{ rowData.productionOrderDto?.customerName || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="label">开始日期</text>
            <text class="value">{{ formatDate(rowData.productionOrderDto.startTime) }}</text>
          <view class="info-row">
            <text class="label">开始日期:</text>
            <text class="value">{{ formatDate(rowData.productionOrderDto?.startTime) }}</text>
          </view>
          <view class="info-item full-width">
            <text class="label">完成进度</text>
            <view class="progress-container">
              <up-line-progress :percentage="formatProgress(rowData.productionOrderDto.completionStatus)"
                                :activeColor="progressColor(rowData.productionOrderDto.completionStatus)"
                                height="8"></up-line-progress>
              <text class="progress-text">{{ formatProgress(rowData.productionOrderDto.completionStatus) }}%</text>
          <view class="info-row">
            <text class="label">完成进度:</text>
            <view class="value progress-box">
              <up-line-progress :percentage="formatProgress(rowData.productionOrderDto?.completionStatus)"
                                :activeColor="progressColor(formatProgress(rowData.productionOrderDto?.completionStatus))"
                                :showText="true" />
            </view>
          </view>
        </view>
@@ -86,8 +86,16 @@
              <text class="value">{{ item.workOrder.productName }} / {{ item.workOrder.model }}</text>
            </view>
            <view class="content-row">
              <text class="label">当前工序:</text>
              <text class="value">{{ item.workOrder.operationName || '-' }}</text>
            </view>
            <view class="content-row">
              <text class="label">需求/完成:</text>
              <text class="value">{{ item.workOrder.planQuantity }} / {{ item.workOrder.completeQuantity }}</text>
            </view>
            <view class="content-row">
              <text class="label">报废数量:</text>
              <text class="value error-text">{{ item.workOrder.scrapQty || 0 }}</text>
            </view>
          </view>
          <view class="card-footer">
@@ -183,6 +191,9 @@
                class="detail-item">
            <view class="item-main">
              <view class="item-row"><text class="label">报工单号:</text><text class="value">{{ report.productNo }}</text></view>
              <view class="item-row"><text class="label">产出数量:</text><text class="value">{{ report.quantity || 0 }}</text></view>
              <view class="item-row"><text class="label">报废数量:</text><text class="value error-text">{{ report.scrapQty || 0 }}</text></view>
              <view class="item-row"><text class="label">工时(h):</text><text class="value">{{ report.workHour || 0 }}</text></view>
              <view class="item-row"><text class="label">创建人:</text><text class="value">{{ report.userName }}</text></view>
              <view class="item-row"><text class="label">创建时间:</text><text class="value">{{ formatDate(report.createTime, '{y}-{m}-{d} {h}:{i}') }}</text></view>
            </view>
@@ -215,11 +226,14 @@
          <view class="input-list-popup">
            <view v-for="(item, idx) in inputListData"
                  :key="idx"
                  class="input-item">
              <view class="input-row"><text class="label">物料名称:</text><text class="value">{{ item.materialName }}</text></view>
              <view class="input-row"><text class="label">规格型号:</text><text class="value">{{ item.model }}</text></view>
              <view class="input-row"><text class="label">投入数量:</text><text class="value">{{ item.quantity }} {{ item.unit }}</text></view>
              <view class="input-row"><text class="label">批次号:</text><text class="value">{{ item.batchNo }}</text></view>
                  class="quality-record">
              <view class="record-title">投入记录 {{ idx + 1 }}</view>
              <view class="info-grid">
                <view class="info-item"><text class="label">报工单号</text><text class="value">{{ item.productNo || '-' }}</text></view>
                <view class="info-item"><text class="label">投入数量</text><text class="value">{{ item.quantity || 0 }} {{ item.unit || '' }}</text></view>
                <view class="info-item full-width"><text class="label">投入产品名称</text><text class="value">{{ item.productName || '-' }}</text></view>
                <view class="info-item full-width"><text class="label">投入产品型号</text><text class="value">{{ item.model || '-' }}</text></view>
              </view>
            </view>
            <view v-if="!inputListData || inputListData.length === 0"
                  class="no-data-minor">{{ inputLoading ? '加载中...' : '暂无投入记录' }}</view>
@@ -253,18 +267,26 @@
                        size="mini" /></view>
              <view class="info-item"><text class="label">检验员</text><text class="value">{{ record.userName }}</text></view>
              <view class="info-item"><text class="label">数量</text><text class="value">{{ record.quantity }} {{ record.unit }}</text></view>
              <view class="info-item"><text class="label">报工单号</text><text class="value">{{ record.reportNo || '-' }}</text></view>
              <view class="info-item"><text class="label">产品名称</text><text class="value">{{ record.productName || '-' }}</text></view>
              <view class="info-item"><text class="label">规格型号</text><text class="value">{{ record.model || '-' }}</text></view>
              <view class="info-item"><text class="label">检测单位</text><text class="value">{{ record.checkCompany || '-' }}</text></view>
            </view>
            <view class="params-table">
              <view class="table-header">
                <text class="col">指标</text>
                <text class="col">单位</text>
                <text class="col">标准值</text>
                <text class="col">内控值</text>
                <text class="col">实际值</text>
              </view>
              <view v-for="(param, pIdx) in record.inspectParamList"
                    :key="pIdx"
                    class="table-row">
                <text class="col">{{ param.parameterItem }}</text>
                <text class="col">{{ param.unit || '-' }}</text>
                <text class="col">{{ param.standardValue }}</text>
                <text class="col">{{ param.controlValue || '-' }}</text>
                <text class="col"
                      :class="{ 'error-text': param.testValue != param.standardValue }">{{ param.testValue }}</text>
              </view>
@@ -440,6 +462,7 @@
      workOrder: row.workOrder || {},
      reports: (row.reportList || []).map(r => ({
        ...r.reportMain,
        ...(r.reportOutputList ? r.reportOutputList[0] : {}),
        id: r.reportMain.id,
        productionOperationParamList: r.reportParamList || [],
      })),
@@ -471,6 +494,8 @@
    qualityRecords.value = inspects.map(i => ({
      ...i.inspect,
      reportNo: i.reportNo,
      productName: row.workOrder?.productName || "-",
      model: row.workOrder?.model || "-",
      userName: i.reportMain?.userName || "-",
      inspectParamList: i.inspectParamList || [],
    }));
@@ -708,6 +733,66 @@
    }
  }
  .base-info {
    background: #fff;
    padding: 24rpx;
    border-radius: 16rpx;
    margin-bottom: 30rpx;
    .info-row {
      margin-bottom: 16rpx;
      font-size: 28rpx;
      display: flex;
      align-items: center;
      &:last-child {
        margin-bottom: 0;
      }
      .label {
        color: #999;
        min-width: 180rpx;
      }
      .value {
        color: #333;
        flex: 1;
        font-weight: 500;
        &.progress-box {
          flex: 1;
        }
      }
    }
  }
  .info-grid {
    display: flex;
    flex-wrap: wrap;
    padding: 10rpx 0;
    .info-item {
      width: 50%;
      margin-bottom: 20rpx;
      display: flex;
      flex-direction: column;
      &.full-width {
        width: 100%;
      }
      .label {
        font-size: 24rpx;
        color: #999;
        margin-bottom: 4rpx;
      }
      .value {
        font-size: 28rpx;
        color: #333;
        font-weight: 500;
      }
    }
  }
  .popup-content {
    background: #fff;
    padding: 30rpx;
src/pages/qualityManagement/finalInspection/add.vue
@@ -51,6 +51,7 @@
      </up-form-item>
      <up-form-item label="指标选择"
                    prop="testStandardId"
                    required
                    border-bottom>
        <up-input v-model="testStandardDisplay"
                  placeholder="请选择指标"
@@ -442,7 +443,7 @@
      { required: true, message: "请选择产品型号", trigger: "change" },
    ],
    testStandardId: [
      { required: false, message: "请选择指标", trigger: "change" },
      { required: true, message: "请选择指标", trigger: "change" },
    ],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
@@ -602,9 +603,9 @@
  // èŽ·å–å·¥åºåˆ—è¡¨
  const getprocessList = () => {
    list().then(res => {
      processList.value = res.data;
    });
    // list().then(res => {
    //   processList.value = res.data;
    // });
  };
  // èŽ·å–äº§å“é€‰é¡¹
@@ -691,6 +692,10 @@
        showToast("请选择产品");
        return;
      }
      if (!form.value.testStandardId) {
        showToast("请选择指标");
        return;
      }
      if (!form.value.checkResult) {
        showToast("请选择检测结果");
        return;
@@ -699,7 +704,7 @@
      loading.value = true;
      form.value.inspectType = 2;
      if (isEdit.value) {
      if (!isEdit.value) {
        tableData.value.forEach(item => {
          delete item.id;
        });
src/pages/qualityManagement/finalInspection/detail.vue
@@ -15,7 +15,8 @@
          </view>
          <text class="header-title">{{ detailData.productName || '-' }}</text>
          <view class="status-tags">
            <u-tag :type="getTagType(detailData.checkResult)"
            <u-tag v-if="detailData.checkResult"
                   :type="getTagType(detailData.checkResult)"
                   size="small"
                   class="status-tag">
              {{ detailData.checkResult || '-' }}
src/pages/qualityManagement/finalInspection/index.vue
@@ -76,7 +76,8 @@
              </view>
            </view>
            <view class="status-tags">
              <u-tag :type="getTagType(item.checkResult)"
              <u-tag v-if="item.checkResult"
                     :type="getTagType(item.checkResult)"
                     size="mini"
                     class="status-tag">
                {{ item.checkResult }}
@@ -117,29 +118,29 @@
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="action-buttons">
            <!-- <u-button type="primary"
            <u-button type="primary"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="startInspection(item)">
              ç¼–辑
            </u-button> -->
            </u-button>
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewDetail(item)">
              è¯¦æƒ…
            </u-button>
            <!-- <u-button type="success"
            <u-button type="success"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="submitInspection(item)">
              æäº¤
            </u-button> -->
            </u-button>
          </view>
          <view class="action-buttons">
            <!-- <u-button type="info"
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewFileList(item)">
@@ -151,7 +152,7 @@
                      :disabled="item.inspectState || item.checkName !== ''"
                      @click.stop="assignInspector(item)">
              åˆ†é…æ£€éªŒå‘˜
            </u-button> -->
            </u-button>
          </view>
        </view>
      </view>
@@ -163,12 +164,12 @@
    </view>
    <!-- åˆ†é¡µç»„ä»¶ -->
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <!-- <view class="fab-button"
    <view class="fab-button"
          @click="addInspection">
      <up-icon name="plus"
               size="24"
               color="#ffffff"></up-icon>
    </view> -->
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
src/pages/qualityManagement/materialInspection/add.vue
@@ -51,6 +51,7 @@
      </up-form-item>
      <up-form-item label="指标选择"
                    prop="testStandardId"
                    required
                    border-bottom>
        <up-input v-model="testStandardDisplay"
                  placeholder="请选择指标"
@@ -114,9 +115,10 @@
                    border-bottom>
        <up-input v-model="form.checkTime"
                  placeholder="请选择检测日期"
                  readonly />
                  readonly
                  @click="showDatePicker" />
        <!-- <template #right>
          <up-icon name="calendar"
          <up-icon name="arrow-right"
                   @click="showDatePicker"></up-icon>
        </template> -->
      </up-form-item>
@@ -191,11 +193,15 @@
      </up-button>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
              :start-year="2020"
              :end-year="2030"
              @confirm="confirmDate" />
    <up-popup :show="showDate"
              mode="bottom"
              @close="showDate = false">
      <up-datetime-picker :show="true"
                          v-model="pickerValue"
                          @confirm="confirmDate"
                          @cancel="showDate = false"
                          mode="date" />
    </up-popup>
    <!-- ä¾›åº”商选择 -->
    <up-action-sheet :show="showSupplierSheet"
                     :actions="supplierOptions"
@@ -337,6 +343,7 @@
  const loading = ref(false);
  // æ—¥æœŸé€‰æ‹©å™¨
  const showDate = ref(false);
  const pickerValue = ref(Date.now());
  // ä¾›åº”商选择
  const showSupplierSheet = ref(false);
  // äº§å“é€‰æ‹©
@@ -448,7 +455,7 @@
      { required: true, message: "请选择产品型号", trigger: "change" },
    ],
    testStandardId: [
      { required: false, message: "请选择指标", trigger: "change" },
      { required: true, message: "请选择指标", trigger: "change" },
    ],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
@@ -483,6 +490,7 @@
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const confirmDate = e => {
    form.value.checkTime = dayjs(e.value).format("YYYY-MM-DD");
    showDate.value = false;
  };
  // é€‰æ‹©ä¾›åº”商
@@ -697,6 +705,10 @@
        showToast("请选择产品");
        return;
      }
      if (!form.value.testStandardId) {
        showToast("请选择指标");
        return;
      }
      if (!form.value.checkResult) {
        showToast("请选择检测结果");
        return;
@@ -705,7 +717,7 @@
      loading.value = true;
      form.value.inspectType = 0;
      if (isEdit.value) {
      if (!isEdit.value) {
        tableData.value.forEach(item => {
          delete item.id;
        });
src/pages/qualityManagement/materialInspection/detail.vue
@@ -15,7 +15,8 @@
          </view>
          <text class="header-title">{{ detailData.productName || '-' }}</text>
          <view class="status-tags">
            <u-tag :type="getTagType(detailData.checkResult)"
            <u-tag v-if="detailData.checkResult"
                   :type="getTagType(detailData.checkResult)"
                   size="small"
                   class="status-tag">
              {{ detailData.checkResult || '-' }}
src/pages/qualityManagement/materialInspection/index.vue
@@ -76,7 +76,8 @@
              </view>
            </view>
            <view class="status-tags">
              <u-tag :type="getTagType(item.checkResult)"
              <u-tag v-if="item.checkResult"
                     :type="getTagType(item.checkResult)"
                     size="mini"
                     class="status-tag">
                {{ item.checkResult }}
@@ -117,29 +118,29 @@
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="action-buttons">
            <!-- <u-button type="primary"
            <u-button type="primary"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="startInspection(item)">
              ç¼–辑
            </u-button> -->
            </u-button>
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewDetail(item)">
              è¯¦æƒ…
            </u-button>
            <!-- <u-button type="success"
            <u-button type="success"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="submitInspection(item)">
              æäº¤
            </u-button> -->
            </u-button>
          </view>
          <view class="action-buttons">
            <!-- <u-button type="info"
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewFileList(item)">
@@ -151,7 +152,7 @@
                      :disabled="item.inspectState || item.checkName !== ''"
                      @click.stop="assignInspector(item)">
              åˆ†é…æ£€éªŒå‘˜
            </u-button> -->
            </u-button>
          </view>
        </view>
      </view>
@@ -163,12 +164,12 @@
    </view>
    <!-- åˆ†é¡µç»„ä»¶ -->
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <!-- <view class="fab-button"
    <view class="fab-button"
          @click="addInspection">
      <up-icon name="plus"
               size="24"
               color="#ffffff"></up-icon>
    </view> -->
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
src/pages/qualityManagement/processInspection/add.vue
@@ -51,6 +51,7 @@
      </up-form-item>
      <up-form-item label="指标选择"
                    prop="testStandardId"
                    required
                    border-bottom>
        <up-input v-model="testStandardDisplay"
                  placeholder="请选择指标"
@@ -184,11 +185,15 @@
      </up-button>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
              :start-year="2020"
              :end-year="2030"
              @confirm="confirmDate" />
    <up-popup :show="showDate"
              mode="bottom"
              @close="showDate = false">
      <up-datetime-picker :show="true"
                          v-model="pickerValue"
                          @confirm="confirmDate"
                          @cancel="showDate = false"
                          mode="date" />
    </up-popup>
    <!-- å·¥åºé€‰æ‹© -->
    <up-action-sheet :show="showprocessSheet"
                     :actions="processOptions"
@@ -313,8 +318,8 @@
    qualityInspectParamInfo,
    qualityInspectDetailByProductId,
    getQualityTestStandardParamByTestStandardId,
    list,
  } from "@/api/qualityManagement/materialInspection.js";
  import { getProcessList } from "@/api/productionManagement/processManagement.js";
  import { userListNoPage } from "@/api/system/user.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
@@ -442,7 +447,7 @@
      { required: true, message: "请选择产品型号", trigger: "change" },
    ],
    testStandardId: [
      { required: false, message: "请选择指标", trigger: "change" },
      { required: true, message: "请选择指标", trigger: "change" },
    ],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
@@ -602,8 +607,8 @@
  // èŽ·å–å·¥åºåˆ—è¡¨
  const getprocessList = () => {
    list().then(res => {
      processList.value = res.data;
    getProcessList({ size: -1, current: -1 }).then(res => {
      processList.value = res.data?.records || res.data || [];
    });
  };
@@ -691,6 +696,10 @@
        showToast("请选择产品");
        return;
      }
      if (!form.value.testStandardId) {
        showToast("请选择指标");
        return;
      }
      if (!form.value.checkResult) {
        showToast("请选择检测结果");
        return;
@@ -699,7 +708,7 @@
      loading.value = true;
      form.value.inspectType = 1;
      if (isEdit.value) {
      if (!isEdit.value) {
        tableData.value.forEach(item => {
          delete item.id;
        });
src/pages/qualityManagement/processInspection/detail.vue
@@ -15,10 +15,11 @@
          </view>
          <text class="header-title">{{ detailData.productName || '-' }}</text>
          <view class="status-tags">
            <u-tag :type="getTagType(detailData.checkResult)"
            <u-tag v-if="detailData.checkResult"
                   :type="getTagType(detailData.checkResult)"
                   size="small"
                   class="status-tag">
              {{ detailData.checkResult || '-' }}
              {{ detailData.checkResult }}
            </u-tag>
            <u-tag :type="getStateTagType(detailData.inspectState)"
                   size="small"
src/pages/qualityManagement/processInspection/index.vue
@@ -76,7 +76,8 @@
              </view>
            </view>
            <view class="status-tags">
              <u-tag :type="getTagType(item.checkResult)"
              <u-tag v-if="item.checkResult"
                     :type="getTagType(item.checkResult)"
                     size="mini"
                     class="status-tag">
                {{ item.checkResult }}
@@ -117,29 +118,29 @@
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="action-buttons">
            <!-- <u-button type="primary"
            <u-button type="primary"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="startInspection(item)">
              ç¼–辑
            </u-button> -->
            </u-button>
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewDetail(item)">
              è¯¦æƒ…
            </u-button>
            <!-- <u-button type="success"
            <u-button type="success"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="submitInspection(item)">
              æäº¤
            </u-button> -->
            </u-button>
          </view>
          <view class="action-buttons">
            <!-- <u-button type="info"
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewFileList(item)">
@@ -151,7 +152,7 @@
                      :disabled="item.inspectState || item.checkName !== ''"
                      @click.stop="assignInspector(item)">
              åˆ†é…æ£€éªŒå‘˜
            </u-button> -->
            </u-button>
          </view>
        </view>
      </view>
@@ -163,12 +164,12 @@
    </view>
    <!-- åˆ†é¡µç»„ä»¶ -->
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <!-- <view class="fab-button"
    <view class="fab-button"
          @click="addInspection">
      <up-icon name="plus"
               size="24"
               color="#ffffff"></up-icon>
    </view> -->
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
src/pages/sales/salesAccount/goOut.vue
@@ -1,657 +1,451 @@
<template>
  <view class="account-detail">
  <view class="shipment-page">
    <PageHeader title="发货"
                @back="goBack" />
    <!-- è¡¨å•区域 -->
    <u-form ref="formRef"
            @submit="submitForm"
            :rules="rules"
            :model="form"
            label-width="140rpx">
      <u-form-item prop="typeValue"
                   label="发货类型"
                   required>
        <u-input v-model="typeValue"
                 readonly
                 placeholder="请选择发货方式"
                 @click="showPicker = true" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showPicker = true"></up-icon>
        </template>
      </u-form-item>
    </u-form>
    <!-- é€‰æ‹©å™¨å¼¹çª— -->
    <up-action-sheet :show="showPicker"
                     :actions="productOptions"
                     title="发货方式"
                     @select="onConfirm"
                     @close="showPicker = false" />
    <!-- å®¡æ ¸æµç¨‹åŒºåŸŸ -->
    <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 class="form-container">
      <up-form ref="formRef"
               :model="form"
               :rules="rules"
               label-width="100"
               input-align="right"
               error-message-align="right">
        <!-- åŸºæœ¬ä¿¡æ¯ -->
        <u-cell-group title="基本信息"
                      class="form-section">
          <up-form-item label="发货方式"
                        prop="type"
                        required>
            <up-input v-model="form.type"
                      readonly
                      placeholder="请选择发货方式"
                      @click="showTypePicker = true" />
            <template #right>
              <up-icon name="arrow-right"
                       @click="showTypePicker = true"></up-icon>
            </template>
          </up-form-item>
          <block v-if="form.type === '货车'">
            <up-form-item label="发货车牌"
                          prop="shippingCarNumber"
                          required>
              <up-input v-model="form.shippingCarNumber"
                        placeholder="请输入发货车牌号"
                        clearable />
            </up-form-item>
          </block>
          <block v-if="form.type === '快递'">
            <up-form-item label="快递公司"
                          prop="expressCompany"
                          required>
              <up-input v-model="form.expressCompany"
                        placeholder="请输入快递公司"
                        clearable />
            </up-form-item>
            <up-form-item label="快递单号"
                          prop="expressNumber"
                          required>
              <up-input v-model="form.expressNumber"
                        placeholder="请输入快递单号"
                        clearable />
            </up-form-item>
          </block>
        </u-cell-group>
        <!-- æ‰¹æ¬¡é€‰æ‹© -->
        <u-cell-group title="批次选择"
                      class="form-section">
          <view class="section-header-info">
            <text class="subtitle">待发货数量: {{ goOutData.noQuantity || 0 }}</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 v-if="batchList.length === 0"
                class="empty-text">
            <text>暂无可用库存批次</text>
          </view>
          <view v-else
                class="batch-list">
            <view v-for="(item, index) in batchList"
                  :key="index"
                  class="batch-card">
              <view class="batch-header">
                <text class="batch-no">批号: {{ item.batchNo }}</text>
                <text class="batch-qty">库存: {{ getAvailableQty(item) }}</text>
              </view>
              <view class="approver-info">
                <text class="approver-name">{{ step.nickName }}</text>
              <up-divider></up-divider>
              <view class="batch-body">
                <up-form-item label="发货数量">
                  <up-input v-model="item.deliveryQuantity"
                            type="digit"
                            placeholder="0.00"
                            input-align="right"
                            @blur="onBatchQtyChange(item)" />
                </up-form-item>
              </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>
        </u-cell-group>
        <!-- å‘货图片 -->
        <u-cell-group title="发货图片"
                      class="form-section">
          <view class="upload-container">
            <up-upload :fileList="fileList"
                       @afterRead="afterRead"
                       @delete="deleteFile"
                       multiple
                       :maxCount="9"
                       width="160rpx"
                       height="160rpx" />
          </view>
        </u-cell-group>
      </up-form>
    </view>
    <!-- åº•部按钮 -->
    <view class="footer-btns">
      <u-button class="cancel-btn"
                @click="goBack">取消</u-button>
      <u-button class="save-btn"
                @click="submitForm">发货</u-button>
    </view>
    <FooterButtons confirmText="确认发货"
                   @cancel="goBack"
                   @confirm="submitForm" />
    <!-- å‘货方式选择器 -->
    <up-action-sheet :show="showTypePicker"
                     :actions="typeActions"
                     title="发货方式"
                     @select="onTypeSelect"
                     @close="showTypePicker = false" />
  </view>
</template>
<script setup>
  import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
  import config from "@/config";
  import { ref, onMounted, reactive } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import FooterButtons from "@/components/FooterButtons.vue";
  import { addShippingInfo } from "@/api/salesManagement/salesLedger";
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  import { userListNoPageByTenantId } from "@/api/system/user";
  import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory";
  import { getToken } from "@/utils/auth";
  const data = reactive({
    form: {
      approveTime: "",
      approveId: "",
      approveUser: "",
      approveUserName: "",
      approveDeptName: "",
      approveDeptId: "",
      approveReason: "",
      checkResult: "",
      tempFileIds: [],
      approverList: [], // æ–°å¢žå­—段,存储所有节点的审批人id
      startDate: "",
      endDate: "",
      location: "",
      price: "",
    },
    rules: {
      typeValue: [{ required: false, message: "请选择", trigger: "change" }],
    },
  });
  const { form, rules } = toRefs(data);
  const showPicker = ref(false);
  const productOptions = ref([
    {
      value: "货车",
      name: "货车",
    },
    {
      value: "快递",
      name: "快递",
    },
  ]);
  const operationType = ref("");
  const currentApproveStatus = ref("");
  const approverNodes = ref([]);
  const userList = ref([]);
  const formRef = ref(null);
  const approveType = ref(0);
  const goOutData = ref({});
  const batchList = ref([]);
  const fileList = ref([]);
  const showTypePicker = ref(false);
  const typeActions = [
    { name: "货车", value: "货车" },
    { name: "快递", value: "快递" },
  ];
  const form = reactive({
    type: "货车",
    shippingCarNumber: "",
    expressCompany: "",
    expressNumber: "",
  });
  const rules = {
    type: [{ required: true, message: "请选择发货方式", trigger: "change" }],
    shippingCarNumber: [
      {
        required: true,
        validator: (rule, value, callback) => {
          if (form.type === "货车" && !value) {
            return false;
          }
          return true;
        },
        message: "请输入车牌号",
        trigger: "blur",
      },
    ],
    expressCompany: [
      {
        required: true,
        validator: (rule, value, callback) => {
          if (form.type === "快递" && !value) {
            return false;
          }
          return true;
        },
        message: "请输入快递公司",
        trigger: "blur",
      },
    ],
    expressNumber: [
      {
        required: true,
        validator: (rule, value, callback) => {
          if (form.type === "快递" && !value) {
            return false;
          }
          return true;
        },
        message: "请输入快递单号",
        trigger: "blur",
      },
    ],
  };
  const formRef = ref(null);
  onMounted(async () => {
    try {
      userListNoPageByTenantId().then(res => {
        userList.value = res.data;
      });
      // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–å‘è´§è¯¦æƒ…
      goOutData.value = JSON.parse(uni.getStorageSync("goOutData"));
      console.log(goOutData.value, "goOutData.value");
      // åˆå§‹åŒ–审批流程节点,默认一个节点
      approverNodes.value = [{ id: 1, userId: null }];
      // ç›‘听联系人选择事件
      uni.$on("selectContact", handleSelectContact);
    } catch (error) {
      console.error("获取失败:", error);
    const storedData = uni.getStorageSync("goOutData");
    goOutData.value = JSON.parse(storedData || "{}");
    if (goOutData.value.productModelId) {
      loadBatches(goOutData.value.productModelId);
    }
  });
  onUnmounted(() => {
    // ç§»é™¤äº‹ä»¶ç›‘听
    uni.$off("selectContact", handleSelectContact);
  });
  const typeValue = ref("货车");
  const onConfirm = item => {
    // è®¾ç½®é€‰ä¸­çš„部门
    typeValue.value = item.name;
    showPicker.value = false;
  const loadBatches = async modelId => {
    if (!modelId) return;
    try {
      const res = await getStockInventoryByModelId(modelId);
      const rawList = Array.isArray(res?.data)
        ? res.data
        : res?.data?.records || res?.data?.rows || res || [];
      const seenIds = new Set();
      batchList.value = rawList
        .filter(item => {
          if (!item?.id || !item?.batchNo || seenIds.has(item.id)) {
            return false;
          }
          seenIds.add(item.id);
          return true;
        })
        .map(item => ({
          ...item,
          deliveryQuantity: "",
        }));
    } catch (e) {
      console.error("加载批次失败", e);
    }
  };
  const goBack = () => {
    // æ¸…除本地存储的数据
    uni.removeStorageSync("operationType");
    uni.removeStorageSync("invoiceLedgerEditRow");
    uni.removeStorageSync("approveType");
    uni.navigateBack();
  const getAvailableQty = item => {
    const quantity =
      item?.qualitity ??
      item?.quantity ??
      item?.unLockedQuantity ??
      item?.qualifiedUnLockedQuantity ??
      item?.qualifiedQuantity ??
      item?.stockQuantity;
    return quantity ?? 0;
  };
  const submitForm = () => {
    // æ£€æŸ¥æ¯ä¸ªå®¡æ‰¹æ­¥éª¤æ˜¯å¦éƒ½æœ‰å®¡æ‰¹äºº
    const hasEmptyStep = approverNodes.value.some(step => !step.nickName);
    if (hasEmptyStep) {
      showToast("请为每个审批步骤选择审批人");
  const onBatchQtyChange = item => {
    const val = parseFloat(item.deliveryQuantity);
    if (isNaN(val) || val <= 0) {
      item.deliveryQuantity = "";
      return;
    }
    formRef.value
      .validate()
      .then(valid => {
        if (valid) {
          // è¡¨å•校验通过,可以提交数据
          // æ”¶é›†æ‰€æœ‰èŠ‚ç‚¹çš„å®¡æ‰¹äººid
          console.log("approverNodes---", approverNodes.value);
          const approveUserIds = approverNodes.value
            .map(node => node.userId)
            .join(",");
          const params = {
            salesLedgerId: goOutData.value.salesLedgerId,
            salesLedgerProductId: goOutData.value.id,
            type: typeValue.value,
            approveUserIds,
          };
          console.log(params, "params");
          addShippingInfo(params).then(res => {
            showToast("发货成功");
            setTimeout(() => {
              goBack();
            }, 500);
          });
        }
      })
      .catch(error => {
        console.error("表单校验失败:", error);
        // å°è¯•获取具体的错误字段
        if (error && error.errors) {
          const firstError = error.errors[0];
          if (firstError) {
            uni.showToast({
              title: firstError.message || "表单校验失败,请检查必填项",
              icon: "none",
            });
            return;
    const available = getAvailableQty(item);
    if (val > available) {
      uni.showToast({ title: "不能超过库存数量", icon: "none" });
      item.deliveryQuantity = available.toString();
    }
    const totalToShip = Number(goOutData.value.noQuantity || 0);
    const otherBatchesTotal = batchList.value.reduce((sum, b) => {
      if (b.id === item.id) return sum;
      return sum + Number(b.deliveryQuantity || 0);
    }, 0);
    if (val + otherBatchesTotal > totalToShip) {
      uni.showToast({ title: "总数不能超过待发货数量", icon: "none" });
      item.deliveryQuantity = (totalToShip - otherBatchesTotal).toString();
    }
  };
  const onTypeSelect = item => {
    form.type = item.name;
    showTypePicker.value = false;
  };
  const afterRead = async event => {
    const { file } = event;
    const lists = [].concat(file);
    const token = getToken();
    for (let i = 0; i < lists.length; i++) {
      const item = lists[i];
      const uploadIndex = fileList.value.length;
      fileList.value.push({
        ...item,
        status: "uploading",
        message: "上传中",
      });
      uni.uploadFile({
        url: config.baseUrl + "/common/upload",
        filePath: item.url,
        name: "files",
        header: {
          Authorization: "Bearer " + token,
        },
        success: res => {
          try {
            const data = JSON.parse(res.data);
            if (data.code === 200) {
              const fileData = Array.isArray(data.data)
                ? data.data[0]
                : data.data || data;
              fileList.value[uploadIndex].status = "success";
              fileList.value[uploadIndex].message = "";
              fileList.value[uploadIndex].url = fileData.url;
              fileList.value[uploadIndex].storageBlobDTO = fileData;
            } else {
              fileList.value[uploadIndex].status = "failed";
              fileList.value[uploadIndex].message = data.msg || "上传失败";
            }
          } catch (e) {
            fileList.value[uploadIndex].status = "failed";
            fileList.value[uploadIndex].message = "解析失败";
          }
        }
        // æ˜¾ç¤ºé€šç”¨é”™è¯¯ä¿¡æ¯
        uni.showToast({
          title: "表单校验失败,请检查必填项",
          icon: "none",
        });
        },
        fail: () => {
          fileList.value[uploadIndex].status = "failed";
          fileList.value[uploadIndex].message = "网络异常";
        },
      });
    }
  };
  // å¤„理联系人选择结果
  const handleSelectContact = data => {
    const { stepIndex, contact } = data;
    // å°†é€‰ä¸­çš„联系人设置为对应审批步骤的审批人
    approverNodes.value[stepIndex].userId = contact.userId;
    approverNodes.value[stepIndex].nickName = contact.nickName;
  const deleteFile = event => {
    fileList.value.splice(event.index, 1);
  };
  const addApprover = stepIndex => {
    // è·³è½¬åˆ°è”系人选择页面
    uni.setStorageSync("stepIndex", stepIndex);
    uni.navigateTo({
      url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect",
    });
  };
  const goBack = () => uni.navigateBack();
  const addApprovalStep = () => {
    // æ·»åŠ æ–°çš„å®¡æ‰¹æ­¥éª¤
    approverNodes.value.push({ userId: null, nickName: null });
  };
  const submitForm = async () => {
    const valid = await formRef.value.validate().catch(() => false);
    if (!valid) return;
  const removeApprover = stepIndex => {
    // ç§»é™¤å®¡æ‰¹äºº
    approverNodes.value[stepIndex].userId = null;
    approverNodes.value[stepIndex].nickName = null;
  };
    const selectedBatches = batchList.value.filter(
      b => parseFloat(b.deliveryQuantity) > 0
    );
    if (selectedBatches.length === 0) {
      uni.showToast({ title: "请至少填写一个批次的发货数量", icon: "none" });
      return;
    }
  const removeApprovalStep = stepIndex => {
    // ç¡®ä¿è‡³å°‘保留一个审批步骤
    if (approverNodes.value.length > 1) {
      approverNodes.value.splice(stepIndex, 1);
    } else {
      uni.showToast({
        title: "至少需要一个审批步骤",
        icon: "none",
      });
    // Check if any file is still uploading
    if (fileList.value.some(f => f.status === "uploading")) {
      uni.showToast({ title: "请等待图片上传完成", icon: "none" });
      return;
    }
    const payload = {
      salesLedgerId: goOutData.value.salesLedgerId,
      salesLedgerProductId: goOutData.value.id,
      type: form.type,
      shippingCarNumber: form.type === "货车" ? form.shippingCarNumber : "",
      expressCompany: form.type === "快递" ? form.expressCompany : "",
      expressNumber: form.type === "快递" ? form.expressNumber : "",
      storageBlobDTOs: fileList.value
        .filter(f => f.status === "success")
        .map(f => f.storageBlobDTO),
      batchNo: selectedBatches.map(b => b.id),
      batchNoDetailList: selectedBatches.map(b => ({
        stockInventoryId: b.id,
        batchNo: b.batchNo,
        quantity: parseFloat(b.deliveryQuantity),
        productModelId: goOutData.value.productModelId,
      })),
    };
    try {
      uni.showLoading({ title: "提交中..." });
      const res = await addShippingInfo(payload);
      uni.hideLoading();
      uni.showToast({ title: "发货成功" });
      setTimeout(() => goBack(), 500);
    } catch (e) {
      uni.hideLoading();
      uni.showToast({ title: "发货失败", icon: "none" });
    }
  };
</script>
<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);
  .shipment-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100px;
  }
  .approval-header {
    margin-bottom: 16px;
  .form-container {
    padding: 12px 12px 0;
  }
  .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;
  .form-section {
    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; // ç¡®ä¿æ–‡å­—行高一致
    overflow: hidden;
    box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
  }
  .approver-item {
  .section-header-info {
    padding: 10px 18px;
    background: #f8fbff;
    display: flex;
    align-items: center;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
    border-radius: 16px;
    padding: 16px;
    justify-content: flex-end;
    .subtitle {
      font-size: 13px;
      color: #7a8599;
    }
  }
  .batch-list {
    padding: 12px;
    display: flex;
    flex-direction: column;
    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;
  }
  .footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
  .batch-card {
    background: #fff;
    border-radius: 12px;
    padding: 0 12px 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
    border: 1px solid #f0f3f7;
  }
  .batch-header {
    padding: 12px 0;
    display: flex;
    justify-content: space-around;
    justify-content: space-between;
    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;
  }
  .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;
  }
  // åŠ¨ç”»å®šä¹‰
  @keyframes pulse {
    0% {
      transform: scale(1);
      opacity: 1;
    .batch-no {
      font-size: 14px;
      font-weight: 600;
      color: #22324d;
    }
    50% {
      transform: scale(1.2);
      opacity: 0.7;
    }
    100% {
      transform: scale(1);
      opacity: 1;
    .batch-qty {
      font-size: 13px;
      color: #2979ff;
      background: #eef6ff;
      padding: 2px 8px;
      border-radius: 4px;
    }
  }
  @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;
  .empty-text {
    padding: 30px 12px;
    text-align: center;
    color: #999;
    font-size: 14px;
  }
</style>
  .upload-container {
    padding: 12px 18px;
  }
  :deep(.u-cell-group__title) {
    padding: 14px 18px 10px !important;
    font-size: 15px !important;
    font-weight: 600 !important;
    color: #22324d !important;
    background: #f8fbff !important;
  }
  :deep(.u-form-item) {
    padding: 0 18px !important;
  }
</style>
src/pages/sales/salesQuotation/detail.vue
@@ -1,7 +1,7 @@
<template>
  <view class="customer-detail-page">
    <PageHeader title="报价详情" @back="goBack" />
    <PageHeader title="报价详情"
                @back="goBack" />
    <view class="detail-content">
      <view class="section">
        <view class="section-title">基础信息</view>
@@ -44,24 +44,13 @@
          </view>
        </view>
      </view>
      <view class="section">
        <view class="section-title">审批节点</view>
        <view v-if="approverNames.length" class="info-list">
          <view v-for="(name, index) in approverNames" :key="index" class="info-item">
            <text class="info-label">审批节点 {{ index + 1 }}</text>
            <text class="info-value">{{ name }}</text>
          </view>
        </view>
        <view v-else class="empty-box">
          <text>暂无审批节点</text>
        </view>
      </view>
      <view class="section">
        <view class="section-title">产品明细</view>
        <view v-if="detailData.products && detailData.products.length > 0" class="product-list">
          <view v-for="(item, index) in detailData.products" :key="index" class="product-card">
        <view v-if="detailData.products && detailData.products.length > 0"
              class="product-list">
          <view v-for="(item, index) in detailData.products"
                :key="index"
                class="product-card">
            <view class="product-head">产品 {{ index + 1 }}</view>
            <view class="info-item">
              <text class="info-label">产品名称</text>
@@ -89,13 +78,16 @@
            </view>
          </view>
        </view>
        <view v-else class="empty-box">
        <view v-else
              class="empty-box">
          <text>暂无产品明细</text>
        </view>
      </view>
    </view>
    <FooterButtons cancelText="返回" confirmText="编辑" @cancel="goBack" @confirm="goEdit" />
    <FooterButtons cancelText="返回"
                   confirmText="编辑"
                   @cancel="goBack"
                   @confirm="goEdit" />
  </view>
</template>
@@ -108,29 +100,27 @@
  const quotationId = ref("");
  const detailData = ref({});
  const approverNames = computed(() => {
    const approverText = detailData.value.approveUserNames || detailData.value.approverNames || detailData.value.approveUserIds || "";
    if (Array.isArray(approverText)) return approverText.filter(Boolean);
    return String(approverText)
      .split(",")
      .map(item => item.trim())
      .filter(Boolean);
  });
  const goBack = () => {
    uni.navigateBack();
  };
  const goEdit = () => {
    if (!quotationId.value) return;
    uni.navigateTo({ url: `/pages/sales/salesQuotation/edit?id=${quotationId.value}` });
    uni.navigateTo({
      url: `/pages/sales/salesQuotation/edit?id=${quotationId.value}`,
    });
  };
  const formatAmount = amount => `Â¥${Number(amount || 0).toFixed(2)}`;
  const loadDetailFromStorage = () => {
    const cachedData = uni.getStorageSync("salesQuotationDetail");
    detailData.value = cachedData || {};
    if (cachedData && (cachedData.id === quotationId.value || cachedData.id === Number(quotationId.value))) {
      detailData.value = cachedData;
    } else {
      detailData.value = cachedData || {};
      console.warn("未找到对应的报价单缓存数据");
    }
  };
  onLoad(options => {
src/pages/sales/salesQuotation/edit.vue
@@ -1,173 +1,196 @@
<template>
  <view class="account-detail">
    <PageHeader :title="pageTitle" @back="goBack" />
    <PageHeader :title="pageTitle"
                @back="goBack" />
    <view class="form-container">
      <up-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="110"
        input-align="right"
        error-message-align="right"
      >
        <u-cell-group title="基础信息" class="form-section">
          <up-form-item label="客户名称" prop="customer" required>
            <up-input v-model="form.customer" placeholder="请选择客户" readonly @click="showCustomerSheet = true" />
      <up-form ref="formRef"
               :model="form"
               :rules="rules"
               label-width="110"
               input-align="right"
               error-message-align="right">
        <u-cell-group title="基础信息"
                      class="form-section">
          <up-form-item label="客户名称"
                        prop="customer"
                        required>
            <up-input v-model="form.customer"
                      placeholder="请选择客户"
                      readonly
                      @click="showCustomerSheet = true" />
            <template #right>
              <up-icon name="arrow-right" @click="showCustomerSheet = true"></up-icon>
              <up-icon name="arrow-right"
                       @click="showCustomerSheet = true"></up-icon>
            </template>
          </up-form-item>
          <up-form-item label="业务员" prop="salesperson" required>
            <up-input
              v-model="form.salesperson"
              placeholder="请选择业务员"
              readonly
              @click="showSalespersonSheet = true"
            />
          <up-form-item label="业务员"
                        prop="salesperson"
                        required>
            <up-input v-model="form.salesperson"
                      placeholder="请选择业务员"
                      readonly
                      @click="showSalespersonSheet = true" />
            <template #right>
              <up-icon name="arrow-right" @click="showSalespersonSheet = true"></up-icon>
              <up-icon name="arrow-right"
                       @click="showSalespersonSheet = true"></up-icon>
            </template>
          </up-form-item>
          <up-form-item label="报价日期" prop="quotationDate" required>
            <up-input
              v-model="form.quotationDate"
              placeholder="请选择报价日期"
              readonly
              @click="showQuotationDatePicker = true"
            />
          <up-form-item label="报价日期"
                        prop="quotationDate"
                        required>
            <up-input v-model="form.quotationDate"
                      placeholder="请选择报价日期"
                      readonly
                      @click="showQuotationDatePicker = true" />
            <template #right>
              <up-icon name="arrow-right" @click="showQuotationDatePicker = true"></up-icon>
              <up-icon name="arrow-right"
                       @click="showQuotationDatePicker = true"></up-icon>
            </template>
          </up-form-item>
          <up-form-item label="有效期至" prop="validDate" required>
            <up-input
              v-model="form.validDate"
              placeholder="请选择有效期"
              readonly
              @click="showValidDatePicker = true"
            />
          <up-form-item label="有效期至"
                        prop="validDate"
                        required>
            <up-input v-model="form.validDate"
                      placeholder="请选择有效期"
                      readonly
                      @click="showValidDatePicker = true" />
            <template #right>
              <up-icon name="arrow-right" @click="showValidDatePicker = true"></up-icon>
              <up-icon name="arrow-right"
                       @click="showValidDatePicker = true"></up-icon>
            </template>
          </up-form-item>
          <up-form-item label="付款方式" prop="paymentMethod" required>
            <up-input v-model="form.paymentMethod" placeholder="请输入付款方式" clearable />
          <up-form-item label="付款方式"
                        prop="paymentMethod"
                        required>
            <up-input v-model="form.paymentMethod"
                      placeholder="请输入付款方式"
                      clearable />
          </up-form-item>
          <up-form-item label="备注" prop="remark">
            <up-textarea v-model="form.remark" placeholder="请输入备注" auto-height />
          <up-form-item label="备注"
                        prop="remark">
            <up-textarea v-model="form.remark"
                         placeholder="请输入备注"
                         auto-height />
          </up-form-item>
        </u-cell-group>
        <u-cell-group title="审批节点" class="form-section">
        <u-cell-group title="产品信息"
                      class="form-section">
          <view class="section-tools">
            <up-button type="primary" size="small" text="新增节点" @click="addApproverNode" />
            <up-button type="primary"
                       size="small"
                       text="新增产品"
                       @click="addProduct" />
          </view>
          <view v-if="salespersonList.length === 0" class="empty-text">
            <text>暂无可选审批人,请检查用户数据</text>
          </view>
          <view class="node-list">
            <view v-for="(node, index) in approverNodes" :key="node.id" class="node-card">
              <view class="node-top">
                <text class="node-title">审批节点 {{ index + 1 }}</text>
                <up-icon
                  v-if="approverNodes.length > 1"
                  name="trash"
                  color="#ee0a24"
                  size="18"
                  @click="removeApproverNode(index)"
                ></up-icon>
              </view>
              <view class="picker-field" @click="openApproverPicker(index)">
                <up-input :model-value="node.nickName || ''" placeholder="请选择审批人" readonly disabled />
                <up-icon name="arrow-right" color="#909399" size="16"></up-icon>
              </view>
            </view>
          </view>
        </u-cell-group>
        <u-cell-group title="产品信息" class="form-section">
          <view class="section-tools">
            <up-button type="primary" size="small" text="新增产品" @click="addProduct" />
          </view>
          <view v-if="form.products.length === 0" class="empty-text">
          <view v-if="form.products.length === 0"
                class="empty-text">
            <text>暂无产品,请先添加产品</text>
          </view>
          <view v-else class="product-list">
            <view v-for="(product, index) in form.products" :key="product.uid" class="product-card">
          <view v-else
                class="product-list">
            <view v-for="(product, index) in form.products"
                  :key="product.uid"
                  class="product-card">
              <view class="product-header">
                <text class="product-title">产品 {{ index + 1 }}</text>
                <up-icon name="trash" color="#ee0a24" size="18" @click="removeProduct(index)"></up-icon>
                <up-icon name="trash"
                         color="#ee0a24"
                         size="18"
                         @click="removeProduct(index)"></up-icon>
              </view>
              <up-divider></up-divider>
              <view class="product-body">
                <up-form-item label="产品名称">
                  <up-input
                    v-model="product.product"
                    placeholder="请选择产品"
                    readonly
                    @click="openProductPicker(index)"
                  />
                  <up-input v-model="product.product"
                            placeholder="请选择产品"
                            readonly
                            @click="openProductPicker(index)" />
                  <template #right>
                    <up-icon name="arrow-right" @click="openProductPicker(index)"></up-icon>
                    <up-icon name="arrow-right"
                             @click="openProductPicker(index)"></up-icon>
                  </template>
                </up-form-item>
                <up-form-item label="规格型号">
                  <up-input
                    v-model="product.specification"
                    placeholder="请选择规格型号"
                    readonly
                    @click="openModelPicker(index)"
                  />
                  <up-input v-model="product.ProductModel"
                            placeholder="请选择规格型号"
                            readonly
                            @click="openModelPicker(index)" />
                  <template #right>
                    <up-icon name="arrow-right" @click="openModelPicker(index)"></up-icon>
                    <up-icon name="arrow-right"
                             @click="openModelPicker(index)"></up-icon>
                  </template>
                </up-form-item>
                <up-form-item label="单位">
                  <up-input v-model="product.unit" placeholder="请输入单位" clearable />
                  <up-input v-model="product.unit"
                            placeholder="请输入单位"
                            clearable />
                </up-form-item>
                <up-form-item label="数量">
                  <up-input
                    v-model="product.quantity"
                    type="number"
                    placeholder="请输入数量"
                    clearable
                    @blur="calculateAmount(product)"
                  />
                  <up-input v-model="product.quantity"
                            type="number"
                            placeholder="请输入数量"
                            clearable
                            @blur="calculateAmount(product)" />
                </up-form-item>
                <up-form-item label="单价">
                  <up-input
                    v-model="product.unitPrice"
                    type="number"
                    placeholder="请输入单价"
                    clearable
                    @blur="calculateAmount(product)"
                  />
                  <up-input v-model="product.unitPrice"
                            type="number"
                            placeholder="请输入单价"
                            clearable
                            @blur="calculateAmount(product)" />
                </up-form-item>
                <up-form-item label="金额">
                  <up-input :model-value="formatAmount(product.amount)" disabled placeholder="自动计算" />
                  <up-input :model-value="formatAmount(product.amount)"
                            disabled
                            placeholder="自动计算" />
                </up-form-item>
              </view>
            </view>
          </view>
        </u-cell-group>
        <u-cell-group title="汇总信息" class="form-section">
        <u-cell-group title="汇总信息"
                      class="form-section">
          <up-form-item label="报价总额">
            <up-input :model-value="formatAmount(totalAmount)" disabled placeholder="自动汇总" />
            <up-input :model-value="formatAmount(totalAmount)"
                      disabled
                      placeholder="自动汇总" />
          </up-form-item>
        </u-cell-group>
      </up-form>
    </view>
    <FooterButtons :loading="loading" confirmText="保存" @cancel="goBack" @confirm="handleSubmit" />
    <up-action-sheet :show="showCustomerSheet" title="选择客户" :actions="customerActions" @select="onSelectCustomer" @close="showCustomerSheet = false" />
    <up-action-sheet :show="showSalespersonSheet" title="选择业务员" :actions="salespersonActions" @select="onSelectSalesperson" @close="showSalespersonSheet = false" />
    <up-action-sheet :show="showProductSheet" title="选择产品" :actions="productActions" @select="onSelectProduct" @close="showProductSheet = false" />
    <up-action-sheet :show="showModelSheet" title="选择规格型号" :actions="modelActions" @select="onSelectModel" @close="showModelSheet = false" />
    <up-datetime-picker :show="showQuotationDatePicker" v-model="quotationDateValue" mode="date" @confirm="onQuotationDateConfirm" @cancel="showQuotationDatePicker = false" />
    <up-datetime-picker :show="showValidDatePicker" v-model="validDateValue" mode="date" @confirm="onValidDateConfirm" @cancel="showValidDatePicker = false" />
    <FooterButtons :loading="loading"
                   confirmText="保存"
                   @cancel="goBack"
                   @confirm="handleSubmit" />
    <up-action-sheet :show="showCustomerSheet"
                     title="选择客户"
                     :actions="customerActions"
                     @select="onSelectCustomer"
                     @close="showCustomerSheet = false" />
    <up-action-sheet :show="showSalespersonSheet"
                     title="选择业务员"
                     :actions="salespersonActions"
                     @select="onSelectSalesperson"
                     @close="showSalespersonSheet = false" />
    <up-action-sheet :show="showProductSheet"
                     title="选择产品"
                     :actions="productActions"
                     @select="onSelectProduct"
                     @close="showProductSheet = false" />
    <up-action-sheet :show="showModelSheet"
                     title="选择规格型号"
                     :actions="modelActions"
                     @select="onSelectModel"
                     @close="showModelSheet = false" />
    <up-datetime-picker :show="showQuotationDatePicker"
                        v-model="quotationDateValue"
                        mode="date"
                        @confirm="onQuotationDateConfirm"
                        @cancel="showQuotationDatePicker = false" />
    <up-datetime-picker :show="showValidDatePicker"
                        v-model="validDateValue"
                        mode="date"
                        @confirm="onValidDateConfirm"
                        @cancel="showValidDatePicker = false" />
  </view>
</template>
@@ -179,7 +202,11 @@
  import { formatDateToYMD } from "@/utils/ruoyi";
  import { modelList, productTreeList } from "@/api/basicData/product";
  import { userListNoPageByTenantId } from "@/api/system/user";
  import { addQuotation, getCustomerList, getQuotationDetail, updateQuotation } from "@/api/salesManagement/salesQuotation";
  import {
    addQuotation,
    getCustomerList,
    updateQuotation,
  } from "@/api/salesManagement/salesQuotation";
  const formRef = ref();
  const loading = ref(false);
@@ -199,47 +226,73 @@
  const modelActions = ref([]);
  let uidSeed = 1;
  let nextApproverId = 2;
  const form = ref({
    id: undefined,
    quotationNo: "",
    customerId: undefined,
    customer: "",
    salesperson: "",
    quotationDate: "",
    validDate: "",
    paymentMethod: "",
    status: "待审批",
    status: "草稿",
    remark: "",
    approveUserIds: "",
    products: [],
    subtotal: 0,
    freight: 0,
    otherFee: 0,
    discountRate: 0,
    discountAmount: 0,
    totalAmount: 0,
  });
  const approverNodes = ref([{ id: 1, userId: "", nickName: "" }]);
  const rules = {
    customer: [{ required: true, message: "请选择客户", trigger: "change" }],
    salesperson: [{ required: true, message: "请选择业务员", trigger: "change" }],
    quotationDate: [{ required: true, message: "请选择报价日期", trigger: "change" }],
    quotationDate: [
      { required: true, message: "请选择报价日期", trigger: "change" },
    ],
    validDate: [{ required: true, message: "请选择有效期", trigger: "change" }],
    paymentMethod: [{ required: true, message: "请输入付款方式", trigger: "blur" }],
    paymentMethod: [
      { required: true, message: "请输入付款方式", trigger: "blur" },
    ],
  };
  const pageTitle = computed(() => (quotationId.value ? "编辑报价" : "新增报价"));
  const totalAmount = computed(() =>
    Number((form.value.products || []).reduce((sum, item) => sum + Number(item.amount || 0), 0).toFixed(2))
    Number(
      (form.value.products || [])
        .reduce((sum, item) => sum + Number(item.amount || 0), 0)
        .toFixed(2)
    )
  );
  const customerActions = computed(() => customerList.value.map(item => ({ name: item.customerName, value: item.customerName })));
  const salespersonActions = computed(() => salespersonList.value.map(item => ({ name: item.nickName, value: item.nickName })));
  const productActions = computed(() => productList.value.map(item => ({ name: item.label, value: item.value, label: item.label })));
  const customerActions = computed(() =>
    customerList.value.map(item => ({
      name: item.customerName,
      value: item.id,
    }))
  );
  const salespersonActions = computed(() =>
    salespersonList.value.map(item => ({
      name: item.nickName,
      value: item.nickName,
    }))
  );
  const productActions = computed(() =>
    productList.value.map(item => ({
      name: item.label,
      value: item.value,
      label: item.label,
    }))
  );
  const createEmptyProduct = () => ({
    uid: `p_${uidSeed++}`,
    productId: "",
    product: "",
    specificationId: "",
    specification: "",
    productModelId: "",
    ProductModel: "",
    unit: "",
    quantity: 1,
    unitPrice: 0,
@@ -254,7 +307,10 @@
        if (item.children && item.children.length) {
          walk(item.children);
        } else {
          result.push({ label: item.label || item.productName || "", value: item.id || item.value });
          result.push({
            label: item.label || item.productName || "",
            value: item.id || item.value,
          });
        }
      });
    };
@@ -266,18 +322,12 @@
  const goBack = () => uni.navigateBack();
  const calculateAmount = product => {
    product.amount = Number((Number(product.quantity || 0) * Number(product.unitPrice || 0)).toFixed(2));
    product.amount = Number(
      (Number(product.quantity || 0) * Number(product.unitPrice || 0)).toFixed(2)
    );
    form.value.totalAmount = totalAmount.value;
  };
  const addApproverNode = () => approverNodes.value.push({ id: nextApproverId++, userId: "", nickName: "" });
  const removeApproverNode = index => approverNodes.value.splice(index, 1);
  const openApproverPicker = index => {
    uni.setStorageSync("stepIndex", index);
    uni.navigateTo({
      url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect",
    });
  };
  const addProduct = () => form.value.products.push(createEmptyProduct());
  const removeProduct = index => {
    form.value.products.splice(index, 1);
@@ -285,8 +335,14 @@
  };
  const fetchModelOptions = async (productId, product) => {
    const rows = await modelList({ id: productId }).catch(() => []);
    product.modelOptions = Array.isArray(rows) ? rows : [];
    try {
      const res = await modelList({ id: productId });
      const rows = res?.data?.records || res?.data || res?.records || res || [];
      product.modelOptions = Array.isArray(rows) ? rows : [];
    } catch (error) {
      console.error("获取规格型号失败:", error);
      product.modelOptions = [];
    }
  };
  const openProductPicker = index => {
@@ -300,7 +356,11 @@
      uni.showToast({ title: "请先选择产品", icon: "none" });
      return;
    }
    modelActions.value = (current.modelOptions || []).map(item => ({ name: item.model, value: item.id, unit: item.unit }));
    modelActions.value = (current.modelOptions || []).map(item => ({
      name: item.model || item.specification,
      value: item.id,
      unit: item.unit,
    }));
    if (!modelActions.value.length) {
      uni.showToast({ title: "暂无规格型号", icon: "none" });
      return;
@@ -309,27 +369,21 @@
  };
  const onSelectCustomer = action => {
    form.value.customer = action.value;
    form.value.customerId = action.value;
    form.value.customer = action.name;
    showCustomerSheet.value = false;
  };
  const onSelectSalesperson = action => {
    form.value.salesperson = action.value;
    showSalespersonSheet.value = false;
  };
  const onSelectApprover = data => {
    const { stepIndex, contact } = data || {};
    if (stepIndex === undefined || !contact) return;
    if (!approverNodes.value[stepIndex]) return;
    approverNodes.value[stepIndex].userId = contact.userId;
    approverNodes.value[stepIndex].nickName = contact.nickName;
  };
  const onSelectProduct = action => {
    const current = form.value.products[currentProductIndex.value];
    if (!current) return;
    current.productId = action.value;
    current.product = action.label;
    current.specificationId = "";
    current.specification = "";
    current.productModelId = "";
    current.ProductModel = "";
    current.unit = "";
    current.modelOptions = [];
    showProductSheet.value = false;
@@ -338,8 +392,8 @@
  const onSelectModel = action => {
    const current = form.value.products[currentProductIndex.value];
    if (!current) return;
    current.specificationId = action.value;
    current.specification = action.name;
    current.productModelId = action.value;
    current.ProductModel = action.name;
    current.unit = action.unit || current.unit;
    showModelSheet.value = false;
  };
@@ -353,70 +407,114 @@
  };
  const fetchBaseOptions = async () => {
      const [customers, users, productTree] = await Promise.all([
        getCustomerList({ current: -1, size: -1 }).catch(() => ({})),
        userListNoPageByTenantId().catch(() => ({})),
        productTreeList().catch(() => []),
      ]);
    const [customers, users, productTree] = await Promise.all([
      getCustomerList({ current: -1, size: -1 }).catch(() => ({})),
      userListNoPageByTenantId().catch(() => ({})),
      productTreeList().catch(() => []),
    ]);
    customerList.value = customers?.data?.records || customers?.records || [];
    const userRows = users?.data || [];
    salespersonList.value = Array.isArray(userRows) ? userRows : [];
    productList.value = flattenProductTree(Array.isArray(productTree) ? productTree : productTree?.data || []);
    productList.value = flattenProductTree(
      Array.isArray(productTree) ? productTree : productTree?.data || []
    );
  };
  // æ ¹æ®åç§°åæŸ¥èŠ‚ç‚¹ id,便于仅存名称时的反显
  const findNodeIdByLabel = (nodes, label) => {
    if (!label) return null;
    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i];
      if (node.label === label) return node.value;
      if (node.children && node.children.length > 0) {
        const found = findNodeIdByLabel(node.children, label);
        if (found !== null && found !== undefined) return found;
      }
    }
    return null;
  };
  const normalizeProductRows = async rows => {
    const normalized = await Promise.all((Array.isArray(rows) ? rows : []).map(async item => {
      const row = {
        uid: `p_${uidSeed++}`,
        productId: item.productId || "",
        product: item.product || item.productName || "",
        specificationId: item.specificationId || "",
        specification: item.specification || "",
        unit: item.unit || "",
        quantity: Number(item.quantity || 1),
        unitPrice: Number(item.unitPrice || 0),
        amount: Number(item.amount || 0),
        modelOptions: [],
      };
      if (row.productId) await fetchModelOptions(row.productId, row);
      return row;
    }));
    const normalized = await Promise.all(
      (Array.isArray(rows) ? rows : []).map(async item => {
        const productName = item.product || item.productName || "";
        // ä¼˜å…ˆç”¨ productId;如果只有名称,尝试反查 id ä»¥ä¾¿é€‰æ‹©å™¨åæ˜¾
        let resolvedProductId =
          item.productId ||
          findNodeIdByLabel(productList.value, productName) ||
          "";
        const row = {
          uid: `p_${uidSeed++}`,
          productId: resolvedProductId,
          product: productName,
          productModelId: item.productModelId || "",
          ProductModel: item.ProductModel || item.specification || "",
          unit: item.unit || "",
          quantity: Number(item.quantity || 1),
          unitPrice: Number(item.unitPrice || 0),
          amount: Number(item.amount || 0),
          modelOptions: [],
        };
        if (row.productId) {
          await fetchModelOptions(row.productId, row);
          // å¦‚果没有 productModelId ä½†æœ‰ ProductModel åç§°ï¼Œå°è¯•从 modelOptions ä¸­åŒ¹é… ID
          if (!row.productModelId && row.ProductModel) {
            const foundModel = row.modelOptions.find(
              m =>
                m.model === row.ProductModel ||
                m.specification === row.ProductModel
            );
            if (foundModel) {
              row.productModelId = foundModel.id;
              // ç»Ÿä¸€ä½¿ç”¨ modelOptions ä¸­çš„字段
              row.ProductModel =
                foundModel.model || foundModel.specification || row.ProductModel;
              row.unit = foundModel.unit || row.unit;
            }
          }
        }
        return row;
      })
    );
    form.value.products = normalized;
  };
  const loadDetail = async () => {
    if (!quotationId.value) return;
    uni.showLoading({ title: "加载中...", mask: true });
    try {
      const res = await getQuotationDetail({ id: quotationId.value });
      const data = res?.data || {};
    // ç›´æŽ¥ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–æ•°æ®ï¼Œä¸å†è°ƒç”¨è¯¦æƒ…æŽ¥å£
    const cachedData = uni.getStorageSync("salesQuotationDetail");
    if (
      cachedData &&
      (cachedData.id === quotationId.value ||
        cachedData.id === Number(quotationId.value))
    ) {
      const data = cachedData;
      form.value = {
        ...form.value,
        id: data.id,
        quotationNo: data.quotationNo || "",
        customerId: data.customerId,
        customer: data.customer || "",
        salesperson: data.salesperson || "",
        quotationDate: data.quotationDate || "",
        validDate: data.validDate || "",
        paymentMethod: data.paymentMethod || "",
        status: data.status || "待审批",
        status: data.status || "草稿",
        remark: data.remark || "",
        subtotal: data.subtotal || 0,
        freight: data.freight || 0,
        otherFee: data.otherFee || 0,
        discountRate: data.discountRate || 0,
        discountAmount: data.discountAmount || 0,
        totalAmount: data.totalAmount || 0,
      };
      await normalizeProductRows(data.products || []);
      if (data.approveUserIds) {
        const ids = String(data.approveUserIds).split(",").map(item => item.trim()).filter(Boolean);
        approverNodes.value = ids.map((userId, index) => ({
          id: index + 1,
          userId,
          nickName: salespersonList.value.find(item => String(item.userId) === String(userId))?.nickName || "",
        }));
        nextApproverId = approverNodes.value.length + 1;
      }
      form.value.totalAmount = totalAmount.value;
    } catch {
      uni.showToast({ title: "获取详情失败", icon: "error" });
    } finally {
      uni.hideLoading();
    } else {
      console.warn("未找到缓存的报价单详情数据");
    }
  };
@@ -425,16 +523,16 @@
      uni.showToast({ title: "请至少添加一个产品", icon: "none" });
      return false;
    }
    const invalid = form.value.products.some(item => !item.productId || !item.specificationId || !item.unit || !Number(item.quantity) || !Number(item.unitPrice));
    const invalid = form.value.products.some(
      item =>
        !item.productId ||
        !item.productModelId ||
        !item.unit ||
        !Number(item.quantity) ||
        !Number(item.unitPrice)
    );
    if (invalid) {
      uni.showToast({ title: "请完善产品信息", icon: "none" });
      return false;
    }
    return true;
  };
  const validateApprovers = () => {
    if (approverNodes.value.some(item => !item.userId)) {
      uni.showToast({ title: "请选择审批人", icon: "none" });
      return false;
    }
    return true;
@@ -442,17 +540,20 @@
  const handleSubmit = async () => {
    const valid = await formRef.value.validate().catch(() => false);
    if (!valid || !validateApprovers() || !validateProducts()) return;
    if (!valid || !validateProducts()) return;
    loading.value = true;
    // åŒæ­¥æœ€æ–°çš„æ€»é¢
    form.value.totalAmount = totalAmount.value;
    form.value.subtotal = totalAmount.value;
    const payload = {
      ...form.value,
      approveUserIds: approverNodes.value.map(item => item.userId).join(","),
      totalAmount: totalAmount.value,
      products: form.value.products.map(item => ({
        productId: item.productId,
        product: item.product,
        specificationId: item.specificationId,
        specification: item.specification,
        productModelId: item.productModelId,
        ProductModel: item.ProductModel,
        quantity: Number(item.quantity || 0),
        unit: item.unit,
        unitPrice: Number(item.unitPrice || 0),
@@ -486,16 +587,12 @@
  onMounted(async () => {
    await fetchBaseOptions();
    uni.$on("selectContact", onSelectApprover);
    if (quotationId.value) {
      await loadDetail();
    }
  });
  onUnmounted(() => {
    uni.$off("selectContact", onSelectApprover);
    uni.removeStorageSync("stepIndex");
  });
  onUnmounted(() => {});
</script>
<style scoped lang="scss">
@@ -547,7 +644,6 @@
    padding: 12px 12px 0;
  }
  .node-list,
  .product-list {
    padding: 12px;
    display: flex;
@@ -555,31 +651,12 @@
    gap: 12px;
  }
  .node-card {
    background: #f8fbff;
    border-radius: 12px;
    padding: 12px;
    border: 1px solid #e6eef8;
  }
  .picker-field {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .picker-field :deep(.u-input) {
    flex: 1;
  }
  .node-top,
  .product-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .node-title,
  .product-title {
    font-size: 14px;
    font-weight: 600;
src/pages/sales/salesQuotation/index.vue
@@ -1,47 +1,50 @@
<template>
  <view class="sales-account">
    <PageHeader title="销售报价" @back="goBack" />
    <PageHeader title="销售报价"
                @back="goBack" />
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input
            class="search-text"
            v-model="quotationNo"
            placeholder="请输入报价单号搜索"
            clearable
            @change="getList"
          />
          <up-input class="search-text"
                    v-model="quotationNo"
                    placeholder="请输入报价单号搜索"
                    clearable
                    @change="getList" />
        </view>
        <view class="filter-button" @click="getList">
          <up-icon name="search" size="24" color="#999"></up-icon>
        <view class="filter-button"
              @click="getList">
          <up-icon name="search"
                   size="24"
                   color="#999"></up-icon>
        </view>
      </view>
    </view>
    <view class="tabs-section">
      <up-tabs
        v-model="tabValue"
        :list="tabList"
        itemStyle="width: 20%;height: 80rpx;"
        @change="onTabChange"
      />
      <up-tabs v-model="tabValue"
               :list="tabList"
               itemStyle="width: 20%;height: 80rpx;"
               @change="onTabChange" />
    </view>
    <view v-if="quotationList.length > 0" class="ledger-list">
      <view v-for="item in quotationList" :key="item.id" class="ledger-item">
    <view v-if="quotationList.length > 0"
          class="ledger-list">
      <view v-for="item in quotationList"
            :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>
              <up-icon name="file-text"
                       size="16"
                       color="#ffffff"></up-icon>
            </view>
            <text class="item-id">{{ item.quotationNo || "-" }}</text>
          </view>
          <text class="item-index">{{ item.status || "-" }}</text>
          <up-tag :text="item.status || '待审批'"
                  :type="getStatusType(item.status)"
                  size="mini"
                  shape="circle" />
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">客户名称</text>
@@ -60,43 +63,45 @@
            <text class="detail-value">{{ item.validDate || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">付款方式</text>
            <text class="detail-value">{{ item.paymentMethod || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">报价金额</text>
            <text class="detail-value highlight">{{ formatAmount(item.totalAmount) }}</text>
          </view>
          <view class="detail-row">
          <view class="detail-row"
                v-if="item.remark">
            <text class="detail-label">备注</text>
            <text class="detail-value">{{ item.remark || "-" }}</text>
            <text class="detail-value">{{ item.remark }}</text>
          </view>
        </view>
        <view class="action-buttons">
                    <up-button
                        class="action-btn"
                size="small"
                type="primary"
                :disabled="!canEdit(item)"
                @click="goEdit(item)"
                    >
                        ç¼–辑
                    </up-button>
          <up-button class="action-btn" size="small" @click="goDetail(item)">详情</up-button>
          <up-button class="action-btn" size="small" type="error" plain @click="handleDelete(item)">
          <up-button class="action-btn"
                     size="small"
                     type="primary"
                     :disabled="!canEdit(item)"
                     @click="goEdit(item)">
            ç¼–辑
          </up-button>
          <up-button class="action-btn"
                     size="small"
                     @click="goDetail(item)">详情</up-button>
          <up-button class="action-btn"
                     size="small"
                     type="error"
                     plain
                     @click="handleDelete(item)">
            åˆ é™¤
          </up-button>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
    <view v-else
          class="no-data">
      <text>暂无销售报价数据</text>
    </view>
    <view class="fab-button" @click="goAdd">
      <up-icon name="plus" size="28" color="#ffffff"></up-icon>
    <view class="fab-button"
          @click="goAdd">
      <up-icon name="plus"
               size="28"
               color="#ffffff"></up-icon>
    </view>
  </view>
</template>
@@ -105,7 +110,10 @@
  import { reactive, ref } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import { deleteQuotation, getQuotationList } from "@/api/salesManagement/salesQuotation";
  import {
    deleteQuotation,
    getQuotationList,
  } from "@/api/salesManagement/salesQuotation";
  const quotationNo = ref("");
  const quotationList = ref([]);
@@ -129,11 +137,13 @@
  };
  const goAdd = () => {
    uni.removeStorageSync("salesQuotationDetail");
    uni.navigateTo({ url: "/pages/sales/salesQuotation/edit" });
  };
  const goEdit = item => {
    if (!canEdit(item)) return;
    uni.setStorageSync("salesQuotationDetail", item || {});
    uni.navigateTo({ url: `/pages/sales/salesQuotation/edit?id=${item.id}` });
  };
@@ -159,6 +169,16 @@
    return `Â¥${num.toFixed(2)}`;
  };
  const getStatusType = status => {
    const statusMap = {
      å¾…审批: "info",
      å®¡æ ¸ä¸­: "primary",
      é€šè¿‡: "success",
      æ‹’绝: "danger",
    };
    return statusMap[status] || "info";
  };
  const getList = () => {
    uni.showLoading({ title: "加载中...", mask: true });
    getQuotationList({