更新
This commit is contained in:
@@ -9,6 +9,10 @@
|
||||
<template #icon><DataLine /></template>
|
||||
今日进入明细
|
||||
</el-button>
|
||||
<el-button type="warning" plain @click="openTagDrawer">
|
||||
<template #icon><CollectionTag /></template>
|
||||
标签维度
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="syncing" @click="syncCustomers()">
|
||||
<template #icon><Refresh /></template>
|
||||
立即同步
|
||||
@@ -98,6 +102,38 @@
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签">
|
||||
<el-select
|
||||
v-model="queryParams.tag_ids"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
clearable
|
||||
filterable
|
||||
placeholder="按标签筛选(可多选,命中任一即返回)"
|
||||
:loading="tagStatsLoading"
|
||||
style="min-width: 320px"
|
||||
@change="resetPage"
|
||||
>
|
||||
<el-option-group
|
||||
v-for="g in tagStats.groups"
|
||||
:key="g.group_name || '_'"
|
||||
:label="(g.group_name || '(未分组)') + ' · ' + g.customer_count"
|
||||
>
|
||||
<el-option
|
||||
v-for="t in g.tags"
|
||||
:key="t.tag_id"
|
||||
:value="t.tag_id"
|
||||
:label="t.tag_name"
|
||||
>
|
||||
<span class="flex items-center justify-between gap-3">
|
||||
<span>{{ t.tag_name }}</span>
|
||||
<span class="text-xs text-gray-400">{{ t.customer_count }} 人</span>
|
||||
</span>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
@@ -144,6 +180,27 @@
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标签" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.tags && row.tags.length">
|
||||
<el-tag
|
||||
v-for="t in row.tags"
|
||||
:key="t.tag_id"
|
||||
size="small"
|
||||
effect="light"
|
||||
type="warning"
|
||||
class="mr-1 mb-1 cursor-pointer"
|
||||
@click="addTagToFilter(t.tag_id)"
|
||||
>
|
||||
{{ t.tag_name }}
|
||||
<span v-if="t.group_name" class="ml-1 text-gray-400 text-xs">
|
||||
{{ t.group_name }}
|
||||
</span>
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="添加时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(firstExternalAddTime(row)) }}
|
||||
@@ -293,6 +350,99 @@
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 标签维度面板 -->
|
||||
<el-drawer
|
||||
v-model="showTagDrawer"
|
||||
title="标签维度统计"
|
||||
direction="rtl"
|
||||
size="560px"
|
||||
:destroy-on-close="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between w-full pr-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-base font-semibold">标签维度统计</span>
|
||||
<el-tag type="warning" effect="plain">
|
||||
{{ tagStats.groups.length }} 组 · {{ tagStats.total_tags }} 标签
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-button link type="primary" :loading="tagStatsLoading" @click="loadTagStats(true)">
|
||||
<template #icon><Refresh /></template>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="px-1">
|
||||
<div class="grid grid-cols-3 gap-2 mb-3">
|
||||
<el-card shadow="never" body-class="!p-3">
|
||||
<div class="text-xs text-gray-500">已打标签客户</div>
|
||||
<div class="text-xl font-bold text-warning">{{ tagStats.total_tagged_customers }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="never" body-class="!p-3">
|
||||
<div class="text-xs text-gray-500">标签数</div>
|
||||
<div class="text-xl font-bold">{{ tagStats.total_tags }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="never" body-class="!p-3">
|
||||
<div class="text-xs text-gray-500">关系总数</div>
|
||||
<div class="text-xl font-bold">{{ tagStats.total_relations }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-input
|
||||
v-model="tagSearchKw"
|
||||
placeholder="筛选标签名 / 分组名"
|
||||
clearable
|
||||
size="small"
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<el-empty
|
||||
v-if="filteredTagGroups.length === 0"
|
||||
:description="tagStatsLoading ? '加载中…' : '暂无标签数据'"
|
||||
:image-size="64"
|
||||
/>
|
||||
|
||||
<el-collapse v-else v-model="tagDrawerActiveGroups">
|
||||
<el-collapse-item
|
||||
v-for="g in filteredTagGroups"
|
||||
:key="g.group_name || '_'"
|
||||
:name="g.group_name || '_'"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<span class="font-medium">{{ g.group_name || '(未分组)' }}</span>
|
||||
<el-tag size="small" effect="plain">{{ g.tags.length }}</el-tag>
|
||||
<span class="ml-auto text-xs text-gray-500">
|
||||
最热 {{ g.customer_count }} 人
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="g.tags" size="small" stripe>
|
||||
<el-table-column label="标签" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span class="font-medium">{{ row.tag_name }}</span>
|
||||
<div class="text-xs text-gray-400 truncate">{{ row.tag_id }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户数" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="text-warning font-bold">{{ row.customer_count }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="filterByTag(row.tag_id)">
|
||||
筛选
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 客户详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="showDetail"
|
||||
@@ -336,6 +486,24 @@
|
||||
</div>
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="标签" :span="2">
|
||||
<div v-if="currentCustomer.tags?.length">
|
||||
<el-tag
|
||||
v-for="t in currentCustomer.tags"
|
||||
:key="t.tag_id"
|
||||
size="small"
|
||||
effect="light"
|
||||
type="warning"
|
||||
class="mr-1 mb-1"
|
||||
>
|
||||
{{ t.tag_name }}
|
||||
<span v-if="t.group_name" class="ml-1 text-gray-400 text-xs">
|
||||
{{ t.group_name }}
|
||||
</span>
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -343,7 +511,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { Refresh, Setting, DataLine } from '@element-plus/icons-vue'
|
||||
import { Refresh, Setting, DataLine, CollectionTag } from '@element-plus/icons-vue'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
import {
|
||||
@@ -353,7 +521,8 @@ import {
|
||||
qywxSyncSettingsGet,
|
||||
qywxSyncSettingsSave,
|
||||
qywxCustomerTodayArrival,
|
||||
qywxCustomerTodayArrivalList
|
||||
qywxCustomerTodayArrivalList,
|
||||
qywxCustomerTagStats
|
||||
} from '@/api/qywx'
|
||||
|
||||
const syncing = ref(false)
|
||||
@@ -388,11 +557,100 @@ const syncSettings = reactive({
|
||||
interval: 3600
|
||||
})
|
||||
|
||||
const queryParams = reactive({
|
||||
const queryParams = reactive<{ name: string; follow_user: string; tag_ids: string[] }>({
|
||||
name: '',
|
||||
follow_user: ''
|
||||
follow_user: '',
|
||||
tag_ids: []
|
||||
})
|
||||
|
||||
// ── 标签维度(筛选下拉 + 抽屉面板共用同一份数据) ──────────────────────────
|
||||
interface TagItem {
|
||||
tag_id: string
|
||||
tag_name: string
|
||||
customer_count: number
|
||||
}
|
||||
interface TagGroup {
|
||||
group_name: string
|
||||
customer_count: number
|
||||
tags: TagItem[]
|
||||
}
|
||||
interface TagStatsPayload {
|
||||
total_tags: number
|
||||
total_relations: number
|
||||
total_tagged_customers: number
|
||||
groups: TagGroup[]
|
||||
}
|
||||
|
||||
const tagStats = reactive<TagStatsPayload>({
|
||||
total_tags: 0,
|
||||
total_relations: 0,
|
||||
total_tagged_customers: 0,
|
||||
groups: []
|
||||
})
|
||||
const tagStatsLoading = ref(false)
|
||||
const showTagDrawer = ref(false)
|
||||
const tagSearchKw = ref('')
|
||||
const tagDrawerActiveGroups = ref<string[]>([])
|
||||
|
||||
const filteredTagGroups = computed<TagGroup[]>(() => {
|
||||
const kw = tagSearchKw.value.trim().toLowerCase()
|
||||
if (!kw) return tagStats.groups
|
||||
const result: TagGroup[] = []
|
||||
for (const g of tagStats.groups) {
|
||||
const groupHit = (g.group_name || '').toLowerCase().includes(kw)
|
||||
const tags = g.tags.filter((t) => groupHit || (t.tag_name || '').toLowerCase().includes(kw))
|
||||
if (tags.length) {
|
||||
result.push({ ...g, tags })
|
||||
}
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
async function loadTagStats(showFeedback = false) {
|
||||
tagStatsLoading.value = true
|
||||
try {
|
||||
const res: any = await qywxCustomerTagStats()
|
||||
if (res && typeof res === 'object') {
|
||||
tagStats.total_tags = Number(res.total_tags ?? 0)
|
||||
tagStats.total_relations = Number(res.total_relations ?? 0)
|
||||
tagStats.total_tagged_customers = Number(res.total_tagged_customers ?? 0)
|
||||
tagStats.groups = Array.isArray(res.groups) ? res.groups : []
|
||||
}
|
||||
if (showFeedback) feedback.msgSuccess('已刷新')
|
||||
} catch (e: any) {
|
||||
if (showFeedback) feedback.msgError(e?.message || '加载标签统计失败')
|
||||
console.error('加载标签统计失败', e)
|
||||
} finally {
|
||||
tagStatsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openTagDrawer() {
|
||||
showTagDrawer.value = true
|
||||
// 默认展开第 1 组(最热门);其余折叠避免首屏太长
|
||||
tagDrawerActiveGroups.value = tagStats.groups[0]?.group_name ? [tagStats.groups[0].group_name] : []
|
||||
if (tagStats.groups.length === 0) {
|
||||
loadTagStats()
|
||||
}
|
||||
}
|
||||
|
||||
/** 列表上点击 tag 徽章 → 把这个 tag 加入筛选并重查 */
|
||||
function addTagToFilter(tagId: string) {
|
||||
if (!tagId) return
|
||||
if (!queryParams.tag_ids.includes(tagId)) {
|
||||
queryParams.tag_ids.push(tagId)
|
||||
resetPage()
|
||||
}
|
||||
}
|
||||
|
||||
/** 抽屉里点「筛选」→ 用这个 tag 替换当前筛选 + 关闭抽屉 */
|
||||
function filterByTag(tagId: string) {
|
||||
if (!tagId) return
|
||||
queryParams.tag_ids = [tagId]
|
||||
showTagDrawer.value = false
|
||||
resetPage()
|
||||
}
|
||||
|
||||
// ── 今日进入分布 / 明细(事件流水零误差口径) ──────────────────────────────
|
||||
interface ArrivalStats {
|
||||
total: number
|
||||
@@ -534,6 +792,7 @@ const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
function handleReset() {
|
||||
queryParams.name = ''
|
||||
queryParams.follow_user = ''
|
||||
queryParams.tag_ids = []
|
||||
resetParams()
|
||||
}
|
||||
|
||||
@@ -721,6 +980,7 @@ onMounted(() => {
|
||||
loadStats()
|
||||
loadSyncSettings()
|
||||
loadArrival(false)
|
||||
loadTagStats()
|
||||
startArrivalPolling()
|
||||
})
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user