gaoluyang
2025-09-17 b3b3523e0cd0d5be0eea125fe2ee616528c212a6
销售报价前端页面
已添加2个文件
716 ■■■■■ 文件已修改
src/api/salesManagement/salesQuotation.js 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/index.vue 604 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesQuotation.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
// é”€å”®æŠ¥ä»·é¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢æŠ¥ä»·å•列表
export function quotationList(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: "put",
    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/views/salesManagement/salesQuotation/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,604 @@
<template>
  <div class="app-container">
    <el-card class="box-card">
      <!-- æœç´¢åŒºåŸŸ -->
      <el-row :gutter="20" class="search-row">
        <el-col :span="6">
          <el-input
            v-model="searchForm.quotationNo"
            placeholder="请输入报价单号"
            clearable
            @keyup.enter="handleSearch"
          >
            <template #prefix>
              <el-icon><Search /></el-icon>
            </template>
          </el-input>
        </el-col>
        <el-col :span="6">
          <el-select v-model="searchForm.customer" placeholder="请选择客户" clearable>
            <el-option label="上海科技有限公司" value="上海科技有限公司"></el-option>
            <el-option label="深圳电子有限公司" value="深圳电子有限公司"></el-option>
            <el-option label="北京贸易公司" value="北京贸易公司"></el-option>
          </el-select>
        </el-col>
        <el-col :span="6">
          <el-select v-model="searchForm.status" placeholder="请选择报价状态" clearable>
            <el-option label="草稿" value="草稿"></el-option>
            <el-option label="已发送" value="已发送"></el-option>
            <el-option label="客户确认" value="客户确认"></el-option>
            <el-option label="已过期" value="已过期"></el-option>
          </el-select>
        </el-col>
        <el-col :span="6">
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
          <el-button style="float: right;" type="primary" @click="handleAdd">
            æ–°å¢žæŠ¥ä»·
          </el-button>
        </el-col>
      </el-row>
      <!-- æŠ¥ä»·åˆ—表 -->
      <el-table
        :data="filteredList"
        style="width: 100%"
        v-loading="loading"
        border
        stripe
        height="calc(100vh - 22em)"
      >
        <el-table-column prop="id" label="ID" width="80" align="center"/>
        <el-table-column prop="quotationNo" label="报价单号" width="150" />
        <el-table-column prop="customer" label="客户名称" />
        <el-table-column prop="salesperson" label="业务员" width="100" />
        <el-table-column prop="quotationDate" label="报价日期" width="120" />
        <el-table-column prop="validDate" label="有效期至" width="120" />
        <el-table-column prop="totalAmount" label="报价金额" width="120">
          <template #default="scope">
            Â¥{{ scope.row.totalAmount.toFixed(2) }}
          </template>
        </el-table-column>
        <el-table-column prop="status" label="报价状态" width="100">
          <template #default="scope">
            <el-tag :type="getStatusType(scope.row.status)">
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="250" fixed="right" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
            <el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.status === '草稿'">编辑</el-button>
            <el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.status === '草稿'">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <pagination
        :total="pagination.total"
        layout="total, sizes, prev, pager, next, jumper"
        :page="pagination.currentPage"
        :limit="pagination.pageSize"
        @pagination="handleCurrentChange"
      />
    </el-card>
    <!-- æ–°å¢ž/编辑对话框 -->
    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="900px" :close-on-click-modal="false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
        <!-- åŸºæœ¬ä¿¡æ¯ -->
        <el-card class="form-card" shadow="never">
          <template #header>
            <span class="card-title">基本信息</span>
          </template>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="客户名称" prop="customer">
                <el-select v-model="form.customer" placeholder="请选择客户" style="width: 100%" @change="handleCustomerChange">
                  <el-option label="上海科技有限公司" value="上海科技有限公司"></el-option>
                  <el-option label="深圳电子有限公司" value="深圳电子有限公司"></el-option>
                  <el-option label="北京贸易公司" value="北京贸易公司"></el-option>
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="业务员" prop="salesperson">
                <el-select v-model="form.salesperson" placeholder="请选择业务员" style="width: 100%">
                  <el-option label="陈志强" value="陈志强"></el-option>
                  <el-option label="刘雅婷" value="刘雅婷"></el-option>
                  <el-option label="王建国" value="王建国"></el-option>
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="报价日期" prop="quotationDate">
                <el-date-picker
                  v-model="form.quotationDate"
                  type="date"
                  placeholder="选择报价日期"
                  style="width: 100%"
                  format="YYYY-MM-DD"
                  value-format="YYYY-MM-DD"
                />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="有效期至" prop="validDate">
                <el-date-picker
                  v-model="form.validDate"
                  type="date"
                  placeholder="选择有效期"
                  style="width: 100%"
                  format="YYYY-MM-DD"
                  value-format="YYYY-MM-DD"
                />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="付款方式" prop="paymentMethod">
                <el-select v-model="form.paymentMethod" placeholder="请选择付款方式" style="width: 100%">
                  <el-option label="全款到付" value="全款到付"></el-option>
                  <el-option label="分期付款" value="分期付款"></el-option>
                  <el-option label="月结" value="月结"></el-option>
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="交货期" prop="deliveryPeriod">
                <el-input v-model="form.deliveryPeriod" placeholder="请输入交货期" />
              </el-form-item>
            </el-col>
          </el-row>
        </el-card>
        <!-- äº§å“ä¿¡æ¯ -->
        <el-card class="form-card" shadow="never">
          <template #header>
            <div class="card-header">
              <span class="card-title">产品信息</span>
              <el-button type="primary" size="small" @click="addProduct">添加产品</el-button>
            </div>
          </template>
          <el-table :data="form.products" border style="width: 100%">
            <el-table-column prop="productName" label="产品名称" width="200">
              <template #default="scope">
                <el-input v-model="scope.row.productName" placeholder="请输入产品名称" />
              </template>
            </el-table-column>
            <el-table-column prop="specification" label="规格型号" width="150">
              <template #default="scope">
                <el-input v-model="scope.row.specification" placeholder="规格型号" />
              </template>
            </el-table-column>
            <el-table-column prop="quantity" label="数量" width="100">
              <template #default="scope">
                <el-input-number v-model="scope.row.quantity" :min="1" :precision="0" style="width: 100%" />
              </template>
            </el-table-column>
            <el-table-column prop="unit" label="单位" width="80">
              <template #default="scope">
                <el-input v-model="scope.row.unit" placeholder="单位" />
              </template>
            </el-table-column>
            <el-table-column prop="unitPrice" label="单价" width="120">
              <template #default="scope">
                <el-input-number v-model="scope.row.unitPrice" :min="0" :precision="2" style="width: 100%" @change="calculateAmount(scope.row)" />
              </template>
            </el-table-column>
            <el-table-column prop="amount" label="金额" width="120">
              <template #default="scope">
                <span>Â¥{{ scope.row.amount.toFixed(2) }}</span>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="80" align="center">
              <template #default="scope">
                <el-button link type="danger" @click="removeProduct(scope.$index)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </el-card>
        <!-- è´¹ç”¨ä¿¡æ¯ -->
        <el-card class="form-card" shadow="never">
          <template #header>
            <span class="card-title">费用信息</span>
          </template>
          <el-row :gutter="20">
            <el-col :span="8">
              <el-form-item label="产品小计">
                <el-input-number v-model="form.subtotal" :precision="2" :min="0" style="width: 100%" readonly />
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="运费">
                <el-input-number v-model="form.freight" :precision="2" :min="0" style="width: 100%" @change="calculateTotal" />
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="其他费用">
                <el-input-number v-model="form.otherFee" :precision="2" :min="0" style="width: 100%" @change="calculateTotal" />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="8">
              <el-form-item label="折扣率(%)">
                <el-input-number v-model="form.discountRate" :precision="2" :min="0" :max="100" style="width: 100%" @change="calculateTotal" />
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="折扣金额">
                <el-input-number v-model="form.discountAmount" :precision="2" :min="0" style="width: 100%" readonly />
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="报价总额">
                <el-input-number v-model="form.totalAmount" :precision="2" :min="0" style="width: 100%" readonly />
              </el-form-item>
            </el-col>
          </el-row>
        </el-card>
        <!-- å¤‡æ³¨ä¿¡æ¯ -->
        <el-card class="form-card" shadow="never">
          <template #header>
            <span class="card-title">备注信息</span>
          </template>
          <el-form-item label="备注" prop="remark">
            <el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息" rows="3"></el-input>
          </el-form-item>
        </el-card>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="dialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">ç¡® å®š</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æŸ¥çœ‹è¯¦æƒ…对话框 -->
    <el-dialog v-model="viewDialogVisible" title="报价详情" width="800px">
      <el-descriptions :column="2" border>
        <el-descriptions-item label="报价单号">{{ currentQuotation.quotationNo }}</el-descriptions-item>
        <el-descriptions-item label="客户名称">{{ currentQuotation.customer }}</el-descriptions-item>
        <el-descriptions-item label="业务员">{{ currentQuotation.salesperson }}</el-descriptions-item>
        <el-descriptions-item label="报价日期">{{ currentQuotation.quotationDate }}</el-descriptions-item>
        <el-descriptions-item label="有效期至">{{ currentQuotation.validDate }}</el-descriptions-item>
        <el-descriptions-item label="付款方式">{{ currentQuotation.paymentMethod }}</el-descriptions-item>
        <el-descriptions-item label="交货期">{{ currentQuotation.deliveryPeriod }}</el-descriptions-item>
        <el-descriptions-item label="报价状态">
          <el-tag :type="getStatusType(currentQuotation.status)">{{ currentQuotation.status }}</el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="报价总额" :span="2">
          <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">Â¥{{ currentQuotation.totalAmount?.toFixed(2) }}</span>
        </el-descriptions-item>
      </el-descriptions>
      <div style="margin-top: 20px;">
        <h4>产品明细</h4>
        <el-table :data="currentQuotation.products" border style="width: 100%">
          <el-table-column prop="productName" label="产品名称" />
          <el-table-column prop="specification" label="规格型号" />
          <el-table-column prop="quantity" label="数量" />
          <el-table-column prop="unit" label="单位" />
          <el-table-column prop="unitPrice" label="单价">
            <template #default="scope">
              Â¥{{ scope.row.unitPrice.toFixed(2) }}
            </template>
          </el-table-column>
          <el-table-column prop="amount" label="金额">
            <template #default="scope">
              Â¥{{ scope.row.amount.toFixed(2) }}
            </template>
          </el-table-column>
        </el-table>
      </div>
      <div v-if="currentQuotation.remark" style="margin-top: 20px;">
        <h4>备注</h4>
        <p>{{ currentQuotation.remark }}</p>
      </div>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import Pagination from '@/components/PIMTable/Pagination.vue'
// å“åº”式数据
const loading = ref(false)
const searchForm = reactive({
  quotationNo: '',
  customer: '',
  status: ''
})
const quotationList = ref([
  {
    id: 1,
    quotationNo: 'QT202312001',
    customer: '上海科技有限公司',
    salesperson: '陈志强',
    quotationDate: '2023-12-01',
    validDate: '2023-12-31',
    totalAmount: 50000.00,
    paymentMethod: '全款到付',
    deliveryPeriod: '30天',
    status: '已发送',
    remark: '重要客户报价',
    products: [
      { productName: '工业传感器', specification: 'SEN-001', quantity: 10, unit: '个', unitPrice: 5000, amount: 50000 }
    ]
  },
  {
    id: 2,
    quotationNo: 'QT202312002',
    customer: '深圳电子有限公司',
    salesperson: '刘雅婷',
    quotationDate: '2023-12-02',
    validDate: '2023-12-31',
    totalAmount: 35000.00,
    paymentMethod: '分期付款',
    deliveryPeriod: '20天',
    status: '客户确认',
    remark: '常规报价',
    products: [
      { productName: '控制模块', specification: 'CTL-002', quantity: 5, unit: '个', unitPrice: 7000, amount: 35000 }
    ]
  },
  {
    id: 3,
    quotationNo: 'QT202312003',
    customer: '北京贸易公司',
    salesperson: '王建国',
    quotationDate: '2023-12-03',
    validDate: '2023-12-31',
    totalAmount: 28000.00,
    paymentMethod: '月结',
    deliveryPeriod: '15天',
    status: '草稿',
    remark: '新客户报价',
    products: [
      { productName: '数据采集器', specification: 'DAQ-003', quantity: 4, unit: '个', unitPrice: 7000, amount: 28000 }
    ]
  }
])
const pagination = reactive({
  total: 3,
  currentPage: 1,
  pageSize: 10
})
const dialogVisible = ref(false)
const viewDialogVisible = ref(false)
const dialogTitle = ref('新增报价')
const form = reactive({
  customer: '',
  salesperson: '',
  quotationDate: '',
  validDate: '',
  paymentMethod: '',
  deliveryPeriod: '',
  status: '草稿',
  remark: '',
  products: [],
  subtotal: 0,
  freight: 0,
  otherFee: 0,
  discountRate: 0,
  discountAmount: 0,
  totalAmount: 0
})
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: 'change' }],
  deliveryPeriod: [{ required: true, message: '请输入交货期', trigger: 'blur' }]
}
const isEdit = ref(false)
const editId = ref(null)
const currentQuotation = ref({})
const formRef = ref()
// è®¡ç®—属性
const filteredList = computed(() => {
  let list = quotationList.value
  if (searchForm.quotationNo) {
    list = list.filter(item => item.quotationNo.includes(searchForm.quotationNo))
  }
  if (searchForm.customer) {
    list = list.filter(item => item.customer === searchForm.customer)
  }
  if (searchForm.status) {
    list = list.filter(item => item.status === searchForm.status)
  }
  return list
})
// æ–¹æ³•
const getStatusType = (status) => {
  const statusMap = {
    '草稿': 'info',
    '已发送': 'primary',
    '客户确认': 'success',
    '已过期': 'danger'
  }
  return statusMap[status] || 'info'
}
const handleSearch = () => {
  // æœç´¢é€»è¾‘已在computed中处理
}
const resetSearch = () => {
  searchForm.quotationNo = ''
  searchForm.customer = ''
  searchForm.status = ''
}
const handleAdd = () => {
  dialogTitle.value = '新增报价'
  isEdit.value = false
  resetForm()
  dialogVisible.value = true
}
const handleView = (row) => {
  currentQuotation.value = row
  viewDialogVisible.value = true
}
const handleEdit = (row) => {
  dialogTitle.value = '编辑报价'
  isEdit.value = true
  editId.value = row.id
  Object.assign(form, row)
  dialogVisible.value = true
}
const handleDelete = (row) => {
  ElMessageBox.confirm('确认删除该报价单吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    const index = quotationList.value.findIndex(item => item.id === row.id)
    if (index > -1) {
      quotationList.value.splice(index, 1)
      pagination.total--
      ElMessage.success('删除成功')
    }
  })
}
const resetForm = () => {
  form.customer = ''
  form.salesperson = ''
  form.quotationDate = ''
  form.validDate = ''
  form.paymentMethod = ''
  form.deliveryPeriod = ''
  form.status = '草稿'
  form.remark = ''
  form.products = []
  form.subtotal = 0
  form.freight = 0
  form.otherFee = 0
  form.discountRate = 0
  form.discountAmount = 0
  form.totalAmount = 0
}
const addProduct = () => {
  form.products.push({
    productName: '',
    specification: '',
    quantity: 1,
    unit: '',
    unitPrice: 0,
    amount: 0
  })
}
const removeProduct = (index) => {
  form.products.splice(index, 1)
  calculateSubtotal()
}
const calculateAmount = (product) => {
  product.amount = product.quantity * product.unitPrice
  calculateSubtotal()
}
const calculateSubtotal = () => {
  form.subtotal = form.products.reduce((sum, product) => sum + product.amount, 0)
  calculateTotal()
}
const calculateTotal = () => {
  form.discountAmount = form.subtotal * (form.discountRate / 100)
  form.totalAmount = form.subtotal + form.freight + form.otherFee - form.discountAmount
}
const handleCustomerChange = () => {
  // å¯ä»¥æ ¹æ®å®¢æˆ·ä¿¡æ¯è‡ªåŠ¨å¡«å……ä¸€äº›é»˜è®¤å€¼
}
const handleSubmit = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      if (form.products.length === 0) {
        ElMessage.warning('请至少添加一个产品')
        return
      }
      if (isEdit.value) {
        // ç¼–辑
        const index = quotationList.value.findIndex(item => item.id === editId.value)
        if (index > -1) {
          quotationList.value[index] = { ...form, id: editId.value }
          ElMessage.success('编辑成功')
        }
      } else {
        // æ–°å¢ž
        const newId = Math.max(...quotationList.value.map(item => item.id)) + 1
        const quotationNo = `QT${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(new Date().getDate()).padStart(2, '0')}${String(newId).padStart(3, '0')}`
        quotationList.value.push({
          ...form,
          id: newId,
          quotationNo: quotationNo
        })
        pagination.total++
        ElMessage.success('新增成功')
      }
      dialogVisible.value = false
    }
  })
}
const handleCurrentChange = (val) => {
  pagination.currentPage = val.page
  pagination.pageSize = val.limit
}
</script>
<style scoped>
.search-row {
  margin-bottom: 20px;
}
.form-card {
  margin-bottom: 20px;
}
.card-title {
  font-weight: bold;
  color: #303133;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.dialog-footer {
  text-align: right;
}
</style>