Files
mom-web/src/views/erp/purchase/inquiry/components/InquiryQuoteForm.vue
2026-03-05 16:52:12 +08:00

512 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>