Files
lilishop-uniapp/pages/passport/login.vue
Yer11214 349ffa4f34 refactor: 重构多个组件以支持 Vue 3 语法和功能
- 将多个组件转换为 `<script setup>` 语法,提升可读性和性能
- 优化状态管理和事件处理逻辑,简化代码结构
- 更新样式和布局以适应新组件结构
- 添加新功能和修复已知问题,提升用户体验
2026-07-08 18:37:11 +08:00

838 lines
19 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.

<template>
<div class="wrapper">
<div v-if="!wechatLogin" class="login-body">
<u-navbar :auto-back="showBack" :border="false"></u-navbar>
<div>
<div class="title">{{ loginTitleWay[current].title }}</div>
<div :class="current == 1 ? 'desc-light' : 'desc'">
{{ loginTitleWay[current].desc
}}<span v-if="current == 1">{{secrecyMobile(mobile) }}</span>
</div>
</div>
<!-- 手机号 -->
<div v-show="!enableUserPwdBox">
<div v-show="current == 0">
<u-input
border="none"
:custom-style="inputStyle"
:placeholder-style="placeholderStyle"
font-size="40rpx"
color="#333"
placeholder="请输入手机号 (11位)"
class="mobile"
focus
v-model="mobile"
type="number"
maxlength="11"
/>
<div :class="!enableFetchCode ? 'disable' : 'fetch'" @click="fetchCode" class="btn">
获取验证码
</div>
</div>
<!-- 输入验证码 -->
<div v-if="current == 1" class="box-code">
<verifyCode
type="bottom"
@confirm="submit"
boxActiveColor="#D8D8D8"
v-model="code"
:is-focus="true"
boxNormalColor="#D8D8D8"
cursorColor="#D8D8D8"
/>
<div class="fetch-btn">
<u-code change-text="验证码已发送x" end-text="重新获取验证码" unique-key="page-login"
:seconds="seconds" @end="end" @start="start" ref="uCode" @change="codeChange">
</u-code>
<span @tap="fetchCode" :style="{ color: codeColor }">
{{ tips }}</span>
</div>
</div>
</div>
<!-- 帐号密码登录 -->
<div v-show="enableUserPwdBox">
<u-input
border="none"
:custom-style="inputStyle"
:placeholder-style="placeholderStyle"
font-size="40rpx"
color="#333"
placeholder="请输入用户名"
class="mobile"
focus
v-model="userData.username"
/>
<u-input
border="none"
:custom-style="inputStyle"
:placeholder-style="placeholderStyle"
font-size="40rpx"
color="#333"
placeholder="请输入密码"
class="mobile"
focus
v-model="userData.password"
type="password"
/>
<div :class="!enableUserBtnColor ? 'disable' : 'fetch'" @click="passwordLogin" class="btn">
帐号密码登录
</div>
</div>
<div class="privacy-row" v-show="current != 1">
<u-checkbox
class="privacy-check"
usedAlone
shape="circle"
v-model:checked="enablePrivacy"
:active-color="lightColor"
:icon-size="18"
></u-checkbox>
<div class="tips">
未注册的手机号验证后将自动创建用户账号登录即代表您已同意<span @click="navigateToPrivacy('PRIVACY_POLICY')">隐私协议</span>
<span @click="navigateToPrivacy('USER_AGREEMENT')">
用户协议
</span>
</div>
</div>
<div v-if="current != 1" class="user-password-tips" @click="enableUserPwdBox = !enableUserPwdBox">
{{ !enableUserPwdBox ? "帐号密码" : "手机号" }}登录
</div>
<!-- 循环出当前可使用的第三方登录模式 -->
<div class="flex login-list">
<template v-for="(item, index) in loginList" :key="index">
<div v-if="item.code" :style="{ background: item.color }" class="login-item">
<u-icon v-if="item.title != 'APPLE'" color="#fff" size="42" :name="item.icon"
@click="navigateLogin(item)">
</u-icon>
<u-image v-else src="/static/appleidButton@2x.png" :lazy-load="false" @click="navigateLogin(item)"
width="80" height="80" />
</div>
</template>
</div>
<myVerification v-if="codeFlag" @send="handleVerification" class="verification" ref="verification"
business="LOGIN" />
</div>
<view v-else>
<wechatH5Login />
</view>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, nextTick, getCurrentInstance, onMounted } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { useStore } from '@/store'
import { openIdLogin, loginCallback, webConnect } from '@/api/connect.js'
import api from '@/config/api.js'
import { sendMobile, smsLogin, userLogin } from '@/api/login'
import myVerification from '@/components/verification/verification.vue'
import verifyCode from '@/components/verify-code/verify-code'
import { getUserInfo } from '@/api/members'
import { whetherNavigate } from '@/utils/Foundation'
import storage from '@/utils/storage.js'
import wechatH5Login from './wechatH5Login.vue'
import { md5 } from '@/utils/md5.js'
import { secrecyMobile } from '@/utils/filters.js'
interface LoginListItem {
icon: string
color: string
title: string
code: string
appcode?: string
}
const store = useStore()
const { proxy } = getCurrentInstance()!
const lightColor = computed(() => store.getters.lightColor)
const aiderLightColor = computed(() => store.getters.aiderLightColor)
const wechatLogin = ref(false)
const flage = ref(false)
const codeFlag = ref(true)
const tips = ref('')
const enableUserPwdBox = ref(false)
const current = ref(0)
const codeColor = ref('#999')
const seconds = ref(60)
const loginTitleWay = [
{ title: '欢迎登录', desc: '登录后更精彩,美好生活即将开始' },
{ title: '请输入验证码', desc: '已经发送验证码至' },
]
const userData = reactive({ username: '', password: '' })
const showBack = ref(false)
const enableFetchCode = ref(false)
const enableUserBtnColor = ref(false)
const enablePrivacy = ref(false)
const mobile = ref('')
const code = ref('')
const inputStyle = {
height: '104rpx',
'border-bottom': '1rpx solid #D8D8D8',
'letter-spacing': '1rpx',
padding: '0',
}
const placeholderStyle = 'font-size: 32rpx;line-height: 32rpx;color: #999999;'
const loginList = ref<LoginListItem[]>([
{ icon: 'weixin-fill', color: '#00a327', title: '微信', code: 'WECHAT' },
{ icon: 'qq-fill', color: '#38ace9', title: 'QQ', code: 'QQ' },
{ icon: 'apple-fill', color: '#000000', title: 'Apple', code: 'APPLE' },
])
const clientType = ref('')
const verification = ref<any>(null)
const uCode = ref<any>(null)
onShow(() => {
// 只要是app登录的全部清除内容
// #ifdef APP-PLUS
storage.setAccessToken('')
storage.setRefreshToken('')
storage.setUserInfo({})
// #endif
//#ifdef H5
const isWXBrowser = /micromessenger/i.test(navigator.userAgent)
if (isWXBrowser) {
webConnect('WECHAT').then((res) => {
const data = res.data
if (data.success) {
window.location = data.result
}
})
}
//#endif
})
onMounted(() => {
// #ifndef APP-PLUS
//判断是否微信浏览器
const ua = window.navigator.userAgent.toLowerCase()
if (ua.match(/MicroMessenger/i) == 'micromessenger') {
wechatLogin.value = true
return
}
// #endif
/**
* 条件编译判断当前客户端类型
*/
//#ifdef H5
clientType.value = 'H5'
//#endif
//#ifdef APP-PLUS
clientType.value = 'APP'
/**如果是app 加载支持的登录方式*/
uni.getProvider({
service: 'oauth',
success: (result) => {
loginList.value = result.provider.map((value) => {
let title = ''
let codeVal = ''
let color = '#8b8b8b'
let icon = ''
switch (value) {
case 'weixin':
icon = 'weixin-circle-fill'
color = '#00a327'
title = '微信'
codeVal = 'WECHAT'
break
case 'qq':
icon = 'qq-circle-fill'
color = '#38ace9'
title = 'QQ'
codeVal = 'QQ'
break
case 'apple':
icon = 'apple-fill'
color = '#000000'
title = 'Apple'
codeVal = 'APPLE'
break
}
return {
title,
code: codeVal,
color,
icon,
appcode: value,
}
})
},
fail: (error) => {
uni.showToast({
title: '获取登录通道失败' + error,
duration: 2000,
icon: 'none',
})
},
})
//#endif
//特殊平台,登录方式需要过滤
// #ifdef H5
methodFilter(['QQ'])
// #endif
//微信小程序,只支持微信登录
// #ifdef MP-WEIXIN
methodFilter(['WECHAT'])
// #endif
})
watch(current, (val) => {
showBack.value = val ? true : false
})
watch(
userData,
() => {
enableUserBtnColor.value = !!(userData.username && userData.password)
},
{ deep: true }
)
watch(mobile, (val) => {
if (val.length == 11) {
enableFetchCode.value = true
}
})
watch(flage, async (val) => {
if (val) {
if (enableUserPwdBox.value) {
submitUserLogin()
return
}
// 首次发送时 uCode 尚未挂载;重新获取时才检查倒计时
const canSendCode = current.value === 0 || uCode.value?.canGetCode
if (!canSendCode) {
proxy.$u.toast('请倒计时结束后再发送')
return
}
uni.showLoading({})
const res = await sendMobile(mobile.value)
if (store.state.isShowToast) {
uni.hideLoading()
}
if (res.data.success) {
code.value = ''
current.value = 1
nextTick(() => {
uCode.value?.start()
})
} else {
uni.showToast({
title: res.data.message,
duration: 2000,
icon: 'none',
})
flage.value = false
verification.value?.getCode()
}
} else {
verification.value?.hide()
}
})
onLoad((options) => {
if (options && options.state) {
stateLogin(options.state as string)
}
})
function stateLogin(state: string) {
loginCallback(state).then((res) => {
const data = res.data
if (data.success) {
storage.setAccessToken(data.result.accessToken)
storage.setRefreshToken(data.result.refreshToken)
uni.showToast({
title: '登录成功!',
icon: 'none',
})
getUserInfo().then((user) => {
if (user.data.success) {
storage.setUserInfo(user.data.result)
storage.setHasLogin(true)
} else {
setTimeout(() => {
uni.showToast({
title: user.data.message,
icon: 'none',
})
storage.setAccessToken('')
storage.setRefreshToken('')
uni.switchTab({
url: '/pages/tabbar/user/my',
})
}, 500)
}
})
getCurrentPages().length > 1
? uni.navigateBack({
delta: getCurrentPages().length - 2,
})
: uni.switchTab({
url: '/pages/tabbar/home/index',
})
}
})
}
function methodFilter(codes: string[]) {
const way: LoginListItem[] = []
loginList.value.forEach((item) => {
if (codes.length != 0) {
codes.forEach((val) => {
if (item.code == val) {
way.push(item)
}
})
} else {
uni.showToast({
title: '配置有误请联系管理员',
duration: 2000,
icon: 'none',
})
}
})
loginList.value = way
}
async function nonH5OpenId(item: LoginListItem) {
let loginRes: UniApp.LoginRes | null = null
await uni.login({
provider: item.appcode,
// #ifdef MP-ALIPAY
scopes: 'auth_user',
// #endif
success(res) {
loginRes = res
uni.setStorageSync('type', item.code)
// #ifndef MP-WEIXIN
uni.setStorageSync('openid', res.authResult.openid)
res.authResult.unionId && uni.setStorageSync('unionId', res.authResult.unionId)
// #endif
},
fail() {
uni.showToast({
title: '第三方登录暂不可用!',
icon: 'none',
duration: 3000,
})
},
complete() {
uni.getUserInfo({
provider: item.appcode,
success(infoRes) {
uni.setStorageSync('nickname', infoRes.userInfo.nickName)
uni.setStorageSync('avatar', infoRes.userInfo.avatarUrl)
uni.setStorageSync('unionId', infoRes.userInfo.unionId || infoRes.userInfo.unionid)
// #ifdef MP-WEIXIN
if (loginRes) {
weixinMPOpenID(loginRes).then(() => {
goOpenidLogin('WECHAT_MP')
})
}
// #endif
// #ifndef MP-WEIXIN
goOpenidLogin('APP')
// #endif
},
})
},
})
}
function goOpenidLogin(loginClientType: string) {
const params: Record<string, any> = {
uuid: uni.getStorageSync('openid'),
source: uni.getStorageSync('type'),
nickname: uni.getStorageSync('nickname'),
username: uni.getStorageSync('openid'),
avatar: uni.getStorageSync('avatar'),
uniAccessToken: uni.getStorageSync('uni_access_token'),
type: clientType.value,
token: { unionId: '', openId: uni.getStorageSync('openid') },
}
uni.getStorageSync('unionId')
? (params.token.unionId = uni.getStorageSync('unionId'))
: delete params.token
openIdLogin(params, loginClientType).then((res) => {
if (!res.data.success) {
uni.showToast({
title: '第三方登录暂不可用',
icon: 'none',
duration: 3000,
})
return
}
storage.setAccessToken(res.data.result.accessToken)
storage.setRefreshToken(res.data.result.refreshToken)
uni.showToast({
title: '第三方登录成功!',
icon: 'none',
})
getUserInfo().then((user) => {
if (user.data.success) {
storage.setUserInfo(user.data.result)
storage.setHasLogin(true)
uni.switchTab({
url: '/pages/tabbar/home/index',
})
if (user.data.result.mobile) {
whetherNavigate()
} else {
uni.navigateTo({
url: '/pages/passport/bindUserPhone',
})
}
} else {
setTimeout(() => {
uni.showToast({
title: user.data.message,
icon: 'none',
})
storage.setAccessToken('')
storage.setRefreshToken('')
uni.switchTab({
url: '/pages/tabbar/user/my',
})
}, 500)
}
})
})
}
async function weixinMPOpenID(res: UniApp.LoginRes) {
await miniProgramLogin(res.code).then((loginRes) => {
uni.setStorageSync('openid', loginRes.data)
})
}
function navigateLogin(connectLogin: LoginListItem) {
// #ifdef H5
const codeVal = connectLogin.code
const buyer = api.buyer
window.open(buyer + `/passport/connect/connect/login/web/` + codeVal, '_self')
// #endif
// #ifdef APP-PLUS
nonH5OpenId(connectLogin)
// #endif
}
function submit() {
storage.setHasLogin(false)
storage.setAccessToken('')
storage.setRefreshToken('')
storage.setUserInfo({})
smsLogin({ mobile: mobile.value, code: code.value }, clientType.value).then((res) => {
getUserInfoMethods(res)
})
}
function getUserInfoMethods(res: any) {
if (res.data.success) {
storage.setAccessToken(res.data.result.accessToken)
storage.setRefreshToken(res.data.result.refreshToken)
getUserInfo().then((user) => {
if (user.data.success) {
storage.setUserInfo(user.data.result)
storage.setHasLogin(true)
storage.setAutoCp(0)
uni.showToast({
title: '提示',
content: '登录成功!',
icon: 'none',
})
whetherNavigate()
} else {
setTimeout(() => {
uni.showToast({
title: user.data.message,
icon: 'none',
})
storage.setAccessToken('')
storage.setRefreshToken('')
uni.switchTab({
url: '/pages/tabbar/user/my',
})
}, 500)
}
})
}
}
function handleVerification(val: string) {
flage.value = val == store.state.verificationKey ? true : false
}
function navigateToPrivacy(val: string) {
uni.navigateTo({
url: '/pages/mine/help/tips?type=' + val,
})
}
function start() {
codeColor.value = '#999'
proxy.$u.toast('验证码已发送')
flage.value = false
codeFlag.value = false
verification.value?.hide()
}
function codeChange(text: string) {
tips.value = text
}
function end() {
codeColor.value = lightColor.value
codeFlag.value = true
console.log(codeColor.value)
}
function passwordLogin() {
if (!enablePrivacy.value) {
uni.showToast({
title: '请同意用户隐私',
duration: 2000,
icon: 'none',
})
return false
}
if (!userData.username) {
uni.showToast({
title: '请填写用户名',
duration: 2000,
icon: 'none',
})
return false
}
if (!userData.password) {
uni.showToast({
title: '请填写密码',
duration: 2000,
icon: 'none',
})
return false
}
if (!flage.value) {
verification.value?.error()
return false
}
}
async function submitUserLogin() {
const params = JSON.parse(JSON.stringify(userData))
params.password = md5(params.password)
try {
const res = await userLogin(params, clientType.value)
if (res.data.success) {
getUserInfoMethods(res)
} else {
verification.value?.getCode()
flage.value = false
}
} catch {
verification.value?.getCode()
}
}
function fetchCode() {
if (!enablePrivacy.value) {
uni.showToast({
title: '请同意用户隐私',
duration: 2000,
icon: 'none',
})
return false
}
if (!proxy.$u.test.mobile(mobile.value)) {
uni.showToast({
title: '请填写正确手机号',
duration: 2000,
icon: 'none',
})
return false
}
if (tips.value == '重新获取验证码') {
uni.showLoading({ title: '加载中' })
if (!codeFlag.value) {
const timer = setInterval(() => {
if (verification.value) {
verification.value.error()
}
clearInterval(timer)
}, 100)
}
if (store.state.isShowToast) {
uni.hideLoading()
}
}
if (!flage.value) {
verification.value?.error()
return false
}
}
declare function miniProgramLogin(code: string | undefined): Promise<{ data: any }>
</script>
<style>
page {
background: #fff;
}
</style>
<style lang="scss" scoped>
.login-body {
flex: 1;
display: flex;
flex-direction: column;
}
.wrapper {
padding: 0 80rpx;
min-height: 100vh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.title {
padding-top: calc(104rpx);
font-style: normal;
line-height: 1;
font-weight: 500;
font-size: 56rpx;
color: #333;
}
.box-code {
margin-top: 78rpx;
}
.desc,
.desc-light {
font-size: 32rpx;
line-height: 32rpx;
color: #333333;
margin-top: 40rpx;
}
.desc {
color: #333;
}
.desc-light {
color: #999999;
>span {
color: #333;
margin-left: 8rpx;
}
}
.mobile {
margin-top: 80rpx;
}
.disable {
background: linear-gradient(90deg, #ffdcba 2.21%, #ffcfb2 99.86%);
}
.fetch {
background: linear-gradient(57.72deg, #ff8a19 18.14%, #ff5e00 98.44%);
}
.btn {
border-radius: 100px;
width: 590rpx;
margin-top: 97rpx;
height: 80rpx;
font-size: 30rpx;
line-height: 80rpx;
text-align: center;
color: #ffffff;
}
.tips {
flex: 1;
font-size: 24rpx;
line-height: 36rpx;
margin-top: 0;
color: #666;
>span {
color: $light-color;
}
}
.privacy-row {
display: flex;
align-items: flex-start;
margin-top: 32rpx;
}
.privacy-check {
flex-shrink: 0;
margin-top: 4rpx;
:deep(.u-checkbox) {
margin: 0;
}
}
.fetch-btn {
width: 370rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
background: #f2f2f2;
border-radius: 100rpx;
font-size: 28rpx;
color: #999;
margin: 71rpx auto 0 auto;
}
.login-list {
display: flex;
width: 590rpx;
margin-top: auto;
margin-bottom: 80rpx;
align-items: center;
justify-content: center;
}
.login-item {
width: 80rpx;
border-radius: 10rpx;
height: 80rpx;
display: flex;
justify-content: center;
align-items: center;
margin: 0 20rpx;
}
.user-password-tips {
text-align: center;
color: $light-color;
margin: 40rpx 0;
font-size: 28rpx;
}
</style>