/** * OmniCard AI (CRMmp) - 名片主潜客详情与跟进备忘业务驱动层 * 拉线索详情 + 足迹分页,备忘只追加不覆盖 */ import { LeadEntity, LeadStatus, OwnerPageData, OwnerPageQuery, RadarTrack, } from '../../common/types.uts'; import { fetchOwnerLeadInfo, fetchOwnerRadarPage, } from '../../common/api.uts'; import { httpPost } from '../../common/request.uts'; import { getCurrentLocale, getOmniText } from '../../common/locale.uts'; import { statusLabel } from './useLeads.uts'; import { formatDepth, formatLocation, formatStay } from './useRadar.uts'; export type LeadDetailCopyKey = | 'title' | 'score' | 'timeline' | 'notes' | 'notesPlaceholder' | 'save' | 'back' | 'empty' | 'missingId' | 'error' | 'loading' | 'stay' | 'depth'; export type LeadDetailPageOptions = { id?: string; }; export type LoadDetailApi = { fetchOwnerLeadInfo?: (id: string | number) => Promise; fetchOwnerRadarPage?: ( query: OwnerPageQuery ) => Promise>; }; export type LoadDetailResult = { lead: LeadEntity | null; tracks: RadarTrack[]; empty: boolean; errorMessage: string; }; export type SaveNoteInput = { id: string | number; notes?: string; next: string; status?: LeadStatus; }; export type SaveNoteApi = { httpPost?: ( url: string, data: Record ) => Promise; updateOwnerLeadStatus?: ( id: string | number, status: LeadStatus ) => Promise; }; export type LeadDetailState = { rawId: string; leadId: string | number | null; lead: LeadEntity | null; name: string; company: string; status: string; intentScore: number; existingNotes: string; nextNote: string; attachments: string[]; tracks: RadarTrack[]; loading: boolean; saving: boolean; empty: boolean; missingId: boolean; errorMessage: string; }; const DEFAULT_PAGE = 1; const DEFAULT_SIZE = 20; function isZhLocale(): boolean { return getCurrentLocale() === 'zh-CN'; } /** * 页面文案:标题 / 意向 / 轨迹 / 保存走词典,缺 id 与占位走本地词典 */ export function resolveLeadDetailText(key: LeadDetailCopyKey): string { const locale = getCurrentLocale(); if (key === 'title') { return getOmniText(locale, 'crm.follow_up'); } if (key === 'score') { return getOmniText(locale, 'radar.lead_score'); } if (key === 'timeline') { return getOmniText(locale, 'radar.visitor_trace'); } if (key === 'notes') { return getOmniText(locale, 'crm.follow_up'); } if (key === 'save') { return getOmniText(locale, 'common.save'); } 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 === 'notesPlaceholder') { return zh ? '追加一条跟进备忘' : 'Add a follow-up note'; } if (key === 'back') { return zh ? '返回列表' : 'Back to list'; } if (key === 'missingId') { return zh ? '缺少潜客编号' : 'Lead ID is missing'; } if (key === 'stay') { return zh ? '停留' : 'Stay'; } if (key === 'depth') { return zh ? '滚动深度' : 'Scroll depth'; } return ''; } export function statusLabelOf(status: string): string { return statusLabel(status); } export function formatTrackStay(ms: number | null | undefined): string { return formatStay(ms); } export function formatTrackDepth(depth: number | null | undefined): string { return formatDepth(depth); } export function formatTrackPlace(country?: string, city?: string): string { return formatLocation(country, city); } /** * 解析为正整数。非正整数返回 null */ export function parseLeadId( raw: string | number | null | undefined ): string | number | null { if (raw == null) { return null; } if (typeof raw === 'number') { if (raw <= 0) { return null; } if (Math.floor(raw) !== raw) { return null; } return raw; } const text = String(raw).trim(); if (text === '') { return null; } if (!/^[1-9]\d*$/.test(text)) { return null; } return Number(text); } /** * 拼接备忘:新内容追加在旧内容后,空输入不覆盖 */ export function appendNote( existing: string | null | undefined, next: string | null | undefined ): string { const prev = existing != null ? String(existing).trim() : ''; const extra = next != null ? String(next).trim() : ''; if (prev === '') { return extra; } if (extra === '') { return prev; } return `${prev}\n${extra}`; } function readErrorMessage(err: unknown): string { if (err instanceof Error && err.message !== '') { return err.message; } return resolveLeadDetailText('error'); } function parseTrackList( payload: OwnerPageData | null | undefined ): RadarTrack[] { if (payload == null || payload.list == null) { return []; } return payload.list; } /** * 拉取潜客详情与足迹。可注入 fetchOwnerLeadInfo / fetchOwnerRadarPage */ export async function loadDetail( id: string | number | null | undefined, api?: LoadDetailApi ): Promise { const leadId = parseLeadId(id); if (leadId == null) { return { lead: null, tracks: [], empty: true, errorMessage: '', }; } const fetchInfo = api != null && api.fetchOwnerLeadInfo != null ? api.fetchOwnerLeadInfo : fetchOwnerLeadInfo; const fetchRadar = api != null && api.fetchOwnerRadarPage != null ? api.fetchOwnerRadarPage : fetchOwnerRadarPage; try { const lead = await fetchInfo(leadId); const visitorId = lead.visitorId != null ? String(lead.visitorId) : ''; const radarQuery: OwnerPageQuery = { page: DEFAULT_PAGE, size: DEFAULT_SIZE, keyWord: visitorId, cardId: lead.cardId, }; try { const payload = await fetchRadar(radarQuery); const tracks = parseTrackList(payload); return { lead, tracks, empty: false, errorMessage: '', }; } catch (radarErr) { return { lead, tracks: [], empty: false, errorMessage: readErrorMessage(radarErr), }; } } catch (err) { return { lead: null, tracks: [], empty: true, errorMessage: readErrorMessage(err), }; } } /** * 只更新 notes:POST /admin/crm/lead/update { id, notes } */ export async function saveNote( input: SaveNoteInput, api?: SaveNoteApi ): Promise { const notes = appendNote(input.notes, input.next); const post = api != null && api.httpPost != null ? api.httpPost : httpPost; const payload: Record = { id: input.id, notes, }; return post('/admin/crm/lead/update', payload); } export function createLeadDetailState(): LeadDetailState { return { rawId: '', leadId: null, lead: null, name: '', company: '', status: '', intentScore: 0, existingNotes: '', nextNote: '', attachments: [], tracks: [], loading: false, saving: false, empty: false, missingId: false, errorMessage: '', }; } export function applyLeadDetailOptions( state: LeadDetailState, options: LeadDetailPageOptions | null ): boolean { if (options == null || options.id == null) { state.rawId = ''; state.leadId = null; state.missingId = true; state.empty = true; return false; } const parsed = parseLeadId(options.id); state.rawId = String(options.id).trim(); state.leadId = parsed; state.missingId = parsed == null; state.empty = parsed == null; return parsed != null; } function applyLeadToState(state: LeadDetailState, lead: LeadEntity): void { state.lead = lead; state.name = lead.name; state.company = lead.company != null ? lead.company : ''; state.status = lead.status; state.intentScore = lead.intentScore; state.existingNotes = lead.notes != null ? lead.notes : ''; } export function applyLoadResult( state: LeadDetailState, result: LoadDetailResult ): void { if (result.lead != null) { applyLeadToState(state, result.lead); state.empty = false; state.missingId = false; } else { state.lead = null; state.empty = true; } state.tracks = result.tracks; state.errorMessage = result.errorMessage; } export async function refreshLeadDetail( state: LeadDetailState, api?: LoadDetailApi ): Promise { if (state.missingId || state.leadId == null) { const skipped: LoadDetailResult = { lead: null, tracks: [], empty: true, errorMessage: '', }; applyLoadResult(state, skipped); state.missingId = true; return skipped; } if (state.loading) { return { lead: state.lead, tracks: state.tracks, empty: state.empty, errorMessage: state.errorMessage, }; } state.loading = true; state.errorMessage = ''; const result = await loadDetail(state.leadId, api); applyLoadResult(state, result); state.loading = false; return result; } export async function submitLeadNote( state: LeadDetailState, api?: SaveNoteApi ): Promise { if (state.leadId == null || state.saving) { return false; } const next = state.nextNote.trim(); if (next === '') { return false; } state.saving = true; state.errorMessage = ''; try { const saved = await saveNote( { id: state.leadId, notes: state.existingNotes, next, status: state.lead != null ? state.lead.status : undefined, }, api ); applyLeadToState(state, saved); state.nextNote = ''; state.saving = false; return true; } catch (err) { state.saving = false; state.errorMessage = readErrorMessage(err); return false; } } export function goBackFromLeadDetail(): void { uni.navigateBack({ delta: 1, fail: () => { uni.switchTab({ url: '/pages/owner/leads', }); }, }); }