文件
OmniCard/mpuniapp/pages/owner/leads-detail.uvue
wikiboss 62e0ae4435 feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:01:47 +08:00

1399 行
34 KiB
Plaintext

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

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

<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>