补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
407 行
9.4 KiB
Plaintext
407 行
9.4 KiB
Plaintext
/**
|
||
* OmniCard AI (CRMmp) - 展会团队数据业务驱动层
|
||
* 并行汇总线索分页与雷达足迹,产出数字摘要
|
||
*/
|
||
|
||
import {
|
||
LeadEntity,
|
||
OwnerPageData,
|
||
OwnerPageQuery,
|
||
RadarTrack,
|
||
TeamRankItem,
|
||
} from '../../../common/types.uts';
|
||
import {
|
||
fetchCurrentEvent,
|
||
fetchOwnerLeadPage,
|
||
fetchOwnerRadarPage,
|
||
fetchTeamLiveRank,
|
||
} from '../../../common/api.uts';
|
||
import { getCurrentLocale, getOmniText } from '../../../common/locale.uts';
|
||
|
||
export type OverviewCopyKey =
|
||
| 'title'
|
||
| 'desc'
|
||
| 'totalLeads'
|
||
| 'hotCount'
|
||
| 'totalTracks'
|
||
| 'topCountry'
|
||
| 'recent'
|
||
| 'refresh'
|
||
| 'empty'
|
||
| 'error'
|
||
| 'loading'
|
||
| 'unknownCountry'
|
||
| 'rankTitle'
|
||
| 'liveBadge'
|
||
| 'syncTime'
|
||
| 'leadsUnit'
|
||
| 'touchesUnit'
|
||
| 'scoreUnit'
|
||
| 'championBadge'
|
||
| 'emptyLeads';
|
||
|
||
export type OverviewSummary = {
|
||
totalLeads: number;
|
||
hotCount: number;
|
||
totalTracks: number;
|
||
topCountry: string;
|
||
};
|
||
|
||
export type LoadOverviewInput = {
|
||
page?: number;
|
||
size?: number;
|
||
cardId?: string | number;
|
||
};
|
||
|
||
export type LoadOverviewApi = {
|
||
fetchOwnerLeadPage?: (
|
||
query: OwnerPageQuery
|
||
) => Promise<OwnerPageData<LeadEntity>>;
|
||
fetchOwnerRadarPage?: (
|
||
query: OwnerPageQuery
|
||
) => Promise<OwnerPageData<RadarTrack>>;
|
||
fetchTeamLiveRank?: (eventId?: string) => Promise<TeamRankItem[]>;
|
||
fetchCurrentEvent?: () => Promise<{ code?: string } | null>;
|
||
};
|
||
|
||
export type LoadOverviewResult = {
|
||
leads: LeadEntity[];
|
||
tracks: RadarTrack[];
|
||
ranks: TeamRankItem[];
|
||
summary: OverviewSummary;
|
||
empty: boolean;
|
||
errorMessage: string;
|
||
};
|
||
|
||
export type OverviewState = {
|
||
leads: LeadEntity[];
|
||
tracks: RadarTrack[];
|
||
ranks: TeamRankItem[];
|
||
summary: OverviewSummary;
|
||
loading: boolean;
|
||
empty: boolean;
|
||
errorMessage: string;
|
||
};
|
||
|
||
const DEFAULT_PAGE = 1;
|
||
const DEFAULT_SIZE = 20;
|
||
|
||
function isZhLocale(): boolean {
|
||
return getCurrentLocale() === 'zh-CN';
|
||
}
|
||
|
||
/**
|
||
* 页面文案:线索 / 足迹走词典,其余中英本地词典
|
||
*/
|
||
export function resolveOverviewText(key: OverviewCopyKey): string {
|
||
const locale = getCurrentLocale();
|
||
if (key === 'totalLeads') {
|
||
return getOmniText(locale, 'crm.leads');
|
||
}
|
||
if (key === 'totalTracks') {
|
||
return getOmniText(locale, 'radar.visitor_trace');
|
||
}
|
||
if (key === 'recent') {
|
||
return getOmniText(locale, 'crm.follow_up');
|
||
}
|
||
if (key === 'empty') {
|
||
return getOmniText(locale, 'common.empty');
|
||
}
|
||
if (key === 'error') {
|
||
return getOmniText(locale, 'common.error');
|
||
}
|
||
if (key === 'loading') {
|
||
return getOmniText(locale, 'common.loading');
|
||
}
|
||
|
||
const zh = isZhLocale();
|
||
if (key === 'title') {
|
||
return zh ? '展会数据' : 'Expo Data';
|
||
}
|
||
if (key === 'desc') {
|
||
return zh ? '汇总线索与足迹' : 'Leads and visitor trail at a glance';
|
||
}
|
||
if (key === 'hotCount') {
|
||
return zh ? '高意向' : 'Hot leads';
|
||
}
|
||
if (key === 'topCountry') {
|
||
return zh ? '主要国家' : 'Top country';
|
||
}
|
||
if (key === 'refresh') {
|
||
return zh ? '刷新数据' : 'Refresh Data';
|
||
}
|
||
if (key === 'unknownCountry') {
|
||
return zh ? '未标注' : 'Unspecified';
|
||
}
|
||
if (key === 'rankTitle') {
|
||
return zh ? '现场排行' : 'Floor ranking';
|
||
}
|
||
if (key === 'liveBadge') {
|
||
return zh ? '实时展会数据' : 'LIVE EXPO';
|
||
}
|
||
if (key === 'syncTime') {
|
||
return zh ? '已同步' : 'Synced';
|
||
}
|
||
if (key === 'leadsUnit') {
|
||
return zh ? '转化线索' : 'Leads';
|
||
}
|
||
if (key === 'touchesUnit') {
|
||
return zh ? '交互足迹' : 'Touches';
|
||
}
|
||
if (key === 'scoreUnit') {
|
||
return zh ? '分' : 'pts';
|
||
}
|
||
if (key === 'championBadge') {
|
||
return zh ? '冠军' : 'TOP 1';
|
||
}
|
||
if (key === 'emptyLeads') {
|
||
return zh ? '暂无新线索' : 'No new leads yet';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function normalizePage(raw: number | null | undefined): number {
|
||
if (raw == null || !Number.isFinite(raw) || raw < 1) {
|
||
return DEFAULT_PAGE;
|
||
}
|
||
return Math.floor(raw);
|
||
}
|
||
|
||
function normalizeSize(raw: number | null | undefined): number {
|
||
if (raw == null || !Number.isFinite(raw) || raw < 1) {
|
||
return DEFAULT_SIZE;
|
||
}
|
||
return Math.floor(raw);
|
||
}
|
||
|
||
function parseList<T>(payload: OwnerPageData<T> | null | undefined): T[] {
|
||
if (payload == null || payload.list == null) {
|
||
return [];
|
||
}
|
||
if (!Array.isArray(payload.list)) {
|
||
return [];
|
||
}
|
||
return payload.list;
|
||
}
|
||
|
||
function readErrorMessage(err: unknown): string {
|
||
if (err instanceof Error && err.message !== '') {
|
||
return err.message;
|
||
}
|
||
return resolveOverviewText('error');
|
||
}
|
||
|
||
function emptySummary(): OverviewSummary {
|
||
return {
|
||
totalLeads: 0,
|
||
hotCount: 0,
|
||
totalTracks: 0,
|
||
topCountry: '',
|
||
};
|
||
}
|
||
|
||
function pickTopCountry(tracks: RadarTrack[]): string {
|
||
const names: string[] = [];
|
||
const counts: number[] = [];
|
||
for (let i = 0; i < tracks.length; i++) {
|
||
const raw = tracks[i].country;
|
||
const country = raw != null ? String(raw).trim() : '';
|
||
if (country === '') {
|
||
continue;
|
||
}
|
||
let idx = -1;
|
||
for (let j = 0; j < names.length; j++) {
|
||
if (names[j] === country) {
|
||
idx = j;
|
||
break;
|
||
}
|
||
}
|
||
if (idx < 0) {
|
||
names.push(country);
|
||
counts.push(1);
|
||
} else {
|
||
counts[idx] = counts[idx] + 1;
|
||
}
|
||
}
|
||
let topCountry = '';
|
||
let topCount = 0;
|
||
for (let i = 0; i < names.length; i++) {
|
||
if (counts[i] > topCount) {
|
||
topCount = counts[i];
|
||
topCountry = names[i];
|
||
}
|
||
}
|
||
return topCountry;
|
||
}
|
||
|
||
/**
|
||
* 按线索与足迹列表汇总战报数字
|
||
*/
|
||
export function summarize(
|
||
leads: LeadEntity[],
|
||
tracks: RadarTrack[]
|
||
): OverviewSummary {
|
||
const safeLeads = leads != null ? leads : [];
|
||
const safeTracks = tracks != null ? tracks : [];
|
||
let hotCount = 0;
|
||
for (let i = 0; i < safeLeads.length; i++) {
|
||
if (safeLeads[i].status === 'hot') {
|
||
hotCount += 1;
|
||
}
|
||
}
|
||
return {
|
||
totalLeads: safeLeads.length,
|
||
hotCount,
|
||
totalTracks: safeTracks.length,
|
||
topCountry: pickTopCountry(safeTracks),
|
||
};
|
||
}
|
||
|
||
function buildQuery(input?: LoadOverviewInput): OwnerPageQuery {
|
||
const query: OwnerPageQuery = {
|
||
page: normalizePage(input != null ? input.page : null),
|
||
size: normalizeSize(input != null ? input.size : null),
|
||
};
|
||
if (input != null && input.cardId != null) {
|
||
query.cardId = input.cardId;
|
||
}
|
||
return query;
|
||
}
|
||
|
||
/**
|
||
* 并行拉取线索分页与雷达足迹。可注入两个 page API
|
||
*/
|
||
export async function loadOverview(
|
||
input?: LoadOverviewInput,
|
||
api?: LoadOverviewApi
|
||
): Promise<LoadOverviewResult> {
|
||
const query = buildQuery(input);
|
||
const fetchLead =
|
||
api != null && api.fetchOwnerLeadPage != null
|
||
? api.fetchOwnerLeadPage
|
||
: fetchOwnerLeadPage;
|
||
const fetchRadar =
|
||
api != null && api.fetchOwnerRadarPage != null
|
||
? api.fetchOwnerRadarPage
|
||
: fetchOwnerRadarPage;
|
||
|
||
try {
|
||
const pages = await Promise.all([fetchLead(query), fetchRadar(query)]);
|
||
const leads = parseList(pages[0]);
|
||
const tracks = parseList(pages[1]);
|
||
const summary = summarize(leads, tracks);
|
||
const ranks = await loadEventRanks(api);
|
||
return {
|
||
leads,
|
||
tracks,
|
||
ranks,
|
||
summary,
|
||
empty: leads.length === 0 && tracks.length === 0,
|
||
errorMessage: '',
|
||
};
|
||
} catch (err) {
|
||
return {
|
||
leads: [],
|
||
tracks: [],
|
||
ranks: [],
|
||
summary: emptySummary(),
|
||
empty: false,
|
||
errorMessage: readErrorMessage(err),
|
||
};
|
||
}
|
||
}
|
||
|
||
async function loadEventRanks(api?: LoadOverviewApi): Promise<TeamRankItem[]> {
|
||
const fetchRank =
|
||
api != null && api.fetchTeamLiveRank != null
|
||
? api.fetchTeamLiveRank
|
||
: api == null
|
||
? fetchTeamLiveRank
|
||
: null;
|
||
if (fetchRank == null) {
|
||
return [];
|
||
}
|
||
try {
|
||
let eventId = '';
|
||
const fetchEvent =
|
||
api != null && api.fetchCurrentEvent != null
|
||
? api.fetchCurrentEvent
|
||
: api == null
|
||
? fetchCurrentEvent
|
||
: null;
|
||
if (fetchEvent != null) {
|
||
const current = await fetchEvent();
|
||
if (current != null && current.code != null) {
|
||
eventId = current.code;
|
||
}
|
||
}
|
||
const list = await fetchRank(eventId);
|
||
return list != null ? list : [];
|
||
} catch (_err) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
export function createOverviewState(): OverviewState {
|
||
return {
|
||
leads: [],
|
||
tracks: [],
|
||
ranks: [],
|
||
summary: emptySummary(),
|
||
loading: false,
|
||
empty: true,
|
||
errorMessage: '',
|
||
};
|
||
}
|
||
|
||
export function applyOverviewResult(
|
||
state: OverviewState,
|
||
result: LoadOverviewResult
|
||
): void {
|
||
state.leads = result.leads;
|
||
state.tracks = result.tracks;
|
||
state.ranks = result.ranks;
|
||
state.summary = result.summary;
|
||
state.empty = result.empty;
|
||
state.errorMessage = result.errorMessage;
|
||
}
|
||
|
||
export function formatTopCountry(country: string): string {
|
||
if (country != null && country.trim() !== '') {
|
||
return country.trim();
|
||
}
|
||
return resolveOverviewText('unknownCountry');
|
||
}
|
||
|
||
/**
|
||
* 页面主 CTA:刷新战报
|
||
*/
|
||
export async function refreshOverview(
|
||
state: OverviewState,
|
||
api?: LoadOverviewApi
|
||
): Promise<LoadOverviewResult> {
|
||
if (state.loading) {
|
||
return {
|
||
leads: state.leads,
|
||
tracks: state.tracks,
|
||
ranks: state.ranks,
|
||
summary: state.summary,
|
||
empty: state.empty,
|
||
errorMessage: state.errorMessage,
|
||
};
|
||
}
|
||
state.loading = true;
|
||
state.errorMessage = '';
|
||
const result = await loadOverview({ page: DEFAULT_PAGE, size: DEFAULT_SIZE }, api);
|
||
applyOverviewResult(state, result);
|
||
state.loading = false;
|
||
if (result.errorMessage === '') {
|
||
try {
|
||
uni.vibrateShort({ type: 'light' });
|
||
} catch (_e) {
|
||
// 兼容不支持振动的端环境
|
||
}
|
||
}
|
||
return result;
|
||
}
|