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

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

1191 行
30 KiB
Plaintext

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

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

<template>
<view class="exchange-page" :class="pageRtlClass" :style="pageTheme">
<!-- 顶部受控局部极光微光晕 (面积 < 8%) -->
<view class="ambient-aurora-glow"></view>
<!-- 顶部快捷返回栏 -->
<view class="exchange-top-bar">
<view class="top-back-btn" :aria-label="t('backPortal')" @click="handleBackPortal">
<text class="top-back-arrow">←</text>
<text class="top-back-text">{{ t('backPortal') }}</text>
</view>
</view>
<scroll-view class="exchange-scroll" :scroll-y="true">
<view class="exchange-content">
<!-- 1. Hero 身份与场景展台 -->
<view class="hero-stage">
<view class="hero-badge">
<view class="hero-badge-dot"></view>
<text class="hero-badge-text">{{ t('badge') }}</text>
</view>
<text class="hero-title">{{ t('title') }}</text>
<text class="hero-desc">{{ t('desc') }}</text>
</view>
<!-- 缺少名片标识阻断警告 -->
<view v-if="!state.canSubmit" class="exchange-warn">
<view class="warn-badge">
<text class="warn-badge-icon">!</text>
</view>
<text class="warn-text">{{ t('warnMissingId') }}</text>
</view>
<!-- 2. 留资成功反馈卡片 (Success State) -->
<view v-if="submitSuccess" class="success-card">
<view class="success-icon-wrap">
<view class="success-icon-ring"></view>
<view class="success-icon-tick"></view>
</view>
<text class="success-title">{{ t('successTitle') }}</text>
<text class="success-desc">{{ t('successDesc') }}</text>
<view class="success-summary">
<view class="summary-row">
<text class="summary-label">{{ t('nameLabel') }}:</text>
<text class="summary-val">{{ submittedName }}</text>
</view>
<view class="summary-row">
<text class="summary-label">{{ t('contactSectionTitle') }}:</text>
<text class="summary-val">{{ submittedContact }}</text>
</view>
<view v-if="submittedIntentLabel !== ''" class="summary-row">
<text class="summary-label">{{ t('intentTitle') }}:</text>
<text class="summary-val">{{ submittedIntentLabel }}</text>
</view>
</view>
<button class="primary-cta-btn" @click="handleBackPortal">
<text class="primary-cta-btn-text">{{ t('backPortal') }}</text>
</button>
<button class="secondary-action-btn" @click="handleResetAndAgain">
<text class="secondary-action-btn-text">{{ t('submitAgain') }}</text>
</button>
</view>
<!-- 3. Bento Grid 表单容器区 (Active Form) -->
<view v-else class="bento-container">
<!-- Bento 模组 A: AI 纸质名片拍照/选图快速识别入库 -->
<view class="bento-card ocr-card">
<view class="ocr-top-row">
<view class="ocr-identity">
<view class="ocr-ai-icon-box">
<text class="ocr-ai-icon-text">SCAN</text>
</view>
<view class="ocr-titles">
<text class="ocr-title">{{ t('ocrTitle') }}</text>
<text class="ocr-subtitle">{{ t('ocrSubtitle') }}</text>
</view>
</view>
<view class="ocr-ai-badge">
<text class="ocr-ai-badge-text">{{ t('badgeAi') }}</text>
</view>
</view>
<view class="ocr-action-row">
<button
class="ocr-btn"
:class="{ 'ocr-btn-disabled': ocrState.scanning }"
:disabled="ocrState.scanning"
:aria-label="ocrCtaLabel"
@click="handleOcrScan"
>
<view class="ocr-camera-icon">
<view class="ocr-camera-lens"></view>
</view>
<text class="ocr-btn-text">{{ ocrCtaLabel }}</text>
</button>
</view>
<!-- 已拍摄/上传名片缩略图预览与移除 -->
<view v-if="ocrState.imagePath !== ''" class="ocr-preview-card">
<view class="ocr-preview-thumb-wrap" @click="handlePreviewOcrImage">
<image class="ocr-preview-img" :src="ocrState.imagePath" mode="aspectFill" />
</view>
<view class="ocr-preview-info" @click="handlePreviewOcrImage">
<text class="ocr-preview-title">{{ t('ocrPreviewTitle') }}</text>
<text class="ocr-preview-hint">{{ t('ocrPreviewHint') }} ↗</text>
</view>
<view
class="ocr-preview-delete"
:aria-label="t('ocrDelete')"
@click="handleRemoveOcrImage"
>
<text class="ocr-preview-delete-text">{{ t('ocrDelete') }}</text>
</view>
</view>
</view>
<!-- Bento 模组 B: 核心商务联系信息 -->
<view class="bento-card contact-card">
<view class="card-section-head">
<text class="card-section-title">{{ t('contactSectionTitle') }}</text>
<text class="card-section-desc">{{ t('contactSectionDesc') }}</text>
</view>
<!-- 姓名 (必填) -->
<view class="field">
<view class="field-label-row">
<text class="field-label">{{ t('nameLabel') }}</text>
<text class="field-req-tag">{{ t('requiredMark') }}</text>
</view>
<input
class="field-input"
v-model="state.form.name"
:placeholder="t('namePlaceholder')"
:disabled="!state.canSubmit || state.submitting"
/>
</view>
<!-- 所属公司 (选填) -->
<view class="field">
<view class="field-label-row">
<text class="field-label">{{ t('companyLabel') }}</text>
</view>
<input
class="field-input"
v-model="state.form.company"
:placeholder="t('companyPlaceholder')"
:disabled="!state.canSubmit || state.submitting"
/>
</view>
<!-- WhatsApp (全球常用) -->
<view class="field">
<view class="field-label-row">
<text class="field-label">{{ t('whatsappLabel') }}</text>
<view class="wa-pill">
<text class="wa-pill-text">{{ t('whatsappBadge') }}</text>
</view>
</view>
<input
class="field-input"
type="tel"
v-model="state.form.phone"
:placeholder="t('whatsappPlaceholder')"
:disabled="!state.canSubmit || state.submitting"
/>
</view>
<!-- 邮箱 -->
<view class="field">
<view class="field-label-row">
<text class="field-label">{{ t('emailLabel') }}</text>
</view>
<input
class="field-input"
type="email"
v-model="state.form.email"
:placeholder="t('emailPlaceholder')"
:disabled="!state.canSubmit || state.submitting"
/>
</view>
<view class="contact-rule-tip">
<text class="contact-rule-icon">ⓘ</text>
<text class="contact-rule-text">{{ t('whatsappOrEmailTip') }}</text>
</view>
</view>
<!-- Bento 模组 C: 商务意向快捷选择与需求留言 -->
<view class="bento-card intent-card">
<view class="card-section-head">
<text class="card-section-title">{{ t('intentTitle') }}</text>
</view>
<!-- 快捷意向胶囊标签 -->
<view class="intent-chips-wrap">
<view
v-for="item in intentList"
:key="item.key"
class="intent-chip"
:class="{ 'intent-chip-active': selectedIntentKey === item.key }"
@click="handleSelectIntent(item)"
>
<text
class="intent-chip-text"
:class="{ 'intent-chip-text-active': selectedIntentKey === item.key }"
>{{ t(item.labelKey) }}</text>
</view>
</view>
<!-- 备注文本域 -->
<view class="field field-notes">
<text class="field-label">{{ t('notesLabel') }}</text>
<textarea
class="field-textarea"
v-model="state.form.notes"
:placeholder="t('notesPlaceholder')"
:disabled="!state.canSubmit || state.submitting"
/>
</view>
</view>
<!-- Bento 模组 D: 国际隐私合规与企业级安全背书 -->
<view class="bento-card privacy-card">
<view class="privacy-icon-box">
<view class="privacy-shield-badge">
<view class="shield-core-dot"></view>
</view>
</view>
<view class="privacy-text-wrap">
<text class="privacy-title">{{ t('privacyTitle') }}</text>
<text class="privacy-desc">{{ t('privacyDesc') }}</text>
</view>
</view>
</view>
<!-- 底部吸底主 CTA 留白防遮挡垫片 -->
<view v-if="!submitSuccess" class="exchange-bottom-spacer"></view>
</view>
</scroll-view>
<!-- 4. 单屏唯一主 CTA 悬浮底栏 -->
<view v-if="!submitSuccess" class="exchange-cta-bar">
<button
class="primary-cta-btn"
:class="{ 'primary-cta-btn-disabled': !state.canSubmit || state.submitting }"
:disabled="!state.canSubmit || state.submitting"
:aria-label="submitCtaLabel"
@click="handleSubmit"
>
<text class="primary-cta-btn-text">{{ submitCtaLabel }}</text>
</button>
</view>
</view>
</template>
<script setup lang="uts">
import { utsLightColors, utsRadius, utsSpacing, utsTypography } from '../../common/tokens.uts';
import { getCurrentLocale, isRtl } from '../../common/locale.uts';
import {
createExchangeState,
applyExchangeOptions,
submitExchange,
isExchangeSubmitSuccess,
resolveExchangeText,
ExchangePageOptions,
} from './useExchange.uts';
import {
createOcrScanState,
handleCardOcrUpload,
previewOcrImage,
removeOcrImage,
} from './useOcrExchange.uts';
type IntentItem = {
key: string;
labelKey: string;
};
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-float': utsTypography.shadowFloat,
'--oc-shadow': utsTypography.shadow,
'--oc-space-4': utsSpacing.space4,
'--oc-space-8': utsSpacing.space8,
'--oc-space-12': utsSpacing.space12,
'--oc-space-16': utsSpacing.space16,
'--oc-space-20': utsSpacing.space20,
'--oc-space-24': utsSpacing.space24,
'--oc-space-32': utsSpacing.space32,
};
const state = reactive(createExchangeState());
const ocrState = reactive(createOcrScanState());
const submitSuccess = ref<boolean>(false);
const submittedName = ref<string>('');
const submittedContact = ref<string>('');
const selectedIntentKey = ref<string>('');
const intentList: IntentItem[] = [
{ key: 'cooperate', labelKey: 'intentCooperate' },
{ key: 'purchase', labelKey: 'intentPurchase' },
{ key: 'inquiry', labelKey: 'intentInquiry' },
{ key: 'agent', labelKey: 'intentAgent' },
{ key: 'tech', labelKey: 'intentTech' },
];
const pageRtlClass = computed((): string => {
return isRtl(getCurrentLocale()) ? 'exchange-rtl' : '';
});
const ocrCtaLabel = computed((): string => {
return ocrState.scanning ? t('ocrScanning') : t('ocrScan');
});
const submitCtaLabel = computed((): string => {
return state.submitting ? t('submitting') : t('submit');
});
const submittedIntentLabel = computed((): string => {
if (selectedIntentKey.value === '') {
return '';
}
for (let i = 0; i < intentList.length; i++) {
if (intentList[i].key === selectedIntentKey.value) {
return t(intentList[i].labelKey);
}
}
return '';
});
function t(key: string): string {
return resolveExchangeText(key);
}
function handleSelectIntent(item: IntentItem): void {
const label = t(item.labelKey);
if (selectedIntentKey.value === item.key) {
selectedIntentKey.value = '';
return;
}
selectedIntentKey.value = item.key;
const currentNotes = state.form.notes.trim();
if (currentNotes === '') {
state.form.notes = `[${label}] `;
} else if (currentNotes.startsWith('[')) {
const endBracket = currentNotes.indexOf(']');
if (endBracket !== -1) {
state.form.notes = `[${label}]` + currentNotes.substring(endBracket + 1);
} else {
state.form.notes = `[${label}] ` + currentNotes;
}
} else {
state.form.notes = `[${label}] ` + currentNotes;
}
}
function handlePreviewOcrImage(): void {
previewOcrImage(ocrState);
}
function handleRemoveOcrImage(): void {
removeOcrImage(ocrState);
}
async function handleOcrScan(): Promise<void> {
if (ocrState.scanning) {
return;
}
await handleCardOcrUpload(state.form, ocrState);
}
async function handleSubmit(): Promise<void> {
if (!state.canSubmit || state.submitting) {
return;
}
const nameCache = state.form.name;
const contactCache = state.form.phone !== '' ? state.form.phone : state.form.email;
const lead = await submitExchange(state);
// 仅接口明确成功(含 persisted===true 或有效落库 id)才展示完成态
if (isExchangeSubmitSuccess(lead)) {
submittedName.value = nameCache;
submittedContact.value = contactCache;
submitSuccess.value = true;
}
}
function handleResetAndAgain(): void {
submitSuccess.value = false;
selectedIntentKey.value = '';
submittedName.value = '';
submittedContact.value = '';
}
function handleBackPortal(): void {
// #ifndef UNI-APP-X-TEST
try {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
return;
}
} catch (_e) {}
const targetId = state.cardId !== '' ? state.cardId : state.slug;
const url = targetId !== '' ? `/pages/portal/index?id=${targetId}` : '/pages/portal/index';
uni.reLaunch({ url });
// #endif
}
onLoad((options: ExchangePageOptions) => {
applyExchangeOptions(state, options);
});
</script>
<style scoped>
.exchange-page {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--oc-bg, #F5F8FC);
box-sizing: border-box;
}
/* 顶部受控局部极光弥散微光晕 (< 8%) */
.ambient-aurora-glow {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 180px;
background: radial-gradient(circle at 50% -20%, rgba(0, 210, 255, 0.12) 0%, rgba(0, 136, 255, 0.06) 45%, rgba(99, 102, 241, 0) 100%);
pointer-events: none;
z-index: 0;
}
/* RTL 镜像适配 */
.exchange-rtl {
direction: rtl;
text-align: right;
}
.exchange-rtl .top-back-arrow {
transform: scaleX(-1);
}
.exchange-rtl .hero-stage,
.exchange-rtl .card-section-head,
.exchange-rtl .ocr-titles,
.exchange-rtl .privacy-text-wrap {
text-align: right;
}
.exchange-rtl .ocr-preview-hint {
transform: scaleX(-1);
display: inline-block;
}
/* 顶部返回导航栏 */
.exchange-top-bar {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
min-height: 44px;
padding-inline: 16px;
background-color: transparent;
z-index: 10;
}
.top-back-btn {
display: flex;
flex-direction: row;
align-items: center;
min-height: 44px;
min-width: 44px;
cursor: pointer;
}
.top-back-arrow {
font-size: 16px;
font-weight: 600;
color: var(--oc-primary, #0088FF);
margin-inline-end: 4px;
}
.top-back-text {
font-size: 14px;
font-weight: 500;
color: var(--oc-primary, #0088FF);
}
/* 滚动主视图 */
.exchange-scroll {
position: relative;
flex: 1;
z-index: 1;
}
.exchange-content {
display: flex;
flex-direction: column;
padding-inline: 16px;
padding-block-start: 4px;
padding-block-end: 24px;
}
/* 1. Hero 展台 */
.hero-stage {
display: flex;
flex-direction: column;
margin-block-end: 16px;
padding-inline: 4px;
}
.hero-badge {
display: flex;
flex-direction: row;
align-items: center;
align-self: flex-start;
padding: 4px 12px;
background-color: var(--oc-primary-soft, #EBF5FF);
border: 1px solid rgba(0, 136, 255, 0.2);
border-radius: var(--oc-radius-pill, 9999px);
margin-block-end: 10px;
}
.hero-badge-dot {
width: 6px;
height: 6px;
border-radius: 3px;
background-color: var(--oc-primary, #0088FF);
margin-inline-end: 6px;
}
.hero-badge-text {
font-size: 12px;
font-weight: 600;
color: var(--oc-primary, #0088FF);
letter-spacing: 0.2px;
}
.hero-title {
font-size: 24px;
font-weight: 700;
color: var(--oc-text, #101828);
line-height: 1.25;
margin-block-end: 6px;
}
.hero-desc {
font-size: 14px;
color: var(--oc-text-2, #64748B);
line-height: 1.5;
}
/* 警示条 (缺少名片标识) */
.exchange-warn {
display: flex;
flex-direction: row;
align-items: center;
padding: 12px 16px;
background-color: #FFF9F0;
border: 1px solid var(--oc-warning, #F79009);
border-radius: var(--oc-radius-btn, 14px);
margin-block-end: 16px;
}
.warn-badge {
width: 22px;
height: 22px;
border-radius: 11px;
background-color: var(--oc-warning, #F79009);
display: flex;
align-items: center;
justify-content: center;
margin-inline-end: 10px;
}
.warn-badge-icon {
font-size: 12px;
font-weight: 700;
color: #FFFFFF;
}
.warn-text {
flex: 1;
font-size: 13px;
color: #B54708;
line-height: 1.4;
}
/* 2. 留资成功反馈卡片 */
.success-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 32px 20px;
background-color: var(--oc-surface, #FFFFFF);
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: var(--oc-radius-card, 20px);
box-shadow: var(--oc-shadow-card, 0 8px 30px rgba(0, 0, 0, 0.04));
margin-block-end: 24px;
text-align: center;
}
.success-icon-wrap {
position: relative;
width: 64px;
height: 64px;
border-radius: 32px;
background-color: rgba(18, 183, 106, 0.12);
display: flex;
align-items: center;
justify-content: center;
margin-block-end: 16px;
}
.success-icon-ring {
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border: 2px dashed rgba(18, 183, 106, 0.35);
border-radius: 36px;
}
.success-icon-tick {
width: 14px;
height: 24px;
border-right: 3px solid var(--oc-success, #12B76A);
border-bottom: 3px solid var(--oc-success, #12B76A);
transform: rotate(45deg);
margin-bottom: 6px;
}
.ocr-camera-icon {
width: 18px;
height: 14px;
border: 1.5px solid var(--oc-text, #101828);
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 6px;
}
.ocr-camera-lens {
width: 6px;
height: 6px;
border: 1.5px solid var(--oc-text, #101828);
border-radius: 9999px;
}
.success-title {
font-size: 20px;
font-weight: 700;
color: var(--oc-text, #101828);
margin-block-end: 8px;
}
.success-desc {
font-size: 14px;
color: var(--oc-text-2, #64748B);
line-height: 1.6;
margin-block-end: 20px;
}
.success-summary {
width: 100%;
padding: 16px;
background-color: var(--oc-bg, #F5F8FC);
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: 12px;
margin-block-end: 24px;
box-sizing: border-box;
}
.summary-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-block-end: 8px;
}
.summary-row:last-child {
margin-block-end: 0;
}
.summary-label {
font-size: 13px;
color: var(--oc-text-2, #64748B);
}
.summary-val {
font-size: 13px;
font-weight: 600;
color: var(--oc-text, #101828);
}
.secondary-action-btn {
margin-block-start: 12px;
height: 44px;
min-height: 44px;
background-color: transparent;
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: var(--oc-radius-btn, 14px);
display: flex;
align-items: center;
justify-content: center;
width: 100%;
cursor: pointer;
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.secondary-action-btn:active {
opacity: 0.8;
}
.secondary-action-btn-text {
font-size: 14px;
font-weight: 500;
color: var(--oc-text-2, #64748B);
}
/* 3. Bento Grid 容器 */
.bento-container {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Bento 卡片基础样式 */
.bento-card {
background-color: var(--oc-surface, #FFFFFF);
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: var(--oc-radius-card, 20px);
box-shadow: var(--oc-shadow-card, 0 8px 30px rgba(0, 0, 0, 0.04));
padding: 20px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
/* 模组 A: OCR 极速识别卡 */
.ocr-card {
background: linear-gradient(180deg, #FFFFFF 0%, #FAFCFF 100%);
border: 1px solid rgba(0, 136, 255, 0.15);
}
.ocr-top-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-block-end: 14px;
}
.ocr-identity {
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
}
.ocr-ai-icon-box {
width: 36px;
height: 36px;
border-radius: 10px;
background: var(--oc-primary-gradient, linear-gradient(135deg, #00A3FF 0%, #0B63E5 100%));
display: flex;
align-items: center;
justify-content: center;
margin-inline-end: 10px;
}
.ocr-ai-icon-text {
font-size: 13px;
font-weight: 800;
color: #FFFFFF;
}
.ocr-titles {
display: flex;
flex-direction: column;
}
.ocr-title {
font-size: 15px;
font-weight: 700;
color: var(--oc-text, #101828);
margin-block-end: 2px;
}
.ocr-subtitle {
font-size: 12px;
color: var(--oc-text-2, #64748B);
}
.ocr-ai-badge {
padding: 4px 8px;
background-color: var(--oc-primary-soft, #EBF5FF);
border-radius: 6px;
}
.ocr-ai-badge-text {
font-size: 11px;
font-weight: 600;
color: var(--oc-primary, #0088FF);
}
.ocr-action-row {
display: flex;
flex-direction: row;
}
.ocr-btn {
height: 44px;
min-height: 44px;
padding: 0 16px;
background-color: var(--oc-primary-soft, #EBF5FF);
border: 1px solid var(--oc-primary, #0088FF);
border-radius: var(--oc-radius-btn, 14px);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
cursor: pointer;
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ocr-btn:active {
opacity: 0.85;
}
.ocr-btn-disabled {
opacity: 0.6;
}
.ocr-btn-symbol {
font-size: 15px;
margin-inline-end: 6px;
}
.ocr-btn-text {
font-size: 15px;
font-weight: 600;
color: var(--oc-primary, #0088FF);
}
/* 缩略图预览条 */
.ocr-preview-card {
display: flex;
flex-direction: row;
align-items: center;
padding: 8px 12px;
background-color: #FFFFFF;
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: 12px;
margin-block-start: 12px;
}
.ocr-preview-thumb-wrap {
width: 48px;
height: 48px;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--oc-border, #EBF1F7);
margin-inline-end: 12px;
cursor: pointer;
}
.ocr-preview-img {
width: 100%;
height: 100%;
}
.ocr-preview-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
cursor: pointer;
}
.ocr-preview-title {
font-size: 13px;
font-weight: 600;
color: var(--oc-text, #101828);
}
.ocr-preview-hint {
font-size: 11px;
color: var(--oc-primary, #0088FF);
margin-block-start: 2px;
}
.ocr-preview-delete {
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
padding-inline: 8px;
cursor: pointer;
}
.ocr-preview-delete-text {
font-size: 12px;
color: var(--oc-error, #F04438);
}
/* 模组 B: 联系人表单卡 */
.card-section-head {
display: flex;
flex-direction: column;
margin-block-end: 14px;
}
.card-section-title {
font-size: 16px;
font-weight: 700;
color: var(--oc-text, #101828);
margin-block-end: 2px;
}
.card-section-desc {
font-size: 12px;
color: var(--oc-text-2, #64748B);
}
.field {
display: flex;
flex-direction: column;
margin-block-end: 14px;
}
.field-label-row {
display: flex;
flex-direction: row;
align-items: center;
margin-block-end: 6px;
}
.field-label {
font-size: 13px;
font-weight: 600;
color: var(--oc-text, #101828);
}
.field-req-tag {
font-size: 13px;
font-weight: 700;
color: var(--oc-error, #F04438);
margin-inline-start: 4px;
}
.wa-pill {
padding: 2px 8px;
background-color: rgba(18, 183, 106, 0.1);
border-radius: var(--oc-radius-pill, 9999px);
margin-inline-start: 6px;
}
.wa-pill-text {
font-size: 11px;
font-weight: 600;
color: var(--oc-success, #12B76A);
}
.field-input {
height: 44px;
min-height: 44px;
padding-inline: 14px;
background-color: var(--oc-bg, #F5F8FC);
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: var(--oc-radius-btn, 14px);
font-size: 15px;
color: var(--oc-text, #101828);
box-sizing: border-box;
}
.contact-rule-tip {
display: flex;
flex-direction: row;
align-items: center;
padding: 8px 12px;
background-color: var(--oc-primary-soft, #EBF5FF);
border-radius: 8px;
margin-block-start: 2px;
}
.contact-rule-icon {
font-size: 13px;
color: var(--oc-primary, #0088FF);
margin-inline-end: 6px;
}
.contact-rule-text {
font-size: 12px;
color: var(--oc-primary, #0088FF);
}
/* 模组 C: 意向快捷标签 */
.intent-chips-wrap {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
margin-block-end: 14px;
}
.intent-chip {
padding: 8px 14px;
min-height: 36px;
background-color: var(--oc-bg, #F5F8FC);
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: var(--oc-radius-pill, 9999px);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.intent-chip:active {
opacity: 0.85;
}
.intent-chip-active {
background-color: var(--oc-primary-soft, #EBF5FF);
border-color: var(--oc-primary, #0088FF);
}
.intent-chip-text {
font-size: 13px;
font-weight: 500;
color: var(--oc-text-2, #64748B);
}
.intent-chip-text-active {
color: var(--oc-primary, #0088FF);
font-weight: 600;
}
.field-notes {
margin-block-end: 0;
}
.field-textarea {
min-height: 84px;
padding: 12px 14px;
background-color: var(--oc-bg, #F5F8FC);
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: var(--oc-radius-btn, 14px);
font-size: 14px;
color: var(--oc-text, #101828);
line-height: 1.5;
box-sizing: border-box;
}
/* 模组 D: 隐私承诺 */
.privacy-card {
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 16px;
background-color: rgba(245, 248, 252, 0.7);
border: 1px dashed var(--oc-border, #EBF1F7);
box-shadow: none;
}
.privacy-icon-box {
margin-inline-end: 10px;
padding-block-start: 1px;
}
.privacy-shield-badge {
width: 18px;
height: 18px;
border-radius: 5px;
background-color: var(--oc-primary-soft, #EBF5FF);
border: 1px solid var(--oc-primary, #0088FF);
display: flex;
align-items: center;
justify-content: center;
}
.shield-core-dot {
width: 6px;
height: 6px;
border-radius: 9999px;
background-color: var(--oc-primary, #0088FF);
}
.privacy-text-wrap {
flex: 1;
display: flex;
flex-direction: column;
}
.privacy-title {
font-size: 13px;
font-weight: 600;
color: var(--oc-text, #101828);
margin-block-end: 3px;
}
.privacy-desc {
font-size: 11px;
color: var(--oc-text-2, #64748B);
line-height: 1.5;
}
/* 底部防遮挡垫片 */
.exchange-bottom-spacer {
height: calc(88px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px));
}
/* 4. 单屏唯一主 CTA 悬浮底栏 */
.exchange-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-color: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(12px);
border-top: 1px solid var(--oc-border, #EBF1F7);
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.03);
box-sizing: border-box;
z-index: 100;
}
.primary-cta-btn {
height: 48px;
min-height: 44px;
background: var(--oc-primary-gradient, linear-gradient(135deg, #00A3FF 0%, #0B63E5 100%));
border-radius: var(--oc-radius-btn, 14px);
box-shadow: var(--oc-shadow-cta, 0 8px 20px rgba(0, 136, 255, 0.22));
display: flex;
align-items: center;
justify-content: center;
width: 100%;
border-width: 0;
cursor: pointer;
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.primary-cta-btn:active {
opacity: 0.85;
}
.primary-cta-btn-disabled {
background: #E3E7ED !important;
box-shadow: none !important;
cursor: not-allowed;
}
.primary-cta-btn-text {
color: #FFFFFF;
font-size: 16px;
font-weight: 600;
letter-spacing: 0.3px;
}
.primary-cta-btn-disabled .primary-cta-btn-text {
color: var(--oc-text-2, #64748B);
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
</style>