张诺
19 小时以前 ccd67e291e00a2ad9c29ad8df43de6fab5a4afed
src/views/salesManagement/salesQuotation/index.vue
@@ -231,43 +231,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">
@@ -298,6 +307,52 @@
                maxlength="500"
                show-word-limit
              ></el-input>
            </el-form-item>
          </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="附件">
              <el-upload
                v-model:file-list="fileList"
                :action="upload.url"
                multiple
                ref="fileUpload"
                auto-upload
                :headers="upload.headers"
                :before-upload="handleBeforeUpload"
                :on-error="handleUploadError"
                :on-success="handleUploadSuccess"
                :on-remove="handleRemove"
                :on-preview="handlePreview"
              >
                <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">
                    文件格式支持 doc,docx,xls,xlsx,ppt,pptx,pdf,txt,xml,jpg,jpeg,png,gif,bmp,rar,zip,7z
                  </div>
                </template>
              </el-upload>
            </el-form-item>
          </div>
        </el-card>
@@ -341,19 +396,25 @@
        <p>{{ currentQuotation.remark }}</p>
      </div>
    </el-dialog>
    <filePreview ref="filePreviewRef" />
  </div>
</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, Paperclip, View, Download } 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 {userListNoPage} from "@/api/system/user.js";
import {customerList} from "@/api/salesManagement/salesLedger.js";
import { customerList, delLedgerFile } from "@/api/salesManagement/salesLedger.js";
import {modelList, productTreeList} from "@/api/basicData/product.js";
import { getToken } from "@/utils/auth";
import filePreview from "@/components/filePreview/index.vue";
const { proxy } = getCurrentInstance()
// 响应式数据
const loading = ref(false)
@@ -375,6 +436,13 @@
const dialogVisible = ref(false)
const viewDialogVisible = ref(false)
const dialogTitle = ref('新增报价')
const fileList = ref([])
const fileUpload = ref()
const filePreviewRef = ref()
const upload = reactive({
  url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
  headers: { Authorization: "Bearer " + getToken() },
})
const form = reactive({
  quotationNo: '',
  customer: '',
@@ -390,16 +458,34 @@
  otherFee: 0,
  discountRate: 0,
  discountAmount: 0,
  totalAmount: 0
  totalAmount: 0,
  tempFileIds: []
})
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([]);
@@ -427,15 +513,6 @@
// 计算属性
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,12 +531,33 @@
  searchForm.quotationNo = ''
  searchForm.customer = ''
  searchForm.status = ''
  // 重置到第一页并重新查询
  pagination.currentPage = 1
  handleSearch()
}
const normalizeQuotationFiles = (raw) => {
  const list =
    (raw && Array.isArray(raw.salesLedgerFiles) && raw.salesLedgerFiles) ||
    (raw && Array.isArray(raw.quotationFiles) && raw.quotationFiles) ||
    (raw && Array.isArray(raw.fileList) && raw.fileList) ||
    (raw && Array.isArray(raw.files) && raw.files) ||
    []
  return list
    .map((item) => ({
      id: item?.id,
      name: item?.fileName || item?.name || item?.originalName || item?.filename || "附件",
      url: item?.fileUrl || item?.url || item?.path || item?.tempPath,
      tempId: item?.tempId,
    }))
    .filter((i) => i.url)
}
const handleAdd = async () => {
  dialogTitle.value = '新增报价'
  isEdit.value = false
  resetForm()
  fileList.value = []
  // 重置审批人节点
  approverNodes.value = [{ id: 1, userId: null }]
  nextApproverId = 2
@@ -685,6 +783,7 @@
    userName: item.userName || ''
  }));
  
  fileList.value = normalizeQuotationFiles(row)
  dialogVisible.value = true
}
@@ -726,6 +825,9 @@
  form.discountRate = 0
  form.discountAmount = 0
  form.totalAmount = 0
  form.tempFileIds = []
  form.files = []
  fileList.value = []
}
const addProduct = () => {
@@ -767,6 +869,57 @@
  // 可以根据客户信息自动填充一些默认值
}
function handleBeforeUpload() {
  proxy?.$modal?.loading?.("正在上传文件,请稍候...")
  return true
}
function handleUploadError() {
  proxy?.$modal?.closeLoading?.()
  ElMessage.error("上传文件失败")
}
function handleUploadSuccess(res, file) {
  proxy?.$modal?.closeLoading?.()
  if (res?.code === 200) {
    file.tempId = res?.data?.tempId
    const url = res?.data?.tempPath || res?.data?.url
    if (url) file.url = url
    file.name = res?.data?.originalName || file?.name
    ElMessage.success("上传成功")
  } else {
    ElMessage.error(res?.msg || "上传失败")
    fileUpload.value?.handleRemove?.(file)
  }
}
function handleRemove(file) {
  if (!isEdit.value) return
  if (!file?.id) return
  delLedgerFile([file.id]).then((res) => {
    if (res?.code === 200) {
      ElMessage.success("删除成功")
    } else {
      ElMessage.error(res?.msg || "删除失败")
    }
  }).catch(() => {
    ElMessage.error("删除失败")
  })
}
const handleDownload = (file) => {
  if (!file?.url) return
  proxy?.$modal?.loading?.("正在下载文件,请稍候...")
  proxy.$download.name(file.url);
  proxy?.$modal?.closeLoading?.()
}
function handlePreview(file) {
  const url = file?.url || file?.response?.data?.tempPath || file?.response?.data?.url
  if (!url) return
  filePreviewRef.value?.open?.(url)
}
function triggerRemoveFile(file) {
  fileUpload.value?.handleRemove?.(file)
}
const handleSubmit = () => {
  formRef.value.validate((valid) => {
    if (valid) {
@@ -774,7 +927,7 @@
        ElMessage.warning('请至少添加一个产品')
        return
      }
      // 审批人必填校验
      const hasEmptyApprover = approverNodes.value.some(node => !node.userId)
      if (hasEmptyApprover) {
@@ -784,6 +937,13 @@
      
      // 收集所有节点的审批人id
      form.approveUserIds = approverNodes.value.map(node => node.userId).join(',')
      form.files = fileList.value.map(f => ({
        tempId: f.tempId,
        name: f.name,
        url: f.url,
        uid: f.uid,
      })) || []
      
      // 计算所有产品的单价总和
      form.totalAmount = form.products.reduce((sum, product) => {
@@ -822,10 +982,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=>{
@@ -844,6 +1008,7 @@
        // 审批人(用于编辑时反显)
        approveUserIds: item.approveUserIds || '',
        remark: item.remark || '',
        salesLedgerFiles: normalizeQuotationFiles(item),
        products: item.products ? item.products.map(product => ({
          productId: product.productId || '',
          product: product.product || product.productName || '',
@@ -956,6 +1121,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;