gaoluyang
20 小时以前 2d157a517d45b34acfdc0a540078d57c105877e5
升级app
1.添加客户档案、销售报价
已添加7个文件
已修改3个文件
2017 ■■■■■ 文件已修改
src/api/basicData/customerFile.js 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesQuotation.js 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/basicData/customerFile/detail.vue 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/basicData/customerFile/edit.vue 342 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/basicData/customerFile/index.vue 183 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/detail.vue 235 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/edit.vue 613 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/index.vue 226 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/works.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/customerFile.js
@@ -50,3 +50,44 @@
    })
}
// æ–°å¢žå®¢æˆ·è·Ÿè¿›
export function addCustomerFollow(data) {
    return request({
        url: '/basic/customer-follow/add',
        method: 'post',
        data: data
    })
}
// ä¿®æ”¹å®¢æˆ·è·Ÿè¿›
export function updateCustomerFollow(data) {
    return request({
        url: '/basic/customer-follow/edit',
        method: 'put',
        data: data,
    })
}
// åˆ é™¤å®¢æˆ·è·Ÿè¿›
export function delCustomerFollow(id) {
    return request({
        url: '/basic/customer-follow/'+id,
        method: 'delete',
    })
}
// å›žè®¿æé†’-新增/更新
export function addReturnVisit(data) {
    return request({
        url: '/basic/customer-follow/return-visit',
        method: 'post',
        data: data
    })
}
// èŽ·å–å›žè®¿æé†’è¯¦æƒ…
export function getReturnVisit(id) {
    return request({
        url: '/basic/customer-follow/return-visit/' + id,
        method: 'get'
    })
}
src/api/salesManagement/salesQuotation.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
// é”€å”®æŠ¥ä»·é¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢æŠ¥ä»·å•列表
export function getQuotationList(query) {
  return request({
    url: "/sales/quotation/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢æŠ¥ä»·å•详情
export function getQuotationDetail(query) {
  return request({
    url: "/sales/quotation/detail",
    method: "get",
    params: query,
  });
}
// æ–°å¢žæŠ¥ä»·å•
export function addQuotation(data) {
  return request({
    url: "/sales/quotation/add",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹æŠ¥ä»·å•
export function updateQuotation(data) {
  return request({
    url: "/sales/quotation/update",
    method: "post",
    data: data,
  });
}
// åˆ é™¤æŠ¥ä»·å•
export function deleteQuotation(query) {
  return request({
    url: "/sales/quotation/delete",
    method: "delete",
    data: query,
  });
}
// å‘送报价单
export function sendQuotation(data) {
  return request({
    url: "/sales/quotation/send",
    method: "post",
    data: data,
  });
}
// æŠ¥ä»·å•转订单
export function convertToOrder(data) {
  return request({
    url: "/sales/quotation/convertToOrder",
    method: "post",
    data: data,
  });
}
// æŸ¥è¯¢å®¢æˆ·åˆ—表
export function getCustomerList(query) {
  return request({
    url: "/basic/customer/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢äº§å“åˆ—表
export function getProductList(query) {
  return request({
    url: "/basic/product/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢ä¸šåŠ¡å‘˜åˆ—è¡¨
export function getSalespersonList(query) {
  return request({
    url: "/system/user/salespersonList",
    method: "get",
    params: query,
  });
}
// å¯¼å‡ºæŠ¥ä»·å•
export function exportQuotation(query) {
  return request({
    url: "/sales/quotation/export",
    method: "get",
    params: query,
    responseType: "blob",
  });
}
// æ‰“印报价单
export function printQuotation(query) {
  return request({
    url: "/sales/quotation/print",
    method: "get",
    params: query,
    responseType: "blob",
  });
}
src/pages.json
@@ -73,6 +73,27 @@
      }
    },
    {
      "path": "pages/basicData/customerFile/index",
      "style": {
        "navigationBarTitleText": "客户档案",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/basicData/customerFile/edit",
      "style": {
        "navigationBarTitleText": "客户信息",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/basicData/customerFile/detail",
      "style": {
        "navigationBarTitleText": "客户详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/sales/salesAccount/index",
      "style": {
        "navigationBarTitleText": "销售台账",
@@ -80,6 +101,27 @@
      }
    },
    {
      "path": "pages/sales/salesQuotation/index",
      "style": {
        "navigationBarTitleText": "销售报价",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/sales/salesQuotation/edit",
      "style": {
        "navigationBarTitleText": "销售报价",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/sales/salesQuotation/detail",
      "style": {
        "navigationBarTitleText": "报价详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/sales/salesAccount/out",
      "style": {
        "navigationBarTitleText": "发货状态",
src/pages/basicData/customerFile/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,205 @@
<template>
  <view class="customer-detail-page">
    <PageHeader title="客户详情" @back="goBack" />
    <view class="detail-content">
      <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.customerName || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">客户分类</text>
            <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>
      <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.contactPerson || "-" }}</text>
          </view>
          <view class="info-item">
            <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>
          </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.maintainer || "-" }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">维护时间</text>
            <text class="info-value">{{ detailData.maintenanceTime || "-" }}</text>
          </view>
        </view>
      </view>
    </view>
    <FooterButtons
      cancelText="返回"
      confirmText="编辑"
      @cancel="goBack"
      @confirm="goEdit"
    />
  </view>
</template>
<script setup>
  import { ref } from "vue";
  import { onLoad, onShow } from "@dcloudio/uni-app";
  import FooterButtons from "@/components/FooterButtons.vue";
  import { getCustomer } from "@/api/basicData/customerFile";
  const customerId = ref("");
  const detailData = ref({});
  const goBack = () => {
    uni.navigateBack();
  };
  const goEdit = () => {
    if (!customerId.value) return;
    uni.navigateTo({ url: `/pages/basicData/customerFile/edit?id=${customerId.value}` });
  };
  const getDetail = () => {
    if (!customerId.value) return;
    uni.showLoading({ title: "加载中...", mask: true });
    getCustomer(customerId.value)
      .then(res => {
        detailData.value = res.data || {};
      })
      .catch(() => {
        uni.showToast({ title: "获取详情失败", icon: "error" });
      })
      .finally(() => {
        uni.hideLoading();
      });
  };
  onLoad(options => {
    if (options?.id) {
      customerId.value = options.id;
      getDetail();
    }
  });
  onShow(() => {
    if (customerId.value) {
      getDetail();
    }
  });
</script>
<style scoped lang="scss">
  .customer-detail-page {
    min-height: 100vh;
    background-color: #f5f5f5;
    padding-bottom: 90px;
  }
  .detail-content {
    padding: 16px;
  }
  .section {
    background: #ffffff;
    border-radius: 12px;
    margin-bottom: 16px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
  .section-title {
    padding: 16px;
    font-size: 16px;
    font-weight: 600;
    color: #303133;
    border-bottom: 1px solid #f0f0f0;
  }
  .info-list {
    padding: 8px 0;
  }
  .info-item {
    display: flex;
    padding: 12px 16px;
    border-bottom: 1px solid #f8f8f8;
  }
  .info-item:last-child {
    border-bottom: none;
  }
  .info-label {
    width: 120px;
    font-size: 14px;
    color: #606266;
  }
  .info-value {
    flex: 1;
    font-size: 14px;
    color: #303133;
    text-align: right;
    word-break: break-all;
  }
</style>
src/pages/basicData/customerFile/edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,342 @@
<template>
  <view class="account-detail">
    <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="customerName" required>
            <up-input
              v-model="form.customerName"
              placeholder="请输入客户名称"
              clearable
            />
          </up-form-item>
          <up-form-item label="客户分类" prop="customerType" required>
            <up-input
              v-model="customerTypeText"
              placeholder="请选择客户分类"
              readonly
              @click="showCustomerTypeSheet = true"
            />
            <template #right>
              <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-input
              v-model="form.companyAddress"
              placeholder="请输入公司地址"
              clearable
            />
          </up-form-item>
          <up-form-item label="公司电话" prop="companyPhone">
            <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>
        </u-cell-group>
        <u-cell-group title="联系信息" class="form-section">
          <up-form-item label="联系人" prop="contactPerson">
            <up-input
              v-model="form.contactPerson"
              placeholder="请输入联系人"
              clearable
            />
          </up-form-item>
          <up-form-item label="联系电话" prop="contactPhone">
            <up-input
              v-model="form.contactPhone"
              placeholder="请输入联系电话"
              clearable
            />
          </up-form-item>
        </u-cell-group>
        <u-cell-group title="银行信息" class="form-section">
          <up-form-item label="银行基本户" prop="basicBankAccount">
            <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="请输入开户行号"
              clearable
            />
          </up-form-item>
        </u-cell-group>
        <u-cell-group title="维护信息" class="form-section">
          <up-form-item label="维护人" prop="maintainer">
            <up-input
              v-model="form.maintainer"
              disabled
              placeholder="自动填充"
            />
          </up-form-item>
          <up-form-item label="维护时间" prop="maintenanceTime">
            <up-input
              v-model="form.maintenanceTime"
              disabled
              placeholder="自动填充"
            />
          </up-form-item>
        </u-cell-group>
      </up-form>
    </view>
    <FooterButtons
      :loading="loading"
      confirmText="保存"
      @cancel="goBack"
      @confirm="handleSubmit"
    />
    <up-action-sheet
      :show="showCustomerTypeSheet"
      title="选择客户分类"
      :actions="customerTypeActions"
      @select="onSelectCustomerType"
      @close="showCustomerTypeSheet = false"
    />
  </view>
</template>
<script setup>
  import { computed, onMounted, ref } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import FooterButtons from "@/components/FooterButtons.vue";
  import useUserStore from "@/store/modules/user";
  import { formatDateToYMD } from "@/utils/ruoyi";
  import { addCustomer, getCustomer, updateCustomer } from "@/api/basicData/customerFile";
  const userStore = useUserStore();
  const formRef = ref();
  const loading = ref(false);
  const customerId = ref("");
  const showCustomerTypeSheet = ref(false);
  const form = ref({
    customerName: "",
    customerType: "",
    taxpayerIdentificationNumber: "",
    companyAddress: "",
    companyPhone: "",
    corporation: "",
    agent: "",
    fax: "",
    contactPerson: "",
    contactPhone: "",
    basicBankAccount: "",
    bankAccount: "",
    bankName: "",
    bankCode: "",
    maintainer: "",
    maintenanceTime: "",
  });
  const rules = {
    customerName: [{ required: true, message: "请输入客户名称", trigger: "blur" }],
    customerType: [{ required: true, message: "请选择客户分类", trigger: "change" }],
  };
  const customerTypeActions = [
    { name: "零售客户", value: "零售客户" },
    { name: "经销商客户", value: "经销商客户" },
  ];
  const pageTitle = computed(() =>
    customerId.value ? "编辑客户" : "新增客户"
  );
  const customerTypeText = computed(() => form.value.customerType || "");
  const goBack = () => {
    uni.navigateBack();
  };
  const initForAdd = () => {
    form.value.maintainer = userStore.nickName || "";
    form.value.maintenanceTime = formatDateToYMD(Date.now());
  };
  const loadDetail = () => {
    if (!customerId.value) return;
    uni.showLoading({ title: "加载中...", mask: true });
    getCustomer(customerId.value)
      .then(res => {
        form.value = { ...form.value, ...(res.data || {}) };
      })
      .catch(() => {
        uni.showToast({ title: "获取详情失败", icon: "error" });
      })
      .finally(() => {
        uni.hideLoading();
      });
  };
  const onSelectCustomerType = action => {
    form.value.customerType = action.value;
    showCustomerTypeSheet.value = false;
  };
  const handleSubmit = async () => {
    const valid = await formRef.value.validate().catch(() => false);
    if (!valid) return;
    loading.value = true;
    const action = customerId.value ? updateCustomer : addCustomer;
    action({ ...form.value, id: customerId.value || undefined })
      .then(() => {
        uni.showToast({ title: "保存成功", icon: "success" });
        setTimeout(() => {
          uni.navigateBack();
        }, 300);
      })
      .catch(() => {
        uni.showToast({ title: "保存失败", icon: "error" });
      })
      .finally(() => {
        loading.value = false;
      });
  };
  onMounted(async () => {
    if (!userStore.nickName) {
      await userStore.getInfo().catch(() => null);
    }
    if (!customerId.value) {
      initForAdd();
    }
  });
  onLoad(options => {
    if (options?.id) {
      customerId.value = options.id;
      loadDetail();
    }
  });
</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, #f4f8ff 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 {
    margin-bottom: 12px;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
  }
  :deep(.u-cell-group__title) {
    padding: 14px 18px 10px !important;
    font-size: 15px !important;
    font-weight: 600 !important;
    color: #22324d !important;
    background: #f8fbff !important;
  }
  :deep(.u-form-item__content__slot) {
    flex: 1;
  }
  :deep(.u-input__content) {
    justify-content: flex-end;
  }
  :deep(.u-input__content__field-wrapper__field),
  :deep(.u-input__input) {
    text-align: right !important;
  }
</style>
src/pages/basicData/customerFile/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,183 @@
<template>
  <view class="sales-account">
    <PageHeader title="客户档案" @back="goBack" />
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input
            class="search-text"
            v-model="customerName"
            placeholder="请输入客户名称"
            clearable
            @change="getList"
          />
        </view>
        <view class="filter-button" @click="getList">
          <up-icon name="search" size="24" color="#999999"></up-icon>
        </view>
      </view>
    </view>
    <view class="tabs-section">
      <up-tabs
        v-model="tabValue"
        :list="tabList"
        itemStyle="width: 33.33%;height: 80rpx;"
        @change="onTabChange"
      />
    </view>
    <view v-if="list.length > 0" class="ledger-list">
      <view v-for="item in list" :key="item.id" class="ledger-item">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="account-fill" size="16" color="#ffffff"></up-icon>
            </view>
            <text class="item-id">{{ item.customerName || "-" }}</text>
          </view>
          <text class="item-index">{{ item.customerType || "-" }}</text>
        </view>
        <up-divider></up-divider>
        <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>
          <view class="detail-row">
            <text class="detail-label">公司地址</text>
            <text class="detail-value">{{ item.companyAddress || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">法人</text>
            <text class="detail-value">{{ item.corporation || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">代理人</text>
            <text class="detail-value">{{ item.agent || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">维护人</text>
            <text class="detail-value">{{ item.maintainer || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">维护时间</text>
            <text class="detail-value">{{ item.maintenanceTime || "-" }}</text>
          </view>
        </view>
        <view class="action-buttons">
                    <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
          >
        </view>
      </view>
    </view>
    <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>
  </view>
</template>
<script setup>
  import { reactive, ref } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import { listCustomer } from "@/api/basicData/customerFile";
  const customerName = ref("");
  const list = ref([]);
  const tabList = reactive([
    { 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/basicData/customerFile/edit" });
  };
  const goEdit = item => {
    uni.navigateTo({ url: `/pages/basicData/customerFile/edit?id=${item.id}` });
  };
  const goDetail = item => {
    uni.navigateTo({ url: `/pages/basicData/customerFile/detail?id=${item.id}` });
  };
  const onTabChange = val => {
    tabValue.value = val.index;
    getList();
  };
  const getCurrentCustomerType = () => {
    const currentTab = tabList[tabValue.value];
    return currentTab?.value || "";
  };
  const getList = () => {
    uni.showLoading({ title: "加载中...", mask: true });
    listCustomer({
      ...page,
      customerName: customerName.value,
      customerType: getCurrentCustomerType(),
    })
      .then(res => {
        list.value = res?.records || res?.data?.records || [];
      })
      .catch(() => {
        uni.showToast({ title: "查询失败", icon: "error" });
      })
      .finally(() => {
        uni.hideLoading();
      });
  };
  onShow(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .tabs-section {
    background: #ffffff;
    padding: 0 12px 8px 12px;
  }
  .item-index {
    max-width: 180rpx;
    text-align: center;
  }
  .detail-value {
    max-width: 70%;
    word-break: break-all;
  }
</style>
src/pages/sales/salesQuotation/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,235 @@
<template>
  <view class="customer-detail-page">
    <PageHeader title="报价详情" @back="goBack" />
    <view class="detail-content">
      <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>
      </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 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>
        </view>
        <view v-else class="empty-box">
          <text>暂无产品明细</text>
        </view>
      </view>
    </view>
    <FooterButtons cancelText="返回" confirmText="编辑" @cancel="goBack" @confirm="goEdit" />
  </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";
  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}` });
  };
  const formatAmount = amount => `Â¥${Number(amount || 0).toFixed(2)}`;
  const loadDetailFromStorage = () => {
    const cachedData = uni.getStorageSync("salesQuotationDetail");
    detailData.value = cachedData || {};
  };
  onLoad(options => {
    if (options?.id) {
      quotationId.value = options.id;
    }
    loadDetailFromStorage();
  });
  onShow(() => {
    loadDetailFromStorage();
  });
</script>
<style scoped lang="scss">
  .customer-detail-page {
    min-height: 100vh;
    background-color: #f5f5f5;
    padding-bottom: 90px;
  }
  .detail-content {
    padding: 16px;
  }
  .section {
    background: #ffffff;
    border-radius: 12px;
    margin-bottom: 16px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
  .section-title {
    padding: 16px;
    font-size: 16px;
    font-weight: 600;
    color: #303133;
    border-bottom: 1px solid #f0f0f0;
  }
  .info-list {
    padding: 8px 0;
  }
  .info-item {
    display: flex;
    padding: 12px 16px;
    border-bottom: 1px solid #f8f8f8;
  }
  .info-item:last-child {
    border-bottom: none;
  }
  .info-label {
    width: 120px;
    font-size: 14px;
    color: #606266;
  }
  .info-value {
    flex: 1;
    font-size: 14px;
    color: #303133;
    text-align: right;
    word-break: break-all;
  }
  .highlight {
    color: #2979ff;
    font-weight: 600;
  }
  .empty-box {
    padding: 20px 16px;
    font-size: 14px;
    color: #999;
    text-align: center;
  }
  .product-list {
    padding: 12px;
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  .product-card {
    background: #f9fafc;
    border-radius: 10px;
    overflow: hidden;
  }
  .product-head {
    padding: 12px 16px;
    font-size: 14px;
    font-weight: 600;
    color: #22324d;
    border-bottom: 1px solid #eef2f7;
  }
</style>
src/pages/sales/salesQuotation/edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,613 @@
<template>
  <view class="account-detail">
    <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>
        <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">
            <text>暂无产品,请先添加产品</text>
          </view>
          <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>
              </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>
                <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.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-form-item>
                <up-form-item label="金额">
                  <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">
          <up-form-item label="报价总额">
            <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" />
  </view>
</template>
<script setup>
  import { computed, onMounted, onUnmounted, 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";
  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 productActions = computed(() => productList.value.map(item => ({ name: item.label, value: item.value, label: item.label })));
  const createEmptyProduct = () => ({
    uid: `p_${uidSeed++}`,
    productId: "",
    product: "",
    specificationId: "",
    specification: "",
    unit: "",
    quantity: 1,
    unitPrice: 0,
    amount: 0,
    modelOptions: [],
  });
  const flattenProductTree = nodes => {
    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 });
        }
      });
    };
    walk(nodes);
    return result;
  };
  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));
    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);
    form.value.totalAmount = totalAmount.value;
  };
  const fetchModelOptions = async (productId, product) => {
    const rows = await modelList({ id: productId }).catch(() => []);
    product.modelOptions = Array.isArray(rows) ? rows : [];
  };
  const openProductPicker = index => {
    currentProductIndex.value = index;
    showProductSheet.value = true;
  };
  const openModelPicker = index => {
    currentProductIndex.value = index;
    const current = form.value.products[index];
    if (!current?.productId) {
      uni.showToast({ title: "请先选择产品", icon: "none" });
      return;
    }
    modelActions.value = (current.modelOptions || []).map(item => ({ name: item.model, value: item.id, unit: item.unit }));
    if (!modelActions.value.length) {
      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;
    current.productId = action.value;
    current.product = action.label;
    current.specificationId = "";
    current.specification = "";
    current.unit = "";
    current.modelOptions = [];
    showProductSheet.value = false;
    fetchModelOptions(action.value, current);
  };
  const onSelectModel = action => {
    const current = form.value.products[currentProductIndex.value];
    if (!current) return;
    current.specificationId = action.value;
    current.specification = action.name;
    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 : [];
    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;
    }));
    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 || {};
      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 validateProducts = () => {
    if (!form.value.products.length) {
      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));
    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 handleSubmit = async () => {
    const valid = await formRef.value.validate().catch(() => false);
    if (!valid || !validateApprovers() || !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)
      .then(() => {
        uni.showToast({ title: "保存成功", icon: "success" });
        setTimeout(() => uni.navigateBack(), 300);
      })
      .catch(() => {
        uni.showToast({ title: "保存失败", icon: "error" });
      })
      .finally(() => {
        loading.value = false;
      });
  };
  onLoad(options => {
    if (options?.id) {
      quotationId.value = options.id;
      form.value.id = options.id;
    } else {
      const today = formatDateToYMD(Date.now());
      form.value.quotationDate = today;
      form.value.validDate = today;
    }
  });
  onMounted(async () => {
    await fetchBaseOptions();
    uni.$on("selectContact", onSelectApprover);
    if (quotationId.value) {
      await loadDetail();
    }
  });
  onUnmounted(() => {
    uni.$off("selectContact", onSelectApprover);
    uni.removeStorageSync("stepIndex");
  });
</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 {
    margin-bottom: 12px;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
  }
  .section-tools {
    display: flex;
    justify-content: flex-end;
    padding: 12px 12px 0;
  }
  .node-list,
  .product-list {
    padding: 12px;
    display: flex;
    flex-direction: column;
    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;
    color: #22324d;
  }
  .product-card {
    background: #fff;
    border-radius: 12px;
    padding: 0 12px 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
  .product-header {
    padding: 12px 0;
  }
  .empty-text {
    padding: 16px 12px;
    color: #999;
    font-size: 14px;
  }
  :deep(.u-cell-group__title) {
    padding: 14px 18px 10px !important;
    font-size: 15px !important;
    font-weight: 600 !important;
    color: #22324d !important;
    background: #f8fbff !important;
  }
</style>
src/pages/sales/salesQuotation/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,226 @@
<template>
  <view class="sales-account">
    <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"
          />
        </view>
        <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"
      />
    </view>
    <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>
            </view>
            <text class="item-id">{{ item.quotationNo || "-" }}</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>
          </view>
          <view class="detail-row">
            <text class="detail-label">业务员</text>
            <text class="detail-value">{{ item.salesperson || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">报价日期</text>
            <text class="detail-value">{{ item.quotationDate || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">有效期至</text>
            <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">
            <text class="detail-label">备注</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>
        </view>
      </view>
    </view>
    <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>
  </view>
</template>
<script setup>
  import { reactive, ref } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import { deleteQuotation, getQuotationList } from "@/api/salesManagement/salesQuotation";
  const quotationNo = ref("");
  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 goEdit = item => {
    if (!canEdit(item)) return;
    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 onTabChange = val => {
    tabValue.value = val.index;
    getList();
  };
  const getCurrentStatus = () => {
    const currentTab = tabList[tabValue.value];
    return currentTab?.value || "";
  };
  const formatAmount = amount => {
    const num = Number(amount || 0);
    return `Â¥${num.toFixed(2)}`;
  };
  const getList = () => {
    uni.showLoading({ title: "加载中...", mask: true });
    getQuotationList({
      ...page,
      quotationNo: quotationNo.value,
      status: getCurrentStatus(),
    })
      .then(res => {
        const records = res?.data?.records || res?.records || [];
        quotationList.value = Array.isArray(records) ? records : [];
      })
      .catch(() => {
        uni.showToast({ title: "查询失败", icon: "error" });
      })
      .finally(() => {
        uni.hideLoading();
      });
  };
  const handleDelete = item => {
    if (!item?.id) return;
    uni.showModal({
      title: "删除确认",
      content: "确认删除该报价单吗?",
      success: res => {
        if (!res.confirm) return;
        uni.showLoading({ title: "处理中...", mask: true });
        deleteQuotation(item.id)
          .then(() => {
            uni.showToast({ title: "删除成功", icon: "success" });
            getList();
          })
          .catch(() => {
            uni.showToast({ title: "删除失败", icon: "error" });
          })
          .finally(() => {
            uni.hideLoading();
          });
      },
    });
  };
  onShow(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/sales-common.scss";
  .tabs-section {
    background: #ffffff;
    padding: 0 12px 8px 12px;
  }
  .item-index {
    max-width: 180rpx;
    text-align: center;
  }
  .detail-value {
    max-width: 70%;
    word-break: break-all;
  }
</style>
src/pages/works.vue
@@ -301,6 +301,14 @@
  const marketingItems = reactive([
    {
      icon: "/static/images/icon/xiaoshoutaizhang.svg",
      label: "客户档案",
    },
    {
      icon: "/static/images/icon/xiaoshoutaizhang.svg",
      label: "销售报价",
    },
    {
      icon: "/static/images/icon/xiaoshoutaizhang.svg",
      label: "销售台账",
    },
    {
@@ -551,11 +559,21 @@
  const handleCommonItemClick = item => {
    // æ ¹æ®ä¸åŒçš„功能项进行跳转
    switch (item.label) {
      case "客户档案":
        uni.navigateTo({
          url: "/pages/basicData/customerFile/index",
        });
        break;
      case "销售台账":
        uni.navigateTo({
          url: "/pages/sales/salesAccount/index",
        });
        break;
      case "销售报价":
        uni.navigateTo({
          url: "/pages/sales/salesQuotation/index",
        });
        break;
      case "开票登记":
        uni.navigateTo({
          url: "/pages/sales/invoicingRegistration/index",