创建uniapp基础形态

This commit is contained in:
2026-07-15 17:39:10 +08:00
parent 743c5cb4eb
commit 11e0763766
22 changed files with 5311 additions and 43 deletions
+446
View File
@@ -0,0 +1,446 @@
<template>
<view class="login-page">
<view class="page-inner">
<!-- 品牌区 -->
<view class="header">
<text class="title">欢迎回来</text>
<text class="subtitle">登录云泽开启你的旅程</text>
</view>
<!-- 登录方式切换 -->
<view class="tabs">
<view
class="tab"
:class="{ active: loginType === 'phone' }"
@tap="loginType = 'phone'"
>手机登录</view>
<view
class="tab"
:class="{ active: loginType === 'account' }"
@tap="loginType = 'account'"
>账号登录</view>
</view>
<!-- 表单 -->
<view class="form">
<template v-if="loginType === 'phone'">
<view class="field">
<u-input
v-model="phone"
placeholder="请输入手机号"
type="number"
maxlength="11"
border="none"
color="#3c9cff"
:customStyle="inputStyle"
:placeholderStyle="placeholderStyle"
>
<template #prefix>
<text class="prefix">+86</text>
</template>
</u-input>
</view>
<view class="field field-row">
<u-input
v-model="code"
placeholder="请输入验证码"
type="number"
maxlength="6"
border="none"
color="#3c9cff"
:customStyle="inputStyle"
:placeholderStyle="placeholderStyle"
/>
<text
class="code-link"
:class="{ disabled: countdown > 0 }"
@tap="sendCode"
>{{ countdown > 0 ? `${countdown}s 后重发` : '获取验证码' }}</text>
</view>
</template>
<template v-else>
<view class="field">
<u-input
v-model="username"
placeholder="请输入账号"
border="none"
color="#3c9cff"
:customStyle="inputStyle"
:placeholderStyle="placeholderStyle"
/>
</view>
<view class="field">
<u-input
v-model="password"
placeholder="请输入密码"
type="password"
border="none"
color="#3c9cff"
:customStyle="inputStyle"
:placeholderStyle="placeholderStyle"
/>
</view>
</template>
<view class="btn-primary" @tap="handleLogin"> </view>
<view class="btn-ghost" @tap="handleTestLogin">测试登录</view>
</view>
<!-- 协议 -->
<view class="agreement" @tap="agreed = !agreed">
<view class="agree-dot" :class="{ on: agreed }">
<text v-if="agreed" class="agree-check"></text>
</view>
<text class="agree-text">
登录即表示同意
<text class="agree-link">用户协议</text>
<text class="agree-link">隐私政策</text>
</text>
</view>
<!-- 第三方 -->
<view class="oauth">
<text class="oauth-label">其他方式</text>
<view class="oauth-icons">
<view class="oauth-btn" @tap="socialLogin('wechat')">
<FaIcon name="weixin" type="brands" color="#3c9cff" :size="22" />
</view>
<view class="oauth-btn" @tap="socialLogin('qq')">
<FaIcon name="qq" type="brands" color="#3c9cff" :size="22" />
</view>
<view class="oauth-btn" @tap="socialLogin('alipay')">
<FaIcon name="alipay" type="brands" color="#3c9cff" :size="22" />
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { loginSuccess, isLoggedIn } from '@/utils/auth.js'
const loginType = ref('phone')
const phone = ref('')
const code = ref('')
const username = ref('')
const password = ref('')
const agreed = ref(true)
const countdown = ref(0)
let timer = null
const inputStyle = {
backgroundColor: 'transparent',
padding: '0 8rpx',
height: '96rpx',
fontSize: '28rpx'
}
const placeholderStyle = 'color: #9acafc; font-size: 28rpx'
onMounted(() => {
if (isLoggedIn()) {
uni.reLaunch({ url: '/pages/dashboard/dashboard' })
}
})
function sendCode() {
if (countdown.value > 0) return
if (!/^1\d{10}$/.test(phone.value)) {
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
return
}
countdown.value = 60
timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) clearInterval(timer)
}, 1000)
uni.showToast({ title: '验证码已发送', icon: 'success' })
}
function navigateAfterLogin() {
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.reLaunch({ url: '/pages/dashboard/dashboard' })
}, 500)
}
function handleTestLogin() {
loginSuccess({ nickname: '测试用户', phone: '13800000000' })
navigateAfterLogin()
}
function handleLogin() {
if (!agreed.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
return
}
if (loginType.value === 'phone') {
if (!/^1\d{10}$/.test(phone.value)) {
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
return
}
if (!code.value || code.value.length < 4) {
uni.showToast({ title: '请输入验证码', icon: 'none' })
return
}
loginSuccess({ phone: phone.value, nickname: '用户' + phone.value.slice(-4) })
} else {
if (!username.value.trim()) {
uni.showToast({ title: '请输入账号', icon: 'none' })
return
}
if (!password.value.trim()) {
uni.showToast({ title: '请输入密码', icon: 'none' })
return
}
loginSuccess({ nickname: username.value })
}
navigateAfterLogin()
}
function socialLogin(type) {
if (!agreed.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
return
}
const names = { wechat: '微信', qq: 'QQ', alipay: '支付宝' }
loginSuccess({ nickname: names[type] + '用户' })
uni.showToast({ title: `${names[type]}登录成功`, icon: 'success' })
setTimeout(() => {
uni.reLaunch({ url: '/pages/dashboard/dashboard' })
}, 500)
}
</script>
<style lang="scss" scoped>
.login-page {
min-height: 100vh;
background: #ffffff;
}
.page-inner {
padding: 0 56rpx;
padding-top: calc(var(--status-bar-height, 44px) + 80rpx);
}
/* ---- 品牌 ---- */
.header {
margin-bottom: 72rpx;
}
.logo {
width: 88rpx;
height: 88rpx;
border-radius: 22rpx;
background: #ffffff;
border: 2rpx solid #e8f0f8;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 36rpx;
}
.logo-char {
font-size: 44rpx;
font-weight: 600;
color: $u-primary;
}
.title {
display: block;
font-size: 52rpx;
font-weight: 600;
color: $u-primary;
letter-spacing: 2rpx;
}
.subtitle {
display: block;
font-size: 26rpx;
color: $u-primary-disabled;
margin-top: 12rpx;
}
/* ---- Tab ---- */
.tabs {
display: flex;
gap: 48rpx;
margin-bottom: 48rpx;
}
.tab {
font-size: 30rpx;
color: $u-primary-disabled;
padding-bottom: 12rpx;
position: relative;
&.active {
color: $u-primary;
font-weight: 600;
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 4rpx;
background: $u-primary;
border-radius: 2rpx;
}
}
}
/* ---- 表单 ---- */
.form {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.field {
background: #ffffff;
border: 2rpx solid #e8f0f8;
border-radius: 16rpx;
padding: 0 28rpx;
overflow: hidden;
}
.field-row {
display: flex;
align-items: center;
}
.prefix {
font-size: 28rpx;
color: $u-primary;
padding-right: 20rpx;
margin-right: 20rpx;
border-right: 1rpx solid $u-primary-disabled;
}
.code-link {
flex-shrink: 0;
font-size: 26rpx;
color: $u-primary;
padding-left: 16rpx;
white-space: nowrap;
&.disabled {
color: $u-primary-disabled;
}
}
.btn-primary {
height: 96rpx;
background: $u-primary;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
color: #fff;
letter-spacing: 6rpx;
margin-top: 16rpx;
&:active {
background: $u-primary-dark;
}
}
.btn-ghost {
height: 96rpx;
background: #ffffff;
border: 2rpx solid $u-primary;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: $u-primary;
&:active {
background: #f8fbff;
}
}
/* ---- 协议 ---- */
.agreement {
display: flex;
align-items: flex-start;
gap: 12rpx;
margin-top: 36rpx;
}
.agree-dot {
width: 30rpx;
height: 30rpx;
border-radius: 50%;
border: 2rpx solid $u-primary-disabled;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 2rpx;
&.on {
background: $u-primary;
border-color: $u-primary;
}
}
.agree-check {
font-size: 18rpx;
color: #fff;
}
.agree-text {
font-size: 22rpx;
color: $u-primary-disabled;
line-height: 1.7;
}
.agree-link {
color: $u-primary;
}
/* ---- 第三方 ---- */
.oauth {
margin-top: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 28rpx;
padding-bottom: 60rpx;
}
.oauth-label {
font-size: 24rpx;
color: $u-primary-disabled;
}
.oauth-icons {
display: flex;
gap: 40rpx;
}
.oauth-btn {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #ffffff;
border: 2rpx solid #e8f0f8;
display: flex;
align-items: center;
justify-content: center;
&:active {
border-color: $u-primary-disabled;
}
}
</style>