提交 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() {
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,
} }
// 短信登录
model.loading = true
// API.phoneLogin(params)
API.sysLogin(params)
.then(async (body) => {
console.log('body', body)
if (body) {
// 设置登录缓存信息
userStore.setToken(body.token)
// 登录成功后查询用户信息 function login() {
const user = await API.getUserInfo() const rules = loginMode.value === 1 ? model.form.codeLoginRules : model.form.pwdLoginRules
userStore.setUserInfo(user) form?.value.validator(model.form.data, rules).then(async (res: { isPassed: boolean }) => {
if (res.isPassed) {
// 打开登录页 // 登录前先做滑动验证 (Requirement 7)
goHome() slideType.value = 'login'
Message.toast(`登录成功, 欢迎回来~`) showSlide.value = true
} 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
}
})
} }
function onSlideSuccess() {
if (slideType.value === 'sms') {
// 发送验证码
const params = { const params = {
/* mobile: model.form.data.username,
smsmode: '0', */
mobile: model.form.data.username, mobile: model.form.data.username,
smsmode: 1, smsmode: '1',
} }
API.sysSms(params) API.sysSms(params)
.then(async (body) => { .then(async () => {
Message.toast('验证码已发送') Message.toast('验证码已发送')
console.log('body', body)
// 开始倒计时
startCountdown() startCountdown()
}) })
.catch(() => { .catch(() => {
// 即使请求失败也显示倒计时,防止重复点击 Message.toast('验证码发送失败')
startCountdown()
}) })
} 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() { 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) {
if (val.index === 0) {
model.form.data.read = false 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>
<view class="user_text_view"><text class="view_text">账号</text></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="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 <fui-input
height="94rpx"
:padding="['0rpx', '0rpx', '0rpx', '12rpx']"
class="input"
autocomplete="off"
:required="false"
clearable
trim
placeholder="请输入账号"
v-model="model.form.data.username" v-model="model.form.data.username"
name="mobile" :padding="['24rpx', '0']"
:placeholder="loginMode === 1 ? '请输入手机号' : '请输入账号'"
backgroundColor="transparent" backgroundColor="transparent"
borderColor="transparent" borderColor="#F0F0F0"
maxlength="11" borderBottom
height="110rpx"
size="34"
/> />
</view> </view>
<view class="user_phone mt50">
<image class="user_phone_img" src="/static/images/register/pwd.png" /> <view class="field-item" v-if="loginMode === 1">
<view class="user_text_view"><text class="view_text">密码</text></view> <text class="field-label">验证码</text>
</view> <view class="code-inner">
<view class="input-bottom-border">
<fui-input <fui-input
height="94rpx" v-model="model.form.data.code"
:padding="['0rpx', '0rpx', '0rpx', '12rpx']" :padding="['24rpx', '0']"
class="input" placeholder="请输入验证码"
password
clearable
trim
placeholder="请输入密码"
v-model="model.form.data.password"
backgroundColor="transparent" 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> </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>
<view class="input-bottom-border">
<view class="field-item" v-if="loginMode === 2">
<text class="field-label">密码</text>
<fui-input <fui-input
height="94rpx" v-model="model.form.data.password"
:padding="['0rpx', '0rpx', '0rpx', '12rpx']" type="password"
class="input" :padding="['24rpx', '0']"
autocomplete="off" placeholder="请输入密码"
:required="false"
clearable
trim
type="number"
placeholder="请输入手机号"
v-model="model.form.data.username"
name="mobile"
backgroundColor="transparent" backgroundColor="transparent"
borderColor="transparent" borderColor="#F0F0F0"
maxlength="11" borderBottom
height="110rpx"
size="34"
/> />
</view> </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> -->
<view class="submit_btn_view"> <!-- 登录决策 -->
<view class="action-footer">
<fui-button <fui-button
height="72rpx" text="开启农务数字化"
background="#5DB66F" background="linear-gradient(90deg, #5DB66F 0%, #4CAF50 100%)"
size="28rpx" radius="100rpx"
radius="36rpx" height="110rpx"
text="立即登录"
@click="login" @click="login"
:disabled="model.loading"
:loading="model.loading" :loading="model.loading"
:disabled="model.loading"
size="36"
shadow
/> />
<view class="secondary-links">
<view class="reg-btn" @click="goRegister">新用户注册</view>
</view> </view>
</view> </view>
</fui-form>
<!-- </view> --> <!-- 隐私合规 -->
<fui-checkbox-group class="checkbox" name="checkbox"> <view class="policy-wrapper">
<view class="fui-list__item fiexdText"> <fui-checkbox-group>
<view class="fui-align__center" style="justify-content: center"> <fui-label class="policy-label">
<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">
<fui-checkbox <fui-checkbox
value="true" color="#5DB66F"
color="#4da25b" :scale="0.7"
:checked="model.form.data.read" :checked="model.form.data.read"
@change="(e) => (model.form.data.read = e.checked)" @change="(e) => (model.form.data.read = e.checked)"
style="margin-right: 10rpx; width: 32rpx; height: 32rpx; margin-top: 2rpx"
/> />
<text style="color: #999; font-size: 28rpx">已阅读并同意</text> <view class="policy-text">
</fui-label> 同意<text class="link" @click.stop="Link.to(Link.services, '服务协议')">《服务协议》</text>与<text class="link" @click.stop="Link.to(Link.privacy, '隐私政策')">《隐私政策》</text>
<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> </view>
</fui-label>
</fui-checkbox-group> </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> </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> </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;
50% {
border-right-color: transparent;
}
} }
.login_warp { /* 顶部视觉层:农业生产全景 */
.header-visual {
height: 600rpx;
position: relative; position: relative;
font-size: 24rpx; padding-top: var(--status-bar-height);
height: calc(100vh); overflow: hidden;
block-size: 100% 100%;
background-color: #fafefc;
.login_top_bg { .header-scene {
width: 750rpx;
height: 1324rpx;
position: absolute; position: absolute;
left: 0rpx; width: 100%;
top: 0rpx; height: 100%;
top: 0;
left: 0;
filter: brightness(0.8);
} }
.login_top_warp { .header-overlay {
width: 750rpx;
height: 482rpx;
position: relative;
.login_hello {
position: absolute; position: absolute;
left: 50rpx; width: 100%;
top: 226rpx; height: 100%;
color: rgb(51 51 51 / 100%); top: 0;
left: 0;
.text_hello { background: linear-gradient(180deg, rgba(93, 182, 111, 0.4) 0%, rgba(76, 175, 80, 0.8) 100%);
font-size: 32rpx;
font-weight: 400;
letter-spacing: 0rpx;
line-height: 40rpx;
} }
.login_server_name { /* 漂浮装饰:增强数智感 */
margin-top: 32rpx; .float-icon {
position: absolute;
.text_server_name { opacity: 0.6;
font-size: 40rpx; filter: drop-shadow(0 4rpx 10rpx rgba(0,0,0,0.2));
font-weight: 500;
letter-spacing: 0rpx; &.device-1 {
line-height: 40rpx; 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 { .brand-hero {
display: flex;
justify-content: center;
flex-wrap: wrap;
position: relative; position: relative;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 50rpx;
.login-input-area { .logo-box {
width: 650rpx; width: 160rpx;
// border:1rpx red solid; height: 160rpx;
.user_phone { background: #fff;
border-radius: 44rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
box-shadow: 0 16rpx 32rpx rgba(0,0,0,0.15);
.user_phone_img { .logo-img {
width: 40rpx; width: 100rpx;
height: 40rpx; height: 100rpx;
} }
} }
.user_text_view { .brand-info {
margin-left: 12rpx; margin-top: 30rpx;
height: 40rpx; text-align: center;
line-height: 40rpx; color: #fff;
.view_text { .app-title {
font-size: 30rpx; font-size: 48rpx;
font-weight: 500; font-weight: 800;
letter-spacing: 0rpx; letter-spacing: 4rpx;
color: rgb(51 51 51 / 100%); text-shadow: 0 4rpx 8rpx rgba(0,0,0,0.2);
}
} }
.input-bottom-border { .app-slogan {
border-bottom: 2rpx #eee solid; margin-top: 12rpx;
} font-size: 26rpx;
opacity: 0.9;
display: flex;
align-items: center;
justify-content: center;
.mt50 { .weather-icon {
margin-top: 50rpx; width: 40rpx;
height: 40rpx;
margin-right: 12rpx;
}
}
} }
} }
} }
.submit_btn_view { /* 登录卡片层 */
margin-top: 120rpx; .main-card-container {
width: 650rpx; padding: 0 40rpx;
margin-top: -100rpx;
position: relative;
z-index: 20;
} }
.fui-descr { .aesthetic-card {
letter-spacing: 1rpx; background: #ffffff;
padding: 50rpx; border-radius: 56rpx;
font-size: 24rpx; padding: 80rpx 50rpx 60rpx;
color: #b2b2b2; box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.08);
padding-top: 12rpx;
padding-bottom: 48rpx;
::v-deep(.fui-text__content) { .card-header {
text-indent: 0 !important; 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 { .field-item {
position: fixed; margin-bottom: 44rpx;
left: 0rpx;
bottom: 32rpx; .field-label {
width: 100%; font-size: 26rpx;
z-index: 10; color: #999;
margin-left: 4rpx;
} }
.privacy-wrap { .code-inner {
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
} justify-content: space-between;
.input, .sms-trigger {
.btn__box { white-space: nowrap;
width: 100%; font-size: 28rpx;
height: 100rpx; color: #5DB66F;
margin-top: 60rpx; padding: 16rpx 32rpx;
} background: #f1f9f2;
border-radius: 50rpx;
margin-left: 20rpx;
font-weight: 500;
.fiexdText { &.inactive {
width: 100%; color: #bbb;
margin-top: 40rpx; 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 { .action-footer {
position: relative; margin-top: 80rpx;
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 { .secondary-links {
position: absolute; margin-top: 40rpx;
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;
text-align: center; text-align: center;
margin-bottom: 32rpx;
.auto-hint {
font-size: 24rpx;
color: #ccc;
margin-bottom: 24rpx;
} }
.dialog-content { .reg-btn {
font-size: 28rpx; font-size: 30rpx;
color: #666; color: #666;
text-align: center; font-weight: 500;
line-height: 40rpx; display: inline-block;
margin-bottom: 48rpx; padding: 10rpx 40rpx;
border: 1rpx solid #eee;
border-radius: 40rpx;
}
} }
.dialog-buttons {
display: flex;
gap: 24rpx;
} }
.cancel-btn, .policy-wrapper {
.confirm-btn { margin-top: 80rpx;
flex: 1;
height: 80rpx; .policy-label {
border-radius: 40rpx;
display: flex; display: flex;
align-items: center; align-items: flex-start;
justify-content: center;
cursor: pointer;
} }
.cancel-btn { .policy-text {
background-color: #fff; font-size: 24rpx;
border: 2rpx solid #5db66f; color: #bbb;
margin-left: 12rpx;
line-height: 1.4;
} }
.cancel-text { .link {
font-size: 28rpx; color: #5DB66F;
color: #5db66f;
font-weight: 500; 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; font-size: 28rpx;
color: #fff; color: #666;
font-weight: 500; line-height: 1.8;
.link-inline {
color: #5DB66F;
font-weight: bold;
}
} }
.cancel-btn:active { .footer-aesthetic {
opacity: 0.8; flex: 1;
background: linear-gradient(180deg, #f7faf8 0%, #edf3ef 100%);
} }
.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'
showSlide.value = true
}
})
}
function doRegister() {
const params = { const params = {
phone: model.form.data.phone, phone: model.form.data.phone,
password: model.form.data.password, password: model.form.data.password,
code: model.form.data.code, code: model.form.data.code,
} }
// 短信登录
model.loading = true model.loading = true
API.sysRegister(params) API.sysRegister(params)
.then(async (body) => { .then(async (body) => {
console.log('body', body)
if (body) { if (body) {
// 打开登录页
goLogin()
Message.toast(`注册成功, 请登录~`) Message.toast(`注册成功, 请登录~`)
} else { setTimeout(() => {
Message.toast(body.message) goLogin()
return false }, 1500)
} }
}) })
.finally(() => { .finally(() => {
model.loading = false 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
}
})
} }
function onSlideSuccess() {
if (slideType.value === 'sms') {
const params = { const params = {
mobile: model.form.data.phone, mobile: model.form.data.phone,
smsmode: 1, smsmode: 2, // 2-注册
} }
API.sysSms(params) API.sysSms(params)
.then(async (body) => { .then(async () => {
Message.toast('验证码已发送') Message.toast('验证码已发送')
console.log('body', body)
// 开始倒计时
startCountdown() startCountdown()
}) })
.catch(() => { .catch(() => {
// 即使请求失败也显示倒计时,防止重复点击 Message.toast('验证码发送失败')
startCountdown()
}) })
} 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) { onHide(() => {
model.text = defaultText if (countdownTimer) {
} else if (count.value < 0) { clearInterval(countdownTimer)
model.text = '' countdownTimer = null
} else {
model.text = defaultText.slice(0, count.value)
}
}, 200)
} }
}, })
)
onHide(() => { onUnload(() => {
loop && clearInterval(loop) if (countdownTimer) {
loop = null clearInterval(countdownTimer)
direction = 'right' countdownTimer = null
count.value = 0 }
model.show = false
}) })
</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">
<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>
<!-- 注册卡片 -->
<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 <fui-input
height="100rpx"
class="input"
autocomplete="off"
:required="false"
clearable
trim
placeholder="请输入手机号/账号"
v-model="model.form.data.phone" v-model="model.form.data.phone"
name="mobile" :padding="['24rpx', '0']"
placeholder="请输入手机号"
backgroundColor="transparent" backgroundColor="transparent"
borderColor="#DDDDDD" borderColor="#F0F0F0"
borderBottom
height="110rpx"
size="34"
maxlength="11" maxlength="11"
/> />
<view class="reigister-form-item !hidden">
<image class="reigister-form-image" src="/static/images/register/pwd.png" />
<text>密码</text>
</view> </view>
<view class="field-item">
<text class="field-label">验证码</text>
<view class="code-inner">
<fui-input <fui-input
height="100rpx" v-model="model.form.data.code"
class="input !hidden" :padding="['24rpx', '0']"
password placeholder="请输入验证码"
autocomplete="new-password"
code
:required="false"
clearable
trim
placeholder="请输入密码"
v-model="model.form.data.password"
name="code"
marginTop="10"
backgroundColor="transparent" backgroundColor="transparent"
borderColor="#DDDDDD" borderColor="#F0F0F0"
borderBottom
height="110rpx"
size="34"
autocomplete="off"
/> />
<view class="reigister-form-item"> <view class="sms-trigger" :class="{ inactive: model.countdown > 0 }" @click="smsCode">
<image class="reigister-form-image" src="/static/images/register/sms.png" /> {{ model.countdown > 0 ? `${model.countdown}s后重发` : '获取验证码' }}
<text>验证码</text>
</view> </view>
</view>
</view>
<view class="field-item">
<text class="field-label">设置密码</text>
<fui-input <fui-input
:padding="['20rpx', '32rpx']" v-model="model.form.data.password"
placeholder="请输入验证码" type="password"
:bottomLeft="0" :padding="['24rpx', '0']"
marginTop="10" placeholder="请设置6位以上登录密码"
v-model="model.form.data.code"
backgroundColor="transparent" backgroundColor="transparent"
borderColor="#DDDDDD" borderColor="#F0F0F0"
> borderBottom
<fui-button height="110rpx"
width="200rpx" size="34"
height="64rpx" autocomplete="new-password"
: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 class="btn__box flex-center p-32rpx box-border"> </view>
<view class="action-footer">
<fui-button <fui-button
height="88rpx"
background="#67c17a"
size="32rpx"
radius="8rpx"
text="立即注册" text="立即注册"
background="linear-gradient(90deg, #5DB66F 0%, #4CAF50 100%)"
radius="100rpx"
height="110rpx"
@click="register" @click="register"
:disabled="model.loading"
:loading="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>
<view class="flex-center p-32rpx box-border btn-register" @click="goLogin"> 已有账号,立即登录 </view>
</fui-form> </fui-form>
</view> </view>
</view> </view>
<view class="footer-aesthetic"></view>
<SlideVerify v-model:show="showSlide" @success="onSlideSuccess" />
</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 { .header-visual {
0%, height: 550rpx;
100% { position: relative;
border-right-color: #333; 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% { .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.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; position: relative;
font-size: 24rpx; z-index: 10;
height: calc(100vh); padding: 20rpx 40rpx;
block-size: 100% 100%;
// background-size: 100% 100%;
// border: 1px solid #000;
background-color: #fff;
} }
.fui-descr { .brand-hero {
letter-spacing: 1rpx; position: relative;
padding: 50rpx; z-index: 10;
font-size: 24rpx; display: flex;
color: #b2b2b2; flex-direction: column;
padding-top: 12rpx; align-items: center;
padding-bottom: 48rpx; 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) { .logo-img {
text-indent: 0 !important; width: 90rpx;
height: 90rpx;
} }
} }
.register-bg-wrap { .brand-info {
position: absolute; margin-top: 24rpx;
width: 100%; text-align: center;
height: 30vh; color: #fff;
// border: 1px solid #000;
.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; display: flex;
flex-direction: column;
justify-content: center;
align-items: center; align-items: center;
background-image: url('/static/images/register/register.png'); justify-content: center;
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
.logo-content-wrap { .weather-icon {
display: flex; width: 36rpx;
flex-direction: column; height: 36rpx;
justify-content: left; margin-right: 10rpx;
align-items: flex-start; }
width: 86%; }
}
}
}
.logo-text-1 { .main-card-container {
font-size: 30rpx; padding: 0 40rpx;
font-weight: 400; margin-top: -80rpx;
letter-spacing: 0; position: relative;
margin-top: 4.25rem; z-index: 20;
color: rgb(51 51 51 / 70%);
vertical-align: middle;
} }
.logo-text { .aesthetic-card {
font-size: 40rpx; background: #ffffff;
font-weight: 500; border-radius: 50rpx;
letter-spacing: 0; padding: 70rpx 50rpx 60rpx;
margin-top: 40rpx; box-shadow: 0 15rpx 50rpx rgba(0, 0, 0, 0.06);
color: rgb(51 51 51 / 100%);
vertical-align: middle; .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 { .field-item {
position: absolute; margin-bottom: 40rpx;
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;
.reigister-form-image { .field-label {
width: 60rpx; font-size: 26rpx;
height: 60rpx; color: #999;
margin-left: 4rpx;
} }
text { .code-inner {
font-size: 32rpx;
display: flex; 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; margin-left: 20rpx;
font-weight: 500;
&.inactive {
color: #bbb;
background: #f8f8f8;
}
}
} }
} }
.form {
width: 100%;
z-index: 10;
margin-top: 540rpx;
} }
.checkbox { .action-footer {
position: fixed; margin-top: 70rpx;
left: 0rpx;
bottom: 32rpx; .secondary-links {
width: 100%; margin-top: 40rpx;
z-index: 10; 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; display: flex;
align-items: center; /* 垂直居中 */
justify-content: center; justify-content: center;
}
.checkbox-align {
line-height: 1;
display: flex;
align-items: center; align-items: center;
} }
.input, .policy-text {
.btn__box { font-size: 24rpx;
width: 100%; color: #bbb;
height: 100rpx; margin-left: 12rpx;
margin-top: 60rpx; display: flex;
align-items: center;
} }
.fiexdText { .link {
// position: absolute; color: #5DB66F;
width: 100%; font-weight: 500;
margin-top: 40rpx; }
} }
: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 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>
<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> </view>
</fui-form>
</view> </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" /> <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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论