refactor(crm): 潜客详情与跟进页面深度重构与出海触达优化
这个提交包含在:
@@ -88,12 +88,24 @@ type UseLeadDetailModule = {
|
||||
existing: string | null | undefined,
|
||||
next: string | null | undefined
|
||||
) => string;
|
||||
appendStructuredNote: (
|
||||
existing: string | null | undefined,
|
||||
next: string | null | undefined,
|
||||
attachments?: string[]
|
||||
) => string;
|
||||
formatWhatsAppUrl: (phone: string | null | undefined) => string;
|
||||
generateAiIcebreaker: (
|
||||
lead: LeadEntity | null,
|
||||
tracks: RadarTrack[],
|
||||
locale?: string
|
||||
) => string;
|
||||
saveNote: (
|
||||
input: {
|
||||
id: string | number;
|
||||
notes?: string;
|
||||
next: string;
|
||||
status?: LeadStatus;
|
||||
attachments?: string[];
|
||||
},
|
||||
api?: SaveNoteApi
|
||||
) => Promise<LeadEntity>;
|
||||
@@ -262,6 +274,42 @@ describe('OmniCard AI 名片主潜客详情业务逻辑 (useLeadDetail.uts)', ()
|
||||
expect(result.notes).toBe('Met at GITEX\nAsked for quote');
|
||||
expect(result.status).toBe('hot');
|
||||
});
|
||||
|
||||
it('3.3 updateLeadStage 调用 updateOwnerLeadStatus 推进商机阶段', async () => {
|
||||
const { updateLeadStage } = loadUseLeadDetail() as UseLeadDetailModule & {
|
||||
updateLeadStage: (
|
||||
state: Record<string, unknown>,
|
||||
newStatus: LeadStatus,
|
||||
api?: SaveNoteApi & {
|
||||
updateOwnerLeadStatus?: (
|
||||
id: string | number,
|
||||
status: LeadStatus
|
||||
) => Promise<LeadEntity>;
|
||||
}
|
||||
) => Promise<boolean>;
|
||||
};
|
||||
const lead = buildLead({ status: 'new' });
|
||||
const state: Record<string, unknown> = {
|
||||
leadId: 21,
|
||||
lead,
|
||||
status: 'new',
|
||||
saving: false,
|
||||
errorMessage: '',
|
||||
};
|
||||
const updateOwnerLeadStatus = jest.fn(
|
||||
async (id: string | number, status: LeadStatus) => {
|
||||
expect(id).toBe(21);
|
||||
expect(status).toBe('hot');
|
||||
return buildLead({ id, status });
|
||||
}
|
||||
);
|
||||
|
||||
const ok = await updateLeadStage(state, 'hot', { updateOwnerLeadStatus });
|
||||
expect(ok).toBe(true);
|
||||
expect(state.status).toBe('hot');
|
||||
expect(state.saving).toBe(false);
|
||||
expect(updateOwnerLeadStatus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('4. resolveLeadDetailText 只增文案', () => {
|
||||
@@ -272,12 +320,58 @@ describe('OmniCard AI 名片主潜客详情业务逻辑 (useLeadDetail.uts)', ()
|
||||
expect(resolveLeadDetailText('comingSoon')).toBe('即将提供');
|
||||
expect(resolveLeadDetailText('uploadHint').length).toBeGreaterThan(0);
|
||||
expect(resolveLeadDetailText('save').length).toBeGreaterThan(0);
|
||||
expect(resolveLeadDetailText('stageTitle').length).toBeGreaterThan(0);
|
||||
expect(resolveLeadDetailText('historyTitle').length).toBeGreaterThan(0);
|
||||
expect(resolveLeadDetailText('stageUpdated').length).toBeGreaterThan(0);
|
||||
expect(resolveLeadDetailText('copiedToast').length).toBeGreaterThan(0);
|
||||
expect(resolveLeadDetailText('whatsappAction').length).toBeGreaterThan(0);
|
||||
expect(resolveLeadDetailText('saveContactAction').length).toBeGreaterThan(0);
|
||||
expect(resolveLeadDetailText('generateAiAction').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('5. 销售触达与 AI 赋能函数', () => {
|
||||
it('5.1 formatWhatsAppUrl 清洗国际号码并生成 wa.me 链接', () => {
|
||||
const { formatWhatsAppUrl } = loadUseLeadDetail();
|
||||
|
||||
expect(formatWhatsAppUrl('+971-50-1234567')).toBe('https://wa.me/971501234567');
|
||||
expect(formatWhatsAppUrl('0086 138-0000-0000')).toBe('https://wa.me/8613800000000');
|
||||
expect(formatWhatsAppUrl('')).toBe('');
|
||||
expect(formatWhatsAppUrl(null)).toBe('');
|
||||
expect(formatWhatsAppUrl(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('5.2 generateAiIcebreaker 生成个性化破冰销售话术', () => {
|
||||
const { generateAiIcebreaker } = loadUseLeadDetail();
|
||||
const lead = buildLead({ name: 'Elena Rostova', company: 'Dubai Global Trading' });
|
||||
const tracks = [buildTrack({ viewedBlocks: ['case', 'pricing'] })];
|
||||
|
||||
const scriptZh = generateAiIcebreaker(lead, tracks, 'zh-CN');
|
||||
expect(scriptZh).toContain('Elena Rostova');
|
||||
expect(scriptZh.length).toBeGreaterThan(20);
|
||||
|
||||
const scriptEn = generateAiIcebreaker(lead, tracks, 'en-US');
|
||||
expect(scriptEn).toContain('Elena');
|
||||
expect(scriptEn.length).toBeGreaterThan(20);
|
||||
});
|
||||
|
||||
it('5.3 appendStructuredNote 结构化拼接备忘与附件链接', () => {
|
||||
const { appendStructuredNote } = loadUseLeadDetail();
|
||||
|
||||
const resWithAttach = appendStructuredNote(
|
||||
'Initial meeting',
|
||||
'Follow-up call agreed',
|
||||
['https://cdn.example.com/card.jpg']
|
||||
);
|
||||
expect(resWithAttach).toContain('Initial meeting');
|
||||
expect(resWithAttach).toContain('Follow-up call agreed');
|
||||
expect(resWithAttach).toContain('https://cdn.example.com/card.jpg');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('OmniCard AI 名片主潜客详情页契约 (leads-detail.uvue)', () => {
|
||||
it('页面存在,足迹用 list-view,备忘用 textarea,单一主 CTA', () => {
|
||||
it('页面存在,足迹用 list-view,备忘用 textarea,单一主 CTA,支持 WhatsApp 与 AI 破冰', () => {
|
||||
expect(fs.existsSync(DETAIL_PAGE)).toBe(true);
|
||||
expect(fs.existsSync(DETAIL_UTS)).toBe(true);
|
||||
|
||||
@@ -294,17 +388,24 @@ describe('OmniCard AI 名片主潜客详情页契约 (leads-detail.uvue)', () =>
|
||||
expect(pageSource.includes('<textarea')).toBe(true);
|
||||
expect(pageSource.includes('min-height: 88px')).toBe(true);
|
||||
expect(pageSource.includes('primary-cta-btn')).toBe(true);
|
||||
expect(template.includes('WhatsApp')).toBe(false);
|
||||
expect(pageSource.includes('calc(88px + env(safe-area-inset-bottom, 0px))')).toBe(
|
||||
true
|
||||
);
|
||||
expect(pageSource.includes('lead-status-hot')).toBe(true);
|
||||
expect(pageSource.includes('#F04438')).toBe(true);
|
||||
expect(pageSource.includes('aiScript')).toBe(true);
|
||||
expect(pageSource.includes('comingSoon')).toBe(true);
|
||||
expect(pageSource.includes('uploadHint')).toBe(true);
|
||||
expect(pageSource.includes('image-uploader')).toBe(true);
|
||||
expect(logicSource.includes('async function submitLeadNote')).toBe(true);
|
||||
expect(
|
||||
combined.includes('handleWhatsApp') || logicSource.includes('formatWhatsAppUrl')
|
||||
).toBe(true);
|
||||
expect(
|
||||
combined.includes('handleSaveContact') || logicSource.includes('saveContactToDevice')
|
||||
).toBe(true);
|
||||
expect(
|
||||
combined.includes('handleGenerateAiScript') || logicSource.includes('generateAiIcebreaker')
|
||||
).toBe(true);
|
||||
expect(
|
||||
combined.includes('fetchOwnerLeadInfo') || combined.includes('loadDetail')
|
||||
).toBe(true);
|
||||
|
||||
在新工单中引用
屏蔽一个用户