补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
1244 行
33 KiB
Plaintext
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>
|