refactor(crm): 潜客详情与跟进页面深度重构与出海触达优化

这个提交包含在:
wikiboss
2026-09-21 15:10:51 +08:00
父节点 daf385c8e0
当前提交 3c8049f686
共有 3 个文件被更改,包括 1161 次插入 和 158 次删除

查看文件

@@ -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);

文件差异内容过多而无法显示 加载差异

查看文件

@@ -13,6 +13,7 @@ import {
import {
fetchOwnerLeadInfo,
fetchOwnerRadarPage,
updateOwnerLeadStatus,
} from '../../common/api.uts';
import { httpPost } from '../../common/request.uts';
import { getCurrentLocale, getOmniText } from '../../common/locale.uts';
@@ -32,7 +33,22 @@ export type LeadDetailCopyKey =
| 'error'
| 'loading'
| 'stay'
| 'depth';
| 'depth'
| 'aiScript'
| 'comingSoon'
| 'uploadHint'
| 'stageTitle'
| 'historyTitle'
| 'stageUpdated'
| 'copiedToast'
| 'sourceLabel'
| 'whatsappAction'
| 'saveContactAction'
| 'generateAiAction'
| 'copyScriptAction'
| 'scriptCopiedToast'
| 'savedContactToast'
| 'leadTitle';
export type LeadDetailPageOptions = {
id?: string;
@@ -57,6 +73,7 @@ export type SaveNoteInput = {
notes?: string;
next: string;
status?: LeadStatus;
attachments?: string[];
};
export type SaveNoteApi = {
@@ -87,6 +104,8 @@ export type LeadDetailState = {
empty: boolean;
missingId: boolean;
errorMessage: string;
aiScript: string;
aiGenerating: boolean;
};
const DEFAULT_PAGE = 1;
@@ -142,6 +161,53 @@ export function resolveLeadDetailText(key: LeadDetailCopyKey): string {
if (key === 'depth') {
return zh ? '滚动深度' : 'Scroll depth';
}
if (key === 'aiScript') {
return zh
? '根据访问轨迹生成跟进话术'
: 'Talking points will be generated from this visit trail';
}
if (key === 'comingSoon') {
return zh ? '即将提供' : 'Coming soon';
}
if (key === 'uploadHint') {
return zh ? '可附加现场照片或名片图' : 'Add a meeting photo or card image';
}
if (key === 'stageTitle') {
return zh ? '商机阶段' : 'Pipeline Stage';
}
if (key === 'historyTitle') {
return zh ? '历史跟进纪要' : 'Follow-up History';
}
if (key === 'stageUpdated') {
return zh ? '阶段已更新' : 'Stage updated';
}
if (key === 'copiedToast') {
return zh ? '已复制到剪贴板' : 'Copied to clipboard';
}
if (key === 'sourceLabel') {
return zh ? '触达来源' : 'Source';
}
if (key === 'whatsappAction') {
return 'WhatsApp';
}
if (key === 'saveContactAction') {
return zh ? '保存到通讯录' : 'Save Contact';
}
if (key === 'generateAiAction') {
return zh ? '一键生成破冰话术' : 'Generate Script';
}
if (key === 'copyScriptAction') {
return zh ? '复制话术' : 'Copy Script';
}
if (key === 'scriptCopiedToast') {
return zh ? '话术已复制' : 'Script copied';
}
if (key === 'savedContactToast') {
return zh ? '联系人已保存' : 'Contact saved';
}
if (key === 'leadTitle') {
return zh ? '潜客档案' : 'Lead Profile';
}
return '';
}
@@ -207,6 +273,133 @@ export function appendNote(
return `${prev}\n${extra}`;
}
/**
* 清洗手机号并生成 WhatsApp 直达链接 (wa.me)
*/
export function formatWhatsAppUrl(rawPhone: string | null | undefined): string {
if (rawPhone == null) {
return '';
}
const trimmed = String(rawPhone).trim();
if (trimmed === '') {
return '';
}
let digits = trimmed.replace(/[^0-9]/g, '');
if (digits.startsWith('00')) {
digits = digits.slice(2);
}
if (digits === '') {
return '';
}
return `https://wa.me/${digits}`;
}
/**
* 智能破冰话术生成器 (基于客户姓名、公司、渠道与意向板块)
*/
export function generateAiIcebreaker(
lead: LeadEntity | null,
tracks: RadarTrack[],
locale?: string
): string {
if (lead == null) {
return '';
}
const currentLoc =
locale != null && locale !== '' ? locale : getCurrentLocale();
const isZh = currentLoc === 'zh-CN';
const name = lead.name.trim();
const company =
lead.company != null && lead.company.trim() !== ''
? lead.company.trim()
: '';
const source =
lead.source != null ? String(lead.source).toUpperCase() : 'OMNICARD';
let keyTopic = '';
if (tracks.length > 0) {
for (const track of tracks) {
if (track.viewedBlocks != null && track.viewedBlocks.length > 0) {
keyTopic = track.viewedBlocks[0];
break;
}
}
}
if (isZh) {
const companyPart = company !== '' ? `${company}的` : '';
const topicPart = keyTopic !== '' ? `与【${keyTopic}】` : '';
return `您好 ${name},我是来自 OmniCard 的商务顾问。注意到您此前通过 ${source} 触达并关注了我们${companyPart}智能数字名片${topicPart}方案。请问您近期是否有具体的数字化升级或商务展会名片需求?我可为您准备一份定制演示方案。`;
}
const firstWord = name.split(' ')[0];
const companyPart = company !== '' ? ` for ${company}` : '';
return `Hi ${firstWord}, this is the OmniCard team. Noticed you recently connected via ${source} and explored our digital identity solutions. Would love to see if you have any questions or if you'd like a quick tailored demo${companyPart}. Looking forward to connecting!`;
}
/**
* 结构化拼接备忘:将附件图片持久化嵌入备忘文本,避免数据丢失
*/
export function appendStructuredNote(
existing: string | null | undefined,
next: string | null | undefined,
attachments?: string[]
): string {
const prev = existing != null ? String(existing).trim() : '';
let extra = next != null ? String(next).trim() : '';
if (attachments != null && attachments.length > 0) {
const attachList = attachments.filter((url) => url.trim() !== '');
if (attachList.length > 0) {
const attachText = attachList.join(' ');
extra = extra !== '' ? `${extra}\n[${attachText}]` : `[${attachText}]`;
}
}
if (prev === '') {
return extra;
}
if (extra === '') {
return prev;
}
return `${prev}\n${extra}`;
}
/**
* 保存到手机系统通讯录 (vCard 兼容)
*/
export async function saveContactToDevice(
lead: LeadEntity | null
): Promise<boolean> {
if (lead == null) {
return false;
}
try {
if (typeof uni.addPhoneContact === 'function') {
uni.addPhoneContact({
firstName: lead.name,
organization: lead.company != null ? lead.company : '',
mobilePhoneNumber: lead.phone != null ? lead.phone : '',
email: lead.email != null ? lead.email : '',
success: () => {
uni.showToast({
title: resolveLeadDetailText('savedContactToast'),
icon: 'none',
});
},
fail: () => {
const contactText = `${lead.name} ${
lead.phone != null ? lead.phone : ''
} ${lead.email != null ? lead.email : ''}`.trim();
uni.setClipboardData({
data: contactText,
});
},
});
return true;
}
} catch (e) {}
return false;
}
function readErrorMessage(err: unknown): string {
if (err instanceof Error && err.message !== '') {
return err.message;
@@ -286,13 +479,17 @@ export async function loadDetail(
}
/**
* 只更新 notes:POST /admin/crm/lead/update { id, notes }
* 更新 notes(支持附件结构化嵌入):POST /admin/crm/lead/update { id, notes }
*/
export async function saveNote(
input: SaveNoteInput,
api?: SaveNoteApi
): Promise<LeadEntity> {
const notes = appendNote(input.notes, input.next);
const notes = appendStructuredNote(
input.notes,
input.next,
input.attachments
);
const post =
api != null && api.httpPost != null ? api.httpPost : httpPost;
const payload: Record<string, string | number> = {
@@ -320,6 +517,8 @@ export function createLeadDetailState(): LeadDetailState {
empty: false,
missingId: false,
errorMessage: '',
aiScript: '',
aiGenerating: false,
};
}
@@ -418,11 +617,13 @@ export async function submitLeadNote(
notes: state.existingNotes,
next,
status: state.lead != null ? state.lead.status : undefined,
attachments: state.attachments,
},
api
);
applyLeadToState(state, saved);
state.nextNote = '';
state.attachments = [];
state.saving = false;
return true;
} catch (err) {
@@ -432,6 +633,35 @@ export async function submitLeadNote(
}
}
export async function updateLeadStage(
state: LeadDetailState,
newStatus: LeadStatus,
api?: SaveNoteApi
): Promise<boolean> {
if (state.leadId == null || state.saving) {
return false;
}
const prevStatus = state.status;
state.status = newStatus;
state.saving = true;
state.errorMessage = '';
try {
const updateFn =
api != null && api.updateOwnerLeadStatus != null
? api.updateOwnerLeadStatus
: updateOwnerLeadStatus;
const updated = await updateFn(state.leadId, newStatus);
applyLeadToState(state, updated);
state.saving = false;
return true;
} catch (err) {
state.status = prevStatus;
state.saving = false;
state.errorMessage = readErrorMessage(err);
return false;
}
}
export function goBackFromLeadDetail(): void {
uni.navigateBack({
delta: 1,