| | |
| | | <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> |
| | | |
| | |
| | | </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> |
| | | |
| | | <!-- 查看详情对话框 --> |
| | | <el-dialog v-model="viewDialogVisible" title="报价详情" width="800px"> |
| | | <el-descriptions :column="2" border> |
| | |
| | | </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) |
| | |
| | | }) |
| | | |
| | | 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: '', |
| | |
| | | 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 |
| | |
| | | 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 () => { |
| | | dialogTitle.value = '新增报价' |
| | | isEdit.value = false |