提交 dcb2c886 作者: 廖在望

feat: 优化登录和注册页面逻辑。

上级 2a2d7e73
<script setup lang="ts">
/**
* 滑动验证组件
* 用法:<SlideVerify v-model:show="showSlide" @success="onVerifySuccess" />
*/
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: 'update:show', val: boolean): void
(e: 'success'): void
(e: 'close'): void
}>()
const TRACK_WIDTH = 560 // rpx,滑道宽度(与样式保持一致)
const BTN_WIDTH = 96 // rpx,滑块宽度
const maxRpx = TRACK_WIDTH - BTN_WIDTH // 最大可拖动距离(rpx)
const state = reactive({
translateX: 0, // 当前滑块位移(px,运行时换算)
isDragging: false,
startX: 0,
maxPx: 0, // 运行时根据屏幕宽度换算
verified: false,
text: '按住滑块,拖动到最右侧',
})
// 将 rpx 换算为 px
function rpxToPx(rpx: number): number {
const screenWidth = uni.getSystemInfoSync().windowWidth
return (rpx / 750) * screenWidth
}
watch(() => props.show, (val) => {
if (val) {
reset()
state.maxPx = rpxToPx(maxRpx)
}
})
function reset() {
state.translateX = 0
state.isDragging = false
state.verified = false
state.text = '按住滑块,拖动到最右侧'
}
function onTouchStart(e: any) {
if (state.verified) return
state.isDragging = true
state.startX = e.touches[0].clientX
}
function onTouchMove(e: any) {
if (!state.isDragging || state.verified) return
const dx = e.touches[0].clientX - state.startX
state.translateX = Math.max(0, Math.min(dx, state.maxPx))
}
function onTouchEnd() {
if (!state.isDragging) return
state.isDragging = false
// 距终点 10px 以内视为验证通过
if (state.translateX >= state.maxPx - rpxToPx(10)) {
state.translateX = state.maxPx
state.verified = true
state.text = '验证通过'
setTimeout(() => {
emit('success')
close()
}, 600)
} else {
// 未拖到底,回弹
state.translateX = 0
}
}
function close() {
emit('update:show', false)
emit('close')
reset()
}
</script>
<template>
<view class="sv-mask" v-if="show" @click.self="close">
<view class="sv-panel">
<view class="sv-title">安全验证</view>
<view class="sv-desc">{{ state.text }}</view>
<!-- 滑道 -->
<view class="sv-track">
<!-- 已划过的高亮区 -->
<view class="sv-fill" :style="{ width: state.translateX + 'px' }" />
<!-- 滑块 -->
<view
class="sv-btn"
:class="{ success: state.verified }"
:style="{ transform: `translateX(${state.translateX}px)` }"
@touchstart.prevent="onTouchStart"
@touchmove.prevent="onTouchMove"
@touchend.prevent="onTouchEnd"
>
<text class="sv-arrow" v-if="!state.verified">&#8594;</text>
<text class="sv-check" v-else">&#10003;</text>
</view>
</view>
<view class="sv-close" @click="close">
<text>取消</text>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.sv-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.sv-panel {
width: 640rpx;
background: #fff;
border-radius: 24rpx;
padding: 48rpx 40rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.16);
}
.sv-title {
font-size: 34rpx;
font-weight: 700;
color: #222;
margin-bottom: 12rpx;
}
.sv-desc {
font-size: 26rpx;
color: #888;
margin-bottom: 40rpx;
}
.sv-track {
position: relative;
width: 560rpx;
height: 96rpx;
background: #f0f0f0;
border-radius: 48rpx;
overflow: hidden;
}
.sv-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
background: linear-gradient(90deg, #b8e8c0, #5db66f);
border-radius: 48rpx 0 0 48rpx;
transition: width 0.05s;
}
.sv-btn {
position: absolute;
left: 0;
top: 0;
width: 96rpx;
height: 96rpx;
background: #fff;
border-radius: 48rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.18);
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s;
z-index: 2;
&.success {
background: #5db66f;
}
}
.sv-arrow {
font-size: 40rpx;
color: #5db66f;
font-weight: 700;
}
.sv-check {
font-size: 40rpx;
color: #fff;
font-weight: 700;
}
.sv-close {
margin-top: 32rpx;
text {
font-size: 28rpx;
color: #aaa;
}
}
</style>
<script setup lang="ts"> <script setup lang="ts">
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { closeSplashscreenAndChechUpgrade } from '@/utils/upgrade' import { closeSplashscreenAndChechUpgrade } from '@/utils/upgrade'
import { isDevMode } from '@/utils/env'
import Link from '@/utils/const/link' import Link from '@/utils/const/link'
import SlideVerify from '@/components/slide-verify/SlideVerify.vue'
import * as API from '/@/api/model/userInfo' import * as API from '/@/api/model/userInfo'
import { Message } from '@/common'
const userStore = useUserStore() const userStore = useUserStore()
onShow(async () => { onShow(async () => {
// 恢复倒计时:计算实际剩余秒数,重启定时器
if (model.countdown > 0 && countdownEndTime > 0) {
const remaining = Math.max(0, Math.ceil((countdownEndTime - Date.now()) / 1000))
if (remaining > 0) {
model.countdown = remaining
startCountdownTimer()
} else {
model.countdown = 0
countdownEndTime = 0
}
}
const token = userStore.getToken const token = userStore.getToken
if (token) { if (token) {
model.isLogin = true model.isLogin = true
...@@ -24,17 +35,18 @@ ...@@ -24,17 +35,18 @@
} }
}) })
const defaultText = '湘农数智农服'
const readConfirmShow = ref<boolean>(false) const readConfirmShow = ref<boolean>(false)
const form = ref() const form = ref()
const showSlide = ref(false) const slideVerifyRef = ref()
const showSlideVerify = ref(false)
const slideType = ref<'sms' | 'login'>('login') const canLogin = computed(() => {
return !!model.form.data.username && !!model.form.data.code
})
const model = reactive({ const model = reactive({
isLogin: false, isLogin: false,
loading: false, loading: false,
text: defaultText,
countdown: 0, countdown: 0,
form: { form: {
phoneRules: [ phoneRules: [
...@@ -44,7 +56,7 @@ ...@@ -44,7 +56,7 @@
msg: ['请输入手机号', '请输入正确的手机号'], msg: ['请输入手机号', '请输入正确的手机号'],
}, },
], ],
codeLoginRules: [ loginRules: [
{ {
name: 'username', name: 'username',
rule: ['required', 'isMobile'], rule: ['required', 'isMobile'],
...@@ -55,20 +67,6 @@ ...@@ -55,20 +67,6 @@
rule: ['required'], rule: ['required'],
msg: ['请输入验证码'], msg: ['请输入验证码'],
}, },
{
name: 'read',
validator: [
{
msg: '请阅读并同意服务协议和隐私政策',
method: (value: boolean) => {
if (!value) {
readConfirmShow.value = true
}
return value
},
},
],
},
], ],
data: { data: {
username: '', username: '',
...@@ -79,18 +77,18 @@ ...@@ -79,18 +77,18 @@
}) })
let countdownTimer: any = null let countdownTimer: any = null
let countdownEndTime = 0
if (isDevMode()) {
model.form.data.username = 'admin'
model.form.data.read = true
}
function login() { function login() {
form?.value.validator(model.form.data, model.form.codeLoginRules).then(async (res: { isPassed: boolean }) => { form?.value.validator(model.form.data, model.form.loginRules).then(async (res: { isPassed: boolean }) => {
if (res.isPassed) { if (res.isPassed) {
// 登录前先做滑动验证 (Requirement 7) if (!model.form.data.read) {
slideType.value = 'login' readConfirmShow.value = true
showSlide.value = true return
}
doLogin()
} else {
Message.toast(res.errorMsg)
} }
}) })
} }
...@@ -98,20 +96,19 @@ ...@@ -98,20 +96,19 @@
function smsCode() { function smsCode() {
form?.value.validator(model.form.data, model.form.phoneRules).then(async (res: { isPassed: boolean }) => { form?.value.validator(model.form.data, model.form.phoneRules).then(async (res: { isPassed: boolean }) => {
if (res.isPassed) { if (res.isPassed) {
if (model.countdown > 0) if (model.countdown > 0) return
return showSlideVerify.value = true
slideType.value = 'sms' slideVerifyRef.value.reset()
showSlide.value = true } else {
Message.toast(res.errorMsg)
} }
}) })
} }
function onSlideSuccess() { function onSlideSuccess() {
if (slideType.value === 'sms') { showSlideVerify.value = false
// 立即开启倒计时(乐观更新),接口报错也不停止,防止恶意连点
startCountdown() startCountdown()
// 发送验证码
const params = { const params = {
mobile: model.form.data.username, mobile: model.form.data.username,
smsmode: '1', smsmode: '1',
...@@ -121,18 +118,12 @@ return ...@@ -121,18 +118,12 @@ return
Message.toast('验证码已发送') Message.toast('验证码已发送')
}) })
.catch((err) => { .catch((err) => {
// 即使接口返回 code: 130 等错误,也不干扰已启动的倒计时
console.error('短信发送业务异常:', err) console.error('短信发送业务异常:', err)
}) })
} else {
// 执行登录
doLogin()
}
} }
function doLogin() { function doLogin() {
model.loading = true model.loading = true
// 验证码登录
const params = { const params = {
mobile: model.form.data.username, mobile: model.form.data.username,
captcha: model.form.data.code, captcha: model.form.data.code,
...@@ -155,16 +146,23 @@ return ...@@ -155,16 +146,23 @@ return
} }
function startCountdown() { function startCountdown() {
if (countdownTimer) if (countdownTimer) clearInterval(countdownTimer)
clearInterval(countdownTimer)
model.countdown = 60 model.countdown = 60
countdownEndTime = Date.now() + 60 * 1000
startCountdownTimer()
}
function startCountdownTimer() {
if (countdownTimer) clearInterval(countdownTimer)
countdownTimer = setInterval(() => { countdownTimer = setInterval(() => {
model.countdown-- const remaining = Math.max(0, Math.ceil((countdownEndTime - Date.now()) / 1000))
if (model.countdown <= 0) { model.countdown = remaining
if (remaining <= 0) {
clearInterval(countdownTimer) clearInterval(countdownTimer)
countdownTimer = null countdownTimer = null
countdownEndTime = 0
} }
}, 1000) }, 300)
} }
function goHome() { function goHome() {
...@@ -189,60 +187,47 @@ clearInterval(countdownTimer) ...@@ -189,60 +187,47 @@ clearInterval(countdownTimer)
} }
}) })
function onReadConfirm(val: any) { function onReadConfirm() {
if (val.index === 0) {
model.form.data.read = false
} else {
model.form.data.read = true model.form.data.read = true
readConfirmShow.value = false
login() login()
} }
function closeAgreeModal() {
readConfirmShow.value = false readConfirmShow.value = false
} }
</script> </script>
<template> <template>
<view class="login-page"> <view class="login-page">
<!-- 顶部设计:数智化农场全景感 -->
<view class="header-visual"> <view class="header-visual">
<image class="header-scene" src="/static/images/login/farm_base_bg.png" mode="aspectFill" /> <image class="header-scene" src="/static/images/login/farm_base_bg.png" mode="aspectFill"></image>
<view class="header-overlay" /> <view class="header-overlay"></view>
<!-- 顶部装饰元素:漂浮的气象/农业设备(体现数智感) --> <image class="float-icon device-1" src="/static/images/nongchang/device1.png" mode="aspectFit"></image>
<image class="float-icon device-1" src="/static/images/nongchang/device1.png" mode="aspectFit" /> <image class="float-icon device-2" src="/static/images/nongchang/device3.png" mode="aspectFit"></image>
<image class="float-icon device-2" src="/static/images/nongchang/device2.png" mode="aspectFit" />
<!-- 顶部状态栏占位与操作 -->
<view class="top-nav-bar">
<view class="close-btn" @click="goHome">
<fui-icon name="close" :size="48" color="#fff" />
</view>
</view>
<!-- 品牌标识与数智概览 -->
<view class="brand-hero"> <view class="brand-hero">
<view class="logo-box"> <view class="logo-box">
<image class="logo-img" src="/static/logo.png" mode="aspectFit" /> <image class="logo-img" src="/static/logo.png" mode="aspectFit"></image>
</view> </view>
<view class="brand-info"> <view class="brand-info">
<text class="app-title">湘农数智农服</text> <text class="app-title">湘农数智农服</text>
<view class="app-slogan"> <view class="app-slogan">
<image src="/static/images/weather/100.svg" class="weather-icon" />
<text>智慧生产 · 农务管家</text> <text>智慧生产 · 农务管家</text>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<!-- 登录卡片:磨砂玻璃质感 -->
<view class="main-card-container"> <view class="main-card-container">
<view class="aesthetic-card"> <view class="aesthetic-card">
<fui-form ref="form" top="0" :padding="['0', '0']" background="transparent"> <fui-form ref="form" top="0" :padding="['0', '0']" background="transparent" :show="false">
<view class="card-header"> <view class="card-header">
<text class="welcome-text">您好,欢迎登录</text> <text class="welcome-text">您好,欢迎登录</text>
<view class="green-line" /> <view class="green-line"></view>
</view> </view>
<!-- 输入区域 -->
<view class="input-fields"> <view class="input-fields">
<view class="field-item"> <view class="field-item">
<text class="field-label">手机号</text> <text class="field-label">手机号</text>
...@@ -251,11 +236,13 @@ clearInterval(countdownTimer) ...@@ -251,11 +236,13 @@ clearInterval(countdownTimer)
:padding="['24rpx', '0']" :padding="['24rpx', '0']"
placeholder="请输入手机号" placeholder="请输入手机号"
backgroundColor="transparent" backgroundColor="transparent"
placeholderStyle="font-size:16px;"
borderColor="#F0F0F0" borderColor="#F0F0F0"
borderBottom borderBottom
height="110rpx" height="110rpx"
size="34" size="34"
/> maxlength="11"
></fui-input>
</view> </view>
<view class="field-item"> <view class="field-item">
...@@ -265,13 +252,15 @@ clearInterval(countdownTimer) ...@@ -265,13 +252,15 @@ clearInterval(countdownTimer)
v-model="model.form.data.code" v-model="model.form.data.code"
:padding="['24rpx', '0']" :padding="['24rpx', '0']"
placeholder="请输入验证码" placeholder="请输入验证码"
placeholderStyle="font-size:16px;"
backgroundColor="transparent" backgroundColor="transparent"
borderColor="#F0F0F0" borderColor="#F0F0F0"
borderBottom borderBottom
height="110rpx" height="110rpx"
size="34" size="34"
maxlength="6"
autocomplete="off" autocomplete="off"
/> ></fui-input>
<view class="sms-trigger" :class="{ inactive: model.countdown > 0 }" @click="smsCode"> <view class="sms-trigger" :class="{ inactive: model.countdown > 0 }" @click="smsCode">
{{ model.countdown > 0 ? `${model.countdown}s后重发` : '获取验证码' }} {{ model.countdown > 0 ? `${model.countdown}s后重发` : '获取验证码' }}
</view> </view>
...@@ -279,41 +268,43 @@ clearInterval(countdownTimer) ...@@ -279,41 +268,43 @@ clearInterval(countdownTimer)
</view> </view>
</view> </view>
<!-- 登录决策 -->
<view class="action-footer"> <view class="action-footer">
<view :class="['btn-wrapper', { disabled: !canLogin }]">
<fui-button <fui-button
text="开启农务数字化" text="验证并登录"
background="linear-gradient(90deg, #5DB66F 0%, #4CAF50 100%)" background="linear-gradient(90deg, #5DB66F 0%, #4CAF50 100%)"
radius="100rpx" radius="100rpx"
height="110rpx" height="80rpx"
@click="login" @click="login"
:loading="model.loading" :loading="model.loading"
:disabled="model.loading" :disabled="!canLogin || model.loading"
size="36" color="#ffffff"
disabled-color="#888888"
disabled-background="#f5f5f5"
size="30"
shadow shadow
/> ></fui-button>
</view>
<view class="secondary-links"> <view class="secondary-links">
<view class="reg-btn" @click="goRegister">新用户注册</view> <view class="reg-btn" @click="goRegister">还没有账号?立即注册</view>
</view> </view>
</view> </view>
<!-- 隐私合规 -->
<view class="policy-wrapper"> <view class="policy-wrapper">
<fui-checkbox-group> <fui-checkbox-group>
<fui-label class="policy-label"> <fui-label class="policy-label">
<view class="checkbox-align">
<fui-checkbox <fui-checkbox
color="#5DB66F" color="#5DB66F"
:scale="0.7" :scaleRatio="0.7"
:checked="model.form.data.read" :checked="model.form.data.read"
@change="(e) => (model.form.data.read = e.checked)" @change="(e) => (model.form.data.read = e.checked)"
/> ></fui-checkbox>
</view>
<view class="policy-text"> <view class="policy-text">
同意<text class="link" @click.stop="Link.to(Link.services, '服务协议')" 已阅读并同意<text class="link" @click.stop="Link.to(Link.services, '服务协议')">《服务协议》</text>与<text class="link" @click.stop="Link.to(Link.privacy, '隐私政策')">《隐私政策》</text>
>《服务协议》</text ,同意运营商对本人提供的手机号码进行验证。
><text class="link" @click.stop="Link.to(Link.privacy, '隐私政策')"
>《隐私政策》</text
>
</view> </view>
</fui-label> </fui-label>
</fui-checkbox-group> </fui-checkbox-group>
...@@ -322,45 +313,64 @@ clearInterval(countdownTimer) ...@@ -322,45 +313,64 @@ clearInterval(countdownTimer)
</view> </view>
</view> </view>
<!-- 装饰底部:泥土与天空的呼应 --> <view class="footer-aesthetic"></view>
<view class="footer-aesthetic" />
<view class="agree-mask" v-if="readConfirmShow" @click.self="closeAgreeModal">
<!-- 功能弹窗 --> <view class="agree-panel">
<fui-modal <view class="agree-gradient-header"></view>
:show="readConfirmShow" <view class="agree-close" @click="closeAgreeModal">
title="服务协议及隐私保护" <text></text>
:buttons="[ </view>
{ text: '不同意', plain: true }, <view class="agree-title-wrap">
{ text: '同意', plain: false, color: '#fff' }, <view class="agree-title">服务协议及隐私保护</view>
]" <view class="agree-title-line"></view>
@click="onReadConfirm" </view>
> <view class="agree-content">
<view class="modal-notice"> <view class="agree-text">
点击同意即表示您已阅读并理解 为了更好的保障您的合法权益,请您阅读并同意
<text class="link-inline" @click.stop="Link.to(Link.services, '服务协议')">《服务协议》</text> <text class="agree-link" @click.stop="Link.to(Link.services, '用户协议')">《用户协议》</text>
<text class="link-inline" @click.stop="Link.to(Link.privacy, '隐私政策')">《隐私政策》</text> <text class="agree-link" @click.stop="Link.to(Link.privacy, '隐私协议')">《隐私协议》</text>
</view>
<view class="agree-btn" @click="onReadConfirm">同意并继续</view>
</view>
</view>
</view> </view>
</fui-modal>
<SlideVerify v-model:show="showSlide" @success="onSlideSuccess" /> <view class="slide-mask" v-if="showSlideVerify" @click.self="showSlideVerify = false">
<view class="slide-panel">
<view class="slide-gradient-header"></view>
<view class="slide-close" @click="showSlideVerify = false">
<text></text>
</view>
<view class="slide-title-wrap">
<view class="slide-title">安全验证</view>
<view class="slide-title-line"></view>
</view>
<view class="slide-content">
<view class="slide-desc">按住滑块,拖动到最右侧</view>
<fui-slide-verify
ref="slideVerifyRef"
width="540"
height="80"
slider-width="80"
background="#f5f7f5"
active-bg-color="#5DB66F"
pass-color="#5DB66F"
arrow-color="#5DB66F"
line-color="#5DB66F"
color="#999"
active-color="#fff"
@success="onSlideSuccess"
></fui-slide-verify>
</view>
</view>
</view>
</view> </view>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
@keyframes float-slow {
0%,
100% {
transform: translateY(0) rotate(15deg);
}
50% {
transform: translateY(-20rpx) rotate(10deg);
}
}
.login-page { .login-page {
min-height: 100vh; min-height: 100vh;
background-color: #f7faf8; background-color: #f7faf8;
...@@ -369,9 +379,8 @@ clearInterval(countdownTimer) ...@@ -369,9 +379,8 @@ clearInterval(countdownTimer)
overflow-x: hidden; overflow-x: hidden;
} }
/* 顶部视觉层:农业生产全景 */
.header-visual { .header-visual {
height: 600rpx; height: 550rpx;
position: relative; position: relative;
padding-top: var(--status-bar-height); padding-top: var(--status-bar-height);
overflow: hidden; overflow: hidden;
...@@ -382,7 +391,7 @@ clearInterval(countdownTimer) ...@@ -382,7 +391,7 @@ clearInterval(countdownTimer)
height: 100%; height: 100%;
top: 0; top: 0;
left: 0; left: 0;
filter: brightness(0.8); filter: brightness(0.9);
} }
.header-overlay { .header-overlay {
...@@ -391,134 +400,134 @@ clearInterval(countdownTimer) ...@@ -391,134 +400,134 @@ clearInterval(countdownTimer)
height: 100%; height: 100%;
top: 0; top: 0;
left: 0; left: 0;
background: linear-gradient(180deg, rgb(93 182 111 / 40%) 0%, rgb(76 175 80 / 80%) 100%); background: linear-gradient(180deg, rgba(93, 182, 111, 0.3) 0%, rgba(76, 175, 80, 0.7) 100%);
} }
/* 漂浮装饰:增强数智感 */
.float-icon { .float-icon {
position: absolute; position: absolute;
opacity: 0.6; opacity: 0.7;
filter: drop-shadow(0 4rpx 10rpx rgb(0 0 0 / 20%)); filter: drop-shadow(0 4rpx 10rpx rgba(0,0,0,0.15));
&.device-1 { &.device-1 {
width: 120rpx; width: 130rpx;
height: 120rpx; height: 130rpx;
right: -20rpx; right: 20rpx;
top: 150rpx; top: 180rpx;
transform: rotate(15deg); transform: rotate(10deg);
animation: float-slow 4s ease-in-out infinite; animation: float-slow 4s ease-in-out infinite;
} }
&.device-2 { &.device-2 {
width: 100rpx; width: 110rpx;
height: 100rpx; height: 110rpx;
left: 40rpx; left: 30rpx;
bottom: 120rpx; bottom: 100rpx;
transform: rotate(-10deg);
animation: float-slow 3s ease-in-out infinite alternate; animation: float-slow 3s ease-in-out infinite alternate;
} }
} }
.top-nav-bar {
position: relative;
z-index: 10;
display: flex;
justify-content: flex-start;
align-items: center;
padding: 20rpx 40rpx;
}
.brand-hero { .brand-hero {
position: relative; position: relative;
z-index: 10; z-index: 10;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
margin-top: 50rpx; margin-top: 80rpx;
.logo-box { .logo-box {
width: 160rpx; width: 140rpx;
height: 160rpx; height: 140rpx;
background: #fff; background: #fff;
border-radius: 44rpx; border-radius: 40rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 16rpx 32rpx rgb(0 0 0 / 15%); box-shadow: 0 12rpx 24rpx rgba(0,0,0,0.1);
.logo-img { .logo-img {
width: 100rpx; width: 90rpx;
height: 100rpx; height: 90rpx;
} }
} }
.brand-info { .brand-info {
margin-top: 30rpx; margin-top: 24rpx;
text-align: center; text-align: center;
color: #fff; color: #fff;
.app-title { .app-title {
font-size: 48rpx; font-size: 44rpx;
font-weight: 800; font-weight: 800;
letter-spacing: 4rpx; letter-spacing: 2rpx;
text-shadow: 0 4rpx 8rpx rgb(0 0 0 / 20%); background: linear-gradient(90deg, #fff 0%, #fff 40%, rgba(255,255,255,0.3) 50%, #fff 60%, #fff 100%);
background-size: 300% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: shine-text 3s ease-in-out infinite;
} }
.app-slogan { .app-slogan {
margin-top: 12rpx; margin-top: 8rpx;
font-size: 26rpx; font-size: 24rpx;
opacity: 0.9; opacity: 0.95;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.weather-icon { .weather-icon {
width: 40rpx; width: 36rpx;
height: 40rpx; height: 36rpx;
margin-right: 12rpx; margin-right: 10rpx;
} }
} }
.app-slogan text {
background: linear-gradient(90deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.9) 40%, rgba(255,255,255,0.4) 50%, rgba(255,255,255,0.9) 60%, rgba(255,255,255,0.9) 100%);
background-size: 300% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: shine-text 3s ease-in-out infinite 0.5s;
}
} }
} }
} }
/* 登录卡片层 */
.main-card-container { .main-card-container {
padding: 0 40rpx; padding: 0 40rpx;
margin-top: -100rpx; margin-top: -80rpx;
position: relative; position: relative;
z-index: 20; z-index: 20;
} }
.aesthetic-card { .aesthetic-card {
background: #fff; background: #ffffff;
border-radius: 56rpx; border-radius: 50rpx;
padding: 50rpx 50rpx 40rpx; padding: 70rpx 50rpx 60rpx;
box-shadow: 0 20rpx 60rpx rgb(0 0 0 / 8%); box-shadow: 0 15rpx 50rpx rgba(0, 0, 0, 0.06);
.card-header { .card-header {
margin-bottom: 36rpx; margin-bottom: 50rpx;
.welcome-text { .welcome-text {
font-size: 36rpx; font-size: 38rpx;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
} }
.green-line { .green-line {
width: 60rpx; width: 50rpx;
height: 8rpx; height: 7rpx;
background: #5db66f; background: #5DB66F;
border-radius: 4rpx; border-radius: 4rpx;
margin-top: 12rpx; margin-top: 14rpx;
} }
} }
.field-item { .field-item {
margin-bottom: 30rpx; margin-bottom: 40rpx;
.field-label { .field-label {
font-size: 24rpx; font-size: 26rpx;
color: #999; color: #999;
margin-left: 4rpx; margin-left: 4rpx;
} }
...@@ -530,9 +539,9 @@ clearInterval(countdownTimer) ...@@ -530,9 +539,9 @@ clearInterval(countdownTimer)
.sms-trigger { .sms-trigger {
white-space: nowrap; white-space: nowrap;
font-size: 26rpx; font-size: 27rpx;
color: #5db66f; color: #5DB66F;
padding: 12rpx 24rpx; padding: 14rpx 28rpx;
background: #f1f9f2; background: #f1f9f2;
border-radius: 50rpx; border-radius: 50rpx;
margin-left: 20rpx; margin-left: 20rpx;
...@@ -548,64 +557,277 @@ clearInterval(countdownTimer) ...@@ -548,64 +557,277 @@ clearInterval(countdownTimer)
} }
.action-footer { .action-footer {
margin-top: 50rpx; margin-top: 60rpx;
.btn-wrapper {
transition: opacity 0.3s;
}
.secondary-links { .secondary-links {
margin-top: 28rpx; margin-top: 40rpx;
text-align: center; text-align: center;
.auto-hint {
font-size: 24rpx;
color: #ccc;
margin-bottom: 24rpx;
}
.reg-btn { .reg-btn {
font-size: 30rpx; font-size: 28rpx;
color: #666; color: #aaa;
font-weight: 500;
display: inline-block;
padding: 10rpx 40rpx;
border: 1rpx solid #eee;
border-radius: 40rpx;
} }
} }
} }
.policy-wrapper { .policy-wrapper {
margin-top: 50rpx; margin-top: 60rpx;
.policy-label { .policy-label {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: center;
}
.checkbox-align {
line-height: 1;
display: flex;
align-items: center;
flex-shrink: 0;
margin-top: 4rpx;
} }
.policy-text { .policy-text {
font-size: 24rpx; font-size: 24rpx;
color: #bbb; color: #bbb;
margin-left: 12rpx; margin-left: 12rpx;
line-height: 1.4; flex: 1;
line-height: 1.6;
word-break: break-all;
} }
.link { .link {
color: #5db66f; color: #5DB66F;
font-weight: 500; font-weight: 500;
} }
} }
.modal-notice { .footer-aesthetic {
flex: 1;
background: linear-gradient(180deg, #f7faf8 0%, #edf3ef 100%);
}
.agree-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.agree-panel {
width: 640rpx;
background: #ffffff;
border-radius: 50rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 15rpx 50rpx rgba(0, 0, 0, 0.08);
position: relative;
overflow: hidden;
}
.agree-gradient-header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 180rpx;
background: linear-gradient(180deg, #4CAF50 0%, #5DB66F 40%, rgba(93, 182, 111, 0.15) 85%, transparent 100%);
border-radius: 50rpx 50rpx 0 0;
}
.agree-close {
position: absolute;
top: 20rpx;
right: 24rpx;
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.25);
z-index: 2;
text {
font-size: 32rpx;
color: #ffffff;
line-height: 1;
}
}
.agree-title-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 48rpx;
margin-bottom: 20rpx;
position: relative;
z-index: 1;
}
.agree-title {
font-size: 36rpx;
font-weight: 700;
color: #ffffff;
margin-bottom: 10rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.12);
}
.agree-title-line {
width: 48rpx;
height: 6rpx;
background: rgba(255, 255, 255, 0.7);
border-radius: 3rpx;
}
.agree-content {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 36rpx 50rpx 48rpx;
position: relative;
z-index: 1;
}
.agree-text {
font-size: 28rpx; font-size: 28rpx;
color: #666; color: #666;
line-height: 1.8; line-height: 1.8;
text-align: center;
margin-bottom: 44rpx;
}
.link-inline { .agree-link {
color: #5db66f; color: #5DB66F;
font-weight: bold; font-weight: 600;
} }
.agree-btn {
width: 100%;
height: 90rpx;
background: linear-gradient(90deg, #5DB66F 0%, #4CAF50 100%);
border-radius: 100rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #ffffff;
font-weight: 600;
box-shadow: 0 6rpx 20rpx rgba(93, 182, 111, 0.3);
} }
.footer-aesthetic { .slide-mask {
flex: 1; position: fixed;
background: linear-gradient(180deg, #f7faf8 0%, #edf3ef 100%); inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.slide-panel {
width: 640rpx;
background: #ffffff;
border-radius: 50rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 15rpx 50rpx rgba(0, 0, 0, 0.08);
position: relative;
overflow: hidden;
}
.slide-gradient-header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 180rpx;
background: linear-gradient(180deg, #4CAF50 0%, #5DB66F 40%, rgba(93, 182, 111, 0.15) 85%, transparent 100%);
border-radius: 50rpx 50rpx 0 0;
}
.slide-close {
position: absolute;
top: 20rpx;
right: 24rpx;
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.25);
z-index: 2;
text {
font-size: 32rpx;
color: #ffffff;
line-height: 1;
}
}
.slide-title-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 48rpx;
margin-bottom: 20rpx;
position: relative;
z-index: 1;
}
.slide-title {
font-size: 36rpx;
font-weight: 700;
color: #ffffff;
margin-bottom: 10rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.12);
}
.slide-title-line {
width: 48rpx;
height: 6rpx;
background: rgba(255, 255, 255, 0.7);
border-radius: 3rpx;
}
.slide-content {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 36rpx 50rpx 48rpx;
position: relative;
z-index: 1;
}
.slide-desc {
font-size: 26rpx;
color: #999;
margin-bottom: 36rpx;
letter-spacing: 0.5rpx;
}
@keyframes float-slow {
0%, 100% { transform: translateY(0) rotate(5deg); }
50% { transform: translateY(-15rpx) rotate(-5deg); }
}
@keyframes shine-text {
0% { background-position: 0% 0; }
30% { background-position: 0% 0; }
60% { background-position: 100% 0; }
100% { background-position: 100% 0; }
} }
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { useUserStore } from '@/store/modules/user'
import * as API from '/@/api/model/userInfo' import * as API from '/@/api/model/userInfo'
import SlideVerify from '@/components/slide-verify/SlideVerify.vue' import Link from '@/utils/const/link'
import { Message } from '@/common'
const defaultText = '湘农数智农服' const userStore = useUserStore()
const form = ref() const form = ref()
const slideVerifyRef = ref()
const showSlide = ref(false) const showSlide = ref(false)
const slideType = ref<'sms' | 'register'>('register') const slideAction = ref<'sms' | 'submit'>('sms')
const isForgotPwd = ref(false)
onLoad((options) => {
if (options?.type === 'forgot') {
isForgotPwd.value = true
}
})
const model = reactive({ const model = reactive({
loading: false, loading: false,
countdown: 0, countdown: 0,
...@@ -18,14 +30,25 @@ ...@@ -18,14 +30,25 @@
}, },
{ {
name: 'code', name: 'code',
rule: ['required'], rule: ['required', 'minLength:6', 'maxLength:6'],
msg: ['请输入验证码'], msg: ['请输入验证码', '验证码为6位数字', '验证码为6位数字'],
}, },
{ {
name: 'password', name: 'password',
rule: ['required', 'minLength:6'], rule: ['required', 'minLength:6'],
msg: ['请输入密码', '密码长度不能少于6位'], msg: ['请输入密码', '密码长度不能少于6位'],
}, },
{
name: 'confirmPassword',
rule: ['required', 'minLength:6'],
msg: ['请确认密码', '密码长度不能少于6位'],
validator: [
{
msg: '两次输入的密码不一致',
method: (value: string) => value === model.form.data.password,
},
],
},
], ],
rulesPhone: [ rulesPhone: [
{ {
...@@ -37,6 +60,7 @@ ...@@ -37,6 +60,7 @@
data: { data: {
phone: '', phone: '',
password: '', password: '',
confirmPassword: '',
code: '', code: '',
read: false, read: false,
}, },
...@@ -45,7 +69,7 @@ ...@@ -45,7 +69,7 @@
let countdownTimer: any = null let countdownTimer: any = null
function register() { function submit() {
if (!model.form.data.read) { if (!model.form.data.read) {
Message.toast('请阅读并同意服务协议及隐私政策') Message.toast('请阅读并同意服务协议及隐私政策')
return return
...@@ -53,13 +77,16 @@ ...@@ -53,13 +77,16 @@
form?.value.validator(model.form.data, model.form.rules).then(async (res: { isPassed: boolean }) => { form?.value.validator(model.form.data, model.form.rules).then(async (res: { isPassed: boolean }) => {
if (res.isPassed) { if (res.isPassed) {
slideType.value = 'register' slideAction.value = 'submit'
showSlide.value = true showSlide.value = true
slideVerifyRef.value.reset()
} else {
Message.toast(res.errorMsg)
} }
}) })
} }
function doRegister() { function doSubmit() {
const params = { const params = {
phone: model.form.data.phone, phone: model.form.data.phone,
password: model.form.data.password, password: model.form.data.password,
...@@ -67,15 +94,22 @@ ...@@ -67,15 +94,22 @@
} }
model.loading = true model.loading = true
API.sysRegister(params) API.sysRegister(params)
.then(async (body) => { .then(async (body) => {
if (body) { if (body?.token) {
Message.toast(`注册成功, 请登录~`) userStore.setToken(body.token)
setTimeout(() => { const user = await API.getUserInfo()
goLogin() userStore.setUserInfo(user)
}, 1500) Message.toast(isForgotPwd.value ? '密码重置成功' : '注册成功,欢迎加入~')
goHome()
} else {
Message.toast(body?.message || (isForgotPwd.value ? '密码重置失败' : '注册失败'))
} }
}) })
.catch((err) => {
Message.toast(err?.message || (isForgotPwd.value ? '密码重置失败,请稍后重试' : '注册失败,请稍后重试'))
})
.finally(() => { .finally(() => {
model.loading = false model.loading = false
}) })
...@@ -85,30 +119,33 @@ ...@@ -85,30 +119,33 @@
form?.value.validator(model.form.data, model.form.rulesPhone).then(async (res: { isPassed: boolean }) => { form?.value.validator(model.form.data, model.form.rulesPhone).then(async (res: { isPassed: boolean }) => {
if (res.isPassed) { if (res.isPassed) {
if (model.countdown > 0) return if (model.countdown > 0) return
slideType.value = 'sms' slideAction.value = 'sms'
showSlide.value = true showSlide.value = true
slideVerifyRef.value.reset()
} else {
Message.toast(res.errorMsg)
} }
}) })
} }
function onSlideSuccess() { function onSlideSuccess() {
if (slideType.value === 'sms') { showSlide.value = false
// 开启乐观倒计时,不论接口成败,60s内不准重发 if (slideAction.value === 'submit') {
doSubmit()
} else {
startCountdown() startCountdown()
const params = { const params = {
mobile: model.form.data.phone, mobile: model.form.data.phone,
smsmode: 2, // 2-注册 smsmode: isForgotPwd.value ? 3 : 2,
} }
API.sysSms(params) API.sysSms(params)
.then(async () => { .then(async () => {
Message.toast('验证码已发送') Message.toast('验证码已发送')
}) })
.catch((err) => { .catch((err) => {
console.error('注册短信发送失败:', err) console.error('短信发送失败:', err)
}) })
} else {
doRegister()
} }
} }
...@@ -155,30 +192,30 @@ ...@@ -155,30 +192,30 @@
<view class="login-page"> <view class="login-page">
<!-- 顶部视觉层:阳光农场与气象设备 --> <!-- 顶部视觉层:阳光农场与气象设备 -->
<view class="header-visual"> <view class="header-visual">
<image class="header-scene" src="/static/images/login/farm_base_bg.png" mode="aspectFill" /> <image class="header-scene" src="/static/images/login/farm_base_bg.png" mode="aspectFill"></image>
<view class="header-overlay"></view> <view class="header-overlay"></view>
<!-- 气象站与农业设备:体现数智感 --> <!-- 气象站与农业设备:体现数智感 -->
<image class="float-icon device-1" src="/static/images/nongchang/device1.png" mode="aspectFit" /> <image class="float-icon device-1" src="/static/images/nongchang/device1.png" mode="aspectFit"></image>
<image class="float-icon device-2" src="/static/images/nongchang/device3.png" mode="aspectFit" /> <image class="float-icon device-2" src="/static/images/nongchang/device3.png" mode="aspectFit"></image>
<!-- 顶部导航 --> <!-- 顶部导航 -->
<view class="top-nav-bar"> <view class="top-nav-bar">
<view class="back-btn" @click="goLogin"> <view class="back-btn" @click="goLogin">
<fui-icon name="arrowleft" :size="48" color="#fff" /> <fui-icon name="arrowleft" :size="48" color="#fff"></fui-icon>
</view> </view>
</view> </view>
<!-- 品牌标识 --> <!-- 品牌标识 -->
<view class="brand-hero"> <view class="brand-hero">
<view class="logo-box"> <view class="logo-box">
<image class="logo-img" src="/static/logo.png" mode="aspectFit" /> <image class="logo-img" src="/static/logo.png" mode="aspectFit"></image>
</view> </view>
<view class="brand-info"> <view class="brand-info">
<text class="app-title">加入数智农服</text> <text class="app-title">{{ isForgotPwd ? '重置密码' : '加入数智农服' }}</text>
<view class="app-slogan"> <view class="app-slogan">
<image src="/static/images/weather/100.svg" class="weather-icon" /> <image src="/static/images/weather/100.svg" class="weather-icon" mode="aspectFit"></image>
<text>开启您的智慧农业之旅</text> <text>{{ isForgotPwd ? '设置新密码,安全便捷' : '开启您的智慧农业之旅' }}</text>
</view> </view>
</view> </view>
</view> </view>
...@@ -189,7 +226,7 @@ ...@@ -189,7 +226,7 @@
<view class="aesthetic-card"> <view class="aesthetic-card">
<fui-form ref="form" top="0" :padding="['0', '0']" background="transparent"> <fui-form ref="form" top="0" :padding="['0', '0']" background="transparent">
<view class="card-header"> <view class="card-header">
<text class="welcome-text">创建新账号</text> <text class="welcome-text">{{ isForgotPwd ? '重置密码' : '创建新账号' }}</text>
<view class="green-line"></view> <view class="green-line"></view>
</view> </view>
...@@ -198,15 +235,16 @@ ...@@ -198,15 +235,16 @@
<text class="field-label">手机号</text> <text class="field-label">手机号</text>
<fui-input <fui-input
v-model="model.form.data.phone" v-model="model.form.data.phone"
:padding="['24rpx', '0']" :padding="['16rpx', '0']"
placeholder="请输入手机号" placeholder="请输入手机号"
backgroundColor="transparent" backgroundColor="transparent"
placeholderStyle="font-size:16px;"
borderColor="#F0F0F0" borderColor="#F0F0F0"
borderBottom borderBottom
height="110rpx" height="80rpx"
size="34" size="34"
maxlength="11" maxlength="11"
/> ></fui-input>
</view> </view>
<view class="field-item"> <view class="field-item">
...@@ -214,15 +252,17 @@ ...@@ -214,15 +252,17 @@
<view class="code-inner"> <view class="code-inner">
<fui-input <fui-input
v-model="model.form.data.code" v-model="model.form.data.code"
:padding="['24rpx', '0']" :padding="['16rpx', '0']"
placeholder="请输入验证码" placeholder="请输入验证码"
backgroundColor="transparent" backgroundColor="transparent"
placeholderStyle="font-size:16px;"
borderColor="#F0F0F0" borderColor="#F0F0F0"
borderBottom borderBottom
height="110rpx" height="80rpx"
size="34" size="30"
maxlength="6"
autocomplete="off" autocomplete="off"
/> ></fui-input>
<view class="sms-trigger" :class="{ inactive: model.countdown > 0 }" @click="smsCode"> <view class="sms-trigger" :class="{ inactive: model.countdown > 0 }" @click="smsCode">
{{ model.countdown > 0 ? `${model.countdown}s后重发` : '获取验证码' }} {{ model.countdown > 0 ? `${model.countdown}s后重发` : '获取验证码' }}
</view> </view>
...@@ -234,33 +274,56 @@ ...@@ -234,33 +274,56 @@
<fui-input <fui-input
v-model="model.form.data.password" v-model="model.form.data.password"
type="password" type="password"
:padding="['24rpx', '0']" :padding="['16rpx', '0']"
placeholder="请设置6位以上登录密码" placeholder="请设置6位以上密码(建议字母+数字)"
backgroundColor="transparent"
placeholderStyle="font-size:16px;"
borderColor="#F0F0F0"
borderBottom
height="80rpx"
size="34"
autocomplete="new-password"
></fui-input>
</view>
<view class="field-item">
<text class="field-label">确认密码</text>
<fui-input
v-model="model.form.data.confirmPassword"
type="password"
:padding="['16rpx', '0']"
placeholder="请再次输入密码"
backgroundColor="transparent" backgroundColor="transparent"
placeholderStyle="font-size:16px;"
borderColor="#F0F0F0" borderColor="#F0F0F0"
borderBottom borderBottom
height="110rpx" height="80rpx"
size="34" size="34"
autocomplete="new-password" autocomplete="new-password"
/> ></fui-input>
</view> </view>
</view> </view>
<view class="action-footer"> <view class="action-footer">
<view class="btn-wrapper">
<fui-button <fui-button
text="立即注册" :text="isForgotPwd ? '确认重置' : '立即注册'"
background="linear-gradient(90deg, #5DB66F 0%, #4CAF50 100%)" background="linear-gradient(90deg, #5DB66F 0%, #4CAF50 100%)"
radius="100rpx" radius="100rpx"
height="110rpx" height="80rpx"
@click="register" @click="submit"
:loading="model.loading" :loading="model.loading"
:disabled="model.loading" :disabled="model.loading"
size="36" color="#ffffff"
disabled-color="#888888"
disabled-background="#f5f5f5"
size="30"
shadow shadow
/> ></fui-button>
</view>
<view class="secondary-links"> <view class="secondary-links">
<view class="reg-btn" @click="goLogin">已有账号?去登录</view> <view class="reg-btn" @click="goLogin">{{ isForgotPwd ? '返回登录' : '已有账号?去登录' }}</view>
</view> </view>
</view> </view>
...@@ -271,10 +334,10 @@ ...@@ -271,10 +334,10 @@
<view class="checkbox-align"> <view class="checkbox-align">
<fui-checkbox <fui-checkbox
color="#5DB66F" color="#5DB66F"
:scale="0.7" :scaleRatio="0.7"
:checked="model.form.data.read" :checked="model.form.data.read"
@change="(e) => (model.form.data.read = e.checked)" @change="(e) => (model.form.data.read = e.checked)"
/> ></fui-checkbox>
</view> </view>
<view class="policy-text"> <view class="policy-text">
同意<text class="link" @click.stop="Link.to(Link.services, '服务协议')">《服务协议》</text>与<text class="link" @click.stop="Link.to(Link.privacy, '隐私政策')">《隐私政策》</text> 同意<text class="link" @click.stop="Link.to(Link.services, '服务协议')">《服务协议》</text>与<text class="link" @click.stop="Link.to(Link.privacy, '隐私政策')">《隐私政策》</text>
...@@ -288,7 +351,35 @@ ...@@ -288,7 +351,35 @@
<view class="footer-aesthetic"></view> <view class="footer-aesthetic"></view>
<SlideVerify v-model:show="showSlide" @success="onSlideSuccess" /> <view class="slide-mask" v-if="showSlide" @click.self="showSlide = false">
<view class="slide-panel">
<view class="slide-gradient-header"></view>
<view class="slide-close" @click="showSlide = false">
<text></text>
</view>
<view class="slide-title-wrap">
<view class="slide-title">安全验证</view>
<view class="slide-title-line"></view>
</view>
<view class="slide-content">
<view class="slide-desc">按住滑块,拖动到最右侧</view>
<fui-slide-verify
ref="slideVerifyRef"
width="540"
height="80"
slider-width="80"
background="#f5f7f5"
active-bg-color="#5DB66F"
pass-color="#5DB66F"
arrow-color="#5DB66F"
line-color="#5DB66F"
color="#999"
active-color="#fff"
@success="onSlideSuccess"
></fui-slide-verify>
</view>
</view>
</view>
</view> </view>
</template> </template>
...@@ -302,7 +393,7 @@ ...@@ -302,7 +393,7 @@
} }
.header-visual { .header-visual {
height: 550rpx; height: 460rpx;
position: relative; position: relative;
padding-top: var(--status-bar-height); padding-top: var(--status-bar-height);
overflow: hidden; overflow: hidden;
...@@ -331,18 +422,18 @@ ...@@ -331,18 +422,18 @@
filter: drop-shadow(0 4rpx 10rpx rgba(0,0,0,0.15)); filter: drop-shadow(0 4rpx 10rpx rgba(0,0,0,0.15));
&.device-1 { &.device-1 {
width: 130rpx; width: 100rpx;
height: 130rpx; height: 100rpx;
right: 20rpx; right: 20rpx;
top: 180rpx; top: 140rpx;
transform: rotate(10deg); transform: rotate(10deg);
animation: float-slow 4s ease-in-out infinite; animation: float-slow 4s ease-in-out infinite;
} }
&.device-2 { &.device-2 {
width: 110rpx; width: 90rpx;
height: 110rpx; height: 90rpx;
left: 30rpx; left: 30rpx;
bottom: 100rpx; bottom: 80rpx;
animation: float-slow 3s ease-in-out infinite alternate; animation: float-slow 3s ease-in-out infinite alternate;
} }
} }
...@@ -359,11 +450,11 @@ ...@@ -359,11 +450,11 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
margin-top: 20rpx; margin-top: 0rpx;
.logo-box { .logo-box {
width: 140rpx; width: 120rpx;
height: 140rpx; height: 120rpx;
background: #fff; background: #fff;
border-radius: 40rpx; border-radius: 40rpx;
display: flex; display: flex;
...@@ -372,25 +463,25 @@ ...@@ -372,25 +463,25 @@
box-shadow: 0 12rpx 24rpx rgba(0,0,0,0.1); box-shadow: 0 12rpx 24rpx rgba(0,0,0,0.1);
.logo-img { .logo-img {
width: 90rpx; width: 76rpx;
height: 90rpx; height: 76rpx;
} }
} }
.brand-info { .brand-info {
margin-top: 24rpx; margin-top: 16rpx;
text-align: center; text-align: center;
color: #fff; color: #fff;
.app-title { .app-title {
font-size: 44rpx; font-size: 38rpx;
font-weight: 800; font-weight: 800;
letter-spacing: 2rpx; letter-spacing: 2rpx;
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.1); text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.1);
} }
.app-slogan { .app-slogan {
margin-top: 8rpx; margin-top: 4rpx;
font-size: 24rpx; font-size: 24rpx;
opacity: 0.95; opacity: 0.95;
display: flex; display: flex;
...@@ -417,33 +508,34 @@ ...@@ -417,33 +508,34 @@
.aesthetic-card { .aesthetic-card {
background: #ffffff; background: #ffffff;
border-radius: 50rpx; border-radius: 50rpx;
padding: 70rpx 50rpx 60rpx; padding: 36rpx 50rpx 30rpx;
box-shadow: 0 15rpx 50rpx rgba(0, 0, 0, 0.06); box-shadow: 0 15rpx 50rpx rgba(0, 0, 0, 0.06);
.card-header { .card-header {
margin-bottom: 50rpx; margin-bottom: 24rpx;
.welcome-text { .welcome-text {
font-size: 38rpx; font-size: 34rpx;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
} }
.green-line { .green-line {
width: 50rpx; width: 44rpx;
height: 7rpx; height: 6rpx;
background: #5DB66F; background: #5DB66F;
border-radius: 4rpx; border-radius: 3rpx;
margin-top: 14rpx; margin-top: 10rpx;
} }
} }
.field-item { .field-item {
margin-bottom: 40rpx; margin-bottom: 20rpx;
.field-label { .field-label {
font-size: 26rpx; font-size: 24rpx;
color: #999; color: #999;
margin-left: 4rpx; margin-left: 4rpx;
margin-bottom: 2rpx;
} }
.code-inner { .code-inner {
...@@ -453,9 +545,9 @@ ...@@ -453,9 +545,9 @@
.sms-trigger { .sms-trigger {
white-space: nowrap; white-space: nowrap;
font-size: 27rpx; font-size: 24rpx;
color: #5DB66F; color: #5DB66F;
padding: 14rpx 28rpx; padding: 10rpx 24rpx;
background: #f1f9f2; background: #f1f9f2;
border-radius: 50rpx; border-radius: 50rpx;
margin-left: 20rpx; margin-left: 20rpx;
...@@ -471,28 +563,29 @@ ...@@ -471,28 +563,29 @@
} }
.action-footer { .action-footer {
margin-top: 70rpx; margin-top: 40rpx;
.btn-wrapper {
transition: opacity 0.3s;
}
.secondary-links { .secondary-links {
margin-top: 40rpx; margin-top: 38rpx;
text-align: center; text-align: center;
.reg-btn { .reg-btn {
font-size: 28rpx; font-size: 28rpx;
color: #888; color: #aaa;
padding: 10rpx 40rpx;
border: 1rpx solid #f0f0f0;
border-radius: 40rpx;
} }
} }
} }
.policy-wrapper { .policy-wrapper {
margin-top: 60rpx; margin-top: 62rpx;
.policy-label { .policy-label {
display: flex; display: flex;
align-items: center; /* 垂直居中 */ align-items: flex-start;
justify-content: center; justify-content: center;
} }
...@@ -500,12 +593,14 @@ ...@@ -500,12 +593,14 @@
line-height: 1; line-height: 1;
display: flex; display: flex;
align-items: center; align-items: center;
flex-shrink: 0;
margin-top: 4rpx;
} }
.policy-text { .policy-text {
font-size: 24rpx; font-size: 22rpx;
color: #bbb; color: #bbb;
margin-left: 12rpx; margin-left: 10rpx;
display: flex; display: flex;
align-items: center; align-items: center;
} }
...@@ -521,6 +616,100 @@ ...@@ -521,6 +616,100 @@
background: linear-gradient(180deg, #f7faf8 0%, #edf3ef 100%); background: linear-gradient(180deg, #f7faf8 0%, #edf3ef 100%);
} }
.slide-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.slide-panel {
width: 640rpx;
background: #ffffff;
border-radius: 50rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 15rpx 50rpx rgba(0, 0, 0, 0.08);
position: relative;
overflow: hidden;
}
.slide-gradient-header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 180rpx;
background: linear-gradient(180deg, #4CAF50 0%, #5DB66F 40%, rgba(93, 182, 111, 0.15) 85%, transparent 100%);
border-radius: 50rpx 50rpx 0 0;
}
.slide-close {
position: absolute;
top: 20rpx;
right: 24rpx;
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.25);
z-index: 2;
text {
font-size: 32rpx;
color: #ffffff;
line-height: 1;
}
}
.slide-title-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 48rpx;
margin-bottom: 20rpx;
position: relative;
z-index: 1;
}
.slide-title {
font-size: 36rpx;
font-weight: 700;
color: #ffffff;
margin-bottom: 10rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.12);
}
.slide-title-line {
width: 48rpx;
height: 6rpx;
background: rgba(255, 255, 255, 0.7);
border-radius: 3rpx;
}
.slide-content {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 36rpx 50rpx 48rpx;
position: relative;
z-index: 1;
}
.slide-desc {
font-size: 26rpx;
color: #999;
margin-bottom: 36rpx;
letter-spacing: 0.5rpx;
}
@keyframes float-slow { @keyframes float-slow {
0%, 100% { transform: translateY(0) rotate(5deg); } 0%, 100% { transform: translateY(0) rotate(5deg); }
50% { transform: translateY(-15rpx) rotate(-5deg); } 50% { transform: translateY(-15rpx) rotate(-5deg); }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论