zhangwencui
2026-09-17 f670b79094c95d791fc43c8fc8b5858ebf8426dc
src/pages/cooperativeOffice/collaborativeApproval/detail.vue
@@ -8,39 +8,38 @@
            :rules="rules"
            :model="form"
            label-width="140rpx">
      <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-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"
                 :disabled="isDetailMode"
                 :placeholder="approveType === 5 ? '请输入采购事由' : '请输入申请事由'"
                 show-word-limit />
      </u-form-item>
      <u-form-item prop="approveDeptName"
                   label="申请部门"
                   required>
        <u-input v-model="form.approveDeptName"
                 readonly
                 :disabled="isDetailMode"
                 placeholder="请选择申请部门"
                 @click="isDetailMode || (showPicker = true)" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="isDetailMode || (showPicker = true)"></up-icon>
        </template>
      </u-form-item>
      <!-- <u-form-item prop="approveUser"
                   label="申请人"
                   required>
        <u-input v-model="form.approveUserName"
@@ -59,228 +58,91 @@
                   @click="showDatePicker"></up-icon>
        </template>
      </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>
      <!-- approveType=2 请假相关字段 -->
      <u-form-item v-if="approveType === 2"
                   prop="startDate"
                   label="开始时间"
                   required>
        <u-input v-model="form.startDate"
                 readonly
                 :disabled="isDetailMode"
                 placeholder="请假开始时间"
                 @click="showStartDatePicker" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showStartDatePicker"></up-icon>
        </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 />
        </u-form-item>
      </template>
      <!-- 报价审批详情 -->
      <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>
      <u-form-item v-if="approveType === 2"
                   prop="endDate"
                   label="结束时间"
                   required>
        <u-input v-model="form.endDate"
                 readonly
                 :disabled="isDetailMode"
                 placeholder="请假结束时间"
                 @click="showEndDatePicker" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showEndDatePicker"></up-icon>
        </template>
      </u-form-item>
      <!-- approveType=3 出差相关字段 -->
      <u-form-item v-if="approveType === 3"
                   prop="startDateTime"
                   label="出差开始时间"
                   required>
        <u-input v-model="form.startDateTime"
                 readonly
                 :disabled="isDetailMode"
                 placeholder="请选择开始时间"
                 @click="showStartDateTimePicker" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showStartDateTimePicker"></up-icon>
        </template>
      </u-form-item>
      <u-form-item v-if="approveType === 3"
                   prop="endDateTime"
                   label="出差结束时间"
                   required>
        <u-input v-model="form.endDateTime"
                 readonly
                 :disabled="isDetailMode"
                 placeholder="请选择结束时间"
                 @click="showEndDateTimePicker" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showEndDateTimePicker"></up-icon>
        </template>
      </u-form-item>
      <u-form-item v-if="approveType === 3"
                   prop="location"
                   label="出差地点"
                   required>
        <u-input v-model="form.location"
                 :disabled="isDetailMode"
                 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"
                 :disabled="isDetailMode"
                 placeholder="请输入报销金额"
                 clearable />
      </u-form-item>
      <!-- 采购/报价/发货审批详情 -->
      <ApprovalBusinessDetail :approve-type="approveType"
                              :business-no="form.approveReason" />
      <!-- 审批流程(仅详情模式展示,与 Web 一致) -->
      <ApproveFlowSteps v-if="isDetailMode"
                        :steps="approvalSteps"
                        show-pending-opinion />
      <u-form-item v-if="operationType !== 'detail'"
                   label="图片附件"
                   prop="storageBlobDTOS"
@@ -289,7 +151,7 @@
      </u-form-item>
    </u-form>
    <!-- 选择器弹窗 -->
    <template v-if="operationType !== 'detail'">
    <view v-if="operationType !== 'detail'">
      <up-action-sheet :show="showPicker"
                       :actions="productOptions"
                       title="选择部门"
@@ -325,7 +187,27 @@
                            @cancel="showEndDate = false"
                            mode="date" />
      </up-popup>
    </template>
      <!-- 出差开始时间选择器 -->
      <up-popup :show="showStartDateTime"
                mode="bottom"
                @close="showStartDateTime = false">
        <up-datetime-picker :show="true"
                            v-model="startDateTimeValue"
                            @confirm="onStartDateTimeConfirm"
                            @cancel="showStartDateTime = false"
                            mode="datetime" />
      </up-popup>
      <!-- 出差结束时间选择器 -->
      <up-popup :show="showEndDateTime"
                mode="bottom"
                @close="showEndDateTime = false">
        <up-datetime-picker :show="true"
                            v-model="endDateTimeValue"
                            @confirm="onEndDateTimeConfirm"
                            @cancel="showEndDateTime = false"
                            mode="datetime" />
      </up-popup>
    </view>
    <!-- 底部按钮 -->
    <view class="footer-btns"
          v-if="operationType !== 'detail'">
