This commit is contained in:
Your Name
2026-04-30 09:27:42 +08:00
parent dc899e4af4
commit eb782305e6
460 changed files with 10785 additions and 928 deletions
+264 -4
View File
@@ -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