提交 4a6f63f4 作者: 廖在望

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

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