更新
This commit is contained in:
@@ -154,6 +154,50 @@ export function firstVisitConversionOverview(params: FirstVisitConversionParams)
|
||||
)
|
||||
}
|
||||
|
||||
export interface FirstVisitConversionFansDetailParams extends FirstVisitConversionParams {
|
||||
entity_type: 'dept' | 'member'
|
||||
entity_id: string | number
|
||||
admin_id?: number
|
||||
page_no: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface FirstVisitConversionFanDetailRow {
|
||||
external_userid: string
|
||||
customer_name: string
|
||||
wecom_userid: string
|
||||
wecom_staff_name: string
|
||||
add_time: string | null
|
||||
is_deleted: boolean
|
||||
delete_time: string | null
|
||||
}
|
||||
|
||||
export interface FirstVisitConversionFansDetailResult {
|
||||
lists?: FirstVisitConversionFanDetailRow[]
|
||||
rows?: FirstVisitConversionFanDetailRow[]
|
||||
total?: number
|
||||
count?: number
|
||||
page_no?: number
|
||||
page_size?: number
|
||||
entity?: {
|
||||
type?: 'dept' | 'member'
|
||||
id?: string | number
|
||||
admin_id?: number
|
||||
name?: string
|
||||
add_fans_count?: number
|
||||
deleted_fans_count?: number
|
||||
}
|
||||
date_range?: [string, string]
|
||||
}
|
||||
|
||||
/** 一诊加粉明细:时间、渠道及行实体均由调用方显式透传,服务端仍需按 DataScope 收窄。 */
|
||||
export function firstVisitConversionFansDetail(params: FirstVisitConversionFansDetailParams) {
|
||||
return request.get(
|
||||
{ url: '/firstvisit.conversion/fansDetail', params, timeout: 120000 },
|
||||
{ ignoreCancelToken: true }
|
||||
)
|
||||
}
|
||||
|
||||
export interface FirstVisitRegistrationStatsParams {
|
||||
time_type: 'today' | 'yesterday' | 'week' | 'month'
|
||||
dept_id?: number
|
||||
|
||||
@@ -189,7 +189,13 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="add_fans_count" label="加粉" min-width="92" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="fan-count-value">
|
||||
<button
|
||||
v-if="hasFans(row.add_fans_count)"
|
||||
type="button"
|
||||
class="fan-count-value fan-detail-trigger fan-detail-trigger--table"
|
||||
title="查看该行加粉客户明细"
|
||||
@click.stop="openFansDetail(row)"
|
||||
>
|
||||
{{ formatNumber(row.add_fans_count) }}
|
||||
<em
|
||||
v-if="hasDeletedFans(row.deleted_fans_count)"
|
||||
@@ -198,7 +204,8 @@
|
||||
>
|
||||
(-{{ formatNumber(row.deleted_fans_count) }})
|
||||
</em>
|
||||
</span>
|
||||
</button>
|
||||
<span v-else class="fan-count-value is-empty">0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total_open_count" label="开口" min-width="72" align="right" />
|
||||
@@ -292,6 +299,75 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="fansDetailVisible"
|
||||
:title="fansDetailTitle"
|
||||
width="920px"
|
||||
top="7vh"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="fans-detail-dialog"
|
||||
@closed="resetFansDetail"
|
||||
>
|
||||
<div class="fans-detail-context">
|
||||
<span><b>统计区间</b>{{ detailRangeText }}</span>
|
||||
<span><b>媒体渠道</b>{{ detailChannelText }}</span>
|
||||
<span><b>数据范围</b>{{ dashboard.meta.scope_label || '当前权限范围' }}</span>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="fansDetailLoading"
|
||||
:data="fansDetailRows"
|
||||
:row-class-name="fansDetailRowClassName"
|
||||
height="430"
|
||||
class="fans-detail-table"
|
||||
>
|
||||
<el-table-column label="客户" min-width="190">
|
||||
<template #default="{ row }">
|
||||
<div class="customer-cell">
|
||||
<strong class="customer-name" :class="{ 'is-deleted': isDeletedFan(row) }">
|
||||
{{ fanCustomerName(row) }}
|
||||
</strong>
|
||||
<small v-if="row.customer_name && row.external_userid">{{ row.external_userid }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="归属员工" min-width="160">
|
||||
<template #default="{ row }">{{ fanOwnerName(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="添加时间" min-width="180">
|
||||
<template #default="{ row }">{{ fanAddTime(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="isDeletedFan(row)" class="deleted-status">
|
||||
<el-tag type="danger" effect="dark" size="small">已删除</el-tag>
|
||||
<small v-if="row.delete_time">{{ row.delete_time }}</small>
|
||||
</div>
|
||||
<el-tag v-else type="success" effect="plain" size="small">正常</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty :image-size="68" description="当前条件下暂无加粉客户明细" />
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
<div class="fans-detail-footer">
|
||||
<span>共 {{ formatNumber(fansDetailPager.total) }} 位客户</span>
|
||||
<el-pagination
|
||||
v-if="fansDetailPager.total > 0"
|
||||
v-model:current-page="fansDetailPager.page_no"
|
||||
v-model:page-size="fansDetailPager.page_size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="fansDetailPager.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
background
|
||||
@current-change="loadFansDetail"
|
||||
@size-change="handleFansDetailSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -300,7 +376,14 @@ import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Lock, Refresh } from '@element-plus/icons-vue'
|
||||
import vCharts from 'vue-echarts'
|
||||
import { firstVisitConversionOverview, type FirstVisitConversionParams } from '@/api/first_visit'
|
||||
import {
|
||||
firstVisitConversionFansDetail,
|
||||
firstVisitConversionOverview,
|
||||
type FirstVisitConversionFanDetailRow,
|
||||
type FirstVisitConversionFansDetailParams,
|
||||
type FirstVisitConversionFansDetailResult,
|
||||
type FirstVisitConversionParams
|
||||
} from '@/api/first_visit'
|
||||
|
||||
type MetricType = 'count' | 'money' | 'ratio'
|
||||
type MediaChannelOption = {
|
||||
@@ -346,6 +429,13 @@ const query = reactive<FirstVisitConversionParams>({
|
||||
media_channel_code: ''
|
||||
})
|
||||
const customDateRange = ref<string[]>([])
|
||||
const fansDetailVisible = ref(false)
|
||||
const fansDetailLoading = ref(false)
|
||||
const fansDetailRows = ref<FirstVisitConversionFanDetailRow[]>([])
|
||||
const fansDetailPager = reactive({ page_no: 1, page_size: 20, total: 0 })
|
||||
const fansDetailEntity = ref<{ type: 'dept' | 'member'; id: string | number; adminId?: number; name: string }>({
|
||||
type: 'dept', id: 0, name: '当前行'
|
||||
})
|
||||
const deptTreeProps = { value: 'id', label: 'name', children: 'children' }
|
||||
const timeOptions = [
|
||||
{ label: '今日', value: 'today' },
|
||||
@@ -424,6 +514,13 @@ const rankingKind = computed(() => dashboard.meta.ranking_kind || (
|
||||
))
|
||||
const showRankings = computed(() => rankingKind.value !== 'hidden')
|
||||
const rankingSubject = computed(() => rankingKind.value === 'member' ? '组内成员' : '小组')
|
||||
const fansDetailTitle = computed(() => `${fansDetailEntity.value.name} · 加粉客户明细`)
|
||||
const detailRangeText = computed(() => {
|
||||
const startDate = dashboard.meta.start_date || query.start_date || ''
|
||||
const endDate = dashboard.meta.end_date || query.end_date || ''
|
||||
return startDate && endDate ? `${startDate} 至 ${endDate}` : (dashboard.meta.time_label || '当前区间')
|
||||
})
|
||||
const detailChannelText = computed(() => dashboard.meta.selected_media_channel_name || '全部渠道')
|
||||
const totalOrderValue = computed(() => dashboard.rankings.orders.reduce((total, item) => total + Number(item.value || 0), 0))
|
||||
const totalAmountValue = computed(() => dashboard.rankings.amounts.reduce((total, item) => total + Number(item.value || 0), 0))
|
||||
const targetChartOption = computed(() => ({
|
||||
@@ -440,6 +537,7 @@ const targetChartOption = computed(() => ({
|
||||
}))
|
||||
|
||||
let latestDashboardRequestId = 0
|
||||
let latestFansDetailRequestId = 0
|
||||
|
||||
async function loadDashboard() {
|
||||
const requestId = ++latestDashboardRequestId
|
||||
@@ -492,6 +590,89 @@ function handleDeptChange() {
|
||||
loadDashboard()
|
||||
}
|
||||
|
||||
function openFansDetail(row: any) {
|
||||
fansDetailEntity.value = {
|
||||
type: row.type ? 'member' : 'dept',
|
||||
id: row.id,
|
||||
adminId: Number(row.admin_id || 0) || undefined,
|
||||
name: row.name || '当前行'
|
||||
}
|
||||
fansDetailPager.page_no = 1
|
||||
fansDetailRows.value = []
|
||||
fansDetailPager.total = 0
|
||||
fansDetailVisible.value = true
|
||||
loadFansDetail()
|
||||
}
|
||||
|
||||
async function loadFansDetail() {
|
||||
if (!fansDetailVisible.value) return
|
||||
const requestId = ++latestFansDetailRequestId
|
||||
fansDetailLoading.value = true
|
||||
try {
|
||||
const params: FirstVisitConversionFansDetailParams = {
|
||||
...query,
|
||||
time_type: query.time_type,
|
||||
start_date: dashboard.meta.start_date || query.start_date,
|
||||
end_date: dashboard.meta.end_date || query.end_date,
|
||||
media_channel_code: dashboard.meta.selected_media_channel_code || query.media_channel_code || '',
|
||||
entity_type: fansDetailEntity.value.type,
|
||||
entity_id: fansDetailEntity.value.id,
|
||||
admin_id: fansDetailEntity.value.adminId,
|
||||
page_no: fansDetailPager.page_no,
|
||||
page_size: fansDetailPager.page_size
|
||||
}
|
||||
const result = await firstVisitConversionFansDetail(params) as FirstVisitConversionFansDetailResult
|
||||
if (requestId !== latestFansDetailRequestId) return
|
||||
const rows = Array.isArray(result?.lists) ? result.lists : (Array.isArray(result?.rows) ? result.rows : [])
|
||||
fansDetailRows.value = rows
|
||||
fansDetailPager.total = Number(result?.count ?? result?.total ?? rows.length) || 0
|
||||
} catch (error: any) {
|
||||
if (requestId !== latestFansDetailRequestId) return
|
||||
fansDetailRows.value = []
|
||||
fansDetailPager.total = 0
|
||||
ElMessage.error(error?.message || '加粉客户明细加载失败')
|
||||
} finally {
|
||||
if (requestId === latestFansDetailRequestId) fansDetailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleFansDetailSizeChange() {
|
||||
fansDetailPager.page_no = 1
|
||||
loadFansDetail()
|
||||
}
|
||||
|
||||
function resetFansDetail() {
|
||||
++latestFansDetailRequestId
|
||||
fansDetailLoading.value = false
|
||||
fansDetailRows.value = []
|
||||
fansDetailPager.page_no = 1
|
||||
fansDetailPager.total = 0
|
||||
}
|
||||
|
||||
function hasFans(value: any) {
|
||||
return Math.round(Number(value || 0)) > 0
|
||||
}
|
||||
|
||||
function isDeletedFan(row: FirstVisitConversionFanDetailRow) {
|
||||
return Boolean(row.is_deleted)
|
||||
}
|
||||
|
||||
function fanCustomerName(row: FirstVisitConversionFanDetailRow) {
|
||||
return row.customer_name || row.external_userid || '未知客户'
|
||||
}
|
||||
|
||||
function fanOwnerName(row: FirstVisitConversionFanDetailRow) {
|
||||
return row.wecom_staff_name || row.wecom_userid || '未绑定员工'
|
||||
}
|
||||
|
||||
function fanAddTime(row: FirstVisitConversionFanDetailRow) {
|
||||
return row.add_time || '-'
|
||||
}
|
||||
|
||||
function fansDetailRowClassName({ row }: { row: FirstVisitConversionFanDetailRow }) {
|
||||
return isDeletedFan(row) ? 'is-deleted-fan-row' : ''
|
||||
}
|
||||
|
||||
function formatMetric(key: string, type: MetricType) {
|
||||
const value = dashboard.summary[key]
|
||||
if (type === 'money') return formatMoney(value)
|
||||
@@ -658,6 +839,18 @@ onMounted(loadDashboard)
|
||||
}
|
||||
|
||||
.metric-value { display: flex; justify-content: flex-start; }
|
||||
.fan-detail-trigger {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus-visible { outline: 2px solid #2f78df; outline-offset: 3px; border-radius: 3px; }
|
||||
&:disabled { cursor: default; }
|
||||
}
|
||||
.deleted-fans-marker {
|
||||
color: #c65f26;
|
||||
font-size: .58em;
|
||||
@@ -666,6 +859,9 @@ onMounted(loadDashboard)
|
||||
}
|
||||
|
||||
.fan-count-value .deleted-fans-marker { font-size: 11px; }
|
||||
.fan-detail-trigger--table { color: #1769aa; font-weight: 650; }
|
||||
.fan-detail-trigger--table:hover { color: #0f4f82; }
|
||||
.fan-count-value.is-empty { color: #98a2b3; }
|
||||
|
||||
.ranking-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
||||
.panel { min-width: 0; max-width: 100%; padding: 16px; border-radius: 10px; box-sizing: border-box; }
|
||||
@@ -729,6 +925,47 @@ onMounted(loadDashboard)
|
||||
.legend-line.is-target { border-color: #2f78df; border-top-style: dashed; }
|
||||
.target-chart { width: 100%; height: 260px; }
|
||||
|
||||
.fans-detail-context {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 20px;
|
||||
margin: -4px 0 14px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e4e9ef;
|
||||
border-radius: 7px;
|
||||
color: #5d6b7e;
|
||||
background: #f7f9fb;
|
||||
font-size: 12px;
|
||||
|
||||
span { display: inline-flex; gap: 7px; }
|
||||
b { color: #8a95a5; font-weight: 500; }
|
||||
}
|
||||
.fans-detail-table {
|
||||
width: 100%;
|
||||
|
||||
.customer-cell { display: grid; gap: 3px; }
|
||||
.customer-name { color: #273347; font-weight: 650; }
|
||||
.customer-name.is-deleted { color: #c24137; text-decoration: line-through; text-decoration-color: #e8a39e; }
|
||||
.customer-cell small { overflow: hidden; color: #98a2b3; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
:deep(.is-deleted-fan-row > td.el-table__cell) { background: #fff3f2 !important; }
|
||||
}
|
||||
.deleted-status {
|
||||
display: inline-grid;
|
||||
justify-items: center;
|
||||
gap: 4px;
|
||||
|
||||
small { color: #b14e47; font-size: 10px; }
|
||||
}
|
||||
.fans-detail-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-top: 14px;
|
||||
color: #8491a2;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.filter-strip { align-items: flex-start; flex-wrap: wrap; }
|
||||
.range-text { margin-left: 0; }
|
||||
@@ -744,5 +981,7 @@ onMounted(loadDashboard)
|
||||
.employee-select, .dept-select, .channel-select { width: 100%; }
|
||||
.bar-row { grid-template-columns: 100px minmax(70px, 1fr) 96px; }
|
||||
.target-summary { grid-template-columns: 1fr; }
|
||||
.fans-detail-footer { align-items: flex-start; flex-direction: column; }
|
||||
.fans-detail-footer :deep(.el-pagination) { max-width: 100%; flex-wrap: wrap; justify-content: flex-start; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user