zhangwencui
2 天以前 f670b79094c95d791fc43c8fc8b5858ebf8426dc
一些向web对齐
已添加8个文件
已修改31个文件
2986 ■■■■ 文件已修改
src/api/collaborativeApproval/approvalManagement.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/customerService/index.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInRecord.js 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockOut.js 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/paymentLedger.js 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/indicatorStats.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/invoiceLedger.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/receiptPayment.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/_components/ApprovalBusinessDetail.vue 353 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/_components/ApproveFlowSteps.vue 261 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/_utils/approveFlowSteps.js 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/approvalManagement.vue 454 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/approve.vue 249 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/detail.vue 447 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/index.vue 160 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/indexItem.vue 103 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/login.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/message.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/oa/ApproveManage/approve-list/_components/ApproveInstanceDetailBody.vue 207 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/oa/ApproveManage/approve-list/apply.vue 83 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/oa/ApproveManage/approve-list/detail.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/oa/ApproveManage/approve-list/index.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/oa/ApproveManage/approve-list/template-select.vue 59 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/oa/ApproveManage/approve-template/detail.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/oa/ApproveManage/approve-template/edit.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/oa/_utils/approvalModuleListSearch.js 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/oa/_utils/approvalTemplateType.js 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/oa/_utils/approveListUtils.js 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/procurementManagement/paymentLedger/detail.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/procurementManagement/paymentLedger/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/procurementManagement/purchaseReturnOrder/add.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/index.vue 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/receiptPaymentLedger/detail.vue 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/receiptPaymentLedger/index.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/edit.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/works.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/approvalManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
// ååŒå®¡æ‰¹ / å®¡æ‰¹æµç¨‹é…ç½®
import request from "@/utils/request";
/** æŸ¥è¯¢æŸå®¡æ‰¹ç±»åž‹çš„æµç¨‹é…ç½®èŠ‚ç‚¹åˆ—è¡¨ */
export function getApproveProcessConfigNodeList(type) {
  return request({
    url: "/approveProcessConfigNode/list",
    method: "get",
    params: { type },
  });
}
/** ä¿å­˜æŸå®¡æ‰¹ç±»åž‹çš„æµç¨‹é…ç½®èŠ‚ç‚¹ï¼ˆæ•´ç»„è¦†ç›–ï¼‰ */
export function addApproveProcessConfigNode(data) {
  return request({
    url: "/approveProcessConfigNode/add",
    method: "post",
    data,
  });
}
src/api/customerService/index.js
@@ -77,7 +77,7 @@
// æ ¹æ®å®¢æˆ·æŸ¥è¯¢é”€å”®è®¢å•号
export function getSalesLedger(query) {
    return request({
        url: '/afterSalesService/listSalesLedger',
        url: '/shippingInfo/getByCustomerName',
        method: 'get',
        params: query,
    })
src/api/inventoryManagement/stockInRecord.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
import request from "@/utils/request";
// æŸ¥è¯¢å…¥åº“记录列表
export const getStockInRecordListPage = params => {
  return request({
    url: "/stockInRecord/listPage",
    method: "get",
    params,
  });
};
// ä¿®æ”¹å…¥åº“记录
export const updateStockInRecord = (id, data) => {
  return request({
    url: "/stockInRecord/" + id,
    method: "put",
    data,
  });
};
// æ‰¹é‡åˆ é™¤å…¥åº“记录
export const batchDeleteStockInRecords = ids => {
  return request({
    url: "/stockInRecord",
    method: "delete",
    data: ids,
  });
};
// åˆ é™¤å¾…审批入库记录
export const batchDeletePendingStockInRecords = ids => {
  return request({
    url: "/stockInRecord/pending",
    method: "delete",
    data: ids,
  });
};
// æ‰¹é‡å®¡æ‰¹å…¥åº“记录
export const batchApproveStockInRecords = data => {
  return request({
    url: "/stockInRecord/approve",
    method: "post",
    data,
  });
};
// æ‰¹é‡åå®¡å…¥åº“记录
export const batchUnapproveStockInRecords = data => {
  return request({
    url: "/stockInRecord/reAudit",
    method: "post",
    data,
  });
};
src/api/inventoryManagement/stockOut.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
import request from "@/utils/request";
// æŸ¥è¯¢å‡ºåº“记录列表
export const getStockOutPage = params => {
  return request({
    url: "/stockOutRecord/listPage",
    method: "get",
    params,
  });
};
// åˆ é™¤å‡ºåº“记录
export const delStockOut = ids => {
  return request({
    url: "/stockOutRecord",
    method: "delete",
    data: ids,
  });
};
// åˆ é™¤å¾…审批出库记录
export const delPendingStockOut = ids => {
  return request({
    url: "/stockOutRecord/pending",
    method: "delete",
    data: ids,
  });
};
// æ‰¹é‡å®¡æ‰¹å‡ºåº“记录
export const batchApproveStockOutRecords = data => {
  return request({
    url: "/stockOutRecord/approve",
    method: "post",
    data,
  });
};
// æ‰¹é‡åå®¡å‡ºåº“记录
export const batchUnapproveStockOutRecords = data => {
  return request({
    url: "/stockOutRecord/reAudit",
    method: "post",
    data,
  });
};
src/api/procurementManagement/paymentLedger.js
@@ -1,20 +1,20 @@
// é‡‡è´­å°è´¦é¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
/** ä»˜æ¬¾å°è´¦ - ä¾›åº”商往来汇总 */
export function paymentLedgerList(query) {
  return request({
    url: "/purchase/paymentRegistration/supplierNameListPage",
    url: "/purchase/report/supplierTransactions",
    method: "get",
    params: query,
  });
}
// åˆ†é¡µæŸ¥è¯¢
export function paymentRecordList(supplierId) {
/** ä»˜æ¬¾å°è´¦ - ä¾›åº”商往来明细 */
export function paymentRecordList(params) {
  return request({
    url: "/purchase/paymentRegistration/supplierNameListPageDetails",
    url: "/purchase/report/supplierTransactionsDetails",
    method: "get",
    params: supplierId,
    params,
  });
}
src/api/salesManagement/indicatorStats.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
// é”€å”®æŒ‡æ ‡ç»Ÿè®¡æŽ¥å£
import request from "@/utils/request";
/** å®¢æˆ·å¾€æ¥æ±‡æ€» */
export function customewTransactions(query) {
  return request({
    url: "/metricStatistics/customewTransactions",
    method: "get",
    params: query,
  });
}
/** å®¢æˆ·å¾€æ¥æ˜Žç»† */
export function customewTransactionsDetails(query) {
  return request({
    url: "/metricStatistics/customewTransactionsDetails",
    method: "get",
    params: query,
  });
}
src/api/salesManagement/invoiceLedger.js
@@ -55,15 +55,6 @@
    })
}
// åˆ†é¡µæŸ¥è¯¢
export function invoiceLedgerSalesAccount(query) {
    return request({
        url: '/invoiceLedger/salesAccount',
        method: 'get',
        params: query
    })
}
// äº§å“å¼€ç¥¨è®°å½•分页查询
export function registrationProductPage(query) {
    return request({
src/api/salesManagement/receiptPayment.js
@@ -10,15 +10,6 @@
    })
}
// å®¢æˆ·å¾€æ¥è®°å½•查询
export function customerInteractions(query) {
    return request({
        url: '/receiptPayment/customerInteractions',
        method: 'get',
        params: query
    })
}
// è¯¦æƒ…
export function receiptPaymentInfo(query) {
    return request({
src/pages.json
@@ -416,6 +416,13 @@
      }
    },
    {
      "path": "pages/cooperativeOffice/collaborativeApproval/index",
      "style": {
        "navigationBarTitleText": "协同审批",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/cooperativeOffice/collaborativeApproval/index1",
      "style": {
        "navigationBarTitleText": "公出管理",
@@ -992,6 +999,13 @@
      }
    },
    {
      "path": "pages/cooperativeOffice/collaborativeApproval/approvalManagement",
      "style": {
        "navigationBarTitleText": "审批流程配置",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/safeProduction/dangerInvestigation/index",
      "style": {
        "navigationBarTitleText": "隐患排查上报",
src/pages/cooperativeOffice/collaborativeApproval/_components/ApprovalBusinessDetail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,353 @@
<!--
  ååŒå®¡æ‰¹ / ä¸šåŠ¡å•æ®è¯¦æƒ…
  approveType 5=采购 6=报价 7=发货,单据号取表单「审批事由」字段承载的编号
-->
<template>
  <view class="business-detail">
    <!-- æŠ¥ä»·å®¡æ‰¹è¯¦æƒ… -->
    <view v-if="isQuotationApproval"
          class="business-detail-block">
      <u-divider text="报价详情"
                 text-size="28rpx"
                 color="#2979ff"></u-divider>
      <u-skeleton :loading="quotationLoading"
                  rows="3"
                  animated>
        <view v-if="!currentQuotation || !currentQuotation.quotationNo"
              class="business-detail-empty">
          æœªæŸ¥è¯¢åˆ°å¯¹åº”报价详情
        </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 class="business-detail-amount">
                  Â¥{{ Number(currentQuotation.totalAmount ?? 0).toFixed(2) }}
                </text>
              </template>
            </u-cell>
          </u-cell-group>
          <view class="business-detail-products">
            <view class="business-detail-subtitle">产品明细</view>
            <view v-for="(item, index) in (currentQuotation.products || [])"
                  :key="index"
                  class="business-detail-item">
              <view class="business-detail-item-head">
                <text class="business-detail-item-name">{{ item.product }}</text>
                <text class="business-detail-amount">Â¥{{ Number(item.unitPrice ?? 0).toFixed(2) }}</text>
              </view>
              <view class="business-detail-item-desc">
                è§„æ ¼: {{ item.specification }} | å•位: {{ item.unit }}
              </view>
            </view>
          </view>
          <view v-if="currentQuotation.remark"
                class="business-detail-remark">
            <view class="business-detail-subtitle">备注</view>
            <view class="business-detail-remark-text">{{ currentQuotation.remark }}</view>
          </view>
        </view>
      </u-skeleton>
    </view>
    <!-- é‡‡è´­å®¡æ‰¹è¯¦æƒ… -->
    <view v-if="isPurchaseApproval"
          class="business-detail-block">
      <u-divider text="采购详情"
                 text-size="28rpx"
                 color="#2979ff"></u-divider>
      <u-skeleton :loading="purchaseLoading"
                  rows="3"
                  animated>
        <view v-if="!currentPurchase || !currentPurchase.purchaseContractNumber"
              class="business-detail-empty">
          æœªæŸ¥è¯¢åˆ°å¯¹åº”采购详情
        </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 class="business-detail-amount">
                  Â¥{{ Number(currentPurchase.contractAmount ?? 0).toFixed(2) }}
                </text>
              </template>
            </u-cell>
          </u-cell-group>
          <view class="business-detail-products">
            <view class="business-detail-subtitle">产品明细</view>
            <view v-for="(item, index) in (currentPurchase.productData || [])"
                  :key="index"
                  class="business-detail-item">
              <view class="business-detail-item-head">
                <text class="business-detail-item-name">{{ item.productCategory }}</text>
                <text class="business-detail-amount">Â¥{{ Number(item.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</text>
              </view>
              <view class="business-detail-item-desc">
                è§„æ ¼: {{ item.specificationModel }} | æ•°é‡: {{ item.quantity }} {{ item.unit }}
              </view>
              <view class="business-detail-item-extra">
                å«ç¨Žå•ä»·: Â¥{{ Number(item.taxInclusiveUnitPrice ?? 0).toFixed(2) }}
              </view>
            </view>
          </view>
        </view>
      </u-skeleton>
    </view>
    <!-- å‘货审批详情 -->
    <view v-if="isDeliveryApproval"
          class="business-detail-block">
      <u-divider text="发货详情"
                 text-size="28rpx"
                 color="#2979ff"></u-divider>
      <u-skeleton :loading="deliveryLoading"
                  rows="3"
                  animated>
        <view v-if="!currentDelivery || !currentDelivery.shippingInfo"
              class="business-detail-empty">
          æœªæŸ¥è¯¢åˆ°å¯¹åº”发货详情
        </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 class="business-detail-products">
            <view class="business-detail-subtitle">产品明细</view>
            <view v-for="(item, index) in deliveryProductList"
                  :key="index"
                  class="business-detail-item">
              <view class="business-detail-item-head">
                <text class="business-detail-item-name">{{ item.productName }}</text>
                <text class="business-detail-count">数量: {{ item.deliveryQuantity }}</text>
              </view>
              <view class="business-detail-item-desc">
                è§„æ ¼: {{ item.specificationModel }}
              </view>
              <view v-if="item.batchNo"
                    class="business-detail-item-extra">
                æ‰¹å·: {{ item.batchNo }}
              </view>
            </view>
          </view>
          <view v-if="currentDelivery.shippingInfo.storageBlobVOs && currentDelivery.shippingInfo.storageBlobVOs.length"
                class="business-detail-remark">
            <view class="business-detail-subtitle">发货图片</view>
            <CommonUpload :model-value="currentDelivery.shippingInfo.storageBlobVOs"
                          disabled />
          </view>
        </view>
      </u-skeleton>
    </view>
  </view>
</template>
<script setup>
  import { computed, ref, watch } from "vue";
  import CommonUpload from "@/components/CommonUpload.vue";
  import { getDeliveryDetailByShippingNo } from "@/api/collaborativeApproval/approvalProcess";
  import { getQuotationList } from "@/api/salesManagement/salesQuotation";
  import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger";
  const props = defineProps({
    /** å®¡æ‰¹ç±»åž‹ï¼š5 é‡‡è´­ / 6 æŠ¥ä»· / 7 å‘è´§ */
    approveType: { type: [Number, String], default: 0 },
    /** ä¸šåŠ¡å•æ®ç¼–å·ï¼ˆè¡¨å•ã€Œå®¡æ‰¹äº‹ç”±ã€æ‰¿è½½ï¼‰ */
    businessNo: { type: String, default: "" },
  });
  const type = computed(() => Number(props.approveType));
  const isQuotationApproval = computed(() => type.value === 6);
  const isPurchaseApproval = computed(() => type.value === 5);
  const isDeliveryApproval = computed(() => type.value === 7);
  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 fetchDetailData = async () => {
    const no = (props.businessNo || "").trim();
    if (!no) return;
    if (isQuotationApproval.value) {
      quotationLoading.value = true;
      getQuotationList({ quotationNo: no })
        .then(res => {
          const records = res?.data?.records || [];
          currentQuotation.value = records[0] || {};
        })
        .catch(() => {
          currentQuotation.value = {};
        })
        .finally(() => {
          quotationLoading.value = false;
        });
      return;
    }
    if (isPurchaseApproval.value) {
      purchaseLoading.value = true;
      getPurchaseByCode({ purchaseContractNumber: no })
        .then(res => {
          currentPurchase.value = res?.data || res || {};
        })
        .catch(() => {
          currentPurchase.value = {};
        })
        .finally(() => {
          purchaseLoading.value = false;
        });
      return;
    }
    if (isDeliveryApproval.value) {
      deliveryLoading.value = true;
      currentDelivery.value = {};
      deliveryProductList.value = [];
      getDeliveryDetailByShippingNo({ shippingNo: no })
        .then(res => {
          const detailData = res?.data || res || {};
          currentDelivery.value = detailData;
          deliveryProductList.value =
            detailData.shippingProductDetailDtoList || [];
        })
        .catch(() => {
          currentDelivery.value = {};
          deliveryProductList.value = [];
        })
        .finally(() => {
          deliveryLoading.value = false;
        });
    }
  };
  watch(
    () => [props.approveType, props.businessNo],
    () => {
      currentQuotation.value = {};
      currentPurchase.value = {};
      currentDelivery.value = {};
      deliveryProductList.value = [];
      fetchDetailData();
    },
    { immediate: true }
  );
</script>
<style scoped lang="scss">
  .business-detail-block {
    margin: 20rpx 0;
  }
  .business-detail-empty {
    padding: 40rpx;
    text-align: center;
    color: #999;
  }
  .business-detail-amount {
    font-size: 32rpx;
    color: #e6a23c;
    font-weight: bold;
  }
  .business-detail-count {
    color: #2979ff;
  }
  .business-detail-products {
    margin-top: 20rpx;
    padding: 0 30rpx;
  }
  .business-detail-subtitle {
    font-size: 28rpx;
    font-weight: bold;
    margin-bottom: 10rpx;
  }
  .business-detail-item {
    background: #f8f8f8;
    border-radius: 8rpx;
    padding: 20rpx;
    margin-bottom: 10rpx;
  }
  .business-detail-item-head {
    display: flex;
    justify-content: space-between;
  }
  .business-detail-item-name {
    font-weight: bold;
  }
  .business-detail-item-desc {
    font-size: 24rpx;
    color: #666;
    margin-top: 10rpx;
  }
  .business-detail-item-extra {
    font-size: 24rpx;
    color: #999;
    margin-top: 4rpx;
  }
  .business-detail-remark {
    margin-top: 20rpx;
    padding: 0 30rpx;
  }
  .business-detail-remark-text {
    font-size: 26rpx;
    color: #666;
    margin-top: 10rpx;
  }
</style>
src/pages/cooperativeOffice/collaborativeApproval/_components/ApproveFlowSteps.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,261 @@
<!--
  ååŒå®¡æ‰¹ / å®¡æ‰¹æµç¨‹èŠ‚ç‚¹ï¼ˆè¯¦æƒ…ã€å®¡æ ¸å…±ç”¨ï¼‰
  steps ç”± _utils/approveFlowSteps.js çš„ mapApprovalSteps ç”Ÿæˆï¼š
  [{ title, approverName, status, statusText, approveTime, opinion, urlTem, isShen }]
-->
<template>
  <view class="approval-process">
    <view class="process-header">
      <text class="process-title">审批流程</text>
    </view>
    <view class="process-steps">
      <view v-for="(step, index) in steps"
            :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>
          </view>
          <view v-if="index < steps.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">{{ approverLabel(step) }}</text>
            <text v-if="step.approveTime"
                  class="step-time">{{ step.approveTime }}</text>
          </view>
          <view v-if="step.opinion"
                class="step-opinion">
            <text class="opinion-label">审批意见:</text>
            <text class="opinion-content">{{ step.opinion }}</text>
          </view>
          <!-- å¾…审节点:与 Web ä¸€è‡´ï¼Œè¯¦æƒ…模式渲染禁用的意见框 -->
          <view v-else-if="showPendingOpinion && step.isShen"
                class="step-opinion step-opinion--pending">
            <text class="opinion-label">审批意见:</text>
            <u-textarea :model-value="step.opinion"
                        disabled
                        height="60"
                        border="surround"
                        placeholder="待审批" />
          </view>
          <!-- ç­¾åå±•示 -->
          <view v-if="step.urlTem"
                class="step-sign">
            <text class="opinion-label">签名:</text>
            <image :src="step.urlTem"
                   mode="widthFix"
                   class="sign-image" />
          </view>
        </view>
      </view>
      <view v-if="!steps.length"
            class="process-empty">暂无审批节点</view>
    </view>
  </view>
</template>
<script setup>
  defineProps({
    steps: { type: Array, default: () => [] },
    /** è¯¦æƒ…模式:待审节点渲染一个禁用的意见框(与 Web ä¸€è‡´ï¼‰ */
    showPendingOpinion: { type: Boolean, default: false },
  });
  /** å®¡æ‰¹äºº-已同意/已驳回/未审批(与 Web å±•示一致) */
  const approverLabel = step =>
    step?.statusText
      ? `${step.approverName}-${step.statusText}`
      : step?.approverName || "";
</script>
<style scoped lang="scss">
  .approval-process {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
  .process-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
  .process-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .process-steps {
    padding: 20px;
  }
  .process-empty {
    font-size: 14px;
    color: #999;
    text-align: center;
  }
  .process-step {
    display: flex;
    position: relative;
    margin-bottom: 24px;
    &:last-child {
      margin-bottom: 0;
      .step-line {
        display: none;
      }
    }
  }
  .step-indicator {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-right: 16px;
  }
  .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;
  }
  .process-step.completed .step-dot {
    background: #52c41a;
    color: #fff;
  }
  .process-step.current .step-dot {
    background: #1890ff;
    color: #fff;
    animation: pulse 2s infinite;
  }
  .process-step.pending .step-dot {
    background: #d9d9d9;
    color: #999;
  }
  .step-line {
    width: 2px;
    height: 40px;
    background: #d9d9d9;
    margin-top: 8px;
  }
  .process-step.completed .step-line {
    background: #52c41a;
  }
  .process-step.rejected .step-dot {
    background: #ff4d4f;
    color: #fff;
  }
  .process-step.rejected .step-line {
    background: #ff4d4f;
  }
  .step-content {
    flex: 1;
    padding-top: 4px;
  }
  .step-info {
    margin-bottom: 8px;
  }
  .step-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
    display: block;
    margin-bottom: 4px;
  }
  .step-approver {
    font-size: 14px;
    color: #666;
    display: block;
    margin-bottom: 4px;
  }
  .step-time {
    font-size: 12px;
    color: #999;
    display: block;
  }
  .step-opinion {
    background: #f8f9fa;
    padding: 12px;
    border-radius: 8px;
    border-left: 4px solid #52c41a;
  }
  .step-opinion--pending {
    background: #fafbfc;
    border-left-color: #d9d9d9;
  }
  .step-sign {
    margin-top: 8px;
  }
  .sign-image {
    width: 180px;
    border-radius: 6px;
    border: 1px solid #eee;
  }
  .opinion-label {
    font-size: 12px;
    color: #666;
    display: block;
    margin-bottom: 4px;
  }
  .opinion-content {
    font-size: 14px;
    color: #333;
    line-height: 1.5;
  }
  @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);
    }
  }
</style>
src/pages/cooperativeOffice/collaborativeApproval/_utils/approveFlowSteps.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
/**
 * ååŒå®¡æ‰¹ï¼šå®¡æ‰¹èŠ‚ç‚¹ â†’ æµç¨‹æ­¥éª¤ï¼ˆä¸Ž Web approvalDia ä¸€è‡´ï¼‰
 * @param {Array} list approveProcessDetails è¿”回的节点数组
 * @returns {Array<{
 *   title: string, approverName: string, status: string, statusText: string,
 *   approveTime: string|null, opinion: string, urlTem: string, isShen: boolean
 * }>}
 */
export function mapApprovalSteps(list) {
  const rows = Array.isArray(list) ? list : [];
  return rows.map((it, idx) => {
    let status = "pending";
    if (it.approveNodeStatus === 1) status = "completed";
    else if (it.approveNodeStatus === 2) status = "rejected";
    else if (it.isShen) status = "current";
    let statusText = "未审批";
    if (it.approveNodeStatus === 2) statusText = "已驳回";
    else if (it.approveNodeStatus === 1) statusText = "已同意";
    return {
      title: `第${idx + 1}步审批`,
      approverName: it.approveNodeUser || "未知用户",
      status,
      statusText,
      approveTime: it.approveTime || null,
      opinion: it.approveNodeReason || "",
      urlTem:
        it.urlTem || (it.url ? String(it.url).replace(/word/g, "img") : ""),
      isShen: !!it.isShen,
    };
  });
}
src/pages/cooperativeOffice/collaborativeApproval/approvalManagement.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,454 @@
<!--
  ååŒå®¡æ‰¹ / å®¡æ‰¹æµç¨‹é…ç½®
  æŒ‰å®¡æ‰¹ç±»åž‹ï¼ˆ1~7)维护审批人节点,接口 /approveProcessConfigNode
-->
<template>
  <view class="approve-config-page">
    <PageHeader title="审批流程配置"
                @back="goBack" />
    <view class="tabs-wrap">
      <up-tabs :list="tabList"
               :current="activeTab"
               line-color="#2979ff"
               @click="onTabClick" />
    </view>
    <scroll-view class="config-scroll"
                 scroll-y
                 :show-scrollbar="false">
      <view class="config-tip">
        <text class="config-tip-title">{{ currentTypeLabel }}</text>
        <text class="config-tip-desc">
          {{ approverList.length ? `已配置 ${approverList.length} ä¸ªå®¡æ‰¹äºº` : "未配置审批人" }}
        </text>
      </view>
      <view v-if="loading"
            class="loading-wrap">
        <up-loading-icon mode="circle" />
        <text class="loading-text">加载中...</text>
      </view>
      <view v-else-if="approverList.length"
            class="node-list">
        <view v-for="(item, index) in approverList"
              :key="index"
              class="node-card">
          <view class="node-head">
            <view class="node-badge">{{ index + 1 }}</view>
            <text class="node-level">{{ levelText(index) }}</text>
            <view class="node-actions">
              <view class="icon-btn"
                    :class="{ 'is-disabled': index === 0 }"
                    @click="moveUp(index)">
                <up-icon name="arrow-up"
                         size="16"
                         color="#2979ff" />
              </view>
              <view class="icon-btn"
                    :class="{ 'is-disabled': index === approverList.length - 1 }"
                    @click="moveDown(index)">
                <up-icon name="arrow-down"
                         size="16"
                         color="#2979ff" />
              </view>
              <view class="icon-btn"
                    @click="removeNode(index)">
                <up-icon name="trash"
                         size="16"
                         color="#f56c6c" />
              </view>
            </view>
          </view>
          <view class="node-body"
                @click="openUserPicker(index)">
            <text :class="['node-approver', item.approverName ? '' : 'is-placeholder']">
              {{ item.approverName || "请选择审批人" }}
            </text>
            <up-icon name="arrow-right"
                     size="16"
                     color="#c0c4cc" />
          </view>
        </view>
      </view>
      <view v-else
            class="empty-wrap">
        <up-empty mode="data"
                  text="暂无审批人配置" />
      </view>
      <view class="add-node"
            @click="addNode">
        <up-icon name="plus"
                 size="16"
                 color="#2979ff" />
        <text class="add-node-text">新增审批人</text>
      </view>
      <view class="bottom-space" />
    </scroll-view>
    <FooterButtons cancel-text="返回"
                   confirm-text="保存配置"
                   :loading="saving"
                   @cancel="goBack"
                   @confirm="handleSave" />
    <OaUserSearchPicker v-model:show="showUserPicker"
                        v-model="pickingUserId"
                        title="选择审批人"
                        :users="userList"
                        @select="onUserPicked" />
  </view>
</template>
<script setup>
  import { computed, ref } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import FooterButtons from "@/components/FooterButtons.vue";
  import OaUserSearchPicker from "@/pages/oa/_components/OaUserSearchPicker.vue";
  import { userListNoPage } from "@/api/system/user";
  import {
    addApproveProcessConfigNode,
    getApproveProcessConfigNodeList,
  } from "@/api/collaborativeApproval/approvalManagement";
  /** ä¸Ž Web approvalManagement ä¸€è‡´ */
  const APPROVE_TYPES = [
    { value: "1", label: "公出管理" },
    { value: "2", label: "请假管理" },
    { value: "3", label: "出差管理" },
    { value: "4", label: "报销管理" },
    { value: "5", label: "采购审批" },
    { value: "6", label: "报价审批" },
    { value: "7", label: "发货审批" },
  ];
  const activeTab = ref(0);
  const approverList = ref([]);
  const loading = ref(false);
  const saving = ref(false);
  const userList = ref([]);
  const showUserPicker = ref(false);
  const pickingUserId = ref("");
  const pickingIndex = ref(-1);
  const tabList = computed(() => APPROVE_TYPES.map(t => ({ name: t.label })));
  const currentTypeLabel = computed(
    () => APPROVE_TYPES[activeTab.value]?.label || ""
  );
  const currentApproveType = computed(
    () => Number(APPROVE_TYPES[activeTab.value]?.value ?? 1)
  );
  const levelText = index => `第${index + 1}级`;
  const unwrapNodeList = res => {
    if (Array.isArray(res?.data)) return res.data;
    if (Array.isArray(res?.rows)) return res.rows;
    if (Array.isArray(res?.data?.records)) return res.data.records;
    return [];
  };
  const unwrapUserList = res => {
    if (Array.isArray(res?.data)) return res.data;
    if (Array.isArray(res?.rows)) return res.rows;
    return [];
  };
  const loadData = async () => {
    loading.value = true;
    try {
      const res = await getApproveProcessConfigNodeList(currentApproveType.value);
      approverList.value = unwrapNodeList(res)
        .map((item, index) => ({
          ...item,
          sortOrder: item.nodeOrder ?? item.sortOrder ?? index + 1,
        }))
        .sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
    } catch {
      approverList.value = [];
      uni.showToast({ title: "加载审批配置失败", icon: "none" });
    } finally {
      loading.value = false;
    }
  };
  const loadUserList = async () => {
    try {
      userList.value = unwrapUserList(await userListNoPage());
    } catch {
      userList.value = [];
      uni.showToast({ title: "加载人员列表失败", icon: "none" });
    }
  };
  const onTabClick = item => {
    activeTab.value = item?.index ?? 0;
    loadData();
  };
  const addNode = () => {
    approverList.value.push({
      id: null,
      approveType: currentApproveType.value,
      approverId: null,
      approverName: "",
      sortOrder: approverList.value.length + 1,
    });
  };
  const removeNode = index => {
    uni.showModal({
      title: "删除确认",
      content: "确定删除该审批人吗?",
      confirmText: "确定删除",
      confirmColor: "#f56c6c",
      success: res => {
        if (!res.confirm) return;
        approverList.value.splice(index, 1);
        approverList.value.forEach((item, idx) => {
          item.sortOrder = idx + 1;
        });
      },
    });
  };
  const swapNode = (from, to) => {
    const list = approverList.value;
    const temp = list[from];
    list[from] = list[to];
    list[to] = temp;
    list.forEach((item, idx) => {
      item.sortOrder = idx + 1;
    });
  };
  const moveUp = index => {
    if (index <= 0) return;
    swapNode(index, index - 1);
  };
  const moveDown = index => {
    if (index >= approverList.value.length - 1) return;
    swapNode(index, index + 1);
  };
  const openUserPicker = index => {
    pickingIndex.value = index;
    pickingUserId.value = approverList.value[index]?.approverId ?? "";
    showUserPicker.value = true;
  };
  const onUserPicked = user => {
    const idx = pickingIndex.value;
    if (idx < 0 || !user) return;
    approverList.value[idx].approverId = user.userId ?? user.id;
    approverList.value[idx].approverName =
      user.nickName || user.userName || "";
  };
  const handleSave = async () => {
    if (saving.value) return;
    if (!approverList.value.length) {
      uni.showToast({ title: "请至少配置一个审批人", icon: "none" });
      return;
    }
    if (approverList.value.some(item => !item.approverId)) {
      uni.showToast({ title: "请选择所有审批人", icon: "none" });
      return;
    }
    const ids = approverList.value.map(item => item.approverId);
    if (new Set(ids).size !== ids.length) {
      uni.showToast({ title: "审批人不能重复", icon: "none" });
      return;
    }
    saving.value = true;
    try {
      await addApproveProcessConfigNode(
        approverList.value.map((item, index) => ({
          approveType: currentApproveType.value,
          nodeOrder: index + 1,
          approverId: item.approverId,
          approverName: item.approverName,
        }))
      );
      uni.showToast({ title: "保存成功", icon: "success" });
      await loadData();
    } catch {
      uni.showToast({ title: "保存失败", icon: "none" });
    } finally {
      saving.value = false;
    }
  };
  const goBack = () => uni.navigateBack();
  onLoad(async () => {
    await loadUserList();
    await loadData();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/sales-common.scss";
  .approve-config-page {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    background: #f0f3f8;
  }
  .tabs-wrap {
    background: #fff;
    border-bottom: 1px solid #f0f0f0;
    padding: 0 8px;
  }
  .config-scroll {
    flex: 1;
    height: 0;
    padding: 10px 12px 0;
  }
  .config-tip {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    padding: 2px 4px 10px;
  }
  .config-tip-title {
    font-size: 15px;
    font-weight: 600;
    color: #303133;
  }
  .config-tip-desc {
    font-size: 12px;
    color: #909399;
  }
  .loading-wrap {
    padding: 48px 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
  }
  .loading-text {
    font-size: 14px;
    color: #909399;
  }
  .node-card {
    background: #fff;
    border-radius: 10px;
    padding: 12px;
    margin-bottom: 10px;
    box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05);
  }
  .node-head {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 10px;
  }
  .node-badge {
    width: 26px;
    height: 26px;
    border-radius: 8px;
    background: #2979ff;
    color: #fff;
    font-size: 14px;
    font-weight: 600;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
  }
  .node-level {
    flex: 1;
    font-size: 15px;
    font-weight: 600;
    color: #303133;
  }
  .node-actions {
    display: flex;
    align-items: center;
    gap: 6px;
  }
  .icon-btn {
    width: 30px;
    height: 30px;
    border-radius: 8px;
    background: #f5f7fa;
    display: flex;
    align-items: center;
    justify-content: center;
    &.is-disabled {
      opacity: 0.4;
      pointer-events: none;
    }
  }
  .node-body {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px;
    background: #f7f9fc;
    border: 1px solid #eef1f6;
    border-radius: 8px;
  }
  .node-approver {
    font-size: 15px;
    color: #303133;
    &.is-placeholder {
      color: #c0c4cc;
    }
  }
  .empty-wrap {
    padding: 40px 20px 20px;
  }
  .add-node {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    height: 44px;
    background: #fff;
    border: 1px dashed #c0c4cc;
    border-radius: 10px;
    &:active {
      opacity: 0.85;
    }
  }
  .add-node-text {
    font-size: 14px;
    color: #2979ff;
  }
  .bottom-space {
    height: calc(80px + env(safe-area-inset-bottom));
  }
</style>
src/pages/cooperativeOffice/collaborativeApproval/approve.vue
@@ -49,58 +49,14 @@
        </view>
      </view>
    </view>
    <!-- é‡‡è´­/报价/发货审批详情 -->
    <view v-if="isBusinessApproval"
          class="business-detail-wrap">
      <ApprovalBusinessDetail :approve-type="approvalData.approveType || approveType"
                              :business-no="approvalData.approveReason" />
    </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="{
            '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>
            </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>
            </view>
            <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;">
              <text class="opinion-label">签名:</text>
              <image :src="step.urlTem"
                     mode="widthFix"
                     style="width:180px;border-radius:6px;border:1px solid #eee;" />
            </view>
          </view>
        </view>
      </view>
    </view>
    <ApproveFlowSteps :steps="approvalSteps" />
    <!-- å®¡æ ¸æ„è§è¾“å…¥ -->
    <view v-if="canApprove"
          class="approval-input">
@@ -134,6 +90,9 @@
    updateApproveNode,
  } from "@/api/collaborativeApproval/approvalProcess";
  import useUserStore from "@/store/modules/user";
  import ApprovalBusinessDetail from "./_components/ApprovalBusinessDetail.vue";
  import ApproveFlowSteps from "./_components/ApproveFlowSteps.vue";
  import { mapApprovalSteps } from "./_utils/approveFlowSteps.js";
  const showToast = message => {
    uni.showToast({
      title: message,
@@ -147,6 +106,14 @@
  const approvalSteps = ref([]);
  const approvalOpinion = ref("");
  const approveId = ref("");
  const approveType = ref(0);
  /** 5 é‡‡è´­ / 6 æŠ¥ä»· / 7 å‘货:需展示业务单据详情 */
  const isBusinessApproval = computed(() =>
    [5, 6, 7].includes(
      Number(approvalData.value.approveType || approveType.value)
    )
  );
  // ä»Žè¯¦æƒ…接口字段对齐 canApprove:仅当有 isShen çš„节点时可审批
  const canApprove = computed(() => {
@@ -155,6 +122,7 @@
  onMounted(() => {
    approveId.value = uni.getStorageSync("approveId");
    approveType.value = Number(uni.getStorageSync("approveType")) || 0;
    if (approveId.value) {
      loadApprovalData();
    }
@@ -170,23 +138,7 @@
      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,
        };
      });
      approvalSteps.value = mapApprovalSteps(list);
    });
  };
@@ -282,6 +234,14 @@
    overflow: hidden;
  }
  .business-detail-wrap {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
    padding: 1px 0;
  }
  .info-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
@@ -319,148 +279,6 @@
    font-size: 14px;
    color: #333;
    flex: 1;
  }
  .approval-process {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
  .process-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
  .process-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .process-steps {
    padding: 20px;
  }
  .process-step {
    display: flex;
    position: relative;
    margin-bottom: 24px;
    &:last-child {
      margin-bottom: 0;
      .step-line {
        display: none;
      }
    }
  }
  .step-indicator {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-right: 16px;
  }
  .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;
  }
  .process-step.completed .step-dot {
    background: #52c41a;
    color: #fff;
  }
  .process-step.current .step-dot {
    background: #1890ff;
    color: #fff;
    animation: pulse 2s infinite;
  }
  .process-step.pending .step-dot {
    background: #d9d9d9;
    color: #999;
  }
  .step-line {
    width: 2px;
    height: 40px;
    background: #d9d9d9;
    margin-top: 8px;
  }
  .process-step.completed .step-line {
    background: #52c41a;
  }
  .process-step.rejected .step-dot {
    background: #ff4d4f;
    color: #fff;
  }
  .process-step.rejected .step-line {
    background: #ff4d4f;
  }
  .step-content {
    flex: 1;
    padding-top: 4px;
  }
  .step-info {
    margin-bottom: 8px;
  }
  .step-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
    display: block;
    margin-bottom: 4px;
  }
  .step-approver {
    font-size: 14px;
    color: #666;
    display: block;
    margin-bottom: 4px;
  }
  .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 {
@@ -517,17 +335,6 @@
    border-radius: 6px;
  }
  @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);
    }
  }
  .signature-section {
    background: #fff;
    padding: 12px 16px 16px;
src/pages/cooperativeOffice/collaborativeApproval/detail.vue
@@ -8,7 +8,6 @@
            :rules="rules"
            :model="form"
            label-width="140rpx">
      <template v-if="operationType !== 'detail'">
        <u-form-item prop="approveReason"
                     label="流程编号">
          <u-input v-model="form.approveId"
@@ -23,21 +22,21 @@
                   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"
                 placeholder="请选择申请部门" /> -->
          <u-input v-model="form.approveDeptName"
                   readonly
                 :disabled="isDetailMode"
                   placeholder="请选择申请部门"
                   @click="showPicker = true" />
                 @click="isDetailMode || (showPicker = true)" />
          <template #right>
            <up-icon name="arrow-right"
                     @click="showPicker = true"></up-icon>
                   @click="isDetailMode || (showPicker = true)"></up-icon>
          </template>
        </u-form-item>
        <!-- <u-form-item prop="approveUser"
@@ -60,12 +59,13 @@
        </template>
      </u-form-item> -->
        <!-- approveType=2 è¯·å‡ç›¸å…³å­—段 -->
        <template v-if="approveType === 2">
          <u-form-item prop="startDate"
      <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>
@@ -73,11 +73,13 @@
                       @click="showStartDatePicker"></up-icon>
            </template>
          </u-form-item>
          <u-form-item prop="endDate"
      <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>
@@ -85,13 +87,41 @@
                       @click="showEndDatePicker"></up-icon>
            </template>
          </u-form-item>
        </template>
        <!-- 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>
@@ -102,185 +132,17 @@
                     required>
          <u-input v-model="form.price"
                   type="number"
                 :disabled="isDetailMode"
                   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>
      <!-- é‡‡è´­/报价/发货审批详情 -->
      <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) {
src/pages/cooperativeOffice/collaborativeApproval/index.vue
@@ -4,6 +4,14 @@
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader :title="pageTitle"
                @back="goBack" />
    <!-- å®¡æ‰¹ç±»åž‹åˆ‡æ¢ï¼ˆä»Žèœå•直接进入时可切换;经 index1~8 è¿›å…¥æ—¶ä¸å±•示) -->
    <view v-if="showTypeTabs"
          class="type-tabs-wrap">
      <up-tabs :list="typeTabList"
               :current="activeTypeIndex"
               line-color="#ed8d05"
               @click="onTypeTabClick" />
    </view>
    <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
