feat: 个人用户聚合视图 — 客户管理双Tab+四模块数据聚合

- customers 表新增 customer_type 列 (unit/individual)
- 客户管理页:单位客户 | 个人用户 Tab 切换
- 个人用户 Tab: 直接内嵌4 Tab 显示所有个人用户的聚合数据
- 个人用户通过业务模块快速新建产生 (customer_type=individual)
- 4个业务API新增 customer_type 筛选参数(JOIN customers)
- 客户列表排序: 个人用户置顶

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-12 22:14:44 +08:00
parent 835e5d38fd
commit 6504a393e3
11 changed files with 238 additions and 1 deletions
+2
View File
@@ -31,6 +31,8 @@ declare module 'vue' {
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
@@ -15,7 +15,16 @@ const search = ref('')
const filterIndustry = ref('')
const filterService = ref('')
const filterManagerId = ref('')
const customerType = ref('unit')
const customers = ref<any[]>([])
// Individual view tab data
const indivTab = ref('visits')
const indivVisits = ref<any[]>([])
const indivPlans = ref<any[]>([])
const indivMini = ref<any[]>([])
const indivKeyVisits = ref<any[]>([])
const indivLoading = ref(false)
const currentPage = ref(1)
const pageSize = ref(25)
const total = ref(0)
@@ -79,6 +88,32 @@ function onPageChange(page: number) { currentPage.value = page; loadCustomers()
function onPageSizeChange(size: number) { pageSize.value = size; currentPage.value = 1; loadCustomers() }
function onFilterChange() { currentPage.value = 1; loadCustomers() }
function switchCustomerType(type: string) {
customerType.value = type
if (type === 'unit') {
onFilterChange()
} else {
loadIndividualData()
}
}
async function loadIndividualData() {
indivLoading.value = true
try {
const [visits, plans, mini, keyVisits] = await Promise.all([
api.get('/visits/', { params: { customer_type: 'individual' } }),
api.get('/work-plans/', { params: { customer_type: 'individual' } }),
api.get('/mini-business/', { params: { customer_type: 'individual' } }),
api.get('/key-visits/', { params: { customer_type: 'individual' } }),
])
indivVisits.value = Array.isArray(visits.data) ? visits.data : (visits.data.items || [])
indivPlans.value = Array.isArray(plans.data) ? plans.data : (plans.data.items || [])
indivMini.value = Array.isArray(mini.data) ? mini.data : (mini.data.items || [])
indivKeyVisits.value = Array.isArray(keyVisits.data) ? keyVisits.data : (keyVisits.data.items || [])
} catch (_) {}
finally { indivLoading.value = false }
}
function buildMonthlyFee(): string {
const amt = form.value.fee_amount.trim()
if (!amt) return ''
@@ -329,6 +364,60 @@ async function handleImport() {
<div class="page-rule"></div>
</div>
<!-- Type Toggle -->
<div style="margin-bottom:14px">
<el-radio-group v-model="customerType" @change="switchCustomerType">
<el-radio-button value="unit">单位客户</el-radio-button>
<el-radio-button value="individual">个人用户</el-radio-button>
</el-radio-group>
</div>
<!-- Individual View: 4 Tabs -->
<div v-if="customerType === 'individual'" v-loading="indivLoading">
<el-tabs v-model="indivTab">
<el-tab-pane label="拜访记录" name="visits">
<el-table :data="indivVisits" size="small" max-height="400" v-if="indivVisits.length">
<el-table-column prop="visit_date" label="日期" width="110" />
<el-table-column prop="visit_method" label="方式" width="80" />
<el-table-column prop="communication_content" label="沟通内容" min-width="250" show-overflow-tooltip />
<el-table-column prop="customer_name" label="客户" width="120" />
</el-table>
<div v-else class="empty-tab">暂无拜访记录</div>
</el-tab-pane>
<el-tab-pane label="工作计划" name="plans">
<el-table :data="indivPlans" size="small" max-height="400" v-if="indivPlans.length">
<el-table-column prop="plan_date" label="计划时间" width="110" />
<el-table-column prop="plan_content" label="工作计划" min-width="250" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80" />
<el-table-column prop="customer_name" label="客户" width="120" />
</el-table>
<div v-else class="empty-tab">暂无工作计划</div>
</el-tab-pane>
<el-tab-pane label="商机跟单" name="mini">
<el-table :data="indivMini" size="small" max-height="400" v-if="indivMini.length">
<el-table-column prop="product_type" label="产品类型" width="110" />
<el-table-column prop="amount" label="金额" width="100" />
<el-table-column prop="status" label="状态" width="80" />
<el-table-column prop="follow_up_detail" label="跟进内容" min-width="200" show-overflow-tooltip />
<el-table-column prop="customer_name" label="客户" width="120" />
</el-table>
<div v-else class="empty-tab">暂无商机记录</div>
</el-tab-pane>
<el-tab-pane label="要客拜访" name="keyVisits">
<el-table :data="indivKeyVisits" size="small" max-height="400" v-if="indivKeyVisits.length">
<el-table-column prop="planned_date" label="计划时间" width="110" />
<el-table-column prop="description" label="内容" min-width="250" show-overflow-tooltip />
<el-table-column prop="urgency_level" label="紧急度" width="80" />
<el-table-column prop="customer_name" label="客户" width="120" />
</el-table>
<div v-else class="empty-tab">暂不要客记录</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- Unit View: Search + Table -->
<template v-if="customerType === 'unit'">
<!-- Search & Filter -->
<el-card style="margin-bottom: 16px;">
<el-row :gutter="12" align="middle">
@@ -534,6 +623,7 @@ async function handleImport() {
<el-button type="primary" :loading="importLoading" @click="handleImport" :disabled="!importFile">确认导入</el-button>
</template>
</el-dialog>
</template>
<!-- Merge confirmation dialog -->
<el-dialog v-model="mergeDialogVisible" title="合并客户" width="520px" :close-on-click-modal="false">