|
|
|
|
@ -0,0 +1,312 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="customer-detail-container">
|
|
|
|
|
<el-card shadow="never" class="main-card">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<span class="header-title">客户详情</span>
|
|
|
|
|
<el-button size="default" type="primary" icon="ArrowLeft" @click="handleBack">返回</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-tabs v-model="activeTab" @tab-change="handleTabChange" class="custom-tabs">
|
|
|
|
|
<el-tab-pane label="客户详情" name="detail">
|
|
|
|
|
<div v-loading="loadingDetail" class="tab-content">
|
|
|
|
|
<el-descriptions :column="3" border v-if="customerInfo" class="customer-descriptions">
|
|
|
|
|
<el-descriptions-item label="客户名称" :span="2">
|
|
|
|
|
<span class="value-text value-highlight">{{ customerInfo?.customerName || '-' }}</span>
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="助记名称">
|
|
|
|
|
<span class="value-text value-highlight">{{ customerInfo?.mnemonicName || '-' }}</span>
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="所属行业">
|
|
|
|
|
<dict-tag :options="industry_id" :value="customerInfo?.industryId" />
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="客户类型">
|
|
|
|
|
<dict-tag :options="customer_type" :value="customerInfo?.customerType" />
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="客户状态">
|
|
|
|
|
<dict-tag :options="customer_status" :value="customerInfo?.customerStatus" />
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="客户级别">
|
|
|
|
|
<dict-tag :options="customer_level" :value="customerInfo?.customerLevel" />
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="客户来源">
|
|
|
|
|
<dict-tag :options="customer_source" :value="customerInfo?.customerSource" />
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="客户经理">{{ customerInfo?.ownerName || '-' }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="企业规模">
|
|
|
|
|
<dict-tag :options="customer_scale" :value="customerInfo?.customerScale" />
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="办公地" :span="2">{{ customerInfo?.detailedAddress || '-' }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="注册地" :span="3">{{ customerInfo?.registeredAddress || '-' }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="商务联系人">{{ customerInfo?.businessContact || '-' }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="商务联系人电话">{{ customerInfo?.businessContactPhone || '-' }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="技术联系人">{{ customerInfo?.technicalContact || '-' }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="技术联系人电话">{{ customerInfo?.technicalContactPhone || '-' }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="法定代表人">{{ customerInfo?.legalRepresentative || '-' }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="营业执照号码">{{ customerInfo?.businessLicenseNumber || '-' }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="税号">{{ customerInfo?.taxNumber || '-' }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="开户银行">{{ customerInfo?.bankAccountOpening || '-' }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="银行账号">{{ customerInfo?.bankNumber || '-' }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="备注" :span="3">{{ customerInfo?.remark || '-' }}</el-descriptions-item>
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
<el-empty v-else description="暂无客户信息" class="empty-state" />
|
|
|
|
|
</div>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
<el-tab-pane label="客户联系人" name="contact">
|
|
|
|
|
<div v-loading="loadingContact" class="tab-content">
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
<i class="el-icon-user section-icon"></i>
|
|
|
|
|
<span class="section-title">客户联系人</span>
|
|
|
|
|
<el-tag v-if="contactTotal > 0" type="info" size="small" class="count-tag"> {{ contactTotal }} 条 </el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="header-actions">
|
|
|
|
|
<el-button size="default" icon="Refresh" @click="loadContactList">刷新</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-table :data="contactList" size="default" border stripe v-loading="loadingContact" class="data-table">
|
|
|
|
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
|
|
|
|
<el-table-column label="联系人姓名" prop="contactName" width="120" />
|
|
|
|
|
<el-table-column label="尊称" prop="sexType" width="80" align="center">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<dict-tag :options="sex_type" :value="scope.row.sexType" />
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="角色" prop="roleType" width="120" align="center">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<dict-tag :options="role_type" :value="scope.row.roleType" />
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="是否首要联系人" prop="firstFlag" width="130" align="center">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<dict-tag :options="first_flag" :value="scope.row.firstFlag" />
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="部门职务" prop="departmentPosition" width="150" show-overflow-tooltip />
|
|
|
|
|
<el-table-column label="手机号码" prop="phoneNumber" width="140" />
|
|
|
|
|
<el-table-column label="固定电话" prop="landlineNumber" width="140" />
|
|
|
|
|
<el-table-column label="电子邮箱" prop="email" min-width="180" show-overflow-tooltip />
|
|
|
|
|
<el-table-column label="微信账号" prop="wechatAccount" width="130" show-overflow-tooltip />
|
|
|
|
|
<el-table-column label="详细地址" prop="detailedAddress" min-width="200" show-overflow-tooltip />
|
|
|
|
|
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
|
|
|
|
|
</el-table>
|
|
|
|
|
<el-empty v-if="!loadingContact && contactList.length === 0" description="暂无联系人信息" class="empty-state" />
|
|
|
|
|
<pagination
|
|
|
|
|
v-show="contactTotal > 0"
|
|
|
|
|
:total="contactTotal"
|
|
|
|
|
v-model:page="contactQuery.pageNum"
|
|
|
|
|
v-model:limit="contactQuery.pageSize"
|
|
|
|
|
@pagination="loadContactList"
|
|
|
|
|
class="pagination-wrapper"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
</el-tabs>
|
|
|
|
|
</el-card>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts" name="CustomerDetail">
|
|
|
|
|
import { computed, reactive, ref, toRefs, watch } from 'vue';
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
|
|
|
import { getCustomerInfo } from '@/api/oa/crm/customerInfo';
|
|
|
|
|
import { CustomerInfoVO } from '@/api/oa/crm/customerInfo/types';
|
|
|
|
|
import { listCustomerContact } from '@/api/oa/crm/customerContact';
|
|
|
|
|
import { CustomerContactVO } from '@/api/oa/crm/customerContact/types';
|
|
|
|
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
const { customer_type, industry_id, customer_source, customer_scale, customer_status, customer_level, sex_type, role_type, first_flag } = toRefs<any>(
|
|
|
|
|
proxy?.useDict(
|
|
|
|
|
'customer_type',
|
|
|
|
|
'industry_id',
|
|
|
|
|
'customer_source',
|
|
|
|
|
'customer_scale',
|
|
|
|
|
'customer_status',
|
|
|
|
|
'customer_level',
|
|
|
|
|
'sex_type',
|
|
|
|
|
'role_type',
|
|
|
|
|
'first_flag'
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const customerId = computed(() => route.params.customerId as string | number | undefined);
|
|
|
|
|
const customerInfo = ref<CustomerInfoVO | null>(null);
|
|
|
|
|
const contactList = ref<CustomerContactVO[]>([]);
|
|
|
|
|
|
|
|
|
|
const activeTab = ref('detail');
|
|
|
|
|
|
|
|
|
|
// 记录已加载的标签页,实现懒加载
|
|
|
|
|
const loadedTabs = ref<Set<string>>(new Set(['detail']));
|
|
|
|
|
|
|
|
|
|
const loadingDetail = ref(false);
|
|
|
|
|
const loadingContact = ref(false);
|
|
|
|
|
|
|
|
|
|
const contactTotal = ref(0);
|
|
|
|
|
const contactQuery = reactive({ pageNum: 1, pageSize: 10, customerId: undefined as string | number | undefined });
|
|
|
|
|
|
|
|
|
|
const handleBack = () => {
|
|
|
|
|
proxy?.$tab.closePage(route);
|
|
|
|
|
router.back();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadCustomerInfo = async () => {
|
|
|
|
|
if (!customerId.value) return;
|
|
|
|
|
loadingDetail.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const res = await getCustomerInfo(customerId.value);
|
|
|
|
|
customerInfo.value = res.data;
|
|
|
|
|
} finally {
|
|
|
|
|
loadingDetail.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadContactList = async () => {
|
|
|
|
|
if (!customerId.value) {
|
|
|
|
|
contactList.value = [];
|
|
|
|
|
contactTotal.value = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
loadingContact.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const res: any = await listCustomerContact({
|
|
|
|
|
...contactQuery,
|
|
|
|
|
customerId: customerId.value
|
|
|
|
|
});
|
|
|
|
|
contactList.value = res.rows || [];
|
|
|
|
|
contactTotal.value = res.total || 0;
|
|
|
|
|
} finally {
|
|
|
|
|
loadingContact.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 标签切换处理
|
|
|
|
|
const handleTabChange = (tabName: string) => {
|
|
|
|
|
if (!loadedTabs.value.has(tabName)) {
|
|
|
|
|
loadedTabs.value.add(tabName);
|
|
|
|
|
switch (tabName) {
|
|
|
|
|
case 'contact':
|
|
|
|
|
contactQuery.pageNum = 1;
|
|
|
|
|
contactQuery.customerId = customerId.value;
|
|
|
|
|
loadContactList();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => customerId.value,
|
|
|
|
|
() => {
|
|
|
|
|
if (customerId.value) {
|
|
|
|
|
loadedTabs.value.clear();
|
|
|
|
|
loadedTabs.value.add('detail');
|
|
|
|
|
loadCustomerInfo();
|
|
|
|
|
contactQuery.customerId = customerId.value;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
);
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
.customer-detail-container {
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
|
|
|
|
.main-card {
|
|
|
|
|
:deep(.el-card__header) {
|
|
|
|
|
.card-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
|
|
.header-title {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #303133;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-card__body) {
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.custom-tabs {
|
|
|
|
|
:deep(.el-tabs__content) {
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab-content {
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
padding-bottom: 12px;
|
|
|
|
|
border-bottom: 1px solid #e4e7ed;
|
|
|
|
|
|
|
|
|
|
.header-left {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
|
|
|
|
.section-icon {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
color: #409eff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #303133;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.customer-descriptions {
|
|
|
|
|
.value-text {
|
|
|
|
|
&.value-highlight {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #409eff;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.data-table {
|
|
|
|
|
:deep(.el-table__header) {
|
|
|
|
|
th {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-table__body) {
|
|
|
|
|
tr:hover {
|
|
|
|
|
background-color: #f5f7fa;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-state {
|
|
|
|
|
padding: 40px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pagination-wrapper {
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|