gaoluyang
2026-04-27 36c8ae70cae3de90e642b080553abe70d3345c74
天津宝东app
1.部署修改
2.按照web端逻辑修改销售报价和客户档案的逻辑
已添加1个文件
已修改11个文件
1356 ■■■■■ 文件已修改
src/api/basicData/customerFile.js 94 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesQuotationProduct.js 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/manifest.json 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/basicData/customerFile/detail.vue 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/basicData/customerFile/edit.vue 80 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/basicData/customerFile/index.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/detail.vue 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/edit.vue 551 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/index.vue 170 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/works.vue 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/versionUpgrade.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/customerFile.js
@@ -1,7 +1,5 @@
// å®¢æˆ·æ¡£æ¡ˆé¡µé¢æŽ¥å£
import request from '@/utils/request'
// åˆ†é¡µæŸ¥è¯¢
export function listCustomer(query) {
    return request({
        url: '/basic/customer/list',
@@ -9,14 +7,76 @@
        params: query
    })
}
// æŸ¥è¯¢å®¢æˆ·æ¡£æ¡ˆè¯¦ç»†
// å®¢æˆ·æ¡£æ¡ˆç§æµ·æŸ¥è¯¢
export function listCustomerPrivatePool(query) {
    return request({
        url: '/customerPrivatePool/listPage',
        method: 'get',
        params: query
    })
}
export function addCustomerPrivatePool(data) {
    return request({
        url: '/customerPrivatePool/add',
        method: 'post',
        data: data
    })
}
export function addCustomerPrivate(data) {
    return request({
        url: '/customerPrivate/add',
        method: 'post',
        data: data
    })
}
export function delCustomerPrivate(ids) {
    return request({
        url: '/customerPrivate/delete',
        method: 'delete',
        data: ids
    })
}
export function delCustomerPrivatePool(id) {
    return request({
        url: '/customerPrivatePool/delete/' + id,
        method: 'delete',
    })
}
export function shareCustomer(data) {
    return request({
        url: '/customerPrivatePool/together',
        method: 'post',
        data: data
    })
}
export function getCustomer(id) {
    return request({
        url: '/basic/customer/' + id,
        method: 'get'
    })
}
// æ–°å¢žå®¢æˆ·æ¡£æ¡ˆ
export function getCustomerPrivatePoolById(id) {
    return request({
        url: '/customerPrivatePool/getbyId/' + id,
        method: 'get'
    })
}
export function getCustomerPrivatePoolInfo(id) {
    return request({
        url: '/customerPrivatePool/info/' + id,
        method: 'get'
    })
}
export function addCustomer(data) {
    return request({
        url: '/basic/customer/addCustomer',
@@ -24,7 +84,7 @@
        data: data
    })
}
// ä¿®æ”¹å®¢æˆ·æ¡£æ¡ˆ
export function updateCustomer(data) {
    return request({
        url: '/basic/customer/updateCustomer',
@@ -32,7 +92,15 @@
        data: data
    })
}
// å¯¼å‡ºå®¢æˆ·æ¡£æ¡ˆ
export function updateCustomerPrivatePool(data) {
    return request({
        url: '/customerPrivatePool/update',
        method: 'put',
        data: data
    })
}
export function exportCustomer(query) {
    return request({
        url: '/basic/customer/export',
@@ -41,7 +109,7 @@
        responseType: 'blob'
    })
}
// åˆ é™¤å®¢æˆ·æ¡£æ¡ˆ
export function delCustomer(ids) {
    return request({
        url: '/basic/customer/delCustomer',
@@ -50,8 +118,6 @@
    })
}
// æ–°å¢žå®¢æˆ·è·Ÿè¿›
export function addCustomerFollow(data) {
    return request({
        url: '/basic/customer-follow/add',
@@ -60,7 +126,6 @@
    })
}
// ä¿®æ”¹å®¢æˆ·è·Ÿè¿›
export function updateCustomerFollow(data) {
    return request({
        url: '/basic/customer-follow/edit',
@@ -68,15 +133,14 @@
        data: data,
    })
}
// åˆ é™¤å®¢æˆ·è·Ÿè¿›
export function delCustomerFollow(id) {
    return request({
        url: '/basic/customer-follow/'+id,
        url: '/basic/customer-follow/' + id,
        method: 'delete',
    })
}
// å›žè®¿æé†’-新增/更新
export function addReturnVisit(data) {
    return request({
        url: '/basic/customer-follow/return-visit',
@@ -84,10 +148,10 @@
        data: data
    })
}
// èŽ·å–å›žè®¿æé†’è¯¦æƒ…
export function getReturnVisit(id) {
    return request({
        url: '/basic/customer-follow/return-visit/' + id,
        method: 'get'
    })
}
}
src/api/salesManagement/salesQuotationProduct.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
import request from "@/utils/request";
export function quotationProductListPage(query) {
  return request({
    url: "/sales/quotationProduct/listPage",
    method: "get",
    params: query,
  });
}
export function quotationProductList(query) {
  return request({
    url: "/sales/quotationProduct/list",
    method: "get",
    params: query,
  });
}
export function getQuotationProductInfo(id) {
  return request({
    url: `/sales/quotationProduct/${id}`,
    method: "get",
  });
}
export function addOrUpdateQuotationProduct(data) {
  return request({
    url: "/sales/quotationProduct/addOrUpdate",
    method: "post",
    data,
  });
}
export function editQuotationProduct(data) {
  return request({
    url: "/sales/quotationProduct/edit",
    method: "post",
    data,
  });
}
export function deleteQuotationProduct(ids) {
  return request({
    url: "/sales/quotationProduct/delete",
    method: "delete",
    data: ids,
  });
}
src/config.js
@@ -1,7 +1,7 @@
// åº”用全局配置
const config = {
  baseUrl: "http://1.15.17.182:9003",
  fileUrl: "http://1.15.17.182:9002",
  baseUrl: "http://1.15.17.182:9046",
  fileUrl: "http://1.15.17.182:9047",
  // åº”用信息
  appInfo: {
    // åº”用名称
src/manifest.json
@@ -1,6 +1,6 @@
{
    "name" : "信息管理",
    "appid" : "__UNI__099A590",
    "name" : "天津宝东",
    "appid" : "__UNI__FDF4041",
    "description" : "",
    "versionName" : "1.1.5",
    "versionCode" : 100,
src/pages/basicData/customerFile/detail.vue
@@ -15,28 +15,12 @@
            <text class="info-value">{{ detailData.customerType || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">纳税人识别号</text>
            <text class="info-value">{{ detailData.taxpayerIdentificationNumber || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">公司地址</text>
            <text class="info-value">{{ detailData.companyAddress || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">公司电话</text>
            <text class="info-value">{{ detailData.companyPhone || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">法人</text>
            <text class="info-value">{{ detailData.corporation || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">代理人</text>
            <text class="info-value">{{ detailData.agent || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">传真</text>
            <text class="info-value">{{ detailData.fax || "-" }}</text>
          </view>
        </view>
      </view>
@@ -52,27 +36,9 @@
            <text class="info-label">联系电话</text>
            <text class="info-value">{{ detailData.contactPhone || "-" }}</text>
          </view>
        </view>
      </view>
      <view class="section">
        <view class="section-title">银行信息</view>
        <view class="info-list">
          <view class="info-item">
            <text class="info-label">银行基本户</text>
            <text class="info-value">{{ detailData.basicBankAccount || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">银行账号</text>
            <text class="info-value">{{ detailData.bankAccount || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">开户银行</text>
            <text class="info-value">{{ detailData.bankName || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">开户行号</text>
            <text class="info-value">{{ detailData.bankCode || "-" }}</text>
            <text class="info-label">联系人岗位</text>
            <text class="info-value">{{ detailData.contactPosition || "-" }}</text>
          </view>
        </view>
      </view>
src/pages/basicData/customerFile/edit.vue
@@ -19,7 +19,7 @@
              clearable
            />
          </up-form-item>
          <up-form-item label="客户分类" prop="customerType" required>
          <up-form-item label="客户分类" prop="customerType">
            <up-input
              v-model="customerTypeText"
              placeholder="请选择客户分类"
@@ -30,48 +30,17 @@
              <up-icon name="arrow-right" @click="showCustomerTypeSheet = true"></up-icon>
            </template>
          </up-form-item>
          <up-form-item
            label="纳税人识别号"
            prop="taxpayerIdentificationNumber"
          >
            <up-input
              v-model="form.taxpayerIdentificationNumber"
              placeholder="请输入纳税人识别号"
              clearable
            />
          </up-form-item>
          <up-form-item label="公司地址" prop="companyAddress">
          <up-form-item label="公司地址" prop="companyAddress" required>
            <up-input
              v-model="form.companyAddress"
              placeholder="请输入公司地址"
              clearable
            />
          </up-form-item>
          <up-form-item label="公司电话" prop="companyPhone">
          <up-form-item label="公司电话" prop="companyPhone" required>
            <up-input
              v-model="form.companyPhone"
              placeholder="请输入公司电话"
              clearable
            />
          </up-form-item>
          <up-form-item label="法人" prop="corporation">
            <up-input
              v-model="form.corporation"
              placeholder="请输入法人"
              clearable
            />
          </up-form-item>
          <up-form-item label="代理人" prop="agent">
            <up-input
              v-model="form.agent"
              placeholder="请输入代理人"
              clearable
            />
          </up-form-item>
          <up-form-item label="传真" prop="fax">
            <up-input
              v-model="form.fax"
              placeholder="请输入传真"
              clearable
            />
          </up-form-item>
@@ -92,34 +61,10 @@
              clearable
            />
          </up-form-item>
        </u-cell-group>
        <u-cell-group title="银行信息" class="form-section">
          <up-form-item label="银行基本户" prop="basicBankAccount">
          <up-form-item label="联系人岗位" prop="contactPosition">
            <up-input
              v-model="form.basicBankAccount"
              placeholder="请输入银行基本户"
              clearable
            />
          </up-form-item>
          <up-form-item label="银行账号" prop="bankAccount">
            <up-input
              v-model="form.bankAccount"
              placeholder="请输入银行账号"
              clearable
            />
          </up-form-item>
          <up-form-item label="开户银行" prop="bankName">
            <up-input
              v-model="form.bankName"
              placeholder="请输入开户银行"
              clearable
            />
          </up-form-item>
          <up-form-item label="开户行号" prop="bankCode">
            <up-input
              v-model="form.bankCode"
              placeholder="请输入开户行号"
              v-model="form.contactPosition"
              placeholder="请输入联系人岗位"
              clearable
            />
          </up-form-item>
@@ -178,30 +123,25 @@
  const form = ref({
    customerName: "",
    customerType: "",
    taxpayerIdentificationNumber: "",
    companyAddress: "",
    companyPhone: "",
    corporation: "",
    agent: "",
    fax: "",
    contactPerson: "",
    contactPhone: "",
    basicBankAccount: "",
    bankAccount: "",
    bankName: "",
    bankCode: "",
    contactPosition: "",
    maintainer: "",
    maintenanceTime: "",
  });
  const rules = {
    customerName: [{ required: true, message: "请输入客户名称", trigger: "blur" }],
    customerType: [{ required: true, message: "请选择客户分类", trigger: "change" }],
    companyAddress: [{ required: true, message: "请输入公司地址", trigger: "blur" }],
    companyPhone: [{ required: true, message: "请输入公司电话", trigger: "blur" }],
  };
  const customerTypeActions = [
    { name: "零售客户", value: "零售客户" },
    { name: "经销商客户", value: "经销商客户" },
    { name: "连锁店", value: "连锁店" },
  ];
  const pageTitle = computed(() =>
src/pages/basicData/customerFile/index.vue
@@ -23,7 +23,7 @@
      <up-tabs
        v-model="tabValue"
        :list="tabList"
        itemStyle="width: 33.33%;height: 80rpx;"
        itemStyle="width: 25%;height: 80rpx;"
        @change="onTabChange"
      />
    </view>
@@ -44,10 +44,6 @@
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">纳税人识别号</text>
            <text class="detail-value">{{ item.taxpayerIdentificationNumber || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">公司电话</text>
            <text class="detail-value">{{ item.companyPhone || "-" }}</text>
          </view>
@@ -56,12 +52,16 @@
            <text class="detail-value">{{ item.companyAddress || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">法人</text>
            <text class="detail-value">{{ item.corporation || "-" }}</text>
            <text class="detail-label">联系人</text>
            <text class="detail-value">{{ item.contactPerson || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">代理人</text>
            <text class="detail-value">{{ item.agent || "-" }}</text>
            <text class="detail-label">联系电话</text>
            <text class="detail-value">{{ item.contactPhone || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">联系人岗位</text>
            <text class="detail-value">{{ item.contactPosition || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">维护人</text>
@@ -106,6 +106,7 @@
    { name: "全部客户", value: "" },
    { name: "零售客户", value: "零售客户" },
    { name: "经销商客户", value: "经销商客户" },
    { name: "连锁店", value: "连锁店" },
  ]);
  const tabValue = ref(0);
src/pages/sales/salesQuotation/detail.vue
@@ -6,142 +6,109 @@
      <view class="section">
        <view class="section-title">基础信息</view>
        <view class="info-list">
          <view class="info-item">
            <text class="info-label">报价单号</text>
            <text class="info-value">{{ detailData.quotationNo || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">客户名称</text>
            <text class="info-value">{{ detailData.customer || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">业务员</text>
            <text class="info-value">{{ detailData.salesperson || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">报价日期</text>
            <text class="info-value">{{ detailData.quotationDate || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">有效期至</text>
            <text class="info-value">{{ detailData.validDate || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">付款方式</text>
            <text class="info-value">{{ detailData.paymentMethod || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">审批状态</text>
            <text class="info-value">{{ detailData.status || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">报价总额</text>
            <text class="info-value highlight">{{ formatAmount(detailData.totalAmount) }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">备注</text>
            <text class="info-value">{{ detailData.remark || "-" }}</text>
          </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 class="info-item"><text class="info-label">报价单号</text><text class="info-value">{{ detailData.quotationNo || "-" }}</text></view>
          <view class="info-item"><text class="info-label">客户</text><text class="info-value">{{ detailData.customer || "-" }}</text></view>
          <view class="info-item"><text class="info-label">业务员</text><text class="info-value">{{ detailData.salesperson || "-" }}</text></view>
          <view class="info-item"><text class="info-label">报价日期</text><text class="info-value">{{ detailData.quotationDate || "-" }}</text></view>
          <view class="info-item"><text class="info-label">有效期至</text><text class="info-value">{{ detailData.validDate || "-" }}</text></view>
          <view class="info-item"><text class="info-label">付款方式</text><text class="info-value">{{ detailData.paymentMethod || "-" }}</text></view>
          <view class="info-item"><text class="info-label">总额</text><text class="info-value highlight">{{ formatAmount(totalAmount) }}</text></view>
          <view class="info-item"><text class="info-label">备注</text><text class="info-value">{{ detailData.remark || "-" }}</text></view>
        </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="products.length" class="product-list">
          <view v-for="(item, index) in products" :key="index" class="product-card">
            <view class="product-head">产品 {{ index + 1 }}</view>
            <view class="info-item">
              <text class="info-label">产品名称</text>
              <text class="info-value">{{ item.product || item.productName || "-" }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">规格型号</text>
              <text class="info-value">{{ item.specification || "-" }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">单位</text>
              <text class="info-value">{{ item.unit || "-" }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">数量</text>
              <text class="info-value">{{ item.quantity || "-" }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">单价</text>
              <text class="info-value">{{ formatAmount(item.unitPrice) }}</text>
            </view>
            <view class="info-item">
              <text class="info-label">金额</text>
              <text class="info-value highlight">{{ formatAmount(item.amount) }}</text>
            </view>
            <view class="info-item"><text class="info-label">产品</text><text class="info-value">{{ item.product || item.productName || "-" }}</text></view>
            <view class="info-item"><text class="info-label">规格</text><text class="info-value">{{ item.specification || "-" }}</text></view>
            <view class="info-item"><text class="info-label">单位</text><text class="info-value">{{ item.unit || "-" }}</text></view>
            <view class="info-item"><text class="info-label">纸张</text><text class="info-value">{{ item.paper || "-" }}</text></view>
            <view class="info-item"><text class="info-label">定量</text><text class="info-value">{{ item.paperWeight || "-" }}</text></view>
            <view class="info-item"><text class="info-label">数量</text><text class="info-value">{{ Number(item.quantity || 0) }}</text></view>
            <view class="info-item"><text class="info-label">单价</text><text class="info-value">{{ formatAmount(item.unitPrice) }}</text></view>
            <view class="info-item"><text class="info-label">印版费</text><text class="info-value">{{ formatAmount(item.printingFee) }}</text></view>
            <view class="info-item"><text class="info-label">刀版费</text><text class="info-value">{{ formatAmount(item.dieCuttingFee) }}</text></view>
            <view class="info-item"><text class="info-label">磨具费</text><text class="info-value">{{ formatAmount(item.grindingFee) }}</text></view>
            <view class="info-item"><text class="info-label">金额</text><text class="info-value highlight">{{ formatAmount(item.amount) }}</text></view>
          </view>
        </view>
        <view v-else class="empty-box">
          <text>暂无产品明细</text>
        </view>
        <view v-else class="empty-box"><text>暂无产品明细</text></view>
      </view>
    </view>
    <FooterButtons cancelText="返回" confirmText="编辑" @cancel="goBack" @confirm="goEdit" />
    <view class="detail-footer">
      <up-button type="primary" @click="goBack">返回</up-button>
    </view>
  </view>
</template>
<script setup>
  import { computed, ref } from "vue";
  import { onLoad, onShow } from "@dcloudio/uni-app";
  import FooterButtons from "@/components/FooterButtons.vue";
  import PageHeader from "@/components/PageHeader.vue";
  import { getQuotationDetail } from "@/api/salesManagement/salesQuotation";
  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 products = computed(() => {
    const rows = detailData.value?.products;
    if (Array.isArray(rows) && rows.length) return rows;
    if (detailData.value?.product || detailData.value?.productName) return [detailData.value];
    return [];
  });
  const goBack = () => {
    uni.navigateBack();
  };
  const totalAmount = computed(() => {
    const backendTotal = Number(detailData.value?.totalAmount || 0);
    if (backendTotal > 0) return backendTotal;
    return Number(
      products.value
        .reduce((sum, item) => {
          const unitPrice = Number(item?.unitPrice || 0);
          const printingFee = Number(item?.printingFee || 0);
          const dieCuttingFee = Number(item?.dieCuttingFee || 0);
          const grindingFee = Number(item?.grindingFee || 0);
          return sum + unitPrice + printingFee + dieCuttingFee + grindingFee;
        }, 0)
        .toFixed(2)
    );
  });
  const goEdit = () => {
    if (!quotationId.value) return;
    uni.navigateTo({ url: `/pages/sales/salesQuotation/edit?id=${quotationId.value}` });
  };
  const goBack = () => uni.navigateBack();
  const formatAmount = amount => `Â¥${Number(amount || 0).toFixed(2)}`;
  const loadDetailFromStorage = () => {
    const cachedData = uni.getStorageSync("salesQuotationDetail");
    detailData.value = cachedData || {};
    if (cachedData && typeof cachedData === "object") detailData.value = cachedData;
  };
  const loadDetailFromApi = () => {
    if (!quotationId.value) return Promise.resolve();
    uni.showLoading({ title: "加载中...", mask: true });
    return getQuotationDetail({ id: quotationId.value })
      .then(res => {
        detailData.value = res?.data || detailData.value || {};
      })
      .catch(() => {
        uni.showToast({ title: "加载详情失败", icon: "error" });
      })
      .finally(() => {
        uni.hideLoading();
      });
  };
  onLoad(options => {
    if (options?.id) {
      quotationId.value = options.id;
    }
    if (options?.id) quotationId.value = options.id;
    loadDetailFromStorage();
    loadDetailFromApi();
  });
  onShow(() => {
    loadDetailFromStorage();
    loadDetailFromApi();
  });
</script>
@@ -232,4 +199,15 @@
    color: #22324d;
    border-bottom: 1px solid #eef2f7;
  }
  .detail-footer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
    background: #fff;
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
    z-index: 10;
  }
</style>
src/pages/sales/salesQuotation/edit.vue
@@ -3,246 +3,122 @@
    <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" />
            <template #right>
              <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"
            />
            <template #right>
              <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"
            />
            <template #right>
              <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"
            />
            <template #right>
              <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>
          <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">
          <view class="section-tools">
            <up-button type="primary" size="small" text="新增节点" @click="addApproverNode" />
          </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>
      <up-form ref="formRef" :model="form" label-width="110" input-align="right" error-message-align="right">
        <u-cell-group title="产品信息" class="form-section">
          <view class="section-tools">
            <up-button type="primary" size="small" text="新增产品" @click="addProduct" />
            <up-button type="primary" size="small" text="新增产品" :disabled="isEditMode" @click="addProduct" />
          </view>
          <view v-if="form.products.length === 0" class="empty-text">
            <text>暂无产品,请先添加产品</text>
          </view>
          <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-for="(product, index) in form.products" :key="product.uid || index" 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>
              </view>
              <up-divider></up-divider>
              <view class="product-body">
                <up-form-item label="产品名称">
                  <up-input
                    v-model="product.product"
                    placeholder="请选择产品"
                    readonly
                    @click="openProductPicker(index)"
                  />
                  <template #right>
                    <up-icon name="arrow-right" @click="openProductPicker(index)"></up-icon>
                  </template>
                <up-form-item label="产品">
                  <up-input v-model="product.product" placeholder="请选择产品" readonly @click="openProductPicker(index)" />
                  <template #right><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)"
                  />
                  <template #right>
                    <up-icon name="arrow-right" @click="openModelPicker(index)"></up-icon>
                  </template>
                <up-form-item label="规格">
                  <up-input v-model="product.specification" placeholder="请选择规格" readonly @click="openModelPicker(index)" />
                  <template #right><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-form-item>
                <up-form-item label="单位"><up-input v-model="product.unit" placeholder="请输入单位" clearable /></up-form-item>
                <up-form-item label="纸张"><up-input v-model="product.paper" placeholder="请输入纸张" clearable /></up-form-item>
                <up-form-item label="定量"><up-input v-model="product.paperWeight" 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 v-model="product.printingFee" type="number" placeholder="请输入印版费" clearable @blur="syncTotalAmount" />
                </up-form-item>
                <up-form-item label="刀版费">
                  <up-input v-model="product.dieCuttingFee" type="number" placeholder="请输入刀版费" clearable @blur="syncTotalAmount" />
                </up-form-item>
                <up-form-item label="磨具费">
                  <up-input v-model="product.grindingFee" type="number" placeholder="请输入磨具费" clearable @blur="syncTotalAmount" />
                </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-textarea v-model="form.remark" placeholder="请输入备注(选填)" auto-height />
          </up-form-item>
        </u-cell-group>
        <u-cell-group title="汇总" class="form-section">
          <up-form-item label="报价总额">
            <up-input :model-value="formatAmount(totalAmount)" disabled placeholder="自动汇总" />
          </up-form-item>
          <view class="summary-tip">总额规则:单价 + å°ç‰ˆè´¹ + åˆ€ç‰ˆè´¹ + ç£¨å…·è´¹ï¼ˆæŒ‰äº§å“é€è¡Œæ±‚和)</view>
        </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" />
    <up-action-sheet :show="showModelSheet" title="选择规格" :actions="modelActions" @select="onSelectModel" @close="showModelSheet = false" />
  </view>
</template>
<script setup>
  import { computed, onMounted, onUnmounted, ref } from "vue";
  import { computed, onMounted, ref } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import FooterButtons from "@/components/FooterButtons.vue";
  import PageHeader from "@/components/PageHeader.vue";
  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 { addOrUpdateQuotationProduct, editQuotationProduct } from "@/api/salesManagement/salesQuotationProduct";
  const formRef = ref();
  const loading = ref(false);
  const quotationId = ref("");
  const showCustomerSheet = ref(false);
  const showSalespersonSheet = ref(false);
  const showProductSheet = ref(false);
  const showModelSheet = ref(false);
  const showQuotationDatePicker = ref(false);
  const showValidDatePicker = ref(false);
  const quotationDateValue = ref(Date.now());
  const validDateValue = ref(Date.now());
  const currentProductIndex = ref(-1);
  const customerList = ref([]);
  const salespersonList = ref([]);
  const productList = ref([]);
  const modelActions = ref([]);
  let uidSeed = 1;
  let nextApproverId = 2;
  const form = ref({
    id: undefined,
    quotationNo: "",
    customer: "",
    salesperson: "",
    quotationDate: "",
    validDate: "",
    paymentMethod: "",
    status: "待审批",
    remark: "",
    approveUserIds: "",
    products: [],
    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" }],
    validDate: [{ required: true, message: "请选择有效期", trigger: "change" }],
    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))
  );
  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 isEditMode = computed(() => Boolean(quotationId.value));
  const productActions = computed(() => productList.value.map(item => ({ name: item.label, value: item.value, label: item.label })));
  const totalAmount = computed(() => calcTotalAmountFromProducts(form.value.products));
  const createEmptyProduct = () => ({
    uid: `p_${uidSeed++}`,
    id: "",
    salesQuotationId: "",
    productId: "",
    product: "",
    specificationId: "",
    specification: "",
    unit: "",
    paper: "",
    paperWeight: "",
    quantity: 1,
    unitPrice: 0,
    printingFee: 0,
    dieCuttingFee: 0,
    grindingFee: 0,
    amount: 0,
    modelOptions: [],
  });
@@ -251,37 +127,56 @@
    const result = [];
    const walk = list => {
      (list || []).forEach(item => {
        if (item.children && item.children.length) {
          walk(item.children);
        } else {
          result.push({ label: item.label || item.productName || "", value: item.id || item.value });
        }
        if (item.children && item.children.length) walk(item.children);
        else result.push({ label: item.label || item.productName || "", value: item.id || item.value });
      });
    };
    walk(nodes);
    return result;
  };
  const findProductIdByLabel = label => {
    if (!label) return "";
    const hit = (productList.value || []).find(item => item.label === label);
    return hit?.value || "";
  };
  const formatAmount = amount => `Â¥${Number(amount || 0).toFixed(2)}`;
  const goBack = () => uni.navigateBack();
  const calculateAmount = product => {
    product.amount = Number((Number(product.quantity || 0) * Number(product.unitPrice || 0)).toFixed(2));
  const calcTotalAmountFromProducts = products =>
    Number(
      (products || [])
        .reduce((sum, item) => {
          const unitPrice = Number(item?.unitPrice || 0);
          const printingFee = Number(item?.printingFee || 0);
          const dieCuttingFee = Number(item?.dieCuttingFee || 0);
          const grindingFee = Number(item?.grindingFee || 0);
          return sum + unitPrice + printingFee + dieCuttingFee + grindingFee;
        }, 0)
        .toFixed(2)
    );
  const syncTotalAmount = () => {
    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 calculateAmount = product => {
    product.amount = Number((Number(product.quantity || 0) * Number(product.unitPrice || 0)).toFixed(2));
    syncTotalAmount();
  };
  const addProduct = () => form.value.products.push(createEmptyProduct());
  const addProduct = () => {
    if (isEditMode.value) {
      uni.showToast({ title: "编辑模式下不允许新增产品", icon: "none" });
      return;
    }
    form.value.products.push(createEmptyProduct());
  };
  const removeProduct = index => {
    form.value.products.splice(index, 1);
    form.value.totalAmount = totalAmount.value;
    syncTotalAmount();
  };
  const fetchModelOptions = async (productId, product) => {
@@ -293,6 +188,7 @@
    currentProductIndex.value = index;
    showProductSheet.value = true;
  };
  const openModelPicker = index => {
    currentProductIndex.value = index;
    const current = form.value.products[index];
@@ -302,27 +198,12 @@
    }
    modelActions.value = (current.modelOptions || []).map(item => ({ name: item.model, value: item.id, unit: item.unit }));
    if (!modelActions.value.length) {
      uni.showToast({ title: "暂无规格型号", icon: "none" });
      uni.showToast({ title: "暂无规格数据", icon: "none" });
      return;
    }
    showModelSheet.value = true;
  };
  const onSelectCustomer = action => {
    form.value.customer = action.value;
    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;
@@ -335,6 +216,7 @@
    showProductSheet.value = false;
    fetchModelOptions(action.value, current);
  };
  const onSelectModel = action => {
    const current = form.value.products[currentProductIndex.value];
    if (!current) return;
@@ -343,81 +225,70 @@
    current.unit = action.unit || current.unit;
    showModelSheet.value = false;
  };
  const onQuotationDateConfirm = e => {
    form.value.quotationDate = formatDateToYMD(e.value);
    showQuotationDatePicker.value = false;
  };
  const onValidDateConfirm = e => {
    form.value.validDate = formatDateToYMD(e.value);
    showValidDatePicker.value = false;
  };
  const fetchBaseOptions = async () => {
      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 : [];
  const fetchProductOptions = async () => {
    const productTree = await productTreeList().catch(() => []);
    productList.value = flattenProductTree(Array.isArray(productTree) ? productTree : productTree?.data || []);
  };
  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 row = {
          uid: `p_${uidSeed++}`,
          id: item.id || "",
          salesQuotationId: item.salesQuotationId || "",
          productId: item.productId || "",
          product: item.product || item.productName || "",
          specificationId: item.specificationId || "",
          specification: item.specification || "",
          unit: item.unit || "",
          paper: item.paper || "",
          paperWeight: item.paperWeight || "",
          quantity: Number(item.quantity || 1),
          unitPrice: Number(item.unitPrice || 0),
          printingFee: Number(item.printingFee || 0),
          dieCuttingFee: Number(item.dieCuttingFee || 0),
          grindingFee: Number(item.grindingFee || 0),
          amount: Number(item.amount || Number(item.quantity || 0) * Number(item.unitPrice || 0)),
          modelOptions: [],
        };
        if (row.productId) {
          await fetchModelOptions(row.productId, row);
          if (!row.specificationId && row.specification) {
            const matchedModel = (row.modelOptions || []).find(model => model.model === row.specification);
            if (matchedModel) {
              row.specificationId = matchedModel.id;
              if (!row.unit) row.unit = matchedModel.unit || "";
            }
          }
        }
        return row;
      })
    );
    form.value.products = normalized;
  };
  const loadDetail = async () => {
  const loadEditFromStorage = async () => {
    if (!quotationId.value) return;
    uni.showLoading({ title: "加载中...", mask: true });
    try {
      const res = await getQuotationDetail({ id: quotationId.value });
      const data = res?.data || {};
      form.value = {
        ...form.value,
        id: data.id,
        quotationNo: data.quotationNo || "",
        customer: data.customer || "",
        salesperson: data.salesperson || "",
        quotationDate: data.quotationDate || "",
        validDate: data.validDate || "",
        paymentMethod: data.paymentMethod || "",
        status: data.status || "待审批",
        remark: data.remark || "",
      };
      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();
    }
    const cached = uni.getStorageSync("salesQuotationEdit");
    if (!cached || typeof cached !== "object") return;
    if (cached.id && String(cached.id) !== String(quotationId.value)) return;
    const data = cached;
    form.value = {
      ...form.value,
      id: data.id || form.value.id,
      remark: data.remark || "",
    };
    const rows = Array.isArray(data.products) && data.products.length ? data.products : [data];
    const normalizedRows = rows.map(item => ({
      ...item,
      productId: item.productId || findProductIdByLabel(item.product || item.productName || ""),
    }));
    await normalizeProductRows(normalizedRows);
    syncTotalAmount();
  };
  const validateProducts = () => {
@@ -425,42 +296,64 @@
      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.specificationId || !item.unit || !Number(item.unitPrice || 0));
    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;
  const buildProductPayload = item => {
    const quantity = Number(item?.quantity || 0);
    const unitPrice = Number(item?.unitPrice || 0);
    const printingFee = Number(item?.printingFee || 0);
    const dieCuttingFee = Number(item?.dieCuttingFee || 0);
    const grindingFee = Number(item?.grindingFee || 0);
    return {
      id: item?.id || undefined,
      salesQuotationId: item?.salesQuotationId || null,
      product: item?.product || "",
      specification: item?.specification || "",
      unit: item?.unit || "",
      paper: item?.paper || "",
      paperWeight: item?.paperWeight || "",
      unitPrice,
      printingFee,
      dieCuttingFee,
      grindingFee,
      quantity,
      amount: Number(item?.amount ?? quantity * unitPrice),
      remark: form.value.remark || "",
    };
  };
  const handleSubmit = async () => {
    const valid = await formRef.value.validate().catch(() => false);
    if (!valid || !validateApprovers() || !validateProducts()) return;
    if (!validateProducts()) return;
    loading.value = true;
    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,
        quantity: Number(item.quantity || 0),
        unit: item.unit,
        unitPrice: Number(item.unitPrice || 0),
        amount: Number(item.amount || 0),
      })),
    };
    const action = quotationId.value ? updateQuotation : addQuotation;
    action(payload)
    if (quotationId.value) {
      const editingItem = form.value.products[0] || {};
      const payload = buildProductPayload({
        ...editingItem,
        id: editingItem.id || quotationId.value,
      });
      editQuotationProduct(payload)
        .then(() => {
          uni.showToast({ title: "保存成功", icon: "success" });
          setTimeout(() => uni.navigateBack(), 300);
        })
        .catch(() => {
          uni.showToast({ title: "保存失败", icon: "error" });
        })
        .finally(() => {
          loading.value = false;
        });
      return;
    }
    const payloadList = form.value.products.map(item => buildProductPayload(item));
    addOrUpdateQuotationProduct(payloadList)
      .then(() => {
        uni.showToast({ title: "保存成功", icon: "success" });
        setTimeout(() => uni.navigateBack(), 300);
@@ -478,60 +371,21 @@
      quotationId.value = options.id;
      form.value.id = options.id;
    } else {
      const today = formatDateToYMD(Date.now());
      form.value.quotationDate = today;
      form.value.validDate = today;
      form.value.products = [];
    }
  });
  onMounted(async () => {
    await fetchBaseOptions();
    uni.$on("selectContact", onSelectApprover);
    if (quotationId.value) {
      await loadDetail();
    }
  });
  onUnmounted(() => {
    uni.$off("selectContact", onSelectApprover);
    uni.removeStorageSync("stepIndex");
    await fetchProductOptions();
    if (quotationId.value) await loadEditFromStorage();
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .account-detail {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100px;
  }
  .form-container {
    padding: 12px 12px 0;
  }
  .hero-card {
    margin-bottom: 12px;
    padding: 18px 18px 16px;
    border-radius: 16px;
    background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%);
    box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08);
  }
  .hero-title {
    display: block;
    font-size: 18px;
    font-weight: 600;
    color: #1f2d3d;
    margin-bottom: 6px;
  }
  .hero-desc {
    display: block;
    font-size: 13px;
    line-height: 1.6;
    color: #7a8599;
  }
  .form-section {
@@ -547,7 +401,6 @@
    padding: 12px 12px 0;
  }
  .node-list,
  .product-list {
    padding: 12px;
    display: flex;
@@ -555,31 +408,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;
@@ -603,6 +437,13 @@
    font-size: 14px;
  }
  .summary-tip {
    padding: 0 24rpx 24rpx;
    color: #909399;
    font-size: 12px;
    line-height: 1.6;
  }
  :deep(.u-cell-group__title) {
    padding: 14px 18px 10px !important;
    font-size: 15px !important;
src/pages/sales/salesQuotation/index.vue
@@ -7,25 +7,16 @@
        <view class="search-input">
          <up-input
            class="search-text"
            v-model="quotationNo"
            placeholder="请输入报价单号搜索"
            v-model="searchForm.product"
            placeholder="请输入产品名称搜索"
            clearable
            @change="getList"
            @change="getList(true)"
          />
        </view>
        <view class="filter-button" @click="getList">
        <view class="filter-button" @click="getList(true)">
          <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"
      />
    </view>
    <view v-if="quotationList.length > 0" class="ledger-list">
@@ -35,37 +26,44 @@
            <view class="document-icon">
              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
            </view>
            <text class="item-id">{{ item.quotationNo || "-" }}</text>
            <text class="item-id">{{ item.product || "-" }}</text>
          </view>
          <text class="item-index">{{ item.status || "-" }}</text>
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">客户名称</text>
            <text class="detail-value">{{ item.customer || "-" }}</text>
            <text class="detail-label">规格</text>
            <text class="detail-value">{{ item.specification || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">业务员</text>
            <text class="detail-value">{{ item.salesperson || "-" }}</text>
            <text class="detail-label">纸张</text>
            <text class="detail-value">{{ item.paper || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">报价日期</text>
            <text class="detail-value">{{ item.quotationDate || "-" }}</text>
            <text class="detail-label">定量</text>
            <text class="detail-value">{{ item.paperWeight || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">有效期至</text>
            <text class="detail-value">{{ item.validDate || "-" }}</text>
            <text class="detail-label">单价</text>
            <text class="detail-value">{{ formatAmount(item.unitPrice) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">付款方式</text>
            <text class="detail-value">{{ item.paymentMethod || "-" }}</text>
            <text class="detail-label">印版费</text>
            <text class="detail-value">{{ formatAmount(item.printingFee) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">报价金额</text>
            <text class="detail-value highlight">{{ formatAmount(item.totalAmount) }}</text>
            <text class="detail-label">刀版费</text>
            <text class="detail-value">{{ formatAmount(item.dieCuttingFee) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">磨具费</text>
            <text class="detail-value">{{ formatAmount(item.grindingFee) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">数量</text>
            <text class="detail-value">{{ Number(item.quantity || 0) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">备注</text>
@@ -74,25 +72,15 @@
        </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" type="primary" @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>
          <up-button class="action-btn" size="small" type="error" plain @click="handleDelete(item)">删除</up-button>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
      <text>暂无销售报价数据</text>
      <text>暂无数据</text>
    </view>
    <view class="fab-button" @click="goAdd">
@@ -105,70 +93,73 @@
  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 } from "@/api/salesManagement/salesQuotation";
  import { quotationProductListPage } from "@/api/salesManagement/salesQuotationProduct";
  const quotationNo = ref("");
  const searchForm = reactive({ product: "" });
  const quotationList = ref([]);
  const tabList = reactive([
    { name: "全部", value: "" },
    { name: "待审批", value: "待审批" },
    { name: "审核中", value: "审核中" },
    { name: "通过", value: "通过" },
    { name: "拒绝", value: "拒绝" },
  ]);
  const tabValue = ref(0);
  const page = {
    current: -1,
    size: -1,
  };
  const goBack = () => {
    uni.navigateBack();
  };
  const goAdd = () => {
    uni.navigateTo({ url: "/pages/sales/salesQuotation/edit" });
  };
  const goBack = () => uni.navigateBack();
  const goAdd = () => uni.navigateTo({ url: "/pages/sales/salesQuotation/edit" });
  const goEdit = item => {
    if (!canEdit(item)) return;
    const source = item?.__raw || item || {};
    uni.setStorageSync("salesQuotationEdit", source);
    uni.navigateTo({ url: `/pages/sales/salesQuotation/edit?id=${item.id}` });
  };
  const goDetail = item => {
    uni.setStorageSync("salesQuotationDetail", item || {});
    uni.navigateTo({ url: `/pages/sales/salesQuotation/detail?id=${item.id}` });
  };
  const canEdit = item => ["待审批", "拒绝"].includes(item?.status);
  const formatAmount = amount => `Â¥${Number(amount || 0).toFixed(2)}`;
  const onTabChange = val => {
    tabValue.value = val.index;
    getList();
  };
  const calcTotalAmountFromProducts = products =>
    Number(
      (products || [])
        .reduce((sum, product) => {
          const unitPrice = Number(product?.unitPrice || 0);
          const printingFee = Number(product?.printingFee || 0);
          const dieCuttingFee = Number(product?.dieCuttingFee || 0);
          const grindingFee = Number(product?.grindingFee || 0);
          return sum + unitPrice + printingFee + dieCuttingFee + grindingFee;
        }, 0)
        .toFixed(2)
    );
  const getCurrentStatus = () => {
    const currentTab = tabList[tabValue.value];
    return currentTab?.value || "";
  };
  const formatAmount = amount => {
    const num = Number(amount || 0);
    return `Â¥${num.toFixed(2)}`;
  const normalizeQuotation = row => {
    const sourceProducts = Array.isArray(row?.products) && row.products.length ? row.products : [row];
    const first = sourceProducts[0] || {};
    return {
      ...row,
      __raw: row,
      customer: row?.customer || row?.customerName || first?.customer || first?.customerName || "",
      salesperson: row?.salesperson || row?.salesman || row?.salesPerson || first?.salesperson || "",
      quotationDate: row?.quotationDate || row?.quoteDate || first?.quotationDate || "",
      validDate: row?.validDate || row?.expireDate || first?.validDate || "",
      paymentMethod: row?.paymentMethod || row?.paymentType || first?.paymentMethod || "",
      product: first.product || first.productName || row?.product || "",
      specification: first.specification || row?.specification || "",
      paper: first.paper || row?.paper || "",
      paperWeight: first.paperWeight || row?.paperWeight || "",
      unitPrice: Number(first.unitPrice || row?.unitPrice || 0),
      printingFee: Number(first.printingFee || row?.printingFee || 0),
      dieCuttingFee: Number(first.dieCuttingFee || row?.dieCuttingFee || 0),
      grindingFee: Number(first.grindingFee || row?.grindingFee || 0),
      quantity: Number(first.quantity || row?.quantity || 0),
      quotationNo: row?.quotationNo || first?.quotationNo || "",
      totalAmount: Number(row?.totalAmount || calcTotalAmountFromProducts(sourceProducts)),
    };
  };
  const getList = () => {
    uni.showLoading({ title: "加载中...", mask: true });
    getQuotationList({
      ...page,
      quotationNo: quotationNo.value,
      status: getCurrentStatus(),
    quotationProductListPage({
      current: -1,
      size: -1,
      product: String(searchForm.product || "").trim(),
    })
      .then(res => {
        const records = res?.data?.records || res?.records || [];
        quotationList.value = Array.isArray(records) ? records : [];
        quotationList.value = Array.isArray(records) ? records.map(normalizeQuotation) : [];
      })
      .catch(() => {
        uni.showToast({ title: "查询失败", icon: "error" });
@@ -182,11 +173,11 @@
    if (!item?.id) return;
    uni.showModal({
      title: "删除确认",
      content: "确认删除该报价单吗?",
      content: "确认删除该报价吗?",
      success: res => {
        if (!res.confirm) return;
        uni.showLoading({ title: "处理中...", mask: true });
        deleteQuotation(item.id)
        deleteQuotation([item.id])
          .then(() => {
            uni.showToast({ title: "删除成功", icon: "success" });
            getList();
@@ -208,11 +199,6 @@
<style scoped lang="scss">
  @import "@/styles/sales-common.scss";
  .tabs-section {
    background: #ffffff;
    padding: 0 12px 8px 12px;
  }
  .item-index {
    max-width: 180rpx;
src/pages/works.vue
@@ -89,27 +89,27 @@
      </view>
    </view>
    <!-- äººåŠ›èµ„æºæ¨¡å— -->
    <view class="common-module collaboration-module"
          v-if="hasHumanResourcesItems">
      <view class="module-header">
        <view class="module-title-container">
          <text class="module-title">人力资源</text>
        </view>
      </view>
      <view class="module-content">
        <up-grid :border="false"
                 col="4">
          <up-grid-item v-for="(item, index) in humanResourcesItems"
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
        </up-grid>
      </view>
    </view>
<!--    <view class="common-module collaboration-module"-->
<!--          v-if="hasHumanResourcesItems">-->
<!--      <view class="module-header">-->
<!--        <view class="module-title-container">-->
<!--          <text class="module-title">人力资源</text>-->
<!--        </view>-->
<!--      </view>-->
<!--      <view class="module-content">-->
<!--        <up-grid :border="false"-->
<!--                 col="4">-->
<!--          <up-grid-item v-for="(item, index) in humanResourcesItems"-->
<!--                        :key="index"-->
<!--                        @click="handleCommonItemClick(item)">-->
<!--            <view class="icon-container">-->
<!--              <image :src="item.icon" class="item-icon"></image>-->
<!--            </view>-->
<!--            <text class="item-label">{{item.label}}</text>-->
<!--          </up-grid-item>-->
<!--        </up-grid>-->
<!--      </view>-->
<!--    </view>-->
    <!-- ç”Ÿäº§ç®¡æŽ§æ¨¡å— -->
    <view class="common-module equipment-module"
          v-if="hasProductionItems">
@@ -154,50 +154,28 @@
        </up-grid>
      </view>
    </view>
    <!-- æ¡£æ¡ˆç®¡ç†æ¨¡å— -->
    <view class="common-module archive-module"
          v-if="hasArchiveManagementItems">
      <view class="module-header">
        <view class="module-title-container">
          <text class="module-title">档案管理</text>
        </view>
      </view>
      <view class="module-content">
        <up-grid :border="false"
                 col="4">
          <up-grid-item v-for="(item, index) in archiveManagementItems"
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
        </up-grid>
      </view>
    </view>
    <!-- å”®åŽæœåŠ¡æ¨¡å— -->
    <view class="common-module after-sales-module"
          v-if="hasAfterSalesServiceItems">
      <view class="module-header">
        <view class="module-title-container">
          <text class="module-title">售后服务</text>
        </view>
      </view>
      <view class="module-content">
        <up-grid :border="false"
                 col="4">
          <up-grid-item v-for="(item, index) in afterSalesServiceItems"
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
        </up-grid>
      </view>
    </view>
<!--    <view class="common-module after-sales-module"-->
<!--          v-if="hasAfterSalesServiceItems">-->
<!--      <view class="module-header">-->
<!--        <view class="module-title-container">-->
<!--          <text class="module-title">售后服务</text>-->
<!--        </view>-->
<!--      </view>-->
<!--      <view class="module-content">-->
<!--        <up-grid :border="false"-->
<!--                 col="4">-->
<!--          <up-grid-item v-for="(item, index) in afterSalesServiceItems"-->
<!--                        :key="index"-->
<!--                        @click="handleCommonItemClick(item)">-->
<!--            <view class="icon-container">-->
<!--              <image :src="item.icon" class="item-icon"></image>-->
<!--            </view>-->
<!--            <text class="item-label">{{item.label}}</text>-->
<!--          </up-grid-item>-->
<!--        </up-grid>-->
<!--      </view>-->
<!--    </view>-->
    <!-- è´¨é‡ç®¡ç†æ¨¡å— -->
    <view class="common-module collaboration-module"
          v-if="hasQualityItems">
@@ -243,27 +221,27 @@
      </view>
    </view>
    <!-- å®‰å…¨ç”Ÿäº§æ¨¡å— -->
    <view class="common-module collaboration-module"
          v-if="hasSafetyItems">
      <view class="module-header">
        <view class="module-title-container">
          <text class="module-title">安全生产</text>
        </view>
      </view>
      <view class="module-content">
        <up-grid :border="false"
                 col="4">
          <up-grid-item v-for="(item, index) in safetyItems"
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container">
              <image :src="item.icon" class="item-icon"></image>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
        </up-grid>
      </view>
    </view>
<!--    <view class="common-module collaboration-module"-->
<!--          v-if="hasSafetyItems">-->
<!--      <view class="module-header">-->
<!--        <view class="module-title-container">-->
<!--          <text class="module-title">安全生产</text>-->
<!--        </view>-->
<!--      </view>-->
<!--      <view class="module-content">-->
<!--        <up-grid :border="false"-->
<!--                 col="4">-->
<!--          <up-grid-item v-for="(item, index) in safetyItems"-->
<!--                        :key="index"-->
<!--                        @click="handleCommonItemClick(item)">-->
<!--            <view class="icon-container">-->
<!--              <image :src="item.icon" class="item-icon"></image>-->
<!--            </view>-->
<!--            <text class="item-label">{{item.label}}</text>-->
<!--          </up-grid-item>-->
<!--        </up-grid>-->
<!--      </view>-->
<!--    </view>-->
    <DownloadProgressMask />
  </view>
@@ -307,7 +285,7 @@
  const marketingItems = reactive([
    {
      icon: "/static/images/icon/kehudangan.svg",
      label: "客户档案",
      label: "客户档案(私海)",
    },
    {
      icon: "/static/images/icon/xiaoshoubaojia.svg",
@@ -332,6 +310,10 @@
    {
      icon: "/static/images/icon/gongyingshangwanglai.svg",
      label: "供应商往来",
    },
    {
      icon: "/static/images/icon/gongyingshangdangan.svg",
      label: "供应商档案",
    },
    {
      icon: "/static/images/icon/caigouguanli.svg",
@@ -384,14 +366,6 @@
    {
      icon: "/static/images/icon/jiekuanguanli.svg",
      label: "借款管理",
    },
  ]);
  // æ¡£æ¡ˆç®¡ç†åŠŸèƒ½æ•°æ®
  const archiveManagementItems = reactive([
    {
      icon: "/static/images/icon/gongyingshangdangan.svg",
      label: "供应商档案",
    },
  ]);
@@ -565,7 +539,7 @@
  const handleCommonItemClick = item => {
    // æ ¹æ®ä¸åŒçš„功能项进行跳转
    switch (item.label) {
      case "客户档案":
      case "客户档案(私海)":
        uni.navigateTo({
          url: "/pages/basicData/customerFile/index",
        });
@@ -1110,8 +1084,8 @@
    // å®šä¹‰èœå•配置映射
    const menuMapping = {
      purchase: { target: purchaseItems, specialMapping: { "供应商档案": "供应商管理" } },
      collaboration: { target: collaborationItems, specialMapping: { "规章制度": "规章制度管理" } },
      archiveManagement: { target: archiveManagementItems, specialMapping: { "供应商档案": "供应商管理" } },
    };
    console.log(allowedMenuTitles)
    // é€šç”¨è¿‡æ»¤å‡½æ•°
@@ -1128,9 +1102,8 @@
    // è¿‡æ»¤å„个模块
    filterArray(marketingItems);
    filterArray(purchaseItems);
    filterArray(purchaseItems, menuMapping.purchase.specialMapping);
    filterArray(financeManagementItems);
    filterArray(archiveManagementItems, menuMapping.archiveManagement.specialMapping);
    filterArray(collaborationItems, menuMapping.collaboration.specialMapping);
    filterArray(safetyItems);
    filterArray(humanResourcesItems);
@@ -1144,7 +1117,6 @@
  const hasMarketingItems = computed(() => marketingItems.length > 0);
  const hasPurchaseItems = computed(() => purchaseItems.length > 0);
  const hasFinanceManagementItems = computed(() => financeManagementItems.length > 0);
  const hasArchiveManagementItems = computed(() => archiveManagementItems.length > 0);
  const hasAfterSalesServiceItems = computed(() => afterSalesServiceItems.length > 0);
  const hasCollaborationItems = computed(() => collaborationItems.length > 0);
  const hasSafetyItems = computed(() => safetyItems.length > 0);
src/utils/versionUpgrade.js
@@ -272,7 +272,7 @@
    lastVersionCheckAt = now;
    console.log(`${logPrefix} è§¦å‘版本检查,来源=${from}`);
    const currentVersion = await getCurrentVersion(logPrefix);
    await checkAppVersionUpgrade(logPrefix, currentVersion);
    // await checkAppVersionUpgrade(logPrefix, currentVersion);
  };
  return { triggerVersionCheck };