473 lines
11 KiB
Vue
473 lines
11 KiB
Vue
|
|
<template>
|
||
|
|
<view class="login-container">
|
||
|
|
<!-- 顶部标题 -->
|
||
|
|
<view class="login-header">
|
||
|
|
<text class="login-title">欢迎登录</text>
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<!-- 登录方式切换 -->
|
||
|
|
<view class="login-tabs">
|
||
|
|
<view
|
||
|
|
class="login-tab"
|
||
|
|
:class="{ 'active': loginType === 'account' }"
|
||
|
|
@click="loginType = 'account'"
|
||
|
|
>
|
||
|
|
账号密码
|
||
|
|
</view>
|
||
|
|
<view
|
||
|
|
class="login-tab"
|
||
|
|
:class="{ 'active': loginType === 'phone' }"
|
||
|
|
@click="loginType = 'phone'"
|
||
|
|
>
|
||
|
|
手机验证码
|
||
|
|
</view>
|
||
|
|
<view
|
||
|
|
class="login-tab"
|
||
|
|
:class="{ 'active': loginType === 'wx' }"
|
||
|
|
@click="loginType = 'wx'"
|
||
|
|
>
|
||
|
|
微信登录
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<!-- 登录表单 -->
|
||
|
|
<view class="login-form">
|
||
|
|
<!-- 账号密码登录 -->
|
||
|
|
<view v-if="loginType === 'account'">
|
||
|
|
<view class="form-item">
|
||
|
|
<input
|
||
|
|
v-model="form.username"
|
||
|
|
type="text"
|
||
|
|
placeholder="请输入用户名/手机号"
|
||
|
|
class="input"
|
||
|
|
/>
|
||
|
|
</view>
|
||
|
|
<view class="form-item">
|
||
|
|
<input
|
||
|
|
v-model="form.password"
|
||
|
|
type="password"
|
||
|
|
placeholder="请输入密码"
|
||
|
|
class="input"
|
||
|
|
/>
|
||
|
|
</view>
|
||
|
|
<view class="form-item captcha">
|
||
|
|
<input
|
||
|
|
v-model="form.code"
|
||
|
|
type="text"
|
||
|
|
placeholder="验证码"
|
||
|
|
class="input"
|
||
|
|
/>
|
||
|
|
<image
|
||
|
|
:src="captchaUrl"
|
||
|
|
@click="refreshCaptcha"
|
||
|
|
class="captcha-img"
|
||
|
|
/>
|
||
|
|
</view>
|
||
|
|
<button
|
||
|
|
class="login-btn"
|
||
|
|
:disabled="isLogining"
|
||
|
|
@click="handleLogin"
|
||
|
|
>
|
||
|
|
{{ isLogining ? '登录中...' : '登录' }}
|
||
|
|
</button>
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<!-- 手机验证码登录 -->
|
||
|
|
<view v-else-if="loginType === 'phone'">
|
||
|
|
<view class="form-item">
|
||
|
|
<input
|
||
|
|
v-model="form.phone"
|
||
|
|
type="tel"
|
||
|
|
placeholder="请输入手机号"
|
||
|
|
class="input"
|
||
|
|
/>
|
||
|
|
</view>
|
||
|
|
<view class="form-item">
|
||
|
|
<input
|
||
|
|
v-model="form.code"
|
||
|
|
type="number"
|
||
|
|
placeholder="请输入验证码"
|
||
|
|
class="input"
|
||
|
|
/>
|
||
|
|
<text
|
||
|
|
v-if="countdown > 0"
|
||
|
|
class="countdown"
|
||
|
|
>{{ countdown }}s</text>
|
||
|
|
<text
|
||
|
|
v-else
|
||
|
|
class="send-code"
|
||
|
|
@click="sendCode"
|
||
|
|
>发送验证码</text>
|
||
|
|
</view>
|
||
|
|
<button
|
||
|
|
class="login-btn"
|
||
|
|
:disabled="isLogining"
|
||
|
|
@click="handleLogin"
|
||
|
|
>
|
||
|
|
{{ isLogining ? '登录中...' : '登录' }}
|
||
|
|
</button>
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<!-- 微信登录 -->
|
||
|
|
<view v-else-if="loginType === 'wx'">
|
||
|
|
<button
|
||
|
|
class="wx-login-btn"
|
||
|
|
@click="wxLogin"
|
||
|
|
:disabled="isLogining"
|
||
|
|
>
|
||
|
|
<image src="/static/icon-wechat.png" class="wx-icon" />
|
||
|
|
微信一键登录
|
||
|
|
</button>
|
||
|
|
<view class="one-click-login" @click="getPhone">
|
||
|
|
<image src="/static/icon-phone.png" class="phone-icon" />
|
||
|
|
本机一键登录
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<!-- 底部链接 -->
|
||
|
|
<view class="login-footer">
|
||
|
|
<text class="link" @click="toRegister">注册新账号</text>
|
||
|
|
<text class="link" @click="toForget">忘记密码</text>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
import { ref, onMounted } from 'vue';
|
||
|
|
import { login, getCodeImg, sendPhoneCode, verifyPhoneCode } from '@/api/login';
|
||
|
|
import { register } from '@/api/system/user';
|
||
|
|
import { getWxCode } from '@/utils/geek';
|
||
|
|
import { wxLogin, wxRegister } from '@/api/oauth';
|
||
|
|
import { setToken } from '@/utils/auth';
|
||
|
|
import { useUserStore } from '@/store/modules/user';
|
||
|
|
import modal from '@/plugins/modal';
|
||
|
|
|
||
|
|
export default {
|
||
|
|
setup() {
|
||
|
|
// 当前登录方式 (account/phone/wx)
|
||
|
|
const loginType = ref('account');
|
||
|
|
// 表单数据
|
||
|
|
const form = ref({
|
||
|
|
username: '',
|
||
|
|
password: '',
|
||
|
|
phone: '',
|
||
|
|
code: '',
|
||
|
|
captcha: ''
|
||
|
|
});
|
||
|
|
// 验证码图片
|
||
|
|
const captchaUrl = ref('');
|
||
|
|
// 倒计时(用于发送验证码)
|
||
|
|
const countdown = ref(0);
|
||
|
|
// 登录中状态
|
||
|
|
const isLogining = ref(false);
|
||
|
|
// 用户store
|
||
|
|
const userStore = useUserStore();
|
||
|
|
|
||
|
|
// 初始化验证码
|
||
|
|
const initCaptcha = () => {
|
||
|
|
getCodeImg().then(res => {
|
||
|
|
captchaUrl.value = res;
|
||
|
|
}).catch(() => {
|
||
|
|
modal.toast('验证码加载失败');
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
// 刷新验证码
|
||
|
|
const refreshCaptcha = () => {
|
||
|
|
initCaptcha();
|
||
|
|
};
|
||
|
|
|
||
|
|
// 发送手机验证码
|
||
|
|
const sendCode = () => {
|
||
|
|
if (!form.value.phone) {
|
||
|
|
modal.toast('请输入手机号');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
sendPhoneCode({ phone: form.value.phone }).then(() => {
|
||
|
|
modal.toast('验证码已发送');
|
||
|
|
countdown.value = 60;
|
||
|
|
const timer = setInterval(() => {
|
||
|
|
countdown.value--;
|
||
|
|
if (countdown.value <= 0) clearInterval(timer);
|
||
|
|
}, 1000);
|
||
|
|
}).catch(err => {
|
||
|
|
modal.toast(err.message || '发送失败');
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
// 处理登录
|
||
|
|
const handleLogin = async () => {
|
||
|
|
isLogining.value = true;
|
||
|
|
try {
|
||
|
|
if (loginType.value === 'account') {
|
||
|
|
// 账号密码登录
|
||
|
|
if (!form.value.username || !form.value.password || !form.value.code) {
|
||
|
|
modal.toast('请填写完整信息');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const res = await login(
|
||
|
|
form.value.username,
|
||
|
|
form.value.password,
|
||
|
|
form.value.code,
|
||
|
|
''
|
||
|
|
);
|
||
|
|
handleLoginSuccess(res);
|
||
|
|
} else if (loginType.value === 'phone') {
|
||
|
|
// 手机验证码登录
|
||
|
|
if (!form.value.phone || !form.value.code) {
|
||
|
|
modal.toast('请填写完整信息');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// 1. 验证手机验证码
|
||
|
|
await verifyPhoneCode({ phone: form.value.phone, code: form.value.code });
|
||
|
|
// 2. 检查用户是否存在(自动注册逻辑)
|
||
|
|
try {
|
||
|
|
// 尝试用手机号登录(后端会自动注册新用户)
|
||
|
|
const res = await login(
|
||
|
|
form.value.phone,
|
||
|
|
'123456', // 默认密码(首次登录自动注册时使用)
|
||
|
|
'',
|
||
|
|
''
|
||
|
|
);
|
||
|
|
handleLoginSuccess(res);
|
||
|
|
} catch (err) {
|
||
|
|
// 用户不存在,自动注册
|
||
|
|
await register({
|
||
|
|
username: form.value.phone,
|
||
|
|
phonenumber: form.value.phone,
|
||
|
|
password: '123456'
|
||
|
|
});
|
||
|
|
// 注册后重新登录
|
||
|
|
const res = await login(
|
||
|
|
form.value.phone,
|
||
|
|
'123456',
|
||
|
|
'',
|
||
|
|
''
|
||
|
|
);
|
||
|
|
handleLoginSuccess(res);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
modal.toast(err.message || '登录失败');
|
||
|
|
} finally {
|
||
|
|
isLogining.value = false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 处理登录成功
|
||
|
|
const handleLoginSuccess = (res) => {
|
||
|
|
setToken(res.token);
|
||
|
|
userStore.setUserInfo(res.user);
|
||
|
|
uni.switchTab({ url: '/pages/index' });
|
||
|
|
};
|
||
|
|
|
||
|
|
// 微信登录
|
||
|
|
const wxLogin = async () => {
|
||
|
|
isLogining.value = true;
|
||
|
|
try {
|
||
|
|
const code = await getWxCode();
|
||
|
|
const res = await wxLogin('pub', code); // pub: 公众号/小程序
|
||
|
|
if (res.token) {
|
||
|
|
handleLoginSuccess(res);
|
||
|
|
} else {
|
||
|
|
modal.toast('微信登录失败,请重试');
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
modal.toast('微信登录异常: ' + err.message);
|
||
|
|
} finally {
|
||
|
|
isLogining.value = false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 本机一键登录(获取手机号)
|
||
|
|
const getPhone = async () => {
|
||
|
|
try {
|
||
|
|
const res = await uni.getPhoneNumber();
|
||
|
|
if (res.code === 0) {
|
||
|
|
form.value.phone = res.code;
|
||
|
|
modal.toast('手机号获取成功');
|
||
|
|
} else {
|
||
|
|
modal.toast('获取手机号失败');
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
modal.toast('获取手机号异常: ' + err.message);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 跳转到注册页面
|
||
|
|
const toRegister = () => {
|
||
|
|
uni.navigateTo({
|
||
|
|
url: '/pages_system/pages/login/register'
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
// 跳转到忘记密码页面
|
||
|
|
const toForget = () => {
|
||
|
|
uni.navigateTo({
|
||
|
|
url: '/pages_system/pages/login/forget'
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
onMounted(() => {
|
||
|
|
initCaptcha();
|
||
|
|
});
|
||
|
|
|
||
|
|
return {
|
||
|
|
loginType,
|
||
|
|
form,
|
||
|
|
captchaUrl,
|
||
|
|
countdown,
|
||
|
|
isLogining,
|
||
|
|
handleLogin,
|
||
|
|
sendCode,
|
||
|
|
refreshCaptcha,
|
||
|
|
wxLogin,
|
||
|
|
getPhone,
|
||
|
|
toRegister,
|
||
|
|
toForget
|
||
|
|
};
|
||
|
|
}
|
||
|
|
};
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.login-container {
|
||
|
|
padding: 40rpx;
|
||
|
|
background: #f5f5f5;
|
||
|
|
min-height: 100vh;
|
||
|
|
}
|
||
|
|
|
||
|
|
.login-header {
|
||
|
|
text-align: center;
|
||
|
|
margin-bottom: 80rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.login-title {
|
||
|
|
font-size: 40rpx;
|
||
|
|
font-weight: bold;
|
||
|
|
color: #333;
|
||
|
|
}
|
||
|
|
|
||
|
|
.login-tabs {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-around;
|
||
|
|
margin-bottom: 60rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.login-tab {
|
||
|
|
padding: 16rpx 32rpx;
|
||
|
|
border-radius: 40rpx;
|
||
|
|
border: 1px solid #ddd;
|
||
|
|
font-size: 28rpx;
|
||
|
|
color: #666;
|
||
|
|
}
|
||
|
|
|
||
|
|
.login-tab.active {
|
||
|
|
background: #007AFF;
|
||
|
|
color: white;
|
||
|
|
border-color: #007AFF;
|
||
|
|
}
|
||
|
|
|
||
|
|
.login-form {
|
||
|
|
background: white;
|
||
|
|
border-radius: 20rpx;
|
||
|
|
padding: 40rpx;
|
||
|
|
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.08);
|
||
|
|
}
|
||
|
|
|
||
|
|
.form-item {
|
||
|
|
margin-bottom: 30rpx;
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.input {
|
||
|
|
width: 100%;
|
||
|
|
padding: 20rpx;
|
||
|
|
border: 1px solid #eee;
|
||
|
|
border-radius: 12rpx;
|
||
|
|
font-size: 28rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.captcha {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.captcha-img {
|
||
|
|
width: 200rpx;
|
||
|
|
height: 80rpx;
|
||
|
|
margin-left: 20rpx;
|
||
|
|
border-radius: 10rpx;
|
||
|
|
background: #f0f0f0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.countdown {
|
||
|
|
color: #007AFF;
|
||
|
|
font-size: 26rpx;
|
||
|
|
margin-left: 10rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.send-code {
|
||
|
|
color: #007AFF;
|
||
|
|
font-size: 26rpx;
|
||
|
|
margin-left: 10rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.login-btn {
|
||
|
|
background: #007AFF;
|
||
|
|
color: white;
|
||
|
|
border-radius: 12rpx;
|
||
|
|
font-size: 32rpx;
|
||
|
|
height: 88rpx;
|
||
|
|
line-height: 88rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.wx-login-btn {
|
||
|
|
background: #07C160;
|
||
|
|
color: white;
|
||
|
|
border-radius: 12rpx;
|
||
|
|
font-size: 32rpx;
|
||
|
|
height: 88rpx;
|
||
|
|
line-height: 88rpx;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
gap: 16rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.wx-icon {
|
||
|
|
width: 40rpx;
|
||
|
|
height: 40rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.one-click-login {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
gap: 16rpx;
|
||
|
|
color: #666;
|
||
|
|
font-size: 28rpx;
|
||
|
|
margin-top: 30rpx;
|
||
|
|
padding: 16rpx;
|
||
|
|
border-radius: 12rpx;
|
||
|
|
background: #f5f5f5;
|
||
|
|
}
|
||
|
|
|
||
|
|
.phone-icon {
|
||
|
|
width: 36rpx;
|
||
|
|
height: 36rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.login-footer {
|
||
|
|
text-align: center;
|
||
|
|
margin-top: 60rpx;
|
||
|
|
color: #999;
|
||
|
|
font-size: 28rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.link {
|
||
|
|
color: #007AFF;
|
||
|
|
margin: 0 20rpx;
|
||
|
|
}
|
||
|
|
</style>
|