diff --git a/admin/src/views/stats/conversion/index.vue b/admin/src/views/stats/conversion/index.vue index b50fced9..ec4e8889 100644 --- a/admin/src/views/stats/conversion/index.vue +++ b/admin/src/views/stats/conversion/index.vue @@ -129,7 +129,7 @@ @@ -146,17 +146,17 @@ - + @@ -311,10 +311,13 @@ const overview = reactive>({ 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,56 +478,110 @@ 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(() => ({ - tooltip: { trigger: 'axis' }, - legend: { data: ['诊单金额', '接诊诊单', '加粉数'] }, - grid: { left: 48, right: 24, top: 48, bottom: 48 }, - xAxis: { - type: 'category', - data: rankingChartData.value.names, - axisLabel: { - interval: 0, - rotate: rankingChartData.value.names.length > 6 ? 25 : 0 - } - }, - yAxis: [ - { type: 'value', name: '金额 / 数量' } - ], - series: [ - { - name: '诊单金额', - type: 'bar', - barMaxWidth: 36, - data: rankingChartData.value.amounts, - itemStyle: { color: '#4a78ff' } +const rankingChartOption = computed(() => { + const common = { + tooltip: { trigger: 'axis' }, + grid: { left: 48, right: 24, top: 48, bottom: 48 }, + xAxis: { + type: 'category', + data: rankingChartData.value.names, + axisLabel: { + interval: 0, + rotate: rankingChartData.value.names.length > 6 ? 25 : 0 + } }, - { - name: '接诊诊单', - type: 'line', - smooth: true, - showSymbol: true, - symbolSize: 8, - lineStyle: { width: 3 }, - data: rankingChartData.value.orderCounts, - itemStyle: { color: '#15b8a6' } - }, - { - name: '加粉数', - type: 'line', - smooth: true, - showSymbol: true, - symbolSize: 8, - lineStyle: { width: 3 }, - data: rankingChartData.value.fanCounts, - itemStyle: { color: '#ff9f43' } + yAxis: [ + { type: 'value', name: '金额 / 数量' } + ] + } + + if (!isDoctorDimension.value) { + 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.orderCounts, + itemStyle: { color: '#15b8a6' } + }, + { + name: '加粉数', + type: 'line', + smooth: true, + showSymbol: true, + symbolSize: 8, + lineStyle: { width: 3 }, + data: rankingChartData.value.fanCounts, + 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 } ] })) diff --git a/server/app/adminapi/logic/stats/ConversionLogic.php b/server/app/adminapi/logic/stats/ConversionLogic.php index 8531f623..c8392f0f 100755 --- a/server/app/adminapi/logic/stats/ConversionLogic.php +++ b/server/app/adminapi/logic/stats/ConversionLogic.php @@ -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> $rows * @return array */ - 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> $rows * @return array */ - 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']; - $ranking['fan_counts'][] = $row['add_fans_count']; - $ranking['rois'][] = $row['roi']; + 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