Files
yaokongqi/pages/login-guide/login-guide.uvue
T
2026-09-03 22:21:38 +08:00

716 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<!-- 顶部:头像 + 登录状态入口(左),右上角蓝牙/WiFi/信号/时间(与 activate-device 页一致) -->
<view class="header">
<view class="header-user" @click="onHeaderUserClick">
<image class="header-avatar" src="/static/images/img-virtual.png" mode="aspectFill"></image>
<text class="header-name">{{ isLoggedIn ? '已登录' : '未登录' }}</text>
</view>
<view class="header-spacer"></view>
<view class="header-status">
<image class="status-icon" src="/static/icons/status-bluetooth.png" mode="aspectFit"></image>
<image class="status-icon status-icon-gap" src="/static/icons/status-wifi.png" mode="aspectFit"></image>
<image class="status-icon status-icon-gap" src="/static/icons/status-cellular.png" mode="aspectFit"></image>
<text class="header-time">{{ clockText }}</text>
</view>
</view>
<view class="content">
<!-- 左大卡:宣传位(限定 5 张,自动轮巡 + 左右滑动浏览) -->
<view class="card main-card">
<swiper
class="card-swiper"
:autoplay="true"
:circular="true"
:interval="4000"
:duration="400"
:indicator-dots="true"
indicator-color="rgba(255, 255, 255, 0.4)"
indicator-active-color="#FFFFFF"
>
<swiper-item v-for="(item, index) in promoImages" :key="index" @click="onPromoClick(index)">
<image class="card-bg" :src="item" mode="aspectFill"></image>
</swiper-item>
</swiper>
</view>
<!-- 右列 -->
<view class="right-col">
<!-- 右上卡:推荐位(限定 3 张,自动轮巡 + 左右滑动浏览) -->
<view class="card top-card">
<swiper
class="card-swiper"
:autoplay="true"
:circular="true"
:interval="4000"
:duration="400"
:indicator-dots="true"
indicator-color="rgba(255, 255, 255, 0.4)"
indicator-active-color="#FFFFFF"
>
<swiper-item v-for="(item, index) in recoImages" :key="index" @click="onRecoClick(index)">
<image class="card-bg" :src="item" mode="aspectFill"></image>
</swiper-item>
</swiper>
</view>
<!-- 下排两卡 -->
<view class="bottom-row">
<view class="card func-card func-left" @click="onVirtualClick">
<image class="card-bg" src="/static/images/img-virtual.png" mode="aspectFill"></image>
<view class="func-badge">
<image class="func-icon" src="/static/icons/icon-virtual.png" mode="aspectFit"></image>
<text class="func-text">虚拟体验</text>
</view>
</view>
<view class="card func-card func-right" @click="showModal = true">
<image class="card-bg" src="/static/images/img-login.png" mode="aspectFill"></image>
<view class="func-badge">
<image class="func-icon" src="/static/icons/icon-login.png" mode="aspectFit"></image>
<text class="func-text">立即登陆</text>
</view>
</view>
</view>
</view>
</view>
<!-- ============== 登录弹窗 ============== -->
<view v-if="showModal" class="mask" @click="onMaskClick">
<view class="modal" @click.stop>
<!-- 关闭 X -->
<view class="modal-close" @click="showModal = false">
<view class="modal-close-line1"></view>
<view class="modal-close-line2"></view>
</view>
<!-- 左侧插画(新容器:蓝描边 + 图居中 aspectFit,不裁切) -->
<view class="modal-banner-wrap">
<image class="modal-banner" src="/static/images/login-banner.png" mode="aspectFit"></image>
</view>
<!-- 右侧表单 -->
<view class="modal-form">
<text class="modal-switch">账号密码登录</text>
<!-- 手机号 -->
<view class="field" :class="{ fieldFocus: focusType === 'phone' }">
<view class="field-icon">
<view class="icon-user-1"></view>
<view class="icon-user-2"></view>
</view>
<input
class="field-input"
v-model="phone"
type="number"
maxlength="11"
placeholder="请填写手机号"
placeholder-class="field-placeholder"
@focus="focusType = 'phone'"
@blur="focusType = ''"
/>
</view>
<!-- 验证码:必填,限制 4 位数字,点击唤起数字键盘 -->
<view class="field" :class="{ fieldFocus: focusType === 'code' }">
<view class="field-icon">
<view class="icon-sms-box"></view>
<view class="icon-sms-flap"></view>
</view>
<input
class="field-input"
v-model="code"
type="number"
maxlength="4"
placeholder="请填写验证码"
placeholder-class="field-placeholder"
@focus="focusType = 'code'"
@blur="focusType = ''"
/>
<text class="code-btn" :class="{ disabled: smsCountdown > 0 }" @click="onGetSms">
{{ smsBtnText }}
</text>
</view>
<!-- 提交按钮 -->
<view class="submit-btn" @click="onSubmitLogin">
<text class="submit-text">注册或登录</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
// ====== 宣传位 / 推荐位轮播数据 ======
// 宣传位:限定 5 张(暂用现有素材循环占位,接入投放系统后替换为真实图片列表)
const promoImages = ref<string[]>([
'/static/images/bg-main.png',
'/static/images/bg-top-right.png',
'/static/images/img-virtual.png',
'/static/images/img-login.png',
'/static/images/login-banner.png'
])
// 推荐位:限定 3 张
const recoImages = ref<string[]>([
'/static/images/bg-top-right.png',
'/static/images/bg-main.png',
'/static/images/img-login.png'
])
// 查看投放内容(预留:后续接入真实投放详情/跳转链接)
function onPromoClick(index : number) : void {
uni.showToast({ title: '查看投放内容 ' + (index + 1).toString(), icon: 'none' })
}
function onRecoClick(index : number) : void {
uni.showToast({ title: '查看投放内容 ' + (index + 1).toString(), icon: 'none' })
}
// ====== 登录弹窗状态 ======
const showModal = ref<boolean>(false)
const isLoggedIn = ref<boolean>(false)
const phone = ref<string>('13513321454')
const code = ref<string>('1234')
const focusType = ref<string>('')
// 验证码 60s 倒计时
const smsCountdown = ref<number>(0)
// 上次成功获取验证码的时间戳(用于 5s/次 频次限制)
let lastGetTs : number = 0
// 倒计时定时器句柄(-1 表示无)
let _smsTimer : number = -1
// 顶部「未登录」入口:未登录时打开登录弹窗,已登录时提示
function onHeaderUserClick() : void {
if (isLoggedIn.value) {
uni.showToast({ title: '已登录', icon: 'none' })
return
}
showModal.value = true
}
// 关闭弹窗:点击遮罩空白处
function onMaskClick() : void {
showModal.value = false
}
// 虚拟体验:暂未开放
function onVirtualClick() : void {
uni.showToast({ title: '暂未开放', icon: 'none' })
}
// 校验手机号(必填,11 位有效数字,1 开头)
function isValidPhone(p : string) : boolean {
if (p.length != 11) return false
if (!p.startsWith('1')) return false
for (let i = 0; i < p.length; i++) {
if (p[i] < '0' || p[i] > '9') return false
}
return true
}
// ====== 验证码按钮文案:倒计时中显示「Ns」,倒计时结束显示「重新获取」,初始「获取验证码」 ======
const hasGotOnce = ref<boolean>(false)
const smsBtnText = computed<string>((): string => {
if (smsCountdown.value > 0) return smsCountdown.value.toString() + 's'
if (hasGotOnce.value) return '重新获取'
return '获取验证码'
})
// ====== 每日获取次数限制:手机号 / 10 次 / 天(本地按 手机号+日期 计数,接入 API 后由服务端校验) ======
const SMS_DAILY_LIMIT = 10
function smsCountKey() : string {
const d = new Date()
return 'sms_count_' + phone.value + '_' + d.getFullYear().toString() + '-' + (d.getMonth() + 1).toString() + '-' + d.getDate().toString()
}
function getTodaySmsCount() : number {
const v = uni.getStorageSync(smsCountKey())
if (v == null) return 0
const n = parseInt(v as string)
if (isNaN(n)) return 0
return n
}
function incrTodaySmsCount() : void {
uni.setStorageSync(smsCountKey(), (getTodaySmsCount() + 1).toString())
}
// 60s 倒计时循环
function startTick() : void {
_smsTimer = setTimeout(() : void => {
smsCountdown.value -= 1
if (smsCountdown.value > 0) {
startTick()
} else {
_smsTimer = -1
}
}, 1000)
}
// 点击【获取验证码】:验证手机号 → 频次/次数校验 → 发送 + 60s 倒计时
function onGetSms() : void {
if (smsCountdown.value > 0) return
// 验证失败:toast「请填写正确的手机号」
if (!isValidPhone(phone.value)) {
uni.showToast({ title: '请填写正确的手机号', icon: 'none' })
return
}
// 获取频次:5s/次,超过 toast「间隔时间过短」
const now = Date.now()
if (lastGetTs > 0 && now - lastGetTs < 5000) {
uni.showToast({ title: '间隔时间过短', icon: 'none' })
return
}
// 获取次数:手机号/10次/天,超出 toast「获取次数今日已达上限」
if (getTodaySmsCount() >= SMS_DAILY_LIMIT) {
uni.showToast({ title: '获取次数今日已达上限', icon: 'none' })
return
}
lastGetTs = now
incrTodaySmsCount()
hasGotOnce.value = true
// TODO: 接入真实短信发送 API
smsCountdown.value = 60
startTick()
}
// 点击【注册或登录】:验证编辑信息
function onSubmitLogin() : void {
if (!isValidPhone(phone.value)) {
uni.showToast({ title: '请填写正确的手机号', icon: 'none' })
return
}
// 验证码:必填,4 位数字
if (code.value.length != 4) {
uni.showToast({ title: '请填写正确的验证码', icon: 'none' })
return
}
// TODO: 接入真实注册登录 API + 设备绑定状态查询
isLoggedIn.value = true
showModal.value = false
// 验证成功 → 进入【添加设备】页面
uni.navigateTo({ url: '/pages/activate-device/activate-device' })
}
// ====== 顶部状态栏时间(与 activate-device 页一致:HH:mm 每秒刷新) ======
const clockText = ref<string>('00:00')
let _clockTimer : number = -1
function pad2(n : number) : string {
return n < 10 ? '0' + n.toString() : n.toString()
}
function refreshClock() : void {
const d = new Date()
clockText.value = pad2(d.getHours()) + ':' + pad2(d.getMinutes())
}
function startClock() : void {
refreshClock()
_clockTimer = setInterval(() : void => {
refreshClock()
}, 1000)
}
onLoad(() : void => {
startClock()
})
// 页面卸载时清理倒计时定时器 + 时钟定时器
onUnload(() : void => {
if (_smsTimer != -1) {
clearTimeout(_smsTimer)
_smsTimer = -1
}
if (_clockTimer != -1) {
clearInterval(_clockTimer)
_clockTimer = -1
}
})
</script>
<style>
/* ==================== 登录引导页基础 ==================== */
.page {
width: 750rpx;
height: 100%;
background-color: #040912;
flex-direction: column;
}
/* 顶部栏:设计稿 1920×104 → 750×40.63rpx;头像 60×60 在 (60,22),文字 36px 在 (144,28),右侧蓝牙/WiFi/信号/时间 */
.header {
width: 100%;
height: 40.63rpx; /* 104 → 40.63rpx */
flex-direction: row;
align-items: center;
padding-left: 23.44rpx; /* 60 → 23.44rpx */
padding-right: 23.44rpx; /* 右侧与 activate-device 页一致 */
}
.header-user {
flex-direction: row;
align-items: center;
}
/* 头像 60×60 → 23.44rpx 圆形 */
.header-avatar {
width: 23.44rpx;
height: 23.44rpx;
border-radius: 11.72rpx; /* 50% */
}
/* 「未登录」36px Regular 白;与头像间距 24 → 9.38rpx */
.header-name {
margin-left: 9.38rpx;
font-size: 14.06rpx; /* 36 → 14.06rpx */
color: #FFFFFF;
}
.header-spacer {
flex: 1;
}
.header-status {
flex-direction: row;
align-items: center;
}
/* 三个状态栏图标:均为 23.44rpx(60×60 Figma px),相邻间距 24 → 9.38rpx */
.status-icon {
width: 23.44rpx;
height: 23.44rpx;
}
.status-icon-gap {
margin-left: 9.38rpx;
}
/* 时间:与信号图标间距 24 → 9.38rpx;36px Regular 白;行高 14.06rpx 与图标对齐 */
.header-time {
margin-left: 9.38rpx;
font-size: 14.06rpx;
line-height: 14.06rpx;
color: #FFFFFF;
text-align: right;
}
.content {
flex: 1;
flex-direction: row;
padding-left: 23.44rpx; /* 60 → 23.44rpx */
padding-right: 23.44rpx;
padding-bottom: 23.44rpx;
padding-top: 14.06rpx; /* 设计稿卡片 y=140,header 高 104 → 间距 36 → 14.06rpx */
}
.card {
background-color: #D8D8D8;
overflow: hidden;
position: relative;
border-radius: 7.81rpx; /* 20 → 7.81rpx */
}
.card-bg {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
/* 轮播铺满卡片(宣传位 / 推荐位) */
.card-swiper {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.main-card {
width: 343.75rpx; /* 880 → 343.75rpx */
height: 100%;
flex-shrink: 0;
}
.right-col {
flex: 1;
margin-left: 10.94rpx; /* 28 → 10.94rpx */
flex-direction: column;
}
.top-card {
flex: 486;
width: 100%;
}
.bottom-row {
flex: 486;
margin-top: 10.94rpx;
flex-direction: row;
width: 100%;
}
.func-card {
flex: 1;
width: 0;
height: 100%;
align-items: center;
justify-content: center;
}
.func-right {
margin-left: 10.94rpx;
flex-shrink: 0;
}
/* 徽标 192×48 → 75×18.75rpx;圆角 20 → 7.81rpx */
.func-badge {
width: 75rpx;
height: 18.75rpx;
border-radius: 7.81rpx;
flex-direction: row;
align-items: center;
justify-content: center;
background-color: #242738;
box-shadow: 0 0.78rpx 3.13rpx rgba(0, 0, 0, 0.4);
}
/* 图标 36×36 → 14.06rpx;文字 36 Heavy→14.06rpx;间距 13→5.08rpx */
.func-icon {
width: 14.06rpx;
height: 14.06rpx;
}
.func-text {
color: #FFFFFF;
font-weight: 900;
font-size: 14.06rpx;
margin-left: 5.08rpx;
}
/* ==================== 登录弹窗 ==================== */
/* 设计稿:1920 基准;遮罩 1920×1200 rgba(31,35,41,0.6) */
.mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(31, 35, 41, 0.6);
align-items: center;
justify-content: center;
z-index: 999;
}
/* 弹窗 1280×720 → 500 × 281.25rpx;背景 #030A16;圆角 20 → 7.81rpx;相对画板 320/240 居中 */
.modal {
width: 500rpx;
height: 281.25rpx;
background-color: #030A16;
border-radius: 7.81rpx;
flex-direction: row;
position: relative;
overflow: hidden;
box-shadow: 0 3.91rpx 15.63rpx rgba(0, 0, 0, 0.5);
}
/* 关闭 X 30×30 → 11.72rpx 方形;相对弹窗 1230/20 → 弹窗右上 50/7.81rpx */
.modal-close {
position: absolute;
top: 7.81rpx;
right: 19.53rpx; /* 设计稿 (1280-1230-30)=20 → 7.81rpx,稍放大一点更易点 */
width: 11.72rpx;
height: 11.72rpx;
justify-content: center;
align-items: center;
}
.modal-close-line1 {
position: absolute;
width: 11.72rpx;
height: 1rpx;
background-color: #FFFFFF;
transform: rotate(45deg);
}
.modal-close-line2 {
position: absolute;
width: 11.72rpx;
height: 1rpx;
background-color: #FFFFFF;
transform: rotate(-45deg);
}
/* 左侧插画容器:设计稿 Mask group 弹窗内 32/154 556×380 → 用比例换算 + 匹配图1效果
修正:
1. 加上蓝边(~2px 粗亮蓝描边,与 Figma 截图的蓝色一致)
2. 图片按 aspectFit 居中展示真实车身 PNG,不裁切
*/
.modal-banner-wrap {
position: absolute;
left: 12.5rpx;
top: 60.16rpx;
width: 217.19rpx;
height: 148.44rpx;
background-color: transparent;
align-items: center;
justify-content: center;
}
.modal-banner {
width: 80%;
height: 80%;
border-radius: 0;
}
/* 右侧表单:起点 620/162 570×60 → 占弹窗右,用比例 */
.modal-form {
position: absolute;
left: 242.19rpx; /* 620/1280*500 ≈ 242.19 */
top: 63.28rpx; /* 162/720*281.25 = 63.28 */
width: 222.66rpx; /* 570/1280*500 ≈ 222.66 */
flex-direction: column;
}
/* 切换文字:「账号密码登录」 20 → 7.81rpx;位置 y=162,距离顶部 0 → 相对弹窗 form 起点 162 即 top=0 对应 */
.modal-switch {
font-size: 7.81rpx; /* 20 → 7.81rpx */
color: #FFFFFF;
line-height: 14.06rpx; /* 36px lineHeight → 14.06rpx */
margin-bottom: 7.03rpx; /* 210 - 162 - 36 = 12 → 4.69rpx 微调 → 7.03rpx 使下间距合理 */
}
/* 输入框 570×60 → 222.66×23.44rpx;背景 rgba(36,39,56,0.8);圆角 4 → 1.56rpx;与上一个间距 38(308-270=38 → 210到308 98 → 38.28rpx 作为 form 内间距) */
.field {
margin-top: 14.84rpx; /* 输入框间距 (308-210-60=38) → 14.84rpx */
width: 222.66rpx;
height: 23.44rpx; /* 60 → 23.44rpx */
background-color: rgba(36, 39, 56, 0.8);
border-radius: 1.56rpx; /* 4 → 1.56rpx */
flex-direction: row;
align-items: center;
padding-left: 6.25rpx; /* 图标起点 16px相对输入框 → 6.25rpx */
padding-right: 3.91rpx; /* 小内边距 */
position: relative;
border-width: 0.78rpx;
border-style: solid;
border-color: transparent;
}
/* 设计稿「注册或登录」按钮有边框 rgba(217,217,217,0.8) 2px → 输入框聚焦时按同样风格高亮 */
.fieldFocus {
border-color: rgba(217, 217, 217, 0.8);
border-width: 0.78rpx;
}
/* 图标 24×24 相对输入框 left 16/top 18(评论框图标)、top 228-210=18 for 手机号 → 都用字段垂直居中 */
.field-icon {
width: 9.38rpx; /* 24 → 9.38rpx */
height: 9.38rpx;
margin-right: 2.34rpx; /* 占位 672-636-24 = 12 → 4.69rpx 一半值,实际用 2.34 使视觉紧凑 */
position: relative;
flex-shrink: 0;
}
/* 用户图标(Figma user备份2):大致由上半圆 + 下方梯形组成,用两个 view 拼出轮廓 */
.icon-user-1 {
position: absolute;
left: 1.56rpx;
top: 0;
width: 6.25rpx;
height: 6.25rpx;
border-width: 0.78rpx;
border-style: solid;
border-color: #D8D8D8;
border-radius: 3.13rpx; /* 50%效果 */
}
.icon-user-2 {
position: absolute;
left: 0;
bottom: 0;
width: 9.38rpx;
height: 4.69rpx;
border-top-left-radius: 4.69rpx;
border-top-right-radius: 4.69rpx;
border-width: 0.78rpx;
border-style: solid;
border-color: #D8D8D8;
border-bottom-width: 0;
}
/* 消息图标(Figma「评论」框 + 遮罩):信封轮廓 矩形 20×14 + 翻盖 17×5 */
.icon-sms-box {
position: absolute;
left: 0.78rpx;
top: 2.0rpx;
width: 7.81rpx; /* 20 → 7.81 */
height: 5.47rpx; /* 14 → 5.47 */
border-width: 0.78rpx;
border-style: solid;
border-color: #D8D8D8;
border-radius: 1.17rpx; /* 3 → 1.17 */
background-color: transparent;
}
.icon-sms-flap {
position: absolute;
left: 1.17rpx;
top: 2.81rpx;
width: 6.64rpx; /* 17 → 6.64 */
height: 1.95rpx; /* 5 → 1.95 */
border-top-width: 0.78rpx;
border-top-style: solid;
border-top-color: #D8D8D8;
}
.field-input {
flex: 1;
height: 23.44rpx;
color: #FFFFFF;
font-size: 9.38rpx; /* 24 → 9.38rpx */
line-height: 14.06rpx; /* 36 → 14.06rpx */
padding: 0;
margin: 0;
background-color: transparent;
}
.field-placeholder {
color: #999999;
font-size: 9.38rpx;
}
/* 获取验证码 右对齐 input;设计稿 454+100=554 (<570 框内),字号 20 白 */
.code-btn {
flex-shrink: 0;
font-size: 7.81rpx; /* 20 → 7.81rpx */
line-height: 14.06rpx; /* 36 → 14.06rpx */
color: #FFFFFF;
text-align: right;
padding-left: 3.91rpx;
}
.code-btn.disabled {
color: #999999;
}
/* 提交按钮 570×60 → 222.66×23.44rpx;同输入框背景 + 边框 2px rgba(217,217,217,0.8);圆角 4 → 1.56rpx;距上 (514-308-60=146) → 57.03rpx */
.submit-btn {
margin-top: 57.03rpx;
width: 222.66rpx;
height: 23.44rpx;
background-color: rgba(36, 39, 56, 0.8);
border-radius: 1.56rpx;
border-width: 0.78rpx; /* 2px → 0.78rpx */
border-style: solid;
border-color: rgba(217, 217, 217, 0.8);
align-items: center;
justify-content: center;
}
.submit-text {
color: #FFFFFF;
font-size: 9.38rpx; /* 24 → 9.38rpx */
line-height: 14.84rpx; /* 38 → 14.84rpx */
text-align: center;
}
</style>