张诺
17 小时以前 d9b5c1f1310a4449ba3ffbefc9b15ec246789d53
src/views/salesManagement/salesQuotation/index.vue
@@ -38,6 +38,9 @@
          <el-button style="float: right;" type="primary" @click="handleAdd">
            新增报价
          </el-button>
          <el-button style="float: right;" type="primary" @click="handleImport">
            导入文件
          </el-button>
        </el-col>
      </el-row>
@@ -70,8 +73,8 @@
        </el-table-column>
        <el-table-column label="操作" width="200" 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)" :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>
          </template>
        </el-table-column>
@@ -231,43 +234,52 @@
            <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-tree-select
                           v-model="scope.row.productId"
                           placeholder="请选择"
                           clearable
                           check-strictly
                           @change="getModels($event, scope.row)"
                           :data="productOptions"
                           :render-after-expand="false"
                           style="width: 100%"
                        />
                <el-form-item :prop="`products.${scope.$index}.productId`" class="product-table-form-item">
                  <el-tree-select
                    v-model="scope.row.productId"
                    placeholder="请选择"
                    clearable
                    check-strictly
                    @change="getModels($event, scope.row)"
                    :data="productOptions"
                    :render-after-expand="false"
                    style="width: 100%"
                  />
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="specification" label="规格型号" width="150">
            <el-table-column prop="specification" label="规格型号" width="200">
              <template #default="scope">
                        <el-select
                           v-model="scope.row.specificationId"
                           placeholder="请选择"
                           clearable
                           @change="getProductModel($event, scope.row)"
                        >
                           <el-option
                              v-for="item in scope.row.modelOptions || []"
                              :key="item.id"
                              :label="item.model"
                              :value="item.id"
                           />
                        </el-select>
                <el-form-item :prop="`products.${scope.$index}.specificationId`" class="product-table-form-item">
                  <el-select
                    v-model="scope.row.specificationId"
                    placeholder="请选择"
                    clearable
                    @change="getProductModel($event, scope.row)"
                    style="width: 100%"
                  >
                    <el-option
                      v-for="item in scope.row.modelOptions || []"
                      :key="item.id"
                      :label="item.model"
                      :value="item.id"
                    />
                  </el-select>
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="unit" label="单位">
              <template #default="scope">
                <el-input v-model="scope.row.unit" placeholder="单位" />
                <el-form-item :prop="`products.${scope.$index}.unit`" class="product-table-form-item">
                  <el-input v-model="scope.row.unit" placeholder="单位" clearable/>
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="unitPrice" label="单价">
              <template #default="scope">
                <el-input-number v-model="scope.row.unitPrice" :min="0" :precision="2" style="width: 100%" />
                <el-form-item :prop="`products.${scope.$index}.unitPrice`" class="product-table-form-item">
                  <el-input-number v-model="scope.row.unitPrice" :min="0" :precision="2" style="width: 100%" />
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="80" align="center">
@@ -303,6 +315,106 @@
        </el-card>
      </el-form>
      </div>
    </FormDialog>
    <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>
              <span class="card-title">审批人选择</span>
              <el-button type="primary" size="small" @click="addImportApproverNode" class="header-btn">
                <el-icon><Plus /></el-icon>
                新增节点
              </el-button>
            </div>
          </template>
          <div class="form-content">
            <el-row>
              <el-col :span="24">
                <el-form-item>
                  <div class="approver-nodes-container">
                    <div
                      v-for="(node, index) in importApproverNodes"
                      :key="node.id"
                      class="approver-node-item"
                    >
                      <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>
                      </div>
                      <el-select
                        v-model="node.userId"
                        placeholder="选择人员"
                        class="approver-select"
                        clearable
                      >
                        <el-option
                          v-for="user in userList"
                          :key="user.userId"
                          :label="user.nickName"
                          :value="user.userId"
                        />
                      </el-select>
                      <el-button
                        type="danger"
                        size="small"
                        :icon="Delete"
                        @click="removeImportApproverNode(index)"
                        v-if="importApproverNodes.length > 1"
                        class="remove-btn"
                      >删除</el-button>
                    </div>
                  </div>
                </el-form-item>
              </el-col>
            </el-row>
          </div>
        </el-card>
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><Paperclip /></el-icon>
              <span class="card-title">附件材料</span>
            </div>
          </template>
          <div class="form-content">
            <el-form-item label="附件材料" prop="files">
          <el-upload
            v-model:file-list="importFileList"
            :limit="1"
            ref="fileUpload"
            :auto-upload="false"
            :on-change="handleFileChange"
            :on-exceed="handleExceed"
            :on-remove="handleRemove"
            :on-preview="handlePreview"
            :show-file-list="true"
          >
            <el-button type="primary">上传</el-button>
            <template #file="{ file }">
              <div style="display:flex; align-items:center; gap: 10px; width: 100%;">
                <span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
                  {{ file.name }}
                </span>
