zhangwencui
2026-05-16 ae6af903490a340aff77e6bdb73d299505dc6d42
销售报价web端同步修改接口
已修改3个文件
719 ■■■■■ 文件已修改
src/pages/sales/salesQuotation/detail.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/edit.vue 547 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/index.vue 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/detail.vue
@@ -1,7 +1,7 @@
<template>
  <view class="customer-detail-page">
    <PageHeader title="报价详情" @back="goBack" />
    <PageHeader title="报价详情"
                @back="goBack" />
    <view class="detail-content">
      <view class="section">
        <view class="section-title">基础信息</view>
@@ -44,24 +44,13 @@
          </view>
        </view>
      </view>
      <view class="section">
        <view class="section-title">审批节点</view>
        <view v-if="approverNames.length" class="info-list">
          <view v-for="(name, index) in approverNames" :key="index" class="info-item">
            <text class="info-label">审批节点 {{ index + 1 }}</text>
            <text class="info-value">{{ name }}</text>
          </view>
        </view>
        <view v-else class="empty-box">
          <text>暂无审批节点</text>
        </view>
      </view>
      <view class="section">
        <view class="section-title">产品明细</view>
        <view v-if="detailData.products && detailData.products.length > 0" class="product-list">
          <view v-for="(item, index) in detailData.products" :key="index" class="product-card">
        <view v-if="detailData.products && detailData.products.length > 0"
              class="product-list">
          <view v-for="(item, index) in detailData.products"
                :key="index"
                class="product-card">
            <view class="product-head">产品 {{ index + 1 }}</view>
            <view class="info-item">
              <text class="info-label">产品名称</text>
@@ -89,13 +78,16 @@
            </view>
          </view>
        </view>
        <view v-else class="empty-box">
        <view v-else
              class="empty-box">
          <text>暂无产品明细</text>
        </view>
      </view>
    </view>
    <FooterButtons cancelText="返回" confirmText="编辑" @cancel="goBack" @confirm="goEdit" />
    <FooterButtons cancelText="返回"
                   confirmText="编辑"
                   @cancel="goBack"
                   @confirm="goEdit" />
  </view>
