feat(oa/crm): 添加客户详情页面并优化客户列表功能

- 新增客户详情页面组件,支持查看客户基本信息和联系人列表
- 将客户列表中的客户名称链接指向客户详情页面
- 在客户列表操作栏新增出差记录按钮
dev
Yangk 2 days ago
parent 9090059e20
commit e475029725

@ -165,6 +165,12 @@ export const constantRoutes: RouteRecordRaw[] = [
component: () => import('@/views/oa/crm/businessTripApply/edit.vue'),
name: 'BusinessTripApplyEdit',
meta: { title: '出差申请编辑', activeMenu: '/oa/crm/businessTripApply' }
},
{
path: 'customerDetail/:customerId',
component: () => import('@/views/oa/crm/customerDetail/index.vue'),
name: 'CustomerDetail',
meta: { title: '客户详情', activeMenu: '/oa/crm/customerInfo' }
}
]
},

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

@ -77,7 +77,7 @@
<el-table-column label="客户ID" align="center" prop="customerId" width="80" v-if="columns[0].visible" />
<el-table-column label="客户名称" align="center" prop="customerName" width="200" v-if="columns[2].visible" show-overflow-tooltip>
<template #default="scope">
<el-link type="primary" underline @click="handleTripHistory(scope.row)">
<el-link type="primary" underline @click="handleDetail(scope.row)">
{{ scope.row.customerName }}
</el-link>
</template>
@ -155,11 +155,14 @@
<dict-tag :options="active_flag" :value="scope.row.activeFlag" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="180" class-name="small-padding fixed-width">
<el-table-column label="操作" align="center" fixed="right" width="220" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="客户关系" placement="top">
<el-button link type="primary" icon="Connection" @click="handleViewRelation(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="出差记录" placement="top">
<el-button link type="primary" icon="Tickets" @click="handleTripHistory(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['oa/crm:customerInfo:edit']"></el-button>
</el-tooltip>
@ -895,6 +898,13 @@ const handleViewRelation = async (row: CustomerInfoVO) => {
}
};
/** 查看客户详情 */
const handleDetail = (row: CustomerInfoVO) => {
router.push({
path: `/oa/crm/customerDetail/${row.customerId}`
});
};
/** 查看客户出差交流记录 */
const handleTripHistory = (row: CustomerInfoVO) => {
router.push({

Loading…
Cancel
Save