|
|
@@ -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>
|