first commit
This commit is contained in:
511
src/views/erp/purchase/inquiry/components/InquiryQuoteForm.vue
Normal file
511
src/views/erp/purchase/inquiry/components/InquiryQuoteForm.vue
Normal file
@@ -0,0 +1,511 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user