899 行
21 KiB
Plaintext
899 行
21 KiB
Plaintext
<template>
|
||
<view class="login-page" :class="pageRtlClass" :style="pageTheme">
|
||
<!-- 顶部极光弥散微光晕背景 (受控光晕 < 8%) -->
|
||
<view class="aurora-glow-spot"></view>
|
||
|
||
<!-- 顶部导航轻工具条 -->
|
||
<view class="top-nav-bar">
|
||
<view
|
||
class="nav-back-action"
|
||
role="button"
|
||
aria-label="Back"
|
||
@click="handleGoBack"
|
||
>
|
||
<text class="nav-back-arrow">‹</text>
|
||
</view>
|
||
<view class="nav-locale-action">
|
||
<i18n-selector :model-value="currentLocale" @change="handleLocaleChange" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 主视口内容容器 -->
|
||
<view class="login-viewport">
|
||
<!-- 品牌 Hero 模组 -->
|
||
<view class="brand-hero">
|
||
<view class="logo-box">
|
||
<image class="brand-logo" src="/static/logo.svg" mode="aspectFit" />
|
||
</view>
|
||
<view class="brand-badge">
|
||
<view class="badge-dot"></view>
|
||
<text class="badge-text">{{ t('brandTag') }}</text>
|
||
</view>
|
||
<text class="brand-title">{{ t('title') }}</text>
|
||
<text class="brand-subtitle">{{ t('subtitle') }}</text>
|
||
</view>
|
||
|
||
<!-- Bento Grid 纯白质感主卡片 -->
|
||
<view class="bento-auth-card">
|
||
<!-- 分段式认证模式切换器 (Segmented Tab Bar) -->
|
||
<view class="segmented-tabs" role="tablist">
|
||
<view
|
||
class="segment-item"
|
||
:class="{ 'segment-item-active': state.tab === 'password' }"
|
||
role="tab"
|
||
:aria-selected="state.tab === 'password'"
|
||
@click="handleSwitchTab('password')"
|
||
>
|
||
<text class="segment-label" :class="{ 'segment-label-active': state.tab === 'password' }">
|
||
{{ t('tabPassword') }}
|
||
</text>
|
||
</view>
|
||
<view
|
||
class="segment-item"
|
||
:class="{ 'segment-item-active': state.tab === 'phone' }"
|
||
role="tab"
|
||
:aria-selected="state.tab === 'phone'"
|
||
@click="handleSwitchTab('phone')"
|
||
>
|
||
<text class="segment-label" :class="{ 'segment-label-active': state.tab === 'phone' }">
|
||
{{ t('tabPhone') }}
|
||
</text>
|
||
</view>
|
||
<view
|
||
class="segment-item"
|
||
:class="{ 'segment-item-active': state.tab === 'email' }"
|
||
role="tab"
|
||
:aria-selected="state.tab === 'email'"
|
||
@click="handleSwitchTab('email')"
|
||
>
|
||
<text class="segment-label" :class="{ 'segment-label-active': state.tab === 'email' }">
|
||
{{ t('tabEmail') }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 动态认证表单模组 -->
|
||
<view class="form-content">
|
||
<!-- 1. 账号密码登录模式 -->
|
||
<view v-if="state.tab === 'password'" class="tab-panel">
|
||
<view class="field-item">
|
||
<input
|
||
class="field-input"
|
||
type="text"
|
||
v-model="state.username"
|
||
:placeholder="t('usernamePlaceholder')"
|
||
:disabled="state.submitting"
|
||
/>
|
||
</view>
|
||
<view class="field-item">
|
||
<input
|
||
class="field-input"
|
||
type="password"
|
||
v-model="state.password"
|
||
:placeholder="t('passwordPlaceholder')"
|
||
:disabled="state.submitting"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 快捷体验测试账号 Bento 胶囊 -->
|
||
<view
|
||
class="quick-demo-pill"
|
||
role="button"
|
||
:aria-label="t('testAccountTip')"
|
||
@click="handleAutofillTest"
|
||
>
|
||
<text class="demo-tag">DEMO</text>
|
||
<text class="demo-text">{{ t('testAccountTip') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 2. 国际手机号验证码模式 -->
|
||
<view v-else-if="state.tab === 'phone'" class="tab-panel">
|
||
<view class="field-item phone-row">
|
||
<view class="country-pill">
|
||
<text class="country-text">{{ state.countryCode }}</text>
|
||
<text class="country-arrow">▾</text>
|
||
</view>
|
||
<input
|
||
class="field-input phone-input"
|
||
type="number"
|
||
v-model="state.phone"
|
||
:placeholder="t('phonePlaceholder')"
|
||
:disabled="state.submitting"
|
||
/>
|
||
</view>
|
||
<view class="field-item sms-row">
|
||
<input
|
||
class="field-input sms-input"
|
||
type="number"
|
||
v-model="state.smsCode"
|
||
:placeholder="t('smsCode')"
|
||
:disabled="state.submitting"
|
||
/>
|
||
<button
|
||
class="sms-send-btn"
|
||
:class="{ 'sms-send-btn-disabled': state.countdown > 0 }"
|
||
:disabled="state.countdown > 0"
|
||
@click="handleSendSmsCode"
|
||
>
|
||
<text class="sms-send-text">{{ smsBtnText }}</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 3. 工作邮箱 Magic Link 模式 -->
|
||
<view v-else-if="state.tab === 'email'" class="tab-panel">
|
||
<view class="field-item">
|
||
<input
|
||
class="field-input"
|
||
type="text"
|
||
v-model="state.email"
|
||
:placeholder="t('emailPlaceholder')"
|
||
:disabled="state.submitting"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 错误提示柔和条 -->
|
||
<view v-if="state.errorMessage !== ''" class="error-banner">
|
||
<view class="error-icon-box">
|
||
<text class="error-icon-mark">!</text>
|
||
</view>
|
||
<text class="error-text">{{ state.errorMessage }}</text>
|
||
</view>
|
||
|
||
<!-- 单屏唯一醒目主 CTA -->
|
||
<button
|
||
class="primary-cta-btn"
|
||
:class="{ 'primary-cta-btn-loading': state.submitting }"
|
||
role="button"
|
||
:aria-label="loginAriaLabel"
|
||
@click="handlePrimarySubmit"
|
||
>
|
||
<text class="primary-cta-text">{{ loginCtaLabel }}</text>
|
||
</button>
|
||
|
||
<!-- 国际标配社交登录入口 -->
|
||
<view class="social-divider">
|
||
<view class="divider-line"></view>
|
||
<text class="divider-text">{{ t('orContinueWith') }}</text>
|
||
<view class="divider-line"></view>
|
||
</view>
|
||
|
||
<view class="social-actions">
|
||
<view
|
||
class="social-pill"
|
||
role="button"
|
||
:aria-label="t('google')"
|
||
@click="handleSocialLogin('google')"
|
||
>
|
||
<text class="social-badge-icon google-icon">G</text>
|
||
<text class="social-pill-text">{{ t('google') }}</text>
|
||
</view>
|
||
<view
|
||
class="social-pill"
|
||
role="button"
|
||
:aria-label="t('apple')"
|
||
@click="handleSocialLogin('apple')"
|
||
>
|
||
<text class="social-badge-icon apple-icon"></text>
|
||
<text class="social-pill-text">{{ t('apple') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部合规与协议说明 -->
|
||
<view class="footer-legal">
|
||
<text class="legal-text">{{ t('termsNotice') }}</text>
|
||
<text class="legal-link" @click="handleOpenTerms">{{ t('termsOfService') }}</text>
|
||
<text class="legal-text">{{ t('and') }}</text>
|
||
<text class="legal-link" @click="handleOpenPrivacy">{{ t('privacyPolicy') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { utsLightColors, utsRadius, utsSpacing, utsTypography } from '../../common/tokens.uts';
|
||
import { getCurrentLocale, isRtl, setAppLocale, OmniLocale } from '../../common/locale.uts';
|
||
import {
|
||
createLoginState,
|
||
submitPasswordLogin,
|
||
submitPhoneLogin,
|
||
sendMagicLink,
|
||
sendSmsCode,
|
||
tapSocialLogin,
|
||
fillTestAccount,
|
||
tickCountdown,
|
||
switchLoginTab,
|
||
resolveLoginText,
|
||
LoginTab,
|
||
SocialProvider,
|
||
LoginState,
|
||
} from './useLogin.uts';
|
||
|
||
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-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(createLoginState());
|
||
const currentLocale = ref<string>(getCurrentLocale());
|
||
let countdownTimerId: number | null = null;
|
||
|
||
const pageRtlClass = computed((): string => {
|
||
return isRtl(currentLocale.value) ? 'login-rtl' : '';
|
||
});
|
||
|
||
const loginCtaLabel = computed((): string => {
|
||
if (state.submitting) {
|
||
return t('loggingIn');
|
||
}
|
||
if (state.tab === 'email') {
|
||
return t('magicLink');
|
||
}
|
||
return t('login');
|
||
});
|
||
|
||
const loginAriaLabel = computed((): string => {
|
||
return loginCtaLabel.value;
|
||
});
|
||
|
||
const smsBtnText = computed((): string => {
|
||
if (state.countdown > 0) {
|
||
return `${state.countdown}${t('countdownSuffix')}`;
|
||
}
|
||
return t('sendCode');
|
||
});
|
||
|
||
function t(key: string): string {
|
||
const _ = currentLocale.value;
|
||
return resolveLoginText(key, currentLocale.value);
|
||
}
|
||
|
||
function handleLocaleChange(loc: OmniLocale): void {
|
||
setAppLocale(loc);
|
||
currentLocale.value = loc;
|
||
}
|
||
|
||
function handleSwitchTab(tab: LoginTab): void {
|
||
switchLoginTab(state, tab);
|
||
}
|
||
|
||
function handleAutofillTest(): void {
|
||
fillTestAccount(state);
|
||
}
|
||
|
||
function handleSendSmsCode(): void {
|
||
const ok = sendSmsCode(state);
|
||
if (ok) {
|
||
startCountdown();
|
||
}
|
||
}
|
||
|
||
function startCountdown(): void {
|
||
if (countdownTimerId != null) {
|
||
return;
|
||
}
|
||
countdownTimerId = setInterval(() => {
|
||
tickCountdown(state);
|
||
if (state.countdown <= 0) {
|
||
stopCountdown();
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
function stopCountdown(): void {
|
||
if (countdownTimerId != null) {
|
||
clearInterval(countdownTimerId);
|
||
countdownTimerId = null;
|
||
}
|
||
}
|
||
|
||
function handlePrimarySubmit(): void {
|
||
if (state.tab === 'password') {
|
||
submitPasswordLogin(state);
|
||
} else if (state.tab === 'phone') {
|
||
submitPhoneLogin(state);
|
||
} else if (state.tab === 'email') {
|
||
sendMagicLink(state);
|
||
}
|
||
}
|
||
|
||
function handleSocialLogin(provider: SocialProvider): void {
|
||
tapSocialLogin(provider);
|
||
}
|
||
|
||
function handleGoBack(): void {
|
||
try {
|
||
const pages = getCurrentPages();
|
||
if (pages.length > 1) {
|
||
uni.navigateBack();
|
||
return;
|
||
}
|
||
} catch (e) {}
|
||
uni.switchTab({
|
||
url: '/pages/owner/card/index',
|
||
});
|
||
}
|
||
|
||
function handleOpenTerms(): void {
|
||
uni.showToast({
|
||
title: t('termsOfService'),
|
||
icon: 'none',
|
||
});
|
||
}
|
||
|
||
function handleOpenPrivacy(): void {
|
||
uni.showToast({
|
||
title: t('privacyPolicy'),
|
||
icon: 'none',
|
||
});
|
||
}
|
||
|
||
onUnmounted(() => {
|
||
stopCountdown();
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.login-page {
|
||
position: relative;
|
||
min-height: 100vh;
|
||
background-color: var(--oc-bg, #F5F8FC);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
box-sizing: border-box;
|
||
padding-inline: var(--oc-space-16, 16px);
|
||
padding-top: 16px;
|
||
padding-bottom: calc(36px + env(safe-area-inset-bottom, 0px));
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.login-rtl {
|
||
direction: rtl;
|
||
}
|
||
|
||
/* 受控极光弥散微光斑 (< 8% 视口面积) */
|
||
.aurora-glow-spot {
|
||
position: absolute;
|
||
top: -60px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 280px;
|
||
height: 220px;
|
||
background: linear-gradient(135deg, rgba(0, 210, 255, 0.16) 0%, rgba(0, 136, 255, 0.12) 50%, rgba(99, 102, 241, 0.08) 100%);
|
||
filter: blur(40px);
|
||
border-radius: 9999px;
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
|
||
/* 顶部导航工具栏 (触控热区 >= 44x44) */
|
||
.top-nav-bar {
|
||
position: relative;
|
||
z-index: 1;
|
||
width: 100%;
|
||
max-width: 420px;
|
||
height: 44px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-block-end: 12px;
|
||
}
|
||
|
||
.nav-back-action {
|
||
width: 44px;
|
||
height: 44px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 9999px;
|
||
background-color: rgba(255, 255, 255, 0.7);
|
||
border: 1px solid var(--oc-border, #EBF1F7);
|
||
}
|
||
|
||
.nav-back-action:active {
|
||
background-color: #FFFFFF;
|
||
}
|
||
|
||
.nav-back-arrow {
|
||
font-size: 26px;
|
||
color: var(--oc-text, #101828);
|
||
line-height: 1;
|
||
}
|
||
|
||
.nav-locale-action {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
/* 主视口居中容器 (移动端限制 400px 以内自适应) */
|
||
.login-viewport {
|
||
position: relative;
|
||
z-index: 1;
|
||
width: 100%;
|
||
max-width: 380px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
/* 品牌 Hero 模组 */
|
||
.brand-hero {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-block-end: 20px;
|
||
}
|
||
|
||
.logo-box {
|
||
width: 56px;
|
||
height: 56px;
|
||
background-color: var(--oc-surface, #FFFFFF);
|
||
border-radius: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 8px 24px rgba(0, 136, 255, 0.1);
|
||
border: 1px solid rgba(235, 241, 247, 0.8);
|
||
margin-block-end: 12px;
|
||
}
|
||
|
||
.brand-logo {
|
||
width: 38px;
|
||
height: 38px;
|
||
}
|
||
|
||
.brand-badge {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding-inline: 10px 12px;
|
||
padding-block: 4px;
|
||
background-color: var(--oc-primary-soft, #EBF5FF);
|
||
border-radius: 9999px;
|
||
margin-block-end: 10px;
|
||
}
|
||
|
||
.badge-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background-color: var(--oc-primary, #0088FF);
|
||
margin-inline-end: 6px;
|
||
}
|
||
|
||
.badge-text {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: var(--oc-primary, #0088FF);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.brand-title {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: var(--oc-text, #101828);
|
||
line-height: 1.25;
|
||
letter-spacing: -0.02em;
|
||
text-align: center;
|
||
margin-block-end: 6px;
|
||
}
|
||
|
||
.brand-subtitle {
|
||
font-size: 13px;
|
||
color: var(--oc-text-2, #64748B);
|
||
line-height: 1.5;
|
||
text-align: center;
|
||
}
|
||
|
||
/* Bento Grid 纯白质感主卡片容器 */
|
||
.bento-auth-card {
|
||
width: 100%;
|
||
background-color: var(--oc-surface, #FFFFFF);
|
||
border-radius: var(--oc-radius-card, 20px);
|
||
border: 1px solid var(--oc-border, #EBF1F7);
|
||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.04), 0 2px 8px rgba(11, 99, 229, 0.03);
|
||
padding: 20px 18px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* 分段式 Tab 切换器 (Segmented Control) */
|
||
.segmented-tabs {
|
||
display: flex;
|
||
flex-direction: row;
|
||
background-color: #F1F5F9;
|
||
border-radius: 12px;
|
||
padding: 3px;
|
||
margin-block-end: 18px;
|
||
}
|
||
|
||
.segment-item {
|
||
flex: 1;
|
||
height: 36px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 9px;
|
||
transition: all 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
}
|
||
|
||
.segment-item-active {
|
||
background-color: #FFFFFF;
|
||
box-shadow: 0 2px 6px rgba(16, 24, 40, 0.06);
|
||
}
|
||
|
||
.segment-label {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--oc-text-2, #64748B);
|
||
}
|
||
|
||
.segment-label-active {
|
||
color: var(--oc-text, #101828);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 表单主体内容区 */
|
||
.form-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.tab-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.field-item {
|
||
margin-block-end: 12px;
|
||
}
|
||
|
||
.field-input {
|
||
width: 100%;
|
||
height: 46px;
|
||
padding-inline: 14px;
|
||
font-size: 15px;
|
||
color: var(--oc-text, #101828);
|
||
background-color: #F8FAFC;
|
||
border: 1px solid var(--oc-border, #EBF1F7);
|
||
border-radius: 12px;
|
||
box-sizing: border-box;
|
||
transition: border-color 150ms cubic-bezier(0.2, 0.8, 0.2, 1), background-color 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
}
|
||
|
||
.field-input:focus {
|
||
background-color: #FFFFFF;
|
||
border-color: var(--oc-primary, #0088FF);
|
||
}
|
||
|
||
/* 快捷测试账号 Bento 胶囊 */
|
||
.quick-demo-pill {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
background-color: #F8FAFC;
|
||
border: 1px dashed #CBD5E1;
|
||
border-radius: 10px;
|
||
padding: 8px 12px;
|
||
min-height: 40px;
|
||
margin-block-end: 14px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.quick-demo-pill:active {
|
||
background-color: var(--oc-primary-soft, #EBF5FF);
|
||
border-color: var(--oc-primary, #0088FF);
|
||
}
|
||
|
||
.demo-tag {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: #FFFFFF;
|
||
background-color: var(--oc-primary, #0088FF);
|
||
padding-inline: 6px;
|
||
padding-block: 2px;
|
||
border-radius: 4px;
|
||
margin-inline-end: 8px;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.demo-text {
|
||
font-size: 12px;
|
||
color: var(--oc-text-2, #64748B);
|
||
line-height: 1.4;
|
||
flex: 1;
|
||
}
|
||
|
||
/* 手机号复合输入行 */
|
||
.phone-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.country-pill {
|
||
width: 72px;
|
||
height: 46px;
|
||
background-color: #F8FAFC;
|
||
border: 1px solid var(--oc-border, #EBF1F7);
|
||
border-radius: 12px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.country-text {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--oc-text, #101828);
|
||
}
|
||
|
||
.country-arrow {
|
||
font-size: 10px;
|
||
color: var(--oc-text-2, #64748B);
|
||
}
|
||
|
||
.phone-input {
|
||
flex: 1;
|
||
}
|
||
|
||
/* 验证码复合输入行 */
|
||
.sms-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.sms-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.sms-send-btn {
|
||
width: 108px;
|
||
height: 46px;
|
||
background-color: var(--oc-primary-soft, #EBF5FF);
|
||
border: 1px solid rgba(0, 136, 255, 0.2);
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0;
|
||
}
|
||
|
||
.sms-send-btn::after {
|
||
border: none;
|
||
}
|
||
|
||
.sms-send-btn:active {
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.sms-send-btn-disabled {
|
||
background-color: #F1F5F9;
|
||
border-color: #E2E8F0;
|
||
}
|
||
|
||
.sms-send-text {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--oc-primary, #0088FF);
|
||
}
|
||
|
||
.sms-send-btn-disabled .sms-send-text {
|
||
color: #94A3B8;
|
||
}
|
||
|
||
/* 柔和错误提示条 */
|
||
.error-banner {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
background-color: #FEF3F2;
|
||
border: 1px solid #FECDCA;
|
||
border-radius: 10px;
|
||
padding: 8px 12px;
|
||
margin-block-end: 14px;
|
||
}
|
||
|
||
.error-icon-box {
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
background-color: var(--oc-error, #F04438);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-inline-end: 8px;
|
||
}
|
||
|
||
.error-icon-mark {
|
||
color: #FFFFFF;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
}
|
||
|
||
.error-text {
|
||
font-size: 12px;
|
||
color: #B42318;
|
||
line-height: 1.4;
|
||
flex: 1;
|
||
}
|
||
|
||
/* 单屏唯一核心主 CTA 按钮 */
|
||
.primary-cta-btn {
|
||
width: 100%;
|
||
height: 48px;
|
||
min-height: 44px;
|
||
background: linear-gradient(135deg, #00A3FF 0%, #0B63E5 100%);
|
||
border-radius: var(--oc-radius-btn, 14px);
|
||
border-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 8px 20px rgba(0, 136, 255, 0.22);
|
||
margin-block-end: 18px;
|
||
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
}
|
||
|
||
.primary-cta-btn::after {
|
||
border: none;
|
||
}
|
||
|
||
.primary-cta-btn:active {
|
||
opacity: 0.88;
|
||
}
|
||
|
||
.primary-cta-btn-loading {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.primary-cta-text {
|
||
color: #FFFFFF;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
/* 分割线与国际社交快捷通道 */
|
||
.social-divider {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
margin-block-end: 14px;
|
||
}
|
||
|
||
.divider-line {
|
||
flex: 1;
|
||
height: 1px;
|
||
background-color: var(--oc-border, #EBF1F7);
|
||
}
|
||
|
||
.divider-text {
|
||
font-size: 11px;
|
||
color: var(--oc-text-2, #94A3B8);
|
||
padding-inline: 10px;
|
||
}
|
||
|
||
.social-actions {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 10px;
|
||
}
|
||
|
||
.social-pill {
|
||
flex: 1;
|
||
height: 42px;
|
||
min-height: 44px;
|
||
background-color: #F8FAFC;
|
||
border: 1px solid var(--oc-border, #EBF1F7);
|
||
border-radius: 12px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.social-pill:active {
|
||
background-color: #F1F5F9;
|
||
}
|
||
|
||
.social-badge-icon {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: var(--oc-text, #101828);
|
||
}
|
||
|
||
.social-pill-text {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--oc-text, #101828);
|
||
}
|
||
|
||
/* 底部合规协议区 */
|
||
.footer-legal {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: center;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
margin-block-start: 22px;
|
||
}
|
||
|
||
.legal-text {
|
||
font-size: 12px;
|
||
color: var(--oc-text-2, #64748B);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.legal-link {
|
||
font-size: 12px;
|
||
color: var(--oc-primary, #0088FF);
|
||
text-decoration: underline;
|
||
margin-inline: 4px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* 动效适配 */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
* {
|
||
animation: none !important;
|
||
transition: none !important;
|
||
}
|
||
}
|
||
</style>
|