Files
shanxia2.0/web-admin/src/components/CustomerSelector.vue
T
2026-08-08 20:43:48 +08:00

438 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-dialog
title="选择客户"
v-model="dialogVisible"
width="1500px"
align-center
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
:close-on-click-modal="false"
append-to-body
>
<!-- 客户列表 -->
<div class="panel dialog-list-panel">
<div class="list-toolbar">
<div class="list-toolbar-row customer-selector-toolbar-row">
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="客户名称/拼音首字母"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAddCustomer">
<el-icon><Plus /></el-icon>新增客户
</el-button>
<el-button class="btn-ghost" :disabled="!selectedRow" @click="handleEditCustomer">
<el-icon><Edit /></el-icon>编辑客户
</el-button>
<el-button class="btn-ghost" :disabled="!selectedRow" @click="handleMaintainInvoice">
<el-icon><Stamp /></el-icon>维护开票信息
</el-button>
</div>
</div>
<div class="list-body list-wrap dialog-list-body-only">
<el-table
ref="tableRef"
v-loading="loading"
:data="customerList"
class="list-table"
stripe
highlight-current-row
@current-change="handleCurrentChange"
:max-height="400"
>
<el-table-column type="index" label="序号" min-width="60" align="center" header-align="center" />
<el-table-column label="客户名称" prop="customerName" min-width="200" align="center" header-align="center" :show-overflow-tooltip="true" />
<el-table-column label="客户简称" prop="shortName" min-width="120" align="center" header-align="center" :show-overflow-tooltip="true">
<template #default="scope">
<span v-if="hasShortName(scope.row)">{{ scope.row.shortName }}</span>
<el-tag v-else type="warning" size="small">未填写</el-tag>
</template>
</el-table-column>
<el-table-column label="客户类型" prop="customerType" min-width="150" align="center" header-align="center">
<template #default="scope">
<DictTag dict-type="crm_customer_type" :value="scope.row.customerType" />
</template>
</el-table-column>
<el-table-column label="所属行业" prop="industry" min-width="120" align="center" header-align="center" :show-overflow-tooltip="true">
<template #default="scope">
<DictTag dict-type="crm_customer_industry" :value="scope.row.industry" />
</template>
</el-table-column>
<el-table-column label="客户级别" prop="level" min-width="100" align="center" header-align="center">
<template #default="scope">
<DictTag dict-type="crm_customer_level" :value="scope.row.level" />
</template>
</el-table-column>
<el-table-column label="联系人" prop="contactName" min-width="100" align="center" header-align="center" :show-overflow-tooltip="true" />
<el-table-column label="手机号码" prop="mobile" min-width="120" align="center" header-align="center" />
<el-table-column label="地区" prop="province" min-width="120" align="center" header-align="center">
<template #default="scope">
<span>{{ scope.row.province }} {{ scope.row.city }}</span>
</template>
</el-table-column>
<el-table-column label="负责人" prop="ownerUserName" min-width="100" align="center" header-align="center" :show-overflow-tooltip="true" />
<el-table-column label="操作" fixed="right" min-width="120" align="center" header-align="center" class-name="col-actions">
<template #default="scope">
<el-button link type="primary" @click.stop="handleEditShortName(scope.row)">
<el-icon><Edit /></el-icon>{{ hasShortName(scope.row) ? '编辑简称' : '填写简称' }}
</el-button>
</template>
</el-table-column>
</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="currentCustomer">
<el-divider content-position="left">当前选中客户</el-divider>
<div class="selected-customer">
<el-descriptions :column="2" border>
<el-descriptions-item label="客户名称">{{ currentCustomer.customerName }}</el-descriptions-item>
<el-descriptions-item label="客户简称">{{ currentCustomer.shortName }}</el-descriptions-item>
<el-descriptions-item label="客户类型">
<DictTag dict-type="crm_customer_type" :value="currentCustomer.customerType" />
</el-descriptions-item>
<el-descriptions-item label="客户级别">
<DictTag dict-type="crm_customer_level" :value="currentCustomer.level" />
</el-descriptions-item>
<el-descriptions-item label="所属行业">
<DictTag dict-type="crm_customer_industry" :value="currentCustomer.industry" />
</el-descriptions-item>
<el-descriptions-item label="联系人">{{ currentCustomer.contactName || '-' }}</el-descriptions-item>
<el-descriptions-item label="手机号码">{{ currentCustomer.mobile || '-' }}</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="!currentCustomer || !hasShortName(currentCustomer)"
>
<el-icon><Check /></el-icon>确定
</el-button>
</div>
</template>
</el-dialog>
<!-- 填写/编辑客户简称 -->
<el-dialog
v-model="shortNameDialogVisible"
:title="hasShortName(shortNameForm) ? '编辑客户简称' : '填写客户简称'"
width="480px"
align-center
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
:close-on-click-modal="false"
append-to-body
>
<el-form
ref="shortNameFormRef"
:model="shortNameForm"
:rules="shortNameRules"
label-position="top"
class="crm-dialog-form"
>
<el-form-item label="客户名称">
<el-input :model-value="shortNameForm.customerName" disabled />
</el-form-item>
<el-form-item label="客户简称" prop="shortName">
<el-input
v-model="shortNameForm.shortName"
placeholder="请输入客户简称"
maxlength="100"
show-word-limit
clearable
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button class="btn-ghost" @click="shortNameDialogVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="shortNameSaving" @click="handleSaveShortName">
<el-icon><Check /></el-icon>确定
</el-button>
</div>
</template>
</el-dialog>
<CustomerFormDialog ref="customerFormRef" @success="handleCustomerFormSuccess" />
<!-- 维护开票信息(一客户多条 + 默认;关闭后不自动回填外层,需重新「确定」选用客户) -->
<el-dialog
v-model="invoiceDialogVisible"
:title="invoiceDialogTitle"
width="1200px"
align-center
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
:close-on-click-modal="false"
append-to-body
destroy-on-close
>
<InvoiceInfo v-if="invoiceCustomerId" :customer-id="invoiceCustomerId" />
<template #footer>
<div class="dialog-footer">
<el-button class="btn-ghost" @click="invoiceDialogVisible = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="CustomerSelector">
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 CustomerFormDialog from '@/views/crm/customer/CustomerFormDialog.vue'
import InvoiceInfo from '@/views/crm/customer/components/InvoiceInfo.vue'
import { Check, Close, Edit, Plus, Stamp } from '@element-plus/icons-vue'
import { computed, nextTick, ref, watch } from 'vue'
import type { ElFormInstance, FormItemRule } from 'element-plus'
import { fetchCustomerList, fetchUpdateCustomer } from '@/api/crm/customer'
const dictStore = useDictStore()
interface Props {
visible: boolean
}
const props = defineProps<Props>()
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
selectedRow.value = null
currentCustomer.value = null
getList()
}
}
)
watch(dialogVisible, (val) => {
emit('update:visible', val)
})
const queryParams = ref<Record<string, any>>({
pageNum: 1,
pageSize: 20,
keyword: undefined as string | undefined
})
const quickSearchKeyword = ref('')
const customerList = ref<any[]>([])
const loading = ref(false)
const total = ref(0)
/** 表格当前高亮行(可无简称,用于编辑) */
const selectedRow = ref<any | null>(null)
/** 可确认选用的客户(须已填简称) */
const currentCustomer = ref<any | null>(null)
const tableRef = ref<any>()
const customerFormRef = ref<InstanceType<typeof CustomerFormDialog>>()
const shortNameDialogVisible = ref(false)
const shortNameSaving = ref(false)
const shortNameFormRef = ref<ElFormInstance>()
const shortNameForm = ref({
id: '' as string | number,
customerName: '',
shortName: ''
})
const shortNameRules: Record<string, FormItemRule[]> = {
shortName: [{ required: true, message: '客户简称不能为空', trigger: 'blur' }]
}
const hasShortName = (row?: { shortName?: string } | null) => Boolean(String(row?.shortName || '').trim())
const getList = async () => {
loading.value = true
try {
const res = await fetchCustomerList({
page: queryParams.value.pageNum,
pageSize: queryParams.value.pageSize,
keyword: queryParams.value.keyword || undefined
})
customerList.value = res.list || res.rows || []
total.value = res.total ?? 0
} catch (error) {
console.error('获取客户列表失败:', error)
} finally {
loading.value = false
}
}
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 handleAddCustomer = () => {
customerFormRef.value?.open()
}
const handleEditCustomer = () => {
if (!selectedRow.value?.id) {
ElMessage.warning('请先选择一条客户记录')
return
}
customerFormRef.value?.open(selectedRow.value.id)
}
const invoiceDialogVisible = ref(false)
const invoiceCustomerId = ref<string | number>('')
const invoiceCustomerName = ref('')
const invoiceDialogTitle = computed(() =>
invoiceCustomerName.value
? `维护开票信息 — ${invoiceCustomerName.value}`
: '维护开票信息'
)
const handleMaintainInvoice = () => {
if (!selectedRow.value?.id) {
ElMessage.warning('请先选择一条客户记录')
return
}
invoiceCustomerId.value = selectedRow.value.id
invoiceCustomerName.value = selectedRow.value.customerName || ''
invoiceDialogVisible.value = true
}
const handleCustomerFormSuccess = () => {
getList()
}
const handleCurrentChange = (row: any) => {
selectedRow.value = row || null
if (!row) {
currentCustomer.value = null
return
}
if (!hasShortName(row)) {
currentCustomer.value = null
return
}
currentCustomer.value = row
}
const handleEditShortName = (row: any) => {
shortNameForm.value = {
id: row.id,
customerName: row.customerName || '',
shortName: row.shortName || ''
}
shortNameDialogVisible.value = true
nextTick(() => shortNameFormRef.value?.clearValidate())
}
const handleSaveShortName = async () => {
const valid = await shortNameFormRef.value?.validate().catch(() => false)
if (!valid) return
shortNameSaving.value = true
try {
const trimmedShortName = shortNameForm.value.shortName.trim()
await fetchUpdateCustomer({
id: shortNameForm.value.id,
shortName: trimmedShortName
})
ElMessage.success('客户简称保存成功')
const targetId = shortNameForm.value.id
const updatedRow = customerList.value.find((item) => item.id === targetId)
if (updatedRow) {
updatedRow.shortName = trimmedShortName
}
if (selectedRow.value?.id === targetId) {
selectedRow.value = { ...selectedRow.value, shortName: trimmedShortName }
}
if (currentCustomer.value?.id === targetId || (selectedRow.value?.id === targetId && hasShortName(selectedRow.value))) {
currentCustomer.value = { ...(selectedRow.value || currentCustomer.value), shortName: trimmedShortName }
}
shortNameDialogVisible.value = false
} catch (error) {
console.error('保存客户简称失败:', error)
ElMessage.error('保存客户简称失败')
} finally {
shortNameSaving.value = false
}
}
const handleConfirm = () => {
if (!currentCustomer.value) {
if (selectedRow.value && !hasShortName(selectedRow.value)) {
ElMessage.warning('该客户尚未填写简称,请先填写客户简称后再选择')
}
return
}
if (!hasShortName(currentCustomer.value)) {
ElMessage.warning('该客户尚未填写简称,请先填写客户简称后再选择')
return
}
emit('confirm', currentCustomer.value)
handleClose()
}
const handleClose = () => {
selectedRow.value = null
currentCustomer.value = null
dialogVisible.value = false
}
</script>
<style scoped>
.selected-customer {
padding: 10px;
background-color: #f5f7fa;
border-radius: 4px;
}
/* 弹窗 append-to-body 后不在 .page-container 内,需自行限制检索框宽度,避免独占一行 */
.customer-selector-toolbar-row {
flex-wrap: nowrap;
}
.customer-selector-toolbar-row :deep(.quick-search-input) {
width: 280px;
max-width: 280px;
flex: 0 0 280px;
}
</style>