调整综合转化统计医生维度口径

This commit is contained in:
2026-07-02 11:55:15 +08:00
parent e2a1e62867
commit 4a17fe8a0c
2 changed files with 213 additions and 92 deletions
+103 -19
View File
@@ -129,7 +129,7 @@
<el-card class="!border-none" shadow="never">
<template #header>
<div class="card-header">
<span>诊单金额占比</span>
<span>{{ amountPieTitle }}</span>
<span class="card-hint">TOP 10</span>
</div>
</template>
@@ -146,17 +146,17 @@
<el-card class="!border-none" shadow="never">
<template #header>
<div class="card-header">
<span>加粉占比</span>
<span>{{ secondaryPieTitle }}</span>
<span class="card-hint">TOP 10</span>
</div>
</template>
<v-charts
v-if="fanPieHasData"
v-if="secondaryPieHasData"
class="stats-chart"
:option="fanPieOption"
:option="secondaryPieOption"
autoresize
/>
<el-empty v-else description="暂无加粉数据" />
<el-empty v-else :description="secondaryPieEmptyText" />
</el-card>
</el-col>
</el-row>
@@ -311,10 +311,13 @@ const overview = reactive<Record<string, any>>({
amounts: [],
order_counts: [],
fan_counts: [],
rois: []
rois: [],
appointment_counts: [],
interview_counts: []
},
amount_share: [],
fan_share: []
fan_share: [],
order_share: []
}
})
@@ -392,7 +395,7 @@ const currentEntityId = computed({
}
})
const summaryCards: MetricCard[] = [
const defaultSummaryCards: MetricCard[] = [
{ key: 'add_fans_count', label: '加粉数', type: 'count' },
{ key: 'paid_appointment_count', label: '付费挂号', type: 'count' },
{ key: 'free_appointment_count', label: '免费挂号', type: 'count' },
@@ -410,7 +413,15 @@ const summaryCards: MetricCard[] = [
{ key: 'roi', label: 'ROI', type: 'ratio' }
]
const tableColumns: MetricColumn[] = [
const doctorSummaryCards: MetricCard[] = [
{ key: 'appointment_total_count', label: '挂号数', type: 'count' },
{ key: 'interview_count', label: '面诊数', type: 'count' },
{ key: 'completed_order_count', label: '接诊单数', type: 'count' },
{ key: 'completed_order_amount', label: '开药金额', type: 'money' },
{ key: 'receive_rate', label: '接诊率', type: 'percent' }
]
const defaultTableColumns: MetricColumn[] = [
{ key: 'add_fans_count', label: '加粉数', type: 'count' },
{ key: 'total_open_count', label: '总开口', type: 'count', placeholder: true },
{ key: 'unreplied_count', label: '未回复', type: 'count', placeholder: true },
@@ -431,6 +442,18 @@ const tableColumns: MetricColumn[] = [
{ key: 'roi', label: 'ROI', type: 'ratio', minWidth: 90 }
]
const doctorTableColumns: MetricColumn[] = [
{ key: 'appointment_total_count', label: '挂号数', type: 'count' },
{ key: 'interview_count', label: '面诊数', type: 'count' },
{ key: 'completed_order_count', label: '接诊单数', type: 'count' },
{ key: 'completed_order_amount', label: '开药金额', type: 'money', minWidth: 120 },
{ key: 'receive_rate', label: '接诊率', type: 'percent', minWidth: 100 }
]
const isDoctorDimension = computed(() => queryParams.dimension === 'doctor')
const summaryCards = computed(() => isDoctorDimension.value ? doctorSummaryCards : defaultSummaryCards)
const tableColumns = computed(() => isDoctorDimension.value ? doctorTableColumns : defaultTableColumns)
const dateRangeText = computed(() => {
if (!overview.date_range?.length) return '未选择'
return `${overview.date_range[0]}${overview.date_range[1]}`
@@ -438,7 +461,14 @@ const dateRangeText = computed(() => {
const rankingChartHasData = computed(() => overview.charts.ranking.names.length > 0)
const amountPieHasData = computed(() => overview.charts.amount_share.length > 0)
const fanPieHasData = computed(() => overview.charts.fan_share.length > 0)
const secondaryPieHasData = computed(() =>
isDoctorDimension.value
? (overview.charts.order_share?.length || 0) > 0
: (overview.charts.fan_share?.length || 0) > 0
)
const amountPieTitle = computed(() => isDoctorDimension.value ? '开药金额占比' : '诊单金额占比')
const secondaryPieTitle = computed(() => isDoctorDimension.value ? '接诊单数占比' : '加粉占比')
const secondaryPieEmptyText = computed(() => isDoctorDimension.value ? '暂无接诊数据' : '暂无加粉数据')
const rankingChartData = computed(() => {
const ranking = overview.charts?.ranking || {}
@@ -448,14 +478,16 @@ const rankingChartData = computed(() => {
return {
names,
amounts: normalize(Array.isArray(ranking.amounts) ? ranking.amounts : []),
appointmentCounts: normalize(Array.isArray(ranking.appointment_counts) ? ranking.appointment_counts : []),
interviewCounts: normalize(Array.isArray(ranking.interview_counts) ? ranking.interview_counts : []),
orderCounts: normalize(Array.isArray(ranking.order_counts) ? ranking.order_counts : []),
fanCounts: normalize(Array.isArray(ranking.fan_counts) ? ranking.fan_counts : []),
fanCounts: normalize(Array.isArray(ranking.fan_counts) ? ranking.fan_counts : [])
}
})
const rankingChartOption = computed(() => ({
const rankingChartOption = computed(() => {
const common = {
tooltip: { trigger: 'axis' },
legend: { data: ['诊单金额', '接诊诊单', '加粉数'] },
grid: { left: 48, right: 24, top: 48, bottom: 48 },
xAxis: {
type: 'category',
@@ -467,7 +499,13 @@ const rankingChartOption = computed(() => ({
},
yAxis: [
{ type: 'value', name: '金额 / 数量' }
],
]
}
if (!isDoctorDimension.value) {
return {
...common,
legend: { data: ['诊单金额', '接诊诊单', '加粉数'] },
series: [
{
name: '诊单金额',
@@ -497,7 +535,53 @@ const rankingChartOption = computed(() => ({
itemStyle: { color: '#ff9f43' }
}
]
}))
}
}
return {
...common,
legend: { data: ['开药金额', '挂号数', '面诊数', '接诊单数'] },
series: [
{
name: '开药金额',
type: 'bar',
barMaxWidth: 36,
data: rankingChartData.value.amounts,
itemStyle: { color: '#4a78ff' }
},
{
name: '挂号数',
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 8,
lineStyle: { width: 3 },
data: rankingChartData.value.appointmentCounts,
itemStyle: { color: '#15b8a6' }
},
{
name: '面诊数',
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 8,
lineStyle: { width: 3 },
data: rankingChartData.value.interviewCounts,
itemStyle: { color: '#ff9f43' }
},
{
name: '接诊单数',
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 8,
lineStyle: { width: 3 },
data: rankingChartData.value.orderCounts,
itemStyle: { color: '#7c3aed' }
}
]
}
})
const amountPieOption = computed(() => ({
tooltip: { trigger: 'item' },
@@ -511,7 +595,7 @@ const amountPieOption = computed(() => ({
},
series: [
{
name: '诊单金额',
name: amountPieTitle.value,
type: 'pie',
radius: ['42%', '70%'],
center: ['50%', '42%'],
@@ -531,7 +615,7 @@ const amountPieOption = computed(() => ({
]
}))
const fanPieOption = computed(() => ({
const secondaryPieOption = computed(() => ({
tooltip: { trigger: 'item' },
legend: {
bottom: 0,
@@ -543,7 +627,7 @@ const fanPieOption = computed(() => ({
},
series: [
{
name: '加粉数',
name: secondaryPieTitle.value,
type: 'pie',
radius: ['42%', '70%'],
center: ['50%', '42%'],
@@ -558,7 +642,7 @@ const fanPieOption = computed(() => ({
labelLayout: {
hideOverlap: true,
},
data: overview.charts.fan_share
data: isDoctorDimension.value ? overview.charts.order_share : overview.charts.fan_share
}
]
}))
@@ -48,8 +48,8 @@ class ConversionLogic
$emptyResult = [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary([]),
'charts' => self::buildCharts([]),
'summary' => self::buildSummary([], $dimension),
'charts' => self::buildCharts([], $dimension),
'lists' => [],
'count' => 0,
'page_no' => $pageNo,
@@ -57,8 +57,8 @@ class ConversionLogic
'extend' => [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary([]),
'charts' => self::buildCharts([]),
'summary' => self::buildSummary([], $dimension),
'charts' => self::buildCharts([], $dimension),
],
];
if ($includeFilters) {
@@ -75,8 +75,8 @@ class ConversionLogic
$result = [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary([]),
'charts' => self::buildCharts([]),
'summary' => self::buildSummary([], $dimension),
'charts' => self::buildCharts([], $dimension),
'lists' => [],
'count' => 0,
'page_no' => $pageNo,
@@ -84,8 +84,8 @@ class ConversionLogic
'extend' => [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary([]),
'charts' => self::buildCharts([]),
'summary' => self::buildSummary([], $dimension),
'charts' => self::buildCharts([], $dimension),
],
];
if ($includeFilters) {
@@ -124,8 +124,8 @@ class ConversionLogic
$result = [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary([]),
'charts' => self::buildCharts([]),
'summary' => self::buildSummary([], $dimension),
'charts' => self::buildCharts([], $dimension),
'lists' => [],
'count' => 0,
'page_no' => $pageNo,
@@ -133,8 +133,8 @@ class ConversionLogic
'extend' => [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary([]),
'charts' => self::buildCharts([]),
'summary' => self::buildSummary([], $dimension),
'charts' => self::buildCharts([], $dimension),
],
];
if ($includeFilters) {
@@ -191,8 +191,8 @@ class ConversionLogic
$result = [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary($allRows),
'charts' => self::buildCharts($chartRows),
'summary' => self::buildSummary($allRows, $dimension),
'charts' => self::buildCharts($chartRows, $dimension),
'lists' => $pagedRows,
'count' => count($allRows),
'page_no' => $pageNo,
@@ -200,8 +200,8 @@ class ConversionLogic
'extend' => [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary($allRows),
'charts' => self::buildCharts($chartRows),
'summary' => self::buildSummary($allRows, $dimension),
'charts' => self::buildCharts($chartRows, $dimension),
],
];
if ($includeFilters) {
@@ -226,8 +226,8 @@ class ConversionLogic
$result = [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary($rows),
'charts' => self::buildCharts($rows),
'summary' => self::buildSummary($rows, $dimension),
'charts' => self::buildCharts($rows, $dimension),
'lists' => array_slice($rows, $offset, $pageSize),
'count' => $count,
'page_no' => $pageNo,
@@ -235,8 +235,8 @@ class ConversionLogic
'extend' => [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary($rows),
'charts' => self::buildCharts($rows),
'summary' => self::buildSummary($rows, $dimension),
'charts' => self::buildCharts($rows, $dimension),
],
];
if ($includeFilters) {
@@ -1226,7 +1226,7 @@ class ConversionLogic
$entity['paid_appointment_rate'] = self::percent($paidAppointmentCount, $addFansCount);
$entity['open_appointment_rate'] = self::percent($paidAppointmentCount, $totalOpenCount);
$entity['interview_rate'] = self::percent($interviewCount, $appointmentTotalCount);
$entity['receive_rate'] = self::percent($completedOrderCount, $addFansCount);
$entity['receive_rate'] = self::receiveRate($completedOrderCount, $addFansCount, $interviewCount, $dimension);
$entity['interview_receive_rate'] = self::percent($completedOrderCount, $interviewCount);
$entity['open_receive_rate'] = self::percent($completedOrderCount, $totalOpenCount);
$entity['avg_unit_price'] = self::safeDivideMoney($completedOrderAmount, $completedOrderCount);
@@ -1459,7 +1459,7 @@ class ConversionLogic
$node['paid_appointment_rate'] = self::percent($paidAppointmentCount, $addFansCount);
$node['open_appointment_rate'] = self::percent($paidAppointmentCount, $totalOpenCount);
$node['interview_rate'] = self::percent($interviewCount, $appointmentTotalCount);
$node['receive_rate'] = self::percent($completedOrderCount, $addFansCount);
$node['receive_rate'] = self::receiveRate($completedOrderCount, $addFansCount, $interviewCount, 'dept');
$node['interview_receive_rate'] = self::percent($completedOrderCount, $interviewCount);
$node['open_receive_rate'] = self::percent($completedOrderCount, $totalOpenCount);
$node['avg_unit_price'] = self::safeDivideMoney($completedOrderAmount, $completedOrderCount);
@@ -2010,7 +2010,12 @@ class ConversionLogic
'paid_appointment_rate' => self::percent($paidAppointmentCount, $addFansCount),
'open_appointment_rate' => self::percent($paidAppointmentCount, $totalOpenCount),
'interview_rate' => self::percent($interviewCount, $appointmentTotalCount),
'receive_rate' => self::percent($completedOrderCount, $addFansCount),
'receive_rate' => self::receiveRate(
$completedOrderCount,
$addFansCount,
$interviewCount,
'member'
),
'interview_receive_rate' => self::percent($completedOrderCount, $interviewCount),
'open_receive_rate' => self::percent($completedOrderCount, $totalOpenCount),
'avg_unit_price' => self::safeDivideMoney($completedOrderAmount, $completedOrderCount),
@@ -2149,7 +2154,7 @@ class ConversionLogic
* @param array<int, array<string, mixed>> $rows
* @return array<string, mixed>
*/
private static function buildSummary(array $rows): array
private static function buildSummary(array $rows, string $dimension = 'dept'): array
{
$summary = [
'add_fans_count' => 0,
@@ -2181,7 +2186,12 @@ class ConversionLogic
$summary['paid_appointment_rate'] = self::percent($summary['paid_appointment_count'], $summary['add_fans_count']);
$summary['open_appointment_rate'] = self::percent($summary['paid_appointment_count'], $summary['total_open_count']);
$summary['interview_rate'] = self::percent($summary['interview_count'], $summary['appointment_total_count']);
$summary['receive_rate'] = self::percent($summary['completed_order_count'], $summary['add_fans_count']);
$summary['receive_rate'] = self::receiveRate(
$summary['completed_order_count'],
$summary['add_fans_count'],
$summary['interview_count'],
$dimension
);
$summary['interview_receive_rate'] = self::percent($summary['completed_order_count'], $summary['interview_count']);
$summary['open_receive_rate'] = self::percent($summary['completed_order_count'], $summary['total_open_count']);
$summary['avg_unit_price'] = self::safeDivideMoney($summary['completed_order_amount'], $summary['completed_order_count']);
@@ -2310,7 +2320,7 @@ class ConversionLogic
* @param array<int, array<string, mixed>> $rows
* @return array<string, mixed>
*/
private static function buildCharts(array $rows): array
private static function buildCharts(array $rows, string $dimension = 'dept'): array
{
$chartableRows = array_values(array_filter($rows, static fn (array $row): bool => !((bool)($row['_virtual_bucket'] ?? false))));
$topRows = array_slice($chartableRows, 0, 10);
@@ -2319,29 +2329,56 @@ class ConversionLogic
'names' => [],
'amounts' => [],
'order_counts' => [],
'fan_counts' => [],
'rois' => [],
];
if ($dimension === 'doctor') {
$ranking['appointment_counts'] = [];
$ranking['interview_counts'] = [];
} else {
$ranking['fan_counts'] = [];
$ranking['rois'] = [];
}
foreach ($topRows as $row) {
$ranking['names'][] = $row['name'];
$ranking['amounts'][] = $row['completed_order_amount'];
$ranking['order_counts'][] = $row['completed_order_count'];
if ($dimension === 'doctor') {
$ranking['appointment_counts'][] = $row['appointment_total_count'];
$ranking['interview_counts'][] = $row['interview_count'];
} else {
$ranking['fan_counts'][] = $row['add_fans_count'];
$ranking['rois'][] = $row['roi'];
}
}
return [
$charts = [
'ranking' => $ranking,
'amount_share' => array_map(
static fn (array $row): array => ['name' => $row['name'], 'value' => $row['completed_order_amount']],
array_filter($topRows, static fn (array $row): bool => (float)$row['completed_order_amount'] > 0)
),
'fan_share' => array_map(
];
if ($dimension === 'doctor') {
$charts['order_share'] = array_map(
static fn (array $row): array => ['name' => $row['name'], 'value' => $row['completed_order_count']],
array_filter($topRows, static fn (array $row): bool => (int)$row['completed_order_count'] > 0)
);
} else {
$charts['fan_share'] = array_map(
static fn (array $row): array => ['name' => $row['name'], 'value' => $row['add_fans_count']],
array_filter($topRows, static fn (array $row): bool => (int)$row['add_fans_count'] > 0)
),
];
);
}
return $charts;
}
private static function receiveRate(int $completedOrderCount, int $addFansCount, int $interviewCount, string $dimension): float
{
$denominator = $dimension === 'doctor' ? $interviewCount : $addFansCount;
return self::percent($completedOrderCount, $denominator);
}
private static function percent(int $numerator, int $denominator): float