补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
1399 行
34 KiB
Plaintext
1399 行
34 KiB
Plaintext
<template>
|
||
<view class="lead-detail-page" :class="pageRtlClass" :style="pageTheme">
|
||
<!-- Ambient Aurora Top Subtle Glow -->
|
||
<view class="detail-aurora-glow"></view>
|
||
|
||
<view class="lead-detail-container">
|
||
<!-- Empty / Missing ID State -->
|
||
<view v-if="state.missingId || state.empty" class="lead-empty">
|
||
<view class="empty-icon-wrap">
|
||
<view class="empty-icon-dot"></view>
|
||
</view>
|
||
<text class="lead-empty-text">{{ emptyMessage }}</text>
|
||
<button class="primary-cta-btn" @click="handleBack">
|
||
<text class="primary-cta-btn-text">{{ backLabel }}</text>
|
||
</button>
|
||
</view>
|
||
|
||
<!-- Skeleton Loading State -->
|
||
<view v-else-if="state.loading" class="lead-skeleton">
|
||
<view class="skeleton-line skeleton-title"></view>
|
||
<view class="skeleton-card"></view>
|
||
<view class="skeleton-card"></view>
|
||
<view class="skeleton-card"></view>
|
||
</view>
|
||
|
||
<!-- Main Body -->
|
||
<view v-else class="lead-body">
|
||
<!-- 1. Lead Profile Bento Card (Hero Executive Header) -->
|
||
<view class="lead-card lead-profile">
|
||
<view class="profile-hero-row">
|
||
<view class="lead-avatar-wrap">
|
||
<text class="lead-avatar-text">{{ leadInitial }}</text>
|
||
</view>
|
||
<view class="lead-main-info">
|
||
<view class="lead-name-row">
|
||
<text class="lead-name">{{ state.name }}</text>
|
||
<view v-if="sourceText !== ''" class="source-tag">
|
||
<text class="source-tag-text">{{ sourceText }}</text>
|
||
</view>
|
||
</view>
|
||
<text v-if="state.company !== ''" class="lead-company">{{ state.company }}</text>
|
||
</view>
|
||
<view class="lead-badge-col">
|
||
<view class="lead-score-pill" :class="{ 'lead-score-hot-pill': state.intentScore >= 80 }">
|
||
<text class="lead-score-text">{{ scoreLabel }} {{ state.intentScore }}</text>
|
||
</view>
|
||
<view class="lead-status-pill" :class="statusPillClass">
|
||
<text class="lead-status" :class="{ 'lead-status-hot': isHotStatus }">{{ statusText }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Modern Global Outreach Action Matrix (One-Tap Touchpoints) -->
|
||
<view v-if="hasOutreachOptions" class="lead-outreach-row">
|
||
<!-- WhatsApp Action (High Contrast Brand Green) -->
|
||
<view v-if="hasWhatsApp" class="outreach-pill whatsapp-pill" @click="handleWhatsApp">
|
||
<view class="outreach-dot bg-whatsapp"></view>
|
||
<text class="outreach-pill-text whatsapp-text">{{ whatsappLabel }}</text>
|
||
</view>
|
||
|
||
<!-- Phone Call Action -->
|
||
<view v-if="phoneText !== ''" class="outreach-pill phone-pill" @click="handleCallPhone">
|
||
<view class="outreach-dot bg-phone"></view>
|
||
<text class="outreach-pill-text">{{ phoneLabel }}: {{ phoneText }}</text>
|
||
</view>
|
||
|
||
<!-- Save to Phone Contacts (vCard) -->
|
||
<view class="outreach-pill contact-pill" @click="handleSaveContact">
|
||
<view class="outreach-dot bg-contact"></view>
|
||
<text class="outreach-pill-text contact-text">{{ saveContactLabel }}</text>
|
||
</view>
|
||
|
||
<!-- Copy Email Action -->
|
||
<view v-if="emailText !== ''" class="outreach-pill email-pill" @click="handleCopyEmail">
|
||
<view class="outreach-dot bg-email"></view>
|
||
<text class="outreach-pill-text">{{ emailLabel }}: {{ emailText }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 2. Commercial Stage Advancement Bento Card (Stepper Flow) -->
|
||
<view class="lead-card stage-card">
|
||
<view class="card-header-row">
|
||
<view class="card-title-group">
|
||
<view class="card-indicator bg-brand"></view>
|
||
<text class="section-title">{{ stageTitleLabel }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="stage-stepper">
|
||
<view
|
||
class="stage-pill"
|
||
:class="{ 'stage-pill-active': state.status === 'new', 'stage-pill-active-new': state.status === 'new' }"
|
||
@click="handleStageChange('new')"
|
||
>
|
||
<text class="stage-pill-text">{{ statusLabelOf('new') }}</text>
|
||
</view>
|
||
<view
|
||
class="stage-pill"
|
||
:class="{ 'stage-pill-active': state.status === 'contacted', 'stage-pill-active-contacted': state.status === 'contacted' }"
|
||
@click="handleStageChange('contacted')"
|
||
>
|
||
<text class="stage-pill-text">{{ statusLabelOf('contacted') }}</text>
|
||
</view>
|
||
<view
|
||
class="stage-pill"
|
||
:class="{ 'stage-pill-active': state.status === 'hot', 'stage-pill-active-hot': state.status === 'hot' }"
|
||
@click="handleStageChange('hot')"
|
||
>
|
||
<text class="stage-pill-text">{{ statusLabelOf('hot') }}</text>
|
||
</view>
|
||
<view
|
||
class="stage-pill"
|
||
:class="{ 'stage-pill-active': state.status === 'won', 'stage-pill-active-won': state.status === 'won' }"
|
||
@click="handleStageChange('won')"
|
||
>
|
||
<text class="stage-pill-text">{{ statusLabelOf('won') }}</text>
|
||
</view>
|
||
<view
|
||
class="stage-pill"
|
||
:class="{ 'stage-pill-active': state.status === 'archived', 'stage-pill-active-archived': state.status === 'archived' }"
|
||
@click="handleStageChange('archived')"
|
||
>
|
||
<text class="stage-pill-text">{{ statusLabelOf('archived') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 3. Follow-up Script Bento Card -->
|
||
<view class="lead-card lead-section ai-card">
|
||
<view class="ai-header-row">
|
||
<view class="ai-identity">
|
||
<view class="ai-pulse-dot"></view>
|
||
<text class="ai-badge-text">{{ aiScriptTitle }}</text>
|
||
</view>
|
||
<view class="ai-gen-action" @click="handleGenerateAiScript">
|
||
<text class="ai-gen-btn-text">{{ generateAiLabel }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<text class="ai-script">{{ t('aiScript') }}</text>
|
||
|
||
<view v-if="state.aiScript !== ''" class="ai-result-box">
|
||
<text class="ai-result-text">{{ state.aiScript }}</text>
|
||
<view class="ai-result-actions">
|
||
<view class="ai-copy-btn" @click="handleCopyScript">
|
||
<text class="ai-copy-btn-text">{{ copyScriptLabel }}</text>
|
||
</view>
|
||
<view class="ai-copy-btn ai-apply-btn" @click="handleApplyAiScript">
|
||
<text class="ai-copy-btn-text">{{ applyScriptLabel }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 4. Visitor Radar Footprint Timeline Bento Card -->
|
||
<view class="lead-card lead-section timeline-card">
|
||
<view class="card-header-row">
|
||
<view class="card-title-group">
|
||
<view class="card-indicator bg-radar"></view>
|
||
<text class="section-title">{{ timelineLabel }}</text>
|
||
</view>
|
||
<view class="count-badge">
|
||
<text class="count-badge-text">{{ tracksCountText }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<list-view class="timeline" :scroll-y="true">
|
||
<list-item v-if="filteredTracks.length === 0">
|
||
<view class="timeline-empty">
|
||
<text class="timeline-empty-text">{{ t('empty') }}</text>
|
||
</view>
|
||
</list-item>
|
||
<list-item
|
||
v-for="item in filteredTracks"
|
||
:key="item.id"
|
||
>
|
||
<view class="timeline-item">
|
||
<view class="timeline-header-line">
|
||
<view class="timeline-place-wrap">
|
||
<view class="timeline-dot"></view>
|
||
<text class="timeline-place">{{ formatPlace(item.country, item.city) }}</text>
|
||
</view>
|
||
<view class="timeline-touch-wrap">
|
||
<text class="timeline-touch">{{ item.touchType }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="timeline-metrics">
|
||
<text class="timeline-metric">{{ t('stay') }} {{ formatStayMs(item.stayDurationMs) }}</text>
|
||
<text class="timeline-metric-sep">·</text>
|
||
<text class="timeline-metric">{{ t('depth') }} {{ formatDepthPct(item.scrollDepth) }}</text>
|
||
</view>
|
||
</view>
|
||
</list-item>
|
||
</list-view>
|
||
</view>
|
||
|
||
<!-- 5. Follow-up Notes & Evidence Bento Card -->
|
||
<view class="lead-card lead-section notes-section">
|
||
<view class="card-header-row">
|
||
<view class="card-title-group">
|
||
<view class="card-indicator bg-notes"></view>
|
||
<text class="section-title">{{ notesLabel }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="parsedNotes.cleanText !== '' || parsedNotes.imageUrls.length > 0" class="notes-history-box">
|
||
<text class="notes-history-label">{{ historyLabel }}</text>
|
||
<text v-if="parsedNotes.cleanText !== ''" class="existing-notes">{{ parsedNotes.cleanText }}</text>
|
||
<view v-if="parsedNotes.imageUrls.length > 0" class="history-images-grid">
|
||
<image
|
||
v-for="(imgUrl, idx) in parsedNotes.imageUrls"
|
||
:key="idx"
|
||
class="history-thumb"
|
||
:src="imgUrl"
|
||
mode="aspectFill"
|
||
@click="handlePreviewImage(imgUrl, parsedNotes.imageUrls)"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="notes-compose-box">
|
||
<textarea
|
||
class="notes-input"
|
||
style="min-height: 88px;"
|
||
v-model="state.nextNote"
|
||
:placeholder="notesPlaceholder"
|
||
:disabled="state.saving"
|
||
/>
|
||
</view>
|
||
|
||
<view class="notes-attach-box">
|
||
<text class="upload-hint">{{ t('uploadHint') }}</text>
|
||
<view class="notes-attachments">
|
||
<image-uploader
|
||
v-model="state.attachments"
|
||
:multiple="true"
|
||
:maxCount="6"
|
||
:disabled="state.saving"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Error Feedback -->
|
||
<view v-if="state.errorMessage !== ''" class="lead-error">
|
||
<text class="lead-error-text">{{ state.errorMessage }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 6. Floating Primary CTA Bar (Mobile Safe Area) -->
|
||
<view v-if="!(state.missingId || state.empty || state.loading)" class="lead-cta-bar">
|
||
<view class="lead-cta-inner">
|
||
<button
|
||
class="primary-cta-btn"
|
||
:class="{ 'primary-cta-btn-disabled': state.saving }"
|
||
:disabled="state.saving"
|
||
@click="handleSave"
|
||
>
|
||
<text class="primary-cta-btn-text">{{ saveLabel }}</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { getCurrentLocale, getOmniText, isRtl } from '../../common/locale.uts';
|
||
import { utsLightColors, utsRadius, utsSpacing, utsTypography } from '../../common/tokens.uts';
|
||
import {
|
||
applyLeadDetailOptions,
|
||
callLeadPhone,
|
||
createLeadDetailState,
|
||
formatTrackDepth,
|
||
formatTrackPlace,
|
||
formatTrackStay,
|
||
formatWhatsAppUrl,
|
||
generateAiIcebreaker,
|
||
goBackFromLeadDetail,
|
||
openLeadWhatsApp,
|
||
parseNoteAttachments,
|
||
refreshLeadDetail,
|
||
resolveLeadDetailText,
|
||
saveContactToDevice,
|
||
statusLabelOf,
|
||
submitLeadNote,
|
||
updateLeadStage,
|
||
LeadDetailCopyKey,
|
||
LeadDetailPageOptions,
|
||
ParsedNoteResult,
|
||
} from './useLeadDetail.uts';
|
||
import { parseLeadStatus } from './useLeads.uts';
|
||
import { RadarTrack } from '../../common/types.uts';
|
||
import ImageUploader from '../../components/image-uploader/image-uploader.uvue';
|
||
|
||
const pageTheme: Record<string, string> = {
|
||
'--oc-primary': utsLightColors.primary,
|
||
'--oc-primary-gradient': utsLightColors.primaryGradient,
|
||
'--oc-primary-soft': utsLightColors.primarySoft,
|
||
'--oc-aurora': utsLightColors.aurora,
|
||
'--oc-bg': utsLightColors.bg,
|
||
'--oc-surface': utsLightColors.surface,
|
||
'--oc-border': utsLightColors.border,
|
||
'--oc-text': utsLightColors.text,
|
||
'--oc-text-2': utsLightColors.text2,
|
||
'--oc-success': utsLightColors.success,
|
||
'--oc-warning': utsLightColors.warning,
|
||
'--oc-error': utsLightColors.error,
|
||
'--oc-radius-btn': utsRadius.button,
|
||
'--oc-radius-card': utsRadius.card,
|
||
'--oc-radius-modal': utsRadius.modal,
|
||
'--oc-radius-pill': utsRadius.pill,
|
||
'--oc-shadow-card': utsTypography.shadowCard,
|
||
'--oc-shadow-cta': utsTypography.shadowCta,
|
||
'--oc-shadow': utsTypography.shadow,
|
||
'--oc-space-8': utsSpacing.space8,
|
||
'--oc-space-12': utsSpacing.space12,
|
||
'--oc-space-16': utsSpacing.space16,
|
||
'--oc-space-24': utsSpacing.space24,
|
||
};
|
||
|
||
const state = reactive(createLeadDetailState());
|
||
const scoreLabel = ref(getOmniText(getCurrentLocale(), 'radar.lead_score'));
|
||
const timelineLabel = ref(getOmniText(getCurrentLocale(), 'radar.visitor_trace'));
|
||
const notesLabel = ref(getOmniText(getCurrentLocale(), 'crm.follow_up'));
|
||
const notesPlaceholder = ref(resolveLeadDetailText('notesPlaceholder'));
|
||
const backLabel = ref(resolveLeadDetailText('back'));
|
||
const stageTitleLabel = ref(resolveLeadDetailText('stageTitle'));
|
||
const historyLabel = ref(resolveLeadDetailText('historyTitle'));
|
||
const sourceTitleLabel = ref(resolveLeadDetailText('sourceLabel'));
|
||
const phoneLabel = ref(getOmniText(getCurrentLocale(), 'portal.phone'));
|
||
const emailLabel = ref(getOmniText(getCurrentLocale(), 'portal.email'));
|
||
const whatsappLabel = ref(resolveLeadDetailText('whatsappAction'));
|
||
const saveContactLabel = ref(resolveLeadDetailText('saveContactAction'));
|
||
const generateAiLabel = ref(resolveLeadDetailText('generateAiAction'));
|
||
const copyScriptLabel = ref(resolveLeadDetailText('copyScriptAction'));
|
||
const applyScriptLabel = ref(resolveLeadDetailText('applyScriptAction'));
|
||
|
||
const pageRtlClass = computed((): string => {
|
||
return isRtl(getCurrentLocale()) ? 'lead-rtl' : '';
|
||
});
|
||
|
||
const emptyMessage = computed((): string => {
|
||
if (state.missingId) {
|
||
return resolveLeadDetailText('missingId');
|
||
}
|
||
if (state.errorMessage !== '') {
|
||
return state.errorMessage;
|
||
}
|
||
return resolveLeadDetailText('empty');
|
||
});
|
||
|
||
const leadInitial = computed((): string => {
|
||
const trimmed = (state.name != null ? state.name : '').trim();
|
||
if (trimmed === '') {
|
||
return '?';
|
||
}
|
||
return trimmed.charAt(0).toUpperCase();
|
||
});
|
||
|
||
const isHotStatus = computed((): boolean => {
|
||
return state.status === 'hot' || state.intentScore >= 80;
|
||
});
|
||
|
||
const statusText = computed((): string => {
|
||
return statusLabelOf(state.status);
|
||
});
|
||
|
||
const statusPillClass = computed((): string => {
|
||
if (state.status === 'hot') {
|
||
return 'lead-status-hot-pill';
|
||
}
|
||
if (state.status === 'won') {
|
||
return 'lead-status-won-pill';
|
||
}
|
||
if (state.status === 'contacted') {
|
||
return 'lead-status-contacted-pill';
|
||
}
|
||
return '';
|
||
});
|
||
|
||
const phoneText = computed((): string => {
|
||
return (state.phone != null ? state.phone : '').trim();
|
||
});
|
||
|
||
const emailText = computed((): string => {
|
||
return (state.email != null ? state.email : '').trim();
|
||
});
|
||
|
||
const sourceText = computed((): string => {
|
||
const src = (state.source != null ? state.source : '').trim();
|
||
if (src === 'nfc') {
|
||
return 'NFC';
|
||
}
|
||
if (src === 'qr') {
|
||
return 'QR';
|
||
}
|
||
if (src === 'link') {
|
||
return 'Link';
|
||
}
|
||
if (src === 'ai_chat') {
|
||
return 'AI Chat';
|
||
}
|
||
return src.toUpperCase();
|
||
});
|
||
|
||
const hasWhatsApp = computed((): boolean => {
|
||
return formatWhatsAppUrl(state.phone != null ? state.phone : '') !== '';
|
||
});
|
||
|
||
const hasOutreachOptions = computed((): boolean => {
|
||
return phoneText.value !== '' || emailText.value !== '' || hasWhatsApp.value;
|
||
});
|
||
|
||
const filteredTracks = computed((): RadarTrack[] => {
|
||
return state.tracks;
|
||
});
|
||
|
||
const tracksCountText = computed((): string => {
|
||
return `${state.tracks.length}`;
|
||
});
|
||
|
||
const saveLabel = computed((): string => {
|
||
if (state.saving) {
|
||
return resolveLeadDetailText('saving');
|
||
}
|
||
return resolveLeadDetailText('save');
|
||
});
|
||
|
||
const aiScriptTitle = computed((): string => {
|
||
return resolveLeadDetailText('aiScriptTitle');
|
||
});
|
||
|
||
function t(key: LeadDetailCopyKey): string {
|
||
return resolveLeadDetailText(key);
|
||
}
|
||
|
||
function formatPlace(country?: string, city?: string): string {
|
||
return formatTrackPlace(country, city);
|
||
}
|
||
|
||
function formatStayMs(ms: number): string {
|
||
return formatTrackStay(ms);
|
||
}
|
||
|
||
function formatDepthPct(depth: number): string {
|
||
return formatTrackDepth(depth);
|
||
}
|
||
|
||
function handleBack(): void {
|
||
goBackFromLeadDetail();
|
||
}
|
||
|
||
function handleCallPhone(): void {
|
||
callLeadPhone(state.phone);
|
||
}
|
||
|
||
function handleCopyEmail(): void {
|
||
if (emailText.value === '') {
|
||
return;
|
||
}
|
||
uni.setClipboardData({
|
||
data: emailText.value,
|
||
showToast: true,
|
||
});
|
||
}
|
||
|
||
function handleWhatsApp(): void {
|
||
openLeadWhatsApp(state.phone);
|
||
}
|
||
|
||
async function handleSaveContact(): Promise<void> {
|
||
const ok = await saveContactToDevice({
|
||
name: state.name,
|
||
company: state.company,
|
||
phone: state.phone,
|
||
email: state.email,
|
||
});
|
||
if (ok) {
|
||
uni.showToast({
|
||
title: resolveLeadDetailText('savedContactToast'),
|
||
icon: 'success',
|
||
});
|
||
}
|
||
}
|
||
|
||
async function handleStageChange(targetStatus: string): Promise<void> {
|
||
const parsed = parseLeadStatus(targetStatus);
|
||
if (parsed == null || parsed === state.status || state.saving) {
|
||
return;
|
||
}
|
||
await updateLeadStage(state, parsed);
|
||
}
|
||
|
||
function handleGenerateAiScript(): void {
|
||
if (state.aiGenerating) return;
|
||
state.aiGenerating = true;
|
||
state.aiScript = generateAiIcebreaker(state.lead, state.tracks, getCurrentLocale());
|
||
state.aiGenerating = false;
|
||
}
|
||
|
||
function handleCopyScript(): void {
|
||
if (state.aiScript === '') {
|
||
return;
|
||
}
|
||
uni.setClipboardData({
|
||
data: state.aiScript,
|
||
showToast: true,
|
||
});
|
||
}
|
||
|
||
async function handleSave(): Promise<void> {
|
||
const next = state.nextNote.trim();
|
||
const hasAttachments =
|
||
state.attachments != null && state.attachments.length > 0;
|
||
if (next === '' && !hasAttachments) {
|
||
uni.showToast({
|
||
title: resolveLeadDetailText('notesRequiredToast'),
|
||
icon: 'none',
|
||
});
|
||
return;
|
||
}
|
||
// 仅接口 code === 1000(submitLeadNote 已校验)时提示备忘已保存并清空输入
|
||
const ok = await submitLeadNote(state);
|
||
if (ok) {
|
||
uni.showToast({
|
||
title: resolveLeadDetailText('noteSavedToast'),
|
||
icon: 'none',
|
||
});
|
||
}
|
||
}
|
||
|
||
const parsedNotes = computed((): ParsedNoteResult => {
|
||
return parseNoteAttachments(state.existingNotes);
|
||
});
|
||
|
||
function handleApplyAiScript(): void {
|
||
if (state.aiScript === '') {
|
||
return;
|
||
}
|
||
state.nextNote =
|
||
state.nextNote.trim() !== ''
|
||
? `${state.nextNote}\n${state.aiScript}`
|
||
: state.aiScript;
|
||
uni.showToast({
|
||
title: resolveLeadDetailText('scriptCopiedToast'),
|
||
icon: 'none',
|
||
});
|
||
}
|
||
|
||
function handlePreviewImage(current: string, urls: string[]): void {
|
||
uni.previewImage({
|
||
current,
|
||
urls,
|
||
});
|
||
}
|
||
|
||
onLoad((options: LeadDetailPageOptions) => {
|
||
try {
|
||
uni.setNavigationBarTitle({
|
||
title: resolveLeadDetailText('leadTitle'),
|
||
});
|
||
} catch (e) {}
|
||
applyLeadDetailOptions(state, options);
|
||
refreshLeadDetail(state);
|
||
});
|
||
|
||
onShow(() => {
|
||
if (!state.missingId && state.id !== '') {
|
||
refreshLeadDetail(state);
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* Root Container */
|
||
.lead-detail-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
background-color: var(--oc-bg, #F5F8FC);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.lead-rtl {
|
||
direction: rtl;
|
||
}
|
||
|
||
/* Ambient Subtle Aurora Top Glow */
|
||
.detail-aurora-glow {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 150px;
|
||
background: radial-gradient(circle at 50% -20%, rgba(0, 163, 255, 0.12) 0%, rgba(99, 102, 241, 0.05) 55%, rgba(245, 248, 252, 0) 100%);
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
|
||
.lead-detail-container {
|
||
position: relative;
|
||
z-index: 1;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow-y: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
max-width: 480px;
|
||
width: 100%;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.ai-apply-btn {
|
||
margin-inline-start: 8px;
|
||
background-color: var(--oc-primary-soft, #EBF5FF);
|
||
}
|
||
|
||
.history-images-grid {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-block-start: 8px;
|
||
}
|
||
|
||
.history-thumb {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 8px;
|
||
background-color: #EBF1F7;
|
||
border: 1px solid var(--oc-border, #EBF1F7);
|
||
}
|
||
|
||
/* Empty State */
|
||
.lead-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 60px 24px;
|
||
}
|
||
|
||
.empty-icon-wrap {
|
||
width: 52px;
|
||
height: 52px;
|
||
border-radius: 26px;
|
||
background-color: var(--oc-surface, #F7F8FA);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-block-end: 16px;
|
||
}
|
||
|
||
.empty-icon-dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 6px;
|
||
background-color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
.lead-empty-text {
|
||
font-size: 15px;
|
||
color: var(--oc-text-2, #667085);
|
||
text-align: center;
|
||
margin-block-end: 24px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* Skeleton Loading */
|
||
.lead-skeleton {
|
||
padding: 16px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.skeleton-line {
|
||
height: 24px;
|
||
background-color: var(--oc-border, #E3E7ED);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.skeleton-title {
|
||
width: 50%;
|
||
margin-block-end: 6px;
|
||
}
|
||
|
||
.skeleton-card {
|
||
height: 120px;
|
||
background-color: var(--oc-border, #E3E7ED);
|
||
border-radius: var(--oc-radius-card, 20px);
|
||
}
|
||
|
||
/* Lead Body */
|
||
.lead-body {
|
||
padding-inline: var(--oc-space-16, 16px);
|
||
padding-block-start: var(--oc-space-16, 16px);
|
||
padding-block-end: calc(88px + env(safe-area-inset-bottom, 0px));
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* Generic Bento Card */
|
||
.lead-card {
|
||
background-color: var(--oc-surface, #FFFFFF);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: var(--oc-radius-card, 20px);
|
||
box-shadow: var(--oc-shadow-card, 0 8px 30px rgba(0, 0, 0, 0.04));
|
||
padding: 16px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* 1. Profile Hero Bento Card */
|
||
.lead-profile {
|
||
background: linear-gradient(150deg, #FFFFFF 0%, #F9FBFE 100%);
|
||
}
|
||
|
||
.profile-hero-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-block-end: 14px;
|
||
}
|
||
|
||
.lead-avatar-wrap {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 24px;
|
||
background: linear-gradient(135deg, #EBF5FF 0%, #D8EBFF 100%);
|
||
border: 1px solid #B2DDFF;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-inline-end: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.lead-avatar-text {
|
||
font-size: 18px;
|
||
font-weight: 800;
|
||
color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.lead-main-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.lead-name-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
margin-block-end: 3px;
|
||
}
|
||
|
||
.lead-name {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: var(--oc-text, #101828);
|
||
line-height: 1.25;
|
||
margin-inline-end: 6px;
|
||
}
|
||
|
||
.source-tag {
|
||
background-color: var(--oc-surface, #F7F8FA);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: 4px;
|
||
padding-inline: 5px;
|
||
padding-block: 1px;
|
||
}
|
||
|
||
.source-tag-text {
|
||
font-size: 9px;
|
||
font-weight: 700;
|
||
color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
.lead-company {
|
||
font-size: 13px;
|
||
color: var(--oc-text-2, #667085);
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.lead-badge-col {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.lead-score-pill {
|
||
padding-inline: 8px;
|
||
padding-block: 2px;
|
||
border-radius: 9999px;
|
||
background-color: var(--oc-surface, #F7F8FA);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
}
|
||
|
||
.lead-score-hot-pill {
|
||
background-color: #FEE4E2;
|
||
border-color: #FECDCA;
|
||
}
|
||
|
||
.lead-score-text {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
.lead-score-hot-pill .lead-score-text {
|
||
color: #F04438;
|
||
}
|
||
|
||
.lead-status-pill {
|
||
padding-inline: 8px;
|
||
padding-block: 2px;
|
||
border-radius: 9999px;
|
||
background-color: var(--oc-primary-soft, #EBF5FF);
|
||
border: 1px solid #B2DDFF;
|
||
}
|
||
|
||
.lead-status-hot-pill {
|
||
background-color: #FFF1F0;
|
||
border-color: #FFA39E;
|
||
}
|
||
|
||
.lead-status-won-pill {
|
||
background-color: #ECFDF3;
|
||
border-color: #A6F4C5;
|
||
}
|
||
|
||
.lead-status-contacted-pill {
|
||
background-color: #FFFAEB;
|
||
border-color: #FEDF89;
|
||
}
|
||
|
||
.lead-status {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.lead-status-hot {
|
||
color: #F04438;
|
||
}
|
||
|
||
/* Outreach Action Matrix */
|
||
.lead-outreach-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
padding-block-start: 10px;
|
||
border-top: 1px solid var(--oc-border, #E3E7ED);
|
||
}
|
||
|
||
.outreach-pill {
|
||
height: 34px;
|
||
min-height: 34px;
|
||
padding-inline: 12px;
|
||
border-radius: 9999px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
background-color: var(--oc-surface, #F7F8FA);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
}
|
||
|
||
.outreach-pill:active {
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.outreach-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 3px;
|
||
margin-inline-end: 6px;
|
||
}
|
||
|
||
.bg-whatsapp {
|
||
background-color: #12B76A;
|
||
}
|
||
|
||
.bg-phone {
|
||
background-color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.bg-contact {
|
||
background-color: #0E9384;
|
||
}
|
||
|
||
.bg-email {
|
||
background-color: #667085;
|
||
}
|
||
|
||
.outreach-pill-text {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--oc-text, #101828);
|
||
}
|
||
|
||
.whatsapp-pill {
|
||
background-color: #ECFDF3;
|
||
border-color: #A6F4C5;
|
||
}
|
||
|
||
.whatsapp-text {
|
||
color: #027A48;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.contact-pill {
|
||
background-color: #F0FDF9;
|
||
border-color: #B5EADB;
|
||
}
|
||
|
||
.contact-text {
|
||
color: #0E9384;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 2. Commercial Stage Advancement Card */
|
||
.card-header-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-block-end: 12px;
|
||
}
|
||
|
||
.card-title-group {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.card-indicator {
|
||
width: 6px;
|
||
height: 14px;
|
||
border-radius: 3px;
|
||
margin-inline-end: 8px;
|
||
}
|
||
|
||
.bg-brand {
|
||
background-color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.bg-radar {
|
||
background-color: #0E9384;
|
||
}
|
||
|
||
.bg-notes {
|
||
background-color: #7F56D9;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: var(--oc-text, #101828);
|
||
}
|
||
|
||
.stage-stepper {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 6px;
|
||
overflow-x: auto;
|
||
scrollbar-width: none;
|
||
padding-block: 2px;
|
||
}
|
||
|
||
.stage-pill {
|
||
flex: 1;
|
||
min-width: 58px;
|
||
height: 34px;
|
||
border-radius: 10px;
|
||
background-color: var(--oc-surface, #F7F8FA);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
}
|
||
|
||
.stage-pill:active {
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.stage-pill-text {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
.stage-pill-active {
|
||
font-weight: 700;
|
||
}
|
||
|
||
.stage-pill-active-new {
|
||
background-color: var(--oc-primary-soft, #EBF5FF);
|
||
border-color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.stage-pill-active-new .stage-pill-text {
|
||
color: var(--oc-primary, #0B5CFF);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.stage-pill-active-contacted {
|
||
background-color: #FFFAEB;
|
||
border-color: #F79009;
|
||
}
|
||
|
||
.stage-pill-active-contacted .stage-pill-text {
|
||
color: #B54708;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.stage-pill-active-hot {
|
||
background-color: #FFF1F0;
|
||
border-color: #F04438;
|
||
}
|
||
|
||
.stage-pill-active-hot .stage-pill-text {
|
||
color: #F04438;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.stage-pill-active-won {
|
||
background-color: #ECFDF3;
|
||
border-color: #12B76A;
|
||
}
|
||
|
||
.stage-pill-active-won .stage-pill-text {
|
||
color: #027A48;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.stage-pill-active-archived {
|
||
background-color: #F2F4F7;
|
||
border-color: #98A2B3;
|
||
}
|
||
|
||
.stage-pill-active-archived .stage-pill-text {
|
||
color: #475467;
|
||
font-weight: 700;
|
||
}
|
||
|
||
/* 3. AI Copilot Bento Card */
|
||
.ai-card {
|
||
background: linear-gradient(150deg, #FFFFFF 0%, #FAF8FE 100%);
|
||
border-color: #E0D7FE;
|
||
}
|
||
|
||
.ai-header-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-block-end: 8px;
|
||
}
|
||
|
||
.ai-identity {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.ai-pulse-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 4px;
|
||
background-color: #7F56D9;
|
||
margin-inline-end: 6px;
|
||
}
|
||
|
||
.ai-badge-text {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: #6941C6;
|
||
}
|
||
|
||
.ai-gen-action {
|
||
padding-inline: 10px;
|
||
height: 28px;
|
||
border-radius: 9999px;
|
||
background-color: #F4EBFF;
|
||
border: 1px solid #D6BBFB;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
}
|
||
|
||
.ai-gen-action:active {
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.ai-gen-btn-text {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: #6941C6;
|
||
}
|
||
|
||
.ai-script {
|
||
font-size: 12px;
|
||
color: var(--oc-text-2, #667085);
|
||
line-height: 1.4;
|
||
margin-block-end: 10px;
|
||
}
|
||
|
||
.ai-result-box {
|
||
padding: 12px;
|
||
background-color: #FFFFFF;
|
||
border: 1px solid #E0D7FE;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.ai-result-text {
|
||
font-size: 13px;
|
||
color: var(--oc-text, #101828);
|
||
line-height: 1.6;
|
||
margin-block-end: 8px;
|
||
}
|
||
|
||
.ai-result-actions {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.ai-copy-btn {
|
||
padding-inline: 10px;
|
||
height: 26px;
|
||
border-radius: 9999px;
|
||
background-color: var(--oc-primary-soft, #EBF5FF);
|
||
border: 1px solid #B2DDFF;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.ai-copy-btn-text {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
/* 4. Radar Footprint Timeline */
|
||
.timeline-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.count-badge {
|
||
padding-inline: 7px;
|
||
padding-block: 2px;
|
||
background-color: var(--oc-surface, #F7F8FA);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: 9999px;
|
||
}
|
||
|
||
.count-badge-text {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
.timeline {
|
||
max-height: 220px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.timeline-empty {
|
||
padding: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.timeline-empty-text {
|
||
font-size: 12px;
|
||
color: var(--oc-text-2, #667085);
|
||
font-style: italic;
|
||
}
|
||
|
||
.timeline-item {
|
||
padding: 10px 12px;
|
||
background-color: var(--oc-surface, #F7F8FA);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: 12px;
|
||
margin-block-end: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.timeline-header-line {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-block-end: 4px;
|
||
}
|
||
|
||
.timeline-place-wrap {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.timeline-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 3px;
|
||
background-color: #0E9384;
|
||
margin-inline-end: 6px;
|
||
}
|
||
|
||
.timeline-place {
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: var(--oc-text, #101828);
|
||
}
|
||
|
||
.timeline-touch-wrap {
|
||
background-color: #FFFFFF;
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: 4px;
|
||
padding-inline: 5px;
|
||
padding-block: 1px;
|
||
}
|
||
|
||
.timeline-touch {
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
.timeline-metrics {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.timeline-metric {
|
||
font-size: 11px;
|
||
color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
.timeline-metric-sep {
|
||
font-size: 11px;
|
||
color: var(--oc-border, #E3E7ED);
|
||
}
|
||
|
||
/* 5. Notes & Attachments */
|
||
.notes-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.notes-history-box {
|
||
padding: 10px 12px;
|
||
background-color: #F8FAFC;
|
||
border: 1px solid #E2E8F0;
|
||
border-radius: 12px;
|
||
margin-block-end: 12px;
|
||
}
|
||
|
||
.notes-history-label {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: var(--oc-text-2, #667085);
|
||
margin-block-end: 4px;
|
||
}
|
||
|
||
.existing-notes {
|
||
font-size: 13px;
|
||
color: var(--oc-text, #101828);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.notes-compose-box {
|
||
margin-block-end: 12px;
|
||
}
|
||
|
||
.notes-input {
|
||
width: 100%;
|
||
padding: 12px;
|
||
box-sizing: border-box;
|
||
background-color: var(--oc-surface, #F7F8FA);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: 14px;
|
||
font-size: 14px;
|
||
color: var(--oc-text, #101828);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.notes-attach-box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.upload-hint {
|
||
font-size: 11px;
|
||
color: var(--oc-text-2, #667085);
|
||
margin-block-end: 8px;
|
||
}
|
||
|
||
.notes-attachments {
|
||
display: flex;
|
||
}
|
||
|
||
.lead-error {
|
||
padding: 12px;
|
||
background-color: #FFF1F0;
|
||
border: 1px solid #FFA39E;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.lead-error-text {
|
||
font-size: 12px;
|
||
color: #F04438;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 6. Fixed Floating CTA Bar */
|
||
.lead-cta-bar {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: var(--window-bottom, 0px);
|
||
padding: 12px 16px calc(16px + env(safe-area-inset-bottom, 0px));
|
||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.9) 0%, #FFFFFF 100%);
|
||
border-top: 1px solid var(--oc-border, #E3E7ED);
|
||
box-sizing: border-box;
|
||
z-index: 100;
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.lead-cta-inner {
|
||
display: flex;
|
||
width: 100%;
|
||
}
|
||
|
||
.primary-cta-btn {
|
||
height: 48px;
|
||
min-height: 44px;
|
||
width: 100%;
|
||
border-width: 0;
|
||
border-radius: 9999px;
|
||
background-color: var(--oc-primary, #0B5CFF);
|
||
background-image: var(--oc-primary-gradient, linear-gradient(135deg, #00A3FF 0%, #0B63E5 100%));
|
||
box-shadow: var(--oc-shadow-cta, 0 8px 20px rgba(0, 136, 255, 0.22));
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
}
|
||
|
||
.primary-cta-btn:active {
|
||
opacity: 0.88;
|
||
}
|
||
|
||
.primary-cta-btn-disabled {
|
||
background-color: var(--oc-border, #E3E7ED);
|
||
background-image: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.primary-cta-btn-text {
|
||
color: #FFFFFF;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.primary-cta-btn-disabled .primary-cta-btn-text {
|
||
color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
* {
|
||
animation: none !important;
|
||
transition: none !important;
|
||
}
|
||
}
|
||
</style>
|