| | |
| | | <div class="card-header-wrapper"> |
| | | <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-button type="primary" size="small" @click="openProductSelect" class="header-btn"> |
| | | <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-column prop="product" label="产品名称" width="200"> |
| | | <el-table-column type="index" label="序号" width="60" align="center" /> |
| | | <el-table-column prop="product" label="产品名称" min-width="180"> |
| | | <template #default="scope"> |
| | | <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%" |
| | | /> |
| | | <span>{{ scope.row.product || '--' }}</span> |
| | | </el-form-item> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="specification" label="规格型号" width="200"> |
| | | <el-table-column prop="specification" label="规格型号" min-width="160"> |
| | | <template #default="scope"> |
| | | <el-form-item :prop="`products.${scope.$index}.productModelId`" class="product-table-form-item"> |
| | | <el-select |
| | | v-model="scope.row.productModelId" |
| | | 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> |
| | | <span>{{ scope.row.specification || '--' }}</span> |
| | | </el-form-item> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="unit" label="单位"> |
| | | <el-table-column prop="unit" label="单位" width="120"> |
| | | <template #default="scope"> |
| | | <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="单价"> |
| | | <el-table-column prop="unitPrice" label="单价" min-width="180"> |
| | | <template #default="scope"> |
| | | <el-form-item :prop="`products.${scope.$index}.unitPrice`" class="product-table-form-item"> |
| | | <el-input-number v-model="scope.row.unitPrice" :min="0" :precision="6" :step="0.000001" style="width: 100%" /> |
| | |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <el-empty v-else description="暂无产品,请点击添加产品" :image-size="80" /> |
| | | <el-empty v-else description="暂无产品,请点击选择产品" :image-size="80" /> |
| | | </div> |
| | | </el-card> |
| | | |
| | |
| | | </el-form> |
| | | </div> |
| | | </FormDialog> |
| | | |
| | | <!-- 选择产品弹窗 --> |
| | | <ProductSelectDialog v-model="productSelectVisible" @confirm="handleProductSelect" /> |
| | | |
| | | <!-- 查看详情对话框 --> |
| | | <el-dialog v-model="viewDialogVisible" title="报价详情" width="800px"> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, onMounted, markRaw, shallowRef } from 'vue' |
| | | import { ref, reactive, computed, onMounted } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { Search, Document, Box, EditPen, Plus } from '@element-plus/icons-vue' |
| | | import Pagination from '@/components/PIMTable/Pagination.vue' |
| | | import FormDialog from '@/components/Dialog/FormDialog.vue' |
| | | import ProductSelectDialog from '@/views/basicData/product/ProductSelectDialog.vue' |
| | | import {getQuotationList,addQuotation,updateQuotation,deleteQuotation} from '@/api/salesManagement/salesQuotation.js' |
| | | import {modelList, productTreeList} from "@/api/basicData/product.js"; |
| | | import {listCustomer} from "@/api/basicData/customer.js"; |
| | | import { userListNoPage } from "@/api/system/user.js"; |
| | | import { formatDecimal, roundAmount } from '@/utils/numberFormat'; |
| | |
| | | |
| | | const quotationList = ref([]) |
| | | const userList = ref([]) |
| | | const productOptions = ref([]); |
| | | const modelOptions = ref([]); |
| | | const pagination = reactive({ |
| | | total: 3, |
| | | currentPage: 1, |
| | |
| | | |
| | | const dialogVisible = ref(false) |
| | | const viewDialogVisible = ref(false) |
| | | const productSelectVisible = ref(false) |
| | | const dialogTitle = ref('新增报价') |
| | | const form = reactive({ |
| | | quotationNo: '', |
| | |
| | | isEdit.value = false |
| | | resetForm() |
| | | dialogVisible.value = true |
| | | getProductOptions(); |
| | | fetchCustomerOptions() |
| | | } |
| | | |
| | |
| | | customerOption.value = res.data.records; |
| | | }); |
| | | } |
| | | const getProductOptions = () => { |
| | | // 返回 Promise,便于编辑时 await 确保能反显 |
| | | return productTreeList().then((res) => { |
| | | productOptions.value = convertIdToValue(res); |
| | | return productOptions.value |
| | | }); |
| | | }; |
| | | function convertIdToValue(data) { |
| | | return data.map((item) => { |
| | | const { id, children, ...rest } = item; |
| | | const newItem = { |
| | | ...rest, |
| | | value: id, // 将 id 改为 value |
| | | }; |
| | | if (children && children.length > 0) { |
| | | newItem.children = convertIdToValue(children); |
| | | } |
| | | |
| | | return newItem; |
| | | }); |
| | | // 打开选择产品弹窗(支持多选) |
| | | const openProductSelect = () => { |
| | | productSelectVisible.value = true |
| | | } |
| | | // 根据名称反查节点 id,便于仅存名称时的反显 |
| | | function findNodeIdByLabel(nodes, label) { |
| | | if (!label) return null; |
| | | for (let i = 0; i < nodes.length; i++) { |
| | | const node = nodes[i]; |
| | | if (node.label === label) return node.value; |
| | | if (node.children && node.children.length > 0) { |
| | | const found = findNodeIdByLabel(node.children, label); |
| | | if (found !== null && found !== undefined) return found; |
| | | } |
| | | } |
| | | return null; |
| | | // 选择产品弹窗确认回调:批量追加到产品明细 |
| | | const handleProductSelect = (rows) => { |
| | | if (!Array.isArray(rows) || rows.length === 0) return |
| | | const getRowKey = (row) => String(row.productModelId || `${row.productId || ''}-${row.specification || ''}`) |
| | | const existingKeys = new Set(form.products.map(getRowKey)) |
| | | let addedCount = 0 |
| | | rows.forEach((row) => { |
| | | const productModelId = row.id ?? row.productModelId ?? '' |
| | | const item = { |
| | | productId: row.productId ?? '', |
| | | product: row.productName || row.product || '', |
| | | productName: row.productName || row.product || '', |
| | | productModelId, |
| | | specification: row.model || row.specification || '', |
| | | quantity: 1, |
| | | unit: row.unit || '', |
| | | unitPrice: 0, |
| | | amount: 0 |
| | | } |
| | | const key = getRowKey(item) |
| | | if (existingKeys.has(key)) return |
| | | existingKeys.add(key) |
| | | form.products.push(item) |
| | | addedCount++ |
| | | }) |
| | | if (addedCount === 0) { |
| | | ElMessage.warning('所选产品已在明细中,请勿重复选择') |
| | | } |
| | | } |
| | | const getModels = (value, row) => { |
| | | if (!row) return; |
| | | // 如果清空选择,则清空相关字段 |
| | | if (!value) { |
| | | row.productId = ''; |
| | | row.product = ''; |
| | | row.modelOptions = []; |
| | | row.productModelId = ''; |
| | | row.specification = ''; |
| | | row.unit = ''; |
| | | return; |
| | | } |
| | | // 更新 productId(v-model 已经自动更新,这里确保一致性) |
| | | row.productId = value; |
| | | // 找到对应的 label 并赋值给 row.product |
| | | const label = findNodeById(productOptions.value, value); |
| | | if (label) { |
| | | row.product = label; |
| | | } |
| | | // 获取规格型号列表,设置到当前行的 modelOptions |
| | | modelList({ id: value }).then((res) => { |
| | | row.modelOptions = res || []; |
| | | }); |
| | | }; |
| | | const getProductModel = (value, row) => { |
| | | if (!row) return; |
| | | // 如果清空选择,则清空相关字段 |
| | | if (!value) { |
| | | row.productModelId = ''; |
| | | row.specification = ''; |
| | | row.unit = ''; |
| | | return; |
| | | } |
| | | // 更新 productModelId(v-model 已经自动更新,这里确保一致性) |
| | | row.productModelId = value; |
| | | const modelOptions = row.modelOptions || []; |
| | | const index = modelOptions.findIndex((item) => item.id === value); |
| | | if (index !== -1) { |
| | | row.specification = modelOptions[index].model; |
| | | row.unit = modelOptions[index].unit; |
| | | } else { |
| | | row.specification = ''; |
| | | row.unit = ''; |
| | | } |
| | | }; |
| | | const findNodeById = (nodes, productId) => { |
| | | for (let i = 0; i < nodes.length; i++) { |
| | | if (nodes[i].value === productId) { |
| | | return nodes[i].label; // 找到节点,返回 label |
| | | } |
| | | if (nodes[i].children && nodes[i].children.length > 0) { |
| | | const foundLabel = findNodeById(nodes[i].children, productId); |
| | | if (foundLabel) { |
| | | return foundLabel; // 在子节点中找到,返回 label |
| | | } |
| | | } |
| | | } |
| | | return null; // 没有找到节点,返回null |
| | | }; |
| | | const handleView = (row) => { |
| | | // 只复制需要的字段,避免将组件引用放入响应式对象 |
| | | currentQuotation.value = { |
| | |
| | | isEdit.value = true |
| | | editId.value = row.id |
| | | form.id = row.id || form.id || null |
| | | // 先加载产品树数据,否则 el-tree-select 无法反显产品名称 |
| | | await getProductOptions() |
| | | await fetchCustomerOptions() |
| | | |
| | | // 只复制需要的字段,避免将组件引用放入响应式对象 |
| | |
| | | form.paymentMethod = row.paymentMethod || '' |
| | | form.status = row.status || '草稿' |
| | | form.remark = row.remark || '' |
| | | form.products = row.products ? await Promise.all(row.products.map(async (product) => { |
| | | const productName = product.product || product.productName || '' |
| | | // 优先用 productId;如果只有名称,尝试反查 id 以便树选择器反显 |
| | | const resolvedProductId = product.productId |
| | | ? Number(product.productId) |
| | | : findNodeIdByLabel(productOptions.value, productName) || '' |
| | | |
| | | // 如果有产品ID,加载对应的规格型号列表 |
| | | let modelOptions = []; |
| | | let resolvedProductModelId = product.productModelId || ''; |
| | | |
| | | if (resolvedProductId) { |
| | | try { |
| | | const res = await modelList({ id: resolvedProductId }); |
| | | modelOptions = res || []; |
| | | |
| | | // 如果返回的数据没有 productModelId,但有 specification 名称,根据名称查找 ID |
| | | if (!resolvedProductModelId && product.specification) { |
| | | const foundModel = modelOptions.find(item => item.model === product.specification); |
| | | if (foundModel) { |
| | | resolvedProductModelId = foundModel.id; |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.error('加载规格型号失败:', error); |
| | | } |
| | | } |
| | | |
| | | return { |
| | | productId: resolvedProductId, |
| | | product: productName, |
| | | productModelId: resolvedProductModelId, |
| | | specification: product.specification || '', |
| | | quantity: product.quantity || 0, |
| | | unit: product.unit || '', |
| | | unitPrice: product.unitPrice || 0, |
| | | amount: product.amount || 0, |
| | | modelOptions: modelOptions // 为每行添加独立的规格型号列表 |
| | | } |
| | | form.products = row.products ? row.products.map((product) => ({ |
| | | productId: product.productId || '', |
| | | product: product.product || product.productName || '', |
| | | productName: product.productName || product.product || '', |
| | | productModelId: product.productModelId || '', |
| | | specification: product.specification || '', |
| | | quantity: product.quantity || 0, |
| | | unit: product.unit || '', |
| | | unitPrice: product.unitPrice || 0, |
| | | amount: product.amount || 0 |
| | | })) : [] |
| | | form.subtotal = row.subtotal || 0 |
| | | form.freight = row.freight || 0 |
| | |
| | | form.discountRate = 0 |
| | | form.discountAmount = 0 |
| | | form.totalAmount = 0 |
| | | } |
| | | |
| | | const addProduct = () => { |
| | | form.products.push({ |
| | | productId: '', |
| | | product: '', |
| | | productName: '', |
| | | productModelId: '', |
| | | quantity: 1, |
| | | unit: '', |
| | | unitPrice: 0, |
| | | amount: 0, |
| | | modelOptions: [] // 为每行添加独立的规格型号列表 |
| | | }) |
| | | } |
| | | |
| | | const removeProduct = (index) => { |