chenhj
18 小时以前 cb76922f5e8ccade2bdd3e1c73c64c2fe711c45f
feat(salesQuotation): 添加导入详情功能及优化界面交互

- 新增 ImportQuotationDetail 组件用于显示导入报价单详情
- 在报价单列表中增加“导入详情”按钮,点击可查看详细信息
- 优化报价单界面模板空白和缩进格式,提升代码可读性
- 新增导入报价单表单的审批人选择与附件上传功能
- 修正审批节点和导入审批节点数据结构一致性
- 调整文件上传提示,支持Excel格式模板下载及上传
- 增加查看详情对话框中产品明细及备注显示完善
- 优化按钮和图标排版,统一元素样式与交互体验
已添加2个文件
已修改1个文件
242 ■■■■ 文件已修改
src/api/salesManagement/salesQuotationRecord.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/ImportQuotationDetail.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/index.vue 126 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesQuotationRecord.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
// é”€å”®æŠ¥ä»·é¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢æŠ¥ä»·å•列表
export function getQuotationRecordList(query) {
  return request({
    url: "/sales/quotationRecord/listPage",
    method: "get",
    params: query,
  });
}
src/views/salesManagement/salesQuotation/ImportQuotationDetail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,105 @@
<script setup>
import {ref} from 'vue'
import {getQuotationRecordList} from "@/api/salesManagement/salesQuotationRecord.js";
const props = defineProps({
  showModal: {
    type: Boolean,
    required: true
  },
  quotationId: {
    type: Number,
    required: true
  }
})
const emit = defineEmits(['update:showModal'])
const dialogVisible = computed({
  get: () => props.showModal,
  set: (val) => emit('update:showModal', val)
})
const leftTableData = ref([])
const rightTableData = ref([])
const selectedLeftRow = ref(null)
const handleRowClick = (row) => {
  selectedLeftRow.value = row
  rightTableData.value = row.products || []
}
const tableRowClassName = ({row}) => {
  return selectedLeftRow.value === row ? 'selected-row' : ''
}
const fetchData = () => {
  getQuotationRecordList({quotationRecordId: props.quotationId}).then(res => {
    const data = res.data.records || []
    data.forEach(item => {
      leftTableData.value.push(JSON.parse(item?.info || '{}'))
    })
  })
}
const leftColumns = [
  {prop: 'customer', label: '客户名称', minWidth: '120'},
  {prop: 'salesperson', label: '业务员', minWidth: '120'},
  {prop: 'quotationDate', label: '报价日期', minWidth: '120'},
  {prop: 'validDate', label: '有效期至', minWidth: '120'},
  {prop: 'paymentMethod', label: '支付方式', minWidth: '120'},
  {prop: 'remark', label: '备注', minWidth: '120'}
]
const rightColumns = [
  {prop: 'product', label: '产品名称', minWidth: '120'},
  {prop: 'specification', label: '型号', minWidth: '120'},
  {prop: 'unit', label: '单位', minWidth: '100'},
  {prop: 'unitPrice', label: '单价', minWidth: '100'}
]
onMounted(() => {
  fetchData()
})
</script>
<template>
  <el-dialog v-model="dialogVisible" title="详情" width="1000px" :close-on-click-modal="false">
    <el-row :gutter="20">
      <el-col :span="12">
        <div class="table-title">报价单记录</div>
        <el-table :data="leftTableData" border height="400" stripe highlight-current-row
                  @row-click="handleRowClick" :row-class-name="tableRowClassName">
          <el-table-column v-for="col in leftColumns" :key="col.prop" :prop="col.prop" :label="col.label"
                           :min-width="col.minWidth" show-overflow-tooltip/>
        </el-table>
      </el-col>
      <el-col :span="12">
        <div class="table-title">报价单产品</div>
        <el-table :data="rightTableData" border height="400" stripe>
          <el-table-column v-for="col in rightColumns" :key="col.prop" :prop="col.prop" :label="col.label"
                           :min-width="col.minWidth" show-overflow-tooltip/>
        </el-table>
      </el-col>
    </el-row>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="dialogVisible = false">关闭</el-button>
      </div>
    </template>
  </el-dialog>
</template>
<style scoped>
.table-title {
  font-size: 15px;
  font-weight: bold;
  margin-bottom: 10px;
  color: #303133;
}
:deep(.selected-row) {
  background-color: #ecf5ff !important;
}
</style>
src/views/salesManagement/salesQuotation/index.vue
@@ -11,13 +11,16 @@
            @keyup.enter="handleSearch"
          >
            <template #prefix>
              <el-icon><Search /></el-icon>
              <el-icon>
                <Search/>
              </el-icon>
            </template>
          </el-input>
        </el-col>
        <el-col :span="8">
          <el-select v-model="searchForm.customer" placeholder="请选择客户" clearable>
                        <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.customerName">
            <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName"
                       :value="item.customerName">
                            {{
                                item.customerName + "——" + item.taxpayerIdentificationNumber
                            }}
