提交 4a6f63f4 作者: 廖在望

feat: app样式调整和增加验证码登录

上级 17c12bdd
......@@ -100,6 +100,14 @@ export function editFarmbase(params = {}) {
params,
})
}
export function getFarmbaseDetail(params: any = {}) {
return getFarmbaseInfoById(params)
}
export function saveFarmbase(params: any = {}) {
return params?.id ? editFarmbase(params) : addFarmbase(params)
}
/**
* 基地列表
* @param {any} params
......
<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>
......@@ -141,7 +141,7 @@
overflow-x: hidden;
.section {
padding: 28rpx 38rpx 382rpx;
background-image: url('/static/images/codefun/05019411cdb9383e51ab8923569568df.png');
background-image: url('../../../static/images/codefun/05019411cdb9383e51ab8923569568df.png');
background-size: 100% 100%;
background-repeat: no-repeat;
.group {
......
......@@ -465,7 +465,7 @@
padding-bottom: 32rpx;
.section {
padding: 0 28rpx 8rpx;
background-image: url('/static/images/codefun/7a5dc4ee864fe55da98b41c14ee3b931.png');
background-image: url('../../static/images/codefun/7a5dc4ee864fe55da98b41c14ee3b931.png');
background-size: 100% 100%;
background-repeat: no-repeat;
.group {
......
......@@ -163,7 +163,7 @@
overflow-x: hidden;
.section {
padding: 28rpx 38rpx 382rpx;
background-image: url('/static/images/codefun/7f3f04389e8c4f41e9cf97f290ffa85d.png');
background-image: url('../../../static/images/codefun/7f3f04389e8c4f41e9cf97f290ffa85d.png');
background-size: 100% 100%;
background-repeat: no-repeat;
.group {
......
......@@ -186,7 +186,7 @@
overflow-x: hidden;
.section {
padding: 28rpx 42rpx 382rpx;
background-image: url('/static/images/codefun/319caa972700f74982472280d1edb65e.png');
background-image: url('../../../static/images/codefun/319caa972700f74982472280d1edb65e.png');
background-size: 100% 100%;
background-repeat: no-repeat;
.group {
......
......@@ -146,7 +146,7 @@
overflow-x: hidden;
.section {
padding: 28rpx 38rpx 382rpx;
background-image: url('/static/images/codefun/ad85a97458671a968e690f0108861184.png');
background-image: url('../../../static/images/codefun/ad85a97458671a968e690f0108861184.png');
background-size: 100% 100%;
background-repeat: no-repeat;
.group {
......
......@@ -161,7 +161,7 @@
overflow-x: hidden;
.section {
padding: 28rpx 38rpx 382rpx;
background-image: url('/static/images/codefun/d64598e1f76d3e1977d820f8b2f02843.png');
background-image: url('../../../static/images/codefun/d64598e1f76d3e1977d820f8b2f02843.png');
background-size: 100% 100%;
background-repeat: no-repeat;
.group {
......
......@@ -153,7 +153,7 @@
overflow-x: hidden;
.section {
padding: 28rpx 38rpx 382rpx;
background-image: url('/static/images/codefun/b5493813a21cb38af4c6d8314e21b88c.png');
background-image: url('../../../static/images/codefun/b5493813a21cb38af4c6d8314e21b88c.png');
background-size: 100% 100%;
background-repeat: no-repeat;
.group {
......
......@@ -498,7 +498,7 @@ return 'status-disconnected'
.section_5 {
padding: 12rpx 0 116rpx;
background-image: url('/static/images/codefun/4be80e2618f3c4b4aa1ce64fd9063abf.png');
background-image: url('../../static/images/codefun/4be80e2618f3c4b4aa1ce64fd9063abf.png');
background-size: 100% 100%;
background-repeat: no-repeat;
width: 160rpx;
......
......@@ -805,7 +805,7 @@
.section {
padding: 0 28rpx 30rpx 28rpx;
background-image: url('/static/images/codefun/7a5dc4ee864fe55da98b41c14ee3b931.png');
background-image: url('../../static/images/codefun/7a5dc4ee864fe55da98b41c14ee3b931.png');
background-size: 100% 100%;
background-repeat: no-repeat;
.group {
......@@ -1159,7 +1159,7 @@
.section_7 {
padding: 20rpx 0 130rpx;
border-radius: 16rpx 16rpx 0rpx 0rpx;
background-image: url('/static/images/codefun/6082011896d83113283c720e943a4999.png');
background-image: url('../../static/images/codefun/6082011896d83113283c720e943a4999.png');
background-size: 100% 100%;
background-repeat: no-repeat;
}
......@@ -1178,7 +1178,7 @@
.section_9 {
padding: 20rpx 0 130rpx;
border-radius: 16rpx 16rpx 0rpx 0rpx;
background-image: url('/static/images/codefun/7a4ec325d59361d6716c2ec1394550bb.png');
background-image: url('../../static/images/codefun/7a4ec325d59361d6716c2ec1394550bb.png');
background-size: 100% 100%;
background-repeat: no-repeat;
}
......@@ -1197,7 +1197,7 @@
.section_8 {
padding: 20rpx 0 130rpx;
border-radius: 16rpx 16rpx 0rpx 0rpx;
background-image: url('/static/images/codefun/e812ea5af6105a808af058cb0796b7ee.png');
background-image: url('../../static/images/codefun/e812ea5af6105a808af058cb0796b7ee.png');
background-size: 100% 100%;
background-repeat: no-repeat;
}
......@@ -1322,14 +1322,14 @@
mix-blend-mode: NOTTHROUGH;
.section_15 {
padding-top: 96rpx;
background-image: url('/static/images/codefun/7d1feb7eb973f087dcabf21b283162bc.png');
background-image: url('../../static/images/codefun/7d1feb7eb973f087dcabf21b283162bc.png');
background-size: 100% 100%;
background-repeat: no-repeat;
height: 200rpx;
}
.section_16 {
padding-top: 96rpx;
background-image: url('/static/images/codefun/8d11e3cbcb918502bb6582a24cddb930.png');
background-image: url('../../static/images/codefun/8d11e3cbcb918502bb6582a24cddb930.png');
background-size: 100% 100%;
background-repeat: no-repeat;
height: 200rpx;
......
<script setup lang="ts">
// import { PUSH_CLIENT_KEY } from '/@/enums/cacheEnum'
import { useUserStore } from '@/store/modules/user'
import { checkUpgrade } from '@/utils/upgrade'
import { closeSplashscreenAndChechUpgrade } from '@/utils/upgrade'
import { isDevMode } from '@/utils/env'
import Link from '@/utils/const/link'
import SlideVerify from '@/components/slide-verify/SlideVerify.vue'
import * as API from '/@/api/model/userInfo'
const userStore = useUserStore()
onShow(async () => {
// 检查是否有 token
const token = userStore.getToken
if (token) {
model.isLogin = true
}
if (model.isLogin) {
// 刷新用户信息
await userStore.refreshUserInfo()
// 跳转到登录页
goHome()
Message.toast(`欢迎回来~`)
} else {
// 关闭启动页并检查更新
try {
await checkUpgrade()
await closeSplashscreenAndChechUpgrade()
} catch (error) {
console.log('error', error)
} finally {
nextTick(() => {
model.show = true
})
}
}
})
// 页面数据
const defaultText = '湘农数智农服'
const readConfirmShow = ref<boolean>(false)
const form = ref()
const showSlide = ref(false)
const loginMode = ref(1) // 1: 验证码登录, 2: 密码登录
const slideType = ref<'sms' | 'login'>('login')
const model = reactive({
show: false,
isLogin: false,
loading: false,
text: defaultText,
countdown: 0, // 倒计时秒数
countdownTimer: null, // 倒计时定时器
countdown: 0,
form: {
phoneRules: [
{
name: 'username',
rule: ['required'],
msg: ['请输入手机号'],
rule: ['required', 'isMobile'],
msg: ['请输入手机号', '请输入正确的手机号'],
},
],
rules: [
codeLoginRules: [
{
name: 'username',
rule: ['required'],
msg: ['请输入号'],
rule: ['required', 'isMobile'],
msg: ['请输入手机号', '请输入正确的手机号'],
},
{
name: 'password',
name: 'code',
rule: ['required'],
msg: ['请输入密码'],
msg: ['请输入验证码'],
},
{
name: 'read',
validator: [
{
msg: '请阅读并同意服务协议和隐私政策',
method: (value: boolean) => {
if (!value) {
readConfirmShow.value = true
}
return value
},
/* {
},
],
},
],
pwdLoginRules: [
{
name: 'username',
rule: ['required'],
msg: ['请输入手机号'],
msg: ['请输入号'],
},
{
name: 'code',
name: 'password',
rule: ['required'],
msg: ['请输入验证码'],
}, */
msg: ['请输入码'],
},
{
name: 'read',
validator: [
{
msg: '请阅读并同意服务协议和隐私政策',
method: (value: boolean) => {
readConfirmShow.value = !value
if (!value) {
readConfirmShow.value = true
}
return value
},
},
......@@ -92,588 +100,565 @@
data: {
username: '',
password: '',
// code: '',
code: '',
read: false,
deviceId: '',
},
},
})
// TODO: 开发环境快速填入账户密码,并默认勾选已读隐私政策和服务协议
let countdownTimer: any = null
if (isDevMode()) {
model.form.data.username = ''
// model.form.data.code = ''
model.form.data.password = ''
model.form.data.read = false
model.form.data.read = true
}
/**
* 登录
*/
function login() {
form?.value.validator(model.form.data, model.form.rules).then(async (res: { isPassed: boolean }) => {
if (res.isPassed) {
// 登录参数
/* const params = {
mobile: model.form.data.username,
captcha: model.form.data.code,
} */
const params = {
username: model.form.data.username,
password: model.form.data.password,
function switchMode(mode: number) {
loginMode.value = mode
}
// 短信登录
model.loading = true
// API.phoneLogin(params)
API.sysLogin(params)
.then(async (body) => {
console.log('body', body)
if (body) {
// 设置登录缓存信息
userStore.setToken(body.token)
// 登录成功后查询用户信息
const user = await API.getUserInfo()
userStore.setUserInfo(user)
// 打开登录页
goHome()
Message.toast(`登录成功, 欢迎回来~`)
} else {
Message.toast(body.message)
return false
}
})
.finally(() => {
model.loading = false
})
function login() {
const rules = loginMode.value === 1 ? model.form.codeLoginRules : model.form.pwdLoginRules
form?.value.validator(model.form.data, rules).then(async (res: { isPassed: boolean }) => {
if (res.isPassed) {
// 登录前先做滑动验证 (Requirement 7)
slideType.value = 'login'
showSlide.value = true
}
})
}
/**
* 获取验证码
*/
function smsCode() {
form?.value.validator(model.form.data, model.form.phoneRules).then(async (res: { isPassed: boolean }) => {
if (res.isPassed) {
// 如果已经在倒计时中,不重复发送
if (model.countdown > 0) {
return
if (model.countdown > 0) return
slideType.value = 'sms'
showSlide.value = true
}
})
}
function onSlideSuccess() {
if (slideType.value === 'sms') {
// 发送验证码
const params = {
/* mobile: model.form.data.username,
smsmode: '0', */
mobile: model.form.data.username,
smsmode: 1,
smsmode: '1',
}
API.sysSms(params)
.then(async (body) => {
.then(async () => {
Message.toast('验证码已发送')
console.log('body', body)
// 开始倒计时
startCountdown()
})
.catch(() => {
// 即使请求失败也显示倒计时,防止重复点击
startCountdown()
Message.toast('验证码发送失败')
})
} else {
// 执行登录
doLogin()
}
}
function doLogin() {
model.loading = true
if (loginMode.value === 1) {
// 验证码登录
const params = {
mobile: model.form.data.username,
captcha: model.form.data.code,
}
API.phoneLogin(params)
.then(handleLoginSuccess)
.finally(() => (model.loading = false))
} else {
// 密码登录
const params = {
username: model.form.data.username,
password: model.form.data.password,
}
API.sysLogin(params)
.then(handleLoginSuccess)
.finally(() => (model.loading = false))
}
}
async function handleLoginSuccess(body: any) {
if (body && body.token) {
userStore.setToken(body.token)
const user = await API.getUserInfo()
userStore.setUserInfo(user)
goHome()
Message.toast(`登录成功, 欢迎回来~`)
} else {
Message.toast(body.message || '登录失败')
}
})
}
/**
* 开始倒计时
*/
function startCountdown() {
if (countdownTimer) clearInterval(countdownTimer)
model.countdown = 60
model.countdownTimer = setInterval(() => {
countdownTimer = setInterval(() => {
model.countdown--
if (model.countdown <= 0) {
clearInterval(model.countdownTimer)
model.countdownTimer = null
clearInterval(countdownTimer)
countdownTimer = null
}
}, 1000)
}
/**
* 跳转到门户页
*/
function goHome() {
uni.reLaunch({
url: '/pages/shouye/shouye',
})
uni.reLaunch({ url: '/pages/shouye/shouye' })
}
function goRegister() {
uni.navigateTo({ url: '/pages/login/register' })
}
onHide(() => {
model.show = false
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
})
function handleConfirm() {
model.form.data.read = true
login()
readConfirmShow.value = false
onUnload(() => {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
})
function handleCancel() {
function onReadConfirm(val: any) {
if (val.index === 0) {
model.form.data.read = false
} else {
model.form.data.read = true
login()
}
readConfirmShow.value = false
}
</script>
<template>
<view class="login_warp">
<image class="login_top_bg" src="/static/images/login/login_top_bg.png" />
<view class="login_top_warp">
<view class="login_hello">
<text class="text_hello">您好,欢迎使用</text>
<view class="login_server_name"><text class="text_server_name">湘农数智服务平台</text></view>
<view class="login-page">
<!-- 顶部设计:数智化农场全景感 -->
<view class="header-visual">
<image class="header-scene" src="/static/images/login/farm_base_bg.png" mode="aspectFill" />
<view class="header-overlay"></view>
<!-- 顶部装饰元素:漂浮的气象/农业设备(体现数智感) -->
<image class="float-icon device-1" src="/static/images/nongchang/device1.png" mode="aspectFit" />
<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 class="switch-action" @click="switchMode(loginMode === 1 ? 2 : 1)">
<text>{{ loginMode === 1 ? '密码登录' : '验证码登录' }}</text>
<fui-icon name="arrowright" :size="24" color="#fff" />
</view>
</view>
<fui-form class="form" ref="form" top="0" :padding="['0rpx', '0rpx']" background="#e46962">
<view class="login_content">
<view class="login-input-area">
<view class="user_phone">
<image class="user_phone_img" src="/static/images/register/user.png" />
<view class="user_text_view"><text class="view_text">账号</text></view>
<!-- 品牌标识与数智概览 -->
<view class="brand-hero">
<view class="logo-box">
<image class="logo-img" src="/static/logo.png" mode="aspectFit" />
</view>
<view class="brand-info">
<text class="app-title">湘农数智农服</text>
<view class="app-slogan">
<image src="/static/images/weather/100.svg" class="weather-icon" />
<text>智慧生产 · 农务管家</text>
</view>
</view>
</view>
<view class="input-bottom-border">
</view>
<!-- 登录卡片:磨砂玻璃质感 -->
<view class="main-card-container">
<view class="aesthetic-card">
<fui-form ref="form" top="0" :padding="['0', '0']" background="transparent">
<view class="card-header">
<text class="welcome-text">您好,欢迎登录</text>
<view class="green-line"></view>
</view>
<!-- 输入区域 -->
<view class="input-fields">
<view class="field-item">
<text class="field-label">{{ loginMode === 1 ? '手机号' : '账号' }}</text>
<fui-input
height="94rpx"
:padding="['0rpx', '0rpx', '0rpx', '12rpx']"
class="input"
autocomplete="off"
:required="false"
clearable
trim
placeholder="请输入账号"
v-model="model.form.data.username"
name="mobile"
:padding="['24rpx', '0']"
:placeholder="loginMode === 1 ? '请输入手机号' : '请输入账号'"
backgroundColor="transparent"
borderColor="transparent"
maxlength="11"
borderColor="#F0F0F0"
borderBottom
height="110rpx"
size="34"
/>
</view>
<view class="user_phone mt50">
<image class="user_phone_img" src="/static/images/register/pwd.png" />
<view class="user_text_view"><text class="view_text">密码</text></view>
</view>
<view class="input-bottom-border">
<view class="field-item" v-if="loginMode === 1">
<text class="field-label">验证码</text>
<view class="code-inner">
<fui-input
height="94rpx"
:padding="['0rpx', '0rpx', '0rpx', '12rpx']"
class="input"
password
clearable
trim
placeholder="请输入密码"
v-model="model.form.data.password"
v-model="model.form.data.code"
:padding="['24rpx', '0']"
placeholder="请输入验证码"
backgroundColor="transparent"
borderColor="transparent"
borderColor="#F0F0F0"
borderBottom
height="110rpx"
size="34"
autocomplete="off"
/>
<view class="sms-trigger" :class="{ inactive: model.countdown > 0 }" @click="smsCode">
{{ model.countdown > 0 ? `${model.countdown}s后重发` : '获取验证码' }}
</view>
</view>
<!-- <view class="login-input-area">
<view class="user_phone">
<image class="user_phone_img" src="/static/images/register/user.png" />
<view class="user_text_view"><text class="view_text">手机号</text></view>
</view>
<view class="input-bottom-border">
<view class="field-item" v-if="loginMode === 2">
<text class="field-label">密码</text>
<fui-input
height="94rpx"
:padding="['0rpx', '0rpx', '0rpx', '12rpx']"
class="input"
autocomplete="off"
:required="false"
clearable
trim
type="number"
placeholder="请输入手机号"
v-model="model.form.data.username"
name="mobile"
v-model="model.form.data.password"
type="password"
:padding="['24rpx', '0']"
placeholder="请输入密码"
backgroundColor="transparent"
borderColor="transparent"
maxlength="11"
borderColor="#F0F0F0"
borderBottom
height="110rpx"
size="34"
/>
</view>
<view class="user_phone mt50">
<image class="user_phone_img" src="/static/images/register/sms.png" />
<view class="user_text_view"><text class="view_text">验证码</text></view>
</view>
<view class="input-bottom-border">
<fui-input
height="94rpx"
:padding="['0rpx', '0rpx', '0rpx', '12rpx']"
class="input"
type="number"
placeholder="请输入验证码"
v-model="model.form.data.code"
backgroundColor="transparent"
borderColor="transparent"
><fui-button
width="200rpx"
height="64rpx"
:background="model.countdown > 0 ? '#CCCCCC' : '#67c17a'"
:color="model.countdown > 0 ? '#67c17a' : '#fff'"
@click="smsCode"
:size="28"
:disabled="model.countdown > 0"
:text="model.countdown > 0 ? `${model.countdown}秒后重试` : '获取验证码'"
/>
</fui-input>
</view>
</view> -->
<view class="submit_btn_view">
<!-- 登录决策 -->
<view class="action-footer">
<fui-button
height="72rpx"
background="#5DB66F"
size="28rpx"
radius="36rpx"
text="立即登录"
text="开启农务数字化"
background="linear-gradient(90deg, #5DB66F 0%, #4CAF50 100%)"
radius="100rpx"
height="110rpx"
@click="login"
:disabled="model.loading"
:loading="model.loading"
:disabled="model.loading"
size="36"
shadow
/>
<view class="secondary-links">
<view class="reg-btn" @click="goRegister">新用户注册</view>
</view>
</view>
</fui-form>
<!-- </view> -->
<fui-checkbox-group class="checkbox" name="checkbox">
<view class="fui-list__item fiexdText">
<view class="fui-align__center" style="justify-content: center">
<view
class="fui-text privacy-wrap"
style="font-size: 28rpx; text-align: center; align-items: center"
>
<fui-label style="display: inline-flex; align-items: center">
<!-- 隐私合规 -->
<view class="policy-wrapper">
<fui-checkbox-group>
<fui-label class="policy-label">
<fui-checkbox
value="true"
color="#4da25b"
color="#5DB66F"
:scale="0.7"
:checked="model.form.data.read"
@change="(e) => (model.form.data.read = e.checked)"
style="margin-right: 10rpx; width: 32rpx; height: 32rpx; margin-top: 2rpx"
/>
<text style="color: #999; font-size: 28rpx">已阅读并同意</text>
</fui-label>
<fui-text
@tap="Link.to(Link.services, '服务协议')"
size="28rpx"
text="《服务协议》"
color="#4da25b"
/><text style="color: #999; font-size: 28rpx"></text>
<fui-text
@tap="Link.to(Link.privacy, '隐私政策')"
size="28rpx"
text="《隐私政策》"
color="#4da25b"
/>
</view>
</view>
<!-- 安全区 -->
<fui-safe-area />
<view class="policy-text">
同意<text class="link" @click.stop="Link.to(Link.services, '服务协议')">《服务协议》</text>与<text class="link" @click.stop="Link.to(Link.privacy, '隐私政策')">《隐私政策》</text>
</view>
</fui-label>
</fui-checkbox-group>
<view class="confirm-dialog-overlay" v-show="readConfirmShow">
<view class="confirm-dialog-container">
<!-- 标题 -->
<view class="dialog-title">服务协议及隐私保护</view>
<!-- 内容 -->
<view class="dialog-content">
<text class="fui-descr">
<text> 为了更好地保障您的合法权益,请您阅读并同意以下协议 </text>
<fui-text
@tap="Link.to(Link.services, '服务协议')"
size="28rpx"
text="《服务协议》"
color="#4da25b"
/>
<fui-text
@tap="Link.to(Link.privacy, '隐私政策')"
size="28rpx"
text="《隐私政策》"
color="#4da25b"
/>
</text>
</view>
<!-- 按钮组 -->
<view class="dialog-buttons">
<view class="cancel-btn" @click="handleCancel">
<text class="cancel-text">不同意</text>
</view>
<view class="confirm-btn" @click="handleConfirm">
<text class="confirm-text">同意</text>
</view>
</fui-form>
</view>
</view>
<!-- 装饰底部:泥土与天空的呼应 -->
<view class="footer-aesthetic"></view>
<!-- 功能弹窗 -->
<fui-modal
:show="readConfirmShow"
title="服务协议及隐私保护"
:buttons="[{ text: '不同意', plain: true }, { text: '同意', plain: false, color: '#fff' }]"
@click="onReadConfirm"
>
<view class="modal-notice">
点击同意即表示您已阅读并理解
<text class="link-inline" @click.stop="Link.to(Link.services, '服务协议')">《服务协议》</text>
<text class="link-inline" @click.stop="Link.to(Link.privacy, '隐私政策')">《隐私政策》</text>
</view>
</fui-modal>
<SlideVerify v-model:show="showSlide" @success="onSlideSuccess" />
</view>
</template>
<style lang="less" scoped>
@keyframes blink-caret {
0%,
100% {
border-right-color: #333;
}
50% {
border-right-color: transparent;
}
.login-page {
min-height: 100vh;
background-color: #f7faf8;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
.login_warp {
/* 顶部视觉层:农业生产全景 */
.header-visual {
height: 600rpx;
position: relative;
font-size: 24rpx;
height: calc(100vh);
block-size: 100% 100%;
background-color: #fafefc;
padding-top: var(--status-bar-height);
overflow: hidden;
.login_top_bg {
width: 750rpx;
height: 1324rpx;
.header-scene {
position: absolute;
left: 0rpx;
top: 0rpx;
width: 100%;
height: 100%;
top: 0;
left: 0;
filter: brightness(0.8);
}
.login_top_warp {
width: 750rpx;
height: 482rpx;
position: relative;
.login_hello {
.header-overlay {
position: absolute;
left: 50rpx;
top: 226rpx;
color: rgb(51 51 51 / 100%);
.text_hello {
font-size: 32rpx;
font-weight: 400;
letter-spacing: 0rpx;
line-height: 40rpx;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: linear-gradient(180deg, rgba(93, 182, 111, 0.4) 0%, rgba(76, 175, 80, 0.8) 100%);
}
.login_server_name {
margin-top: 32rpx;
.text_server_name {
font-size: 40rpx;
font-weight: 500;
letter-spacing: 0rpx;
line-height: 40rpx;
/* 漂浮装饰:增强数智感 */
.float-icon {
position: absolute;
opacity: 0.6;
filter: drop-shadow(0 4rpx 10rpx rgba(0,0,0,0.2));
&.device-1 {
width: 120rpx;
height: 120rpx;
right: -20rpx;
top: 150rpx;
transform: rotate(15deg);
animation: float-slow 4s ease-in-out infinite;
}
&.device-2 {
width: 100rpx;
height: 100rpx;
left: 40rpx;
bottom: 120rpx;
transform: rotate(-10deg);
animation: float-slow 3s ease-in-out infinite alternate;
}
}
.top-nav-bar {
position: relative;
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 40rpx;
.switch-action {
color: #fff;
font-size: 28rpx;
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.2);
padding: 10rpx 24rpx;
border-radius: 40rpx;
backdrop-filter: blur(4px);
}
}
.login_content {
display: flex;
justify-content: center;
flex-wrap: wrap;
.brand-hero {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 50rpx;
.login-input-area {
width: 650rpx;
// border:1rpx red solid;
.user_phone {
.logo-box {
width: 160rpx;
height: 160rpx;
background: #fff;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 16rpx 32rpx rgba(0,0,0,0.15);
.user_phone_img {
width: 40rpx;
height: 40rpx;
.logo-img {
width: 100rpx;
height: 100rpx;
}
}
.user_text_view {
margin-left: 12rpx;
height: 40rpx;
line-height: 40rpx;
.brand-info {
margin-top: 30rpx;
text-align: center;
color: #fff;
.view_text {
font-size: 30rpx;
font-weight: 500;
letter-spacing: 0rpx;
color: rgb(51 51 51 / 100%);
}
.app-title {
font-size: 48rpx;
font-weight: 800;
letter-spacing: 4rpx;
text-shadow: 0 4rpx 8rpx rgba(0,0,0,0.2);
}
.input-bottom-border {
border-bottom: 2rpx #eee solid;
}
.app-slogan {
margin-top: 12rpx;
font-size: 26rpx;
opacity: 0.9;
display: flex;
align-items: center;
justify-content: center;
.mt50 {
margin-top: 50rpx;
.weather-icon {
width: 40rpx;
height: 40rpx;
margin-right: 12rpx;
}
}
}
}
}
.submit_btn_view {
margin-top: 120rpx;
width: 650rpx;
/* 登录卡片层 */
.main-card-container {
padding: 0 40rpx;
margin-top: -100rpx;
position: relative;
z-index: 20;
}
.fui-descr {
letter-spacing: 1rpx;
padding: 50rpx;
font-size: 24rpx;
color: #b2b2b2;
padding-top: 12rpx;
padding-bottom: 48rpx;
.aesthetic-card {
background: #ffffff;
border-radius: 56rpx;
padding: 80rpx 50rpx 60rpx;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.08);
::v-deep(.fui-text__content) {
text-indent: 0 !important;
.card-header {
margin-bottom: 60rpx;
.welcome-text {
font-size: 40rpx;
font-weight: bold;
color: #333;
}
.green-line {
width: 60rpx;
height: 8rpx;
background: #5DB66F;
border-radius: 4rpx;
margin-top: 16rpx;
}
.form {
position: absolute;
top: 480rpx;
z-index: 10;
}
.checkbox {
position: fixed;
left: 0rpx;
bottom: 32rpx;
width: 100%;
z-index: 10;
.field-item {
margin-bottom: 44rpx;
.field-label {
font-size: 26rpx;
color: #999;
margin-left: 4rpx;
}
.privacy-wrap {
.code-inner {
display: flex;
justify-content: center;
align-items: center;
}
justify-content: space-between;
.input,
.btn__box {
width: 100%;
height: 100rpx;
margin-top: 60rpx;
}
.sms-trigger {
white-space: nowrap;
font-size: 28rpx;
color: #5DB66F;
padding: 16rpx 32rpx;
background: #f1f9f2;
border-radius: 50rpx;
margin-left: 20rpx;
font-weight: 500;
.fiexdText {
width: 100%;
margin-top: 40rpx;
&.inactive {
color: #bbb;
background: #f8f8f8;
}
:deep(.fui-input__border-bottom) {
right: 32rpx !important;
}
.btn-register {
color: cadetblue;
}
}
.confirm-dialog-overlay {
position: fixed;
inset: 0;
background-color: rgb(0 0 0 / 50%);
display: flex;
align-items: center;
justify-content: center;
z-index: 99999;
}
.confirm-dialog-container {
position: relative;
width: 600rpx;
background: linear-gradient(180deg, #e8f5e9 0%, #fff 30%);
border-radius: 32rpx;
padding: 60rpx 48rpx 48rpx;
box-shadow: 0 8rpx 32rpx rgb(0 0 0 / 10%);
}
.action-footer {
margin-top: 80rpx;
.close-btn {
position: absolute;
top: 24rpx;
right: 24rpx;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.close-icon {
font-size: 36rpx;
color: #999;
font-weight: 300;
}
.dialog-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
.secondary-links {
margin-top: 40rpx;
text-align: center;
margin-bottom: 32rpx;
.auto-hint {
font-size: 24rpx;
color: #ccc;
margin-bottom: 24rpx;
}
.dialog-content {
font-size: 28rpx;
.reg-btn {
font-size: 30rpx;
color: #666;
text-align: center;
line-height: 40rpx;
margin-bottom: 48rpx;
font-weight: 500;
display: inline-block;
padding: 10rpx 40rpx;
border: 1rpx solid #eee;
border-radius: 40rpx;
}
}
.dialog-buttons {
display: flex;
gap: 24rpx;
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
.policy-wrapper {
margin-top: 80rpx;
.policy-label {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
align-items: flex-start;
}
.cancel-btn {
background-color: #fff;
border: 2rpx solid #5db66f;
.policy-text {
font-size: 24rpx;
color: #bbb;
margin-left: 12rpx;
line-height: 1.4;
}
.cancel-text {
font-size: 28rpx;
color: #5db66f;
.link {
color: #5DB66F;
font-weight: 500;
}
.confirm-btn {
background: linear-gradient(135deg, #5db66f 0%, #4caf50 100%);
box-shadow: 0 4rpx 12rpx rgb(93 182 111 / 30%);
}
.confirm-text {
.modal-notice {
font-size: 28rpx;
color: #fff;
font-weight: 500;
color: #666;
line-height: 1.8;
.link-inline {
color: #5DB66F;
font-weight: bold;
}
}
.cancel-btn:active {
opacity: 0.8;
.footer-aesthetic {
flex: 1;
background: linear-gradient(180deg, #f7faf8 0%, #edf3ef 100%);
}
.confirm-btn:active {
opacity: 0.9;
transform: scale(0.98);
@keyframes float-slow {
0%, 100% { transform: translateY(0) rotate(15deg); }
50% { transform: translateY(-20rpx) rotate(10deg); }
}
</style>
<script setup lang="ts">
// import { PUSH_CLIENT_KEY } from '/@/enums/cacheEnum'
import * as API from '/@/api/model/userInfo'
import SlideVerify from '@/components/slide-verify/SlideVerify.vue'
onShow(async () => {})
// 页面数据
const defaultText = '湖南省农业服务平台'
const defaultText = '湘农数智农服'
const form = ref()
const showSlide = ref(false)
const slideType = ref<'sms' | 'register'>('register')
const model = reactive({
show: false,
isLogin: false,
loading: false,
text: defaultText,
countdown: 0, // 倒计时秒数
countdownTimer: null, // 倒计时定时器
countdown: 0,
form: {
rules: [
{
name: 'phone',
rule: ['required'],
msg: ['请输入手机号'],
rule: ['required', 'isMobile'],
msg: ['请输入手机号', '请输入正确的手机号'],
},
{
name: 'code',
rule: ['required'],
msg: ['请输入验证码'],
},
{
name: 'password',
rule: ['required', 'minLength:6'],
msg: ['请输入密码', '密码长度不能少于6位'],
},
],
rulesPhone: [
{
name: 'phone',
rule: ['required'],
msg: ['请输入手机号'],
rule: ['required', 'isMobile'],
msg: ['请输入手机号', '请输入正确的手机号'],
},
],
data: {
phone: '',
password: '123@2025',
password: '',
code: '',
read: false,
},
},
})
/**
* 注册
*/
let countdownTimer: any = null
function register() {
if (!model.form.data.read) {
Message.toast('请阅读并同意服务协议及隐私政策')
return
}
form?.value.validator(model.form.data, model.form.rules).then(async (res: { isPassed: boolean }) => {
if (res.isPassed) {
// 注册参数
slideType.value = 'register'
showSlide.value = true
}
})
}
function doRegister() {
const params = {
phone: model.form.data.phone,
password: model.form.data.password,
code: model.form.data.code,
}
// 短信登录
model.loading = true
API.sysRegister(params)
.then(async (body) => {
console.log('body', body)
if (body) {
// 打开登录页
goLogin()
Message.toast(`注册成功, 请登录~`)
} else {
Message.toast(body.message)
return false
setTimeout(() => {
goLogin()
}, 1500)
}
})
.finally(() => {
model.loading = false
})
}
})
}
/**
* 获取验证码
*/
function smsCode() {
form?.value.validator(model.form.data, model.form.rulesPhone).then(async (res: { isPassed: boolean }) => {
if (res.isPassed) {
// 如果已经在倒计时中,不重复发送
if (model.countdown > 0) {
return
if (model.countdown > 0) return
slideType.value = 'sms'
showSlide.value = true
}
})
}
function onSlideSuccess() {
if (slideType.value === 'sms') {
const params = {
mobile: model.form.data.phone,
smsmode: 1,
smsmode: 2, // 2-注册
}
API.sysSms(params)
.then(async (body) => {
.then(async () => {
Message.toast('验证码已发送')
console.log('body', body)
// 开始倒计时
startCountdown()
})
.catch(() => {
// 即使请求失败也显示倒计时,防止重复点击
startCountdown()
Message.toast('验证码发送失败')
})
} else {
doRegister()
}
})
}
/**
* 开始倒计时
*/
function startCountdown() {
if (countdownTimer) clearInterval(countdownTimer)
model.countdown = 60
model.countdownTimer = setInterval(() => {
countdownTimer = setInterval(() => {
model.countdown--
if (model.countdown <= 0) {
clearInterval(model.countdownTimer)
model.countdownTimer = null
clearInterval(countdownTimer)
countdownTimer = null
}
}, 1000)
}
/**
* 跳转到登录页
*/
function goLogin() {
uni.reLaunch({
uni.navigateTo({
url: '/pages/login/login',
})
}
// 添加欢迎登录的文字打字动态效果
let loop = null
let direction = 'right'
const count = ref(defaultText.length)
watch(
() => model.show,
(show) => {
if (show) {
loop && clearInterval(loop)
loop = setInterval(() => {
if (direction === 'right') {
count.value++
if (count.value > defaultText.length + 20) {
direction = 'left'
count.value = defaultText.length
}
} else {
count.value--
if (count.value < 0) {
direction = 'right'
}
function goHome() {
uni.reLaunch({
url: '/pages/shouye/shouye',
})
}
if (count.value > defaultText.length) {
model.text = defaultText
} else if (count.value < 0) {
model.text = ''
} else {
model.text = defaultText.slice(0, count.value)
}
}, 200)
onHide(() => {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
},
)
})
onHide(() => {
loop && clearInterval(loop)
loop = null
direction = 'right'
count.value = 0
model.show = false
onUnload(() => {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
})
</script>
<template>
<view class="warp">
<!-- <image class="login-warp" src="/static/login/login_bg.png" /> -->
<view class="register-form">
<view class="register-bg-wrap">
<!-- <image class="register-bg" src="/static/images/register/register.png" /> -->
<view class="logo-content-wrap">
<view class="logo-text-1">你好,欢迎使用</view>
<view class="logo-text">数字农业服务平台</view>
<view class="login-page">
<!-- 顶部视觉层:阳光农场与气象设备 -->
<view class="header-visual">
<image class="header-scene" src="/static/images/login/farm_base_bg.png" mode="aspectFill" />
<view class="header-overlay"></view>
<!-- 气象站与农业设备:体现数智感 -->
<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" />
<!-- 顶部导航 -->
<view class="top-nav-bar">
<view class="back-btn" @click="goLogin">
<fui-icon name="arrowleft" :size="48" color="#fff" />
</view>
</view>
<fui-form class="form" ref="form" top="50" :padding="['0rpx', '32rpx']" background="#e46962">
<view class="reigister-form-item">
<image class="reigister-form-image" src="/static/images/register/user.png" />
<text>手机号</text>
<!-- 品牌标识 -->
<view class="brand-hero">
<view class="logo-box">
<image class="logo-img" src="/static/logo.png" mode="aspectFit" />
</view>
<view class="brand-info">
<text class="app-title">加入数智农服</text>
<view class="app-slogan">
<image src="/static/images/weather/100.svg" class="weather-icon" />
<text>开启您的智慧农业之旅</text>
</view>
</view>
</view>
</view>
<!-- 注册卡片 -->
<view class="main-card-container">
<view class="aesthetic-card">
<fui-form ref="form" top="0" :padding="['0', '0']" background="transparent">
<view class="card-header">
<text class="welcome-text">创建新账号</text>
<view class="green-line"></view>
</view>
<view class="input-fields">
<view class="field-item">
<text class="field-label">手机号</text>
<fui-input
height="100rpx"
class="input"
autocomplete="off"
:required="false"
clearable
trim
placeholder="请输入手机号/账号"
v-model="model.form.data.phone"
name="mobile"
:padding="['24rpx', '0']"
placeholder="请输入手机号"
backgroundColor="transparent"
borderColor="#DDDDDD"
borderColor="#F0F0F0"
borderBottom
height="110rpx"
size="34"
maxlength="11"
/>
<view class="reigister-form-item !hidden">
<image class="reigister-form-image" src="/static/images/register/pwd.png" />
<text>密码</text>
</view>
<view class="field-item">
<text class="field-label">验证码</text>
<view class="code-inner">
<fui-input
height="100rpx"
class="input !hidden"
password
autocomplete="new-password"
code
:required="false"
clearable
trim
placeholder="请输入密码"
v-model="model.form.data.password"
name="code"
marginTop="10"
v-model="model.form.data.code"
:padding="['24rpx', '0']"
placeholder="请输入验证码"
backgroundColor="transparent"
borderColor="#DDDDDD"
borderColor="#F0F0F0"
borderBottom
height="110rpx"
size="34"
autocomplete="off"
/>
<view class="reigister-form-item">
<image class="reigister-form-image" src="/static/images/register/sms.png" />
<text>验证码</text>
<view class="sms-trigger" :class="{ inactive: model.countdown > 0 }" @click="smsCode">
{{ model.countdown > 0 ? `${model.countdown}s后重发` : '获取验证码' }}
</view>
</view>
</view>
<view class="field-item">
<text class="field-label">设置密码</text>
<fui-input
:padding="['20rpx', '32rpx']"
placeholder="请输入验证码"
:bottomLeft="0"
marginTop="10"
v-model="model.form.data.code"
v-model="model.form.data.password"
type="password"
:padding="['24rpx', '0']"
placeholder="请设置6位以上登录密码"
backgroundColor="transparent"
borderColor="#DDDDDD"
>
<fui-button
width="200rpx"
height="64rpx"
:background="model.countdown > 0 ? '#CCCCCC' : '#67c17a'"
:color="model.countdown > 0 ? '#67c17a' : '#fff'"
@click="smsCode"
:size="28"
:disabled="model.countdown > 0"
:text="model.countdown > 0 ? `${model.countdown}秒后重试` : '获取验证码'"
borderColor="#F0F0F0"
borderBottom
height="110rpx"
size="34"
autocomplete="new-password"
/>
</fui-input>
<view class="btn__box flex-center p-32rpx box-border">
</view>
</view>
<view class="action-footer">
<fui-button
height="88rpx"
background="#67c17a"
size="32rpx"
radius="8rpx"
text="立即注册"
background="linear-gradient(90deg, #5DB66F 0%, #4CAF50 100%)"
radius="100rpx"
height="110rpx"
@click="register"
:disabled="model.loading"
:loading="model.loading"
:disabled="model.loading"
size="36"
shadow
/>
<view class="secondary-links">
<view class="reg-btn" @click="goLogin">已有账号?去登录</view>
</view>
</view>
<!-- 隐私合规:对齐优化 -->
<view class="policy-wrapper">
<fui-checkbox-group>
<fui-label class="policy-label">
<view class="checkbox-align">
<fui-checkbox
color="#5DB66F"
:scale="0.7"
:checked="model.form.data.read"
@change="(e) => (model.form.data.read = e.checked)"
/>
</view>
<view class="policy-text">
同意<text class="link" @click.stop="Link.to(Link.services, '服务协议')">《服务协议》</text>与<text class="link" @click.stop="Link.to(Link.privacy, '隐私政策')">《隐私政策》</text>
</view>
</fui-label>
</fui-checkbox-group>
</view>
<view class="flex-center p-32rpx box-border btn-register" @click="goLogin"> 已有账号,立即登录 </view>
</fui-form>
</view>
</view>
<view class="footer-aesthetic"></view>
<SlideVerify v-model:show="showSlide" @success="onSlideSuccess" />
</view>
</template>
<style lang="less" scoped>
// .login-warp {
// width: 100%;
// height: calc(100vh);
// position: absolute;
// // border: 1px solid #000;
// z-index: 0;
// }
.login-page {
min-height: 100vh;
background-color: #f7faf8;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
@keyframes blink-caret {
0%,
100% {
border-right-color: #333;
.header-visual {
height: 550rpx;
position: relative;
padding-top: var(--status-bar-height);
overflow: hidden;
.header-scene {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
filter: brightness(0.9);
}
50% {
border-right-color: transparent;
.header-overlay {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: linear-gradient(180deg, rgba(93, 182, 111, 0.3) 0%, rgba(76, 175, 80, 0.7) 100%);
}
.float-icon {
position: absolute;
opacity: 0.7;
filter: drop-shadow(0 4rpx 10rpx rgba(0,0,0,0.15));
&.device-1 {
width: 130rpx;
height: 130rpx;
right: 20rpx;
top: 180rpx;
transform: rotate(10deg);
animation: float-slow 4s ease-in-out infinite;
}
&.device-2 {
width: 110rpx;
height: 110rpx;
left: 30rpx;
bottom: 100rpx;
animation: float-slow 3s ease-in-out infinite alternate;
}
}
.warp {
.top-nav-bar {
position: relative;
font-size: 24rpx;
height: calc(100vh);
block-size: 100% 100%;
// background-size: 100% 100%;
// border: 1px solid #000;
background-color: #fff;
z-index: 10;
padding: 20rpx 40rpx;
}
.fui-descr {
letter-spacing: 1rpx;
padding: 50rpx;
font-size: 24rpx;
color: #b2b2b2;
padding-top: 12rpx;
padding-bottom: 48rpx;
.brand-hero {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20rpx;
.logo-box {
width: 140rpx;
height: 140rpx;
background: #fff;
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12rpx 24rpx rgba(0,0,0,0.1);
::v-deep(.fui-text__content) {
text-indent: 0 !important;
.logo-img {
width: 90rpx;
height: 90rpx;
}
}
.register-bg-wrap {
position: absolute;
width: 100%;
height: 30vh;
// border: 1px solid #000;
.brand-info {
margin-top: 24rpx;
text-align: center;
color: #fff;
.app-title {
font-size: 44rpx;
font-weight: 800;
letter-spacing: 2rpx;
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.1);
}
.app-slogan {
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.95;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-image: url('/static/images/register/register.png');
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
justify-content: center;
.logo-content-wrap {
display: flex;
flex-direction: column;
justify-content: left;
align-items: flex-start;
width: 86%;
.weather-icon {
width: 36rpx;
height: 36rpx;
margin-right: 10rpx;
}
}
}
}
}
.logo-text-1 {
font-size: 30rpx;
font-weight: 400;
letter-spacing: 0;
margin-top: 4.25rem;
color: rgb(51 51 51 / 70%);
vertical-align: middle;
.main-card-container {
padding: 0 40rpx;
margin-top: -80rpx;
position: relative;
z-index: 20;
}
.logo-text {
font-size: 40rpx;
font-weight: 500;
letter-spacing: 0;
margin-top: 40rpx;
color: rgb(51 51 51 / 100%);
vertical-align: middle;
.aesthetic-card {
background: #ffffff;
border-radius: 50rpx;
padding: 70rpx 50rpx 60rpx;
box-shadow: 0 15rpx 50rpx rgba(0, 0, 0, 0.06);
.card-header {
margin-bottom: 50rpx;
.welcome-text {
font-size: 38rpx;
font-weight: bold;
color: #333;
}
.green-line {
width: 50rpx;
height: 7rpx;
background: #5DB66F;
border-radius: 4rpx;
margin-top: 14rpx;
}
}
.register-form {
position: absolute;
width: 100%;
height: 60vh;
left: 0;
top: 0;
opacity: 1;
border-radius: 14.03px 14.03px 0 0;
border: 1px solid #fff;
background: linear-gradient(
180deg,
rgb(181 238 215 / 100%) 0%,
rgb(181 238 215 / 50%) 30%,
rgb(255 255 255 / 80%) 100%
);
}
.reigister-form-item {
color: #000;
display: flex;
flex-direction: row;
justify-content: left;
align-items: center;
margin: 10rpx;
.field-item {
margin-bottom: 40rpx;
.reigister-form-image {
width: 60rpx;
height: 60rpx;
.field-label {
font-size: 26rpx;
color: #999;
margin-left: 4rpx;
}
text {
font-size: 32rpx;
.code-inner {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.sms-trigger {
white-space: nowrap;
font-size: 27rpx;
color: #5DB66F;
padding: 14rpx 28rpx;
background: #f1f9f2;
border-radius: 50rpx;
margin-left: 20rpx;
font-weight: 500;
&.inactive {
color: #bbb;
background: #f8f8f8;
}
}
}
}
.form {
width: 100%;
z-index: 10;
margin-top: 540rpx;
}
.checkbox {
position: fixed;
left: 0rpx;
bottom: 32rpx;
width: 100%;
z-index: 10;
.action-footer {
margin-top: 70rpx;
.secondary-links {
margin-top: 40rpx;
text-align: center;
.reg-btn {
font-size: 28rpx;
color: #888;
padding: 10rpx 40rpx;
border: 1rpx solid #f0f0f0;
border-radius: 40rpx;
}
}
}
.policy-wrapper {
margin-top: 60rpx;
.privacy-wrap {
.policy-label {
display: flex;
align-items: center; /* 垂直居中 */
justify-content: center;
}
.checkbox-align {
line-height: 1;
display: flex;
align-items: center;
}
.input,
.btn__box {
width: 100%;
height: 100rpx;
margin-top: 60rpx;
.policy-text {
font-size: 24rpx;
color: #bbb;
margin-left: 12rpx;
display: flex;
align-items: center;
}
.fiexdText {
// position: absolute;
width: 100%;
margin-top: 40rpx;
.link {
color: #5DB66F;
font-weight: 500;
}
}
:deep(.fui-input__border-bottom) {
right: 32rpx !important;
.footer-aesthetic {
flex: 1;
background: linear-gradient(180deg, #f7faf8 0%, #edf3ef 100%);
}
.btn-register {
color: cadetblue;
@keyframes float-slow {
0%, 100% { transform: translateY(0) rotate(5deg); }
50% { transform: translateY(-15rpx) rotate(-5deg); }
}
</style>
......@@ -315,7 +315,7 @@
}
.page-bg {
background: url('/static/images/codefun/7a5dc4ee864fe55da98b41c14ee3b931.png') no-repeat top center;
background: url('../../static/images/codefun/7a5dc4ee864fe55da98b41c14ee3b931.png') no-repeat top center;
background-size: 100%;
}
......
......@@ -1470,7 +1470,7 @@
.image-wrapper {
padding-bottom: 104rpx;
background-image: url('/static/images/codefun/e18202eb8182b8d77c464523c2305fa3.png');
background-image: url('../../static/images/codefun/e18202eb8182b8d77c464523c2305fa3.png');
background-size: 100% 100%;
background-repeat: no-repeat;
......@@ -1912,7 +1912,7 @@
}
.nongchang_box {
background-image: url('/static/images/nongchang/mynongchang-2.png');
background-image: url('../../static/images/nongchang/mynongchang-2.png');
background-repeat: no-repeat;
background-size: 100% 100%;
}
......
......@@ -83,36 +83,239 @@
</script>
<template>
<fui-dialog title="" :buttons="[]" :show="pageData.show" maskClosable @close="close">
<view class="dialog-header">
<text class="dialog-title">预约登记</text>
<fui-icon name="close" :size="40" color="#999" @click="close"></fui-icon>
<fui-bottom-popup :show="pageData.show" @close="close">
<view class="popup-wrap">
<!-- 弹窗头部 -->
<view class="popup-header">
<view class="header-left">
<view class="header-icon">
<fui-icon name="notice" :size="32" color="#fff"></fui-icon>
</view>
<text class="header-title">预约登记</text>
</view>
<view class="close-btn" @click="close">
<fui-icon name="close" :size="36" color="#999"></fui-icon>
</view>
</view>
<!-- 表单区域 -->
<view class="form-section">
<!-- 联系电话 -->
<view class="form-item">
<view class="form-label">
<text class="label-text">联系电话</text>
<text class="required">*</text>
</view>
<input class="form-input" type="number" maxlength="11" placeholder="请输入手机号" v-model="pageData.form.phone" placeholder-class="input-placeholder" />
</view>
<!-- 作业区域 -->
<view class="form-item" @click="pageData.areaShow.address = true">
<view class="form-label">
<text class="label-text">作业区域</text>
<text class="required">*</text>
</view>
<view class="form-picker">
<text :class="['picker-text', pageData.scopeText ? '' : 'placeholder']">
{{ pageData.scopeText || '请选择作业区域' }}
</text>
<fui-icon name="arrowright" :size="28" color="#ccc"></fui-icon>
</view>
</view>
<!-- 详细地址 -->
<view class="form-item">
<view class="form-label">
<text class="label-text">详细地址</text>
<text class="required">*</text>
</view>
<input class="form-input" placeholder="村组/街道门牌号" v-model="pageData.form.address" placeholder-class="input-placeholder" />
</view>
<view class="dialog-body">
<fui-form ref="formRef">
<fui-input label="联系电话" placeholder="请输入手机号" v-model="pageData.form.phone" required type="number" maxlength="11" />
<fui-input label="作业区域" placeholder="请选择" v-model="pageData.scopeText" @click="pageData.areaShow.address = true" disabled required />
<fui-input label="详细地址" placeholder="村组/街道门牌" v-model="pageData.form.address" required />
<fui-input label="作业时间" placeholder="请选择周期" v-model="pageData.form.time" @click="pageData.areaShow.time = true" disabled required />
<fui-textarea label="需求说明" placeholder="简要说明作业要求..." v-model="pageData.form.demand" required height="120rpx" />
<view class="submit-btn-wrap">
<fui-button text="立即预约" radius="100rpx" background="#5db66f" @click="submit" />
<!-- 作业时间 -->
<view class="form-item" @click="pageData.areaShow.time = true">
<view class="form-label">
<text class="label-text">作业时间</text>
<text class="required">*</text>
</view>
<view class="form-picker">
<text :class="['picker-text', pageData.form.time ? '' : 'placeholder']">
{{ pageData.form.time || '请选择作业周期' }}
</text>
<fui-icon name="arrowright" :size="28" color="#ccc"></fui-icon>
</view>
</fui-form>
</view>
</fui-dialog>
<!-- 需求说明 -->
<view class="form-item">
<view class="form-label">
<text class="label-text">需求说明</text>
<text class="required">*</text>
</view>
<textarea class="form-textarea" placeholder="请简要说明作业要求..." v-model="pageData.form.demand" placeholder-class="input-placeholder" />
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-area">
<view class="submit-btn" @click="submit">
<text class="btn-text">立即预约</text>
</view>
</view>
</view>
</fui-bottom-popup>
<fui-date-picker :show="pageData.areaShow.time" type="3" range @change="handleTimeChange" @cancel="pageData.areaShow.time = false" />
<AreaPicker v-model:show="pageData.areaShow.address" :layer="3" title="选择作业区域" @confirm="handleAreaConfirm" />
</template>
<style lang="scss" scoped>
.dialog-header {
display: flex; justify-content: space-between; align-items: center; padding: 20rpx 0;
.dialog-title { font-size: 32rpx; font-weight: bold; color: #333; }
.popup-wrap {
background-color: #fff;
border-radius: 32rpx 32rpx 0 0;
overflow: hidden;
padding-bottom: env(safe-area-inset-bottom);
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 32rpx 24rpx;
background: linear-gradient(135deg, #f8fdf9 0%, #fff 100%);
border-bottom: 1rpx solid #f0f0f0;
.header-left {
display: flex;
align-items: center;
}
.header-icon {
width: 56rpx;
height: 56rpx;
background: linear-gradient(135deg, #fa8c16 0%, #fa541c 100%);
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(250, 140, 22, 0.3);
}
.header-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.close-btn {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
border-radius: 50%;
}
}
.form-section {
padding: 24rpx 32rpx 0;
.form-item {
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
}
.form-label {
display: flex;
align-items: center;
margin-bottom: 14rpx;
.label-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.required {
color: #ff4d4f;
margin-left: 4rpx;
font-size: 28rpx;
}
}
.form-input {
width: 100%;
height: 80rpx;
background-color: #f7f8fa;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
}
.form-picker {
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
background-color: #f7f8fa;
border-radius: 12rpx;
padding: 0 24rpx;
.picker-text {
font-size: 28rpx;
color: #333;
&.placeholder {
color: #bbb;
}
}
}
.form-textarea {
width: 100%;
height: 160rpx;
background-color: #f7f8fa;
border-radius: 12rpx;
padding: 20rpx 24rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
line-height: 1.5;
}
}
.submit-area {
padding: 32rpx 32rpx 40rpx;
.submit-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #fa8c16 0%, #fa541c 100%);
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(250, 140, 22, 0.4);
.btn-text {
font-size: 30rpx;
color: #fff;
font-weight: 600;
letter-spacing: 4rpx;
}
}
}
}
.input-placeholder {
color: #bbb;
font-size: 28rpx;
}
.dialog-body { text-align: left; padding-bottom: 20rpx; }
.submit-btn-wrap { margin-top: 40rpx; }
:deep(.fui-input__label) { font-size: 28rpx; width: 160rpx !important; }
</style>
......@@ -254,7 +254,7 @@
overflow-x: hidden;
.group {
.section_2 {
background-image: url('/static/images/codefun/c122979639a1f9d27d7c57245ab420a6.png');
background-image: url('../../static/images/codefun/c122979639a1f9d27d7c57245ab420a6.png');
background-size: 100% 100%;
background-repeat: no-repeat;
.section_3 {
......@@ -601,7 +601,7 @@
backdrop-filter: blur(4rpx);
.image-wrapper {
padding: 48rpx 0;
background-image: url('/static/images/codefun/5149f97303e2fa97daa823a1452dce11.png');
background-image: url('../../static/images/codefun/5149f97303e2fa97daa823a1452dce11.png');
background-size: 100% 100%;
background-repeat: no-repeat;
width: 240rpx;
......
......@@ -506,7 +506,7 @@ e:\Downloads\农场 (1).png
.section {
padding: 24rpx 28rpx;
background-image: url('/static/images/codefun/7a5dc4ee864fe55da98b41c14ee3b931.png');
background-image: url('../../static/images/codefun/7a5dc4ee864fe55da98b41c14ee3b931.png');
background-size: 100% 100%;
background-repeat: no-repeat;
.group {
......
......@@ -911,7 +911,7 @@ export default {
.section {
padding: 10rpx 28rpx 220rpx;
padding-top: calc(10rpx + var(--status-bar-height));
background-image: url('/static/images/codefun/1086a098c06f7f52e77bd7a646747a13.png');
background-image: url('../../static/images/codefun/1086a098c06f7f52e77bd7a646747a13.png');
background-size: 100% 100%;
background-repeat: no-repeat;
......
......@@ -451,7 +451,7 @@ showAddDialog()
.group {
.section {
padding: 24rpx 20rpx 88rpx;
background-image: url('/static/images/codefun/7a5dc4ee864fe55da98b41c14ee3b931.png');
background-image: url('../../static/images/codefun/7a5dc4ee864fe55da98b41c14ee3b931.png');
background-size: 100% 100%;
background-repeat: no-repeat;
......
......@@ -82,6 +82,42 @@ export const useUserStore = defineStore({
Storage.set(USER_INFO_KEY, info ? JSON.stringify(info) : null)
}
},
async login(params: { username: string; password: string }) {
if (this.loading) {
return
}
this.loading = true
try {
const body = await API.sysLogin(params)
if (body?.token) {
this.setToken(body.token)
}
const userInfo = await API.getUserInfo()
this.setUserInfo(userInfo)
return body
} finally {
this.loading = false
}
},
async phoneLogin(params: { mobile: string; captcha: string }) {
if (this.loading) {
return
}
this.loading = true
try {
const body = await API.phoneLogin(params)
if (body?.token) {
this.setToken(body.token)
}
const userInfo = await API.getUserInfo()
this.setUserInfo(userInfo)
return body
} finally {
this.loading = false
}
},
async logout() {
if (this.loading) {
return
......
......@@ -153,6 +153,7 @@ declare module 'vue' {
Mapbox: typeof import('./../src/components/Map/Mapbox/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SlideVerify: typeof import('./../src/components/slide-verify/SlideVerify.vue')['default']
Src: typeof import('./../src/components/Echarts/src/index.vue')['default']
SuccessfulDialog: typeof import('./../src/components/ConfirmDialog/successfulDialog.vue')['default']
Switch: typeof import('./../src/components/Map/Widgets/Switch/src/Switch.vue')['default']
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论