438 lines
15 KiB
Vue
438 lines
15 KiB
Vue
<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>
|