621 lines
13 KiB
Vue
621 lines
13 KiB
Vue
<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>
|