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

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

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>