refactor(crm): 潜客详情与跟进页面深度重构与出海触达优化
这个提交包含在:
文件差异内容过多而无法显示
加载差异
@@ -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,
|
||||
|
||||
在新工单中引用
屏蔽一个用户