| | |
| | | <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"> |
| | |
| | | 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> |
| | |
| | | <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) |
| | |
| | | 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: '', |
| | |
| | | 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([]); |
| | | |
| | |
| | | // 计算属性 |
| | | 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 |
| | | }) |
| | | |
| | |
| | | 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 |
| | |
| | | userName: item.userName || '' |
| | | })); |
| | | |
| | | fileList.value = normalizeQuotationFiles(row) |
| | | dialogVisible.value = true |
| | | } |
| | | |
| | |
| | | form.discountRate = 0 |
| | | form.discountAmount = 0 |
| | | form.totalAmount = 0 |
| | | form.tempFileIds = [] |
| | | form.files = [] |
| | | fileList.value = [] |
| | | } |
| | | |
| | | const addProduct = () => { |
| | |
| | | // 可以根据客户信息自动填充一些默认值 |
| | | } |
| | | |
| | | 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) { |
| | |
| | | ElMessage.warning('请至少添加一个产品') |
| | | return |
| | | } |
| | | |
| | | |
| | | // 审批人必填校验 |
| | | const hasEmptyApprover = approverNodes.value.some(node => !node.userId) |
| | | if (hasEmptyApprover) { |
| | |
| | | |
| | | // 收集所有节点的审批人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) => { |
| | |
| | | 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=>{ |
| | |
| | | // 审批人(用于编辑时反显) |
| | | approveUserIds: item.approveUserIds || '', |
| | | remark: item.remark || '', |
| | | salesLedgerFiles: normalizeQuotationFiles(item), |
| | | products: item.products ? item.products.map(product => ({ |
| | | productId: product.productId || '', |
| | | product: product.product || product.productName || '', |
| | |
| | | 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; |