@@ -71,11 +74,14 @@
            Â¥{{ scope.row.totalAmount.toFixed(2) }}
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200" fixed="right" align="center">
        <el-table-column label="操作" width="250" fixed="right" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="handleEdit(scope.row)" :disabled="!['待审批','拒绝'].includes(scope.row.status)">编辑</el-button>
            <el-button link type="primary" @click="handleEdit(scope.row)"
                       :disabled="!['待审批','拒绝'].includes(scope.row.status)">编辑
            </el-button>
            <el-button link type="primary" @click="handleView(scope.row)" style="color: #67C23A">查看</el-button>
            <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
            <el-button link type="primary" @click="handleImportDetail(scope.row)">导入详情</el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -91,14 +97,17 @@
    </el-card>
    <!-- æ–°å¢ž/编辑对话框 -->
    <FormDialog v-model="dialogVisible" :title="dialogTitle" width="85%" :close-on-click-modal="false" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
    <FormDialog v-model="dialogVisible" :title="dialogTitle" width="85%" :close-on-click-modal="false"
                @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
      <div class="quotation-form-container">
        <el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="quotation-form">
        <!-- åŸºæœ¬ä¿¡æ¯ -->
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><Document /></el-icon>
                <el-icon class="card-icon">
                  <Document/>
                </el-icon>
              <span class="card-title">基本信息</span>
            </div>
          </template>
@@ -106,8 +115,10 @@
            <el-row :gutter="24">
              <el-col :span="12">
                <el-form-item label="客户名称" prop="customer">
                  <el-select v-model="form.customer" placeholder="请选择客户" style="width: 100%" @change="handleCustomerChange" clearable>
                    <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.customerName">
                    <el-select v-model="form.customer" placeholder="请选择客户" style="width: 100%"
                               @change="handleCustomerChange" clearable>
                      <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName"
                                 :value="item.customerName">
                      {{
                        item.customerName + "——" + item.taxpayerIdentificationNumber
                      }}
@@ -166,10 +177,14 @@
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><UserFilled /></el-icon>
                <el-icon class="card-icon">
                  <UserFilled/>
                </el-icon>
              <span class="card-title">审批人选择</span>
              <el-button type="primary" size="small" @click="addApproverNode" class="header-btn">
                <el-icon><Plus /></el-icon>
                  <el-icon>
                    <Plus/>
                  </el-icon>
                æ–°å¢žèŠ‚ç‚¹
              </el-button>
            </div>
@@ -187,7 +202,9 @@
                      <div class="approver-node-label">
                        <span class="node-step">{{ index + 1 }}</span>
                        <span class="node-text">审批人</span>
                        <el-icon class="arrow-icon"><ArrowRight /></el-icon>
                          <el-icon class="arrow-icon">
                            <ArrowRight/>
                          </el-icon>
                      </div>
                      <el-select
                        v-model="node.userId"
@@ -209,7 +226,8 @@
                        @click="removeApproverNode(index)"
                        v-if="approverNodes.length > 1"
                        class="remove-btn"
                      >删除</el-button>
                        >删除
                        </el-button>
                    </div>
                  </div>
                </el-form-item>
@@ -222,16 +240,21 @@
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><Box /></el-icon>
                <el-icon class="card-icon">
                  <Box/>
                </el-icon>
              <span class="card-title">产品信息</span>
              <el-button type="primary" size="small" @click="addProduct" class="header-btn">
                <el-icon><Plus /></el-icon>
                  <el-icon>
                    <Plus/>
                  </el-icon>
                æ·»åŠ äº§å“
              </el-button>
            </div>
          </template>
          <div class="form-content">
            <el-table :data="form.products" border style="width: 100%" class="product-table" v-if="form.products.length > 0">
              <el-table :data="form.products" border style="width: 100%" class="product-table"
                        v-if="form.products.length > 0">
            <el-table-column prop="product" label="产品名称" width="200">
              <template #default="scope">
                <el-form-item :prop="`products.${scope.$index}.productId`" class="product-table-form-item">
@@ -296,7 +319,9 @@
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><EditPen /></el-icon>
                <el-icon class="card-icon">
                  <EditPen/>
                </el-icon>
              <span class="card-title">备注信息</span>
            </div>
          </template>
@@ -317,15 +342,21 @@
      </div>
    </FormDialog>
    <FormDialog v-model="importDialogVisible" title="导入报价单" width="85%" :close-on-click-modal="false" @close="importDialogVisible = false" @confirm="handleImportSubmit" @cancel="importDialogVisible = false">
    <FormDialog v-model="importDialogVisible" title="导入报价单" width="85%" :close-on-click-modal="false"
                @close="importDialogVisible = false" @confirm="handleImportSubmit"
                @cancel="importDialogVisible = false">
      <!-- å®¡æ‰¹äººä¿¡æ¯ -->
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><UserFilled /></el-icon>
            <el-icon class="card-icon">
              <UserFilled/>
            </el-icon>
              <span class="card-title">审批人选择</span>
              <el-button type="primary" size="small" @click="addImportApproverNode" class="header-btn">
                <el-icon><Plus /></el-icon>
              <el-icon>
                <Plus/>
              </el-icon>
                æ–°å¢žèŠ‚ç‚¹
              </el-button>
            </div>
