Files
shanxia2.0/miniprogram/pages/login/login.vue
T

621 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { computed, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useUserStore } from '@/stores/user'
import { DEFAULT_PLACEHOLDER_IMAGE_URL } from '@/utils/staticAsset'
const userStore = useUserStore()
const form = ref({
username: '',
password: ''
})
const isLoading = ref(false)
const agreeProtocol = ref(false)
const loginDisabled = computed(() => isLoading.value)
const protocolDialogVisible = ref(false)
const protocolDialogForWechat = ref(false)
let protocolDialogResolver = null
onShow(() => {
userStore.checkLoginStatus()
if (userStore.isLoggedIn) {
userStore.navigateToHome()
}
})
const loginPageStyle = computed(() => ({ '--theme-color': '#0a9f6d' }))
const handleSubmit = async () => {
if (!agreeProtocol.value) {
protocolDialogForWechat.value = false
const shouldContinue = await new Promise((resolve) => {
protocolDialogResolver = resolve
protocolDialogVisible.value = true
})
if (!shouldContinue) return
agreeProtocol.value = true
}
if (!form.value.username) {
uni.showToast({ title: '请输入用户名', icon: 'none' })
return
}
if (!form.value.password) {
uni.showToast({ title: '请输入密码', icon: 'none' })
return
}
isLoading.value = true
try {
const success = await userStore.login({
username: form.value.username,
password: form.value.password
})
if (success) {
userStore.navigateToHome()
}
} finally {
isLoading.value = false
}
}
const goUserAgreement = () => {
uni.navigateTo({ url: '/pages/login/user-agreement' })
}
const goPrivacyPolicy = () => {
uni.navigateTo({ url: '/pages/login/privacy-policy' })
}
const toggleProtocolAgreement = () => {
agreeProtocol.value = !agreeProtocol.value
}
const resolveProtocolDialog = (confirmed) => {
protocolDialogVisible.value = false
protocolDialogForWechat.value = false
if (protocolDialogResolver) {
protocolDialogResolver(confirmed)
protocolDialogResolver = null
}
}
const onWechatGate = () => {
if (isLoading.value) return
protocolDialogForWechat.value = true
protocolDialogVisible.value = true
}
const onAgreeGetPhoneNumber = (e) => {
agreeProtocol.value = true
protocolDialogVisible.value = false
protocolDialogForWechat.value = false
onGetPhoneNumber(e)
}
const onGetPhoneNumber = async (e) => {
if (isLoading.value) return
if (!agreeProtocol.value) {
onWechatGate()
return
}
const detail = (e && e.detail) || {}
const errMsg = detail.errMsg || ''
if (errMsg && errMsg.indexOf('ok') === -1) {
uni.showToast({ title: '需要授权手机号,或请使用账号密码登录', icon: 'none' })
return
}
const phoneCode = detail.code
if (!phoneCode) {
uni.showToast({ title: '需要授权手机号,或请使用账号密码登录', icon: 'none' })
return
}
isLoading.value = true
try {
const loginRes = await new Promise((resolve, reject) => {
uni.login({ provider: 'weixin', success: resolve, fail: reject })
})
const ok = await userStore.wechatLogin({
loginCode: loginRes.code,
phoneCode
})
if (ok) {
userStore.navigateToHome()
}
} catch (err) {
uni.showToast({ title: (err && err.message) || '登录失败', icon: 'none' })
} finally {
isLoading.value = false
}
}
</script>
<template>
<view class="login-page" :style="loginPageStyle">
<!-- 背景装饰 -->
<view class="login-bg">
<view class="bg-circle circle-1"></view>
<view class="bg-circle circle-2"></view>
<view class="bg-circle circle-3"></view>
</view>
<view class="login-content">
<view class="logo-section">
<image class="logo-image" :src="DEFAULT_PLACEHOLDER_IMAGE_URL" mode="widthFix" />
<text class="app-slogan">一体化业务移动端</text>
</view>
<view class="login-form">
<view class="form-item">
<text class="form-label">用户名</text>
<view class="form-input-box">
<uni-icons class="form-input-ico" type="person" size="18" color="#c0c4cc" />
<input
v-model="form.username"
class="form-input"
type="text"
placeholder="请输入用户名"
placeholder-class="input-placeholder"
/>
</view>
</view>
<view class="form-item">
<text class="form-label">密码</text>
<view class="form-input-box">
<uni-icons class="form-input-ico" type="locked" size="18" color="#c0c4cc" />
<input
v-model="form.password"
class="form-input"
type="password"
placeholder="请输入密码"
placeholder-class="input-placeholder"
/>
</view>
</view>
<view class="form-actions">
<button class="btn-login" :disabled="loginDisabled" @click="handleSubmit">
<text v-if="isLoading" class="btn-login-text">登录中...</text>
<text v-else class="btn-login-text">账号密码登录</text>
</button>
</view>
</view>
<view class="login-footer">
<view class="footer-line">
<view class="agree-check" @tap="toggleProtocolAgreement">
<view class="agree-check__icon" :class="{ 'agree-check__icon--checked': agreeProtocol }">
<text class="agree-check__tick">✓</text>
</view>
</view>
<text class="footer-text">登录即表示同意</text>
<text class="link" @tap="goUserAgreement">《用户服务协议》</text>
<text class="footer-text">和</text>
<text class="link" @tap="goPrivacyPolicy">《隐私政策》</text>
</view>
</view>
<view class="login-divider">或</view>
<button
v-if="agreeProtocol"
class="login-wechat"
:disabled="isLoading"
open-type="getPhoneNumber"
@getphonenumber="onGetPhoneNumber"
>
<text class="login-wechat__label">手机号快捷登录</text>
</button>
<view
v-else
class="login-wechat"
:class="{ 'is-disabled': isLoading }"
@tap="onWechatGate"
>
<text class="login-wechat__label">手机号快捷登录</text>
</view>
</view>
<view v-if="protocolDialogVisible" class="protocol-dialog-mask" @tap="resolveProtocolDialog(false)">
<view class="protocol-dialog" @tap.stop>
<text class="protocol-dialog__title">协议确认</text>
<text class="protocol-dialog__desc">登录前请先同意《用户服务协议》和《隐私政策》</text>
<view class="protocol-dialog__actions">
<view class="protocol-dialog__btn protocol-dialog__btn--cancel" @tap="resolveProtocolDialog(false)">否</view>
<button
v-if="protocolDialogForWechat"
class="protocol-dialog__btn protocol-dialog__btn--confirm"
open-type="getPhoneNumber"
@getphonenumber="onAgreeGetPhoneNumber"
>
同意并登录
</button>
<view
v-else
class="protocol-dialog__btn protocol-dialog__btn--confirm"
@tap="resolveProtocolDialog(true)"
>
是,继续登录
</view>
</view>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.login-page {
min-height: 100vh;
min-height: 100dvh;
background: linear-gradient(180deg, #f8fff8 0%, #ffffff 100%);
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: calc(32rpx + var(--sq-safe-top, env(safe-area-inset-top, 0px))) 32rpx
calc(32rpx + env(safe-area-inset-bottom, 0px));
/* 与 placeholder 同色,供图标 mask 与 :deep(.input-placeholder) 共用 */
--login-input-placeholder: #c0c4cc;
}
.login-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.login-bg .bg-circle {
position: absolute;
border-radius: 50%;
background: var(--theme-color);
opacity: 0.05;
}
.login-bg .circle-1 {
width: 400rpx;
height: 400rpx;
top: -100rpx;
right: -100rpx;
}
.login-bg .circle-2 {
width: 300rpx;
height: 300rpx;
top: 300rpx;
left: -150rpx;
}
.login-bg .circle-3 {
width: 200rpx;
height: 200rpx;
bottom: 100rpx;
right: -50rpx;
}
.login-content {
position: relative;
z-index: 1;
width: 100%;
max-width: 680rpx;
margin: 0 auto;
box-sizing: border-box;
padding: 0 8rpx;
flex-shrink: 0;
}
/* 对齐 bxapp 登录页 brand-logo:横向自适应宽图,非圆角方标 */
.logo-section {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
margin-bottom: 44rpx;
}
.logo-image {
display: block;
width: 360rpx;
max-width: 48vw;
height: auto;
margin: 0 auto 24rpx;
}
.app-slogan {
font-size: 44rpx;
font-weight: 700;
letter-spacing: 0.02em;
color: #1e293b;
margin-top: 4rpx;
}
.login-form {
background: #fff;
border-radius: 24rpx;
padding: 50rpx 40rpx;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.08);
}
.form-item {
margin-bottom: 36rpx;
}
.form-item:last-of-type {
margin-bottom: 50rpx;
}
.form-label {
display: block;
font-size: 28rpx;
color: #303133;
margin-bottom: 16rpx;
font-weight: 500;
}
.form-input-box {
display: flex;
align-items: center;
height: 88rpx;
background: #f5f7fa;
border-radius: 16rpx;
padding: 0 24rpx;
box-sizing: border-box;
}
.form-input-ico {
margin-right: 10rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.form-input {
flex: 1;
min-width: 0;
height: 100%;
padding: 0;
border: none;
background: transparent;
font-size: 28rpx;
color: #303133;
line-height: 1.25;
}
.form-actions {
width: 100%;
}
.btn-login {
width: 100%;
height: 88rpx;
border-radius: 44rpx;
font-size: 30rpx;
color: #fff;
background: linear-gradient(135deg, var(--theme-color) 0%, #07c160 100%);
border: none;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
line-height: 1.2;
}
.btn-login::after {
border: none;
}
.btn-login.button-hover,
.btn-login:active {
background: linear-gradient(135deg, var(--theme-color) 0%, #07c160 100%);
color: #fff;
opacity: 1;
}
.btn-login-text {
line-height: 1.2;
font-size: 30rpx;
}
.login-footer {
margin-top: 60rpx;
padding: 0 20rpx;
}
.footer-line {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
column-gap: 4rpx;
row-gap: 8rpx;
}
.agree-check {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8rpx;
margin-right: 4rpx;
flex-shrink: 0;
}
.agree-check__icon {
width: 30rpx;
height: 30rpx;
border-radius: 50%;
border: 2rpx solid #c0c4cc;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
}
.agree-check__tick {
font-size: 18rpx;
font-weight: 700;
line-height: 1;
color: transparent;
}
.agree-check__icon--checked {
border-color: var(--theme-color);
background: var(--theme-color);
}
.agree-check__icon--checked .agree-check__tick {
color: #fff;
}
.footer-text,
.link {
font-size: 22rpx;
line-height: 1.6;
}
.footer-text {
color: #909399;
}
.link {
color: var(--theme-color);
}
.protocol-dialog-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.42);
z-index: 99;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.protocol-dialog {
width: 100%;
max-width: 620rpx;
background: #fff;
border-radius: 24rpx;
padding: 36rpx 30rpx 28rpx;
box-sizing: border-box;
box-shadow: 0 14rpx 48rpx rgba(0, 0, 0, 0.16);
}
.protocol-dialog__title {
display: block;
text-align: center;
font-size: 34rpx;
font-weight: 700;
color: #303133;
}
.protocol-dialog__desc {
display: block;
margin-top: 16rpx;
text-align: center;
font-size: 26rpx;
line-height: 1.6;
color: #606266;
}
.protocol-dialog__actions {
margin-top: 30rpx;
display: flex;
gap: 16rpx;
}
.protocol-dialog__btn {
flex: 1;
min-height: 78rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 600;
}
.protocol-dialog__btn--cancel {
background: #f4f7f6;
color: #5b6b64;
border: 1px solid #ddebe5;
}
.protocol-dialog__btn--confirm {
background: linear-gradient(135deg, var(--theme-color) 0%, #07c160 100%);
color: #fff;
border: none;
margin: 0;
padding: 0;
line-height: normal;
}
button.protocol-dialog__btn--confirm::after {
border: none;
}
.login-divider {
display: flex;
align-items: center;
gap: 24rpx;
margin: 44rpx 0 28rpx;
width: 100%;
font-size: 24rpx;
color: #94a3b8;
}
.login-divider::before,
.login-divider::after {
content: '';
flex: 1;
height: 1px;
background: #e2e8f0;
}
.login-wechat {
height: 88rpx;
border-radius: 24rpx;
border: 1px solid #e2e8f0;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
margin: 0;
padding: 0;
line-height: normal;
box-sizing: border-box;
font-size: 28rpx;
font-weight: 500;
color: #1e293b;
}
button.login-wechat::after {
border: none;
}
.login-wechat__label {
font-size: 28rpx;
font-weight: 500;
color: #1e293b;
}
</style>
<!-- placeholder-class 在部分真机需非 scoped 才稳定生效;颜色与 --login-input-placeholder 一致 -->
<style lang="scss">
.input-placeholder {
color: #c0c4cc;
font-size: 28rpx;
}
</style>