@@ -13,6 +21,14 @@
                    v-model="searchForm.approveId"
                    clearable />
        </view>
        <view class="filter-chip"
              :class="{ 'filter-chip--active': searchForm.approveStatus !== '' }"
              @click="openStatusSheet">
          <text class="filter-chip-text">{{ approveStatusText }}</text>
          <up-icon name="arrow-down"
                   size="14"
                   color="#999"></up-icon>
        </view>
        <view class="search-button"
              @click="getList">
          <up-icon name="search"
@@ -21,6 +37,11 @@
        </view>
      </view>
    </view>
    <up-action-sheet :show="showStatusSheet"
                     title="审批状态"
                     :actions="statusActions"
                     @select="onSelectStatus"
                     @close="showStatusSheet = false" />
    <!-- å®¡æ‰¹åˆ—表 -->
    <view class="ledger-list"
          v-if="ledgerList.length > 0">
@@ -105,7 +126,6 @@
                    ç¼–辑
                  </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)">
@@ -131,7 +151,7 @@
    </view>
    <!-- æµ®åŠ¨æ“ä½œæŒ‰é’® -->
    <view class="fab-button"
          v-if="props.approveType != 5 && props.approveType != 6 && props.approveType != 7"
          v-if="activeApproveType != 5 && activeApproveType != 6 && activeApproveType != 7"
          @click="handleAdd">
      <up-icon name="plus"
               size="24"
