补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
1093 行
26 KiB
Plaintext
1093 行
26 KiB
Plaintext
<template>
|
||
<view class="scan-page" :class="pageRtlClass" :style="pageTheme">
|
||
<scroll-view class="scan-scroll" :scroll-y="true">
|
||
<view class="scan-content">
|
||
<!-- 1. Hero Showcase Bento -->
|
||
<view class="hero-bento">
|
||
<!-- Card Style Switcher Pill -->
|
||
<view class="card-style-switcher">
|
||
<view
|
||
class="switcher-pill"
|
||
:class="{ 'switcher-pill-active': selectedCardStyle === 'white' }"
|
||
@click="toggleCardStyle('white')"
|
||
>
|
||
<text class="switcher-text">{{ extraText('cardTypeWhite') }}</text>
|
||
</view>
|
||
<view
|
||
class="switcher-pill"
|
||
:class="{ 'switcher-pill-active': selectedCardStyle === 'black' }"
|
||
@click="toggleCardStyle('black')"
|
||
>
|
||
<text class="switcher-text">{{ extraText('cardTypeBlack') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Physical Card Showcase Stage -->
|
||
<view class="card-stage">
|
||
<!-- NFC single 400ms pulse ring -->
|
||
<view class="scan-ring"></view>
|
||
|
||
<!-- Physical card twin (White / Black) -->
|
||
<view class="nfc-physical-card" :class="'card-theme-' + selectedCardStyle">
|
||
<view class="card-glass-specular"></view>
|
||
<view class="card-header-row">
|
||
<text class="nfc-card-brand">OMNICARD AI</text>
|
||
<view class="nfc-icon-wrap">
|
||
<view class="nfc-signal-bar bar-1"></view>
|
||
<view class="nfc-signal-bar bar-2"></view>
|
||
<view class="nfc-signal-bar bar-3"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card-chip-area">
|
||
<view class="chip-hardware">
|
||
<view class="chip-line"></view>
|
||
<view class="chip-line"></view>
|
||
</view>
|
||
<text class="nfc-card-chip">NTAG 215 · 540B</text>
|
||
</view>
|
||
|
||
<view class="card-footer-row">
|
||
<view class="card-uid-info">
|
||
<text class="card-uid-caption">UID</text>
|
||
<text class="card-uid-val">{{ displayCardUid }}</text>
|
||
</view>
|
||
<view class="card-badge">
|
||
<text class="card-badge-txt">{{ currentCardStatusText }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Title and status indicator -->
|
||
<text class="scan-title">{{ pageTitle }}</text>
|
||
<text class="scan-desc">{{ t('desc') }}</text>
|
||
|
||
<view v-if="statusLine !== ''" class="scan-status" :class="{ 'status-error': state.errorMessage !== '' }">
|
||
<view class="status-dot"></view>
|
||
<text class="scan-status-text">{{ statusLine }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 2. Activated Success Result Bento -->
|
||
<view v-if="state.record != null" class="result-bento">
|
||
<view class="result-header">
|
||
<view class="result-success-icon">
|
||
<text class="result-icon-check">OK</text>
|
||
</view>
|
||
<view class="result-header-text">
|
||
<text class="result-title">{{ extraText('boundSuccessTitle') }}</text>
|
||
<text class="result-desc">{{ extraText('boundSuccessDesc') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="meta-grid">
|
||
<view class="meta-cell">
|
||
<text class="meta-label">{{ t('uidLabel') }}</text>
|
||
<text class="meta-value">{{ state.record!.uid }}</text>
|
||
</view>
|
||
<view class="meta-cell">
|
||
<text class="meta-label">{{ t('cardIdLabel') }}</text>
|
||
<text class="meta-value">#{{ state.record!.cardId || state.cardIdRaw || '-' }}</text>
|
||
</view>
|
||
<view class="meta-cell">
|
||
<text class="meta-label">{{ extraText('hardwareSpecs') }}</text>
|
||
<text class="meta-value">{{ state.record!.batchNo || 'NTAG215-STD' }}</text>
|
||
</view>
|
||
<view class="meta-cell">
|
||
<text class="meta-label">Scene Mode</text>
|
||
<text class="meta-value uppercase-txt">{{ state.record!.sceneMode || 'business' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="re-scan-btn" @click="handleReset">
|
||
<text class="re-scan-text">{{ extraText('actionScanAnother') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 3. 3-Step Quick Guide Bento -->
|
||
<view v-if="state.record == null" class="guide-bento">
|
||
<view class="step-card">
|
||
<view class="step-badge">
|
||
<text class="step-num">1</text>
|
||
</view>
|
||
<text class="step-title">{{ extraText('step1Title') }}</text>
|
||
<text class="step-desc">{{ extraText('step1Desc') }}</text>
|
||
</view>
|
||
|
||
<view class="step-card">
|
||
<view class="step-badge">
|
||
<text class="step-num">2</text>
|
||
</view>
|
||
<text class="step-title">{{ extraText('step2Title') }}</text>
|
||
<text class="step-desc">{{ extraText('step2Desc') }}</text>
|
||
</view>
|
||
|
||
<view class="step-card">
|
||
<view class="step-badge">
|
||
<text class="step-num">3</text>
|
||
</view>
|
||
<text class="step-title">{{ extraText('step3Title') }}</text>
|
||
<text class="step-desc">{{ extraText('step3Desc') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 4. Chip UID and Card Input Bento Form (Shown when state.manualInput) -->
|
||
<view v-if="state.manualInput && state.record == null" class="scan-form">
|
||
<view class="form-header-bar">
|
||
<text class="form-title">{{ extraText('hardwareSpecs') }}</text>
|
||
<text class="form-badge-proto">{{ extraText('hardwareNtag') }}</text>
|
||
</view>
|
||
|
||
<view class="field">
|
||
<text class="field-label">{{ t('uidLabel') }}</text>
|
||
<view class="input-wrapper">
|
||
<input
|
||
class="field-input"
|
||
type="text"
|
||
v-model="state.uid"
|
||
:placeholder="t('uidPlaceholder')"
|
||
:disabled="state.submitting"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="field">
|
||
<text class="field-label">{{ t('cardIdLabel') }}</text>
|
||
<view class="input-wrapper">
|
||
<input
|
||
class="field-input"
|
||
type="number"
|
||
v-model="state.cardIdRaw"
|
||
:placeholder="t('cardIdPlaceholder')"
|
||
:disabled="state.submitting"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="scan-spacer"></view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 5. Fixed Bottom Primary CTA Bar -->
|
||
<view class="scan-cta-bar">
|
||
<button
|
||
class="primary-cta-btn"
|
||
:class="{ 'primary-cta-btn-disabled': isCtaDisabled }"
|
||
:disabled="isCtaDisabled"
|
||
:aria-label="ctaButtonText"
|
||
@click="handleActivate"
|
||
>
|
||
<text class="primary-cta-btn-text">{{ ctaButtonText }}</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { getCurrentLocale, getOmniText, isRtl } from '../../../common/locale.uts';
|
||
import { utsLightColors, utsRadius, utsSpacing, utsTypography } from '../../../common/tokens.uts';
|
||
import {
|
||
applyScanOptions,
|
||
buildNdefUri,
|
||
createScanState,
|
||
resolveScanText,
|
||
resolveStatusLine,
|
||
startScan,
|
||
submitScan,
|
||
writeNdefToTag,
|
||
ScanCopyKey,
|
||
ScanPageOptions,
|
||
} from './useScan.uts';
|
||
|
||
type ExtraCopyKey =
|
||
| 'cardReadyTitle'
|
||
| 'cardReadySubtitle'
|
||
| 'cardTypeWhite'
|
||
| 'cardTypeBlack'
|
||
| 'step1Title'
|
||
| 'step1Desc'
|
||
| 'step2Title'
|
||
| 'step2Desc'
|
||
| 'step3Title'
|
||
| 'step3Desc'
|
||
| 'statusSuccess'
|
||
| 'statusActive'
|
||
| 'statusReady'
|
||
| 'hardwareSpecs'
|
||
| 'hardwareNtag'
|
||
| 'boundSuccessTitle'
|
||
| 'boundSuccessDesc'
|
||
| 'actionDone'
|
||
| 'actionScanAnother'
|
||
| 'submittingText';
|
||
|
||
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': utsTypography.shadow,
|
||
'--oc-space-4': utsSpacing.space4,
|
||
'--oc-space-8': utsSpacing.space8,
|
||
'--oc-space-12': utsSpacing.space12,
|
||
'--oc-space-16': utsSpacing.space16,
|
||
'--oc-space-20': utsSpacing.space20,
|
||
'--oc-space-24': utsSpacing.space24,
|
||
};
|
||
|
||
const state = reactive(createScanState());
|
||
const pageTitle = ref(getOmniText(getCurrentLocale(), 'card.scan_nfc'));
|
||
const selectedCardStyle = ref<'white' | 'black'>('white');
|
||
|
||
const statusLine = computed((): string => {
|
||
return resolveStatusLine(state);
|
||
});
|
||
|
||
const pageRtlClass = computed((): string => {
|
||
return isRtl(getCurrentLocale()) ? 'scan-rtl' : '';
|
||
});
|
||
|
||
const displayCardUid = computed((): string => {
|
||
if (state.record != null && state.record.uid !== '') {
|
||
return state.record.uid;
|
||
}
|
||
if (state.uid !== '') {
|
||
return state.uid;
|
||
}
|
||
return '04:••:••:••:••';
|
||
});
|
||
|
||
const currentCardStatusText = computed((): string => {
|
||
if (state.record != null) {
|
||
return extraText('statusActive');
|
||
}
|
||
if (state.uid !== '') {
|
||
return 'READY';
|
||
}
|
||
return extraText('statusReady');
|
||
});
|
||
|
||
const isCtaDisabled = computed((): boolean => {
|
||
if (state.submitting) {
|
||
return true;
|
||
}
|
||
return false;
|
||
});
|
||
|
||
const ctaButtonText = computed((): string => {
|
||
if (state.submitting) {
|
||
return extraText('submittingText');
|
||
}
|
||
if (state.record != null) {
|
||
return extraText('actionDone');
|
||
}
|
||
return t('cta');
|
||
});
|
||
|
||
function t(key: ScanCopyKey): string {
|
||
return resolveScanText(key);
|
||
}
|
||
|
||
function extraText(key: ExtraCopyKey): string {
|
||
const isZh = getCurrentLocale() === 'zh-CN';
|
||
if (key === 'cardReadyTitle') {
|
||
return isZh ? '请将卡片贴近手机背面上部' : 'Hold card to the top-back of phone';
|
||
}
|
||
if (key === 'cardReadySubtitle') {
|
||
return isZh ? '内置 NTAG 芯片感应距离 1-3cm' : 'NTAG chip sensing range 1-3cm';
|
||
}
|
||
if (key === 'cardTypeWhite') {
|
||
return isZh ? '商务白金卡' : 'White Smart Card';
|
||
}
|
||
if (key === 'cardTypeBlack') {
|
||
return isZh ? '极客曜黑卡' : 'Obsidian Black Card';
|
||
}
|
||
if (key === 'step1Title') {
|
||
return isZh ? '贴卡感应' : 'Tap Card';
|
||
}
|
||
if (key === 'step1Desc') {
|
||
return isZh ? '对准背部 NFC 天线' : 'Align with NFC antenna';
|
||
}
|
||
if (key === 'step2Title') {
|
||
return isZh ? '读取 UID' : 'Read UID';
|
||
}
|
||
if (key === 'step2Desc') {
|
||
return isZh ? '获取芯片唯一序列号' : 'Acquire unique chip ID';
|
||
}
|
||
if (key === 'step3Title') {
|
||
return isZh ? '云端激活' : 'Activate';
|
||
}
|
||
if (key === 'step3Desc') {
|
||
return isZh ? '写入绑定微官网' : 'Bind mobile portal';
|
||
}
|
||
if (key === 'statusSuccess') {
|
||
return isZh ? '卡片已成功激活' : 'Card Successfully Activated';
|
||
}
|
||
if (key === 'statusActive') {
|
||
return isZh ? '已激活生效' : 'Active';
|
||
}
|
||
if (key === 'statusReady') {
|
||
return isZh ? '待感应' : 'Ready to Tap';
|
||
}
|
||
if (key === 'hardwareSpecs') {
|
||
return isZh ? '硬件规格' : 'Hardware Spec';
|
||
}
|
||
if (key === 'hardwareNtag') {
|
||
return isZh ? 'ISO/IEC 14443-A · NTAG213/215/216' : 'ISO/IEC 14443-A · NTAG213/215/216';
|
||
}
|
||
if (key === 'boundSuccessTitle') {
|
||
return isZh ? '物理实体卡绑定成功' : 'NFC Card Successfully Bound';
|
||
}
|
||
if (key === 'boundSuccessDesc') {
|
||
return isZh
|
||
? '云端路由已绑定。芯片内容需等系统写入 API 成功后才会确认。'
|
||
: 'Cloud routing is bound. Chip content is confirmed only after a successful system write.';
|
||
}
|
||
if (key === 'actionDone') {
|
||
return isZh ? '完成' : 'Done';
|
||
}
|
||
if (key === 'actionScanAnother') {
|
||
return isZh ? '激活另一张' : 'Activate Another';
|
||
}
|
||
if (key === 'submittingText') {
|
||
return isZh ? '正在激活绑定...' : 'Activating...';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function toggleCardStyle(style: 'white' | 'black'): void {
|
||
selectedCardStyle.value = style;
|
||
}
|
||
|
||
function handleReset(): void {
|
||
state.record = null;
|
||
state.uid = '';
|
||
state.empty = true;
|
||
state.errorMessage = '';
|
||
// #ifdef APP
|
||
startScan(state);
|
||
// #endif
|
||
}
|
||
|
||
function handleActivate(): void {
|
||
if (state.record != null) {
|
||
// #ifdef APP
|
||
uni.navigateBack();
|
||
// #endif
|
||
// #ifndef APP
|
||
uni.navigateBack({
|
||
fail: () => {
|
||
uni.reLaunch({ url: '/pages/owner/card/index' });
|
||
}
|
||
});
|
||
// #endif
|
||
return;
|
||
}
|
||
void runActivateAndWrite();
|
||
}
|
||
|
||
async function runActivateAndWrite(): Promise<void> {
|
||
const record = await submitScan(state);
|
||
if (record == null) {
|
||
return;
|
||
}
|
||
const cardId =
|
||
record.cardId != null
|
||
? record.cardId
|
||
: state.deviceCardId != null
|
||
? state.deviceCardId
|
||
: state.cardIdRaw;
|
||
const uri = buildNdefUri(cardId);
|
||
await writeNdefToTag(uri);
|
||
}
|
||
|
||
onLoad((options: ScanPageOptions) => {
|
||
applyScanOptions(state, options);
|
||
// #ifdef H5
|
||
state.manualInput = true;
|
||
// #endif
|
||
// #ifdef APP
|
||
startScan(state);
|
||
// #endif
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.scan-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
background-color: var(--oc-bg, #F7F8FA);
|
||
color: var(--oc-text, #101828);
|
||
}
|
||
|
||
.scan-rtl {
|
||
direction: rtl;
|
||
}
|
||
|
||
.scan-scroll {
|
||
flex: 1;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.scan-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding-inline: var(--oc-space-16, 16px);
|
||
padding-block-start: var(--oc-space-16, 16px);
|
||
padding-block-end: calc(96px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
|
||
/* 1. Hero 感应展台 Bento */
|
||
.hero-bento {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
background-color: var(--oc-surface, #FFFFFF);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: var(--oc-radius-card, 20px);
|
||
padding: var(--oc-space-20, 20px) var(--oc-space-16, 16px);
|
||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.04), 0 2px 8px rgba(11, 99, 229, 0.03);
|
||
margin-block-end: var(--oc-space-16, 16px);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 实体卡材质切换 */
|
||
.card-style-switcher {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
background-color: var(--oc-bg, #F7F8FA);
|
||
padding: 3px;
|
||
border-radius: 9999px;
|
||
margin-block-end: var(--oc-space-16, 16px);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
}
|
||
|
||
.switcher-pill {
|
||
padding: 4px 14px;
|
||
border-radius: 9999px;
|
||
transition: all 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
}
|
||
|
||
.switcher-pill-active {
|
||
background-color: var(--oc-surface, #FFFFFF);
|
||
box-shadow: 0 2px 6px rgba(16, 24, 40, 0.08);
|
||
}
|
||
|
||
.switcher-text {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
.switcher-pill-active .switcher-text {
|
||
color: var(--oc-primary, #0B5CFF);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 拟物实体卡展台区 */
|
||
.card-stage {
|
||
position: relative;
|
||
width: 240px;
|
||
height: 152px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-block-end: var(--oc-space-20, 20px);
|
||
}
|
||
|
||
/* NFC 单次脉冲呼吸环 (400ms 一次,不循环) */
|
||
.scan-ring {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
width: 140px;
|
||
height: 140px;
|
||
margin-left: -70px;
|
||
margin-top: -70px;
|
||
box-sizing: border-box;
|
||
border-width: 2px;
|
||
border-style: solid;
|
||
border-color: var(--oc-primary, #0B5CFF);
|
||
border-radius: 70px;
|
||
opacity: 0.4;
|
||
animation-name: scan-pulse;
|
||
animation-duration: 400ms;
|
||
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
animation-iteration-count: 1;
|
||
animation-fill-mode: forwards;
|
||
pointer-events: none;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* 拟物实体卡片外观 */
|
||
.nfc-physical-card {
|
||
position: relative;
|
||
width: 220px;
|
||
height: 136px;
|
||
border-radius: 16px;
|
||
padding: 14px 16px;
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
box-shadow: 0 12px 28px rgba(16, 24, 40, 0.12), 0 2px 6px rgba(0, 0, 0, 0.04);
|
||
z-index: 2;
|
||
transition: all 200ms ease;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 商务白金卡 */
|
||
.card-theme-white {
|
||
background: linear-gradient(135deg, #FFFFFF 0%, #F0F4F8 100%);
|
||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||
}
|
||
|
||
/* 极客曜黑卡 */
|
||
.card-theme-black {
|
||
background: linear-gradient(135deg, #1C1F26 0%, #0F1217 100%);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.card-glass-specular {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 48px;
|
||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0) 100%);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.card-header-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.nfc-card-brand {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.8px;
|
||
}
|
||
|
||
.card-theme-white .nfc-card-brand {
|
||
color: #101828;
|
||
}
|
||
|
||
.card-theme-black .nfc-card-brand {
|
||
color: #F2F4F7;
|
||
}
|
||
|
||
/* 拟物 NFC 信号微图标 */
|
||
.nfc-icon-wrap {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 2px;
|
||
}
|
||
|
||
.nfc-signal-bar {
|
||
width: 3px;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.bar-1 {
|
||
height: 6px;
|
||
}
|
||
|
||
.bar-2 {
|
||
height: 10px;
|
||
}
|
||
|
||
.bar-3 {
|
||
height: 14px;
|
||
}
|
||
|
||
.card-theme-white .nfc-signal-bar {
|
||
background-color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.card-theme-black .nfc-signal-bar {
|
||
background-color: #3894FF;
|
||
}
|
||
|
||
/* 拟物芯片位置 */
|
||
.card-chip-area {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.chip-hardware {
|
||
width: 24px;
|
||
height: 18px;
|
||
border-radius: 4px;
|
||
background: linear-gradient(135deg, #E6CA85 0%, #C4A152 100%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-evenly;
|
||
padding: 2px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.chip-line {
|
||
height: 1px;
|
||
background-color: rgba(0, 0, 0, 0.25);
|
||
}
|
||
|
||
.nfc-card-chip {
|
||
font-size: 10px;
|
||
font-family: monospace;
|
||
}
|
||
|
||
.card-theme-white .nfc-card-chip {
|
||
color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
.card-theme-black .nfc-card-chip {
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.card-footer-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.card-uid-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.card-uid-caption {
|
||
font-size: 9px;
|
||
font-weight: 500;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.card-theme-white .card-uid-caption {
|
||
color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
.card-theme-black .card-uid-caption {
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.card-uid-val {
|
||
font-size: 11px;
|
||
font-family: monospace;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.card-theme-white .card-uid-val {
|
||
color: var(--oc-text, #101828);
|
||
}
|
||
|
||
.card-theme-black .card-uid-val {
|
||
color: #F2F4F7;
|
||
}
|
||
|
||
.card-badge {
|
||
padding: 2px 8px;
|
||
border-radius: 9999px;
|
||
}
|
||
|
||
.card-theme-white .card-badge {
|
||
background-color: rgba(0, 136, 255, 0.1);
|
||
}
|
||
|
||
.card-theme-black .card-badge {
|
||
background-color: rgba(255, 255, 255, 0.12);
|
||
}
|
||
|
||
.card-badge-txt {
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.card-theme-white .card-badge-txt {
|
||
color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.card-theme-black .card-badge-txt {
|
||
color: #3894FF;
|
||
}
|
||
|
||
/* 标题与说明 */
|
||
.scan-title {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: var(--oc-text, #101828);
|
||
margin-block-end: var(--oc-space-8, 8px);
|
||
line-height: 1.25;
|
||
text-align: center;
|
||
}
|
||
|
||
.scan-desc {
|
||
font-size: 13px;
|
||
color: var(--oc-text-2, #667085);
|
||
line-height: 1.5;
|
||
text-align: center;
|
||
margin-block-end: var(--oc-space-12, 12px);
|
||
}
|
||
|
||
.scan-status {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 6px;
|
||
background-color: var(--oc-bg, #F7F8FA);
|
||
padding: 6px 14px;
|
||
border-radius: 9999px;
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
}
|
||
|
||
.status-error {
|
||
background-color: #FEF3F2;
|
||
border-color: #FECDCA;
|
||
}
|
||
|
||
.status-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 3px;
|
||
background-color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.status-error .status-dot {
|
||
background-color: #F04438;
|
||
}
|
||
|
||
.scan-status-text {
|
||
font-size: 12px;
|
||
color: var(--oc-text, #101828);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.status-error .scan-status-text {
|
||
color: #B42318;
|
||
}
|
||
|
||
/* 2. 激活结果展示 Bento */
|
||
.result-bento {
|
||
background-color: var(--oc-surface, #FFFFFF);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: var(--oc-radius-card, 20px);
|
||
padding: var(--oc-space-16, 16px);
|
||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.04);
|
||
margin-block-end: var(--oc-space-16, 16px);
|
||
}
|
||
|
||
.result-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-block-end: var(--oc-space-16, 16px);
|
||
padding-block-end: var(--oc-space-12, 12px);
|
||
border-bottom: 1px solid var(--oc-border, #E3E7ED);
|
||
}
|
||
|
||
.result-success-icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 18px;
|
||
background-color: #ECFDF3;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.result-icon-check {
|
||
color: #12B76A;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.result-header-text {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.result-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--oc-text, #101828);
|
||
}
|
||
|
||
.result-desc {
|
||
font-size: 12px;
|
||
color: var(--oc-text-2, #667085);
|
||
margin-block-start: 2px;
|
||
}
|
||
|
||
.meta-grid {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-block-end: var(--oc-space-12, 12px);
|
||
}
|
||
|
||
.meta-cell {
|
||
flex: 1;
|
||
min-width: 45%;
|
||
background-color: var(--oc-bg, #F7F8FA);
|
||
border-radius: 12px;
|
||
padding: 10px 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.meta-label {
|
||
font-size: 11px;
|
||
color: var(--oc-text-2, #667085);
|
||
margin-block-end: 2px;
|
||
}
|
||
|
||
.meta-value {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--oc-text, #101828);
|
||
}
|
||
|
||
.uppercase-txt {
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.re-scan-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding-block: 8px;
|
||
}
|
||
|
||
.re-scan-text {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
/* 3. 三步极速激活指引 Bento */
|
||
.guide-bento {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 8px;
|
||
margin-block-end: var(--oc-space-16, 16px);
|
||
}
|
||
|
||
.step-card {
|
||
flex: 1;
|
||
background-color: var(--oc-surface, #FFFFFF);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: 16px;
|
||
padding: 12px 10px;
|
||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.02);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
text-align: center;
|
||
}
|
||
|
||
.step-badge {
|
||
width: 22px;
|
||
height: 22px;
|
||
border-radius: 11px;
|
||
background-color: rgba(0, 136, 255, 0.1);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-block-end: 6px;
|
||
}
|
||
|
||
.step-num {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.step-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--oc-text, #101828);
|
||
margin-block-end: 2px;
|
||
}
|
||
|
||
.step-desc {
|
||
font-size: 10px;
|
||
color: var(--oc-text-2, #667085);
|
||
line-height: 1.3;
|
||
}
|
||
|
||
/* 4. 表单与硬件参数 Bento */
|
||
.scan-form {
|
||
background-color: var(--oc-surface, #FFFFFF);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: var(--oc-radius-card, 20px);
|
||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.04);
|
||
padding: var(--oc-space-16, 16px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.form-header-bar {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-block-end: var(--oc-space-12, 12px);
|
||
padding-block-end: var(--oc-space-8, 8px);
|
||
border-bottom: 1px solid var(--oc-border, #E3E7ED);
|
||
}
|
||
|
||
.form-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--oc-text, #101828);
|
||
}
|
||
|
||
.form-badge-proto {
|
||
font-size: 10px;
|
||
color: var(--oc-text-2, #667085);
|
||
background-color: var(--oc-bg, #F7F8FA);
|
||
padding: 2px 8px;
|
||
border-radius: 9999px;
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
}
|
||
|
||
.field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-block-end: var(--oc-space-12, 12px);
|
||
}
|
||
|
||
.field:last-child {
|
||
margin-block-end: 0;
|
||
}
|
||
|
||
.field-label {
|
||
font-size: 13px;
|
||
color: var(--oc-text, #101828);
|
||
font-weight: 600;
|
||
margin-block-end: 6px;
|
||
}
|
||
|
||
.input-wrapper {
|
||
background-color: var(--oc-bg, #F7F8FA);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: var(--oc-radius-btn, 14px);
|
||
padding-inline: var(--oc-space-12, 12px);
|
||
transition: border-color 150ms ease;
|
||
}
|
||
|
||
.field-input {
|
||
height: 44px;
|
||
font-size: 15px;
|
||
color: var(--oc-text, #101828);
|
||
border-width: 0;
|
||
background-color: transparent;
|
||
}
|
||
|
||
.scan-spacer {
|
||
height: 16px;
|
||
}
|
||
|
||
/* 5. 底部固定 CTA Bar */
|
||
.scan-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: var(--oc-surface, #FFFFFF);
|
||
border-top: 1px solid var(--oc-border, #E3E7ED);
|
||
box-sizing: border-box;
|
||
z-index: 100;
|
||
}
|
||
|
||
.primary-cta-btn {
|
||
height: 48px;
|
||
min-height: 44px;
|
||
background-color: var(--oc-primary, #0B5CFF);
|
||
border-radius: var(--oc-radius-btn, 14px);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
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.85;
|
||
}
|
||
|
||
.primary-cta-btn-disabled {
|
||
background-color: 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);
|
||
}
|
||
|
||
/* 脉冲微动画:400ms 单次,不循环(仅 opacity,无 scale) */
|
||
@keyframes scan-pulse {
|
||
0% {
|
||
opacity: 0.4;
|
||
}
|
||
50% {
|
||
opacity: 1;
|
||
}
|
||
100% {
|
||
opacity: 0.4;
|
||
}
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
* {
|
||
animation: none !important;
|
||
transition: none !important;
|
||
}
|
||
|
||
.primary-cta-btn {
|
||
transition: none;
|
||
}
|
||
|
||
.scan-ring {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
</style>
|