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

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

1244 行
33 KiB
Plaintext

<template>
<view class="mode-page" :class="pageRtlClass" :style="pageTheme">
<!-- Ambient Aurora Glow Behind Header -->
<view class="mode-aurora-glow"></view>
<scroll-view class="mode-scroll" :scroll-y="true">
<view class="mode-content">
<!-- Physical NFC Card Simulation Stage -->
<view class="nfc-card-stage">
<view class="nfc-card-simulator" :class="'card-theme-' + state.selectedMode">
<view class="card-glass-shimmer"></view>
<view class="card-inner-content">
<!-- Top Row: Contactless Wave & Status -->
<view class="card-top-bar">
<view class="nfc-wave-group">
<view class="nfc-wave-arc arc-1"></view>
<view class="nfc-wave-arc arc-2"></view>
<view class="nfc-wave-arc arc-3"></view>
<text class="card-brand-label">OMNI NFC</text>
</view>
<view
class="card-status-badge"
:class="state.status === 'suspended' ? 'badge-suspended' : 'badge-active'"
>
<view class="status-pulse-dot"></view>
<text class="status-badge-text">{{ statusText }}</text>
</view>
</view>
<!-- Middle: Gold/Silver Microchip Graphic & Mode Badge -->
<view class="card-middle-row">
<view class="nfc-emv-chip">
<view class="chip-line chip-line-h"></view>
<view class="chip-line chip-line-v"></view>
<view class="chip-core-contact"></view>
</view>
<view class="card-scene-indicator">
<text class="card-scene-title">{{ selectedModeTitle }}</text>
<text class="card-scene-sub">{{ selectedModeSub }}</text>
</view>
</view>
<!-- Bottom Row: UID & Physical Material Tag -->
<view class="card-bottom-bar">
<view class="card-uid-block">
<text class="card-uid-label">CHIP UID</text>
<text class="card-uid-value">{{ displayUid }}</text>
</view>
<view class="card-material-tag">
<text class="card-material-text">{{ cardMaterialText }}</text>
</view>
</view>
</view>
</view>
<text class="card-stage-tip">{{ p('cardTouchHint') }}</text>
</view>
<!-- Bento Scene Mode Selector Grid -->
<view class="bento-section-header">
<text class="bento-section-title">{{ t('title') }}</text>
<text class="bento-section-desc">{{ t('desc') }}</text>
</view>
<view class="bento-scenes-container">
<!-- Business Bento Card -->
<view
class="bento-scene-card"
:class="{ 'bento-scene-active': state.selectedMode === 'business' }"
@click="handleSelect('business')"
>
<view class="scene-card-header">
<view class="scene-icon-wrap icon-expo">
<view class="icon-globe-ring"></view>
<view class="icon-globe-axis"></view>
</view>
<view class="scene-title-group">
<view class="scene-title-row">
<text class="scene-title-text">{{ t('business') }}</text>
<view v-if="state.sceneMode === 'business'" class="active-pill">
<text class="active-pill-text">{{ p('activeScenePill') }}</text>
</view>
</view>
<text class="scene-tag-text">{{ p('businessTag') }}</text>
</view>
<view
class="scene-radio-indicator"
:class="{ 'radio-checked': state.selectedMode === 'business' }"
>
<view v-if="state.selectedMode === 'business'" class="radio-dot"></view>
</view>
</view>
<text class="scene-desc-text">{{ p('businessDesc') }}</text>
<view class="scene-features-pills">
<view class="feature-pill">
<text class="feature-pill-text">{{ p('businessFeature1') }}</text>
</view>
<view class="feature-pill">
<text class="feature-pill-text">{{ p('businessFeature2') }}</text>
</view>
<view class="feature-pill">
<text class="feature-pill-text">{{ p('businessFeature3') }}</text>
</view>
</view>
</view>
<!-- Social Bento Card -->
<view
class="bento-scene-card"
:class="{ 'bento-scene-active': state.selectedMode === 'social' }"
@click="handleSelect('social')"
>
<view class="scene-card-header">
<view class="scene-icon-wrap icon-social">
<view class="icon-contact-card"></view>
<view class="icon-contact-badge"></view>
</view>
<view class="scene-title-group">
<view class="scene-title-row">
<text class="scene-title-text">{{ t('social') }}</text>
<view v-if="state.sceneMode === 'social'" class="active-pill">
<text class="active-pill-text">{{ p('activeScenePill') }}</text>
</view>
</view>
<text class="scene-tag-text">{{ p('socialTag') }}</text>
</view>
<view
class="scene-radio-indicator"
:class="{ 'radio-checked': state.selectedMode === 'social' }"
>
<view v-if="state.selectedMode === 'social'" class="radio-dot"></view>
</view>
</view>
<text class="scene-desc-text">{{ p('socialDesc') }}</text>
<view class="scene-features-pills">
<view class="feature-pill">
<text class="feature-pill-text">{{ p('socialFeature1') }}</text>
</view>
<view class="feature-pill">
<text class="feature-pill-text">{{ p('socialFeature2') }}</text>
</view>
<view class="feature-pill">
<text class="feature-pill-text">{{ p('socialFeature3') }}</text>
</view>
</view>
</view>
</view>
<!-- Hardware Specs Bento Card -->
<view class="specs-bento-card">
<view class="specs-card-header">
<view class="specs-header-lead">
<view class="specs-header-dot"></view>
<text class="specs-card-title">{{ p('specsTitle') }}</text>
</view>
<view class="specs-status-chip">
<text class="specs-status-text">{{ statusText }}</text>
</view>
</view>
<view class="field">
<text class="field-label">{{ t('uidLabel') }}</text>
<input
class="field-input"
type="text"
v-model="state.uid"
:placeholder="t('uidLabel')"
:disabled="state.submitting"
/>
</view>
<view class="field">
<text class="field-label">{{ t('cardIdLabel') }}</text>
<input
class="field-input"
type="text"
v-model="state.cardId"
:placeholder="t('cardIdLabel')"
:disabled="state.submitting"
/>
</view>
<view class="mode-meta">
<view class="meta-row">
<text class="meta-label">{{ t('statusLabel') }}</text>
<text class="meta-value">{{ statusText }}</text>
</view>
<view class="meta-row">
<text class="meta-label">{{ t('sceneLabel') }}</text>
<text class="meta-value">{{ sceneText }}</text>
</view>
</view>
</view>
<!-- Offline Protocol & Trust Assurance Bento Card -->
<view class="protocol-bento-card">
<view class="protocol-header-row">
<view class="protocol-shield-icon">
<view class="shield-inner-dot"></view>
</view>
<text class="protocol-title">{{ p('securityProtocolTitle') }}</text>
</view>
<text class="protocol-desc">{{ p('securityProtocolDesc') }}</text>
<text class="mode-offline-hint">{{ offlineHint }}</text>
</view>
<view class="mode-bottom-spacer"></view>
</view>
</scroll-view>
<view class="mode-cta-bar">
<button
class="primary-cta-btn"
:class="{ 'primary-cta-btn-disabled': state.submitting }"
:disabled="state.submitting"
:aria-label="applyLabel"
@click="handleApply"
>
<text class="primary-cta-btn-text">{{ applyLabel }}</text>
</button>
</view>
</view>
</template>
<script setup lang="uts">
import { utsLightColors, utsRadius, utsSpacing, utsTypography } from '../../../common/tokens.uts';
import { getCurrentLocale, getOmniText, isRtl, OmniLocale } from '../../../common/locale.uts';
import {
createModeState,
applyModeOptions,
selectSceneMode,
submitApplyScene,
resolveModeText,
formatStatusLabel,
formatSceneLabel,
ModeCopyKey,
ModePageOptions,
} from './useMode.uts';
type ModePageCopyKey = ModeCopyKey | 'offlineHint' | 'offlineUnavailable';
type ModeSubCopyKey =
| 'specsTitle'
| 'businessTag'
| 'businessDesc'
| 'businessFeature1'
| 'businessFeature2'
| 'businessFeature3'
| 'socialTag'
| 'socialDesc'
| 'socialFeature1'
| 'socialFeature2'
| 'socialFeature3'
| 'physicalCardTag'
| 'cardTouchHint'
| 'activeScenePill'
| 'pendingApplyPill'
| 'securityProtocolTitle'
| 'securityProtocolDesc';
const MODE_SUB_COPY: Record<OmniLocale, Record<ModeSubCopyKey, string>> = {
'zh-CN': {
specsTitle: '硬件芯片与绑定',
businessTag: 'EXPO & PORTAL',
businessDesc: '碰一碰直达移动微官网全貌、产品画廊与商机留资雷达',
businessFeature1: '微官网',
businessFeature2: '产品矩阵',
businessFeature3: '留资雷达',
socialTag: 'INSTANT VCARD',
socialDesc: '碰一碰一触直接保存电话到通讯录,支持极速双向换卡',
socialFeature1: '直存电话',
socialFeature2: '双向换卡',
socialFeature3: '秒级同步',
physicalCardTag: 'NFC 实体卡联动',
cardTouchHint: '触碰手机即可按当前所选场景直接落地',
activeScenePill: '生效中',
pendingApplyPill: '待应用',
securityProtocolTitle: 'RFC 6350 离线保底协议与安全锁',
securityProtocolDesc: '离线 vCard 烧录待设备能力就绪后开放;挂失后云端立即锁定拒绝访问',
},
'en-US': {
specsTitle: 'Hardware & Card Binding',
businessTag: 'EXPO & PORTAL',
businessDesc: 'One-tap opens mobile portal, product showcase, and visitor radar',
businessFeature1: 'Micro Portal',
businessFeature2: 'Products',
businessFeature3: 'Lead Radar',
socialTag: 'INSTANT VCARD',
socialDesc: 'One-tap saves phone contact directly, supports instant two-way exchange',
socialFeature1: 'Save Phone',
socialFeature2: 'Exchange',
socialFeature3: 'Fast Sync',
physicalCardTag: 'NFC Physical Card',
cardTouchHint: 'Tap against mobile device to trigger active destination',
activeScenePill: 'Active',
pendingApplyPill: 'Pending',
securityProtocolTitle: 'RFC 6350 Offline Protocol & Security Lock',
securityProtocolDesc: 'Offline vCard burn opens when device write is ready; remote lock on suspension',
},
'ar-SA': {
specsTitle: 'الشريحة وربط البطاقة',
businessTag: 'EXPO & PORTAL',
businessDesc: 'لمسة واحدة تفتح البوابة المحمولة وعرض المنتجات ورادار الزوار',
businessFeature1: 'بوابة مصغرة',
businessFeature2: 'المنتجات',
businessFeature3: 'رادار الزوار',
socialTag: 'INSTANT VCARD',
socialDesc: 'لمسة واحدة تحفظ جهة الاتصال مباشرة وتتيح التبادل الفوري',
socialFeature1: 'حفظ الهاتف',
socialFeature2: 'تبادل البطاقة',
socialFeature3: 'مزامنة فورية',
physicalCardTag: 'ربط بطاقة NFC الفعلية',
cardTouchHint: 'المس الهاتف لاختبار المشهد المحدد',
activeScenePill: 'مفعّل',
pendingApplyPill: 'قيد التطبيق',
securityProtocolTitle: 'بروتوكول RFC 6350 دون اتصال وقفل الأمان',
securityProtocolDesc: 'كتابة vCard دون اتصال تُتاح عند جاهزية الجهاز؛ قفل فوري عند الإيقاف',
},
'es-ES': {
specsTitle: 'Hardware y Vinculación',
businessTag: 'EXPO & PORTAL',
businessDesc: 'Un toque abre el portal móvil, catálogo de productos y radar de visitas',
businessFeature1: 'Microportal',
businessFeature2: 'Productos',
businessFeature3: 'Radar de leads',
socialTag: 'INSTANT VCARD',
socialDesc: 'Un toque guarda el contacto en el teléfono y permite intercambio directo',
socialFeature1: 'Guardar contacto',
socialFeature2: 'Intercambio',
socialFeature3: 'Sincro rápida',
physicalCardTag: 'Tarjeta NFC física vinculada',
cardTouchHint: 'Toca el teléfono para activar la escena seleccionada',
activeScenePill: 'Activo',
pendingApplyPill: 'Pendiente',
securityProtocolTitle: 'Protocolo RFC 6350 fuera de línea y bloqueo',
securityProtocolDesc: 'La escritura vCard offline se abrirá cuando el dispositivo lo permita; bloqueo al suspender',
},
'de-DE': {
specsTitle: 'Hardware & Bindung',
businessTag: 'EXPO & PORTAL',
businessDesc: 'Ein Tap öffnet das mobile Portal, Produktkatalog und Besucher-Radar',
businessFeature1: 'Web-Portal',
businessFeature2: 'Produkte',
businessFeature3: 'Radar',
socialTag: 'INSTANT VCARD',
socialDesc: 'Ein Tap speichert den Kontakt direkt ins Adressbuch und tauscht Karten',
socialFeature1: 'Kontakt speichern',
socialFeature2: 'Kartentausch',
socialFeature3: 'Schnell-Sync',
physicalCardTag: 'NFC-Karte Verknüpfung',
cardTouchHint: 'Smartphone berühren, um das Ziel direkt auszulösen',
activeScenePill: 'Aktiv',
pendingApplyPill: 'Ausstehend',
securityProtocolTitle: 'RFC 6350 Offline-Protokoll & Sicherheitssperre',
securityProtocolDesc: 'Offline-vCard-Schreiben erst bei Gerätefähigkeit; sofortige Sperre bei Verlust',
},
'fr-FR': {
specsTitle: 'Puce et Liaison',
businessTag: 'EXPO & PORTAL',
businessDesc: 'Un contact ouvre le portail mobile, la vitrine et le radar de visiteurs',
businessFeature1: 'Micro-portail',
businessFeature2: 'Produits',
businessFeature3: 'Radar prospects',
socialTag: 'INSTANT VCARD',
socialDesc: 'Un contact enregistre directement dans le répertoire et échange la carte',
socialFeature1: 'Enregistrer tél',
socialFeature2: 'Échanger',
socialFeature3: 'Synchro rapide',
physicalCardTag: 'Carte physique NFC',
cardTouchHint: 'Approchez du smartphone pour déclencher la scène choisie',
activeScenePill: 'Actif',
pendingApplyPill: 'En attente',
securityProtocolTitle: 'Protocole hors ligne RFC 6350 et verrou de sécurité',
securityProtocolDesc: 'Écriture vCard hors ligne dès que l’appareil le permet ; verrouillage si perdue',
},
'ja-JP': {
specsTitle: 'ハードウェアと連携',
businessTag: 'EXPO & PORTAL',
businessDesc: 'タッチするだけでモバイルポータル、製品カタログ、来訪者レーダーを表示',
businessFeature1: 'ポータル',
businessFeature2: '製品一覧',
businessFeature3: '商談レーダー',
socialTag: 'INSTANT VCARD',
socialDesc: 'タッチするだけで電話番号を連絡先に直接保存、双方向名刺交換に対応',
socialFeature1: '連絡先保存',
socialFeature2: '名刺交換',
socialFeature3: '即時同期',
physicalCardTag: '物理NFCカード連動',
cardTouchHint: 'スマホにかざすと選択されたシーンが即座に起動します',
activeScenePill: '適用中',
pendingApplyPill: '変更待機',
securityProtocolTitle: 'RFC 6350 オフライン規格&セキュリティロック',
securityProtocolDesc: 'オフラインvCard書き込みは端末対応後に開放、利用停止時は即時遮断',
},
};
const pageTheme: Record<string, string> = {
'--oc-primary': utsLightColors.primary || '#0B5CFF',
'--oc-primary-soft': utsLightColors.primarySoft,
'--oc-ai': utsLightColors.ai,
'--oc-bg': utsLightColors.bg || '#F5F8FC',
'--oc-surface': utsLightColors.surface || '#F7F8FA',
'--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-shadow': utsTypography.shadowCard || utsTypography.shadow,
'--oc-shadow-cta': utsTypography.shadowCta,
'--oc-space-8': utsSpacing.space8,
'--oc-space-12': utsSpacing.space12,
'--oc-space-16': utsSpacing.space16,
'--oc-space-24': utsSpacing.space24,
};
const state = reactive(createModeState());
const pageRtlClass = computed((): string => {
return isRtl(getCurrentLocale()) ? 'mode-rtl' : '';
});
const statusText = computed((): string => {
return formatStatusLabel(state.status);
});
const sceneText = computed((): string => {
return formatSceneLabel(state.sceneMode);
});
const applyLabel = computed((): string => {
if (state.submitting) {
return getOmniText(getCurrentLocale(), 'common.loading');
}
return t('apply');
});
const offlineHint = computed((): string => {
return resolveOfflineHint();
});
const displayUid = computed((): string => {
if (state.uid !== '') {
return state.uid;
}
return 'NFC-STANDBY';
});
const cardMaterialText = computed((): string => {
const m = state.device?.material;
if (m === 'metal') {
return 'METAL HYBRID';
}
if (m === 'wood') {
return 'NATURAL WOOD';
}
return 'PVC CONTACTLESS';
});
const selectedModeTitle = computed((): string => {
return state.selectedMode === 'business' ? t('business') : t('social');
});
const selectedModeSub = computed((): string => {
return state.selectedMode === 'business' ? p('businessTag') : p('socialTag');
});
function resolveOfflineHint(): string {
return resolveModeText('offlineUnavailable');
}
function t(key: ModePageCopyKey): string {
if (key === 'offlineHint' || key === 'offlineUnavailable') {
return resolveModeText('offlineUnavailable');
}
return resolveModeText(key);
}
function p(key: ModeSubCopyKey): string {
const locale = getCurrentLocale();
const bag = MODE_SUB_COPY[locale] != null ? MODE_SUB_COPY[locale] : MODE_SUB_COPY['en-US'];
return bag[key];
}
function handleSelect(mode: string): void {
selectSceneMode(state, mode);
}
async function handleApply(): Promise<void> {
await submitApplyScene(state);
}
onLoad((options: ModePageOptions) => {
applyModeOptions(state, options);
});
</script>
<style scoped>
.mode-page {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--oc-bg, #F5F8FC);
box-sizing: border-box;
}
.mode-rtl {
direction: rtl;
}
.mode-aurora-glow {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 220px;
background: radial-gradient(
circle at 50% -10%,
rgba(0, 136, 255, 0.14) 0%,
rgba(0, 210, 255, 0.06) 45%,
transparent 80%
);
pointer-events: none;
z-index: 0;
}
.mode-scroll {
flex: 1;
z-index: 1;
}
.mode-content {
display: flex;
flex-direction: column;
align-items: center;
padding-inline: var(--oc-space-16, 16px);
padding-block-start: var(--oc-space-16, 16px);
padding-block-end: calc(88px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px));
box-sizing: border-box;
width: 100%;
}
/* 1. Physical NFC Card Stage */
.nfc-card-stage {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
align-items: center;
margin-block-end: var(--oc-space-20, 20px);
}
.nfc-card-simulator {
position: relative;
width: 100%;
height: 180px;
border-radius: var(--oc-radius-card, 20px);
background-color: #FFFFFF;
border: 1px solid var(--oc-border, #EBF1F7);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.05), 0 2px 8px rgba(11, 99, 229, 0.04);
overflow: hidden;
box-sizing: border-box;
transition: all 250ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.card-theme-business {
background: linear-gradient(135deg, #FFFFFF 0%, #F5F9FF 60%, #EBF4FF 100%);
border-color: rgba(0, 136, 255, 0.25);
}
.card-theme-social {
background: linear-gradient(135deg, #FFFFFF 0%, #FAF5FF 60%, #F3EBFF 100%);
border-color: rgba(147, 51, 234, 0.25);
}
.card-glass-shimmer {
position: absolute;
top: -40px;
right: -40px;
width: 160px;
height: 160px;
border-radius: 80px;
background: radial-gradient(
circle,
rgba(255, 255, 255, 0.8) 0%,
rgba(255, 255, 255, 0) 70%
);
pointer-events: none;
}
.card-inner-content {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
padding: 16px 20px;
box-sizing: border-box;
z-index: 2;
}
.card-top-bar {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.nfc-wave-group {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.nfc-wave-arc {
box-sizing: border-box;
border-style: solid;
border-color: var(--oc-primary, #0088FF);
border-radius: 9999px;
opacity: 0.8;
}
.arc-1 {
width: 6px;
height: 12px;
border-width: 0 2px 0 0;
}
.arc-2 {
width: 10px;
height: 18px;
border-width: 0 2px 0 0;
}
.arc-3 {
width: 14px;
height: 24px;
border-width: 0 2px 0 0;
}
.card-brand-label {
font-size: 11px;
font-weight: 700;
letter-spacing: 1px;
color: var(--oc-text-2, #64748B);
margin-inline-start: 4px;
}
.card-status-badge {
display: flex;
flex-direction: row;
align-items: center;
padding: 4px 10px;
border-radius: var(--oc-radius-pill, 9999px);
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid var(--oc-border, #EBF1F7);
}
.badge-active {
border-color: rgba(18, 183, 106, 0.3);
}
.badge-suspended {
border-color: rgba(240, 68, 56, 0.3);
}
.status-pulse-dot {
width: 6px;
height: 6px;
border-radius: 3px;
margin-inline-end: 6px;
background-color: var(--oc-success, #12B76A);
}
.badge-suspended .status-pulse-dot {
background-color: var(--oc-error, #F04438);
}
.status-badge-text {
font-size: 11px;
font-weight: 600;
color: var(--oc-text, #101828);
}
.card-middle-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-block: 4px;
}
.nfc-emv-chip {
position: relative;
width: 38px;
height: 28px;
border-radius: 6px;
background: linear-gradient(135deg, #FDE68A 0%, #D97706 100%);
border: 1px solid #B45309;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.chip-line-h {
position: absolute;
top: 13px;
left: 0;
right: 0;
height: 1px;
background-color: #92400E;
}
.chip-line-v {
position: absolute;
top: 0;
bottom: 0;
left: 18px;
width: 1px;
background-color: #92400E;
}
.chip-core-contact {
position: absolute;
top: 8px;
left: 12px;
width: 14px;
height: 12px;
border-radius: 3px;
border: 1px solid #92400E;
}
.card-scene-indicator {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.card-scene-title {
font-size: 16px;
font-weight: 700;
color: var(--oc-text, #101828);
line-height: 1.25;
}
.card-scene-sub {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.5px;
color: var(--oc-primary, #0088FF);
margin-block-start: 2px;
}
.card-bottom-bar {
display: flex;
flex-direction: row;
align-items: flex-end;
justify-content: space-between;
}
.card-uid-block {
display: flex;
flex-direction: column;
}
.card-uid-label {
font-size: 9px;
font-weight: 600;
color: var(--oc-text-2, #64748B);
letter-spacing: 0.5px;
}
.card-uid-value {
font-size: 13px;
font-weight: 700;
font-family: monospace;
color: var(--oc-text, #101828);
margin-block-start: 2px;
}
.card-material-tag {
padding: 3px 8px;
background-color: rgba(255, 255, 255, 0.85);
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: 6px;
}
.card-material-text {
font-size: 9px;
font-weight: 700;
color: var(--oc-text-2, #64748B);
letter-spacing: 0.5px;
}
.card-stage-tip {
font-size: 12px;
color: var(--oc-text-2, #64748B);
line-height: 1.5;
text-align: center;
margin-block-start: 10px;
}
/* 2. Bento Section Header */
.bento-section-header {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
margin-block-start: 4px;
margin-block-end: 12px;
}
.bento-section-title {
font-size: 20px;
font-weight: 700;
color: var(--oc-text, #101828);
line-height: 1.25;
}
.bento-section-desc {
font-size: 13px;
color: var(--oc-text-2, #64748B);
line-height: 1.6;
margin-block-start: 4px;
}
/* 3. Bento Scenes Grid */
.bento-scenes-container {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 12px;
margin-block-end: 16px;
}
.bento-scene-card {
display: flex;
flex-direction: column;
padding: 16px;
background-color: #FFFFFF;
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: var(--oc-radius-card, 20px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.03);
transition: all 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
box-sizing: border-box;
}
.bento-scene-card:active {
opacity: 0.88;
}
.bento-scene-active {
background-color: #FFFFFF;
border-color: var(--oc-primary, #0088FF);
box-shadow: 0 8px 24px rgba(0, 136, 255, 0.08);
}
.scene-card-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-block-end: 8px;
}
.scene-icon-wrap {
width: 36px;
height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.icon-expo {
background-color: var(--oc-primary-soft, #EBF5FF);
}
.icon-globe-ring {
width: 18px;
height: 18px;
border-radius: 9px;
border: 1.5px solid var(--oc-primary, #0088FF);
box-sizing: border-box;
}
.icon-globe-axis {
position: absolute;
width: 1.5px;
height: 22px;
background-color: var(--oc-primary, #0088FF);
}
.icon-social {
background-color: #F3E8FF;
}
.icon-contact-card {
width: 20px;
height: 14px;
border-radius: 3px;
border: 1.5px solid #9333EA;
box-sizing: border-box;
}
.icon-contact-badge {
position: absolute;
width: 6px;
height: 6px;
border-radius: 3px;
background-color: #9333EA;
top: 15px;
right: 11px;
}
.scene-title-group {
flex: 1;
display: flex;
flex-direction: column;
margin-inline: 12px;
}
.scene-title-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.scene-title-text {
font-size: 16px;
font-weight: 700;
color: var(--oc-text, #101828);
}
.active-pill {
padding: 2px 8px;
background-color: rgba(18, 183, 106, 0.12);
border-radius: var(--oc-radius-pill, 9999px);
}
.active-pill-text {
font-size: 10px;
font-weight: 700;
color: var(--oc-success, #12B76A);
}
.scene-tag-text {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.5px;
color: var(--oc-text-2, #64748B);
margin-block-start: 2px;
}
.scene-radio-indicator {
width: 20px;
height: 20px;
border-radius: 10px;
border: 1.5px solid var(--oc-border, #EBF1F7);
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.radio-checked {
border-color: var(--oc-primary, #0088FF);
background-color: var(--oc-primary-soft, #EBF5FF);
}
.radio-dot {
width: 10px;
height: 10px;
border-radius: 5px;
background-color: var(--oc-primary, #0088FF);
}
.scene-desc-text {
font-size: 13px;
color: var(--oc-text-2, #64748B);
line-height: 1.5;
margin-block-end: 12px;
}
.scene-features-pills {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
}
.feature-pill {
padding: 4px 10px;
background-color: var(--oc-surface, #F7F8FA);
border-radius: 8px;
border: 1px solid var(--oc-border, #EBF1F7);
}
.feature-pill-text {
font-size: 11px;
font-weight: 600;
color: var(--oc-text, #101828);
}
/* 4. Hardware Specs Bento Card */
.specs-bento-card {
width: 100%;
max-width: 400px;
background-color: #FFFFFF;
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: var(--oc-radius-card, 20px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.03);
padding: 20px;
margin-block-end: 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.specs-card-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-block-end: 16px;
}
.specs-header-lead {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.specs-header-dot {
width: 6px;
height: 6px;
border-radius: 3px;
background-color: var(--oc-primary, #0088FF);
}
.specs-card-title {
font-size: 15px;
font-weight: 700;
color: var(--oc-text, #101828);
}
.specs-status-chip {
padding: 3px 8px;
background-color: var(--oc-surface, #F7F8FA);
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: 6px;
}
.specs-status-text {
font-size: 11px;
font-weight: 600;
color: var(--oc-text-2, #64748B);
}
.field {
display: flex;
flex-direction: column;
margin-block-end: 14px;
}
.field-label {
font-size: 13px;
font-weight: 600;
color: var(--oc-text, #101828);
margin-block-end: 6px;
}
.field-input {
height: 44px;
padding-inline: 12px;
background-color: var(--oc-surface, #F7F8FA);
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: var(--oc-radius-btn, 14px);
font-size: 16px;
color: var(--oc-text, #101828);
box-sizing: border-box;
}
.mode-meta {
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: var(--oc-radius-btn, 14px);
padding: 10px 14px;
background-color: var(--oc-surface, #F7F8FA);
}
.meta-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
min-height: 24px;
padding-block: 2px;
}
.meta-label {
font-size: 13px;
color: var(--oc-text-2, #64748B);
}
.meta-value {
font-size: 13px;
font-weight: 600;
color: var(--oc-text, #101828);
}
/* 5. Protocol Bento Card */
.protocol-bento-card {
width: 100%;
max-width: 400px;
background-color: #FFFFFF;
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: var(--oc-radius-card, 20px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.03);
padding: 16px 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.protocol-header-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
margin-block-end: 6px;
}
.protocol-shield-icon {
width: 16px;
height: 18px;
border: 1.5px solid var(--oc-primary, #0088FF);
border-radius: 3px 3px 8px 8px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.shield-inner-dot {
width: 4px;
height: 4px;
border-radius: 2px;
background-color: var(--oc-primary, #0088FF);
}
.protocol-title {
font-size: 13px;
font-weight: 700;
color: var(--oc-text, #101828);
}
.protocol-desc {
font-size: 12px;
color: var(--oc-text-2, #64748B);
line-height: 1.5;
margin-block-end: 6px;
}
.mode-offline-hint {
font-size: 11px;
color: var(--oc-text-2, #64748B);
line-height: 1.5;
padding: 6px 10px;
background-color: var(--oc-surface, #F7F8FA);
border-radius: 8px;
}
/* 6. Spacing & Bottom CTA Bar */
.mode-bottom-spacer {
height: calc(88px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px));
}
.mode-cta-bar {
position: fixed;
left: 0;
right: 0;
bottom: var(--window-bottom, 0px);
padding: 12px var(--oc-space-16, 16px) calc(16px + env(safe-area-inset-bottom, 0px));
background-color: rgba(255, 255, 255, 0.95);
border-top: 1px solid var(--oc-border, #EBF1F7);
box-sizing: border-box;
z-index: 100;
}
.primary-cta-btn {
height: 48px;
min-height: 44px;
width: 100%;
max-width: 400px;
margin: 0 auto;
background: linear-gradient(135deg, #00A3FF 0%, #0B63E5 100%);
border-radius: var(--oc-radius-pill, 9999px);
display: flex;
align-items: center;
justify-content: center;
border-width: 0;
box-shadow: 0 8px 20px rgba(0, 136, 255, 0.22);
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.primary-cta-btn:active {
opacity: 0.88;
}
.primary-cta-btn-disabled {
background: var(--oc-border, #EBF1F7);
box-shadow: none;
}
.primary-cta-btn-text {
color: #FFFFFF;
font-size: 16px;
font-weight: 600;
letter-spacing: 0.5px;
}
.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>