feat: 登录添加验证码
This commit is contained in:
@@ -6,6 +6,8 @@ const WECHAT_MINIAPP_AUTH_BASE_URL = "/api/v1/wechat/miniapp/auth";
|
||||
export interface LoginData {
|
||||
username: string;
|
||||
password: string;
|
||||
captchaId?: string;
|
||||
captchaCode?: string;
|
||||
}
|
||||
|
||||
export interface LoginResult {
|
||||
@@ -18,6 +20,11 @@ export interface LoginResult {
|
||||
openid?: string;
|
||||
}
|
||||
|
||||
export interface CaptchaVO {
|
||||
captchaId: string;
|
||||
captchaBase64: string;
|
||||
}
|
||||
|
||||
export interface SmsLoginData {
|
||||
mobile: string;
|
||||
code: string;
|
||||
@@ -45,6 +52,16 @@ export interface WechatMiniappBindMobileData {
|
||||
}
|
||||
|
||||
const AuthAPI = {
|
||||
/**
|
||||
* 获取图形验证码
|
||||
*/
|
||||
getCaptcha(): Promise<CaptchaVO> {
|
||||
return publicRequest<CaptchaVO>({
|
||||
url: `${AUTH_BASE_URL}/captcha`,
|
||||
method: "GET",
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 账号密码登录
|
||||
*/
|
||||
@@ -58,7 +75,7 @@ const AuthAPI = {
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
*
|
||||
* 演示环境说明:短信服务未配置,验证码固定为 123456
|
||||
*/
|
||||
sendSmsLoginCode(mobile: string): Promise<void> {
|
||||
@@ -83,7 +100,7 @@ const AuthAPI = {
|
||||
|
||||
/**
|
||||
* 微信小程序静默登录
|
||||
*
|
||||
*
|
||||
* 适用场景:个人小程序
|
||||
* - 已绑定手机号的用户:直接返回 token,登录成功
|
||||
* - 未绑定手机号的用户:返回 openid,需调用绑定手机号接口
|
||||
@@ -97,7 +114,7 @@ const AuthAPI = {
|
||||
|
||||
/**
|
||||
* 微信小程序手机号快捷登录
|
||||
*
|
||||
*
|
||||
* 适用场景:企业认证小程序(已开通手机号快捷登录权限)
|
||||
* 一步完成登录,无需绑定流程,自动创建新用户
|
||||
*/
|
||||
@@ -112,7 +129,7 @@ const AuthAPI = {
|
||||
|
||||
/**
|
||||
* 微信小程序绑定手机号
|
||||
*
|
||||
*
|
||||
* 演示环境说明:短信服务未配置,验证码固定为 123456
|
||||
*/
|
||||
wechatMiniappBindMobile(data: WechatMiniappBindMobileData): Promise<LoginResult> {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"type": "page",
|
||||
"name": "login",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
"navigationBarTitleText": "登录"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<!-- Logo -->
|
||||
<view class="login-logo">
|
||||
<image class="logo-image" src="/static/logo.png" mode="aspectFit" />
|
||||
<text class="logo-text">YouLai Admin</text>
|
||||
<text class="logo-text">youlai-app</text>
|
||||
</view>
|
||||
|
||||
<!-- 登录卡片 -->
|
||||
@@ -43,7 +43,7 @@
|
||||
class="input-field"
|
||||
:password="!showPassword"
|
||||
placeholder="请输入密码"
|
||||
maxlength="50"
|
||||
:maxlength="50"
|
||||
@confirm="handleLogin"
|
||||
/>
|
||||
<view class="input-suffix" @click="showPassword = !showPassword">
|
||||
@@ -51,6 +51,25 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图形验证码(密码登录时显示) -->
|
||||
<view v-if="loginMode === 'PASSWORD'" class="input-box">
|
||||
<wd-icon name="shield" size="20" color="#9ca3af" />
|
||||
<input
|
||||
v-model="formData.captchaCode"
|
||||
class="input-field"
|
||||
placeholder="请输入验证码"
|
||||
:maxlength="6"
|
||||
@confirm="handleLogin"
|
||||
/>
|
||||
<image
|
||||
v-if="captchaBase64"
|
||||
class="captcha-img"
|
||||
:src="captchaBase64"
|
||||
mode="aspectFit"
|
||||
@click="fetchCaptcha"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<view v-else class="input-box">
|
||||
<wd-icon name="shield" size="20" color="#9ca3af" />
|
||||
@@ -59,7 +78,7 @@
|
||||
class="input-field"
|
||||
placeholder="请输入验证码"
|
||||
type="number"
|
||||
maxlength="6"
|
||||
:maxlength="6"
|
||||
@confirm="handleLogin"
|
||||
/>
|
||||
<view
|
||||
@@ -77,7 +96,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button class="btn-primary" :disabled="loading || !canSubmit" @click="handleLogin">
|
||||
<button class="btn-primary" :disabled="loading" @click="handleLogin">
|
||||
{{ loading ? "登录中..." : "登 录" }}
|
||||
</button>
|
||||
|
||||
@@ -130,13 +149,11 @@
|
||||
@click="loginMode = 'WECHAT'"
|
||||
/>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 协议 -->
|
||||
<view class="agreement">
|
||||
<wd-checkbox v-model="isAgreePolicy" shape="square" size="16px">
|
||||
<text class="agreement-text">
|
||||
登录即代表同意
|
||||
我已阅读并同意
|
||||
<text class="agreement-link" @click.stop="navigateToAgreement('user')">
|
||||
《用户协议》
|
||||
</text>
|
||||
@@ -147,6 +164,7 @@
|
||||
</text>
|
||||
</wd-checkbox>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -169,7 +187,7 @@
|
||||
class="input-field"
|
||||
placeholder="请输入手机号"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
:maxlength="11"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -180,7 +198,7 @@
|
||||
class="input-field"
|
||||
placeholder="请输入验证码"
|
||||
type="number"
|
||||
maxlength="6"
|
||||
:maxlength="6"
|
||||
/>
|
||||
<view
|
||||
class="sms-btn"
|
||||
@@ -203,6 +221,15 @@
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 协议确认弹窗 -->
|
||||
<wd-message-box selector="policy-box">
|
||||
<view class="text-center text-sm text-gray-500 leading-relaxed">
|
||||
请阅读并同意有来技术
|
||||
<text class="text-blue-500" @click.stop="navigateToAgreement('user')">《用户协议》</text>
|
||||
<text class="text-blue-500" @click.stop="navigateToAgreement('privacy')">《隐私政策》</text>
|
||||
</view>
|
||||
</wd-message-box>
|
||||
|
||||
<wd-toast />
|
||||
</view>
|
||||
</template>
|
||||
@@ -210,17 +237,18 @@
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "login",
|
||||
"style": { "navigationStyle": "custom" }
|
||||
"style": { "navigationBarTitleText": "登录" }
|
||||
}
|
||||
</route>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { useToast, useMessage } from "wot-design-uni";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import AuthAPI from "@/api/auth";
|
||||
|
||||
const toast = useToast();
|
||||
const message = useMessage("policy-box");
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 状态
|
||||
@@ -235,8 +263,13 @@ const formData = ref({
|
||||
username: "admin",
|
||||
password: "123456",
|
||||
code: "123456",
|
||||
captchaCode: "",
|
||||
});
|
||||
|
||||
// 图形验证码
|
||||
const captchaId = ref("");
|
||||
const captchaBase64 = ref("");
|
||||
|
||||
const redirect = ref("/pages/index/index");
|
||||
|
||||
// 绑定手机号相关
|
||||
@@ -251,6 +284,20 @@ const bindMobileForm = ref({
|
||||
code: "",
|
||||
});
|
||||
|
||||
const pendingLoginAction = ref<"FORM" | "WECHAT_PHONE" | null>(null);
|
||||
const pendingWechatPhoneCode = ref<string>("");
|
||||
|
||||
// 获取图形验证码
|
||||
const fetchCaptcha = async () => {
|
||||
try {
|
||||
const res = await AuthAPI.getCaptcha();
|
||||
captchaId.value = res.captchaId;
|
||||
captchaBase64.value = res.captchaBase64;
|
||||
} catch (e) {
|
||||
console.error("获取验证码失败", e);
|
||||
}
|
||||
};
|
||||
|
||||
// 计算属性
|
||||
const loginModeDesc = computed(() => {
|
||||
const modeMap = {
|
||||
@@ -264,7 +311,6 @@ const loginModeDesc = computed(() => {
|
||||
const isValidMobile = (mobile: string) => /^1\d{10}$/.test((mobile || "").trim());
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
if (!isAgreePolicy.value) return false;
|
||||
if (loginMode.value === "PASSWORD") {
|
||||
return (formData.value.username || "").trim() && (formData.value.password || "").trim();
|
||||
}
|
||||
@@ -286,6 +332,8 @@ onLoad((options: any) => {
|
||||
loginMode.value = "PASSWORD";
|
||||
}
|
||||
// #endif
|
||||
// 获取图形验证码
|
||||
fetchCaptcha();
|
||||
});
|
||||
|
||||
onUnload(() => {
|
||||
@@ -305,13 +353,43 @@ const toggleLoginMode = () => {
|
||||
formData.value.code = "";
|
||||
};
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (loading.value || !canSubmit.value) return;
|
||||
if (!isAgreePolicy.value) {
|
||||
toast.error("请先同意用户协议");
|
||||
const openPolicyDialog = (action: "FORM" | "WECHAT_PHONE", phoneCode: string = "") => {
|
||||
pendingLoginAction.value = action;
|
||||
pendingWechatPhoneCode.value = phoneCode;
|
||||
|
||||
message
|
||||
.confirm({ title: "提示" })
|
||||
.then(async () => {
|
||||
isAgreePolicy.value = true;
|
||||
const act = pendingLoginAction.value;
|
||||
const code = pendingWechatPhoneCode.value;
|
||||
pendingLoginAction.value = null;
|
||||
pendingWechatPhoneCode.value = "";
|
||||
|
||||
if (act === "WECHAT_PHONE") {
|
||||
await doWechatPhoneLogin(code);
|
||||
} else if (act === "FORM") {
|
||||
await doFormLogin();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
pendingLoginAction.value = null;
|
||||
pendingWechatPhoneCode.value = "";
|
||||
});
|
||||
};
|
||||
|
||||
async function doFormLogin() {
|
||||
if (!canSubmit.value) {
|
||||
if (loginMode.value === "PASSWORD") {
|
||||
toast.error("请输入用户名和密码");
|
||||
} else if (loginMode.value === "SMS") {
|
||||
toast.error("请输入正确的手机号和验证码");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (loading.value) return;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
@@ -319,6 +397,8 @@ const handleLogin = async () => {
|
||||
await userStore.login({
|
||||
username: formData.value.username,
|
||||
password: formData.value.password,
|
||||
captchaId: captchaId.value,
|
||||
captchaCode: formData.value.captchaCode,
|
||||
});
|
||||
} else {
|
||||
await userStore.loginBySms({
|
||||
@@ -332,9 +412,21 @@ const handleLogin = async () => {
|
||||
setTimeout(() => uni.reLaunch({ url: redirect.value }), 800);
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "登录失败");
|
||||
// 登录失败刷新验证码
|
||||
if (loginMode.value === "PASSWORD") {
|
||||
fetchCaptcha();
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!isAgreePolicy.value) {
|
||||
openPolicyDialog("FORM");
|
||||
return;
|
||||
}
|
||||
await doFormLogin();
|
||||
};
|
||||
|
||||
const handleSendCode = async () => {
|
||||
@@ -378,17 +470,24 @@ const startSmsCountdown = (
|
||||
|
||||
// 微信登录
|
||||
const handleWechatPhoneLogin = async (e: any) => {
|
||||
const phoneCode = e.detail.code;
|
||||
if (!isAgreePolicy.value) {
|
||||
toast.error("请先同意用户协议");
|
||||
openPolicyDialog("WECHAT_PHONE", phoneCode);
|
||||
return;
|
||||
}
|
||||
|
||||
const phoneCode = e.detail.code;
|
||||
if (!phoneCode) {
|
||||
// 用户拒绝授权,尝试个人小程序登录
|
||||
await handleWechatSilentLogin();
|
||||
return;
|
||||
}
|
||||
await doWechatPhoneLogin(phoneCode);
|
||||
};
|
||||
|
||||
async function doWechatPhoneLogin(phoneCode: string) {
|
||||
if (!phoneCode) {
|
||||
await handleWechatSilentLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
@@ -398,14 +497,13 @@ const handleWechatPhoneLogin = async (e: any) => {
|
||||
await userStore.getInfo();
|
||||
toast.success("登录成功");
|
||||
setTimeout(() => uni.reLaunch({ url: redirect.value }), 800);
|
||||
} catch (error: any) {
|
||||
// 企业登录失败,尝试个人小程序登录
|
||||
} catch {
|
||||
toast.info("正在尝试其他登录方式...");
|
||||
await handleWechatSilentLogin();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleWechatSilentLogin = async () => {
|
||||
loading.value = true;
|
||||
@@ -472,7 +570,7 @@ const handleBindMobile = async () => {
|
||||
await userStore.bindMobileForWechatMiniapp({
|
||||
openid: wechatOpenid.value,
|
||||
mobile,
|
||||
code,
|
||||
smsCode: code,
|
||||
});
|
||||
await userStore.getInfo();
|
||||
showBindMobilePopup.value = false;
|
||||
@@ -542,7 +640,7 @@ const navigateToAgreement = (type: string) => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 160px 48px 0;
|
||||
padding: 24px 48px 0;
|
||||
}
|
||||
|
||||
// Logo
|
||||
@@ -613,8 +711,11 @@ const navigateToAgreement = (type: string) => {
|
||||
|
||||
// 表单区域
|
||||
.form-area {
|
||||
& > view + view {
|
||||
margin-top: 16px;
|
||||
> view,
|
||||
> button {
|
||||
&:not(:first-child) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -745,6 +846,12 @@ const navigateToAgreement = (type: string) => {
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.captcha-img {
|
||||
width: 100px;
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
:global(.dark) .sms-btn-active {
|
||||
background-color: rgba(30, 58, 138, 0.3);
|
||||
}
|
||||
@@ -822,37 +929,25 @@ const navigateToAgreement = (type: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 协议
|
||||
.agreement {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 16px;
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
:global(.dark) .agreement {
|
||||
border-top-color: #374151;
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
:global(.dark) .agreement-text {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
:global(.dark) .agreement-link {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
// 绑定手机号弹窗
|
||||
.bind-popup {
|
||||
padding: 24px;
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-card">
|
||||
<view v-if="isLogin" class="section-card">
|
||||
<view class="section-header">
|
||||
<view class="section-title">
|
||||
<wd-icon name="user" size="18" :color="currentThemeColor" />
|
||||
|
||||
Reference in New Issue
Block a user