| | |
| | | <template> |
| | | <view class="login-page"> |
| | | <!-- 顶部品牌区:深蓝底承载品牌 logo 与欢迎语 --> |
| | | <view class="hero" :style="{ paddingTop: statusBarHeight + 'px' }"> |
| | | <view class="nav-inner"> |
| | | <image class="brand-logo" src="/static/logo.png" mode="heightFix" /> |
| | | </view> |
| | | <view class="welcome-block"> |
| | | <text class="welcome-title">欢迎使用</text> |
| | | <view class="brand-row"> |
| | | <text class="brand-name">代谢</text> |
| | | <text class="brand-num">300</text> |
| | | <!-- 背景装饰层:3 处微透十字 + 3 处白色微点阵(置于内容之下,不拦截点击) --> |
| | | <view class="deco-cross deco-cross--tr"></view> |
| | | <view class="deco-cross deco-cross--ml"></view> |
| | | <view class="deco-cross deco-cross--bl"></view> |
| | | <view class="deco-dots deco-dots--lt"></view> |
| | | <view class="deco-dots deco-dots--rt"></view> |
| | | <view class="deco-dots deco-dots--rb"></view> |
| | | |
| | | <!-- 核心内容区:欢迎语 + 官方 Logo + 英译标语 + 医学双背书 + 产业4.0标语卡 --> |
| | | <view class="content" :style="{ paddingTop: statusBarHeight + 40 + 'px' }"> |
| | | <text class="welcome-title">欢迎来到</text> |
| | | <image class="brand-logo" src="/static/metabolism-300-logo.png" mode="widthFix" /> |
| | | <text class="en-slogan">RESET METABOLISM & DELIVER HEALTH</text> |
| | | |
| | | <!-- 医学价值双背书行:严谨医学循证 / 传递健康价值 --> |
| | | <view class="endorse-row"> |
| | | <view class="endorse-item"> |
| | | <image class="endorse-icon" src="/static/medical-plus-icon.png" mode="aspectFit" /> |
| | | <text class="endorse-text">严谨医学循证</text> |
| | | </view> |
| | | <view class="endorse-item"> |
| | | <image |
| | | class="endorse-icon endorse-icon--check" |
| | | src="/static/check-icon.png" |
| | | mode="aspectFit" |
| | | /> |
| | | <text class="endorse-text">传递健康价值</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 产业4.0核心标语卡:整图呈现,图片自带透明留白(左右对称、上下需负边距补偿) --> |
| | | <image class="slogan-card" src="/static/industry-4-0-slogan-card.png" mode="widthFix" /> |
| | | </view> |
| | | |
| | | <!-- 底部主体区:协议勾选 + 手机号一键登录 --> |
| | | <view class="login-body"> |
| | | <!-- 协议勾选 --> |
| | | <!-- 底部操作区:双按钮 + 内嵌验证码面板 + 协议行 --> |
| | | <view class="bottom-area"> |
| | | <!-- 双按钮组:微信一键登录 / 手机验证登录 --> |
| | | <view class="btn-group"> |
| | | <!-- 微信一键登录:未勾选协议时 open-type 置空,由点击事件拦截并弹窗提示 --> |
| | | <button |
| | | class="btn btn--wechat" |
| | | :open-type="agreed ? 'getPhoneNumber' : ''" |
| | | @tap="_handleWechatTap" |
| | | @getphonenumber="_handleGetPhoneNumber" |
| | | > |
| | | <image class="btn-icon" src="/static/wechat-icon.png" mode="aspectFit" /> |
| | | <text class="btn-text">微信一键登录</text> |
| | | </button> |
| | | |
| | | <!-- 手机验证登录:点击展开内嵌手机号+验证码面板(展开后隐藏本按钮) --> |
| | | <view v-if="!showCodePanel" class="btn btn--phone" @tap="_toggleCodePanel"> |
| | | <image class="btn-icon" src="/static/phone-icon.png" mode="aspectFit" /> |
| | | <text class="btn-text">手机验证登录</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 内嵌手机验证登录面板(不新增页面,当前页展开) --> |
| | | <view v-if="showCodePanel" class="code-panel"> |
| | | <!-- 收起入口:按钮已隐藏,由此恢复「手机验证登录」按钮 --> |
| | | <view class="code-panel-collapse" @tap="_toggleCodePanel">收起</view> |
| | | <view class="input-wrap"> |
| | | <input |
| | | v-model="phone" |
| | | class="field-input" |
| | | type="number" |
| | | maxlength="11" |
| | | placeholder="请输入手机号" |
| | | placeholder-style="color:#94A3B8" |
| | | /> |
| | | </view> |
| | | <view class="code-row"> |
| | | <view class="input-wrap input-wrap--flex"> |
| | | <input |
| | | v-model="code" |
| | | class="field-input" |
| | | type="number" |
| | | maxlength="6" |
| | | placeholder="请输入验证码" |
| | | placeholder-style="color:#94A3B8" |
| | | /> |
| | | </view> |
| | | <!-- 获取验证码:倒计时期间禁用,显示剩余秒数 --> |
| | | <view |
| | | class="code-btn" |
| | | :class="{ 'code-btn--disabled': counting }" |
| | | @tap="_handleSendCode" |
| | | > |
| | | {{ counting ? countdown + 's' : '获取验证码' }} |
| | | </view> |
| | | </view> |
| | | <view class="submit-btn" @tap="_handleCodeLogin">登录</view> |
| | | </view> |
| | | |
| | | <!-- 协议勾选行:复选框 + 协议文案(设计稿 16pt 复选框 / 11.5pt 文案) --> |
| | | <view class="agreement-row"> |
| | | <view |
| | | class="checkbox" |
| | | :class="{ 'checkbox--checked': agreed }" |
| | | @tap="agreed = !agreed" |
| | | ></view> |
| | | <text class="agreement-text">我已阅读并同意</text> |
| | | <text class="agreement-link" @tap.stop="_openAgreement('用户协议')">《用户协议》</text> |
| | | <text class="agreement-text">、</text> |
| | | <text class="agreement-link" @tap.stop="_openAgreement('隐私与权限协议')">《隐私与权限协议》</text> |
| | | <view class="agreement-words"> |
| | | <text class="agreement-text">我已阅读并同意</text> |
| | | <text class="agreement-link" @tap.stop="_openAgreement('用户协议')">《用户协议》</text> |
| | | <text class="agreement-text">与</text> |
| | | <text class="agreement-link" @tap.stop="_openAgreement('隐私与权限协议')"> |
| | | 《隐私与权限》 |
| | | </text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 手机号一键登录:未勾选协议时 open-type 置空,由点击事件拦截并弹窗提示 --> |
| | | <button |
| | | class="login-btn" |
| | | :open-type="agreed ? 'getPhoneNumber' : ''" |
| | | @tap="_handleLoginTap" |
| | | @getphonenumber="_handleGetPhoneNumber" |
| | | > |
| | | 手机号一键登录 |
| | | </button> |
| | | |
| | | <!-- 注册入口:跳转注册页 --> |
| | | <!-- 注册入口:设计稿无该入口,按业务要求保留在协议行下方(弱化样式) --> |
| | | <view class="register-entry"> |
| | | <text class="register-tip">还没有账号?</text> |
| | | <text class="register-link" @tap="_goRegister">立即注册</text> |
| | | </view> |
| | | |
| | | <!-- 底部 Home 条(设计稿 135×5pt,居中贴底) --> |
| | | <view class="home-indicator"></view> |
| | | </view> |
| | | |
| | | <!-- 未勾选协议时的确认弹窗 --> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from 'vue' |
| | | import { phoneLogin } from '@/api/index.js' |
| | | import { ref, computed } from 'vue' |
| | | import { onUnload } from '@dcloudio/uni-app' |
| | | import { phoneLogin, phoneCodeLogin, sendSmsCode } from '@/api/index.js' |
| | | import { setToken } from '@/utils/request.js' |
| | | |
| | | // ======================== 数据定义区域 ======================== |
| | |
| | | // 是否已勾选同意用户协议 |
| | | const agreed = ref(false) |
| | | |
| | | // 登录请求进行中标记,防止重复提交 |
| | | // 微信一键登录进行中标记,防止重复提交 |
| | | const logging = ref(false) |
| | | |
| | | // 验证码登录进行中标记,防止重复提交 |
| | | const codeLogging = ref(false) |
| | | |
| | | // 未勾选协议时确认弹窗的显示状态 |
| | | const showAgreementModal = ref(false) |
| | | |
| | | // 状态栏高度(px),用于顶部品牌区留白 |
| | | // 是否展开内嵌手机验证登录面板 |
| | | const showCodePanel = ref(false) |
| | | |
| | | // 验证码登录表单 |
| | | const phone = ref('') |
| | | const code = ref('') |
| | | |
| | | // 验证码倒计时剩余秒数(0 表示未在倒计时) |
| | | const countdown = ref(0) |
| | | |
| | | // 是否处于倒计时状态(用于禁用「获取验证码」) |
| | | const counting = computed(() => countdown.value > 0) |
| | | |
| | | // 倒计时定时器句柄 |
| | | let countdownTimer = null |
| | | |
| | | // 状态栏高度(px),用于顶部内容区留白 |
| | | const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0 |
| | | |
| | | // ======================== 方法定义区域 ======================== |
| | | |
| | | /** 点击登录:未勾选协议时弹窗二次确认 */ |
| | | const _handleLoginTap = () => { |
| | | /** 校验手机号格式 */ |
| | | const _isValidPhone = (phone) => /^1[3-9]\d{9}$/.test(phone) |
| | | |
| | | /** |
| | | * 协议校验:未勾选时弹出确认弹窗并返回 false |
| | | * @returns {Boolean} 是否已同意协议 |
| | | */ |
| | | const _ensureAgreed = () => { |
| | | if (!agreed.value) { |
| | | showAgreementModal.value = true |
| | | return false |
| | | } |
| | | return true |
| | | } |
| | | |
| | | /** 点击微信一键登录:未勾选协议时弹窗二次确认(授权由 button open-type 触发) */ |
| | | const _handleWechatTap = () => { |
| | | _ensureAgreed() |
| | | } |
| | | |
| | | /** 关闭协议确认弹窗 */ |
| | |
| | | } |
| | | |
| | | /** |
| | | * 同意协议:自动勾选(手机号授权必须由用户点击按钮触发, |
| | | * 同意协议:自动勾选(微信授权必须由用户手势触发, |
| | | * 故此处仅提示用户再次点击按钮完成登录) |
| | | */ |
| | | const _handleAgreementConfirm = () => { |
| | | showAgreementModal.value = false |
| | | agreed.value = true |
| | | uni.showToast({ title: '请再次点击「手机号一键登录」', icon: 'none' }) |
| | | uni.showToast({ title: '请再次点击登录按钮', icon: 'none' }) |
| | | } |
| | | |
| | | /** |
| | |
| | | logging.value = true |
| | | try { |
| | | const { data } = await phoneLogin({ code: detail.code }) |
| | | const token = (data && (data.token || data.accessToken)) || '' |
| | | if (!token) throw new Error('登录失败:未获取到登录凭证') |
| | | |
| | | setToken(token) |
| | | uni.reLaunch({ url: '/pages/index/index' }) |
| | | _saveTokenAndEnter(data) |
| | | } catch (err) { |
| | | uni.showToast({ title: err.message || '登录失败', icon: 'none' }) |
| | | } finally { |
| | | logging.value = false |
| | | } |
| | | } |
| | | |
| | | /** 展开/收起内嵌手机验证登录面板 */ |
| | | const _toggleCodePanel = () => { |
| | | showCodePanel.value = !showCodePanel.value |
| | | } |
| | | |
| | | /** 启动 30 秒倒计时,期间禁用「获取验证码」 */ |
| | | const _startCountdown = () => { |
| | | countdown.value = 30 |
| | | countdownTimer = setInterval(() => { |
| | | countdown.value -= 1 |
| | | if (countdown.value <= 0) { |
| | | clearInterval(countdownTimer) |
| | | countdownTimer = null |
| | | } |
| | | }, 1000) |
| | | } |
| | | |
| | | /** 获取登录验证码:校验协议与手机号后下发登录场景验证码 */ |
| | | const _handleSendCode = async () => { |
| | | if (counting.value) return |
| | | if (!_ensureAgreed()) return |
| | | |
| | | const inputPhone = phone.value.trim() |
| | | if (!inputPhone) { |
| | | uni.showToast({ title: '请输入手机号', icon: 'none' }) |
| | | return |
| | | } |
| | | if (!_isValidPhone(inputPhone)) { |
| | | uni.showToast({ title: '请输入正确手机号', icon: 'none' }) |
| | | return |
| | | } |
| | | |
| | | try { |
| | | // 登录场景:后端校验手机号已注册,未注册返回「该手机号未注册,请先注册」 |
| | | await sendSmsCode(inputPhone, 'login') |
| | | uni.showToast({ title: '验证码已发送', icon: 'none' }) |
| | | _startCountdown() |
| | | } catch (err) { |
| | | uni.showToast({ title: err.message || '验证码发送失败', icon: 'none' }) |
| | | } |
| | | } |
| | | |
| | | /** 提交验证码登录:手机号 + 短信验证码换取登录态 */ |
| | | const _handleCodeLogin = async () => { |
| | | if (codeLogging.value) return |
| | | if (!_ensureAgreed()) return |
| | | |
| | | const inputPhone = phone.value.trim() |
| | | const inputCode = code.value.trim() |
| | | if (!inputPhone || !_isValidPhone(inputPhone)) { |
| | | uni.showToast({ title: '请输入正确手机号', icon: 'none' }) |
| | | return |
| | | } |
| | | if (!inputCode) { |
| | | uni.showToast({ title: '请输入验证码', icon: 'none' }) |
| | | return |
| | | } |
| | | |
| | | codeLogging.value = true |
| | | try { |
| | | const { data } = await phoneCodeLogin({ phone: inputPhone, code: inputCode }) |
| | | _saveTokenAndEnter(data) |
| | | } catch (err) { |
| | | uni.showToast({ title: err.message || '登录失败', icon: 'none' }) |
| | | } finally { |
| | | codeLogging.value = false |
| | | } |
| | | } |
| | | |
| | | /** 保存令牌并进入首页(两种登录方式共用的收尾逻辑) */ |
| | | const _saveTokenAndEnter = (data) => { |
| | | // 后端 TokenService 返回字段为 access_token |
| | | const token = (data && (data.access_token || data.token)) || '' |
| | | if (!token) throw new Error('登录失败:未获取到登录凭证') |
| | | |
| | | setToken(token) |
| | | uni.reLaunch({ url: '/pages/index/index' }) |
| | | } |
| | | |
| | | /** 跳转注册页 */ |
| | |
| | | const _openAgreement = (name) => { |
| | | uni.showToast({ title: name + '页面开发中', icon: 'none' }) |
| | | } |
| | | |
| | | // 页面卸载时清理倒计时,避免定时器泄漏 |
| | | onUnload(() => { |
| | | if (countdownTimer) clearInterval(countdownTimer) |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | /* 页面最外层:纵向弹性布局,撑满整屏 */ |
| | | /* ==================== 页面底板 ==================== */ |
| | | |
| | | /* 页面最外层:浅蓝品牌渐变底(165°,0.8 透明),纵向弹性布局撑满整屏 */ |
| | | .login-page { |
| | | position: relative; |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 100vh; |
| | | box-sizing: border-box; |
| | | overflow: hidden; |
| | | background-color: #ffffff; |
| | | background-image: linear-gradient( |
| | | 165deg, |
| | | rgba(211, 228, 248, 0.8) 0%, |
| | | rgba(188, 214, 244, 0.8) 50%, |
| | | rgba(171, 204, 236, 0.8) 100% |
| | | ); |
| | | } |
| | | |
| | | /* 顶部品牌区:深蓝底 + 品牌 logo(底色与 logo 背景 #0b3f6f 一致,使 logo 无缝融入) */ |
| | | .hero { |
| | | min-height: 38vh; |
| | | /* ==================== 背景装饰层 ==================== */ |
| | | |
| | | /* 微透十字:容器尺寸按设计稿,竖条(::before)/横条(::after)等宽居中交叉 */ |
| | | .deco-cross { |
| | | position: absolute; |
| | | z-index: 0; |
| | | pointer-events: none; |
| | | |
| | | &::before, |
| | | &::after { |
| | | content: ''; |
| | | position: absolute; |
| | | background-color: var(--cross-color); |
| | | border-radius: calc(var(--bar) / 2); |
| | | } |
| | | |
| | | /* 竖条:水平居中 */ |
| | | &::before { |
| | | left: var(--offset); |
| | | top: 0; |
| | | width: var(--bar); |
| | | height: 100%; |
| | | } |
| | | |
| | | /* 横条:垂直居中 */ |
| | | &::after { |
| | | left: 0; |
| | | top: var(--offset); |
| | | width: 100%; |
| | | height: var(--bar); |
| | | } |
| | | } |
| | | |
| | | /* 右上十字:设计稿 x290/y10,80×80pt,条宽 22 / R11,#FFFFFF35 */ |
| | | .deco-cross--tr { |
| | | top: 20rpx; |
| | | right: 10rpx; |
| | | width: 160rpx; |
| | | height: 160rpx; |
| | | --bar: 44rpx; |
| | | --offset: 58rpx; |
| | | --cross-color: rgba(255, 255, 255, 0.208); |
| | | } |
| | | |
| | | /* 中左十字:设计稿 x-24/y470,68×68pt,条宽 18 / R9,#FFFFFF30 */ |
| | | .deco-cross--ml { |
| | | top: 57.9%; |
| | | left: -48rpx; |
| | | width: 136rpx; |
| | | height: 136rpx; |
| | | --bar: 36rpx; |
| | | --offset: 50rpx; |
| | | --cross-color: rgba(255, 255, 255, 0.188); |
| | | } |
| | | |
| | | /* 左下十字:设计稿 x-10/y710,100×100pt,条宽 28 / R14,#FFFFFF35 */ |
| | | .deco-cross--bl { |
| | | bottom: 4rpx; |
| | | left: -20rpx; |
| | | width: 200rpx; |
| | | height: 200rpx; |
| | | --bar: 56rpx; |
| | | --offset: 72rpx; |
| | | --cross-color: rgba(255, 255, 255, 0.208); |
| | | } |
| | | |
| | | /* |
| | | * 白色微点阵:微点 3×3pt(6rpx),行内/行间 gap 10pt(20rpx) → 平铺步距 26rpx |
| | | * 说明:设计稿三处点阵均为不规则装饰形状,此处以平铺网格近似 |
| | | */ |
| | | .deco-dots { |
| | | position: absolute; |
| | | z-index: 0; |
| | | pointer-events: none; |
| | | background-image: radial-gradient(circle, rgba(255, 255, 255, 0.333) 3rpx, transparent 3rpx); |
| | | background-size: 26rpx 26rpx; |
| | | } |
| | | |
| | | /* 左上点阵:设计稿 x10/y200(5 列 × 4 行) */ |
| | | .deco-dots--lt { |
| | | top: 24.6%; |
| | | left: 20rpx; |
| | | width: 110rpx; |
| | | height: 84rpx; |
| | | } |
| | | |
| | | /* 右上点阵:设计稿 x310/y130(3 列 × 6 行) */ |
| | | .deco-dots--rt { |
| | | top: 16%; |
| | | right: 72rpx; |
| | | width: 58rpx; |
| | | height: 136rpx; |
| | | } |
| | | |
| | | /* 右下点阵:设计稿 x240/y752(3 列 × 3 行) */ |
| | | .deco-dots--rb { |
| | | bottom: 62rpx; |
| | | right: 212rpx; |
| | | width: 58rpx; |
| | | height: 58rpx; |
| | | } |
| | | |
| | | /* ==================== 核心内容区 ==================== */ |
| | | |
| | | /* 内容区:x28/y100,宽 319pt,元素间距 20pt(40rpx),整体水平居中 */ |
| | | .content { |
| | | position: relative; |
| | | z-index: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | background-color: #0b3f6f; |
| | | /* 底部留出间距,避免欢迎语贴近色块下边缘 */ |
| | | padding-bottom: 90rpx; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | /* 品牌行:固定高度,与小程序右上角胶囊高度对齐 */ |
| | | .nav-inner { |
| | | height: 88rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | padding-left: 28rpx; |
| | | box-sizing: border-box; |
| | | gap: 40rpx; |
| | | padding-left: 56rpx; |
| | | padding-right: 56rpx; |
| | | } |
| | | |
| | | .brand-logo { |
| | | height: 56rpx; |
| | | display: block; |
| | | } |
| | | |
| | | /* 欢迎语与品牌名:margin-top 自动撑开上方空间,使其贴近品牌区底部 */ |
| | | .welcome-block { |
| | | margin-top: auto; |
| | | padding: 0 40rpx; |
| | | } |
| | | |
| | | /* 欢迎语:26pt 500 深板岩色,居中 */ |
| | | .welcome-title { |
| | | display: block; |
| | | font-size: 44rpx; |
| | | font-weight: 700; |
| | | color: #ffffff; |
| | | line-height: 62rpx; |
| | | flex-shrink: 0; |
| | | font-size: 52rpx; |
| | | font-weight: 500; |
| | | line-height: 74rpx; |
| | | text-align: center; |
| | | color: #0f172a; |
| | | } |
| | | |
| | | .brand-row { |
| | | display: flex; |
| | | align-items: flex-end; |
| | | margin-top: 24rpx; |
| | | /* 官方 Logo:设计稿 220×76pt(图 660×228,比例一致) */ |
| | | .brand-logo { |
| | | flex-shrink: 0; |
| | | width: 440rpx; |
| | | height: 152rpx; |
| | | } |
| | | |
| | | .brand-name { |
| | | font-size: 44rpx; |
| | | font-weight: 700; |
| | | color: #ffffff; |
| | | line-height: 62rpx; |
| | | /* 英译标语:11pt 600 深海蓝,字间距 1.5pt,居中 */ |
| | | .en-slogan { |
| | | flex-shrink: 0; |
| | | font-size: 22rpx; |
| | | font-weight: 600; |
| | | letter-spacing: 3rpx; |
| | | text-align: center; |
| | | color: #003e7e; |
| | | } |
| | | |
| | | /* 品牌数字:绿色大号斜体,突出代谢 300 品牌标识 */ |
| | | .brand-num { |
| | | margin-left: 16rpx; |
| | | font-size: 96rpx; |
| | | font-weight: 800; |
| | | font-style: italic; |
| | | color: #8bc34a; |
| | | line-height: 96rpx; |
| | | } |
| | | |
| | | /* 主体区:承载协议勾选与登录按钮 */ |
| | | .login-body { |
| | | flex: 1; |
| | | padding: 300rpx 32rpx 0; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | /* 协议勾选行:整体居中排列 */ |
| | | .agreement-row { |
| | | /* 医学双背书行:占满整行,两项 gap 24pt(48rpx),居中 */ |
| | | .endorse-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-wrap: wrap; |
| | | margin-bottom: 44rpx; |
| | | gap: 48rpx; |
| | | width: 100%; |
| | | } |
| | | |
| | | /* 单项:图标 + 文案,gap 6pt(12rpx) */ |
| | | .endorse-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12rpx; |
| | | } |
| | | |
| | | /* 加号图标 15×15pt → 30rpx */ |
| | | .endorse-icon { |
| | | width: 30rpx; |
| | | height: 30rpx; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | /* 勾选图标 18.2×15pt → 36.4×30rpx */ |
| | | .endorse-icon--check { |
| | | width: 36.4rpx; |
| | | height: 30rpx; |
| | | } |
| | | |
| | | /* 背书文案:12pt 600 正文强色 */ |
| | | .endorse-text { |
| | | font-size: 24rpx; |
| | | font-weight: 600; |
| | | color: #1e293b; |
| | | } |
| | | |
| | | /* |
| | | * 产业4.0核心标语卡:整图呈现 |
| | | * 图 1141×457,卡片描边 bbox [90,72]-[1049,347](视觉 959×275) |
| | | * 目标卡片视觉宽 = 319pt = 638rpx → 图片宽 759rpx、高 304rpx |
| | | * 透明留白:上 48rpx / 下 72rpx 用负边距抵消,左右对称无需补偿 |
| | | */ |
| | | .slogan-card { |
| | | flex-shrink: 0; |
| | | width: 759rpx; |
| | | height: 304rpx; |
| | | margin-top: -48rpx; |
| | | margin-bottom: -72rpx; |
| | | } |
| | | |
| | | /* ==================== 底部操作区 ==================== */ |
| | | |
| | | /* 底部操作区:margin-top 自动撑开,整体贴近底部,左右与内容区同边距 */ |
| | | .bottom-area { |
| | | position: relative; |
| | | z-index: 1; |
| | | margin-top: auto; |
| | | padding: 0 56rpx; |
| | | padding-bottom: calc(32rpx + constant(safe-area-inset-bottom)); |
| | | padding-bottom: calc(32rpx + env(safe-area-inset-bottom)); |
| | | } |
| | | |
| | | /* 双按钮组:纵向排列,间距 14pt(28rpx) */ |
| | | .btn-group { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 28rpx; |
| | | } |
| | | |
| | | /* 按钮基础:H48 → 96rpx,PILL R24 → 48rpx,图标与文字 gap 8pt(16rpx) */ |
| | | .btn { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 16rpx; |
| | | width: 100%; |
| | | height: 96rpx; |
| | | /* 重置小程序 button 默认的 margin: 0 auto(否则 flex 列内宽度会收缩为内容宽) */ |
| | | margin: 0; |
| | | padding: 0; |
| | | border: none; |
| | | border-radius: 48rpx; |
| | | box-sizing: border-box; |
| | | line-height: 1; |
| | | } |
| | | |
| | | /* 按钮内图标 18×18pt → 36rpx */ |
| | | .btn-icon { |
| | | width: 36rpx; |
| | | height: 36rpx; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | /* 按钮文字:15pt 600 白色 */ |
| | | .btn-text { |
| | | font-size: 30rpx; |
| | | font-weight: 600; |
| | | color: #ffffff; |
| | | } |
| | | |
| | | /* 微信一键登录:微信绿渐变(90°)+ 绿色光晕投影 #07C16040 */ |
| | | .btn--wechat { |
| | | background-image: linear-gradient(90deg, #07c160 0%, #05a352 100%); |
| | | box-shadow: 0 12rpx 32rpx rgba(7, 193, 96, 0.25); |
| | | } |
| | | |
| | | /* 覆盖小程序 button 默认边框伪元素 */ |
| | | .btn--wechat::after { |
| | | border: none; |
| | | } |
| | | |
| | | /* 手机验证登录:医疗蓝渐变(90°)+ 蓝色光晕投影 #1E5EBC30 */ |
| | | .btn--phone { |
| | | background-image: linear-gradient(90deg, #1e5ebc 0%, #144a94 100%); |
| | | box-shadow: 0 12rpx 32rpx rgba(30, 94, 188, 0.188); |
| | | } |
| | | |
| | | /* 内嵌验证码面板:表单间距 20rpx */ |
| | | .code-panel { |
| | | margin-top: 32rpx; |
| | | } |
| | | |
| | | /* 收起入口:右对齐弱化文字,点击恢复「手机验证登录」按钮 */ |
| | | .code-panel-collapse { |
| | | margin-bottom: 20rpx; |
| | | text-align: right; |
| | | font-size: 26rpx; |
| | | color: #1e5ebc; |
| | | } |
| | | |
| | | /* 输入框:R8 → 16rpx,H44 → 88rpx,浅底 + 描边 */ |
| | | .input-wrap { |
| | | display: flex; |
| | | align-items: center; |
| | | height: 88rpx; |
| | | padding: 0 28rpx; |
| | | border: 2rpx solid #cbd5e1; |
| | | border-radius: 16rpx; |
| | | background-color: #f8fafc; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .code-row { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-top: 20rpx; |
| | | } |
| | | |
| | | .input-wrap--flex { |
| | | flex: 1; |
| | | min-width: 0; |
| | | margin-right: 16rpx; |
| | | } |
| | | |
| | | .field-input { |
| | | flex: 1; |
| | | height: 100%; |
| | | min-width: 0; |
| | | font-size: 28rpx; |
| | | color: #1e293b; |
| | | } |
| | | |
| | | /* 获取验证码:白底 + 医疗蓝描边胶囊,倒计时期间置灰 */ |
| | | .code-btn { |
| | | width: 208rpx; |
| | | height: 88rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | border: 2rpx solid #1e5ebc; |
| | | border-radius: 16rpx; |
| | | background-color: #ffffff; |
| | | color: #1e5ebc; |
| | | font-size: 26rpx; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .code-btn--disabled { |
| | | border-color: #cbd5e1; |
| | | color: #94a3b8; |
| | | } |
| | | |
| | | /* 登录提交按钮:品牌主行动渐变,PILL R24 */ |
| | | .submit-btn { |
| | | height: 96rpx; |
| | | margin-top: 28rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 48rpx; |
| | | background-image: linear-gradient(90deg, #1e5ebc 0%, #003e7e 100%); |
| | | color: #ffffff; |
| | | font-size: 30rpx; |
| | | font-weight: 600; |
| | | box-shadow: 0 12rpx 28rpx rgba(0, 62, 126, 0.2); |
| | | } |
| | | |
| | | /* 协议勾选行:与按钮组间距 19pt(38rpx),复选框与文案 gap 8pt(16rpx) */ |
| | | .agreement-row { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-top: 38rpx; |
| | | } |
| | | |
| | | /* 协议文案组合:内部字间距 1pt(2rpx) */ |
| | | .agreement-words { |
| | | display: flex; |
| | | align-items: center; |
| | | flex: 1; |
| | | min-width: 0; |
| | | gap: 2rpx; |
| | | margin-left: 16rpx; |
| | | } |
| | | |
| | | /* 勾选框:16×16pt → 32rpx,R4 → 8rpx */ |
| | | .checkbox { |
| | | position: relative; |
| | | width: 32rpx; |
| | | height: 32rpx; |
| | | border: 2rpx solid #c0c4cc; |
| | | border-radius: 4rpx; |
| | | margin-right: 14rpx; |
| | | flex-shrink: 0; |
| | | border: 2rpx solid #cbd5e1; |
| | | border-radius: 8rpx; |
| | | background-color: #ffffff; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .checkbox--checked { |
| | | background-color: #8bc34a; |
| | | border-color: #8bc34a; |
| | | background-color: #003e7e; |
| | | border-color: #003e7e; |
| | | } |
| | | |
| | | /* 勾选后的白色对勾 */ |
| | | .checkbox--checked::after { |
| | | content: ''; |
| | | position: absolute; |
| | | left: 9rpx; |
| | | top: 3rpx; |
| | | left: 10rpx; |
| | | top: 2rpx; |
| | | width: 8rpx; |
| | | height: 16rpx; |
| | | border-right: 3rpx solid #ffffff; |
| | |
| | | transform: rotate(45deg); |
| | | } |
| | | |
| | | /* 协议文案:原稿 11.5pt,按反馈上调至 26rpx 提升可读性 */ |
| | | .agreement-text { |
| | | font-size: 24rpx; |
| | | color: #6b7280; |
| | | font-size: 26rpx; |
| | | color: #64748b; |
| | | } |
| | | |
| | | .agreement-link { |
| | | font-size: 24rpx; |
| | | color: #1a73e8; |
| | | } |
| | | |
| | | /* 手机号一键登录按钮:绿色圆角块,两侧留边距 */ |
| | | .login-btn { |
| | | margin: 0 32rpx; |
| | | height: 96rpx; |
| | | line-height: 96rpx; |
| | | border: none; |
| | | border-radius: 16rpx; |
| | | background-color: #8bc34a; |
| | | color: #ffffff; |
| | | font-size: 32rpx; |
| | | font-size: 26rpx; |
| | | font-weight: 600; |
| | | color: #1e5ebc; |
| | | } |
| | | |
| | | /* 覆盖小程序 button 默认的边框伪元素 */ |
| | | .login-btn::after { |
| | | border: none; |
| | | } |
| | | |
| | | /* 注册入口:居中排列的提示文案 + 链接 */ |
| | | /* 注册入口:居中弱化文案 + 链接 */ |
| | | .register-entry { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-top: 40rpx; |
| | | margin-top: 28rpx; |
| | | } |
| | | |
| | | .register-tip { |
| | | font-size: 26rpx; |
| | | color: #6b7280; |
| | | color: #64748b; |
| | | } |
| | | |
| | | .register-link { |
| | | margin-left: 8rpx; |
| | | font-size: 26rpx; |
| | | color: #1a73e8; |
| | | font-weight: 600; |
| | | color: #1e5ebc; |
| | | } |
| | | |
| | | /* 底部 Home 条:设计稿 135×5pt → 270×10rpx,R2.5 → 5rpx,#0F172A25 */ |
| | | .home-indicator { |
| | | width: 270rpx; |
| | | height: 10rpx; |
| | | margin: 24rpx auto 0; |
| | | border-radius: 5rpx; |
| | | background-color: rgba(15, 23, 42, 0.145); |
| | | } |
| | | |
| | | /* ======================== 未勾选协议确认弹窗 ======================== */ |
| | | |
| | | /* 遮罩层:全屏半透明,居中承载弹窗 */ |
| | | .modal-mask { |
| | | position: fixed; |
| | | left: 0; |
| | | top: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background-color: rgba(0, 0, 0, 0.45); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | z-index: 999; |
| | | background-color: rgba(15, 23, 42, 0.45); |
| | | } |
| | | |
| | | .modal-box { |
| | | width: 560rpx; |
| | | background-color: #ffffff; |
| | | border-radius: 16rpx; |
| | | overflow: hidden; |
| | | border-radius: 24rpx; |
| | | background-color: #ffffff; |
| | | } |
| | | |
| | | /* 标题:绿色居中,下方带分隔线 */ |
| | | .modal-title { |
| | | display: block; |
| | | padding: 40rpx 32rpx 30rpx; |
| | | font-size: 34rpx; |
| | | font-weight: 600; |
| | | color: #8bc34a; |
| | | text-align: center; |
| | | border-bottom: 1rpx solid #ececec; |
| | | color: #003e7e; |
| | | border-bottom: 2rpx solid #e2e8f0; |
| | | } |
| | | |
| | | /* 正文:灰色居中的提示文案 */ |
| | | .modal-content { |
| | | display: block; |
| | | padding: 44rpx 48rpx; |
| | | font-size: 28rpx; |
| | | line-height: 42rpx; |
| | | color: #9aa1ab; |
| | | text-align: center; |
| | | color: #64748b; |
| | | } |
| | | |
| | | /* 按钮行:取消 / 我已阅读并同意 各占一半 */ |
| | | .modal-actions { |
| | | display: flex; |
| | | align-items: center; |
| | |
| | | } |
| | | |
| | | .modal-btn--cancel { |
| | | color: #1f2329; |
| | | color: #334155; |
| | | } |
| | | |
| | | .modal-btn--confirm { |
| | | color: #8bc34a; |
| | | color: #1e5ebc; |
| | | } |
| | | </style> |