<!--                <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>
            </template>
            <template #tip>
              <div class="el-upload__tip">
                支持文档(xls, xlsx)格式
              </div>
            </template>
          </el-upload>
        </el-form-item>
          </div>
        </el-card>
    </FormDialog>
    <!-- 查看详情对话框 -->
@@ -345,15 +457,17 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted, markRaw, shallowRef } from 'vue'
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 } from '@element-plus/icons-vue'
import { Search, Document, UserFilled, Box, EditPen, Plus, ArrowRight, Delete, Download, View } from '@element-plus/icons-vue'
import Pagination from '@/components/PIMTable/Pagination.vue'
import FormDialog from '@/components/Dialog/FormDialog.vue'
import {getQuotationList,addQuotation,updateQuotation,deleteQuotation} from '@/api/salesManagement/salesQuotation.js'
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";
const { proxy } = getCurrentInstance();
// 响应式数据
const loading = ref(false)
@@ -373,7 +487,16 @@
})
const dialogVisible = ref(false)
const importDialogVisible = ref(false)
const viewDialogVisible = ref(false)
const importFileList = ref([])
const importApproverNodes = ref([
  { id: 1, userId: null }
])
let nextImportApproverId = 2
const fileUpload = ref(null)
const dialogTitle = ref('新增报价')
const form = reactive({
  quotationNo: '',
@@ -393,13 +516,30 @@
  totalAmount: 0
})
const rules = {
const baseRules = {
  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 productRowRules = {
  productId: [{ required: true, message: '请选择产品名称', trigger: 'change' }],
  specificationId: [{ required: true, message: '请选择规格型号', trigger: 'change' }],
  unit: [{ required: true, message: '请填写单位', trigger: 'blur' }],
  unitPrice: [{ required: true, message: '请填写单价', trigger: 'change' }]
}
const rules = computed(() => {
  const r = { ...baseRules }
  ;(form.products || []).forEach((_, i) => {
    r[`products.${i}.productId`] = productRowRules.productId
    r[`products.${i}.specificationId`] = productRowRules.specificationId
    r[`products.${i}.unit`] = productRowRules.unit
    r[`products.${i}.unitPrice`] = productRowRules.unitPrice
  })
  return r
})
const userList = ref([]);
const customerOption = ref([]);
@@ -424,18 +564,125 @@
  approverNodes.value.splice(index, 1)
}
// 导入弹窗审批人节点相关
function addImportApproverNode() {
  importApproverNodes.value.push({ id: nextImportApproverId++, userId: null })
}
function removeImportApproverNode(index) {
  importApproverNodes.value.splice(index, 1)
}
function triggerRemoveImportFile(file) {
  const index = importFileList.value.indexOf(file)
  if (index !== -1) {
    importFileList.value.splice(index, 1)
  }
}
async function handleImportSubmit() {
  if (importFileList.value.length === 0) {
    ElMessage.warning('请选择要导入的文件')
    return
  }
  const hasEmptyApprover = importApproverNodes.value.some(node => !node.userId)
  if (hasEmptyApprover) {
    ElMessage.error('请为所有审批节点选择审批人!')
    return
  }
  const selectedFile = importFileList.value[0]
  const rawFile = selectedFile?.raw || selectedFile
  if (!validateImportFile(rawFile)) {
    return
  }
  const formData = new FormData()
  formData.append('file', rawFile)
  // 审核人 IDs,以逗号分割
  const approveUserIds = importApproverNodes.value.map(node => node.userId).join(',')
  formData.append('approveUserIdsJson', approveUserIds)
  loading.value = true
  try {
    const res = await importQuotation(formData)
    if (res.code === 200) {
      ElMessage.success('导入成功')
      importDialogVisible.value = false
      handleSearch()
    }
  } catch (error) {
    console.error('导入失败:', error)
  } finally {
    loading.value = false
  }
}
const validateImportFile = (file) => {
  const fileName = file?.name || ''
  const isExcel = /\.(xls|xlsx)$/i.test(fileName)
  if (!isExcel) {
    ElMessage.error('仅支持 xls/xlsx 格式文件')
    return false
  }
  const isLt100M = (file?.size || 0) / 1024 / 1024 < 100
  if (!isLt100M) {
    ElMessage.error('上传文件大小不能超过 100MB!')
    return false
  }
  return true
}
const handleExceed = (files) => {
  // 达到上限时,新文件替换旧文件
  const file = files?.[0]
  if (!file || !validateImportFile(file)) return
  fileUpload.value?.clearFiles()
  fileUpload.value?.handleStart(file)
  importFileList.value = fileUpload.value?.uploadFiles?.slice(-1) || []
}
const handleFileChange = (file, list) => {
  const currentFile = file?.raw || file
  if (!validateImportFile(currentFile)) {
    fileUpload.value?.handleRemove?.(file)
    importFileList.value = []
    return
  }
  importFileList.value = (list || []).slice(-1)
}
const handleRemove = (file, list) => {
  importFileList.value = list
};
  // 处理文件移除
function triggerRemoveFile(file) {
  fileUpload.value?.handleRemove?.(file) || proxy.$refs.fileUpload?.handleRemove?.(file);
}
// 文件预览
function handlePreview(file) {
  const url = getUploadFileUrl(file)
  if (!url) return
  filePreviewRef.value?.open?.(url)
}
// 文件预览/下载
const handleDownload = (file) => {
  const url = getUploadFileUrl(file)
  if (!url) return
  proxy?.$modal?.loading?.("正在下载文件,请稍候...")
  proxy.$download.name(url);
  proxy?.$modal?.closeLoading?.()
}
// 计算属性
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
})
@@ -454,6 +701,27 @@
  searchForm.quotationNo = ''
  searchForm.customer = ''
  searchForm.status = ''
  // 重置到第一页并重新查询
  pagination.currentPage = 1
  handleSearch()
}
// 导入文件
const handleImport = async () => {
  importFileList.value = []
  // ✅ 清空“导入用”的审批人
  importApproverNodes.value = [{ id: 1, userId: null }]
  nextImportApproverId = 2
  let userLists = await userListNoPage();
  importDialogVisible.value = true
  userList.value = (userLists.data || []).map(item => ({
    userId: item.userId,
    nickName: item.nickName || '',
    userName: item.userName || ''
  }));
}
const handleAdd = async () => {
@@ -774,7 +1042,7 @@
        ElMessage.warning('请至少添加一个产品')
        return
      }
      // 审批人必填校验
      const hasEmptyApprover = approverNodes.value.some(node => !node.userId)
      if (hasEmptyApprover) {
@@ -822,10 +1090,14 @@
const handleCurrentChange = (val) => {
  pagination.currentPage = val.page
  pagination.pageSize = val.limit
  // 分页变化时重新查询列表
  handleSearch()
}
const handleSearch = ()=>{
  const params = {
    ...pagination,
    // 后端分页参数:current / size
    current: pagination.currentPage,
    size: pagination.pageSize,
    ...searchForm
  }
  getQuotationList(params).then(res=>{
@@ -956,6 +1228,17 @@
  padding: 8px 0;
}
.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;
  }
}
.approver-nodes-container {
  display: flex;
  flex-wrap: wrap;