文件
OmniCard/mpuniapp/pages/owner/useLeadDetail.uts

445 行
10 KiB
Plaintext

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

/**
* 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<LeadEntity>;
fetchOwnerRadarPage?: (
query: OwnerPageQuery
) => Promise<OwnerPageData<RadarTrack>>;
};
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<string, string | number>
) => Promise<LeadEntity>;
updateOwnerLeadStatus?: (
id: string | number,
status: LeadStatus
) => Promise<LeadEntity>;
};
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<RadarTrack> | 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<LoadDetailResult> {
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<LeadEntity> {
const notes = appendNote(input.notes, input.next);
const post =
api != null && api.httpPost != null ? api.httpPost : httpPost;
const payload: Record<string, string | number> = {
id: input.id,
notes,
};
return post<LeadEntity>('/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<LoadDetailResult> {
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<boolean> {
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.reLaunch({
url: '/pages/owner/leads',
});
},
});
}