first commit
This commit is contained in:
414
src/views/erp/purchase/inquiry/AutoCompareDialog.vue
Normal file
414
src/views/erp/purchase/inquiry/AutoCompareDialog.vue
Normal file
@@ -0,0 +1,414 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="dialogVisible"
|
||||
title="自动比价"
|
||||
direction="rtl"
|
||||
size="100%"
|
||||
:close-on-press-escape="true"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<div class="mobile-compare" v-loading="loading">
|
||||
<!-- 比价单信息 -->
|
||||
<div class="mobile-compare__section">
|
||||
<div class="mobile-compare__section-title">比价单信息</div>
|
||||
<div class="mobile-compare__info-row">
|
||||
<span class="mobile-compare__info-label">比价单号</span>
|
||||
<span>{{ inquiryData?.no }}</span>
|
||||
</div>
|
||||
<div class="mobile-compare__info-row">
|
||||
<span class="mobile-compare__info-label">产品名称</span>
|
||||
<span>{{ inquiryData?.productName }}</span>
|
||||
</div>
|
||||
<div class="mobile-compare__info-row">
|
||||
<span class="mobile-compare__info-label">需求数量</span>
|
||||
<span>{{ inquiryData?.requireCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 权重配置 -->
|
||||
<div class="mobile-compare__section">
|
||||
<div class="mobile-compare__section-title">
|
||||
评分权重配置
|
||||
<el-tag size="small" :type="totalWeight === 100 ? 'success' : 'danger'" style="margin-left: 8px">
|
||||
合计: {{ totalWeight }}%
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-form :model="weightConfig" label-position="top">
|
||||
<div class="mobile-compare__weight-grid">
|
||||
<el-form-item label="价格权重">
|
||||
<el-input-number v-model="weightConfig.priceWeight" :min="0" :max="100" :precision="0" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="质量权重">
|
||||
<el-input-number v-model="weightConfig.qualityWeight" :min="0" :max="100" :precision="0" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="交付权重">
|
||||
<el-input-number v-model="weightConfig.deliveryWeight" :min="0" :max="100" :precision="0" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务权重">
|
||||
<el-input-number v-model="weightConfig.serviceWeight" :min="0" :max="100" :precision="0" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 供应商报价评分卡片 -->
|
||||
<div class="mobile-compare__section">
|
||||
<div class="mobile-compare__section-title">供应商报价评分</div>
|
||||
<div class="mobile-compare__card-list">
|
||||
<div
|
||||
v-for="(row, idx) in quoteList"
|
||||
:key="idx"
|
||||
class="mobile-compare__card"
|
||||
:class="{ 'mobile-compare__card--best': row.isSelected }"
|
||||
>
|
||||
<div class="mobile-compare__card-header">
|
||||
<span class="mobile-compare__card-name">{{ row.supplierName || '-' }}</span>
|
||||
<el-tag v-if="row.isSelected" type="success" size="small">推荐</el-tag>
|
||||
</div>
|
||||
<div class="mobile-compare__card-info">
|
||||
<div class="mobile-compare__info-row">
|
||||
<span class="mobile-compare__info-label">单价</span>
|
||||
<span>{{ row.unitPrice?.toFixed(2) ?? '-' }}</span>
|
||||
</div>
|
||||
<div class="mobile-compare__info-row">
|
||||
<span class="mobile-compare__info-label">总价</span>
|
||||
<span>{{ row.totalPrice?.toFixed(2) ?? '-' }}</span>
|
||||
</div>
|
||||
<div class="mobile-compare__info-row">
|
||||
<span class="mobile-compare__info-label">交货周期</span>
|
||||
<span>{{ row.deliveryCycle != null ? row.deliveryCycle + '天' : '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-compare__card-scores">
|
||||
<div class="mobile-compare__score-input-group">
|
||||
<div class="mobile-compare__score-input">
|
||||
<span class="mobile-compare__score-input-label">质量评分</span>
|
||||
<el-input-number v-model="row.qualityScore" :min="0" :max="100" :precision="0" size="small" controls-position="right" style="width: 100%" />
|
||||
</div>
|
||||
<div class="mobile-compare__score-input">
|
||||
<span class="mobile-compare__score-input-label">服务评分</span>
|
||||
<el-input-number v-model="row.serviceScore" :min="0" :max="100" :precision="0" size="small" controls-position="right" style="width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-compare__score-results">
|
||||
<div class="mobile-compare__score-item">
|
||||
<span class="mobile-compare__score-item-label">价格</span>
|
||||
<span>{{ row.priceScore?.toFixed(1) ?? '-' }}</span>
|
||||
</div>
|
||||
<div class="mobile-compare__score-item">
|
||||
<span class="mobile-compare__score-item-label">交付</span>
|
||||
<span>{{ row.deliveryScore?.toFixed(1) ?? '-' }}</span>
|
||||
</div>
|
||||
<div class="mobile-compare__score-item mobile-compare__score-item--total">
|
||||
<span class="mobile-compare__score-item-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>
|
||||
<div class="mobile-compare__tips">
|
||||
<p>* 价格得分:价格越低得分越高(最低价得100分)</p>
|
||||
<p>* 交付得分:周期越短得分越高(最短周期得100分)</p>
|
||||
<p>* 质量/服务评分:手动输入(0-100),未填按0分计</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 比价结果 -->
|
||||
<el-alert
|
||||
v-if="bestQuote"
|
||||
:title="`推荐: ${bestQuote.supplierName},综合得分: ${bestQuote.totalScore?.toFixed(2)}`"
|
||||
type="success"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleCompare" type="primary">开始比价</el-button>
|
||||
<el-button @click="handleSave" type="success" :disabled="!bestQuote">保存结果</el-button>
|
||||
<el-button @click="dialogVisible = false">关 闭</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PurchaseInquiryApi, PurchaseInquiryVO, PurchaseInquiryQuoteVO } from '@/api/erp/purchase/inquiry'
|
||||
|
||||
defineOptions({ name: 'AutoCompareDialog' })
|
||||
|
||||
const message = useMessage()
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const inquiryData = ref<PurchaseInquiryVO | null>(null)
|
||||
const quoteList = ref<PurchaseInquiryQuoteVO[]>([])
|
||||
|
||||
/** 权重配置 */
|
||||
const weightConfig = reactive({
|
||||
priceWeight: 40,
|
||||
qualityWeight: 25,
|
||||
deliveryWeight: 20,
|
||||
serviceWeight: 15
|
||||
})
|
||||
|
||||
/** 计算权重合计 */
|
||||
const totalWeight = computed(() => {
|
||||
return weightConfig.priceWeight + weightConfig.qualityWeight +
|
||||
weightConfig.deliveryWeight + weightConfig.serviceWeight
|
||||
})
|
||||
|
||||
/** 最佳报价供应商 */
|
||||
const bestQuote = ref<PurchaseInquiryQuoteVO | null>(null)
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (id: number) => {
|
||||
dialogVisible.value = true
|
||||
loading.value = true
|
||||
bestQuote.value = null
|
||||
try {
|
||||
inquiryData.value = await PurchaseInquiryApi.getPurchaseInquiry(id)
|
||||
quoteList.value = inquiryData.value?.quotes || []
|
||||
// 自动计算各项得分
|
||||
calculateScores()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
defineExpose({ open })
|
||||
|
||||
/** 自动计算各项得分 */
|
||||
const calculateScores = () => {
|
||||
if (!quoteList.value || quoteList.value.length === 0) return
|
||||
|
||||
// 计算总价
|
||||
const requireCount = inquiryData.value?.requireCount || 1
|
||||
quoteList.value.forEach(quote => {
|
||||
if (quote.unitPrice != null && quote.unitPrice > 0) {
|
||||
quote.totalPrice = quote.unitPrice * requireCount
|
||||
} else {
|
||||
quote.totalPrice = 0
|
||||
}
|
||||
})
|
||||
|
||||
// 计算价格得分(价格越低得分越高)
|
||||
const prices = quoteList.value.filter(q => q.unitPrice != null && q.unitPrice > 0).map(q => q.unitPrice!)
|
||||
if (prices.length > 0) {
|
||||
const minPrice = Math.min(...prices)
|
||||
quoteList.value.forEach(quote => {
|
||||
if (quote.unitPrice != null && quote.unitPrice > 0) {
|
||||
quote.priceScore = (minPrice / quote.unitPrice) * 100
|
||||
} else {
|
||||
quote.priceScore = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 计算交付得分(交货周期越短得分越高)
|
||||
const cycles = quoteList.value.filter(q => q.deliveryCycle != null && q.deliveryCycle > 0).map(q => q.deliveryCycle!)
|
||||
if (cycles.length > 0) {
|
||||
const minCycle = Math.min(...cycles)
|
||||
quoteList.value.forEach(quote => {
|
||||
if (quote.deliveryCycle != null && quote.deliveryCycle > 0) {
|
||||
quote.deliveryScore = (minCycle / quote.deliveryCycle) * 100
|
||||
} else {
|
||||
quote.deliveryScore = 0
|
||||
}
|
||||
})
|
||||
} else {
|
||||
quoteList.value.forEach(quote => {
|
||||
quote.deliveryScore = 0
|
||||
})
|
||||
}
|
||||
|
||||
// 计算综合得分
|
||||
quoteList.value.forEach(quote => {
|
||||
quote.totalScore =
|
||||
(quote.priceScore || 0) * (weightConfig.priceWeight / 100) +
|
||||
(quote.qualityScore || 0) * (weightConfig.qualityWeight / 100) +
|
||||
(quote.deliveryScore || 0) * (weightConfig.deliveryWeight / 100) +
|
||||
(quote.serviceScore || 0) * (weightConfig.serviceWeight / 100)
|
||||
})
|
||||
}
|
||||
|
||||
/** 开始比价 */
|
||||
const handleCompare = () => {
|
||||
// 校验权重合计
|
||||
if (totalWeight.value !== 100) {
|
||||
message.warning('权重合计必须等于100%')
|
||||
return
|
||||
}
|
||||
|
||||
// 校验是否有报价
|
||||
if (quoteList.value.length < 2) {
|
||||
message.warning('至少需要2个供应商报价才能进行比价')
|
||||
return
|
||||
}
|
||||
|
||||
// 校验是否有单价
|
||||
const noPriceQuotes = quoteList.value.filter(q => q.unitPrice == null || q.unitPrice <= 0)
|
||||
if (noPriceQuotes.length > 0) {
|
||||
message.warning('所有供应商必须填写单价')
|
||||
return
|
||||
}
|
||||
|
||||
// 使用已有的价格得分和交付得分(在明细中已自动计算),根据自定义权重重新计算综合得分
|
||||
quoteList.value.forEach(quote => {
|
||||
quote.totalScore =
|
||||
(quote.priceScore || 0) * (weightConfig.priceWeight / 100) +
|
||||
(quote.qualityScore || 0) * (weightConfig.qualityWeight / 100) +
|
||||
(quote.deliveryScore || 0) * (weightConfig.deliveryWeight / 100) +
|
||||
(quote.serviceScore || 0) * (weightConfig.serviceWeight / 100)
|
||||
})
|
||||
|
||||
// 找出得分最高的供应商
|
||||
const sortedQuotes = [...quoteList.value].sort((a, b) =>
|
||||
(b.totalScore || 0) - (a.totalScore || 0)
|
||||
)
|
||||
bestQuote.value = sortedQuotes[0]
|
||||
|
||||
// 标记选中状态
|
||||
quoteList.value.forEach(quote => {
|
||||
quote.isSelected = quote === bestQuote.value
|
||||
})
|
||||
|
||||
message.success(`比价完成,推荐供应商: ${bestQuote.value.supplierName}`)
|
||||
}
|
||||
|
||||
/** 保存比价结果 */
|
||||
const handleSave = async () => {
|
||||
if (!inquiryData.value || !bestQuote.value) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 更新比价单数据
|
||||
const data = {
|
||||
...inquiryData.value,
|
||||
quotes: quoteList.value
|
||||
}
|
||||
await PurchaseInquiryApi.updatePurchaseInquiry(data)
|
||||
message.success('保存成功')
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mobile-compare {
|
||||
padding: 12px;
|
||||
&__section {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
&__section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
&__info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
&__info-label {
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
&__weight-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
&__card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
&__card {
|
||||
border: 2px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s;
|
||||
&--best {
|
||||
border-color: #67c23a;
|
||||
background: #f0f9eb;
|
||||
}
|
||||
}
|
||||
&__card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
&__card-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
}
|
||||
&__card-info {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
&__card-scores {
|
||||
padding: 8px 12px;
|
||||
border-top: 1px dashed #e4e7ed;
|
||||
}
|
||||
&__score-input-group {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
&__score-input-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
&__score-results {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
&__score-item {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
&--total {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
&__score-item-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
&__tips {
|
||||
margin-top: 10px;
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
p { margin: 0; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
322
src/views/erp/purchase/inquiry/InquiryForm.vue
Normal file
322
src/views/erp/purchase/inquiry/InquiryForm.vue
Normal file
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
direction="rtl"
|
||||
size="100%"
|
||||
:close-on-press-escape="true"
|
||||
:destroy-on-close="true"
|
||||
:append-to-body="true"
|
||||
class="mobile-form-drawer"
|
||||
>
|
||||
<div class="mobile-form" v-loading="formLoading">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<div class="mobile-form__section">
|
||||
<div class="mobile-form__section-title">基本信息</div>
|
||||
<el-form-item label="比价单号" prop="no">
|
||||
<el-input disabled v-model="formData.no" placeholder="保存时自动生成" />
|
||||
</el-form-item>
|
||||
<el-form-item label="询价时间" prop="inquiryTime">
|
||||
<el-date-picker
|
||||
v-model="formData.inquiryTime"
|
||||
type="date"
|
||||
value-format="x"
|
||||
placeholder="选择询价时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="截止日期" prop="deadline">
|
||||
<el-date-picker
|
||||
v-model="formData.deadline"
|
||||
type="date"
|
||||
value-format="x"
|
||||
placeholder="选择截止日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品" prop="productId">
|
||||
<el-select
|
||||
v-model="formData.productId"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择产品"
|
||||
style="width: 100%"
|
||||
@change="onChangeProduct"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in productList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="mobile-form__input-group">
|
||||
<el-form-item label="需求数量" prop="requireCount">
|
||||
<el-input-number
|
||||
v-model="formData.requireCount"
|
||||
controls-position="right"
|
||||
:min="0.0001"
|
||||
:precision="4"
|
||||
placeholder="需求数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="预算金额" prop="budgetPrice">
|
||||
<el-input-number
|
||||
v-model="formData.budgetPrice"
|
||||
controls-position="right"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
placeholder="预算金额"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input type="textarea" v-model="formData.remark" :rows="2" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 供应商报价 -->
|
||||
<div class="mobile-form__section">
|
||||
<div class="mobile-form__section-title">供应商报价</div>
|
||||
<InquiryQuoteForm ref="quoteFormRef" :quotes="formData.quotes" :disabled="disabled" :require-count="formData.requireCount" />
|
||||
</div>
|
||||
|
||||
<!-- 比价汇总 -->
|
||||
<div class="mobile-form__section" v-if="formData.quotes && formData.quotes.length > 0">
|
||||
<div class="mobile-form__section-title">比价汇总</div>
|
||||
<div class="mobile-form__info-row">
|
||||
<span class="mobile-form__info-label">报价数量</span>
|
||||
<span class="mobile-form__info-value">{{ formData.quotes.length }}</span>
|
||||
</div>
|
||||
<div class="mobile-form__info-row">
|
||||
<span class="mobile-form__info-label">最低报价</span>
|
||||
<span class="mobile-form__info-value" style="color: #67c23a">{{ erpPriceInputFormatter(minQuotePrice) }}</span>
|
||||
</div>
|
||||
<div class="mobile-form__info-row">
|
||||
<span class="mobile-form__info-label">最高报价</span>
|
||||
<span class="mobile-form__info-value" style="color: #e6a23c">{{ erpPriceInputFormatter(maxQuotePrice) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<div class="mobile-form__footer">
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled">确 定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { nextTick } from 'vue'
|
||||
import { PurchaseInquiryApi, PurchaseInquiryVO } from '@/api/erp/purchase/inquiry'
|
||||
import InquiryQuoteForm from './components/InquiryQuoteForm.vue'
|
||||
import { ProductApi, ProductVO } from '@/api/erp/product/product'
|
||||
import { erpPriceInputFormatter } from '@/utils'
|
||||
|
||||
/** ERP 采购比价表单 */
|
||||
defineOptions({ name: 'InquiryForm' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
// 类型声明
|
||||
interface InquiryFormModel {
|
||||
id?: number
|
||||
productId?: number
|
||||
requireCount: number
|
||||
budgetPrice: number
|
||||
inquiryTime?: number
|
||||
deadline?: number
|
||||
remark?: string
|
||||
quotes: PurchaseInquiryQuoteVO[]
|
||||
no?: string
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中
|
||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改;detail - 详情
|
||||
const formData = ref<InquiryFormModel>({
|
||||
id: undefined,
|
||||
productId: undefined,
|
||||
requireCount: 1,
|
||||
budgetPrice: 0,
|
||||
inquiryTime: Date.now(),
|
||||
deadline: undefined,
|
||||
remark: undefined,
|
||||
quotes: [],
|
||||
no: undefined
|
||||
})
|
||||
const formRules = reactive({
|
||||
productId: [{ required: true, message: '产品不能为空', trigger: 'blur' }],
|
||||
requireCount: [{ required: true, message: '需求数量不能为空', trigger: 'blur' }],
|
||||
inquiryTime: [{ required: true, message: '询价时间不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const disabled = computed(() => formType.value === 'detail')
|
||||
const formRef = ref() // 表单 Ref
|
||||
const productList = ref<ProductVO[]>([]) // 产品列表
|
||||
|
||||
const quoteFormRef = ref()
|
||||
|
||||
/** 计算最低报价 */
|
||||
const minQuotePrice = computed(() => {
|
||||
if (!formData.value.quotes || formData.value.quotes.length === 0) {
|
||||
return 0
|
||||
}
|
||||
const prices = formData.value.quotes
|
||||
.filter((q) => q.unitPrice != null && q.unitPrice > 0)
|
||||
.map((q) => q.unitPrice)
|
||||
return prices.length > 0 ? Math.min(...prices) : 0
|
||||
})
|
||||
|
||||
/** 计算最高报价 */
|
||||
const maxQuotePrice = computed(() => {
|
||||
if (!formData.value.quotes || formData.value.quotes.length === 0) {
|
||||
return 0
|
||||
}
|
||||
const prices = formData.value.quotes
|
||||
.filter((q) => q.unitPrice != null && q.unitPrice > 0)
|
||||
.map((q) => q.unitPrice)
|
||||
return prices.length > 0 ? Math.max(...prices) : 0
|
||||
})
|
||||
|
||||
/** 处理产品变更 */
|
||||
const onChangeProduct = (productId: number) => {
|
||||
const product = productList.value.find((item) => item.id === productId)
|
||||
if (product) {
|
||||
// 可以根据产品设置默认值
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = t('action.' + type)
|
||||
formType.value = type
|
||||
await nextTick()
|
||||
resetForm()
|
||||
// 修改时,设置数据
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
formData.value = (await PurchaseInquiryApi.getPurchaseInquiry(id)) as unknown as InquiryFormModel
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
// 加载产品列表
|
||||
productList.value = await ProductApi.getProductSimpleList()
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
await formRef.value.validate()
|
||||
await quoteFormRef.value.validate()
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = formData.value as unknown as PurchaseInquiryVO
|
||||
if (formType.value === 'create') {
|
||||
await PurchaseInquiryApi.createPurchaseInquiry(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await PurchaseInquiryApi.updatePurchaseInquiry(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
productId: undefined,
|
||||
requireCount: 1,
|
||||
budgetPrice: 0,
|
||||
inquiryTime: Date.now(),
|
||||
deadline: undefined,
|
||||
remark: undefined,
|
||||
quotes: []
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mobile-form {
|
||||
padding: 12px;
|
||||
}
|
||||
.mobile-form__section {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.mobile-form__section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.mobile-form__input-group {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
.mobile-form__info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.mobile-form__info-label {
|
||||
color: #909399;
|
||||
}
|
||||
.mobile-form__info-value {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.mobile-form__footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
padding: 12px 16px;
|
||||
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
z-index: 10;
|
||||
margin: 0 -12px -12px;
|
||||
|
||||
.el-button {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
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>
|
||||
115
src/views/erp/purchase/inquiry/components/QuickSupplierForm.vue
Normal file
115
src/views/erp/purchase/inquiry/components/QuickSupplierForm.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="dialogVisible"
|
||||
title="快速新增供应商"
|
||||
direction="rtl"
|
||||
size="100%"
|
||||
:close-on-press-escape="true"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<div class="mobile-quick-supplier" v-loading="formLoading">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
>
|
||||
<el-form-item label="供应商名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入供应商名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人" prop="contact">
|
||||
<el-input v-model="formData.contact" placeholder="请输入联系人" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号码" prop="mobile">
|
||||
<el-input v-model="formData.mobile" placeholder="请输入手机号码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="telephone">
|
||||
<el-input v-model="formData.telephone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button @click="submitForm" type="primary" :loading="formLoading">确 定</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
defineOptions({ name: 'QuickSupplierForm' })
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const formLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const formData = ref({
|
||||
name: undefined as string | undefined,
|
||||
contact: undefined as string | undefined,
|
||||
mobile: undefined as string | undefined,
|
||||
telephone: undefined as string | undefined,
|
||||
remark: undefined as string | undefined,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
sort: 0,
|
||||
type: 1 // 供应商类型
|
||||
})
|
||||
|
||||
const formRules = reactive({
|
||||
name: [{ required: true, message: '供应商名称不能为空', trigger: 'blur' }],
|
||||
mobile: [{ pattern: /^1[3-9]\d{9}$/, message: '手机号码格式不正确', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = () => {
|
||||
dialogVisible.value = true
|
||||
resetForm()
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits<{
|
||||
success: [supplierId: number]
|
||||
}>()
|
||||
|
||||
const submitForm = async () => {
|
||||
await formRef.value?.validate()
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = formData.value as unknown as SupplierVO
|
||||
const supplierId = await SupplierApi.createSupplier(data)
|
||||
message.success('新增供应商成功')
|
||||
dialogVisible.value = false
|
||||
emit('success', supplierId)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
name: undefined,
|
||||
contact: undefined,
|
||||
mobile: undefined,
|
||||
telephone: undefined,
|
||||
remark: undefined,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
sort: 0,
|
||||
type: 1
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mobile-quick-supplier {
|
||||
padding: 12px;
|
||||
}
|
||||
</style>
|
||||
269
src/views/erp/purchase/inquiry/index.vue
Normal file
269
src/views/erp/purchase/inquiry/index.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div class="mobile-purchase-inquiry">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="mobile-header">
|
||||
<div class="mobile-header__search">
|
||||
<el-input v-model="queryParams.no" placeholder="搜索比价单号" clearable @keyup.enter="handleQuery" :prefix-icon="Search" />
|
||||
</div>
|
||||
<div class="mobile-header__actions">
|
||||
<el-button :icon="Filter" circle @click="filterVisible = true" />
|
||||
<el-button type="primary" :icon="Plus" circle @click="openForm('create')" v-hasPermi="['erp:purchase-inquiry:create']" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片列表 -->
|
||||
<div class="mobile-list" v-loading="loading">
|
||||
<div v-if="list.length === 0 && !loading" class="mobile-empty">
|
||||
<el-empty description="暂无比价记录" />
|
||||
</div>
|
||||
<div v-for="item in list" :key="item.id" class="mobile-card" @click="handleCardClick(item)">
|
||||
<div class="mobile-card__header">
|
||||
<span class="mobile-card__no">{{ item.no }}</span>
|
||||
<el-tag v-if="item.status === 10" type="info" size="small">待询价</el-tag>
|
||||
<el-tag v-else-if="item.status === 20" type="warning" size="small">询价中</el-tag>
|
||||
<el-tag v-else-if="item.status === 30" type="success" size="small">已完成</el-tag>
|
||||
</div>
|
||||
<div class="mobile-card__body">
|
||||
<div class="mobile-card__row">
|
||||
<span class="mobile-card__label">产品</span>
|
||||
<span class="mobile-card__value">{{ item.productName || '-' }}</span>
|
||||
</div>
|
||||
<div class="mobile-card__row">
|
||||
<span class="mobile-card__label">询价时间</span>
|
||||
<span class="mobile-card__value">{{ formatDate2(item.inquiryTime) }}</span>
|
||||
</div>
|
||||
<div class="mobile-card__row">
|
||||
<span class="mobile-card__label">截止日期</span>
|
||||
<span class="mobile-card__value">{{ formatDate2(item.deadline) }}</span>
|
||||
</div>
|
||||
<div class="mobile-card__row">
|
||||
<span class="mobile-card__label">创建人</span>
|
||||
<span class="mobile-card__value">{{ item.creatorName || '-' }}</span>
|
||||
</div>
|
||||
<div class="mobile-card__nums">
|
||||
<div class="mobile-card__num-item">
|
||||
<div class="mobile-card__num-val">{{ erpCountInputFormatter(item.requireCount) }}</div>
|
||||
<div class="mobile-card__num-label">需求数量</div>
|
||||
</div>
|
||||
<div class="mobile-card__num-item">
|
||||
<div class="mobile-card__num-val mobile-card__num-val--price">¥{{ erpPriceInputFormatter(item.budgetPrice) }}</div>
|
||||
<div class="mobile-card__num-label">预算金额</div>
|
||||
</div>
|
||||
<div class="mobile-card__num-item">
|
||||
<div class="mobile-card__num-val">{{ item.quoteCount || 0 }}</div>
|
||||
<div class="mobile-card__num-label">报价数</div>
|
||||
</div>
|
||||
<div class="mobile-card__num-item">
|
||||
<div class="mobile-card__num-val" style="color:#67c23a">¥{{ erpPriceInputFormatter(item.minQuotePrice) }}</div>
|
||||
<div class="mobile-card__num-label">最低报价</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-card__footer">
|
||||
<el-button size="small" @click.stop="openForm('detail', item.id)" v-hasPermi="['erp:purchase-inquiry:query']">详情</el-button>
|
||||
<el-button size="small" type="primary" @click.stop="openForm('update', item.id)" v-hasPermi="['erp:purchase-inquiry:update']" :disabled="item.status === 30">编辑</el-button>
|
||||
<el-button size="small" type="primary" @click.stop="handleUpdateStatus(item.id, 20)" v-hasPermi="['erp:purchase-inquiry:update-status']" v-if="item.status === 10">询价</el-button>
|
||||
<el-button size="small" type="warning" @click.stop="openAutoCompare(item.id)" v-hasPermi="['erp:purchase-inquiry:update']" v-if="item.status === 20 && item.quoteCount >= 2">比价</el-button>
|
||||
<el-button size="small" type="success" @click.stop="handleUpdateStatus(item.id, 30)" v-hasPermi="['erp:purchase-inquiry:update-status']" v-if="item.status === 20">完成</el-button>
|
||||
<el-button size="small" type="danger" @click.stop="handleDelete([item.id])" v-hasPermi="['erp:purchase-inquiry:delete']">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="mobile-pagination" v-if="total > 0">
|
||||
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :page-sizes="[10, 20]" layout="total, prev, pager, next" :pager-count="5" @pagination="getList" />
|
||||
</div>
|
||||
|
||||
<!-- 筛选抽屉 -->
|
||||
<el-drawer v-model="filterVisible" title="筛选条件" direction="btt" size="60%">
|
||||
<el-form :model="queryParams" ref="queryFormRef" label-position="top">
|
||||
<el-form-item label="产品" prop="productId">
|
||||
<el-select v-model="queryParams.productId" clearable filterable placeholder="请选择产品" style="width:100%">
|
||||
<el-option v-for="item in productList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="询价时间" prop="inquiryTime">
|
||||
<el-date-picker v-model="queryParams.inquiryTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始" end-placeholder="结束" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width:100%">
|
||||
<el-option label="待询价" :value="10" />
|
||||
<el-option label="询价中" :value="20" />
|
||||
<el-option label="已完成" :value="30" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建人" prop="creator">
|
||||
<el-select v-model="queryParams.creator" clearable filterable placeholder="请选择创建人" style="width:100%">
|
||||
<el-option v-for="item in userList" :key="item.id" :label="item.nickname" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
<el-button type="primary" @click="handleFilterConfirm">确认筛选</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<InquiryForm ref="formRef" @success="getList" />
|
||||
<!-- 自动比价弹窗 -->
|
||||
<AutoCompareDialog ref="autoCompareRef" @success="getList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Search, Filter, Plus } from '@element-plus/icons-vue'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import { PurchaseInquiryApi, PurchaseInquiryVO } from '@/api/erp/purchase/inquiry'
|
||||
import InquiryForm from './InquiryForm.vue'
|
||||
import AutoCompareDialog from './AutoCompareDialog.vue'
|
||||
import { ProductApi, ProductVO } from '@/api/erp/product/product'
|
||||
import { UserVO } from '@/api/system/user'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import { erpCountInputFormatter, erpPriceInputFormatter } from '@/utils'
|
||||
|
||||
/** ERP 采购比价列表 */
|
||||
defineOptions({ name: 'ErpPurchaseInquiry' })
|
||||
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<PurchaseInquiryVO[]>([])
|
||||
const total = ref(0)
|
||||
const filterVisible = ref(false)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
no: undefined,
|
||||
productId: undefined,
|
||||
inquiryTime: [],
|
||||
status: undefined,
|
||||
creator: undefined
|
||||
})
|
||||
const queryFormRef = ref()
|
||||
const exportLoading = ref(false)
|
||||
const productList = ref<ProductVO[]>([])
|
||||
const userList = ref<UserVO[]>([])
|
||||
|
||||
const formatDate2 = (date: any) => {
|
||||
if (!date) return '-'
|
||||
return formatDate(new Date(date), 'YYYY-MM-DD')
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await PurchaseInquiryApi.getPurchaseInquiryPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const handleFilterConfirm = () => {
|
||||
filterVisible.value = false
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
const autoCompareRef = ref()
|
||||
const openAutoCompare = (id: number) => {
|
||||
autoCompareRef.value.open(id)
|
||||
}
|
||||
|
||||
const handleCardClick = (row: PurchaseInquiryVO) => {
|
||||
if (row.status === 30) {
|
||||
openForm('detail', row.id)
|
||||
} else {
|
||||
openForm('update', row.id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (ids: number[]) => {
|
||||
try {
|
||||
await message.delConfirm()
|
||||
await PurchaseInquiryApi.deletePurchaseInquiry(ids)
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleUpdateStatus = async (id: number, status: number) => {
|
||||
try {
|
||||
const statusText = status === 20 ? '开始询价' : '完成'
|
||||
await message.confirm(`确定${statusText}该比价单吗?`)
|
||||
await PurchaseInquiryApi.updatePurchaseInquiryStatus(id, status)
|
||||
message.success(`${statusText}成功`)
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
await message.exportConfirm()
|
||||
exportLoading.value = true
|
||||
const data = await PurchaseInquiryApi.exportPurchaseInquiry(queryParams)
|
||||
download.excel(data, '采购比价.xls')
|
||||
} catch {
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getList()
|
||||
productList.value = await ProductApi.getProductSimpleList()
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mobile-purchase-inquiry {
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.mobile-header {
|
||||
display: flex; gap: 8px; align-items: center; margin-bottom: 12px;
|
||||
&__search { flex: 1; }
|
||||
&__actions { display: flex; gap: 4px; flex-shrink: 0; }
|
||||
}
|
||||
.mobile-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
.mobile-empty { padding: 40px 0; }
|
||||
.mobile-card {
|
||||
background: #fff; border-radius: 10px; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
&__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
||||
&__no { font-weight: 600; font-size: 15px; color: #303133; }
|
||||
&__body { font-size: 13px; }
|
||||
&__row { display: flex; justify-content: space-between; padding: 3px 0; }
|
||||
&__label { color: #909399; flex-shrink: 0; margin-right: 12px; }
|
||||
&__value { color: #606266; text-align: right; }
|
||||
&__nums { display: flex; justify-content: space-around; margin-top: 10px; padding: 10px 0; border-top: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0; }
|
||||
&__num-item { text-align: center; }
|
||||
&__num-val { font-size: 15px; font-weight: 600; color: #303133; &--price { color: #e6a23c; } }
|
||||
&__num-label { font-size: 11px; color: #909399; margin-top: 2px; }
|
||||
&__footer { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
||||
}
|
||||
.mobile-pagination {
|
||||
margin-top: 12px; display: flex; justify-content: center;
|
||||
:deep(.el-pagination) { flex-wrap: wrap; justify-content: center; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user