文件
OmniCard/mpuniapp/pages/owner/useStats.uts

299 行
8.8 KiB
Plaintext

/**
* OmniCard AI (CRMmp) - 名片主独立数据统计与销售漏斗业务驱动层
* 纯函数计算转化率、漏斗阶段折损、渠道成效矩阵与国际化
*/
import { LeadEntity, OwnerPageQuery } from '../../common/types.uts';
import { fetchOwnerLeadPage } from '../../common/api.uts';
import { getCurrentLocale, getOmniText } from '../../common/locale.uts';
export type StatsPeriod = 'today' | '7d' | '30d' | 'all';
export type FunnelStep = {
key: string;
label: string;
count: number;
rate: number;
};
export type ChannelMetric = {
source: 'nfc' | 'qr' | 'link' | 'ai_chat';
label: string;
leadsCount: number;
wonCount: number;
conversionRate: number;
};
export type StatsSummary = {
totalLeads: number;
hotLeads: number;
wonLeads: number;
overallRate: number;
};
export type StatsState = {
period: StatsPeriod;
summary: StatsSummary;
funnel: FunnelStep[];
channels: ChannelMetric[];
loading: boolean;
errorMessage: string;
};
function isZhLocale(): boolean {
return getCurrentLocale() === 'zh-CN';
}
export function calcConversionRate(total: number, won: number): number {
if (total <= 0 || won <= 0) {
return 0;
}
const rate = (won / total) * 100;
return Math.round(rate * 10) / 10;
}
export function calcFunnelStepRate(currentCount: number, baseCount: number): number {
if (baseCount <= 0 || currentCount <= 0) {
return 0;
}
const rate = (currentCount / baseCount) * 100;
return Math.round(rate * 10) / 10;
}
export function resolveStatsText(key: string): string {
const locale = getCurrentLocale();
if (key === 'title') {
return isZhLocale() ? '经营与漏斗统计' : 'Pipeline & Analytics';
}
if (key === 'subtitle') {
return isZhLocale() ? '全渠道商机转化与意向大盘' : 'Full-funnel lead insights & radar score';
}
if (key === 'empty') {
return getOmniText(locale, 'common.empty');
}
if (key === 'error') {
return getOmniText(locale, 'common.error');
}
if (key === 'loading') {
return getOmniText(locale, 'common.loading');
}
if (key === 'retry') {
return getOmniText(locale, 'portal.retry');
}
// Periods
if (key === 'periodToday') {
return isZhLocale() ? '今日' : 'Today';
}
if (key === 'period7d') {
return isZhLocale() ? '近7天' : '7 Days';
}
if (key === 'period30d') {
return isZhLocale() ? '近30天' : '30 Days';
}
if (key === 'periodAll') {
return isZhLocale() ? '全部' : 'All Time';
}
// Funnel & Metrics
if (key === 'metricTotal') {
return isZhLocale() ? '潜客总量' : 'Total Leads';
}
if (key === 'metricHot') {
return isZhLocale() ? '高意向商机' : 'High Intent';
}
if (key === 'metricWon') {
return isZhLocale() ? '签约成交' : 'Deals Won';
}
if (key === 'metricRate') {
return isZhLocale() ? '综合转化率' : 'Conversion';
}
if (key === 'funnelTitle') {
return isZhLocale() ? '商机转化漏斗' : 'Pipeline Funnel';
}
if (key === 'channelTitle') {
return isZhLocale() ? '获客渠道成效' : 'Acquisition Channels';
}
if (key === 'stepNew') {
return isZhLocale() ? '新建线索' : 'New Leads';
}
if (key === 'stepHot') {
return isZhLocale() ? '高意向' : 'Hot Intent';
}
if (key === 'stepContacted') {
return isZhLocale() ? '已接洽' : 'Contacted';
}
if (key === 'stepWon') {
return isZhLocale() ? '赢单成单' : 'Deals Won';
}
if (key === 'sourceNfc') {
return isZhLocale() ? 'NFC 贴卡' : 'NFC Touch';
}
if (key === 'sourceQr') {
return isZhLocale() ? '海报码' : 'Poster QR';
}
if (key === 'sourceLink') {
return isZhLocale() ? '直链分享' : 'Direct Link';
}
if (key === 'sourceAi') {
return isZhLocale() ? 'AI顾问' : 'AI Concierge';
}
if (key === 'liveBadge') {
return isZhLocale() ? '实时大盘' : 'LIVE INSIGHTS';
}
if (key === 'healthTitle') {
return isZhLocale() ? '潜客意向度健康度' : 'Intent Distribution';
}
if (key === 'tierHot') {
return isZhLocale() ? '核心高意向 (80+)' : 'High Intent (80+)';
}
if (key === 'tierWarm') {
return isZhLocale() ? '重点跟进 (50-79)' : 'Warm Leads (50-79)';
}
if (key === 'tierCool') {
return isZhLocale() ? '待培育池 (<50)' : 'Cool Pool (<50)';
}
if (key === 'stepDescNew') {
return isZhLocale() ? '建档接入' : 'Captured';
}
if (key === 'stepDescHot') {
return isZhLocale() ? '高频互动' : 'Engaged';
}
if (key === 'stepDescContacted') {
return isZhLocale() ? '方案沟通' : 'Pitched';
}
if (key === 'stepDescWon') {
return isZhLocale() ? '签约成交' : 'Closed';
}
return key;
}
export function createStatsState(): StatsState {
return {
period: 'all',
summary: {
totalLeads: 0,
hotLeads: 0,
wonLeads: 0,
overallRate: 0,
},
funnel: [
{ key: 'new', label: resolveStatsText('stepNew'), count: 0, rate: 0 },
{ key: 'hot', label: resolveStatsText('stepHot'), count: 0, rate: 0 },
{ key: 'contacted', label: resolveStatsText('stepContacted'), count: 0, rate: 0 },
{ key: 'won', label: resolveStatsText('stepWon'), count: 0, rate: 0 },
],
channels: [
{ source: 'nfc', label: resolveStatsText('sourceNfc'), leadsCount: 0, wonCount: 0, conversionRate: 0 },
{ source: 'qr', label: resolveStatsText('sourceQr'), leadsCount: 0, wonCount: 0, conversionRate: 0 },
{ source: 'link', label: resolveStatsText('sourceLink'), leadsCount: 0, wonCount: 0, conversionRate: 0 },
{ source: 'ai_chat', label: resolveStatsText('sourceAi'), leadsCount: 0, wonCount: 0, conversionRate: 0 },
],
loading: false,
errorMessage: '',
};
}
export function computeStatsFromLeads(leads: LeadEntity[], period: StatsPeriod): StatsState {
const state = createStatsState();
state.period = period;
let totalCount = 0;
let hotCount = 0;
let newCount = 0;
let contactedCount = 0;
let wonCount = 0;
const channelMap: Record<string, { leads: number; won: number }> = {
nfc: { leads: 0, won: 0 },
qr: { leads: 0, won: 0 },
link: { leads: 0, won: 0 },
ai_chat: { leads: 0, won: 0 },
};
for (let i = 0; i < leads.length; i++) {
const item = leads[i];
totalCount++;
if (item.status === 'hot' || item.intentScore >= 80) {
hotCount++;
}
if (item.status === 'new') {
newCount++;
} else if (item.status === 'contacted') {
contactedCount++;
} else if (item.status === 'won') {
wonCount++;
}
const src = item.source != null ? item.source : 'link';
if (channelMap[src] != null) {
channelMap[src].leads++;
if (item.status === 'won') {
channelMap[src].won++;
}
}
}
state.summary.totalLeads = totalCount;
state.summary.hotLeads = hotCount;
state.summary.wonLeads = wonCount;
state.summary.overallRate = calcConversionRate(totalCount, wonCount);
// 漏斗阶梯与转化率
state.funnel[0].count = totalCount;
state.funnel[0].rate = totalCount > 0 ? 100 : 0;
state.funnel[1].count = hotCount;
state.funnel[1].rate = calcFunnelStepRate(hotCount, totalCount);
state.funnel[2].count = contactedCount;
state.funnel[2].rate = calcFunnelStepRate(contactedCount, totalCount);
state.funnel[3].count = wonCount;
state.funnel[3].rate = calcFunnelStepRate(wonCount, totalCount);
// 渠道成效
state.channels[0].leadsCount = channelMap['nfc'].leads;
state.channels[0].wonCount = channelMap['nfc'].won;
state.channels[0].conversionRate = calcConversionRate(channelMap['nfc'].leads, channelMap['nfc'].won);
state.channels[1].leadsCount = channelMap['qr'].leads;
state.channels[1].wonCount = channelMap['qr'].won;
state.channels[1].conversionRate = calcConversionRate(channelMap['qr'].leads, channelMap['qr'].won);
state.channels[2].leadsCount = channelMap['link'].leads;
state.channels[2].wonCount = channelMap['link'].won;
state.channels[2].conversionRate = calcConversionRate(channelMap['link'].leads, channelMap['link'].won);
state.channels[3].leadsCount = channelMap['ai_chat'].leads;
state.channels[3].wonCount = channelMap['ai_chat'].won;
state.channels[3].conversionRate = calcConversionRate(channelMap['ai_chat'].leads, channelMap['ai_chat'].won);
return state;
}
export async function loadStatsData(state: StatsState): Promise<void> {
state.loading = true;
state.errorMessage = '';
try {
const query: OwnerPageQuery = {
page: 1,
size: 100,
};
const res = await fetchOwnerLeadPage(query);
const computed = computeStatsFromLeads(res.list, state.period);
state.summary = computed.summary;
state.funnel = computed.funnel;
state.channels = computed.channels;
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
state.errorMessage = msg !== '' ? msg : resolveStatsText('error');
} finally {
state.loading = false;
}
}