</template>
@@ -108,29 +100,27 @@
  const quotationId = ref("");
  const detailData = ref({});
  const approverNames = computed(() => {
    const approverText = detailData.value.approveUserNames || detailData.value.approverNames || detailData.value.approveUserIds || "";
    if (Array.isArray(approverText)) return approverText.filter(Boolean);
    return String(approverText)
      .split(",")
      .map(item => item.trim())
      .filter(Boolean);
  });
  const goBack = () => {
    uni.navigateBack();
  };
  const goEdit = () => {
    if (!quotationId.value) return;
    uni.navigateTo({ url: `/pages/sales/salesQuotation/edit?id=${quotationId.value}` });
    uni.navigateTo({
      url: `/pages/sales/salesQuotation/edit?id=${quotationId.value}`,
    });
  };
  const formatAmount = amount => `¥${Number(amount || 0).toFixed(2)}`;
  const loadDetailFromStorage = () => {
    const cachedData = uni.getStorageSync("salesQuotationDetail");
    detailData.value = cachedData || {};
    if (cachedData && (cachedData.id === quotationId.value || cachedData.id === Number(quotationId.value))) {
      detailData.value = cachedData;
    } else {
      detailData.value = cachedData || {};
      console.warn("未找到对应的报价单缓存数据");
    }
  };
  onLoad(options => {
src/pages/sales/salesQuotation/edit.vue
@@ -1,173 +1,196 @@
<template>
  <view class="account-detail">
    <PageHeader :title="pageTitle" @back="goBack" />
    <PageHeader :title="pageTitle"
                @back="goBack" />
    <view class="form-container">
      <up-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="110"
        input-align="right"
        error-message-align="right"
      >
        <u-cell-group title="基础信息" class="form-section">
          <up-form-item label="客户名称" prop="customer" required>
            <up-input v-model="form.customer" placeholder="请选择客户" readonly @click="showCustomerSheet = true" />
      <up-form ref="formRef"
               :model="form"
               :rules="rules"
               label-width="110"
               input-align="right"
               error-message-align="right">
        <u-cell-group title="基础信息"
                      class="form-section">
          <up-form-item label="客户名称"
                        prop="customer"
                        required>
            <up-input v-model="form.customer"
                      placeholder="请选择客户"
                      readonly
                      @click="showCustomerSheet = true" />
            <template #right>
              <up-icon name="arrow-right" @click="showCustomerSheet = true"></up-icon>
              <up-icon name="arrow-right"
                       @click="showCustomerSheet = true"></up-icon>
            </template>
          </up-form-item>
          <up-form-item label="业务员" prop="salesperson" required>
            <up-input
              v-model="form.salesperson"
              placeholder="请选择业务员"
              readonly
              @click="showSalespersonSheet = true"
            />
          <up-form-item label="业务员"
                        prop="salesperson"
                        required>
            <up-input v-model="form.salesperson"
                      placeholder="请选择业务员"
                      readonly
                      @click="showSalespersonSheet = true" />
            <template #right>
              <up-icon name="arrow-right" @click="showSalespersonSheet = true"></up-icon>
              <up-icon name="arrow-right"
                       @click="showSalespersonSheet = true"></up-icon>
            </template>
          </up-form-item>
          <up-form-item label="报价日期" prop="quotationDate" required>
            <up-input
              v-model="form.quotationDate"
              placeholder="请选择报价日期"
              readonly
              @click="showQuotationDatePicker = true"
            />
          <up-form-item label="报价日期"
                        prop="quotationDate"
                        required>
            <up-input v-model="form.quotationDate"
                      placeholder="请选择报价日期"
                      readonly
                      @click="showQuotationDatePicker = true" />
            <template #right>
              <up-icon name="arrow-right" @click="showQuotationDatePicker = true"></up-icon>
              <up-icon name="arrow-right"
                       @click="showQuotationDatePicker = true"></up-icon>
            </template>
          </up-form-item>
          <up-form-item label="有效期至" prop="validDate" required>
            <up-input
              v-model="form.validDate"
              placeholder="请选择有效期"
              readonly
              @click="showValidDatePicker = true"
            />
          <up-form-item label="有效期至"
                        prop="validDate"
                        required>
            <up-input v-model="form.validDate"
                      placeholder="请选择有效期"
                      readonly
                      @click="showValidDatePicker = true" />
            <template #right>
              <up-icon name="arrow-right" @click="showValidDatePicker = true"></up-icon>
              <up-icon name="arrow-right"
                       @click="showValidDatePicker = true"></up-icon>
            </template>
          </up-form-item>
          <up-form-item label="付款方式" prop="paymentMethod" required>
            <up-input v-model="form.paymentMethod" placeholder="请输入付款方式" clearable />
          <up-form-item label="付款方式"
                        prop="paymentMethod"
                        required>
            <up-input v-model="form.paymentMethod"
                      placeholder="请输入付款方式"
                      clearable />
          </up-form-item>
          <up-form-item label="备注" prop="remark">
            <up-textarea v-model="form.remark" placeholder="请输入备注" auto-height />
          <up-form-item label="备注"
                        prop="remark">
            <up-textarea v-model="form.remark"
                         placeholder="请输入备注"
                         auto-height />
          </up-form-item>
        </u-cell-group>
        <u-cell-group title="审批节点" class="form-section">
        <u-cell-group title="产品信息"
                      class="form-section">
          <view class="section-tools">
            <up-button type="primary" size="small" text="新增节点" @click="addApproverNode" />
            <up-button type="primary"
                       size="small"
                       text="新增产品"
                       @click="addProduct" />
          </view>
          <view v-if="salespersonList.length === 0" class="empty-text">
            <text>暂无可选审批人,请检查用户数据</text>
          </view>
          <view class="node-list">
            <view v-for="(node, index) in approverNodes" :key="node.id" class="node-card">
              <view class="node-top">
                <text class="node-title">审批节点 {{ index + 1 }}</text>
                <up-icon
                  v-if="approverNodes.length > 1"
                  name="trash"
                  color="#ee0a24"
                  size="18"
                  @click="removeApproverNode(index)"
                ></up-icon>
              </view>
              <view class="picker-field" @click="openApproverPicker(index)">
                <up-input :model-value="node.nickName || ''" placeholder="请选择审批人" readonly disabled />
                <up-icon name="arrow-right" color="#909399" size="16"></up-icon>
              </view>
            </view>
          </view>
        </u-cell-group>
        <u-cell-group title="产品信息" class="form-section">
          <view class="section-tools">
            <up-button type="primary" size="small" text="新增产品" @click="addProduct" />
          </view>
          <view v-if="form.products.length === 0" class="empty-text">
          <view v-if="form.products.length === 0"
                class="empty-text">
            <text>暂无产品,请先添加产品</text>
          </view>
          <view v-else class="product-list">
            <view v-for="(product, index) in form.products" :key="product.uid" class="product-card">
          <view v-else
                class="product-list">
            <view v-for="(product, index) in form.products"
                  :key="product.uid"
                  class="product-card">
              <view class="product-header">
                <text class="product-title">产品 {{ index + 1 }}</text>
                <up-icon name="trash" color="#ee0a24" size="18" @click="removeProduct(index)"></up-icon>
                <up-icon name="trash"
                         color="#ee0a24"
                         size="18"
                         @click="removeProduct(index)"></up-icon>
              </view>
              <up-divider></up-divider>
              <view class="product-body">
                <up-form-item label="产品名称">
                  <up-input
                    v-model="product.product"
                    placeholder="请选择产品"
                    readonly
                    @click="openProductPicker(index)"
                  />
                  <up-input v-model="product.product"
                            placeholder="请选择产品"
                            readonly
                            @click="openProductPicker(index)" />
                  <template #right>
                    <up-icon name="arrow-right" @click="openProductPicker(index)"></up-icon>
                    <up-icon name="arrow-right"
                             @click="openProductPicker(index)"></up-icon>
                  </template>
                </up-form-item>
                <up-form-item label="规格型号">
                  <up-input
                    v-model="product.specification"
                    placeholder="请选择规格型号"
                    readonly
                    @click="openModelPicker(index)"
                  />
                  <up-input v-model="product.ProductModel"
                            placeholder="请选择规格型号"
                            readonly
                            @click="openModelPicker(index)" />
                  <template #right>
                    <up-icon name="arrow-right" @click="openModelPicker(index)"></up-icon>
                    <up-icon name="arrow-right"
                             @click="openModelPicker(index)"></up-icon>
                  </template>
                </up-form-item>
                <up-form-item label="单位">
                  <up-input v-model="product.unit" placeholder="请输入单位" clearable />
                  <up-input v-model="product.unit"
                            placeholder="请输入单位"
                            clearable />
                </up-form-item>
                <up-form-item label="数量">
                  <up-input
                    v-model="product.quantity"
                    type="number"
                    placeholder="请输入数量"
                    clearable
                    @blur="calculateAmount(product)"
                  />
                  <up-input v-model="product.quantity"
                            type="number"
                            placeholder="请输入数量"
                            clearable
                            @blur="calculateAmount(product)" />
                </up-form-item>
                <up-form-item label="单价">
                  <up-input
                    v-model="product.unitPrice"
                    type="number"
                    placeholder="请输入单价"
                    clearable
                    @blur="calculateAmount(product)"
                  />
                  <up-input v-model="product.unitPrice"
                            type="number"
                            placeholder="请输入单价"
                            clearable
                            @blur="calculateAmount(product)" />
                </up-form-item>
                <up-form-item label="金额">
                  <up-input :model-value="formatAmount(product.amount)" disabled placeholder="自动计算" />
                  <up-input :model-value="formatAmount(product.amount)"
                            disabled
                            placeholder="自动计算" />
                </up-form-item>
              </view>
            </view>
          </view>
        </u-cell-group>
        <u-cell-group title="汇总信息" class="form-section">
        <u-cell-group title="汇总信息"
                      class="form-section">
          <up-form-item label="报价总额">
            <up-input :model-value="formatAmount(totalAmount)" disabled placeholder="自动汇总" />
            <up-input :model-value="formatAmount(totalAmount)"
                      disabled
                      placeholder="自动汇总" />
          </up-form-item>
        </u-cell-group>
      </up-form>
    </view>
    <FooterButtons :loading="loading" confirmText="保存" @cancel="goBack" @confirm="handleSubmit" />
    <up-action-sheet :show="showCustomerSheet" title="选择客户" :actions="customerActions" @select="onSelectCustomer" @close="showCustomerSheet = false" />
    <up-action-sheet :show="showSalespersonSheet" title="选择业务员" :actions="salespersonActions" @select="onSelectSalesperson" @close="showSalespersonSheet = false" />
    <up-action-sheet :show="showProductSheet" title="选择产品" :actions="productActions" @select="onSelectProduct" @close="showProductSheet = false" />
    <up-action-sheet :show="showModelSheet" title="选择规格型号" :actions="modelActions" @select="onSelectModel" @close="showModelSheet = false" />
    <up-datetime-picker :show="showQuotationDatePicker" v-model="quotationDateValue" mode="date" @confirm="onQuotationDateConfirm" @cancel="showQuotationDatePicker = false" />
    <up-datetime-picker :show="showValidDatePicker" v-model="validDateValue" mode="date" @confirm="onValidDateConfirm" @cancel="showValidDatePicker = false" />
    <FooterButtons :loading="loading"
                   confirmText="保存"
                   @cancel="goBack"
                   @confirm="handleSubmit" />
    <up-action-sheet :show="showCustomerSheet"
                     title="选择客户"
                     :actions="customerActions"
                     @select="onSelectCustomer"
                     @close="showCustomerSheet = false" />
    <up-action-sheet :show="showSalespersonSheet"
                     title="选择业务员"
                     :actions="salespersonActions"
                     @select="onSelectSalesperson"
                     @close="showSalespersonSheet = false" />
    <up-action-sheet :show="showProductSheet"
                     title="选择产品"
                     :actions="productActions"
                     @select="onSelectProduct"
                     @close="showProductSheet = false" />
    <up-action-sheet :show="showModelSheet"
                     title="选择规格型号"
                     :actions="modelActions"
                     @select="onSelectModel"
                     @close="showModelSheet = false" />
    <up-datetime-picker :show="showQuotationDatePicker"
                        v-model="quotationDateValue"
                        mode="date"
                        @confirm="onQuotationDateConfirm"
                        @cancel="showQuotationDatePicker = false" />
    <up-datetime-picker :show="showValidDatePicker"
                        v-model="validDateValue"
                        mode="date"
                        @confirm="onValidDateConfirm"
                        @cancel="showValidDatePicker = false" />
  </view>
</template>
@@ -179,7 +202,11 @@
  import { formatDateToYMD } from "@/utils/ruoyi";
  import { modelList, productTreeList } from "@/api/basicData/product";
  import { userListNoPageByTenantId } from "@/api/system/user";
  import { addQuotation, getCustomerList, getQuotationDetail, updateQuotation } from "@/api/salesManagement/salesQuotation";
  import {
    addQuotation,
    getCustomerList,
    updateQuotation,
  } from "@/api/salesManagement/salesQuotation";
  const formRef = ref();
  const loading = ref(false);
@@ -199,47 +226,73 @@
  const modelActions = ref([]);
  let uidSeed = 1;
  let nextApproverId = 2;
  const form = ref({
    id: undefined,
    quotationNo: "",
    customerId: undefined,
    customer: "",
    salesperson: "",
    quotationDate: "",
    validDate: "",
    paymentMethod: "",
    status: "待审批",
    status: "草稿",
    remark: "",
    approveUserIds: "",
    products: [],
    subtotal: 0,
    freight: 0,
    otherFee: 0,
    discountRate: 0,
    discountAmount: 0,
    totalAmount: 0,
  });
  const approverNodes = ref([{ id: 1, userId: "", nickName: "" }]);
  const rules = {
    customer: [{ required: true, message: "请选择客户", trigger: "change" }],
    salesperson: [{ required: true, message: "请选择业务员", trigger: "change" }],
    quotationDate: [{ required: true, message: "请选择报价日期", trigger: "change" }],
    quotationDate: [
      { required: true, message: "请选择报价日期", trigger: "change" },
    ],
    validDate: [{ required: true, message: "请选择有效期", trigger: "change" }],
    paymentMethod: [{ required: true, message: "请输入付款方式", trigger: "blur" }],
    paymentMethod: [
      { required: true, message: "请输入付款方式", trigger: "blur" },
    ],
  };
  const pageTitle = computed(() => (quotationId.value ? "编辑报价" : "新增报价"));
  const totalAmount = computed(() =>
    Number((form.value.products || []).reduce((sum, item) => sum + Number(item.amount || 0), 0).toFixed(2))
    Number(
      (form.value.products || [])
        .reduce((sum, item) => sum + Number(item.amount || 0), 0)
        .toFixed(2)
    )
  );
  const customerActions = computed(() => customerList.value.map(item => ({ name: item.customerName, value: item.customerName })));
  const salespersonActions = computed(() => salespersonList.value.map(item => ({ name: item.nickName, value: item.nickName })));
  const productActions = computed(() => productList.value.map(item => ({ name: item.label, value: item.value, label: item.label })));
  const customerActions = computed(() =>
    customerList.value.map(item => ({
      name: item.customerName,
      value: item.id,
    }))
  );
  const salespersonActions = computed(() =>
    salespersonList.value.map(item => ({
      name: item.nickName,
      value: item.nickName,
    }))
  );
  const productActions = computed(() =>
    productList.value.map(item => ({
      name: item.label,
      value: item.value,
      label: item.label,
    }))
  );
  const createEmptyProduct = () => ({
    uid: `p_${uidSeed++}`,
    productId: "",
    product: "",
    specificationId: "",
    specification: "",
    productModelId: "",
    ProductModel: "",
    unit: "",
    quantity: 1,
    unitPrice: 0,
@@ -254,7 +307,10 @@
        if (item.children && item.children.length) {
          walk(item.children);
        } else {
          result.push({ label: item.label || item.productName || "", value: item.id || item.value });
          result.push({
            label: item.label || item.productName || "",
            value: item.id || item.value,
          });
        }
      });
    };
@@ -266,18 +322,12 @@
  const goBack = () => uni.navigateBack();
  const calculateAmount = product => {
    product.amount = Number((Number(product.quantity || 0) * Number(product.unitPrice || 0)).toFixed(2));
    product.amount = Number(
      (Number(product.quantity || 0) * Number(product.unitPrice || 0)).toFixed(2)
    );
    form.value.totalAmount = totalAmount.value;
  };
  const addApproverNode = () => approverNodes.value.push({ id: nextApproverId++, userId: "", nickName: "" });
  const removeApproverNode = index => approverNodes.value.splice(index, 1);
  const openApproverPicker = index => {
    uni.setStorageSync("stepIndex", index);
    uni.navigateTo({
      url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect",
    });
  };
  const addProduct = () => form.value.products.push(createEmptyProduct());
  const removeProduct = index => {
    form.value.products.splice(index, 1);
@@ -285,8 +335,14 @@
  };
  const fetchModelOptions = async (productId, product) => {
    const rows = await modelList({ id: productId }).catch(() => []);
    product.modelOptions = Array.isArray(rows) ? rows : [];
    try {
      const res = await modelList({ id: productId });
      const rows = res?.data?.records || res?.data || res?.records || res || [];
      product.modelOptions = Array.isArray(rows) ? rows : [];
    } catch (error) {
      console.error("获取规格型号失败:", error);
      product.modelOptions = [];
    }
  };
  const openProductPicker = index => {
@@ -300,7 +356,11 @@
      uni.showToast({ title: "请先选择产品", icon: "none" });
      return;
    }
    modelActions.value = (current.modelOptions || []).map(item => ({ name: item.model, value: item.id, unit: item.unit }));
    modelActions.value = (current.modelOptions || []).map(item => ({
      name: item.model || item.specification,
      value: item.id,
      unit: item.unit,
    }));
    if (!modelActions.value.length) {
      uni.showToast({ title: "暂无规格型号", icon: "none" });
      return;
@@ -309,27 +369,21 @@
  };
  const onSelectCustomer = action => {
    form.value.customer = action.value;
    form.value.customerId = action.value;
    form.value.customer = action.name;
    showCustomerSheet.value = false;
  };
  const onSelectSalesperson = action => {
    form.value.salesperson = action.value;
    showSalespersonSheet.value = false;
  };
  const onSelectApprover = data => {
    const { stepIndex, contact } = data || {};
    if (stepIndex === undefined || !contact) return;
    if (!approverNodes.value[stepIndex]) return;
    approverNodes.value[stepIndex].userId = contact.userId;
    approverNodes.value[stepIndex].nickName = contact.nickName;
  };
  const onSelectProduct = action => {
    const current = form.value.products[currentProductIndex.value];
    if (!current) return;
    current.productId = action.value;
    current.product = action.label;
    current.specificationId = "";
    current.specification = "";
    current.productModelId = "";
    current.ProductModel = "";
    current.unit = "";
    current.modelOptions = [];
    showProductSheet.value = false;
@@ -338,8 +392,8 @@
  const onSelectModel = action => {
    const current = form.value.products[currentProductIndex.value];
    if (!current) return;
    current.specificationId = action.value;
    current.specification = action.name;
    current.productModelId = action.value;
    current.ProductModel = action.name;
    current.unit = action.unit || current.unit;
    showModelSheet.value = false;
  };
@@ -353,70 +407,114 @@
  };
  const fetchBaseOptions = async () => {
      const [customers, users, productTree] = await Promise.all([
        getCustomerList({ current: -1, size: -1 }).catch(() => ({})),
        userListNoPageByTenantId().catch(() => ({})),
        productTreeList().catch(() => []),
      ]);
    const [customers, users, productTree] = await Promise.all([
      getCustomerList({ current: -1, size: -1 }).catch(() => ({})),
      userListNoPageByTenantId().catch(() => ({})),
      productTreeList().catch(() => []),
    ]);
    customerList.value = customers?.data?.records || customers?.records || [];
    const userRows = users?.data || [];
    salespersonList.value = Array.isArray(userRows) ? userRows : [];
    productList.value = flattenProductTree(Array.isArray(productTree) ? productTree : productTree?.data || []);
    productList.value = flattenProductTree(
      Array.isArray(productTree) ? productTree : productTree?.data || []
    );
  };
  // 根据名称反查节点 id,便于仅存名称时的反显
  const findNodeIdByLabel = (nodes, label) => {
    if (!label) return null;
    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i];
      if (node.label === label) return node.value;
      if (node.children && node.children.length > 0) {
        const found = findNodeIdByLabel(node.children, label);
        if (found !== null && found !== undefined) return found;
      }
    }
    return null;
  };
  const normalizeProductRows = async rows => {
    const normalized = await Promise.all((Array.isArray(rows) ? rows : []).map(async item => {
      const row = {
        uid: `p_${uidSeed++}`,
        productId: item.productId || "",
        product: item.product || item.productName || "",
        specificationId: item.specificationId || "",
        specification: item.specification || "",
        unit: item.unit || "",
        quantity: Number(item.quantity || 1),
        unitPrice: Number(item.unitPrice || 0),
        amount: Number(item.amount || 0),
        modelOptions: [],
      };
      if (row.productId) await fetchModelOptions(row.productId, row);
      return row;
    }));
    const normalized = await Promise.all(
      (Array.isArray(rows) ? rows : []).map(async item => {
        const productName = item.product || item.productName || "";
        // 优先用 productId;如果只有名称,尝试反查 id 以便选择器反显
        let resolvedProductId =
          item.productId ||
          findNodeIdByLabel(productList.value, productName) ||
          "";
        const row = {
          uid: `p_${uidSeed++}`,
          productId: resolvedProductId,
          product: productName,
          productModelId: item.productModelId || "",
          ProductModel: item.ProductModel || item.specification || "",
          unit: item.unit || "",
          quantity: Number(item.quantity || 1),
          unitPrice: Number(item.unitPrice || 0),
          amount: Number(item.amount || 0),
          modelOptions: [],
        };
        if (row.productId) {
          await fetchModelOptions(row.productId, row);
          // 如果没有 productModelId 但有 ProductModel 名称,尝试从 modelOptions 中匹配 ID
          if (!row.productModelId && row.ProductModel) {
            const foundModel = row.modelOptions.find(
              m =>
                m.model === row.ProductModel ||
                m.specification === row.ProductModel
            );
            if (foundModel) {
              row.productModelId = foundModel.id;
              // 统一使用 modelOptions 中的字段
              row.ProductModel =
                foundModel.model || foundModel.specification || row.ProductModel;
              row.unit = foundModel.unit || row.unit;
            }
          }
        }
        return row;
      })
    );
    form.value.products = normalized;
  };
  const loadDetail = async () => {
    if (!quotationId.value) return;
    uni.showLoading({ title: "加载中...", mask: true });
    try {
      const res = await getQuotationDetail({ id: quotationId.value });
      const data = res?.data || {};
    // 直接从本地存储获取数据,不再调用详情接口
    const cachedData = uni.getStorageSync("salesQuotationDetail");
    if (
      cachedData &&
      (cachedData.id === quotationId.value ||
        cachedData.id === Number(quotationId.value))
    ) {
      const data = cachedData;
      form.value = {
        ...form.value,
        id: data.id,
        quotationNo: data.quotationNo || "",
        customerId: data.customerId,
        customer: data.customer || "",
        salesperson: data.salesperson || "",
        quotationDate: data.quotationDate || "",
        validDate: data.validDate || "",
        paymentMethod: data.paymentMethod || "",
        status: data.status || "待审批",
        status: data.status || "草稿",
        remark: data.remark || "",
        subtotal: data.subtotal || 0,
        freight: data.freight || 0,
        otherFee: data.otherFee || 0,
        discountRate: data.discountRate || 0,
        discountAmount: data.discountAmount || 0,
        totalAmount: data.totalAmount || 0,
      };
      await normalizeProductRows(data.products || []);
      if (data.approveUserIds) {
        const ids = String(data.approveUserIds).split(",").map(item => item.trim()).filter(Boolean);
        approverNodes.value = ids.map((userId, index) => ({
          id: index + 1,
          userId,
          nickName: salespersonList.value.find(item => String(item.userId) === String(userId))?.nickName || "",
        }));
        nextApproverId = approverNodes.value.length + 1;
      }
      form.value.totalAmount = totalAmount.value;
    } catch {
      uni.showToast({ title: "获取详情失败", icon: "error" });
    } finally {
      uni.hideLoading();
    } else {
      console.warn("未找到缓存的报价单详情数据");
    }
  };
@@ -425,16 +523,16 @@
      uni.showToast({ title: "请至少添加一个产品", icon: "none" });
      return false;
    }
    const invalid = form.value.products.some(item => !item.productId || !item.specificationId || !item.unit || !Number(item.quantity) || !Number(item.unitPrice));
    const invalid = form.value.products.some(
      item =>
        !item.productId ||
        !item.productModelId ||
        !item.unit ||
        !Number(item.quantity) ||
        !Number(item.unitPrice)
    );
    if (invalid) {
      uni.showToast({ title: "请完善产品信息", icon: "none" });
      return false;
    }
    return true;
  };
  const validateApprovers = () => {
    if (approverNodes.value.some(item => !item.userId)) {
      uni.showToast({ title: "请选择审批人", icon: "none" });
      return false;
    }
    return true;
@@ -442,17 +540,20 @@
  const handleSubmit = async () => {
    const valid = await formRef.value.validate().catch(() => false);
    if (!valid || !validateApprovers() || !validateProducts()) return;
    if (!valid || !validateProducts()) return;
    loading.value = true;
    // 同步最新的总额
    form.value.totalAmount = totalAmount.value;
    form.value.subtotal = totalAmount.value;
    const payload = {
      ...form.value,
      approveUserIds: approverNodes.value.map(item => item.userId).join(","),
      totalAmount: totalAmount.value,
      products: form.value.products.map(item => ({
        productId: item.productId,
        product: item.product,
        specificationId: item.specificationId,
        specification: item.specification,
        productModelId: item.productModelId,
        ProductModel: item.ProductModel,
        quantity: Number(item.quantity || 0),
        unit: item.unit,
        unitPrice: Number(item.unitPrice || 0),
@@ -486,16 +587,12 @@
  onMounted(async () => {
    await fetchBaseOptions();
    uni.$on("selectContact", onSelectApprover);
    if (quotationId.value) {
      await loadDetail();
    }
  });
  onUnmounted(() => {
    uni.$off("selectContact", onSelectApprover);
    uni.removeStorageSync("stepIndex");
  });
  onUnmounted(() => {});
</script>
<style scoped lang="scss">
@@ -547,7 +644,6 @@
    padding: 12px 12px 0;
  }
  .node-list,
  .product-list {
    padding: 12px;
    display: flex;
@@ -555,31 +651,12 @@
    gap: 12px;
  }
  .node-card {
    background: #f8fbff;
    border-radius: 12px;
    padding: 12px;
    border: 1px solid #e6eef8;
  }
  .picker-field {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .picker-field :deep(.u-input) {
    flex: 1;
  }
  .node-top,
  .product-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .node-title,
  .product-title {
    font-size: 14px;
    font-weight: 600;
src/pages/sales/salesQuotation/index.vue
@@ -1,47 +1,50 @@
<template>
  <view class="sales-account">
    <PageHeader title="销售报价" @back="goBack" />
    <PageHeader title="销售报价"
                @back="goBack" />
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input
            class="search-text"
            v-model="quotationNo"
            placeholder="请输入报价单号搜索"
            clearable
            @change="getList"
          />
          <up-input class="search-text"
                    v-model="quotationNo"
                    placeholder="请输入报价单号搜索"
                    clearable
                    @change="getList" />
        </view>
        <view class="filter-button" @click="getList">
          <up-icon name="search" size="24" color="#999"></up-icon>
        <view class="filter-button"
              @click="getList">
          <up-icon name="search"
                   size="24"
                   color="#999"></up-icon>
        </view>
      </view>
    </view>
    <view class="tabs-section">
      <up-tabs
        v-model="tabValue"
        :list="tabList"
        itemStyle="width: 20%;height: 80rpx;"
        @change="onTabChange"
      />
      <up-tabs v-model="tabValue"
               :list="tabList"
               itemStyle="width: 20%;height: 80rpx;"
               @change="onTabChange" />
    </view>
    <view v-if="quotationList.length > 0" class="ledger-list">
      <view v-for="item in quotationList" :key="item.id" class="ledger-item">
    <view v-if="quotationList.length > 0"
          class="ledger-list">
      <view v-for="item in quotationList"
            :key="item.id"
            class="ledger-item">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
              <up-icon name="file-text"
                       size="16"
                       color="#ffffff"></up-icon>
            </view>
            <text class="item-id">{{ item.quotationNo || "-" }}</text>
          </view>
          <text class="item-index">{{ item.status || "-" }}</text>
          <up-tag :text="item.status || '待审批'"
                  :type="getStatusType(item.status)"
                  size="mini"
                  shape="circle" />
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">客户名称</text>
@@ -60,43 +63,45 @@
            <text class="detail-value">{{ item.validDate || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">付款方式</text>
            <text class="detail-value">{{ item.paymentMethod || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">报价金额</text>
            <text class="detail-value highlight">{{ formatAmount(item.totalAmount) }}</text>
          </view>
          <view class="detail-row">
          <view class="detail-row"
                v-if="item.remark">
            <text class="detail-label">备注</text>
            <text class="detail-value">{{ item.remark || "-" }}</text>
            <text class="detail-value">{{ item.remark }}</text>
          </view>
        </view>
        <view class="action-buttons">
                    <up-button
                        class="action-btn"
                size="small"
                type="primary"
                :disabled="!canEdit(item)"
                @click="goEdit(item)"
                    >
                        编辑
                    </up-button>
          <up-button class="action-btn" size="small" @click="goDetail(item)">详情</up-button>
          <up-button class="action-btn" size="small" type="error" plain @click="handleDelete(item)">
          <up-button class="action-btn"
                     size="small"
                     type="primary"
                     :disabled="!canEdit(item)"
                     @click="goEdit(item)">
            编辑
          </up-button>
          <up-button class="action-btn"
                     size="small"
                     @click="goDetail(item)">详情</up-button>
          <up-button class="action-btn"
                     size="small"
                     type="error"
                     plain
                     @click="handleDelete(item)">
            删除
          </up-button>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
    <view v-else
          class="no-data">
      <text>暂无销售报价数据</text>
    </view>
    <view class="fab-button" @click="goAdd">
      <up-icon name="plus" size="28" color="#ffffff"></up-icon>
    <view class="fab-button"
          @click="goAdd">
      <up-icon name="plus"
               size="28"
               color="#ffffff"></up-icon>
    </view>
  </view>
</template>
@@ -105,7 +110,10 @@
  import { reactive, ref } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import { deleteQuotation, getQuotationList } from "@/api/salesManagement/salesQuotation";
  import {
    deleteQuotation,
    getQuotationList,
  } from "@/api/salesManagement/salesQuotation";
  const quotationNo = ref("");
  const quotationList = ref([]);
@@ -129,11 +137,13 @@
  };
  const goAdd = () => {
    uni.removeStorageSync("salesQuotationDetail");
    uni.navigateTo({ url: "/pages/sales/salesQuotation/edit" });
  };
  const goEdit = item => {
    if (!canEdit(item)) return;
    uni.setStorageSync("salesQuotationDetail", item || {});
    uni.navigateTo({ url: `/pages/sales/salesQuotation/edit?id=${item.id}` });
  };
@@ -159,6 +169,16 @@
    return `¥${num.toFixed(2)}`;
  };
  const getStatusType = status => {
    const statusMap = {
      待审批: "info",
      审核中: "primary",
      通过: "success",
      拒绝: "danger",
    };
    return statusMap[status] || "info";
  };
  const getList = () => {
    uni.showLoading({ title: "加载中...", mask: true });
    getQuotationList({