@@ -343,7 +374,9 @@
                      <div class="approver-node-label">
                        <span class="node-step">{{ index + 1 }}</span>
                        <span class="node-text">审批人</span>
                        <el-icon class="arrow-icon"><ArrowRight /></el-icon>
                      <el-icon class="arrow-icon">
                        <ArrowRight/>
                      </el-icon>
                      </div>
                      <el-select
                        v-model="node.userId"
@@ -365,7 +398,8 @@
                        @click="removeImportApproverNode(index)"
                        v-if="importApproverNodes.length > 1"
                        class="remove-btn"
                      >删除</el-button>
                    >删除
                    </el-button>
                    </div>
                  </div>
                </el-form-item>
@@ -376,11 +410,14 @@
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><Paperclip /></el-icon>
            <el-icon class="card-icon">
              <Paperclip/>
            </el-icon>
              <span class="card-title">附件材料</span>
            </div>
          </template>
          <div class="form-content">
          <el-button type="primary" @click="downloadImportTemplate" style="margin-bottom: 20px">下载模板文件</el-button>
            <el-form-item label="附件材料" prop="files">
          <el-upload
            v-model:file-list="importFileList"
@@ -399,11 +436,11 @@
                <span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
                  {{ file.name }}
                </span>
<!--                <div style="display:flex; align-items:center; gap: 6px;">-->
                  <div style="display:flex; align-items:center; gap: 6px;">
<!--                  <el-button link type="success" :icon="Download" @click="handleDownload(file)" />-->
<!--                  <el-button link type="primary" :icon="View" @click="handlePreview(file)" />-->
<!--                  <el-button link type="danger" :icon="Delete" @click="triggerRemoveFile(file)" />-->
<!--                </div>-->
                  </div>
              </div>
            </template>
            <template #tip>
@@ -430,7 +467,9 @@
<!--          <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>
          <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">Â¥{{
              currentQuotation.totalAmount?.toFixed(2)
            }}</span>
        </el-descriptions-item>
      </el-descriptions>
      
@@ -453,16 +492,33 @@
        <p>{{ currentQuotation.remark }}</p>
      </div>
    </el-dialog>
    <ImportQuotationDetail v-if="showDetail" v-model:showModal="showDetail" :quotationId="currentQuotation.id" />
  </div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, markRaw, shallowRef, getCurrentInstance } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Document, UserFilled, Box, EditPen, Plus, ArrowRight, Delete, Download, View } from '@element-plus/icons-vue'
import {
  Search,
  Document,
  UserFilled,
  Box,
  EditPen,
  Plus,
  ArrowRight,
  Delete,
} from '@element-plus/icons-vue'
import Pagination from '@/components/PIMTable/Pagination.vue'
import FormDialog from '@/components/Dialog/FormDialog.vue'
import {getQuotationList,addQuotation,updateQuotation,deleteQuotation, importQuotation} from '@/api/salesManagement/salesQuotation.js'
import ImportQuotationDetail from '@/views/salesManagement/salesQuotation/ImportQuotationDetail.vue'
import {
  getQuotationList,
  addQuotation,
  updateQuotation,
  deleteQuotation,
  importQuotation
} from '@/api/salesManagement/salesQuotation.js'
import {userListNoPage} from "@/api/system/user.js";
import {customerList} from "@/api/salesManagement/salesLedger.js";
import {modelList, productTreeList} from "@/api/basicData/product.js";
@@ -550,6 +606,7 @@
let nextApproverId = 2
const isEdit = ref(false)
const showDetail = ref(false)
const editId = ref(null)
const currentQuotation = ref({})
const formRef = ref()
@@ -756,6 +813,7 @@
        return productOptions.value
    });
};
function convertIdToValue(data) {
    return data.map((item) => {
        const { id, children, ...rest } = item;
@@ -770,6 +828,7 @@
        return newItem;
    });
}
// æ ¹æ®åç§°åæŸ¥èŠ‚ç‚¹ id,便于仅存名称时的反显
function findNodeIdByLabel(nodes, label) {
    if (!label) return null;
@@ -783,6 +842,7 @@
    }
    return null;
}
const getModels = (value, row) => {
    if (!row) return;
    // å¦‚果清空选择,则清空相关字段
@@ -1087,6 +1147,10 @@
  })
}
const downloadImportTemplate = () => {
  proxy.download("/sales/quotation/downloadTemplate", {}, "报价单导入模板.xlsx");
}
const handleCurrentChange = (val) => {
  pagination.currentPage = val.page
  pagination.pageSize = val.limit
@@ -1145,6 +1209,12 @@
    }))
    });
}
const handleImportDetail = (row) => {
  showDetail.value = true
  currentQuotation.value = row
}
onMounted(()=>{
  handleSearch()
@@ -1230,9 +1300,11 @@
.product-table-form-item {
  margin-bottom: 0;
  :deep(.el-form-item__content) {
    margin-left: 0 !important;
  }
  :deep(.el-form-item__label) {
    width: auto;
    min-width: auto;