调整综合转化统计医生维度口径
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user