This commit is contained in:
zlwu committed 2026-08-08 23:14:40 +08:00
1 parent fdf7d391a6
commit 90ae136f38
11 files changed
+4318 -4087

No files matched your search

+174 -80
View File
@@ -2,9 +2,9 @@
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="1200px"
width="calc(100vw - 48px)"
align-center
class="dialog-form-layout"
class="dialog-form-layout quote-selector-dialog"
modal-class="order-module-dialog-mask"
:close-on-click-modal="false"
append-to-body
@@ -12,53 +12,54 @@
<!-- 提示信息 -->
<el-alert v-if="props.customerId" :title="`已筛选客户的报价单`" type="info" :closable="false" class="mb-3" show-icon />
<!-- 查询表单 -->
<el-form :model="queryParams" ref="queryFormRef" :inline="true" class="mb-3 search-form-inline">
<el-form-item label="报价编号" prop="quoteNo">
<el-input v-model="queryParams.quoteNo" placeholder="请输入报价编号" clearable @keyup.enter="handleQuery" style="width: 150px" />
</el-form-item>
<el-form-item label="报价标题" prop="quoteTitle">
<el-input v-model="queryParams.quoteTitle" placeholder="请输入报价标题" clearable @keyup.enter="handleQuery" style="width: 180px" />
</el-form-item>
<el-form-item label="客户名称" prop="customerName">
<el-input v-model="queryParams.customerName" placeholder="请输入客户名称" clearable @keyup.enter="handleQuery" style="width: 150px" />
</el-form-item>
<el-form-item label="报价状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 120px">
<el-option v-for="dict in quoteStatusFilterOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item class="form-buttons-right">
<el-button type="primary" :loading="loading" @click="handleQuery">
<el-icon><Search /></el-icon>搜索
</el-button>
<el-button :loading="loading" @click="resetQuery">
<el-icon><Refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
<!-- 报价单列表 -->
<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"
class="list-table quote-selector-table"
stripe
highlight-current-row
row-key="id"
@current-change="handleCurrentChange"
:max-height="400"
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="序号" min-width="60" align="center" header-align="center" />
<el-table-column label="报价单编号" prop="quoteNo" min-width="140" align="center" header-align="center" :show-overflow-tooltip="true" />
<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="quoteTitle" min-width="180" align="center" header-align="center" :show-overflow-tooltip="true" />
<el-table-column label="客户名称" prop="customerName" min-width="150" align="center" header-align="center" :show-overflow-tooltip="true" />
<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>
@@ -126,11 +127,12 @@
import { useDictStore } from '@/store/modules/dict'
import DictTag from '@/components/DictTag/index.vue'
import ListPagination from '@/components/ListPagination/index.vue'
import { Check, Close, Refresh, Search } from '@element-plus/icons-vue'
import { ref, watch, computed } from 'vue'
import type { ElFormInstance } from 'element-plus'
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 { QuoteQuery, QuoteVO } from '@/api/crm/quote/types'
import type { QuoteProductForm, QuoteQuery, QuoteVO } from '@/api/crm/quote/types'
interface Props {
visible: boolean
@@ -144,13 +146,6 @@ const dialogTitle = computed(() => {
return props.customerId ? '选择报价单(已筛选当前客户)' : '选择报价单'
})
/** 筛选下拉里不提供「已作废」 */
const quoteStatusFilterOptions = computed(() => {
const list = dictStore.getOptions('crm_quote_status')
if (!Array.isArray(list)) return []
return list.filter((d: { value: string }) => d.value !== 'invalid')
})
const emit = defineEmits(['update:visible', 'confirm'])
const dialogVisible = ref(false)
@@ -160,6 +155,11 @@ watch(
dialogVisible.value = val
if (val) {
await dictStore.loadAllDict()
quickSearchKeyword.value = ''
queryParams.value.pageNum = 1
queryParams.value.keyword = undefined
selectedId.value = ''
currentQuote.value = null
getList()
}
}
@@ -182,19 +182,43 @@ watch(dialogVisible, (val) => {
const queryParams = ref<QuoteQuery>({
pageNum: 1,
pageSize: 20,
quoteNo: undefined,
quoteTitle: undefined,
customerName: undefined,
keyword: undefined,
customerId: props.customerId,
status: undefined,
excludeStatus: 'invalid'
})
const quickSearchKeyword = ref('')
const quoteList = ref<QuoteVO[]>([])
const loading = ref(false)
const total = ref(0)
const currentQuote = ref<QuoteVO | null>(null)
const queryFormRef = ref<ElFormInstance>()
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
@@ -202,10 +226,13 @@ const getList = async () => {
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 {
@@ -213,39 +240,49 @@ const getList = async () => {
}
}
const handleQuery = () => {
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 resetQuery = () => {
queryFormRef.value?.resetFields()
queryParams.value.excludeStatus = 'invalid'
if (props.customerId) {
queryParams.value.customerId = props.customerId
}
handleQuery()
const handleQuickSearchClear = () => {
quickSearchKeyword.value = ''
queryParams.value.pageNum = 1
queryParams.value.keyword = undefined
getList()
}
watchDebounced(
() => ({
quoteNo: queryParams.value.quoteNo,
quoteTitle: queryParams.value.quoteTitle,
customerName: queryParams.value.customerName,
status: queryParams.value.status
}),
() => {
queryParams.value.pageNum = 1
getList()
},
{ debounce: 300, deep: true }
)
const handleCurrentChange = async (row: QuoteVO) => {
if (!row) {
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)
@@ -257,6 +294,14 @@ const handleCurrentChange = async (row: QuoteVO) => {
}
}
const handleCurrentChange = async (row?: QuoteVO) => {
await selectQuoteRow(row)
}
const handleRadioChange = async (row: QuoteVO) => {
await selectQuoteRow(row)
}
const handleConfirm = () => {
emit('confirm', currentQuote.value)
handleClose()
@@ -264,6 +309,7 @@ const handleConfirm = () => {
const handleClose = () => {
currentQuote.value = null
selectedId.value = ''
dialogVisible.value = false
}
</script>
@@ -279,16 +325,64 @@ const handleClose = () => {
margin-bottom: 16px;
}
.mt-3 {
margin-top: 16px;
/* 检索框约占工具栏 1/3 宽,避免独占整行 */
.quote-selector-toolbar-row :deep(.quick-search-input) {
width: 33.333%;
max-width: 33.333%;
flex: 0 0 33.333%;
}
.search-form-inline {
display: flex;
flex-wrap: wrap;
.product-info-cell {
padding: 4px 0;
line-height: 1.5;
text-align: left;
white-space: normal;
}
.form-buttons-right {
margin-left: auto !important;
.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>