Files
mom-web/src/views/erp/purchase/inquiry/components/InquiryQuoteForm.vue

512 lines
17 KiB
Vue
Raw Normal View History

2026-03-05 16:52:12 +08:00
<template>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
v-loading="formLoading"
label-position="top"
:inline-message="true"
:disabled="disabled"
>
<div class="mobile-quote-list">
<div
v-for="(row, $index) in formData"
:key="$index"
class="mobile-quote-card"
:class="{ 'mobile-quote-card--selected': row.isSelected }"
>
<div class="mobile-quote-card__header">
<div class="mobile-quote-card__header-left">
<el-checkbox v-model="row.isSelected" @change="onSelectChange(row, $index)" :disabled="disabled" />
<span class="mobile-quote-card__index">#{{ $index + 1 }}</span>
<span class="mobile-quote-card__name">{{ row.supplierName || '未选择供应商' }}</span>
</div>
<el-button @click="handleDelete($index)" link type="danger" size="small" :disabled="disabled">删除</el-button>
</div>
<div class="mobile-quote-card__body">
<el-form-item label="供应商" :prop="`${$index}.supplierId`" :rules="formRules.supplierId">
<div class="mobile-quote-card__supplier-row">
<el-select
v-model="row.supplierId"
clearable
filterable
@change="onChangeSupplier($event, row)"
placeholder="请选择供应商"
style="flex: 1"
>
<el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-button v-if="!disabled" type="primary" size="small" @click.stop="openQuickSupplierForm($index)">新增</el-button>
</div>
</el-form-item>
<div class="mobile-quote-card__input-group">
<el-form-item label="联系人" :prop="`${$index}.contactName`">
<el-input v-model="row.contactName" placeholder="联系人" />
</el-form-item>
<el-form-item label="联系电话" :prop="`${$index}.contactPhone`">
<el-input v-model="row.contactPhone" placeholder="联系电话" />
</el-form-item>
</div>
<el-form-item label="报价日期" :prop="`${$index}.quoteDate`">
<el-date-picker v-model="row.quoteDate" type="date" value-format="x" placeholder="报价日期" style="width: 100%" />
</el-form-item>
<div class="mobile-quote-card__input-group">
<el-form-item label="单价" :prop="`${$index}.unitPrice`" :rules="formRules.unitPrice">
<el-input-number v-model="row.unitPrice" controls-position="right" :min="0" :precision="4" style="width: 100%" placeholder="单价" />
</el-form-item>
<el-form-item label="总价">
<el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
</el-form-item>
</div>
<div class="mobile-quote-card__input-group">
<el-form-item label="付款条件" :prop="`${$index}.paymentTerms`">
<el-select v-model="row.paymentTerms" placeholder="付款条件" clearable style="width: 100%">
<el-option label="款到发货" value="款到发货" />
<el-option label="货到付款" value="货到付款" />
<el-option label="月结30天" value="月结30天" />
<el-option label="月结60天" value="月结60天" />
<el-option label="月结90天" value="月结90天" />
<el-option label="预付30%" value="预付30%" />
<el-option label="预付50%" value="预付50%" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="交货周期(天)" :prop="`${$index}.deliveryCycle`">
<el-input-number v-model="row.deliveryCycle" controls-position="right" :min="0" :precision="0" style="width: 100%" placeholder="天数" />
</el-form-item>
</div>
<el-form-item label="资质文件" :prop="`${$index}.qualificationFile`">
<UploadFile :is-show-tip="false" v-model="row.qualificationFile" :limit="1" />
</el-form-item>
<el-form-item label="备注" :prop="`${$index}.remark`">
<el-input v-model="row.remark" placeholder="请输入备注" />
</el-form-item>
<!-- 评分区域 -->
<div class="mobile-quote-card__scores">
<div class="mobile-quote-card__scores-title">评分</div>
<div class="mobile-quote-card__input-group">
<el-form-item label="质量评分" :prop="`${$index}.qualityScore`">
<el-input-number v-model="row.qualityScore" controls-position="right" :min="0" :max="100" :precision="0" style="width: 100%" placeholder="0-100" />
</el-form-item>
<el-form-item label="服务评分" :prop="`${$index}.serviceScore`">
<el-input-number v-model="row.serviceScore" controls-position="right" :min="0" :max="100" :precision="0" style="width: 100%" placeholder="0-100" />
</el-form-item>
</div>
<div class="mobile-quote-card__score-row">
<span class="mobile-quote-card__score-label">价格得分</span>
<span :class="row.priceScore >= 100 ? 'mobile-quote-card__score-best' : ''">{{ row.priceScore?.toFixed(2) ?? '-' }}</span>
</div>
<div class="mobile-quote-card__score-row">
<span class="mobile-quote-card__score-label">交付得分</span>
<span :class="row.deliveryScore >= 100 ? 'mobile-quote-card__score-best' : ''">{{ row.deliveryScore?.toFixed(2) ?? '-' }}</span>
</div>
<div class="mobile-quote-card__score-row mobile-quote-card__score-row--total">
<span class="mobile-quote-card__score-label">综合得分</span>
<el-tag v-if="row.totalScore != null" :type="row.isSelected ? 'success' : 'info'" size="small">{{ row.totalScore?.toFixed(2) }}</el-tag>
<span v-else style="color: #c0c4cc">-</span>
</div>
</div>
</div>
</div>
<!-- 合计 -->
<div class="mobile-quote-summary" v-if="formData.length > 0">
<div class="mobile-quote-summary__row">
<span>合计总价</span>
<span>{{ erpPriceInputFormatter(summaryTotalPrice) }}</span>
</div>
</div>
<!-- 添加按钮 -->
<div class="mobile-quote-add" v-if="!disabled">
<el-button @click="handleAdd" round>+ 添加供应商报价</el-button>
</div>
</div>
</el-form>
<!-- 快速新增供应商弹窗 -->
<QuickSupplierForm ref="quickSupplierFormRef" @success="onQuickSupplierSuccess" />
</template>
<script setup lang="ts">
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
import { SupplierEvaluationApi } from '@/api/erp/purchase/supplierEvaluation'
import { erpPriceInputFormatter, erpPriceMultiply, getSumValue } from '@/utils'
import { PurchaseInquiryQuoteVO } from '@/api/erp/purchase/inquiry'
import QuickSupplierForm from './QuickSupplierForm.vue'
const props = withDefaults(
defineProps<{
quotes: PurchaseInquiryQuoteVO[]
disabled?: boolean
requireCount?: number
}>(),
{
quotes: () => [],
disabled: false,
requireCount: 1
}
)
const formLoading = ref(false) // 表单的加载中
const formData = ref<PurchaseInquiryQuoteVO[]>([])
const formRules = reactive({
supplierId: [{ required: true, message: '供应商不能为空', trigger: 'blur' }],
unitPrice: [{ required: true, message: '单价不能为空', trigger: 'blur' }]
})
const formRef = ref<any>([]) // 表单 Ref
const supplierList = ref<SupplierVO[]>([]) // 供应商列表
/** 初始化设置报价项 */
watch(
() => props.quotes,
async (val) => {
formData.value = val as PurchaseInquiryQuoteVO[]
},
{ immediate: true }
)
/** 监听报价变化,计算总价和得分 */
watch(
() => formData.value,
(val) => {
if (!val || val.length === 0) {
return
}
// 计算总价
val.forEach((item: PurchaseInquiryQuoteVO) => {
if (item.unitPrice != null) {
item.totalPrice = erpPriceMultiply(item.unitPrice, props.requireCount) || 0
} else {
item.totalPrice = 0
}
})
// 自动计算得分
calculateScores(val)
},
{ deep: true }
)
/** 自动计算各项得分 */
const calculateScores = (quotes: PurchaseInquiryQuoteVO[]) => {
if (!quotes || quotes.length === 0) return
// 计算价格得分(价格越低得分越高)
const prices = quotes.filter(q => q.unitPrice != null && q.unitPrice > 0).map(q => q.unitPrice!)
if (prices.length > 0) {
const minPrice = Math.min(...prices)
quotes.forEach(quote => {
if (quote.unitPrice != null && quote.unitPrice > 0) {
quote.priceScore = (minPrice / quote.unitPrice) * 100
} else {
quote.priceScore = 0
}
})
}
// 计算交付得分(交货周期越短得分越高)
const cycles = quotes.filter(q => q.deliveryCycle != null && q.deliveryCycle > 0).map(q => q.deliveryCycle!)
if (cycles.length > 0) {
const minCycle = Math.min(...cycles)
quotes.forEach(quote => {
if (quote.deliveryCycle != null && quote.deliveryCycle > 0) {
quote.deliveryScore = (minCycle / quote.deliveryCycle) * 100
} else {
quote.deliveryScore = 0
}
})
} else {
quotes.forEach(quote => {
quote.deliveryScore = 0
})
}
// 计算综合得分默认权重价格40%、质量25%、交付20%、服务15%
quotes.forEach(quote => {
const priceScore = quote.priceScore || 0
const qualityScore = quote.qualityScore || 0
const deliveryScore = quote.deliveryScore || 0
const serviceScore = quote.serviceScore || 0
quote.totalScore = priceScore * 0.4 + qualityScore * 0.25 + deliveryScore * 0.2 + serviceScore * 0.15
})
}
/** 监听需求数量变化,重新计算总价 */
watch(
() => props.requireCount,
() => {
formData.value.forEach((item: PurchaseInquiryQuoteVO) => {
if (item.unitPrice != null) {
item.totalPrice = erpPriceMultiply(item.unitPrice, props.requireCount) || 0
}
})
}
)
/** 合计总价 */
const summaryTotalPrice = computed(() => {
return getSumValue(formData.value.map((item) => Number(item.totalPrice || 0)))
})
/** 新增按钮操作 */
const handleAdd = () => {
const row: PurchaseInquiryQuoteVO = {
id: undefined as any,
inquiryId: undefined as any,
supplierId: undefined as any,
supplierName: undefined as any,
contactName: undefined as any,
contactPhone: undefined as any,
quoteDate: Date.now() as any,
unitPrice: undefined as any,
totalPrice: 0,
paymentTerms: undefined as any,
deliveryCycle: undefined as any,
qualificationFile: undefined as any,
remark: undefined as any,
isSelected: false,
priceScore: undefined as any,
qualityScore: undefined as any,
deliveryScore: undefined as any,
serviceScore: undefined as any,
totalScore: undefined
}
formData.value.push(row)
}
/** 删除按钮操作 */
const handleDelete = (index: number) => {
formData.value.splice(index, 1)
}
/** 处理供应商变更 */
const onChangeSupplier = async (supplierId: number, row: PurchaseInquiryQuoteVO) => {
console.log('onChangeSupplier called with supplierId:', supplierId)
console.log('Current row before changes:', JSON.parse(JSON.stringify(row)))
const supplier = supplierList.value.find((item) => item.id === supplierId)
console.log('Found supplier:', supplier)
if (supplier) {
// 使用 Object.assign 确保响应式更新
Object.assign(row, {
supplierName: supplier.name,
contactName: supplier.contact || '',
contactPhone: supplier.mobile || supplier.telephone || ''
})
console.log('Basic supplier info assigned:', {
supplierName: row.supplierName,
contactName: row.contactName,
contactPhone: row.contactPhone
})
// 自动获取供应商历史评分
try {
console.log('Fetching supplier average score for supplierId:', supplierId)
const averageScore = await SupplierEvaluationApi.getSupplierAverageScore(supplierId)
debugger;
console.log('API response averageScore:', averageScore)
if (averageScore) {
const qualityScore = Math.round(averageScore.avgQualityScore || 0)
const serviceScore = Math.round(averageScore.avgServiceScore || 0)
const priceScore = Math.round(averageScore.avgPriceScore || 0)
const deliveryScore = Math.round(averageScore.avgDeliveryScore || 0)
console.log('Calculated scores:', { qualityScore, serviceScore })
// 使用 Object.assign 确保响应式更新
Object.assign(row, {
qualityScore: qualityScore,
serviceScore: serviceScore,
priceScore: priceScore,
deliveryScore: deliveryScore
})
console.log('Scores assigned to row:', {
qualityScore: row.qualityScore,
serviceScore: row.serviceScore
})
// 强制触发响应式更新
await nextTick()
console.log('After nextTick - row scores:', {
qualityScore: row.qualityScore,
serviceScore: row.serviceScore
})
// 注意priceScore 和 deliveryScore 会在输入单价和交货周期后自动计算
} else {
console.log('No averageScore data returned from API')
Object.assign(row, {
qualityScore: 0,
serviceScore: 0
})
}
} catch (error) {
console.error('获取供应商评分失败:', error)
// 如果获取评分失败,使用默认值
Object.assign(row, {
qualityScore: 0,
serviceScore: 0
})
}
console.log('Final row after all changes:', JSON.parse(JSON.stringify(row)))
} else {
console.log('No supplier found with id:', supplierId)
}
}
/** 处理选中变更 - 单选逻辑 */
const onSelectChange = (row: PurchaseInquiryQuoteVO, index: number) => {
if (row.isSelected) {
// 取消其他选中
formData.value.forEach((item, i) => {
if (i !== index) {
item.isSelected = false
}
})
}
}
/** 表格行样式 - 选中行变绿 */
const tableRowClassName = ({ row }: { row: PurchaseInquiryQuoteVO }) => {
return row.isSelected ? 'selected-row' : ''
}
/** 表单校验 */
const validate = () => {
return (formRef.value as any).validate()
}
defineExpose({ validate })
/** 快速新增供应商 */
const quickSupplierFormRef = ref()
const currentRowIndex = ref<number>(-1)
const openQuickSupplierForm = (index: number) => {
currentRowIndex.value = index
quickSupplierFormRef.value?.open()
}
/** 快速新增供应商成功回调 */
const onQuickSupplierSuccess = async (supplierId: number) => {
// 刷新供应商列表
supplierList.value = await SupplierApi.getSupplierSimpleList()
// 自动选中新增的供应商
if (currentRowIndex.value >= 0 && currentRowIndex.value < formData.value.length) {
const row = formData.value[currentRowIndex.value]
row.supplierId = supplierId
onChangeSupplier(supplierId, row)
}
}
/** 初始化 */
onMounted(async () => {
supplierList.value = await SupplierApi.getSupplierSimpleList()
})
</script>
<style lang="scss" scoped>
.mobile-quote-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobile-quote-card {
background: #fff;
border-radius: 10px;
border: 2px solid #f0f0f0;
overflow: hidden;
transition: border-color 0.2s;
&--selected {
border-color: #67c23a;
background: #f0f9eb;
}
&__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
}
&__header-left {
display: flex;
align-items: center;
gap: 6px;
}
&__index {
font-weight: 600;
font-size: 13px;
color: #909399;
}
&__name {
font-weight: 600;
font-size: 14px;
color: #303133;
}
&__body {
padding: 12px;
}
&__supplier-row {
display: flex;
gap: 8px;
width: 100%;
}
&__input-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
&__scores {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed #e4e7ed;
}
&__scores-title {
font-size: 13px;
font-weight: 600;
color: #606266;
margin-bottom: 8px;
}
&__score-row {
display: flex;
justify-content: space-between;
padding: 3px 0;
font-size: 13px;
color: #606266;
&--total {
padding-top: 6px;
margin-top: 4px;
border-top: 1px solid #f0f0f0;
font-weight: 600;
}
}
&__score-label {
color: #909399;
}
&__score-best {
color: #67c23a;
font-weight: 600;
}
}
.mobile-quote-summary {
background: #fff;
border-radius: 10px;
padding: 12px;
&__row {
display: flex;
justify-content: space-between;
font-size: 14px;
font-weight: 600;
color: #303133;
}
}
.mobile-quote-add {
display: flex;
justify-content: center;
padding: 8px 0;
}
</style>