512 lines
17 KiB
Vue
512 lines
17 KiB
Vue
<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>
|