feat: show intent rate window summaries
This commit is contained in:
parent
846b8c601b
commit
4653a56392
@ -108,6 +108,7 @@ describe('PerformanceConversionDetail', () => {
|
||||
expect(screen.getByText(/经营数据更新至/)).toBeInTheDocument()
|
||||
expect(screen.getAllByText('12.50%').length).toBeGreaterThan(0)
|
||||
expect(screen.getAllByText('¥1,299.50').length).toBeGreaterThan(0)
|
||||
expect(screen.getAllByText('——').length).toBe(3)
|
||||
expect(screen.getByRole('img', { name: /支付金额从/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('table')).toHaveTextContent('2026-07-13')
|
||||
})
|
||||
@ -148,4 +149,25 @@ describe('PerformanceConversionDetail', () => {
|
||||
await waitFor(() => expect(screen.getByRole('heading', { name: '亿家达折叠书桌' })).toBeInTheDocument())
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('shows complete intent-rate averages and change when 15 days are available', async () => {
|
||||
const daily = Array.from({ length: 15 }, (_, index) => ({
|
||||
...detailData.daily[0],
|
||||
date: `2026-07-${String(index + 1).padStart(2, '0')}`,
|
||||
favorites: index < 8 ? 10 : 20,
|
||||
cartUsers: 10,
|
||||
visitors: 100,
|
||||
paymentConversionRate: 0.03,
|
||||
}))
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ success: true, data: { ...detailData, daily } }),
|
||||
} as Response)
|
||||
|
||||
render(<PerformanceConversionDetail workflowId="flow-1" productId="1041432226288" returnHref="/workspaces/business/performance-conversion" />)
|
||||
|
||||
expect(await screen.findByText('23.00%')).toBeInTheDocument()
|
||||
expect(screen.getByText('27.67%')).toBeInTheDocument()
|
||||
expect(screen.getByText('20.29%')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
import DepartmentShell from './DepartmentShell'
|
||||
import {
|
||||
PERFORMANCE_CONVERSION_METRICS,
|
||||
buildIntentRateWindowSummary,
|
||||
type PerformanceConversionDailyMetric,
|
||||
type PerformanceConversionMetricKey,
|
||||
type PerformanceConversionTimeseries,
|
||||
@ -138,6 +139,35 @@ function MetricSummary({ data }: { data: PerformanceConversionTimeseries }) {
|
||||
)
|
||||
}
|
||||
|
||||
function IntentRateSummary({ daily }: { daily: PerformanceConversionDailyMetric[] }) {
|
||||
const summary = buildIntentRateWindowSummary(daily)
|
||||
const formatWindowValue = (value: number | null) => value === null ? '——' : `${(value * 100).toFixed(2)}%`
|
||||
|
||||
return (
|
||||
<section className="border-t border-slate-200 bg-white" aria-labelledby="intent-rate-summary-title">
|
||||
<header className="px-5 py-4 sm:px-7">
|
||||
<h2 id="intent-rate-summary-title" className="font-bold text-slate-950">流程完结后 7日 / 15日平均值</h2>
|
||||
<p className="mt-1 text-xs text-slate-500">从流程完结日开始计算,窗口内每天数据完整时显示,缺少天数或字段显示“——”</p>
|
||||
</header>
|
||||
<dl className="grid gap-px border-y border-slate-200 bg-slate-200 sm:grid-cols-3">
|
||||
<div className="bg-white px-5 py-4 sm:px-7">
|
||||
<dt className="text-xs font-semibold text-slate-500">完结后7日意向率平均</dt>
|
||||
<dd className="mt-1 text-lg font-bold tabular-nums text-slate-950">{formatWindowValue(summary.sevenDayIntentRate)}</dd>
|
||||
</div>
|
||||
<div className="bg-white px-5 py-4 sm:px-7">
|
||||
<dt className="text-xs font-semibold text-slate-500">完结后15日意向率平均</dt>
|
||||
<dd className="mt-1 text-lg font-bold tabular-nums text-slate-950">{formatWindowValue(summary.fifteenDayIntentRate)}</dd>
|
||||
</div>
|
||||
<div className="bg-white px-5 py-4 sm:px-7">
|
||||
<dt className="text-xs font-semibold text-slate-500">意向率环比(15日 / 7日)</dt>
|
||||
<dd className="mt-1 text-lg font-bold tabular-nums text-slate-950">{formatWindowValue(summary.intentRateChange)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p className="px-5 py-3 text-xs leading-5 text-slate-500 sm:px-7">意向率 = (收藏人数 + 加购人数)÷ 商品访客数 × 100% + 支付转化率</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function DetailContent({ data }: { data: PerformanceConversionTimeseries }) {
|
||||
const [metricKey, setMetricKey] = useState<PerformanceConversionMetricKey>('paymentAmount')
|
||||
const [detailGroup, setDetailGroup] = useState<'流量' | '兴趣' | '成交'>('流量')
|
||||
@ -172,6 +202,7 @@ function DetailContent({ data }: { data: PerformanceConversionTimeseries }) {
|
||||
</header>
|
||||
|
||||
<MetricSummary data={data} />
|
||||
<IntentRateSummary daily={data.daily} />
|
||||
|
||||
{!hasDailyData ? (
|
||||
<div className="px-5 py-12 text-center">
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import {
|
||||
BUSINESS_TREND_BRANDS,
|
||||
addCalendarMonths,
|
||||
buildIntentRateWindowSummary,
|
||||
calculateIntentRate,
|
||||
daysBetweenInclusive,
|
||||
extractValidProductIds,
|
||||
isValidDateKey,
|
||||
@ -35,4 +37,64 @@ describe('performance conversion helpers', () => {
|
||||
expect(isValidDateKey('2026-02-29')).toBe(false)
|
||||
expect(isValidDateKey('2026-9-17')).toBe(false)
|
||||
})
|
||||
|
||||
it('calculates intent rate from daily interest and conversion metrics', () => {
|
||||
expect(calculateIntentRate({ favorites: 10, cartUsers: 5, visitors: 100, paymentConversionRate: 0.03 })).toBe(0.18)
|
||||
expect(calculateIntentRate({ favorites: 10, cartUsers: 5, visitors: 0, paymentConversionRate: 0.03 })).toBeNull()
|
||||
expect(calculateIntentRate({ favorites: 10, cartUsers: null, visitors: 100, paymentConversionRate: 0.03 })).toBeNull()
|
||||
})
|
||||
|
||||
it('calculates complete 7-day and 15-day averages with intent-rate change', () => {
|
||||
const daily = Array.from({ length: 15 }, (_, index) => ({
|
||||
date: `2026-07-${String(index + 1).padStart(2, '0')}`,
|
||||
impressions: 1000,
|
||||
visitors: 100,
|
||||
clickRate: 0.1,
|
||||
favorites: index < 8 ? 10 : 20,
|
||||
cartUsers: 10,
|
||||
paymentConversionRate: 0.03,
|
||||
paymentAmount: 100,
|
||||
roi: 1,
|
||||
}))
|
||||
|
||||
const summary = buildIntentRateWindowSummary(daily)
|
||||
expect(summary.sevenDayIntentRate).toBeCloseTo(0.23, 12)
|
||||
expect(summary.fifteenDayIntentRate).toBeCloseTo(0.27666666666666667, 12)
|
||||
expect(summary.intentRateChange).toBeCloseTo(0.20289855072463767, 12)
|
||||
})
|
||||
|
||||
it('returns unavailable values when a window is incomplete or its 7-day rate is zero', () => {
|
||||
const fiveDays = Array.from({ length: 5 }, (_, index) => ({
|
||||
date: `2026-07-${String(index + 1).padStart(2, '0')}`,
|
||||
impressions: 1000,
|
||||
visitors: 100,
|
||||
clickRate: 0.1,
|
||||
favorites: 10,
|
||||
cartUsers: 10,
|
||||
paymentConversionRate: 0.03,
|
||||
paymentAmount: 100,
|
||||
roi: 1,
|
||||
}))
|
||||
expect(buildIntentRateWindowSummary(fiveDays)).toEqual({
|
||||
sevenDayIntentRate: null,
|
||||
fifteenDayIntentRate: null,
|
||||
intentRateChange: null,
|
||||
})
|
||||
|
||||
const zeroSevenDayRate = Array.from({ length: 15 }, (_, index) => ({
|
||||
date: `2026-07-${String(index + 1).padStart(2, '0')}`,
|
||||
impressions: 1000,
|
||||
visitors: 100,
|
||||
clickRate: 0.1,
|
||||
favorites: index < 7 ? 0 : 10,
|
||||
cartUsers: index < 7 ? 0 : 10,
|
||||
paymentConversionRate: index < 7 ? 0 : 0.03,
|
||||
paymentAmount: 100,
|
||||
roi: 1,
|
||||
}))
|
||||
expect(buildIntentRateWindowSummary(zeroSevenDayRate)).toMatchObject({
|
||||
sevenDayIntentRate: 0,
|
||||
intentRateChange: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -66,6 +66,12 @@ export type PerformanceConversionDailyMetric = {
|
||||
roi: number | null
|
||||
}
|
||||
|
||||
export type PerformanceConversionWindowSummary = {
|
||||
sevenDayIntentRate: number | null
|
||||
fifteenDayIntentRate: number | null
|
||||
intentRateChange: number | null
|
||||
}
|
||||
|
||||
export type PerformanceConversionTimeseries = {
|
||||
workflow: PerformanceConversionWorkflowItem
|
||||
product: {
|
||||
@ -87,6 +93,36 @@ export type PerformanceConversionTimeseries = {
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateIntentRate(day: Pick<PerformanceConversionDailyMetric, 'favorites' | 'cartUsers' | 'visitors' | 'paymentConversionRate'>): number | null {
|
||||
const { favorites, cartUsers, visitors, paymentConversionRate } = day
|
||||
if (favorites === null || cartUsers === null || visitors === null || paymentConversionRate === null
|
||||
|| ![favorites, cartUsers, visitors, paymentConversionRate].every((value) => Number.isFinite(value))) {
|
||||
return null
|
||||
}
|
||||
if (visitors <= 0) return null
|
||||
return (favorites + cartUsers) / visitors + paymentConversionRate
|
||||
}
|
||||
|
||||
function averageIntentRate(daily: PerformanceConversionDailyMetric[], windowSize: number) {
|
||||
const window = daily.slice(0, windowSize)
|
||||
if (window.length !== windowSize) return null
|
||||
const values = window.map(calculateIntentRate)
|
||||
if (values.some((value) => value === null)) return null
|
||||
return values.reduce<number>((total, value) => total + (value ?? 0), 0) / windowSize
|
||||
}
|
||||
|
||||
export function buildIntentRateWindowSummary(daily: PerformanceConversionDailyMetric[]): PerformanceConversionWindowSummary {
|
||||
const sevenDayIntentRate = averageIntentRate(daily, 7)
|
||||
const fifteenDayIntentRate = averageIntentRate(daily, 15)
|
||||
const intentRateChange = sevenDayIntentRate !== null
|
||||
&& sevenDayIntentRate !== 0
|
||||
&& fifteenDayIntentRate !== null
|
||||
? (fifteenDayIntentRate - sevenDayIntentRate) / sevenDayIntentRate
|
||||
: null
|
||||
|
||||
return { sevenDayIntentRate, fifteenDayIntentRate, intentRateChange }
|
||||
}
|
||||
|
||||
export function extractValidProductIds(...values: unknown[]): string[] {
|
||||
const candidates = values.flatMap((value) => String(value || '')
|
||||
.split(/[\s,,;;/、|]+/)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user