389 lines
12 KiB
Vue
389 lines
12 KiB
Vue
<template>
|
|
<el-dialog
|
|
:title="dialogTitle"
|
|
v-model="dialogVisible"
|
|
width="calc(100vw - 48px)"
|
|
align-center
|
|
class="dialog-form-layout quote-selector-dialog"
|
|
modal-class="order-module-dialog-mask"
|
|
:close-on-click-modal="false"
|
|
append-to-body
|
|
>
|
|
<!-- 提示信息 -->
|
|
<el-alert v-if="props.customerId" :title="`已筛选客户的报价单`" type="info" :closable="false" class="mb-3" show-icon />
|
|
|
|
<!-- 报价单列表 -->
|
|
<div class="panel dialog-list-panel">
|
|
<div class="list-toolbar">
|
|
<div class="list-toolbar-row quote-selector-toolbar-row">
|
|
<ListQuickSearchInput
|
|
v-model="quickSearchKeyword"
|
|
placeholder="报价编号/客户名称/拼音首字母"
|
|
@search="handleQuickSearch"
|
|
@clear="handleQuickSearchClear"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="list-body list-wrap dialog-list-body-only">
|
|
<el-table
|
|
ref="tableRef"
|
|
v-loading="loading"
|
|
:data="quoteList"
|
|
class="list-table quote-selector-table"
|
|
stripe
|
|
highlight-current-row
|
|
row-key="id"
|
|
@current-change="handleCurrentChange"
|
|
max-height="calc(100vh - 280px)"
|
|
>
|
|
<el-table-column label="" min-width="48" align="center" header-align="center">
|
|
<template #default="{ row }">
|
|
<el-radio v-model="selectedId" :label="String(row.id)" @click.stop @change="handleRadioChange(row)">
|
|
<span class="sr-only">选择</span>
|
|
</el-radio>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column type="index" label="序号" width="60" align="center" header-align="center" />
|
|
<el-table-column label="报价单编号" prop="quoteNo" min-width="200" align="center" header-align="center" :show-overflow-tooltip="true" />
|
|
<el-table-column label="状态" prop="status" min-width="100" align="center" header-align="center">
|
|
<template #default="scope">
|
|
<DictTag dict-type="crm_quote_status" :value="scope.row.status" />
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="客户名称" prop="customerName" min-width="220" align="center" header-align="center" :show-overflow-tooltip="true" />
|
|
<el-table-column label="产品信息" min-width="280" align="left" header-align="center">
|
|
<template #default="{ row }">
|
|
<div class="product-info-cell">
|
|
<div v-for="(line, idx) in formatQuoteProductLines(row)" :key="idx" class="product-info-line">
|
|
{{ line }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="合同关联情况" prop="contractLinkedCount" min-width="120" align="center" header-align="center">
|
|
<template #default="scope">
|
|
<span>{{ scope.row.contractLinkedCount != null ? `已关联${scope.row.contractLinkedCount}个合同` : '未关联' }}</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="报价金额(元)" prop="finalAmount" min-width="130" align="right" header-align="center">
|
|
<template #default="scope">
|
|
<span class="money-display">{{
|
|
scope.row.finalAmount != null
|
|
? `¥${Number(scope.row.finalAmount).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
|
: '-'
|
|
}}</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="报价日期" prop="quoteDate" min-width="120" align="center" header-align="center" />
|
|
<el-table-column label="有效期" prop="expireDate" min-width="120" align="center" header-align="center" />
|
|
<el-table-column label="负责人" prop="ownerUserName" min-width="100" align="center" header-align="center" :show-overflow-tooltip="true" />
|
|
</el-table>
|
|
|
|
<ListPagination
|
|
v-show="total > 0"
|
|
:total="total"
|
|
v-model:page="queryParams.pageNum"
|
|
v-model:limit="queryParams.pageSize"
|
|
@pagination="getList"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 当前选中报价单 -->
|
|
<template v-if="currentQuote">
|
|
<el-divider content-position="left">当前选中报价单</el-divider>
|
|
<div class="selected-quote">
|
|
<el-descriptions :column="3" border>
|
|
<el-descriptions-item label="报价标题">{{ currentQuote.quoteTitle }}</el-descriptions-item>
|
|
<el-descriptions-item label="报价编号">{{ currentQuote.quoteNo }}</el-descriptions-item>
|
|
<el-descriptions-item label="客户名称">{{ currentQuote.customerName }}</el-descriptions-item>
|
|
<el-descriptions-item label="报价金额(元)">
|
|
<span class="money-display">{{
|
|
currentQuote.finalAmount != null
|
|
? `¥${Number(currentQuote.finalAmount).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
|
: '-'
|
|
}}</span>
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="报价日期">{{ currentQuote.quoteDate || '-' }}</el-descriptions-item>
|
|
<el-descriptions-item label="有效期至">{{ currentQuote.expireDate || '-' }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
</div>
|
|
</template>
|
|
|
|
<template #footer>
|
|
<div class="dialog-footer">
|
|
<el-button class="btn-ghost" @click="handleClose">
|
|
<el-icon><Close /></el-icon>取消
|
|
</el-button>
|
|
<el-button class="btn-main" type="primary" @click="handleConfirm" :disabled="!currentQuote">
|
|
<el-icon><Check /></el-icon>确定
|
|
</el-button>
|
|
</div>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts" name="QuoteSelector">
|
|
import { useDictStore } from '@/store/modules/dict'
|
|
import DictTag from '@/components/DictTag/index.vue'
|
|
import ListPagination from '@/components/ListPagination/index.vue'
|
|
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
|
import { Check, Close } from '@element-plus/icons-vue'
|
|
import { ref, watch, computed, nextTick } from 'vue'
|
|
import type { TableInstance } from 'element-plus'
|
|
import { listQuote, getQuote } from '@/api/crm/quote'
|
|
import type { QuoteProductForm, QuoteQuery, QuoteVO } from '@/api/crm/quote/types'
|
|
|
|
interface Props {
|
|
visible: boolean
|
|
customerId?: number | string
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
const dictStore = useDictStore()
|
|
|
|
const dialogTitle = computed(() => {
|
|
return props.customerId ? '选择报价单(已筛选当前客户)' : '选择报价单'
|
|
})
|
|
|
|
const emit = defineEmits(['update:visible', 'confirm'])
|
|
const dialogVisible = ref(false)
|
|
|
|
watch(
|
|
() => props.visible,
|
|
async (val) => {
|
|
dialogVisible.value = val
|
|
if (val) {
|
|
await dictStore.loadAllDict()
|
|
quickSearchKeyword.value = ''
|
|
queryParams.value.pageNum = 1
|
|
queryParams.value.keyword = undefined
|
|
selectedId.value = ''
|
|
currentQuote.value = null
|
|
getList()
|
|
}
|
|
}
|
|
)
|
|
|
|
watch(
|
|
() => props.customerId,
|
|
(val) => {
|
|
if (dialogVisible.value) {
|
|
queryParams.value.customerId = val
|
|
getList()
|
|
}
|
|
}
|
|
)
|
|
|
|
watch(dialogVisible, (val) => {
|
|
emit('update:visible', val)
|
|
})
|
|
|
|
const queryParams = ref<QuoteQuery>({
|
|
pageNum: 1,
|
|
pageSize: 20,
|
|
keyword: undefined,
|
|
customerId: props.customerId,
|
|
excludeStatus: 'invalid'
|
|
})
|
|
|
|
const quickSearchKeyword = ref('')
|
|
const quoteList = ref<QuoteVO[]>([])
|
|
const loading = ref(false)
|
|
const total = ref(0)
|
|
const currentQuote = ref<QuoteVO | null>(null)
|
|
const selectedId = ref('')
|
|
const tableRef = ref<TableInstance>()
|
|
|
|
/** 单位字典 code → 展示文案 */
|
|
const resolveUnitLabel = (unit?: string) => {
|
|
if (unit == null || String(unit).trim() === '') return ''
|
|
return dictStore.getLabel('crm_product_unit', unit) || String(unit).trim()
|
|
}
|
|
|
|
/** 单行产品:产品名称 规格型号 数量 单位;空值跳过,不展示占位符 */
|
|
const formatProductLine = (product: QuoteProductForm): string => {
|
|
const parts = [
|
|
product.productName,
|
|
product.model,
|
|
product.quantity != null ? String(product.quantity) : '',
|
|
resolveUnitLabel(product.unit)
|
|
]
|
|
.map((v) => (v == null ? '' : String(v).trim()))
|
|
.filter(Boolean)
|
|
return parts.join(' ')
|
|
}
|
|
|
|
/** 报价单产品信息:一行一个产品 */
|
|
const formatQuoteProductLines = (row: QuoteVO): string[] => {
|
|
const list = row.productList || []
|
|
return list.map(formatProductLine).filter(Boolean)
|
|
}
|
|
|
|
const getList = async () => {
|
|
loading.value = true
|
|
try {
|
|
queryParams.value.customerId = props.customerId
|
|
const res = await listQuote({
|
|
...queryParams.value,
|
|
keyword: queryParams.value.keyword || undefined,
|
|
customerId: props.customerId != null ? String(props.customerId) : undefined
|
|
})
|
|
quoteList.value = res.rows || []
|
|
total.value = res.total ?? 0
|
|
await nextTick()
|
|
syncTableSelection()
|
|
} catch (error) {
|
|
console.error('获取报价单列表失败:', error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const syncTableSelection = () => {
|
|
const table = tableRef.value
|
|
if (!table) return
|
|
if (!selectedId.value) {
|
|
table.setCurrentRow(undefined)
|
|
return
|
|
}
|
|
const row = quoteList.value.find((item) => String(item.id) === selectedId.value)
|
|
if (row) {
|
|
table.setCurrentRow(row)
|
|
} else {
|
|
table.setCurrentRow(undefined)
|
|
}
|
|
}
|
|
|
|
/** ListQuickSearchInput 内部已 debounce 300ms,输入即检索 */
|
|
const handleQuickSearch = () => {
|
|
queryParams.value.pageNum = 1
|
|
queryParams.value.keyword = quickSearchKeyword.value?.trim() || undefined
|
|
getList()
|
|
}
|
|
|
|
const handleQuickSearchClear = () => {
|
|
quickSearchKeyword.value = ''
|
|
queryParams.value.pageNum = 1
|
|
queryParams.value.keyword = undefined
|
|
getList()
|
|
}
|
|
|
|
const selectQuoteRow = async (row?: QuoteVO | null) => {
|
|
if (!row?.id) {
|
|
selectedId.value = ''
|
|
currentQuote.value = null
|
|
syncTableSelection()
|
|
return
|
|
}
|
|
const rowId = String(row.id)
|
|
if (rowId === selectedId.value && currentQuote.value && String(currentQuote.value.id) === rowId) {
|
|
syncTableSelection()
|
|
return
|
|
}
|
|
selectedId.value = rowId
|
|
syncTableSelection()
|
|
try {
|
|
// 加载完整的报价单信息(包括产品明细),确保 productList 可供合同表单使用
|
|
const res = await getQuote(row.id)
|
|
currentQuote.value = res.data
|
|
} catch (error) {
|
|
console.error('获取报价单详情失败:', error)
|
|
// 降级:使用列表行数据
|
|
currentQuote.value = row
|
|
}
|
|
}
|
|
|
|
const handleCurrentChange = async (row?: QuoteVO) => {
|
|
await selectQuoteRow(row)
|
|
}
|
|
|
|
const handleRadioChange = async (row: QuoteVO) => {
|
|
await selectQuoteRow(row)
|
|
}
|
|
|
|
const handleConfirm = () => {
|
|
emit('confirm', currentQuote.value)
|
|
handleClose()
|
|
}
|
|
|
|
const handleClose = () => {
|
|
currentQuote.value = null
|
|
selectedId.value = ''
|
|
dialogVisible.value = false
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.selected-quote {
|
|
padding: 10px;
|
|
background-color: #f5f7fa;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.mb-3 {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
/* 检索框约占工具栏 1/3 宽,避免独占整行 */
|
|
.quote-selector-toolbar-row :deep(.quick-search-input) {
|
|
width: 33.333%;
|
|
max-width: 33.333%;
|
|
flex: 0 0 33.333%;
|
|
}
|
|
|
|
.product-info-cell {
|
|
padding: 4px 0;
|
|
line-height: 1.5;
|
|
text-align: left;
|
|
white-space: normal;
|
|
}
|
|
|
|
.product-info-line + .product-info-line {
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.quote-selector-table {
|
|
:deep(.el-radio) {
|
|
height: auto;
|
|
margin-right: 0;
|
|
}
|
|
|
|
:deep(.el-radio__label) {
|
|
display: none;
|
|
}
|
|
|
|
/* 选中行:比默认 current-row 更深的薄荷绿底 */
|
|
:deep(.el-table__body tr.current-row > td.el-table__cell) {
|
|
background-color: #a7f3d0 !important;
|
|
}
|
|
|
|
:deep(.el-table__body tr.current-row:hover > td.el-table__cell) {
|
|
background-color: #6ee7b7 !important;
|
|
}
|
|
}
|
|
|
|
.sr-only {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
padding: 0;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
clip: rect(0, 0, 0, 0);
|
|
white-space: nowrap;
|
|
border: 0;
|
|
}
|
|
</style>
|
|
|
|
<style>
|
|
/* 选择报价单弹窗:宽高最大化(贴合 dialog-form-layout 视口留白) */
|
|
.quote-selector-dialog.el-dialog {
|
|
width: calc(100vw - 48px) !important;
|
|
height: calc(100vh - var(--dialog-viewport-inset, 20px) * 2) !important;
|
|
max-width: calc(100vw - 48px) !important;
|
|
max-height: calc(100vh - var(--dialog-viewport-inset, 20px) * 2) !important;
|
|
margin: 0 !important;
|
|
}
|
|
</style>
|