@@ -338,28 +220,21 @@
</template>
<script setup>
  import {
    ref,
    onMounted,
    onUnmounted,
    reactive,
    toRefs,
    computed,
    watch,
  } from "vue";
  import { ref, onMounted, onUnmounted, reactive, toRefs, computed } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import CommonUpload from "@/components/CommonUpload.vue";
  import ApprovalBusinessDetail from "./_components/ApprovalBusinessDetail.vue";
  import ApproveFlowSteps from "./_components/ApproveFlowSteps.vue";
  import { mapApprovalSteps } from "./_utils/approveFlowSteps.js";
  import useUserStore from "@/store/modules/user";
  import { formatDateToYMD } from "@/utils/ruoyi";
  import { formatDateToYMD, parseTime } from "@/utils/ruoyi";
  import {
    getDept,
    approveProcessGetInfo,
    approveProcessAdd,
    approveProcessUpdate,
    getDeliveryDetailByShippingNo,
    approveProcessDetails,
  } from "@/api/collaborativeApproval/approvalProcess";
  import { getQuotationList } from "@/api/salesManagement/salesQuotation";
  import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger";
  const showToast = message => {
    uni.showToast({
      title: message,
@@ -380,6 +255,8 @@
      storageBlobDTOS: [],
      startDate: "",
      endDate: "",
      startDateTime: "",
      endDateTime: "",
      location: "",
      price: "",
    },
@@ -395,6 +272,12 @@
      endDate: [
        { required: false, message: "请选择结束时间", trigger: "change" },
      ],
      startDateTime: [
        { required: false, message: "请选择出差开始时间", trigger: "change" },
      ],
      endDateTime: [
        { required: false, message: "请选择出差结束时间", trigger: "change" },
      ],
      location: [{ required: false, message: "请输入出差地点", trigger: "blur" }],
      price: [{ required: false, message: "请输入报销金额", trigger: "blur" }],
    },
@@ -404,6 +287,21 @@
  const showPicker = ref(false);
  const productOptions = ref([]);
  const operationType = ref("");
  const isDetailMode = computed(() => operationType.value === "detail");
  /** 审批流程节点(仅详情模式展示) */
  const approvalSteps = ref([]);
  const loadApprovalSteps = approveIdValue => {
    if (!approveIdValue) return;
    approveProcessDetails(approveIdValue)
      .then(res => {
        approvalSteps.value = mapApprovalSteps(res?.data);
      })
      .catch(() => {
        approvalSteps.value = [];
      });
  };
  const currentApproveStatus = ref("");
  const formRef = ref(null);
  const message = ref("");
@@ -413,21 +311,12 @@
  const startDateValue = ref(Date.now());
  const showEndDate = ref(false);
  const endDateValue = ref(Date.now());
  const showStartDateTime = ref(false);
  const startDateTimeValue = ref(Date.now());
  const showEndDateTime = ref(false);
  const endDateTimeValue = 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 => {
@@ -446,6 +335,7 @@
  // 显示日期选择器
  const showDatePicker = () => {
    if (isDetailMode.value) return;
    showDate.value = true;
  };
@@ -458,6 +348,7 @@
  // 显示请假开始时间选择器
  const showStartDatePicker = () => {
    if (isDetailMode.value) return;
    showStartDate.value = true;
  };
@@ -468,6 +359,7 @@
  };
  const showEndDatePicker = () => {
    if (isDetailMode.value) return;
    showEndDate.value = true;
  };
@@ -477,88 +369,38 @@
    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;
          });
      }
    }
  // 显示出差开始时间选择器(YYYY-MM-DD HH:mm,与 Web 一致)
  const showStartDateTimePicker = () => {
    if (isDetailMode.value) return;
    startDateTimeValue.value = parseTime(
      form.value.startDateTime,
      "{y}-{m}-{d} {h}:{i}"
    )
      ? new Date(form.value.startDateTime).getTime()
      : Date.now();
    showStartDateTime.value = true;
  };
  // 监听审批事由变化,如果是特定审批类型则尝试获取详情
  watch(
    () => form.value.approveReason,
    newVal => {
      if (isInitialLoading.value) return;
      if (
        newVal &&
        (isQuotationApproval.value ||
          isPurchaseApproval.value ||
          isDeliveryApproval.value)
      ) {
        // 延迟一会再请求,避免输入过程中频繁触发
        debounceFetchDetail();
      }
    }
  );
  const onStartDateTimeConfirm = e => {
    form.value.startDateTime = parseTime(e.value, "{y}-{m}-{d} {h}:{i}");
    showStartDateTime.value = false;
  };
  let timer = null;
  const debounceFetchDetail = () => {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fetchDetailData(form.value);
    }, 800);
  // 显示出差结束时间选择器
  const showEndDateTimePicker = () => {
    if (isDetailMode.value) return;
    endDateTimeValue.value = parseTime(
      form.value.endDateTime,
      "{y}-{m}-{d} {h}:{i}"
    )
      ? new Date(form.value.endDateTime).getTime()
      : Date.now();
    showEndDateTime.value = true;
  };
  const onEndDateTimeConfirm = e => {
    form.value.endDateTime = parseTime(e.value, "{y}-{m}-{d} {h}:{i}");
    showEndDateTime.value = false;
  };
  onMounted(async () => {
@@ -579,9 +421,13 @@
          const row = JSON.parse(storedData);
          currentApproveStatus.value = row.approveStatus;
          isInitialLoading.value = true;
          approveProcessGetInfo({ id: row.approveId, approveReason: "1" })
            .then(res => {
          // 详情模式:拉取审批流程节点
          if (operationType.value === "detail") {
            loadApprovalSteps(row.approveId);
          }
          approveProcessGetInfo({ id: row.approveId, approveReason: "1" }).then(
            res => {
              form.value = { ...res.data };
              // 设置图片列表显示
              const fileData =
@@ -589,15 +435,8 @@
              if (fileData.length > 0) {
                form.value.storageBlobDTOS = fileData;
              }
              // 获取额外详情
              fetchDetailData(res.data);
            })
            .finally(() => {
              // 延迟一会重置,确保 watch 不会被触发
              setTimeout(() => {
                isInitialLoading.value = false;
              }, 100);
            });
            }
          );
        }
      }
    } catch (error) {