@@ -141,13 +161,23 @@
</template>
<script setup>
  import { ref, toRefs, reactive } from "vue";
  import { ref, toRefs, reactive, computed } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import { approveProcessListPage } from "@/api/collaborativeApproval/approvalProcess";
  import { onLoad, onShow } from "@dcloudio/uni-app";
  import useUserStore from "@/store/modules/user";
  // æŽ¥æ”¶çˆ¶ç»„件传递的 approveType å‚æ•°
  /** å®¡æ‰¹çŠ¶æ€ç­›é€‰ï¼ˆä¸Ž Web approvalProcess åˆ—表一致) */
  const APPROVE_STATUS_OPTIONS = [
    { value: "", label: "全部状态" },
    { value: 0, label: "待审核" },
    { value: 1, label: "审核中" },
    { value: 2, label: "审核完成" },
    { value: 3, label: "审核未通过" },
    { value: 4, label: "已重新提交" },
  ];
  // æŽ¥æ”¶çˆ¶ç»„件传递的 approveType å‚数(index1~8 å›ºå®šç±»åž‹ï¼›0 è¡¨ç¤ºæœªæŒ‡å®šï¼‰
  const props = defineProps({
    approveType: {
      type: Number,
@@ -155,22 +185,50 @@
    },
  });
  // æ˜ å°„ approveType åˆ°å¯¹åº”的页面标题
  const getPageTitle = type => {
    const titleMap = {
  // å®¡æ‰¹ç±»åž‹æ ‡é¢˜
  const APPROVE_TYPE_LABELS = {
      1: "公出管理",
      2: "请假管理",
      3: "出差管理",
      4: "报销管理",
      5: "采购管理",
      6: "报价管理",
    5: "采购审批",
    6: "报价审批",
      7: "发货审批",
      8: "危险作业审批",
    };
    return titleMap[type] || "审批管理";
  /** å¯åˆ‡æ¢çš„协同审批类型(与 Web approvalProcess Tab ä¸€è‡´ï¼‰ */
  const TYPE_TAB_VALUES = [1, 2, 3, 4, 5, 6, 7];
  /** çˆ¶çº§ä¼ å…¥çš„固定类型;0 è¡¨ç¤ºæœªæŒ‡å®šï¼Œå¯ç”±é¡µå†… Tab åˆ‡æ¢ */
  const fixedApproveType = computed(() => Number(props.approveType) || 0);
  const activeApproveType = ref(fixedApproveType.value || 1);
  const showTypeTabs = computed(() => fixedApproveType.value === 0);
  const typeTabList = computed(() =>
    TYPE_TAB_VALUES.map(value => ({ name: APPROVE_TYPE_LABELS[value] }))
  );
  const activeTypeIndex = computed(() => {
    const idx = TYPE_TAB_VALUES.indexOf(activeApproveType.value);
    return idx >= 0 ? idx : 0;
  });
  const pageTitle = computed(
    () => APPROVE_TYPE_LABELS[activeApproveType.value] || "协同审批"
  );
  const onTypeTabClick = item => {
    activeApproveType.value = TYPE_TAB_VALUES[item?.index ?? 0] || 1;
    searchForm.value.approveId = "";
    searchForm.value.approveStatus = "";
    getList();
  };
  const pageTitle = getPageTitle(props.approveType);
  /** åˆ—表行自带 approveType,缺失时回退当前 Tab */
  const currentItemApproveType = item =>
    Number(item?.approveType) || activeApproveType.value;
  const userStore = useUserStore();
  // æ•°æ®
@@ -178,9 +236,33 @@
  const data = reactive({
    searchForm: {
      approveId: "",
      approveStatus: "",
    },
  });
  const { searchForm } = toRefs(data);
  const showStatusSheet = ref(false);
  const statusActions = computed(() =>
    APPROVE_STATUS_OPTIONS.map(opt => ({ name: opt.label, value: opt.value }))
  );
  const approveStatusText = computed(() => {
    const hit = APPROVE_STATUS_OPTIONS.find(
      opt => String(opt.value) === String(searchForm.value.approveStatus)
    );
    return hit?.label || "全部状态";
  });
  const openStatusSheet = () => {
    showStatusSheet.value = true;
  };
  const onSelectStatus = action => {
    searchForm.value.approveStatus = action?.value ?? "";
    showStatusSheet.value = false;
    getList();
  };
  // è¿”回上一页
  const goBack = () => {
@@ -195,7 +277,7 @@
    };
    approveProcessListPage({
      ...page,
      approveType: props.approveType,
      approveType: activeApproveType.value,
      ...searchForm.value,
    })
      .then(res => {
@@ -263,7 +345,7 @@
    uni.setStorageSync("invoiceLedgerEditRow", JSON.stringify(item));
    uni.setStorageSync("operationType", "edit");
    uni.setStorageSync("approveId", item.approveId);
    uni.setStorageSync("approveType", props.approveType);
    uni.setStorageSync("approveType", currentItemApproveType(item));
    uni.navigateTo({
      url: "/pages/cooperativeOffice/collaborativeApproval/detail",
    });
@@ -274,7 +356,7 @@
    uni.setStorageSync("invoiceLedgerEditRow", JSON.stringify(item));
    uni.setStorageSync("operationType", "detail");
    uni.setStorageSync("approveId", item.approveId);
    uni.setStorageSync("approveType", props.approveType);
    uni.setStorageSync("approveType", currentItemApproveType(item));
    uni.navigateTo({
      url: "/pages/cooperativeOffice/collaborativeApproval/detail",
    });
@@ -283,23 +365,27 @@
  // æ·»åŠ æ–°è®°å½•
  const handleAdd = () => {
    uni.setStorageSync("operationType", "add");
    uni.setStorageSync("approveType", props.approveType);
    uni.setStorageSync("approveType", activeApproveType.value);
    uni.navigateTo({
      url: `/pages/cooperativeOffice/collaborativeApproval/detail?approveType=${props.approveType}`,
      url: `/pages/cooperativeOffice/collaborativeApproval/detail?approveType=${activeApproveType.value}`,
    });
  };
  // ç‚¹å‡»å®¡æ ¸
  const approve = item => {
    const type = currentItemApproveType(item);
    uni.setStorageSync("approveId", item.approveId);
    uni.setStorageSync("approveType", props.approveType);
    uni.setStorageSync("approveType", type);
    uni.navigateTo({
      url:
        "/pages/cooperativeOffice/collaborativeApproval/approve?approveType=" +
        props.approveType,
        type,
    });
  };
  onLoad(options => {
    // index1~8 ä¼ å›ºå®šç±»åž‹ï¼›èœå•直接进入时可用 ?approveType= æŒ‡å®šï¼Œé»˜è®¤å…¬å‡ºç®¡ç†
    activeApproveType.value =
      Number(options?.approveType) || fixedApproveType.value || 1;
    // è§£æžapproveId
    if (options.approveId) {
      searchForm.value.approveId = options.approveId;
@@ -319,11 +405,49 @@
    margin: 0 !important;
  }
  // å®¡æ‰¹ç±»åž‹åˆ‡æ¢
  .type-tabs-wrap {
    background: #fff;
    border-bottom: 1px solid #f0f0f0;
    padding: 0 8px;
  }
  // æ–‡æ¡£å›¾æ ‡æ ·å¼ - è¦†ç›–公共样式中的背景色
  .document-icon {
    background: #ed8d05;
  }
  // çŠ¶æ€ç­›é€‰ chip
  .filter-chip {
    display: flex;
    align-items: center;
    gap: 4px;
    flex-shrink: 0;
    margin-left: 8px;
    padding: 6px 10px;
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 16px;
    &--active {
      border-color: #2979ff;
      background: #f0f6ff;
    }
  }
  .filter-chip-text {
    font-size: 12px;
    color: #606266;
    max-width: 88px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .filter-chip--active .filter-chip-text {
    color: #2979ff;
  }
  // æµ®åŠ¨æŒ‰é’®æ ·å¼ - è¦†ç›–公共样式中的背景色
  .fab-button {
    background: #ed8d05;
src/pages/indexItem.vue
@@ -15,7 +15,8 @@
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
              <image :src="item.icon"
                     class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
@@ -37,62 +38,64 @@
  // è·¯ç”±æ˜ å°„配置
  const routeMapping = {
    "销售台账": "/pages/sales/salesAccount/index",
    "开票登记": "/pages/sales/invoicingRegistration/index",
    "开票台账": "/pages/sales/invoiceLedger/index",
    "回款登记": "/pages/sales/receiptPayment/index",
    "回款流水": "/pages/sales/receiptPaymentHistory/index",
    "客户往来": "/pages/sales/receiptPaymentLedger/index",
    "采购台账": "/pages/procurementManagement/procurementLedger/index",
    "来票登记": "/pages/procurementManagement/invoiceEntry/index",
    "来票台账": "/pages/procurementManagement/procurementInvoiceLedger/index",
    "付款登记": "/pages/procurementManagement/paymentEntry/index",
    "付款流水": "/pages/procurementManagement/receiptPaymentHistory/index",
    "供应商往来": "/pages/procurementManagement/paymentLedger/index",
    "采购退货单": "/pages/procurementManagement/purchaseReturnOrder/index",
    "供应商管理": "/pages/basicData/supplierManage/index",
    "公出管理": "/pages/cooperativeOffice/collaborativeApproval/index1",
    "请假管理": "/pages/cooperativeOffice/collaborativeApproval/index2",
    "出差管理": "/pages/cooperativeOffice/collaborativeApproval/index3",
    "报销管理": "/pages/cooperativeOffice/collaborativeApproval/index4",
    "采购审批": "/pages/cooperativeOffice/collaborativeApproval/index5",
    "报价审批": "/pages/cooperativeOffice/collaborativeApproval/index6",
    "发货审批": "/pages/cooperativeOffice/collaborativeApproval/index7",
    "协同审批": "/pages/cooperativeOffice/collaborativeApproval/index",
    "会议设置": "/pages/managementMeetings/meetingSettings/index",
    "会议列表": "/pages/managementMeetings/meetingList/index",
    "会议申请": "/pages/managementMeetings/meetApplication/index",
    "会议审批": "/pages/managementMeetings/meetExamine/index",
    "会议发布": "/pages/managementMeetings/meetPublish/index",
    "会议总结": "/pages/managementMeetings/meetSummary/index",
    "会议看板": "/pages/managementMeetings/meetingBoard/index",
    "通知公告": "/pages/cooperativeOffice/noticeManagement/index",
    "知识库": "/pages/managementMeetings/knowledgeBase/index",
    "用印管理": "/pages/managementMeetings/sealManagement/index",
    "规章制度": "/pages/managementMeetings/rulesRegulationsManagement/index",
    "客户拜访": "/pages/cooperativeOffice/clientVisit/index",
    "生产订单": "/pages/productionManagement/productionOrder/index",
    "生产派工": "/pages/productionManagement/productionDispatching/index",
    "工序排产": "/pages/productionManagement/processScheduling/index",
    "生产核算": "/pages/productionManagement/productionAccounting/index",
    "设备台账": "/pages/equipmentManagement/ledger/index",
    "设备报修": "/pages/equipmentManagement/repair/index",
    "设备保养": "/pages/equipmentManagement/upkeep/index",
    "设备巡检": "/pages/inspectionUpload/index",
    "分析追溯": "/pages/equipmentManagement/faultAnalysis/index",
    "智能派单": "/pages/equipmentManagement/smartDispatch/index",
    "作业指导": "/pages/equipmentManagement/sop/index",
    "结果验证": "/pages/equipmentManagement/verification/index",
    é”€å”®å°è´¦: "/pages/sales/salesAccount/index",
    å¼€ç¥¨ç™»è®°: "/pages/sales/invoicingRegistration/index",
    å¼€ç¥¨å°è´¦: "/pages/sales/invoiceLedger/index",
    å›žæ¬¾ç™»è®°: "/pages/sales/receiptPayment/index",
    å›žæ¬¾æµæ°´: "/pages/sales/receiptPaymentHistory/index",
    å®¢æˆ·å¾€æ¥: "/pages/sales/receiptPaymentLedger/index",
    é‡‡è´­å°è´¦: "/pages/procurementManagement/procurementLedger/index",
    æ¥ç¥¨ç™»è®°: "/pages/procurementManagement/invoiceEntry/index",
    æ¥ç¥¨å°è´¦: "/pages/procurementManagement/procurementInvoiceLedger/index",
    ä»˜æ¬¾ç™»è®°: "/pages/procurementManagement/paymentEntry/index",
    ä»˜æ¬¾æµæ°´: "/pages/procurementManagement/receiptPaymentHistory/index",
    ä¾›åº”商往来: "/pages/procurementManagement/paymentLedger/index",
    é‡‡è´­é€€è´§å•: "/pages/procurementManagement/purchaseReturnOrder/index",
    ä¾›åº”商管理: "/pages/basicData/supplierManage/index",
    å…¬å‡ºç®¡ç†: "/pages/cooperativeOffice/collaborativeApproval/index1",
    è¯·å‡ç®¡ç†: "/pages/cooperativeOffice/collaborativeApproval/index2",
    å‡ºå·®ç®¡ç†: "/pages/cooperativeOffice/collaborativeApproval/index3",
    æŠ¥é”€ç®¡ç†: "/pages/cooperativeOffice/collaborativeApproval/index4",
    é‡‡è´­å®¡æ‰¹: "/pages/cooperativeOffice/collaborativeApproval/index5",
    æŠ¥ä»·å®¡æ‰¹: "/pages/cooperativeOffice/collaborativeApproval/index6",
    å‘货审批: "/pages/cooperativeOffice/collaborativeApproval/index7",
    ååŒå®¡æ‰¹: "/pages/cooperativeOffice/collaborativeApproval/index",
    å®¡æ‰¹æµç¨‹é…ç½®:
      "/pages/cooperativeOffice/collaborativeApproval/approvalManagement",
    ä¼šè®®è®¾ç½®: "/pages/managementMeetings/meetingSettings/index",
    ä¼šè®®åˆ—表: "/pages/managementMeetings/meetingList/index",
    ä¼šè®®ç”³è¯·: "/pages/managementMeetings/meetApplication/index",
    ä¼šè®®å®¡æ‰¹: "/pages/managementMeetings/meetExamine/index",
    ä¼šè®®å‘布: "/pages/managementMeetings/meetPublish/index",
    ä¼šè®®æ€»ç»“: "/pages/managementMeetings/meetSummary/index",
    ä¼šè®®çœ‹æ¿: "/pages/managementMeetings/meetingBoard/index",
    é€šçŸ¥å…¬å‘Š: "/pages/cooperativeOffice/noticeManagement/index",
    çŸ¥è¯†åº“: "/pages/managementMeetings/knowledgeBase/index",
    ç”¨å°ç®¡ç†: "/pages/managementMeetings/sealManagement/index",
    è§„章制度: "/pages/managementMeetings/rulesRegulationsManagement/index",
    å®¢æˆ·æ‹œè®¿: "/pages/cooperativeOffice/clientVisit/index",
    ç”Ÿäº§è®¢å•: "/pages/productionManagement/productionOrder/index",
    ç”Ÿäº§æ´¾å·¥: "/pages/productionManagement/productionDispatching/index",
    å·¥åºæŽ’产: "/pages/productionManagement/processScheduling/index",
    ç”Ÿäº§æ ¸ç®—: "/pages/productionManagement/productionAccounting/index",
    è®¾å¤‡å°è´¦: "/pages/equipmentManagement/ledger/index",
    è®¾å¤‡æŠ¥ä¿®: "/pages/equipmentManagement/repair/index",
    è®¾å¤‡ä¿å…»: "/pages/equipmentManagement/upkeep/index",
    è®¾å¤‡å·¡æ£€: "/pages/inspectionUpload/index",
    åˆ†æžè¿½æº¯: "/pages/equipmentManagement/faultAnalysis/index",
    æ™ºèƒ½æ´¾å•: "/pages/equipmentManagement/smartDispatch/index",
    ä½œä¸šæŒ‡å¯¼: "/pages/equipmentManagement/sop/index",
    ç»“果验证: "/pages/equipmentManagement/verification/index",
  };
  // èœå•配置
  const menuConfig = {
    "考勤管理": [
    è€ƒå‹¤ç®¡ç†: [
      { icon: "/static/images/icon/gongchuguanli.svg", label: "公出管理" },
      { icon: "/static/images/icon/qingjiaguanli.svg", label: "请假管理" },
      { icon: "/static/images/icon/chuchaiguanli.svg", label: "出差管理" },
    ],
    "会议管理": [
    ä¼šè®®ç®¡ç†: [
      { icon: "/static/images/icon/huiyishezhi.svg", label: "会议设置" },
      { icon: "/static/images/icon/huiyiliebiao.svg", label: "会议列表" },
      { icon: "/static/images/icon/huiyishenqing.svg", label: "会议申请" },
@@ -101,7 +104,7 @@
      { icon: "/static/images/icon/huiyizongjie.svg", label: "会议总结" },
      { icon: "/static/images/icon/huiyikanban.svg", label: "会议看板" },
    ],
    "协同审批": [
    ååŒå®¡æ‰¹: [
      { icon: "/static/images/icon/gongchuguanli.svg", label: "公出管理" },
      { icon: "/static/images/icon/qingjiaguanli.svg", label: "请假管理" },
      { icon: "/static/images/icon/chuchaiguanli.svg", label: "出差管理" },
src/pages/login.vue
@@ -231,6 +231,16 @@
    }, 30000);
  }
  // tabbar è§’标:登录后定时刷新时可能已不在 tabBar é¡µé¢ï¼Œå¤±è´¥é™é»˜å¿½ç•¥
  function callTabBarApi(api, options) {
    try {
      const ret = uni[api](options);
      if (ret && typeof ret.catch === "function") ret.catch(() => {});
    } catch (e) {
      /* éž tabBar é¡µé¢ï¼Œå¿½ç•¥ */
    }
  }
  // æ›´æ–°æœªè¯»æ¶ˆæ¯æ•°é‡
  function updateNoticeCount(userId) {
    getNoticeCount(userId)
@@ -239,12 +249,12 @@
        console.log("未读消息数量:", count);
        // æ›´æ–°tabbar的角标
        if (count > 0) {
          uni.setTabBarBadge({
          callTabBarApi("setTabBarBadge", {
            index: 2, // æ¶ˆæ¯æ ‡ç­¾é¡µçš„索引
            text: count.toString(),
          });
        } else {
          uni.removeTabBarBadge({
          callTabBarApi("removeTabBarBadge", {
            index: 2,
          });
        }
src/pages/message.vue
@@ -233,6 +233,16 @@
    return null;
  };
  // tabbar è§’标:异步返回时可能已切到非 tabBar é¡µé¢ï¼Œå¤±è´¥é™é»˜å¿½ç•¥
  const callTabBarApi = (api, options) => {
    try {
      const ret = uni[api](options);
      if (ret && typeof ret.catch === "function") ret.catch(() => {});
    } catch (e) {
      /* éž tabBar é¡µé¢ï¼Œå¿½ç•¥ */
    }
  };
  // æ›´æ–° tab æ è§’æ ‡
  const updateTabBarBadge = () => {
    if (userId.value) {
@@ -241,12 +251,12 @@
          const count = res.data || 0;
          // æ›´æ–°tabbar的角标
          if (count > 0) {
            uni.setTabBarBadge({
            callTabBarApi("setTabBarBadge", {
              index: 2, // æ¶ˆæ¯æ ‡ç­¾é¡µçš„索引
              text: count.toString(),
            });
          } else {
            uni.removeTabBarBadge({
            callTabBarApi("removeTabBarBadge", {
              index: 2,
            });
          }
src/pages/oa/ApproveManage/approve-list/_components/ApproveInstanceDetailBody.vue
@@ -50,11 +50,27 @@
        </view>
      </view>
    </view>
    <view class="section-card">
      <view class="section-head">
        <text class="section-title">填报内容</text>
        <text class="section-title">{{ isSpecialApprovalType ? specialDetailTitle : "填报内容" }}</text>
      </view>
      <!-- é‡‡è´­/报价/发货审批:展示业务单据详情(与 Web approve-list ä¸€è‡´ï¼‰ -->
      <template v-if="isSpecialApprovalType">
        <view v-if="businessDetailLoading"
              class="empty-hint">加载中...</view>
        <view v-else-if="!hasBusinessDetail"
              class="empty-hint">{{ specialDetailEmptyText }}</view>
        <view v-else
              class="info-rows">
          <view v-for="(item, idx) in specialDetailRows"
                :key="`special-${idx}`"
                class="info-row">
            <text class="info-label">{{ item.label }}</text>
            <text class="info-value">{{ item.value }}</text>
          </view>
        </view>
      </template>
      <template v-else>
      <view v-if="displayFields.length"
            class="info-rows">
        <view v-for="field in displayFields"
@@ -72,8 +88,8 @@
      </view>
      <view v-else
            class="empty-hint">暂无填报内容</view>
      </template>
    </view>
    <view class="section-card">
      <view class="section-head">
        <text class="section-title">审批流程({{ flowNodes.length }} é¡¹ï¼‰</text>
@@ -112,7 +128,6 @@
      <view v-else
            class="empty-hint">暂无流程节点</view>
    </view>
    <view class="section-card">
      <view class="section-head">
        <text class="section-title">审批记录</text>
@@ -140,8 +155,11 @@
</template>
<script setup>
  import { computed } from "vue";
  import { computed, ref, watch } from "vue";
  import { APPROVAL_MODULE_KEYS } from "../../../_utils/approvalModuleRegistry.js";
  import { getQuotationList } from "@/api/salesManagement/salesQuotation";
  import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger";
  import { getDeliveryDetailByShippingNo } from "@/api/collaborativeApproval/approvalProcess";
  import {
    computeLeaveDurationDisplay,
    computeOvertimeHoursDisplay,
@@ -168,7 +186,19 @@
    moduleKey: { type: String, default: "" },
  });
  const LEVEL_TEXT = ["", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
  const LEVEL_TEXT = [
    "",
    "一",
    "二",
    "三",
    "四",
    "五",
    "六",
    "七",
    "八",
    "九",
    "十",
  ];
  const isBusinessModule = computed(() =>
    [
@@ -181,10 +211,14 @@
  );
  const statusLabel = status =>
    isBusinessModule.value ? businessStatusText(status) : instanceStatusText(status);
    isBusinessModule.value
      ? businessStatusText(status)
      : instanceStatusText(status);
  const statusTagType = status =>
    isBusinessModule.value ? businessStatusTagType(status) : instanceStatusTagType(status);
    isBusinessModule.value
      ? businessStatusTagType(status)
      : instanceStatusTagType(status);
  const displayFields = computed(() => resolveInstanceDisplayFields(props.row));
@@ -216,15 +250,166 @@
  const flowNodes = computed(() => mapTasksToFlowNodes(props.row?.tasks));
  const approvalRecords = computed(() =>
    mapApprovalRecords(props.row?.records)
  );
  const approvalRecords = computed(() => mapApprovalRecords(props.row?.records));
  const rejectReason = computed(() =>
    getRejectReasonFromRecords(props.row?.records)
  );
  const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
  /** é‡‡è´­/报价/发货审批:业务单据详情(与 Web approve-list ä¸€è‡´ï¼‰ */
  const SPECIAL_BUSINESS_TYPES = [5, 6, 7];
  const businessType = computed(() => Number(props.row?.businessType));
  const isSpecialApprovalType = computed(() =>
    SPECIAL_BUSINESS_TYPES.includes(businessType.value)
  );
  const businessDetail = ref({});
  const businessProductList = ref([]);
  const businessDetailLoading = ref(false);
  const specialDetailTitle = computed(() => {
    if (businessType.value === 5) return "采购详情";
    if (businessType.value === 6) return "报价详情";
    return "发货详情";
  });
  const specialDetailEmptyText = computed(() => {
    if (businessType.value === 5) return "未查询到对应采购详情";
    if (businessType.value === 6) return "未查询到对应报价详情";
    return "未查询到对应发货详情";
  });
  const hasBusinessDetail = computed(() => {
    const d = businessDetail.value || {};
    if (businessType.value === 5) return Boolean(d.purchaseContractNumber);
    if (businessType.value === 6) return Boolean(d.quotationNo);
    if (businessType.value === 7) return Boolean(d.shippingInfo);
    return false;
  });
  const formatMoney = val => `Â¥${Number(val ?? 0).toFixed(2)}`;
  const joinDesc = parts => parts.filter(Boolean).join(" Â· ");
  const specialDetailRows = computed(() => {
    const d = businessDetail.value || {};
    const products = businessProductList.value || [];
    const rows = [];
    const push = (label, value) => rows.push({ label, value: value || "--" });
    if (businessType.value === 6) {
      push("报价单号", d.quotationNo);
      push("客户名称", d.customer);
      push("业务员", d.salesperson);
      push("报价日期", d.quotationDate);
      push("有效期至", d.validDate);
      push("付款方式", d.paymentMethod);
      push("报价总额", formatMoney(d.totalAmount));
      products.forEach((p, i) => {
        push(
          p.product || `产品${i + 1}`,
          joinDesc([p.specification, p.unit, formatMoney(p.unitPrice)])
        );
      });
      if (d.remark) push("备注", d.remark);
    } else if (businessType.value === 5) {
      push("采购合同号", d.purchaseContractNumber);
      push("供应商名称", d.supplierName);
      push("项目名称", d.projectName);
      push("销售合同号", d.salesContractNo);
      push("签订日期", d.executionDate);
      push("录入日期", d.entryDate);
      push("付款方式", d.paymentMethod);
      push("合同金额", formatMoney(d.contractAmount));
      products.forEach((p, i) => {
        push(
          p.productCategory || `产品${i + 1}`,
          joinDesc([
            p.specificationModel,
            p.quantity != null && p.quantity !== ""
              ? `${p.quantity}${p.unit || ""}`
              : "",
            `含税单价 ${formatMoney(p.taxInclusiveUnitPrice)}`,
            `含税总价 ${formatMoney(p.taxInclusiveTotalPrice)}`,
          ])
        );
      });
    } else if (businessType.value === 7) {
      const info = d.shippingInfo || {};
      push("销售订单", info.salesContractNo);
      push("发货订单号", info.shippingNo);
      push("客户名称", info.customerName);
      push("发货类型", info.type);
      push("发货日期", info.shippingDate);
      push("审核状态", info.status);
      push("发货车牌号", info.shippingCarNumber);
      push("快递公司", info.expressCompany);
      push("快递单号", info.expressNumber);
      products.forEach((p, i) => {
        push(
          p.productName || `产品${i + 1}`,
          joinDesc([
            p.batchNo ? `批号 ${p.batchNo}` : "",
            p.specificationModel,
            p.deliveryQuantity != null ? `发货数量 ${p.deliveryQuantity}` : "",
          ])
        );
      });
    }
    return rows;
  });
  async function loadBusinessDetail() {
    const row = props.row || {};
    const type = Number(row.businessType);
    businessDetail.value = {};
    businessProductList.value = [];
    if (!SPECIAL_BUSINESS_TYPES.includes(type)) return;
    businessDetailLoading.value = true;
    try {
      if (type === 6) {
        if (!row.quotationNo) return;
        const res = await getQuotationList({ quotationNo: row.quotationNo });
        const hit = res?.data?.records?.[0] || {};
        businessDetail.value = hit;
        businessProductList.value = hit.products || [];
      } else if (type === 5) {
        if (!row.purchaseContractNumber) return;
        const res = await getPurchaseByCode({
          purchaseContractNumber: row.purchaseContractNumber,
          approvalInstanceId: row.id,
        });
        const hit = res?.data || res || {};
        businessDetail.value = hit;
        businessProductList.value = hit.productData || [];
      } else if (type === 7) {
        if (!row.shippingNo) return;
        const res = await getDeliveryDetailByShippingNo({
          shippingNo: row.shippingNo,
        });
        const hit = res?.data || res || {};
        businessDetail.value = hit;
        businessProductList.value = hit.shippingProductDetailDtoList || [];
      }
    } catch {
      businessDetail.value = {};
      businessProductList.value = [];
    } finally {
      businessDetailLoading.value = false;
    }
  }
  watch(
    () => [props.row?.id, props.row?.businessType],
    () => {
      loadBusinessDetail();
    },
    { immediate: true }
  );
</script>
<style scoped lang="scss">
src/pages/oa/ApproveManage/approve-list/apply.vue
@@ -6,7 +6,6 @@
  <view class="approve-apply-page">
    <PageHeader :title="pageTitle"
                @back="goBack" />
    <scroll-view class="form-scroll"
                 scroll-y
                 :show-scrollbar="false">
@@ -42,7 +41,6 @@
            </up-form-item>
          </u-cell-group>
        </up-form>
        <view class="section-card">
          <view class="section-head">
            <text class="section-title">填报内容</text>
@@ -121,7 +119,6 @@
          </up-form>
          <view v-else
                class="empty-hint">该模板暂无填报项</view>
          <!-- è¯·å‡ï¼šå‡æœŸä½™é¢ + æ—¶é•¿è‡ªåŠ¨è®¡ç®— -->
          <view v-if="isLeaveModule"
                class="module-extra-block">
@@ -148,7 +145,6 @@
              </up-form-item>
            </up-form>
          </view>
          <!-- åŠ ç­ï¼šæ—¶é•¿è‡ªåŠ¨è®¡ç®— -->
          <view v-if="isOvertimeModule"
                class="module-extra-block">
@@ -166,7 +162,6 @@
              </up-form-item>
            </up-form>
          </view>
          <!-- è°ƒå²—:原岗位自动带出 -->
          <view v-if="isTransferModule"
                class="module-extra-block">
@@ -182,7 +177,6 @@
            </up-form>
          </view>
        </view>
        <view class="section-card">
          <view class="section-head">
            <text class="section-title">审批流程</text>
@@ -234,14 +228,12 @@
                  text="未获取到模板详情" />
      </view>
    </scroll-view>
    <FooterButtons v-if="!loading && detail"
                   cancel-text="取消"
                   :confirm-text="confirmText"
                   :loading="submitting"
                   @cancel="goBack"
                   @confirm="handleSubmit" />
    <up-popup :show="showDatePicker"
              mode="bottom"
              @close="showDatePicker = false">
@@ -251,7 +243,6 @@
                          @confirm="onDateConfirm"
                          @cancel="onDatePickerCancel" />
    </up-popup>
    <up-action-sheet :show="showSelectSheet"
                     :title="selectSheetTitle"
                     :actions="selectSheetActions"
@@ -312,9 +303,24 @@
    parseFieldDateToTs,
  } from "../../_utils/approvalFormField.js";
  import { EDIT_STORAGE_KEY } from "../../_utils/approveListUtils.js";
  import {
    EDIT_STORAGE_KEY,
    mapFlowNodesToTasks,
  } from "../../_utils/approveListUtils.js";
  const LEVEL_TEXT = ["", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
  const LEVEL_TEXT = [
    "",
    "一",
    "二",
    "三",
    "四",
    "五",
    "六",
    "七",
    "八",
    "九",
    "十",
  ];
  const userStore = useUserStore();
  const moduleKey = ref("");
@@ -363,7 +369,9 @@
  const isEditMode = computed(() => !!instanceId.value);
  const pageTitle = computed(() => (isEditMode.value ? "修改审批" : "发起审批"));
  const confirmText = computed(() => (isEditMode.value ? "保存修改" : "提交审批"));
  const confirmText = computed(() =>
    isEditMode.value ? "保存修改" : "提交审批"
  );
  const applicantName = computed(
    () => userStore.nickName || userStore.name || "-"
@@ -414,8 +422,10 @@
  const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
  const selectSheetTitle = computed(
    () => (activeSelectField.value?.label ? `选择${activeSelectField.value.label}` : "请选择")
  const selectSheetTitle = computed(() =>
    activeSelectField.value?.label
      ? `选择${activeSelectField.value.label}`
      : "请选择"
  );
  const selectSheetActions = computed(() => {
@@ -466,7 +476,8 @@
    });
    const matched = options.find(
      opt =>
        String(opt.value) === String(stored) || String(opt.label) === String(stored)
        String(opt.value) === String(stored) ||
        String(opt.label) === String(stored)
    );
    return (
      matched?.label ||
@@ -547,7 +558,9 @@
      const val = formValues[field.key];
      if (val === undefined || val === null || String(val).trim() === "") {
        const action =
          isSelectField(field) || isDateLikeField(field) || isDatetimerangeField(field)
          isSelectField(field) ||
          isDateLikeField(field) ||
          isDatetimerangeField(field)
            ? "请选择"
            : "请填写";
        uni.showToast({ title: `${action}${field.label}`, icon: "none" });
@@ -569,7 +582,8 @@
          options.length &&
          !options.some(
            opt =>
              String(opt.value) === String(val) || String(opt.label) === String(val)
              String(opt.value) === String(val) ||
              String(opt.label) === String(val)
          )
        ) {
          uni.showToast({ title: `${field.label}选项无效`, icon: "none" });
@@ -602,14 +616,28 @@
      formConfigData.value.fields
    );
    const allFields = formConfigData.value.fields || [];
    const formPayload = {};
    allFields.forEach(field => {
      if (!field?.key) return;
      formPayload[field.key] = formValues[field.key] ?? "";
    });
    Object.entries(formValues).forEach(([key, val]) => {
      if (key && formPayload[key] === undefined) formPayload[key] = val;
    });
    return JSON.stringify({
      prompt: formConfigData.value.prompt,
      fields: allFields.map(field => ({
        ...field,
        value: formValues[field.key] ?? "",
      })),
      summaryPlaceholder: formConfigData.value.prompt || "",
      approvalType: formConfigData.value.approvalType || "",
      fields: allFields.map(({ value: _value, ...rest }) => rest),
      formPayload,
    });
  };
  /** æ¨¡æ¿ nodes â†’ å®¡æ‰¹ä»»åŠ¡ï¼ˆä¸Ž Web buildInstanceDto ä¸€è‡´ï¼‰ */
  const buildInstanceTasks = instanceIdValue =>
    mapFlowNodesToTasks(detail.value?.nodes, {
      instanceId: instanceIdValue,
      templateId: detail.value?.id,
    });
  const buildSavePayload = () => ({
    templateId: detail.value.id,
@@ -623,6 +651,7 @@
    applyTime: parseTime(new Date()),
    deptId: userStore.currentDeptId || undefined,
    formConfig: buildFormConfigPayload(),
    tasks: buildInstanceTasks(),
  });
  const buildUpdatePayload = () => {
@@ -648,6 +677,7 @@
      deptId: row.deptId,
      deleted: row.deleted,
      formConfig: buildFormConfigPayload(),
      tasks: buildInstanceTasks(instanceId.value),
      approveAction: row.approveAction,
      approveComment: row.approveComment,
    };
@@ -780,7 +810,9 @@
      try {
        const res = await findPostOptions();
        const rows = res?.data ?? res?.rows ?? [];
        postIdToName.value = buildPostIdToNameMap(Array.isArray(rows) ? rows : []);
        postIdToName.value = buildPostIdToNameMap(
          Array.isArray(rows) ? rows : []
        );
      } catch {
        postIdToName.value = {};
      }
@@ -794,7 +826,10 @@
      return;
    }
    const user = userById(transferUserPool.value, uid);
    extraForm.originalPostName = resolveOriginalPostName(user, postIdToName.value);
    extraForm.originalPostName = resolveOriginalPostName(
      user,
      postIdToName.value
    );
  }
  const goBack = () => {
src/pages/oa/ApproveManage/approve-list/detail.vue
@@ -6,7 +6,6 @@
  <view class="oa-detail-page">
    <PageHeader :title="pageTitle"
                @back="goBack" />
    <scroll-view v-if="displayReady"
                 class="oa-detail-scroll"
                 scroll-y
@@ -18,13 +17,11 @@
                                 :row="row"
                                 :module-key="detailModuleKey" />
    </scroll-view>
    <view v-else
          class="oa-empty">
      <up-empty mode="data"
                :text="loading ? '加载中' : '未获取到审批数据'" />
    </view>
    <view v-if="displayReady"
          class="oa-page-footer">
      <text class="oa-footer-btn btn-default"
@@ -46,7 +43,6 @@
  import ApproveInstanceDetailBody from "./_components/ApproveInstanceDetailBody.vue";
  import ReimburseInstanceDetailBody from "../../ReimburseManage/_components/ReimburseInstanceDetailBody.vue";
  import { OA_NAV } from "@/config/oaPaths.js";
  import useUserStore from "@/store/modules/user";
  import {
    canApproveInstance,
    canEditBusinessInstanceRow,
@@ -65,7 +61,6 @@
  import { getApprovalModuleConfig } from "../../_utils/approvalModuleRegistry.js";
  import { canEditReimbursementRow } from "../../_utils/finReimbursementMappers.js";
  const userStore = useUserStore();
  const fromBusiness = ref(false);
  const row = ref(null);
  const reimburseRow = ref(null);
@@ -103,7 +98,7 @@
    if (fromBusiness.value) {
      return canEditBusinessInstanceRow(row.value);
    }
    return canModifyInstance(row.value, userStore);
    return canModifyInstance(row.value);
  });
  const showApprove = computed(() => canApproveInstance(row.value));
@@ -158,7 +153,9 @@
    if (isReimburseApprovalInstance(cached)) {
      loading.value = true;
      try {
        const { reimburseRow: mapped } = await loadReimburseDetailForInstance(cached);
        const { reimburseRow: mapped } = await loadReimburseDetailForInstance(
          cached
        );
        reimburseRow.value = mapped;
      } catch {
        uni.showToast({ title: "加载报销详情失败", icon: "none" });
src/pages/oa/ApproveManage/approve-list/index.vue
@@ -5,7 +5,6 @@
  <view class="oa-approval-page">
    <PageHeader title="审批列表"
                @back="goBack" />
    <view class="oa-toolbar">
      <view class="oa-filter-chip active-search">
        <up-icon name="search"
@@ -25,7 +24,6 @@
                 color="#2979ff" />
      </view>
    </view>
    <scroll-view class="oa-list-scroll"
                 scroll-y
                 :show-scrollbar="false"
@@ -47,7 +45,6 @@
              {{ statusText(item.status) }}
            </text>
          </view>
          <view class="oa-card-body">
            <view class="oa-info-grid">
              <view class="oa-info-row">
@@ -73,7 +70,6 @@
              </view>
            </view>
          </view>
          <view v-if="canModify(item) || item.isApprove"
                class="oa-card-foot"
                @click.stop>
@@ -93,7 +89,6 @@
                  text="暂无审批数据" />
      </view>
    </scroll-view>
    <view class="fab-button"
          @click="goAdd">
      <up-icon name="plus"
@@ -109,7 +104,6 @@
  import PageHeader from "@/components/PageHeader.vue";
  import { listApprovalInstancePage } from "@/api/oa/approvalInstance.js";
  import { OA_NAV } from "@/config/oaPaths.js";
  import useUserStore from "@/store/modules/user";
  import { parseTime } from "@/utils/ruoyi";
  import {
    businessStatusClass,
@@ -123,7 +117,6 @@
    stashReimburseEditFromApprove,
  } from "../../_utils/reimburseApproveBridge.js";
  const userStore = useUserStore();
  const queryParams = reactive({ keyword: "" });
  const list = ref([]);
  const pageStatus = ref("loadmore");
@@ -149,7 +142,7 @@
    return parseTime(val, "{y}-{m}-{d} {h}:{i}:{s}") || String(val);
  };
  const canModify = item => canModifyInstance(item, userStore);
  const canModify = item => canModifyInstance(item);
  const currentApproverName = item => {
    const tasks = item?.tasks;
@@ -223,7 +216,7 @@
  const goModify = item => {
    if (!canModify(item)) {
      uni.showToast({ title: "仅进行中的本人申请可编辑", icon: "none" });
      uni.showToast({ title: "仅进行中的审批可编辑", icon: "none" });
      return;
    }
    const mk = inferReimburseModuleKeyFromInstance(item);
src/pages/oa/ApproveManage/approve-list/template-select.vue
@@ -7,7 +7,6 @@
  <view class="template-select-page sales-account">
    <PageHeader :title="pageHeaderTitle"
                @back="goBack" />
    <view v-if="typeOptions.length && !moduleKey"
          class="step-section">
      <view class="tabs-wrap">
@@ -17,12 +16,6 @@
                 @click="onTabClick" />
      </view>
    </view>
    <view v-if="useAllTemplatesFallback && allTemplates.length"
          class="fallback-hint">
      <text>当前类型下无匹配模板,已显示全部 {{ allTemplates.length }} ä¸ªå¯ç”¨æ¨¡æ¿</text>
    </view>
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
@@ -38,7 +31,6 @@
        </view>
      </view>
    </view>
    <scroll-view class="list-scroll"
                 scroll-y
                 :show-scrollbar="false">
@@ -144,29 +136,21 @@
  const currentTypeOption = computed(() => typeOptions.value[activeTab.value]);
  /** æ—  moduleKey ä¸”当前 Tab ç­›ä¸åˆ°æ—¶ï¼Œå±•示全部模板避免「有数据却空白」 */
  const useAllTemplatesFallback = computed(() => {
    if (moduleKey.value) return false;
    if (!allTemplates.value.length) return false;
    const businessType = currentTypeOption.value?.value;
    if (businessType == null || businessType === "") return true;
    return filterTemplatesByBusinessType(allTemplates.value, businessType).length === 0;
  });
  /** ä¸å…è®¸æ‰‹å·¥å‘起审批的类型(采购/报价/发货,由业务单据产生,与 Web ä¸€è‡´ï¼‰ */
  const SUBMIT_EXCLUDED_BUSINESS_TYPES = [5, 6, 7];
  /** å½“前 Tab(或业务模块)下可发起的模板 */
  const currentSource = computed(() => {
    if (moduleKey.value && moduleBusinessTypes.value.length) {
      const filtered = filterTemplatesByBusinessTypes(
      return filterTemplatesByBusinessTypes(
        allTemplates.value,
        moduleBusinessTypes.value
      );
      if (filtered.length) return filtered;
      return allTemplates.value;
    }
    if (useAllTemplatesFallback.value) {
      return allTemplates.value;
    }
    const businessType = currentTypeOption.value?.value;
    return filterTemplatesByBusinessType(allTemplates.value, businessType);
    return filterTemplatesByBusinessType(
      allTemplates.value,
      currentTypeOption.value?.value
    );
  });
  const displayList = computed(() => {
@@ -184,15 +168,11 @@
    if (moduleConfig.value?.label) {
      return `暂无${moduleConfig.value.label}可用模板`;
    }
    if (useAllTemplatesFallback.value) {
      return "当前类型下无匹配模板";
    }
    const typeName = currentTypeOption.value?.name || "该审批类型";
    return `暂无${typeName}下的模板(可切换上方类型)`;
  });
  const businessTypeText = type =>
    getBusinessTypeLabel(type, typeLabelMap.value);
  const businessTypeText = type => getBusinessTypeLabel(type, typeLabelMap.value);
  const enabledText = enabled => {
    const val = String(enabled ?? "");
@@ -222,10 +202,15 @@
        fetchApprovalTemplateTypes(),
        fetchEnabledApprovalTemplates(),
      ]);
      let resolvedOpts = opts?.length ? opts : buildTypeOptionsFromTemplates(templates);
      let resolvedOpts = opts?.length
        ? opts
        : buildTypeOptionsFromTemplates(templates);
      if (!resolvedOpts.length) {
        resolvedOpts = [...FALLBACK_BUSINESS_TYPE_OPTIONS];
      }
      resolvedOpts = resolvedOpts.filter(
        opt => !SUBMIT_EXCLUDED_BUSINESS_TYPES.includes(Number(opt.value))
      );
      typeOptions.value = resolvedOpts;
      typeLabelMap.value = buildTypeLabelMap(resolvedOpts);
      allTemplates.value = templates;
@@ -239,8 +224,8 @@
      }
      if (moduleKey.value) {
        const resolved = resolveModuleBusinessType(moduleKey.value, opts);
        const idx = opts.findIndex(
        const resolved = resolveModuleBusinessType(moduleKey.value, resolvedOpts);
        const idx = resolvedOpts.findIndex(
          opt => String(opt.value) === String(resolved)
        );
        activeTab.value =
@@ -292,16 +277,6 @@
    display: flex;
    flex-direction: column;
    min-height: 100vh;
  }
  .fallback-hint {
    margin: 8px 12px 0;
    padding: 8px 12px;
    font-size: 12px;
    color: #e6a23c;
    background: #fdf6ec;
    border-radius: 6px;
    line-height: 1.5;
  }
  .step-section {
src/pages/oa/ApproveManage/approve-template/detail.vue
@@ -146,8 +146,12 @@
    try {
      const obj = typeof raw === "string" ? JSON.parse(raw) : raw;
      return {
        prompt: obj?.prompt || "",
        fields: Array.isArray(obj?.fields) ? obj.fields : [],
        prompt: obj?.summaryPlaceholder || obj?.prompt || "",
        fields: Array.isArray(obj?.fields)
          ? obj.fields
          : Array.isArray(obj?.formFields)
            ? obj.formFields
            : [],
      };
    } catch {
      return { prompt: "", fields: [] };
src/pages/oa/ApproveManage/approve-template/edit.vue
@@ -1294,7 +1294,7 @@
      businessType: form.businessType,
      templateType: form.templateType,
      formConfig: JSON.stringify({
        prompt: formConfig.prompt?.trim() || "",
        summaryPlaceholder: formConfig.prompt?.trim() || "",
        fields: formConfig.fields,
      }),
      nodes: flowNodes.value.map((node, index) => {
src/pages/oa/_utils/approvalModuleListSearch.js
@@ -1,3 +1,4 @@
import dayjs from "dayjs";
import {
  APPROVAL_MODULE_KEYS,
  APPROVAL_MODULE_REGISTRY,
@@ -226,16 +227,27 @@
  placeholder: "请输入审批单号",
};
/** ç»„装 approvalInstanceDto æŸ¥è¯¢ç‰‡æ®µï¼ˆç”³è¯·äºº + å®¡æ‰¹å•号) */
/** ç»„装 approvalInstanceDto æŸ¥è¯¢ç‰‡æ®µï¼ˆç”³è¯·äºº + å®¡æ‰¹å•号 + çŠ¶æ€ + æ—¶é—´èŒƒå›´ï¼Œä¸Ž Web ä¸€è‡´ï¼‰ */
export function buildApprovalInstanceSearchDto(searchForm = {}, extraParams = {}) {
  const dto = {
    ...(extraParams && typeof extraParams === "object" ? extraParams : {}),
  };
  Object.assign(dto, pickApplicantFromSearchForm(searchForm));
  Object.assign(dto, pickInstanceNoFromSearchForm(searchForm));
  if (searchForm?.status) {
    dto.status = searchForm.status;
  }
  const range = searchForm?.createTimeRange;
  if (Array.isArray(range) && range[0]) {
    dto.createTimeStart = range[0] + (range[0].includes(":") ? "" : " 00:00:00");
  }
  if (Array.isArray(range) && range[1]) {
    dto.createTimeEnd = range[1] + (range[1].includes(":") ? "" : " 23:59:59");
  }
  delete dto.createTime;
  delete dto.createTimeStart;
  delete dto.createTimeEnd;
  return dto;
}
@@ -306,7 +318,12 @@
  if ((sf.instanceNo || "").trim()) return true;
  if ((sf.applicantKeyword || "").trim()) return true;
  if ((sf.applicantName || "").trim()) return true;
  return sf.applicantId != null && sf.applicantId !== "";
  if (sf.applicantId != null && sf.applicantId !== "") return true;
  if (sf.status) return true;
  if (Array.isArray(sf.createTimeRange) && sf.createTimeRange.length === 2) {
    return true;
  }
  return false;
}
function matchApplicantKeyword(row, keyword) {
@@ -372,18 +389,35 @@
  });
}
/** æŒ‰ç”³è¯·äººã€å®¡æ‰¹å•号做前端兜底筛选 */
/** æŒ‰ç”³è¯·äººã€å®¡æ‰¹å•号、状态、时间范围做前端兜底筛选 */
export function filterRowsByModuleSearch(moduleKey, rows, searchForm = {}) {
  const sf = searchForm || {};
  const list = Array.isArray(rows) ? rows : [];
  if (!hasActiveModuleSearch(moduleKey, sf)) return list;
  return list.filter(
    row =>
      matchInstanceNo(row, sf.instanceNo) &&
      matchApplicantId(row, sf.applicantId) &&
      matchApplicantKeyword(row, sf.applicantKeyword || sf.applicantName)
  );
  return list.filter(row => {
    if (!matchInstanceNo(row, sf.instanceNo)) return false;
    if (!matchApplicantId(row, sf.applicantId)) return false;
    if (!matchApplicantKeyword(row, sf.applicantKeyword || sf.applicantName)) {
      return false;
    }
    if (
      sf.status &&
      String(row.statusRaw || row.status).toUpperCase() !== String(sf.status).toUpperCase()
    ) {
      return false;
    }
    if (Array.isArray(sf.createTimeRange) && sf.createTimeRange.length === 2) {
      const rowTime = row.createTime || row.applyTime;
      if (rowTime) {
        const t = dayjs(rowTime);
        const start = dayjs(sf.createTimeRange[0] + " 00:00:00");
        const end = dayjs(sf.createTimeRange[1] + " 23:59:59");
        if (t.isBefore(start) || t.isAfter(end)) return false;
      }
    }
    return true;
  });
}
function prependInstanceNoField(fields, moduleKey) {
src/pages/oa/_utils/approvalTemplateType.js
@@ -97,18 +97,11 @@
  return 0;
}
/** æ‹‰å–已启用模板(自定义 + ç³»ç»Ÿå†…置,与 Web å¯¼å…¥é€»è¾‘一致) */
/** æ‹‰å–可发起审批的模板(仅自定义已启用,与 Web loadSubmitTemplates ä¸€è‡´ï¼‰ */
export async function fetchEnabledApprovalTemplates() {
  const [customRes, builtinRes] = await Promise.all([
    listApprovalTemplateByType(CUSTOM_TEMPLATE_LIST_TYPE),
    listApprovalTemplateByType(SYSTEM_TEMPLATE_TYPE),
  ]);
  const merged = [
    ...unwrapTemplateList(customRes),
    ...unwrapTemplateList(builtinRes),
  ];
  const customRes = await listApprovalTemplateByType(CUSTOM_TEMPLATE_LIST_TYPE);
  const byId = new Map();
  merged.forEach(item => {
  unwrapTemplateList(customRes).forEach(item => {
    if (item?.id != null) byId.set(String(item.id), item);
  });
  return filterEnabledTemplates([...byId.values()]);
src/pages/oa/_utils/approveListUtils.js
@@ -180,6 +180,35 @@
    .map(([, node]) => node);
}
/** æ¨¡æ¿ nodes â†’ å®¡æ‰¹ä»»åŠ¡ï¼ˆä¸Ž Web mapFlowNodesToTasks ä¸€è‡´ï¼‰ */
export function mapFlowNodesToTasks(nodes, { instanceId, templateId } = {}) {
  const list = Array.isArray(nodes) ? nodes : [];
  const tasks = [];
  list.forEach((n, i) => {
    const levelNo = n?.levelNo ?? i + 1;
    const approveType =
      String(n?.approveType || "AND").toUpperCase() === "OR" ? "OR" : "AND";
    (n?.approvers || []).forEach((a, idx) => {
      if (a?.approverId == null || a.approverId === "") return;
      const task = {
        levelNo,
        approveType,
        approverId: a.approverId,
        approverName: a.approverName || "",
        sortNo: a.sortNo ?? idx + 1,
      };
      if (a.id != null) task.id = a.id;
      if (a.nodeId != null) task.nodeId = a.nodeId;
      if (a.templateId != null) task.templateId = a.templateId;
      else if (templateId) task.templateId = templateId;
      if (instanceId) task.instanceId = instanceId;
      if (a.status != null) task.status = a.status;
      tasks.push(task);
    });
  });
  return tasks;
}
/** ç»„装审批提交 DTO(与 Web buildApproveInstanceDto ä¸€è‡´ï¼‰ */
export function buildApproveInstanceDto(id, uiResult, comment) {
  const opinion = (comment || "").trim();
@@ -190,22 +219,9 @@
  };
}
/** æ˜¯å¦æœ¬äººå‘起的审批 */
export function isOwnApplication(item, userStore) {
  const uid = userStore?.id;
  if (item?.applicantId != null && uid != null && uid !== "") {
    return String(item.applicantId) === String(uid);
  }
  const loginName = userStore?.nickName || userStore?.name;
  if (loginName && item?.applicantName) {
    return String(item.applicantName).trim() === String(loginName).trim();
  }
  return false;
}
/** ä»…进行中且本人发起时可编辑 */
export function canModifyInstance(item, userStore) {
  return item?.status === "PENDING" && isOwnApplication(item, userStore);
/** ä»…进行中可编辑(与 Web openEditDialog åˆ¤å®šä¸€è‡´ï¼‰ */
export function canModifyInstance(item) {
  return item?.status === "PENDING";
}
/** å¾…当前用户审批 */
src/pages/procurementManagement/paymentLedger/detail.vue
@@ -11,16 +11,20 @@
        <text class="summary-value">{{ tableData.length }}</text>
      </view>
      <view class="summary-item">
        <text class="summary-label">开票总金额</text>
        <text class="summary-value">{{ formatAmount(invoiceTotal) }}</text>
        <text class="summary-label">合同总金额</text>
        <text class="summary-value">{{ formatAmount(contractTotal) }}</text>
      </view>
      <view class="summary-item">
        <text class="summary-label">回款总金额</text>
        <text class="summary-value highlight">{{ formatAmount(receiptTotal) }}</text>
        <text class="summary-label">付款总金额</text>
        <text class="summary-value highlight">{{ formatAmount(paymentTotal) }}</text>
      </view>
      <view class="summary-item">
        <text class="summary-label">应收总金额</text>
        <text class="summary-value danger">{{ formatAmount(unReceiptTotal) }}</text>
        <text class="summary-label">退货总金额</text>
        <text class="summary-value">{{ formatAmount(returnTotal) }}</text>
      </view>
      <view class="summary-item">
        <text class="summary-label">应付总金额</text>
        <text class="summary-value danger">{{ formatAmount(payableTotal) }}</text>
      </view>
    </view>
    <!-- å›žæ¬¾è®°å½•明细列表 -->
@@ -38,32 +42,36 @@
            </view>
            <text class="item-index">{{ index + 1 }}</text>
          </view>
          <view class="item-date">{{ item.happenTime }}</view>
          <view class="item-date">{{ item.executionDate }}</view>
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">发票金额(元)</text>
            <text class="detail-value">{{ formatAmount(item.invoiceAmount) }}</text>
            <text class="detail-label">采购合同号</text>
            <text class="detail-value">{{ item.purchaseContractNumber || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">合同金额(元)</text>
            <text class="detail-value">{{ formatAmount(item.contractAmount) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">付款金额(元)</text>
            <text class="detail-value highlight">{{ formatAmount(item.currentPaymentAmount) }}</text>
            <text class="detail-value highlight">{{ formatAmount(item.paymentAmount) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">退货金额(元)</text>
            <text class="detail-value">{{ formatAmount(item.returnAmount) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">应付金额(元)</text>
            <text class="detail-value danger">{{ formatAmount(item.payableAmount) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">发生日期</text>
            <text class="detail-value">{{ item.paymentDate }}</text>
          </view>
        </view>
      </view>
    </view>
    <view v-else
          class="no-data">
      <text>暂无回款记录</text>
      <text>暂无付款记录</text>
    </view>
  </view>
</template>
@@ -71,32 +79,35 @@
<script setup>
  import { ref, computed, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import {
    paymentLedgerList,
    paymentRecordList,
  } from "@/api/procurementManagement/paymentLedger";
  import { paymentRecordList } from "@/api/procurementManagement/paymentLedger";
  // å®¢æˆ·ä¿¡æ¯
  // ä¾›åº”商信息
  const supplierId = ref("");
  // è¡¨æ ¼æ•°æ®
  const tableData = ref([]);
  const invoiceTotal = computed(() => {
  const contractTotal = computed(() => {
    return tableData.value.reduce((sum, item) => {
      return sum + (parseFloat(item.invoiceAmount) || 0);
      return sum + (parseFloat(item.contractAmount) || 0);
    }, 0);
  });
  const receiptTotal = computed(() => {
  const paymentTotal = computed(() => {
    return tableData.value.reduce((sum, item) => {
      return sum + (parseFloat(item.receiptAmount) || 0);
      return sum + (parseFloat(item.paymentAmount) || 0);
    }, 0);
  });
  const unReceiptTotal = computed(() => {
  const returnTotal = computed(() => {
    return tableData.value.reduce((sum, item) => {
      return sum + (parseFloat(item.unReceiptAmount) || 0);
      return sum + (parseFloat(item.returnAmount) || 0);
    }, 0);
  });
  const payableTotal = computed(() => {
    return tableData.value.reduce((sum, item) => {
      return sum + (parseFloat(item.payableAmount) || 0);
    }, 0);
  });
@@ -119,15 +130,21 @@
  const getList = () => {
    if (!supplierId.value) {
      uni.showToast({
        title: "客户信息缺失",
        title: "供应商信息缺失",
        icon: "error",
      });
      return;
    }
    showLoadingToast("加载中...");
    paymentRecordList({ supplierId: supplierId.value })
    paymentRecordList({
      supplierId: supplierId.value,
      current: -1,
      size: -1,
    })
      .then(res => {
        tableData.value = res.data;
        // æ˜Žç»†æŽ¥å£å¯èƒ½è¿”回数组或分页对象(与 Web å¤„理一致)
        const data = res?.data;
        tableData.value = Array.isArray(data) ? data : data?.records || [];
        closeToast();
      })
      .catch(() => {
src/pages/procurementManagement/paymentLedger/index.vue
@@ -45,13 +45,17 @@
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">合同金额(元)</text>
              <text class="detail-value">{{ formattedNumber(item.invoiceAmount) }}</text>
              <text class="detail-value">{{ formattedNumber(item.contractAmounts) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">付款金额(元)</text>
              <text class="detail-value">{{ formattedNumber(item.paymentAmount) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">退货金额(元)</text>
              <text class="detail-value">{{ formattedNumber(item.returnAmount) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">应付金额(元)</text>
              <text class="detail-value highlight danger">{{ formattedNumber(item.payableAmount) }}</text>
            </view>
src/pages/procurementManagement/purchaseReturnOrder/add.vue
@@ -547,11 +547,16 @@
  };
  const mergeSelectedProducts = selectedRows => {
    const existing = new Set((form.value.purchaseReturnOrderProductsDtos || []).map(i => String(i.salesLedgerProductId || i.id)));
    const existing = new Set(
      (form.value.purchaseReturnOrderProductsDtos || []).map(i =>
        String(i.stockInRecordId || i.salesLedgerProductId || i.id)
      )
    );
    const toAdd = (selectedRows || [])
      .filter(i => !existing.has(String(i.id)))
      .map(i => ({
        ...i,
        stockInRecordId: i.stockInRecordId || i.id,
        salesLedgerProductId: i.id,
        returnQuantity: 0,
        taxInclusiveTotalPrice: 0,
@@ -602,6 +607,7 @@
    loading.value = true;
    const rows = (form.value.purchaseReturnOrderProductsDtos || []).map(i => {
      const cloned = { ...i };
      cloned.stockInRecordId = cloned.stockInRecordId || cloned.id;
      syncRowTotal(cloned);
      return cloned;
    });
src/pages/qualityManagement/finalInspection/index.vue
@@ -121,7 +121,7 @@
            <u-button type="primary"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      :disabled="!!item.inspectState"
                      @click.stop="startInspection(item)">
              ç¼–辑
            </u-button>
@@ -134,7 +134,7 @@
            <u-button type="success"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      :disabled="!!item.inspectState"
                      @click.stop="submitInspection(item)">
              æäº¤
            </u-button>
@@ -149,7 +149,7 @@
            <u-button type="warning"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState || item.checkName !== ''"
                      :disabled="!!item.inspectState || item.checkName !== ''"
                      @click.stop="assignInspector(item)">
              åˆ†é…æ£€éªŒå‘˜
            </u-button>
src/pages/qualityManagement/materialInspection/index.vue
@@ -121,7 +121,7 @@
            <u-button type="primary"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      :disabled="!!item.inspectState"
                      @click.stop="startInspection(item)">
              ç¼–辑
            </u-button>
@@ -134,7 +134,7 @@
            <u-button type="success"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      :disabled="!!item.inspectState"
                      @click.stop="submitInspection(item)">
              æäº¤
            </u-button>
@@ -149,13 +149,18 @@
            <u-button type="warning"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState || item.checkName !== ''"
                      :disabled="!!item.inspectState || item.checkName !== ''"
                      @click.stop="assignInspector(item)">
              åˆ†é…æ£€éªŒå‘˜
            </u-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else-if="tableLoading"
          class="loading-wrap">
      <up-loading-icon mode="circle" />
      <text class="loading-text">加载中...</text>
    </view>
    <view v-else
          class="no-data">
@@ -295,9 +300,14 @@
    }));
  });
  // è¿”回上一页
  // è¿”回上一页(消息通知直接进入等无上级页面时兜底回工作台)
  const goBack = () => {
    const pages = getCurrentPages();
    if (pages.length > 1) {
    uni.navigateBack();
      return;
    }
    uni.reLaunch({ url: "/pages/works" });
  };
  // æ ¼å¼åŒ–日期时间
@@ -502,7 +512,6 @@
  };
  onMounted(() => {
    getList();
    getUserList();
  });
@@ -723,6 +732,20 @@
    text-align: center;
  }
  // åŠ è½½çŠ¶æ€
  .loading-wrap {
    padding: 60px 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
  }
  .loading-text {
    font-size: 14px;
    color: #909399;
  }
  // æµ®åŠ¨æŒ‰é’®
  .fab-button {
    position: fixed;
src/pages/qualityManagement/processInspection/index.vue
@@ -121,7 +121,7 @@
            <u-button type="primary"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      :disabled="!!item.inspectState"
                      @click.stop="startInspection(item)">
              ç¼–辑
            </u-button>
@@ -134,7 +134,7 @@
            <u-button type="success"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      :disabled="!!item.inspectState"
                      @click.stop="submitInspection(item)">
              æäº¤
            </u-button>
@@ -149,7 +149,7 @@
            <u-button type="warning"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState || item.checkName !== ''"
                      :disabled="!!item.inspectState || item.checkName !== ''"
                      @click.stop="assignInspector(item)">
              åˆ†é…æ£€éªŒå‘˜
            </u-button>
src/pages/sales/receiptPaymentLedger/detail.vue
@@ -11,12 +11,16 @@
        <text class="summary-value">{{ tableData.length }}</text>
      </view>
      <view class="summary-item">
        <text class="summary-label">开票总金额</text>
        <text class="summary-value">{{ formatAmount(invoiceTotal) }}</text>
        <text class="summary-label">合同总金额</text>
        <text class="summary-value">{{ formatAmount(contractTotal) }}</text>
      </view>
      <view class="summary-item">
        <text class="summary-label">回款总金额</text>
        <text class="summary-value highlight">{{ formatAmount(receiptTotal) }}</text>
      </view>
      <view class="summary-item">
        <text class="summary-label">退货总金额</text>
        <text class="summary-value">{{ formatAmount(returnTotal) }}</text>
      </view>
      <view class="summary-item">
        <text class="summary-label">应收总金额</text>
@@ -38,25 +42,29 @@
            </view>
            <text class="item-index">{{ index + 1 }}</text>
          </view>
          <view class="item-date">{{ item.happenTime }}</view>
          <view class="item-date">{{ item.executionDate }}</view>
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">开票金额(元)</text>
            <text class="detail-value">{{ formatAmount(item.invoiceTotal) }}</text>
            <text class="detail-label">销售合同号</text>
            <text class="detail-value">{{ item.salesContractNo || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">合同金额(元)</text>
            <text class="detail-value">{{ formatAmount(item.contractAmount) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">回款金额(元)</text>
            <text class="detail-value highlight">{{ formatAmount(item.receiptPaymentAmount) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">应收金额(元)</text>
            <text class="detail-value danger">{{ formatAmount(item.unReceiptPaymentAmount) }}</text>
            <text class="detail-label">退货金额(元)</text>
            <text class="detail-value">{{ formatAmount(item.returnAmount) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">发生日期</text>
            <text class="detail-value">{{ item.receiptPaymentDate }}</text>
            <text class="detail-label">应收金额(元)</text>
            <text class="detail-value danger">{{ formatAmount(item.receiptableAmount) }}</text>
          </view>
        </view>
      </view>
@@ -71,7 +79,7 @@
<script setup>
  import { ref, computed, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import { customerInteractions } from "@/api/salesManagement/receiptPayment.js";
  import { customewTransactionsDetails } from "@/api/salesManagement/indicatorStats";
  // å®¢æˆ·ä¿¡æ¯
  const customerId = ref("");
@@ -79,9 +87,9 @@
  // è¡¨æ ¼æ•°æ®
  const tableData = ref([]);
  const invoiceTotal = computed(() => {
  const contractTotal = computed(() => {
    return tableData.value.reduce((sum, item) => {
      return sum + (parseFloat(item.invoiceTotal) || 0);
      return sum + (parseFloat(item.contractAmount) || 0);
    }, 0);
  });
@@ -91,9 +99,15 @@
    }, 0);
  });
  const returnTotal = computed(() => {
    return tableData.value.reduce((sum, item) => {
      return sum + (parseFloat(item.returnAmount) || 0);
    }, 0);
  });
  const unReceiptTotal = computed(() => {
    return tableData.value.reduce((sum, item) => {
      return sum + (parseFloat(item.unReceiptPaymentAmount) || 0);
      return sum + (parseFloat(item.receiptableAmount) || 0);
    }, 0);
  });
@@ -129,9 +143,11 @@
      size: -1,
    };
    customerInteractions(param)
    customewTransactionsDetails(param)
      .then(res => {
        tableData.value = res.data;
        // æ˜Žç»†æŽ¥å£å¯èƒ½è¿”回数组或分页对象(与 Web å¤„理一致)
        const data = res?.data;
        tableData.value = Array.isArray(data) ? data : data?.records || [];
        closeToast();
      })
      .catch(() => {
src/pages/sales/receiptPaymentLedger/index.vue
@@ -26,7 +26,7 @@
      <view class="customer-list"
            v-if="tableData.length > 0">
        <view v-for="(item, index) in tableData"
              :key="item.id"
              :key="item.customerId ?? index"
              class="customer-item"
              @click="rowClickMethod(item)">
          <view class="item-header">
@@ -47,16 +47,20 @@
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">开票金额(元)</text>
              <text class="detail-value">{{ formattedNumber(item.invoiceTotal) }}</text>
              <text class="detail-label">合同金额(元)</text>
              <text class="detail-value">{{ formattedNumber(item.contractAmounts) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">回款金额(元)</text>
              <text class="detail-value">{{ formattedNumber(item.receiptPaymentAmount) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">退货金额(元)</text>
              <text class="detail-value">{{ formattedNumber(item.returnAmount) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">应收金额(元)</text>
              <text class="detail-value highlight danger">{{ formattedNumber(item.unReceiptPaymentAmount) }}</text>
              <text class="detail-value highlight danger">{{ formattedNumber(item.receiptableAmount) }}</text>
            </view>
          </view>
        </view>
@@ -72,7 +76,7 @@
<script setup>
  import { onMounted, ref, reactive, toRefs } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import { invoiceLedgerSalesAccount } from "@/api/salesManagement/invoiceLedger";
  import { customewTransactions } from "@/api/salesManagement/indicatorStats";
  const tableData = ref([]);
@@ -106,7 +110,7 @@
  const getList = () => {
    showLoadingToast("加载中...");
    invoiceLedgerSalesAccount({ ...searchForm.value, ...page })
    customewTransactions({ ...searchForm.value, ...page })
      .then(res => {
        tableData.value = res.data.records;
        closeToast();
@@ -139,7 +143,7 @@
  const rowClickMethod = row => {
    // ä½¿ç”¨ uni.setStorageSync å­˜å‚¨å®¢æˆ·ä¿¡æ¯
    uni.setStorageSync("customerId", row.id);
    uni.setStorageSync("customerId", row.customerId);
    // è·³è½¬åˆ°å›žæ¬¾è®°å½•明细页面
    uni.navigateTo({
      url: "/pages/sales/receiptPaymentLedger/detail",
src/pages/sales/salesQuotation/edit.vue
@@ -267,6 +267,13 @@
        .toFixed(2)
    )
  );
  const webSubmitTotalAmount = computed(() =>
    Number(
      (form.value.products || [])
        .reduce((sum, item) => sum + Number(item.unitPrice || 0), 0)
        .toFixed(2)
    )
  );
  const customerActions = computed(() =>
    customerList.value.map(item => ({
      name: item.customerName,
@@ -543,8 +550,8 @@
    if (!valid || !validateProducts()) return;
    loading.value = true;
    // åŒæ­¥æœ€æ–°çš„æ€»é¢
    form.value.totalAmount = totalAmount.value;
    // Web ç«¯æäº¤æ—¶æŒ‰äº§å“å•价汇总 totalAmount,subtotal ä»ä¿ç•™æ˜Žç»†é‡‘额合计用于展示。
    form.value.totalAmount = webSubmitTotalAmount.value;
    form.value.subtotal = totalAmount.value;
    const payload = {
src/pages/works.vue
@@ -559,12 +559,12 @@
        {
            icon: "/static/images/icon/guizhangzhidu.svg",
            label: "审批管理",
            path: "/pages/oa/ApproveManage/approve-template/index",
            path: "/pages/cooperativeOffice/collaborativeApproval/approvalManagement",
        },
        {
            icon: "/static/images/icon/xietongshenpi.svg",
            label: "协同审批",
            path: "/pages/oa/ApproveManage/approve-list/index",
            path: "/pages/cooperativeOffice/collaborativeApproval/index",
        },
    {
      icon: "/static/images/icon/huiyiguanli.svg",