补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
987 行
28 KiB
Plaintext
987 行
28 KiB
Plaintext
<template>
|
|
<view class="qrcode-page" :class="pageRtlClass" :style="pageTheme">
|
|
<!-- Ambient Aurora Glow Behind Header -->
|
|
<view class="showcase-aurora-glow"></view>
|
|
|
|
<scroll-view class="qrcode-scroll" :scroll-y="true">
|
|
<view class="qrcode-content">
|
|
<!-- Top Brand & Status Bar -->
|
|
<view class="showcase-brand-header">
|
|
<view class="brand-info-wrap">
|
|
<text class="brand-title">{{ brandTitle }}</text>
|
|
<text class="brand-subtitle">{{ brandSubtitle }}</text>
|
|
</view>
|
|
<view class="status-live-pill">
|
|
<view class="status-live-dot"></view>
|
|
<text class="status-live-text">{{ offlineBadgeText }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Bento Scene Switcher (3-in-1 Tabs) -->
|
|
<view class="mode-tabs-container">
|
|
<view
|
|
class="mode-tab-item"
|
|
:class="{ 'mode-tab-active': currentMode === 'stand' }"
|
|
@click="handleSelectMode('stand')"
|
|
>
|
|
<text class="mode-tab-text">{{ modeStandText }}</text>
|
|
</view>
|
|
<view
|
|
class="mode-tab-item"
|
|
:class="{ 'mode-tab-active': currentMode === 'social' }"
|
|
@click="handleSelectMode('social')"
|
|
>
|
|
<text class="mode-tab-text">{{ modeSocialText }}</text>
|
|
</view>
|
|
<view
|
|
class="mode-tab-item"
|
|
:class="{ 'mode-tab-active': currentMode === 'badge' }"
|
|
@click="handleSelectMode('badge')"
|
|
>
|
|
<text class="mode-tab-text">{{ modeBadgeText }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Skeleton State -->
|
|
<view v-if="state.loading" class="qrcode-skeleton">
|
|
<view class="skeleton-header"></view>
|
|
<view class="skeleton-slot"></view>
|
|
<view class="skeleton-line-long"></view>
|
|
<view class="skeleton-line-short"></view>
|
|
</view>
|
|
|
|
<!-- Error Feedback -->
|
|
<view v-else-if="state.errorMessage !== ''" class="qrcode-feedback">
|
|
<text class="feedback-title">{{ t('error') }}</text>
|
|
<text class="feedback-desc">{{ state.errorMessage }}</text>
|
|
</view>
|
|
|
|
<!-- Empty Feedback -->
|
|
<view v-else-if="state.empty" class="qrcode-feedback">
|
|
<text class="feedback-title">{{ t('empty') }}</text>
|
|
<text class="feedback-desc">{{ emptyHint }}</text>
|
|
</view>
|
|
|
|
<!-- Main Showcase Stand Card -->
|
|
<view v-else class="showcase-stand-card" :class="'mode-' + currentMode">
|
|
<!-- Badge Hole for Badge Mode -->
|
|
<view v-if="currentMode === 'badge'" class="badge-hole-ring">
|
|
<view class="badge-hole-dot"></view>
|
|
</view>
|
|
|
|
<!-- Social Banner Simulator for Social Mode -->
|
|
<view v-if="currentMode === 'social'" class="social-preview-header">
|
|
<view class="social-avatar-ph"></view>
|
|
<view class="social-meta-info">
|
|
<text class="social-meta-brand">{{ brandTitle }}</text>
|
|
<text class="social-meta-tag">{{ verifiedTagText }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Card Top Meta Tags -->
|
|
<view class="stand-meta-row">
|
|
<view class="stand-meta-pill">
|
|
<text class="stand-meta-text">{{ vcardBadgeText }}</text>
|
|
</view>
|
|
<view class="stand-meta-pill">
|
|
<text class="stand-meta-text">{{ offlineBadgeText }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- QR Code Podium Container -->
|
|
<view class="qrcode-slot">
|
|
<image
|
|
v-if="hasSvg && qrcodeIsImage"
|
|
class="qrcode-mark"
|
|
:src="state.svg"
|
|
mode="aspectFit"
|
|
/>
|
|
<rich-text
|
|
v-else-if="hasSvg"
|
|
class="qrcode-mark"
|
|
:nodes="state.svg"
|
|
></rich-text>
|
|
<text v-else class="qrcode-slot-hint">{{ codeSlotText }}</text>
|
|
</view>
|
|
|
|
<!-- Micro Glow Guidance -->
|
|
<view class="showcase-glow-guide">
|
|
<view class="glow-guide-pulse"></view>
|
|
<text class="glow-guide-text">{{ scanGuideText }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Direct Link Bento Capsule Bar -->
|
|
<view v-if="!state.loading && state.url !== ''" class="direct-link-card">
|
|
<view class="direct-link-body">
|
|
<text class="direct-link-label">{{ linkLabelText }}</text>
|
|
<text class="direct-link-url">{{ state.url }}</text>
|
|
</view>
|
|
<view class="direct-link-copy-pill" @click="handleCopyLink">
|
|
<text class="direct-link-copy-text">{{ copyBadgeText }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Bento Value Props Grid -->
|
|
<view v-if="!state.loading && !state.empty" class="bento-features-row">
|
|
<view class="bento-feature-item">
|
|
<text class="feature-tag">SHOWCASE</text>
|
|
<text class="feature-title">{{ feature1Title }}</text>
|
|
<text class="feature-desc">{{ feature1Desc }}</text>
|
|
</view>
|
|
<view class="bento-feature-item">
|
|
<text class="feature-tag">TRUST</text>
|
|
<text class="feature-title">{{ feature2Title }}</text>
|
|
<text class="feature-desc">{{ feature2Desc }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Bottom Spacer to clear fixed CTA -->
|
|
<view class="qrcode-bottom-spacer"></view>
|
|
</view>
|
|
</scroll-view>
|
|
|
|
<!-- Bottom Fixed Single Primary CTA -->
|
|
<view class="qrcode-cta-bar">
|
|
<button
|
|
class="primary-cta-btn"
|
|
:class="{ 'primary-cta-btn-disabled': !canCopy }"
|
|
:disabled="!canCopy"
|
|
@click="handlePrimaryCta"
|
|
>
|
|
<text class="primary-cta-btn-text">{{ primaryCtaText }}</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 {
|
|
applyQrcodeOptions,
|
|
copyPosterUrl,
|
|
createQrcodeState,
|
|
loadPosterIntoState,
|
|
resolveQrcodeText,
|
|
savePosterToAlbum,
|
|
QrcodeCopyKey,
|
|
QrcodeQuery,
|
|
} from './useCardQrcode.uts';
|
|
|
|
type QrcodePageCopyKey =
|
|
| 'emptyHint'
|
|
| 'codePending'
|
|
| 'codeUnavailable'
|
|
| 'brandTitle'
|
|
| 'brandSubtitle'
|
|
| 'modeStand'
|
|
| 'modeSocial'
|
|
| 'modeBadge'
|
|
| 'scanGuide'
|
|
| 'offlineBadge'
|
|
| 'vcardBadge'
|
|
| 'linkLabel'
|
|
| 'copyBadge'
|
|
| 'saveCta'
|
|
| 'feature1Title'
|
|
| 'feature1Desc'
|
|
| 'feature2Title'
|
|
| 'feature2Desc'
|
|
| 'verifiedTag';
|
|
|
|
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-8': utsSpacing.space8,
|
|
'--oc-space-12': utsSpacing.space12,
|
|
'--oc-space-16': utsSpacing.space16,
|
|
'--oc-space-24': utsSpacing.space24,
|
|
};
|
|
|
|
const QRCODE_PAGE_COPY: Record<OmniLocale, Record<QrcodePageCopyKey, string>> = {
|
|
'zh-CN': {
|
|
emptyHint: '还没有可分享的链接',
|
|
codePending: '码生成中',
|
|
codeUnavailable: '码不可用',
|
|
brandTitle: '展位展台分享码',
|
|
brandSubtitle: '高清矢量二维码 · 离线出示与打印',
|
|
modeStand: '展台立牌',
|
|
modeSocial: '社交海报',
|
|
modeBadge: '胸卡微印',
|
|
scanGuide: '扫码进入微官网 · 即刻互联',
|
|
offlineBadge: '离线就绪',
|
|
vcardBadge: 'vCard 4.0',
|
|
linkLabel: '微官网短链',
|
|
copyBadge: '复制',
|
|
saveCta: '保存海报并复制链接',
|
|
feature1Title: '展台级视觉规范',
|
|
feature1Desc: '支持手机支架展示、易拉宝印刷与大屏投屏',
|
|
feature2Title: '国际合规与隐私',
|
|
feature2Desc: '遵循 RFC 6350 国际协议与 GDPR 隐私规范',
|
|
verifiedTag: '已认证微官网',
|
|
},
|
|
'en-US': {
|
|
emptyHint: 'No shareable link yet',
|
|
codePending: 'Generating code',
|
|
codeUnavailable: 'Code unavailable',
|
|
brandTitle: 'Booth Showcase QR',
|
|
brandSubtitle: 'High-Res Vector Poster · Ready for Print & Display',
|
|
modeStand: 'Booth Stand',
|
|
modeSocial: 'Social Card',
|
|
modeBadge: 'Badge Mini',
|
|
scanGuide: 'Scan to view portal · Connect instantly',
|
|
offlineBadge: 'Offline Ready',
|
|
vcardBadge: 'vCard 4.0',
|
|
linkLabel: 'Portal Short Link',
|
|
copyBadge: 'Copy',
|
|
saveCta: 'Save Poster & Copy Link',
|
|
feature1Title: 'Exhibition Grade Visuals',
|
|
feature1Desc: 'Optimized for booth iPad stands, print rollups & screens',
|
|
feature2Title: 'Global SaaS Compliance',
|
|
feature2Desc: 'RFC 6350 international format & GDPR privacy standard',
|
|
verifiedTag: 'Verified Portal',
|
|
},
|
|
'ar-SA': {
|
|
emptyHint: 'لا يوجد رابط للمشاركة بعد',
|
|
codePending: 'جاري إنشاء الرمز',
|
|
codeUnavailable: 'الرمز غير متاح',
|
|
brandTitle: 'رمز عرض المنصة',
|
|
brandSubtitle: 'ملصق متجه عالي الدقة · جاهز للطباعة والعرض',
|
|
modeStand: 'حامل المنصة',
|
|
modeSocial: 'بطاقة اجتماعية',
|
|
modeBadge: 'شارة مصغرة',
|
|
scanGuide: 'امسح الرمز لعرض الموقع والتواصل فورا',
|
|
offlineBadge: 'جاهز دون اتصال',
|
|
vcardBadge: 'vCard 4.0',
|
|
linkLabel: 'رابط الموقع المختصر',
|
|
copyBadge: 'نسخ',
|
|
saveCta: 'حفظ الملصق ونسخ الرابط',
|
|
feature1Title: 'معايير بصرية احترافية',
|
|
feature1Desc: 'مهيأ للشاشات وحوامل الأجهزة والطباعة',
|
|
feature2Title: 'امتثال عالمي للخصوصية',
|
|
feature2Desc: 'متوافق مع معايير RFC 6350 و GDPR الدولية',
|
|
verifiedTag: 'موقع موثق',
|
|
},
|
|
'es-ES': {
|
|
emptyHint: 'Aun no hay enlace para compartir',
|
|
codePending: 'Generando codigo',
|
|
codeUnavailable: 'Codigo no disponible',
|
|
brandTitle: 'QR para Stand y Eventos',
|
|
brandSubtitle: 'Poster vectorial de alta resolucion para exhibicion',
|
|
modeStand: 'Soporte Stand',
|
|
modeSocial: 'Tarjeta Social',
|
|
modeBadge: 'Identificador',
|
|
scanGuide: 'Escanea para ver el portal y conectar al instante',
|
|
offlineBadge: 'Modo Offline',
|
|
vcardBadge: 'vCard 4.0',
|
|
linkLabel: 'Enlace del portal',
|
|
copyBadge: 'Copiar',
|
|
saveCta: 'Guardar poster y copiar enlace',
|
|
feature1Title: 'Calidad de Exposicion',
|
|
feature1Desc: 'Apto para pantallas de stand, soportes y rollups',
|
|
feature2Title: 'Cumplimiento Global',
|
|
feature2Desc: 'Compatible con estandar RFC 6350 y privacidad GDPR',
|
|
verifiedTag: 'Portal Verificado',
|
|
},
|
|
'de-DE': {
|
|
emptyHint: 'Noch kein teilbarer Link',
|
|
codePending: 'Code wird erstellt',
|
|
codeUnavailable: 'Code nicht verfuegbar',
|
|
brandTitle: 'Messestand QR-Code',
|
|
brandSubtitle: 'Hochaufloesendes Vektorposter fuer Messen und Print',
|
|
modeStand: 'Messestand',
|
|
modeSocial: 'Social Card',
|
|
modeBadge: 'Ausweis Mini',
|
|
scanGuide: 'Scannen fuer Web-Portal und schnellen Kontakt',
|
|
offlineBadge: 'Offline Bereit',
|
|
vcardBadge: 'vCard 4.0',
|
|
linkLabel: 'Portal-Kurzlink',
|
|
copyBadge: 'Kopieren',
|
|
saveCta: 'Poster speichern & Link kopieren',
|
|
feature1Title: 'Messestand-Standard',
|
|
feature1Desc: 'Ideal fuer iPad-Aufsteller, Grossdisplays und Rollups',
|
|
feature2Title: 'Globale Compliance',
|
|
feature2Desc: 'Entspricht RFC 6350 vCard und DSGVO-Standards',
|
|
verifiedTag: 'Verifiziertes Portal',
|
|
},
|
|
'fr-FR': {
|
|
emptyHint: 'Aucun lien a partager',
|
|
codePending: 'Generation du code',
|
|
codeUnavailable: 'Code indisponible',
|
|
brandTitle: 'QR Code de Stand & Salon',
|
|
brandSubtitle: 'Affiche vectorielle haute definition pour exposition',
|
|
modeStand: 'Support Stand',
|
|
modeSocial: 'Carte Sociale',
|
|
modeBadge: 'Badge Mini',
|
|
scanGuide: 'Scannez pour acceder au portail et echanger',
|
|
offlineBadge: 'Pret Hors-ligne',
|
|
vcardBadge: 'vCard 4.0',
|
|
linkLabel: 'Lien court portail',
|
|
copyBadge: 'Copier',
|
|
saveCta: 'Enregistrer l affiche & Copier le lien',
|
|
feature1Title: 'Qualite d Exposition',
|
|
feature1Desc: 'Concu pour les stands, tablettes et impressions rollup',
|
|
feature2Title: 'Conformite Internationale',
|
|
feature2Desc: 'Norme mondiale RFC 6350 et confidentialite RGPD',
|
|
verifiedTag: 'Portail Verifie',
|
|
},
|
|
'ja-JP': {
|
|
emptyHint: '共有できるリンクがありません',
|
|
codePending: 'コードを生成中',
|
|
codeUnavailable: 'コードを利用できません',
|
|
brandTitle: 'ブース展示用QRコード',
|
|
brandSubtitle: '高解像度ベクターポスター · 印刷と展示に対応',
|
|
modeStand: '展示スタンド',
|
|
modeSocial: 'SNSシェア',
|
|
modeBadge: '名札ミニ',
|
|
scanGuide: 'スキャンしてWeb名刺へアクセス',
|
|
offlineBadge: 'オフライン対応',
|
|
vcardBadge: 'vCard 4.0',
|
|
linkLabel: '名刺ショートリンク',
|
|
copyBadge: 'コピー',
|
|
saveCta: 'ポスター保存&リンクコピー',
|
|
feature1Title: '展示会仕様のビジュアル',
|
|
feature1Desc: 'iPadスタンド展示、ロールアップ印刷、大画面に対応',
|
|
feature2Title: 'グローバルコンプライアンス',
|
|
feature2Desc: 'RFC 6350国際規格およびGDPRプライバシーに準拠',
|
|
verifiedTag: '認証済みポータル',
|
|
},
|
|
};
|
|
|
|
const currentMode = ref<string>('stand');
|
|
const state = reactive(createQrcodeState());
|
|
|
|
const canCopy = computed((): boolean => {
|
|
return state.url !== '' && !state.loading;
|
|
});
|
|
|
|
const hasSvg = computed((): boolean => {
|
|
return state.svg !== '';
|
|
});
|
|
|
|
const qrcodeIsImage = computed((): boolean => {
|
|
const raw = state.svg.trim();
|
|
return raw.indexOf('http://') === 0 || raw.indexOf('https://') === 0 || raw.indexOf('data:') === 0;
|
|
});
|
|
|
|
const pageRtlClass = computed((): string => {
|
|
return isRtl(getCurrentLocale()) ? 'qrcode-rtl' : '';
|
|
});
|
|
|
|
const emptyHint = computed((): string => {
|
|
return resolveQrcodePageText('emptyHint');
|
|
});
|
|
|
|
const codeSlotText = computed((): string => {
|
|
if (state.svg !== '') {
|
|
return '';
|
|
}
|
|
if (state.url !== '') {
|
|
return resolveQrcodePageText('codePending');
|
|
}
|
|
return resolveQrcodePageText('codeUnavailable');
|
|
});
|
|
|
|
const brandTitle = computed((): string => resolveQrcodePageText('brandTitle'));
|
|
const brandSubtitle = computed((): string => resolveQrcodePageText('brandSubtitle'));
|
|
const modeStandText = computed((): string => resolveQrcodePageText('modeStand'));
|
|
const modeSocialText = computed((): string => resolveQrcodePageText('modeSocial'));
|
|
const modeBadgeText = computed((): string => resolveQrcodePageText('modeBadge'));
|
|
const scanGuideText = computed((): string => resolveQrcodePageText('scanGuide'));
|
|
const offlineBadgeText = computed((): string => resolveQrcodePageText('offlineBadge'));
|
|
const vcardBadgeText = computed((): string => resolveQrcodePageText('vcardBadge'));
|
|
const linkLabelText = computed((): string => resolveQrcodePageText('linkLabel'));
|
|
const copyBadgeText = computed((): string => resolveQrcodePageText('copyBadge'));
|
|
const primaryCtaText = computed((): string => resolveQrcodePageText('saveCta'));
|
|
const feature1Title = computed((): string => resolveQrcodePageText('feature1Title'));
|
|
const feature1Desc = computed((): string => resolveQrcodePageText('feature1Desc'));
|
|
const feature2Title = computed((): string => resolveQrcodePageText('feature2Title'));
|
|
const feature2Desc = computed((): string => resolveQrcodePageText('feature2Desc'));
|
|
const verifiedTagText = computed((): string => resolveQrcodePageText('verifiedTag'));
|
|
|
|
function resolveQrcodePageText(key: QrcodePageCopyKey): string {
|
|
const locale = getCurrentLocale();
|
|
const bag = QRCODE_PAGE_COPY[locale] != null ? QRCODE_PAGE_COPY[locale] : QRCODE_PAGE_COPY['en-US'];
|
|
return bag[key];
|
|
}
|
|
|
|
function t(key: QrcodeCopyKey): string {
|
|
if (key === 'title') {
|
|
return getOmniText(getCurrentLocale(), 'card.share');
|
|
}
|
|
return resolveQrcodeText(key);
|
|
}
|
|
|
|
function handleSelectMode(mode: string): void {
|
|
currentMode.value = mode;
|
|
}
|
|
|
|
function handleCopy(): void {
|
|
copyPosterUrl(state.url);
|
|
}
|
|
|
|
function handleCopyLink(): void {
|
|
copyPosterUrl(state.url);
|
|
}
|
|
|
|
function handlePrimaryCta(): void {
|
|
if (!canCopy.value) {
|
|
return;
|
|
}
|
|
void (async () => {
|
|
await savePosterToAlbum(state.svg);
|
|
copyPosterUrl(state.url);
|
|
})();
|
|
}
|
|
|
|
onLoad((options: QrcodeQuery) => {
|
|
const slugOrId = applyQrcodeOptions(state, options);
|
|
loadPosterIntoState(state, slugOrId);
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.qrcode-page {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
background-color: var(--oc-bg, #F7F8FA);
|
|
}
|
|
|
|
.qrcode-rtl {
|
|
direction: rtl;
|
|
}
|
|
|
|
.showcase-aurora-glow {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 240px;
|
|
background: radial-gradient(
|
|
circle at 50% 0%,
|
|
rgba(0, 136, 255, 0.15) 0%,
|
|
rgba(0, 210, 255, 0.08) 35%,
|
|
rgba(99, 102, 241, 0.04) 65%,
|
|
transparent 85%
|
|
);
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
|
|
.qrcode-scroll {
|
|
flex: 1;
|
|
z-index: 1;
|
|
}
|
|
|
|
.qrcode-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;
|
|
}
|
|
|
|
.showcase-brand-header {
|
|
width: 100%;
|
|
max-width: 380px;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-block-end: var(--oc-space-16, 16px);
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.brand-info-wrap {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.brand-title {
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
color: var(--oc-text, #101828);
|
|
line-height: 1.25;
|
|
}
|
|
|
|
.brand-subtitle {
|
|
font-size: 12px;
|
|
color: var(--oc-text-2, #667085);
|
|
margin-block-start: 4px;
|
|
}
|
|
|
|
.status-live-pill {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
padding: 4px 10px;
|
|
background-color: var(--oc-surface, #FFFFFF);
|
|
border: 1px solid var(--oc-border, #E3E7ED);
|
|
border-radius: var(--oc-radius-pill, 9999px);
|
|
box-shadow: 0 1px 3px rgba(16, 24, 40, 0.04);
|
|
}
|
|
|
|
.status-live-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 3px;
|
|
background-color: var(--oc-success, #12B76A);
|
|
margin-inline-end: 6px;
|
|
}
|
|
|
|
.status-live-text {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--oc-text-2, #667085);
|
|
}
|
|
|
|
.mode-tabs-container {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
max-width: 380px;
|
|
background-color: var(--oc-surface, #F7F8FA);
|
|
border: 1px solid var(--oc-border, #E3E7ED);
|
|
border-radius: var(--oc-radius-pill, 9999px);
|
|
padding: 4px;
|
|
margin-block-end: var(--oc-space-16, 16px);
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.mode-tab-item {
|
|
flex: 1;
|
|
height: 36px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: var(--oc-radius-pill, 9999px);
|
|
transition: all 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
}
|
|
|
|
.mode-tab-active {
|
|
background-color: var(--oc-bg, #FFFFFF);
|
|
box-shadow: 0 2px 8px rgba(16, 24, 40, 0.06);
|
|
}
|
|
|
|
.mode-tab-text {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--oc-text-2, #667085);
|
|
}
|
|
|
|
.mode-tab-active .mode-tab-text {
|
|
color: var(--oc-primary, #0B5CFF);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.qrcode-skeleton {
|
|
width: 100%;
|
|
max-width: 380px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
background-color: var(--oc-bg, #FFFFFF);
|
|
border: 1px solid var(--oc-border, #E3E7ED);
|
|
border-radius: var(--oc-radius-card, 20px);
|
|
padding: var(--oc-space-24, 24px);
|
|
box-sizing: border-box;
|
|
box-shadow: var(--oc-shadow, 0 8px 30px rgba(0, 0, 0, 0.04));
|
|
}
|
|
|
|
.skeleton-header {
|
|
height: 20px;
|
|
width: 50%;
|
|
background-color: var(--oc-border, #E3E7ED);
|
|
border-radius: 6px;
|
|
margin-block-end: var(--oc-space-16, 16px);
|
|
}
|
|
|
|
.skeleton-slot {
|
|
width: 210px;
|
|
height: 210px;
|
|
background-color: var(--oc-surface, #F7F8FA);
|
|
border: 1px solid var(--oc-border, #E3E7ED);
|
|
border-radius: var(--oc-radius-card, 16px);
|
|
margin-block-end: var(--oc-space-16, 16px);
|
|
}
|
|
|
|
.skeleton-line-long {
|
|
height: 12px;
|
|
width: 75%;
|
|
background-color: var(--oc-border, #E3E7ED);
|
|
border-radius: 6px;
|
|
margin-block-end: 8px;
|
|
}
|
|
|
|
.skeleton-line-short {
|
|
height: 12px;
|
|
width: 45%;
|
|
background-color: var(--oc-border, #E3E7ED);
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.qrcode-feedback {
|
|
width: 100%;
|
|
max-width: 380px;
|
|
background-color: var(--oc-bg, #FFFFFF);
|
|
border: 1px solid var(--oc-border, #E3E7ED);
|
|
border-radius: var(--oc-radius-card, 20px);
|
|
box-shadow: var(--oc-shadow, 0 8px 30px rgba(0, 0, 0, 0.04));
|
|
padding: var(--oc-space-24, 24px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.feedback-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--oc-text, #101828);
|
|
line-height: 1.6;
|
|
text-align: center;
|
|
margin-block-end: var(--oc-space-8, 8px);
|
|
}
|
|
|
|
.feedback-desc {
|
|
font-size: 14px;
|
|
color: var(--oc-text-2, #667085);
|
|
line-height: 1.6;
|
|
text-align: center;
|
|
}
|
|
|
|
.showcase-stand-card {
|
|
position: relative;
|
|
width: 100%;
|
|
max-width: 380px;
|
|
background-color: var(--oc-bg, #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), 0 2px 8px rgba(11, 99, 229, 0.03));
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
box-sizing: border-box;
|
|
margin-block-end: var(--oc-space-16, 16px);
|
|
transition: all 250ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
}
|
|
|
|
.mode-badge {
|
|
border-top: 4px solid var(--oc-primary, #0B5CFF);
|
|
}
|
|
|
|
.badge-hole-ring {
|
|
width: 36px;
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
background-color: var(--oc-surface, #F7F8FA);
|
|
border: 1px solid var(--oc-border, #E3E7ED);
|
|
margin-inline: auto;
|
|
margin-block-end: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.badge-hole-dot {
|
|
width: 12px;
|
|
height: 3px;
|
|
background-color: var(--oc-border, #E3E7ED);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.social-preview-header {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
width: 100%;
|
|
padding-block-end: 12px;
|
|
margin-block-end: 12px;
|
|
border-bottom: 1px dashed var(--oc-border, #E3E7ED);
|
|
}
|
|
|
|
.social-avatar-ph {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 18px;
|
|
background: var(--oc-primary-gradient, linear-gradient(135deg, #00A3FF 0%, #0B63E5 100%));
|
|
margin-inline-end: 12px;
|
|
}
|
|
|
|
.social-meta-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.social-meta-brand {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--oc-text, #101828);
|
|
}
|
|
|
|
.social-meta-tag {
|
|
font-size: 11px;
|
|
color: var(--oc-primary, #0B5CFF);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.stand-meta-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
margin-block-end: 14px;
|
|
}
|
|
|
|
.stand-meta-pill {
|
|
padding: 3px 8px;
|
|
background-color: var(--oc-surface, #F7F8FA);
|
|
border: 1px solid var(--oc-border, #E3E7ED);
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.stand-meta-text {
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: var(--oc-text-2, #667085);
|
|
}
|
|
|
|
.qrcode-slot {
|
|
width: 210px;
|
|
height: 210px;
|
|
margin-inline: auto;
|
|
background-color: #FFFFFF;
|
|
border: 1px solid var(--oc-border, #E3E7ED);
|
|
border-radius: var(--oc-radius-card, 16px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
box-sizing: border-box;
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.03);
|
|
}
|
|
|
|
.qrcode-mark {
|
|
width: 210px;
|
|
height: 210px;
|
|
}
|
|
|
|
.qrcode-slot-hint {
|
|
font-size: 12px;
|
|
color: var(--oc-text-2, #667085);
|
|
line-height: 1.6;
|
|
text-align: center;
|
|
padding-inline: var(--oc-space-12, 12px);
|
|
}
|
|
|
|
.showcase-glow-guide {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 6px 14px;
|
|
background-color: var(--oc-primary-soft, #EBF5FF);
|
|
border-radius: var(--oc-radius-pill, 9999px);
|
|
margin-block-start: 14px;
|
|
}
|
|
|
|
.glow-guide-pulse {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 3px;
|
|
background-color: var(--oc-primary, #0B5CFF);
|
|
margin-inline-end: 8px;
|
|
}
|
|
|
|
.glow-guide-text {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--oc-primary, #0B5CFF);
|
|
}
|
|
|
|
.direct-link-card {
|
|
width: 100%;
|
|
max-width: 380px;
|
|
background-color: var(--oc-bg, #FFFFFF);
|
|
border: 1px solid var(--oc-border, #E3E7ED);
|
|
border-radius: var(--oc-radius-card, 16px);
|
|
padding: 12px 14px;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
box-shadow: var(--oc-shadow, 0 8px 30px rgba(0, 0, 0, 0.04));
|
|
margin-block-end: var(--oc-space-16, 16px);
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.direct-link-body {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
margin-inline-end: 10px;
|
|
}
|
|
|
|
.direct-link-label {
|
|
font-size: 11px;
|
|
color: var(--oc-text-2, #667085);
|
|
margin-block-end: 2px;
|
|
}
|
|
|
|
.direct-link-url {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--oc-text, #101828);
|
|
line-height: 1.4;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.direct-link-copy-pill {
|
|
padding: 6px 12px;
|
|
background-color: var(--oc-surface, #F7F8FA);
|
|
border: 1px solid var(--oc-border, #E3E7ED);
|
|
border-radius: var(--oc-radius-pill, 9999px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: opacity 150ms ease;
|
|
}
|
|
|
|
.direct-link-copy-pill:active {
|
|
opacity: 0.75;
|
|
}
|
|
|
|
.direct-link-copy-text {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--oc-primary, #0B5CFF);
|
|
}
|
|
|
|
.bento-features-row {
|
|
width: 100%;
|
|
max-width: 380px;
|
|
display: flex;
|
|
flex-direction: row;
|
|
gap: 10px;
|
|
margin-block-end: var(--oc-space-16, 16px);
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.bento-feature-item {
|
|
flex: 1;
|
|
background-color: var(--oc-bg, #FFFFFF);
|
|
border: 1px solid var(--oc-border, #E3E7ED);
|
|
border-radius: var(--oc-radius-card, 16px);
|
|
padding: 12px 14px;
|
|
box-shadow: var(--oc-shadow, 0 8px 30px rgba(0, 0, 0, 0.04));
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.feature-tag {
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.5px;
|
|
color: var(--oc-primary, #0B5CFF);
|
|
margin-block-end: 4px;
|
|
}
|
|
|
|
.feature-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--oc-text, #101828);
|
|
margin-block-end: 4px;
|
|
}
|
|
|
|
.feature-desc {
|
|
font-size: 11px;
|
|
color: var(--oc-text-2, #667085);
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.qrcode-bottom-spacer {
|
|
height: 20px;
|
|
}
|
|
|
|
.qrcode-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: var(--oc-bg, #FFFFFF);
|
|
border-top: 1px solid var(--oc-border, #E3E7ED);
|
|
box-sizing: border-box;
|
|
z-index: 100;
|
|
}
|
|
|
|
.primary-cta-btn {
|
|
height: 48px;
|
|
min-height: 44px;
|
|
width: 100%;
|
|
border-width: 0;
|
|
border-radius: var(--oc-radius-btn, 14px);
|
|
background: var(--oc-primary-gradient, var(--oc-primary, #0B5CFF));
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: var(--oc-shadow-cta, 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, #E3E7ED);
|
|
box-shadow: none;
|
|
}
|
|
|
|
.primary-cta-btn-text {
|
|
color: #FFFFFF;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.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>
|