chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,450 @@
|
||||
<template>
|
||||
<view class="captcha-verify">
|
||||
<view class="captcha-verify-header">
|
||||
<!-- 返回按钮 -->
|
||||
<view class="back-box" @tap="handleBack">
|
||||
<text class="iconfont icon-a-Chevronleft"></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="captcha-verify-content">
|
||||
<view class="title">
|
||||
{{
|
||||
phoneOrEmail?.includes("@")
|
||||
? t("Mobile.Auth.inputEmailCode")
|
||||
: t("Mobile.Auth.inputSmsCode")
|
||||
}}
|
||||
</view>
|
||||
<view class="phone-info">
|
||||
{{ t("Mobile.Auth.codeSentToPrefix") }}
|
||||
{{
|
||||
phoneOrEmail?.includes("@")
|
||||
? `${t("Mobile.Auth.codeSentToEmailTarget")} `
|
||||
: `${t("Mobile.Auth.codeSentToPhoneTarget")} `
|
||||
}}
|
||||
{{ `${!phoneOrEmail?.includes("@") ? "86" : ""} ${phoneOrEmail}` }}
|
||||
</view>
|
||||
<view class="captcha-input-container">
|
||||
<input
|
||||
ref="captchaInputRef"
|
||||
class="captcha-input"
|
||||
:class="{ active: isFocused }"
|
||||
type="number"
|
||||
pattern="[0-9]*"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
:value="captchaCode"
|
||||
:focus="isFocused"
|
||||
:adjust-position="false"
|
||||
:hold-keyboard="true"
|
||||
cursor-spacing="0"
|
||||
:placeholder="t('Mobile.Auth.inputVerifyCode')"
|
||||
cursor-color="#5147FF"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@paste="handlePaste"
|
||||
@longpress="handleLongPress"
|
||||
/>
|
||||
</view>
|
||||
<view class="resend-container">
|
||||
<text class="countdown" v-if="countdown > 0">{{
|
||||
t("Mobile.Auth.resendAfterSeconds", { seconds: countdown })
|
||||
}}</text>
|
||||
<text
|
||||
class="resend-btn"
|
||||
:class="{ disabled: countdown > 0 }"
|
||||
@tap="handleResend"
|
||||
>{{ t("Mobile.Auth.resend") }}</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts" setup>
|
||||
import { apiSendCode } from "@/servers/account";
|
||||
import { SUCCESS_CODE, AGENT_NOT_EXIST } from "@/constants/codes.constants";
|
||||
import { useI18n } from "@/utils/i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// 定义组件props
|
||||
interface Props {
|
||||
phoneOrEmail?: string;
|
||||
tenantConfigInfo?: TenantConfigInfo;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
phoneOrEmail: "",
|
||||
tenantConfigInfo: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["verify-login", "back"]);
|
||||
|
||||
const captchaCode = ref<string>("");
|
||||
const countdown = ref<number>(59);
|
||||
let countdownTimer: number | null = null;
|
||||
const captchaInputRef = ref<any>(null);
|
||||
const isFocused = ref<boolean>(false);
|
||||
|
||||
// 判断配置信息是否邮箱登录
|
||||
const isEmailAuth = computed(() => {
|
||||
if (!props.tenantConfigInfo) {
|
||||
return false;
|
||||
}
|
||||
return props.tenantConfigInfo?.authType === 3;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
open();
|
||||
});
|
||||
|
||||
const open = () => {
|
||||
captchaCode.value = "";
|
||||
startCountdown();
|
||||
nextTick(() => {
|
||||
focusInput();
|
||||
});
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
clearCountdown();
|
||||
emit("back");
|
||||
};
|
||||
|
||||
const handleInput = async (event: any) => {
|
||||
let value = event.detail.value || "";
|
||||
|
||||
// 强制过滤非数字字符
|
||||
value = value.replace(/[^0-9]/g, "");
|
||||
|
||||
// 限制最大长度为6位
|
||||
if (value.length > 6) {
|
||||
value = value.slice(0, 6);
|
||||
}
|
||||
|
||||
captchaCode.value = value;
|
||||
|
||||
// 强制更新输入框显示值
|
||||
// #ifdef MP-WEIXIN
|
||||
if (captchaInputRef.value) {
|
||||
captchaInputRef.value.value = value;
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 检查是否输入完成(6位数字)
|
||||
if (value.length === 6 && /^\d{6}$/.test(value)) {
|
||||
emit("verify-login", value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
isFocused.value = true;
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
isFocused.value = false;
|
||||
};
|
||||
|
||||
const handlePaste = async (event: any) => {
|
||||
// #ifdef H5
|
||||
// H5平台使用 clipboardData
|
||||
event.preventDefault();
|
||||
let pasteData = "";
|
||||
if (event.clipboardData && event.clipboardData.getData) {
|
||||
pasteData = event.clipboardData.getData("text");
|
||||
await processPasteData(pasteData);
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN || APP-PLUS
|
||||
// 微信小程序和App平台在用户点击粘贴后触发此事件
|
||||
// 延迟一下确保粘贴操作完成后再读取
|
||||
setTimeout(async () => {
|
||||
await getClipboardAndPaste();
|
||||
}, 100);
|
||||
// #endif
|
||||
};
|
||||
|
||||
const getClipboardAndPaste = async () => {
|
||||
try {
|
||||
const res = await uni.getClipboardData();
|
||||
await processPasteData(res.data);
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.clipboardGetFailed"),
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const processPasteData = async (pasteData: string) => {
|
||||
// 验证粘贴内容是否为6位数字
|
||||
if (pasteData && /^\d{6}$/.test(pasteData)) {
|
||||
// 直接设置验证码
|
||||
captchaCode.value = pasteData;
|
||||
|
||||
// 强制更新输入框显示值
|
||||
// #ifdef MP-WEIXIN
|
||||
if (captchaInputRef.value) {
|
||||
captchaInputRef.value.value = pasteData;
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 触发验证
|
||||
emit("verify-login", pasteData);
|
||||
|
||||
// 提示用户
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.codeAutoFilled"),
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
} else {
|
||||
// 粘贴内容不是6位数字,提示用户
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.pasteSixDigits"),
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleLongPress = async (event: any) => {
|
||||
// #ifdef MP-WEIXIN || APP-PLUS
|
||||
// 长按时直接读取剪贴板并询问是否填充
|
||||
try {
|
||||
const res = await uni.getClipboardData();
|
||||
const clipData = res.data;
|
||||
// 检查剪贴板是否包含6位数字验证码
|
||||
if (clipData && /^\d{6}$/.test(clipData)) {
|
||||
// 直接填充,不询问
|
||||
await processPasteData(clipData);
|
||||
} else if (clipData) {
|
||||
// 剪贴板有内容但不是6位数字
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.clipboardNotSixDigits"),
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.clipboardEmpty"),
|
||||
icon: "none",
|
||||
duration: 1500,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.clipboardReadFailed"),
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
};
|
||||
|
||||
const startCountdown = () => {
|
||||
countdown.value = 59;
|
||||
countdownTimer = setInterval(() => {
|
||||
countdown.value--;
|
||||
if (countdown.value <= 0) {
|
||||
clearCountdown();
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const clearCountdown = () => {
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 兼容微信小程序和H5的聚焦方法
|
||||
const focusInput = () => {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序使用 isFocused 响应式变量控制聚焦
|
||||
isFocused.value = true;
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
// H5平台使用 ref 引用
|
||||
if (captchaInputRef.value && captchaInputRef.value.focus) {
|
||||
captchaInputRef.value.focus();
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
// App平台使用 ref 引用
|
||||
if (captchaInputRef.value && captchaInputRef.value.focus) {
|
||||
captchaInputRef.value.focus();
|
||||
}
|
||||
// #endif
|
||||
};
|
||||
|
||||
const handleResend = async () => {
|
||||
if (countdown.value > 0) return;
|
||||
const params = {
|
||||
type: "LOGIN_OR_REGISTER",
|
||||
// "phone": props.phoneOrEmail
|
||||
};
|
||||
|
||||
if (isEmailAuth.value) {
|
||||
params.email = props.phoneOrEmail;
|
||||
} else {
|
||||
params.phone = props.phoneOrEmail;
|
||||
}
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: t("Mobile.Auth.sending"),
|
||||
mask: false,
|
||||
});
|
||||
// 发送验证码
|
||||
const { code, data, message } = await apiSendCode(params);
|
||||
if (isEmailAuth.value) {
|
||||
// 邮箱验证码
|
||||
if (code === SUCCESS_CODE) {
|
||||
startCountdown();
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.resendSuccess"),
|
||||
icon: "success",
|
||||
});
|
||||
} else if (code === AGENT_NOT_EXIST) {
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: "none",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 手机验证码
|
||||
if (code === SUCCESS_CODE) {
|
||||
startCountdown();
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.resendSuccess"),
|
||||
icon: "success",
|
||||
});
|
||||
} else {
|
||||
// 手机验证失败提示消息
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
clearCountdown();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.captcha-verify {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.captcha-verify-header {
|
||||
.back-box {
|
||||
display: inline-block;
|
||||
width: 58rpx;
|
||||
height: 58rpx;
|
||||
line-height: 58rpx;
|
||||
text-align: center;
|
||||
background-color: rgba(12, 20, 102, 0.04);
|
||||
border-radius: 50%;
|
||||
|
||||
.iconfont {
|
||||
font-size: 40rpx;
|
||||
line-height: 58rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-verify-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
line-height: 56rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(21, 23, 31, 1);
|
||||
margin-bottom: 24rpx;
|
||||
line-height: 64rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.phone-info {
|
||||
font-size: 28rpx;
|
||||
font-weight: 400;
|
||||
color: rgba(21, 23, 31, 0.7);
|
||||
margin-bottom: 80rpx;
|
||||
line-height: 44rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.captcha-input-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 80rpx;
|
||||
|
||||
.captcha-input {
|
||||
width: 100%;
|
||||
// max-width: 600rpx;
|
||||
height: 88rpx;
|
||||
border: 2rpx solid transparent;
|
||||
border-radius: 16rpx;
|
||||
text-align: left;
|
||||
font-size: 32rpx;
|
||||
font-weight: 400;
|
||||
color: rgba(21, 23, 31, 1);
|
||||
background-color: rgba(12, 20, 102, 0.04);
|
||||
padding: 0 24rpx;
|
||||
// letter-spacing: 8rpx;
|
||||
|
||||
&.active {
|
||||
border-color: rgba(81, 71, 255, 1);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: rgba(81, 71, 255, 1);
|
||||
background-color: #fff;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.resend-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 400;
|
||||
line-height: 44rpx;
|
||||
color: rgba(21, 23, 31, 0.7);
|
||||
|
||||
.countdown {
|
||||
}
|
||||
|
||||
.resend-btn {
|
||||
cursor: pointer;
|
||||
color: #5147ff;
|
||||
&.disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,643 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 分段器组件 -->
|
||||
<segmented-control
|
||||
:options="loginOptions"
|
||||
v-model="currentLoginType"
|
||||
@change="handleLoginTypeChange"
|
||||
class="login-segmented"
|
||||
/>
|
||||
<view class="sub-title-wrapper">
|
||||
<text class="sub-title">{{
|
||||
t("Mobile.Auth.welcomeUse", {
|
||||
siteName: tenantConfigInfo?.siteName || "",
|
||||
})
|
||||
}}</text>
|
||||
</view>
|
||||
|
||||
<view class="phone-input-wrapper" v-if="!isEmailAuth">
|
||||
<view class="country-code">
|
||||
<text>{{ t("Mobile.Auth.defaultAreaCode") }}</text>
|
||||
<uni-icon class="iconfont icon-a-Chevrondown" />
|
||||
</view>
|
||||
<input
|
||||
class="phone-input"
|
||||
type="number"
|
||||
:placeholder="t('Mobile.Auth.inputPhone')"
|
||||
v-model="phoneNumber"
|
||||
cursor-color="#5147FF"
|
||||
/>
|
||||
<view class="clear-btn" v-if="phoneNumber" @click="clearPhoneNumber">
|
||||
<uni-icon class="iconfont icon-a-Xcircle-fill clear-icon" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="phone-input-wrapper">
|
||||
<input
|
||||
class="phone-input"
|
||||
type="email"
|
||||
:placeholder="t('Mobile.Auth.inputEmail')"
|
||||
v-model="emailNumber"
|
||||
cursor-color="#5147FF"
|
||||
/>
|
||||
<view class="clear-btn" v-if="emailNumber" @click="clearEmailNumber">
|
||||
<uni-icon class="iconfont icon-a-Xcircle-fill clear-icon" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="password-input-wrapper" v-if="!isCaptchaVerify">
|
||||
<input
|
||||
class="password-input"
|
||||
:password="!showPassword"
|
||||
:placeholder="t('Mobile.Auth.inputPassword')"
|
||||
v-model="password"
|
||||
cursor-color="#5147FF"
|
||||
/>
|
||||
<!-- <view class="eye-btn" @click="togglePasswordVisibility">
|
||||
<text class="eye-icon">{{ showPassword ? '👁' : '🙈' }}</text>
|
||||
</view> -->
|
||||
<view class="clear-btn" v-if="password" @click="clearPassword">
|
||||
<uni-icon class="iconfont icon-a-Xcircle-fill clear-icon" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button
|
||||
class="login-btn"
|
||||
@click="onFinish"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{
|
||||
isCaptchaVerify
|
||||
? t("Mobile.Auth.nextStep")
|
||||
: t("Mobile.Auth.login")
|
||||
}}
|
||||
</button>
|
||||
|
||||
<agreement-checkbox v-model="agreeTerms" />
|
||||
|
||||
<!-- #ifdef H5 || WEB-->
|
||||
<button :id="buttonId" type="button" class="captcha-button" />
|
||||
<aliyun-captcha-h5
|
||||
:config="tenantConfigInfo"
|
||||
:element-id="buttonId"
|
||||
@doAction="handlerSuccess"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from "vue";
|
||||
import { apiLogin } from "@/servers/account";
|
||||
import type { ILoginResult, LoginFieldType } from "@/types/interfaces/login";
|
||||
import { SUCCESS_CODE, AGENT_NOT_EXIST } from "@/constants/codes.constants";
|
||||
import { ACCESS_TOKEN, EXPIRE_DATE } from "@/constants/home.constants";
|
||||
import { apiSendCode } from "@/servers/account";
|
||||
import type { TenantConfigInfo } from "@/types/interfaces/login";
|
||||
import SegmentedControl from "@/components/segmented-control/segmented-control.uvue";
|
||||
import AgreementCheckbox from "@/components/agreement-checkbox/agreement-checkbox.uvue";
|
||||
import { redirectTo } from "@/utils/common.uts";
|
||||
import { useI18n } from "@/utils/i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// 定义组件props
|
||||
interface Props {
|
||||
tenantConfigInfo?: TenantConfigInfo;
|
||||
redirectUrl?: string;
|
||||
isPopup?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
tenantConfigInfo: null,
|
||||
redirectUrl: "",
|
||||
isPopup: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["show-verify", "login-success"]);
|
||||
|
||||
const phoneNumber = ref("");
|
||||
const emailNumber = ref("");
|
||||
const password = ref("");
|
||||
const agreeTerms = ref(false);
|
||||
const showPassword = ref(false);
|
||||
const captchaVerifyParam = ref(""); // 验证码
|
||||
const isCaptchaVerify = ref(false); // 是否验证码
|
||||
const loading = ref(false);
|
||||
// 阿里云验证码按钮id
|
||||
const buttonId = ref<string>("aliyun-captcha-id");
|
||||
|
||||
// 分段器相关数据
|
||||
const currentLoginType = ref("password");
|
||||
const loginOptions = computed(() => [
|
||||
{ label: t("Mobile.Auth.passwordLogin"), value: "password" },
|
||||
{ label: t("Mobile.Auth.codeLoginRegister"), value: "phone" },
|
||||
]);
|
||||
|
||||
// 处理登录方式切换
|
||||
const handleLoginTypeChange = (value: string | number, index: number) => {
|
||||
currentLoginType.value = value as string;
|
||||
|
||||
// 根据选择的登录方式更新状态
|
||||
switch (value) {
|
||||
case "password":
|
||||
isCaptchaVerify.value = false;
|
||||
break;
|
||||
case "phone":
|
||||
isCaptchaVerify.value = true;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 判断配置信息是否邮箱登录
|
||||
const isEmailAuth = computed(() => {
|
||||
if (!props.tenantConfigInfo) {
|
||||
return false;
|
||||
}
|
||||
return props.tenantConfigInfo?.authType === 3;
|
||||
});
|
||||
|
||||
const onFinish = () => {
|
||||
if (!agreeTerms.value) {
|
||||
uni.showModal({
|
||||
title: t("Mobile.Common.tip"),
|
||||
content: t("Mobile.Auth.pleaseAgreeProtocol"),
|
||||
confirmText: t("Mobile.Common.agree"),
|
||||
cancelText: t("Mobile.Common.disagree"),
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
agreeTerms.value = true;
|
||||
doLogin();
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
doLogin();
|
||||
}
|
||||
};
|
||||
|
||||
const doLogin = () => {
|
||||
// 阿里云验证码
|
||||
const tenantConfigInfo = props?.tenantConfigInfo;
|
||||
const { captchaSceneId, captchaPrefix, openCaptcha } =
|
||||
tenantConfigInfo || {};
|
||||
// 只有同时满足三个条件才启用验证码:场景ID存在、身份标存在、开启验证码
|
||||
const needAliyunCaptcha = !!(
|
||||
tenantConfigInfo &&
|
||||
captchaSceneId !== "" &&
|
||||
captchaPrefix !== "" &&
|
||||
openCaptcha
|
||||
);
|
||||
|
||||
// 如果需要阿里云验证码,则点击按钮触发验证码
|
||||
if (needAliyunCaptcha) {
|
||||
// #ifdef H5 || WEB
|
||||
// H5和WEB端验证码
|
||||
document?.getElementById(buttonId.value)?.click();
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序直接执行登录/验证码逻辑
|
||||
handleClickAliyunCaptcha();
|
||||
// #endif
|
||||
} else {
|
||||
//不需要阿里云验证码,直接执行登录/验证码逻辑
|
||||
handlerSuccess();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickAliyunCaptcha = () => {
|
||||
uni.navigateTo({
|
||||
url: "/subpackages/pages/aliyun-captcha/aliyun-captcha",
|
||||
events: {
|
||||
getCaptchaVerifyParam: (captchaVerifyParam) => {
|
||||
// 定义一个getCaptchaVerifyParam事件,获取二次验证参数captchaVerifyParam
|
||||
console.log("验证码返回结果:", captchaVerifyParam);
|
||||
// 发起二次校验
|
||||
handlerSuccess(captchaVerifyParam);
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handlerSuccess = (captchaVerifyParam: string = "") => {
|
||||
if (isCaptchaVerify.value) {
|
||||
// 验证码登录
|
||||
handleNext(captchaVerifyParam);
|
||||
} else {
|
||||
// 密码登录
|
||||
handleLogin(captchaVerifyParam);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = (captchaVerifyParam: string = "") => {
|
||||
if (isEmailAuth.value) {
|
||||
handleLoginByEmail(captchaVerifyParam);
|
||||
} else {
|
||||
handleLoginByPhone(captchaVerifyParam);
|
||||
}
|
||||
};
|
||||
|
||||
// 校验登录参数
|
||||
const validateLoginParams = () => {
|
||||
if (isEmailAuth.value) {
|
||||
// 校验邮箱是否为空
|
||||
if (!emailNumber.value) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.inputEmail"),
|
||||
icon: "none",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
// 校验邮箱格式
|
||||
const emailRegex =
|
||||
/^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/;
|
||||
if (!emailRegex.test(emailNumber.value)) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.invalidEmailFormat"),
|
||||
icon: "none",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// 校验手机号是否为空
|
||||
if (!phoneNumber.value) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.inputPhone"),
|
||||
icon: "none",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
// 校验手机号格式
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(phoneNumber.value)) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.invalidPhoneFormat"),
|
||||
icon: "none",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 下一步
|
||||
const handleNext = async (captchaVerifyParam: string = "") => {
|
||||
// 校验登录参数
|
||||
if (!validateLoginParams()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = {
|
||||
type: "LOGIN_OR_REGISTER",
|
||||
captchaVerifyParam,
|
||||
// "phone": phoneNumber.value
|
||||
};
|
||||
|
||||
if (isEmailAuth.value) {
|
||||
params.email = emailNumber.value;
|
||||
} else {
|
||||
params.phone = phoneNumber.value;
|
||||
}
|
||||
|
||||
const phoneOrEmail = isEmailAuth.value
|
||||
? emailNumber.value
|
||||
: phoneNumber.value;
|
||||
// 发送验证码
|
||||
try {
|
||||
loading.value = true;
|
||||
const { code, data, message } = await apiSendCode(params);
|
||||
if (isEmailAuth.value) {
|
||||
// 邮箱验证码
|
||||
if (code === SUCCESS_CODE) {
|
||||
// 系统未配置邮件服务,请直接输出本次验证码:779895
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.codeSentSuccess"),
|
||||
icon: "none",
|
||||
});
|
||||
emit("show-verify", phoneOrEmail, captchaVerifyParam);
|
||||
} else if (code === AGENT_NOT_EXIST) {
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: "none",
|
||||
duration: 5000,
|
||||
});
|
||||
emit("show-verify", phoneOrEmail, captchaVerifyParam);
|
||||
}
|
||||
} else {
|
||||
// 手机验证码
|
||||
if (code === SUCCESS_CODE) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.codeSentSuccess"),
|
||||
icon: "none",
|
||||
});
|
||||
emit("show-verify", phoneOrEmail, captchaVerifyParam);
|
||||
} else {
|
||||
// 手机验证失败需要将 message 消息提示出来
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 手机号登录
|
||||
const handleLoginByPhone = async (captchaVerifyParam: string = "") => {
|
||||
// 校验登录参数
|
||||
if (!validateLoginParams()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password.value) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.inputPassword"),
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行登录逻辑
|
||||
const params: LoginFieldType = {
|
||||
phoneOrEmail: phoneNumber.value,
|
||||
areaCode: "86",
|
||||
password: password.value,
|
||||
captchaVerifyParam,
|
||||
};
|
||||
try {
|
||||
loading.value = true;
|
||||
const result = await apiLogin(params);
|
||||
handleLoginSuccess(result);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 邮箱登录
|
||||
const handleLoginByEmail = async (captchaVerifyParam: string = "") => {
|
||||
// 校验登录参数
|
||||
if (!validateLoginParams()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password.value) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.inputPassword"),
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行登录逻辑
|
||||
const params: LoginFieldType = {
|
||||
phoneOrEmail: emailNumber.value,
|
||||
areaCode: "",
|
||||
password: password.value,
|
||||
captchaVerifyParam,
|
||||
};
|
||||
|
||||
const { code, data, message } = await apiLogin(params);
|
||||
if (code === SUCCESS_CODE) {
|
||||
// 邮箱登录
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.loginSuccess"),
|
||||
icon: "success",
|
||||
});
|
||||
setTimeout(() => {
|
||||
// #ifdef H5 || WEB
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
uni.setStorageSync(ACCESS_TOKEN, data.token);
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.setStorageSync(ACCESS_TOKEN, data.token);
|
||||
// #endif
|
||||
|
||||
if (props.isPopup) {
|
||||
emit("login-success");
|
||||
} else {
|
||||
redirectTo(props.redirectUrl);
|
||||
}
|
||||
}, 1000);
|
||||
} else if (code === AGENT_NOT_EXIST) {
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 登录成功后
|
||||
const handleLoginSuccess = (result: ILoginResult) => {
|
||||
const { code, data, message } = result;
|
||||
if (code === SUCCESS_CODE) {
|
||||
// 手机号登录
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.loginSuccess"),
|
||||
icon: "success",
|
||||
});
|
||||
setTimeout(() => {
|
||||
// #ifdef H5 || WEB
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
uni.setStorageSync(ACCESS_TOKEN, data.token);
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.setStorageSync(ACCESS_TOKEN, data.token);
|
||||
// #endif
|
||||
|
||||
if (props.isPopup) {
|
||||
emit("login-success");
|
||||
} else {
|
||||
redirectTo(props.redirectUrl);
|
||||
}
|
||||
}, 1000);
|
||||
} else if (code === AGENT_NOT_EXIST) {
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const clearPhoneNumber = () => {
|
||||
phoneNumber.value = "";
|
||||
};
|
||||
|
||||
const clearEmailNumber = () => {
|
||||
emailNumber.value = "";
|
||||
};
|
||||
|
||||
const clearPassword = () => {
|
||||
password.value = "";
|
||||
};
|
||||
|
||||
const togglePasswordVisibility = () => {
|
||||
showPassword.value = !showPassword.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
// .login-segmented {
|
||||
// margin-bottom: 80rpx;
|
||||
// }
|
||||
|
||||
.sub-title-wrapper {
|
||||
margin-top: 80rpx;
|
||||
margin-bottom: 48rpx;
|
||||
|
||||
.sub-title {
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-size: 40rpx;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 56rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
position: absolute;
|
||||
right: 32rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
|
||||
.clear-icon {
|
||||
color: #b2b4b8;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.phone-input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background: rgba(12, 20, 102, 0.04);
|
||||
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
margin-bottom: 36rpx;
|
||||
position: relative;
|
||||
border: 2rpx solid transparent;
|
||||
transition: border-color 0.3s ease;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #5147ff;
|
||||
}
|
||||
|
||||
.country-code {
|
||||
color: rgba(21, 23, 31, 1);
|
||||
margin-right: 24rpx;
|
||||
border-right: 2rpx solid rgba(12, 20, 102, 0.12);
|
||||
padding-right: 24rpx;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
|
||||
text {
|
||||
margin-left: 10rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 400;
|
||||
line-height: 48rpx;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.phone-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 32rpx;
|
||||
color: #15171f;
|
||||
font-weight: 400;
|
||||
line-height: 48rpx;
|
||||
padding-right: 50rpx;
|
||||
caret-color: #5147ff;
|
||||
cursor-color: #5147ff;
|
||||
}
|
||||
}
|
||||
|
||||
.password-input-wrapper {
|
||||
background: rgba(12, 20, 102, 0.04);
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
margin-bottom: 36rpx;
|
||||
position: relative;
|
||||
border: 2rpx solid transparent;
|
||||
transition: border-color 0.3s ease;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #5147ff;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 32rpx;
|
||||
color: #15171f;
|
||||
font-weight: 400;
|
||||
line-height: 48rpx;
|
||||
padding-right: 60rpx;
|
||||
caret-color: #5147ff;
|
||||
cursor-color: #5147ff;
|
||||
}
|
||||
|
||||
.eye-btn {
|
||||
position: absolute;
|
||||
right: 80rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
|
||||
.eye-icon {
|
||||
font-size: 32rpx;
|
||||
color: #b2b4b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
margin-top: 20rpx;
|
||||
width: 100%;
|
||||
border-radius: 16rpx;
|
||||
padding: 25rpx 24rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 400;
|
||||
margin-bottom: 32rpx;
|
||||
line-height: 48rpx;
|
||||
text-align: center;
|
||||
background: #5147ff;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.captcha-button {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<view class="login-lang-wrapper" v-if="langList.length > 1">
|
||||
<view class="login-lang-switcher" @tap="handleOpen">
|
||||
<text class="iconfont icon-Globe"></text>
|
||||
<text class="lang-text">{{ currentLanguageName }}</text>
|
||||
<text class="iconfont icon-a-Chevrondown"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<radio-list-drawer
|
||||
:visible="visible"
|
||||
:title="t('Mobile.Profile.selectLanguage')"
|
||||
:list="languageOptions"
|
||||
:current-value="localCurrentLang"
|
||||
@onClose="handleClose"
|
||||
@onChange="handleLanguageChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { useI18n, normalizeLang } from "@/utils/i18n";
|
||||
import { RadioListItem } from "@/types/interfaces/radio";
|
||||
import RadioListDrawer from "@/components/radio-list-drawer/radio-list-drawer.uvue";
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
const { t, currentLang, langList, setLanguage } = useI18n();
|
||||
|
||||
const visible = ref(false);
|
||||
const localCurrentLang = ref("");
|
||||
|
||||
const languageOptions = computed((): RadioListItem[] => {
|
||||
return langList.value.map((item): RadioListItem => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: normalizeLang(item.lang || ""),
|
||||
disabled: item.status === 0,
|
||||
} as RadioListItem;
|
||||
});
|
||||
});
|
||||
|
||||
const currentLanguageName = computed((): string => {
|
||||
// 优先寻找接口返回的默认项 (isDefault === 1)
|
||||
const defaultItem = langList.value.find((item) => item.isDefault === 1);
|
||||
if (defaultItem != null) {
|
||||
return defaultItem.name;
|
||||
}
|
||||
|
||||
// 兜底逻辑:从当前语言中匹配
|
||||
const normCurrent = normalizeLang(currentLang.value);
|
||||
const currentItem = langList.value.find(
|
||||
(item) => normalizeLang(item.lang || "") === normCurrent,
|
||||
);
|
||||
return currentItem?.name || currentItem?.lang || "Language";
|
||||
});
|
||||
|
||||
const handleOpen = () => {
|
||||
// 优先同步接口返回的默认语言到弹窗选中态
|
||||
const defaultItem = langList.value.find((item) => item.isDefault === 1);
|
||||
if (defaultItem != null) {
|
||||
localCurrentLang.value = normalizeLang(defaultItem.lang || "");
|
||||
} else {
|
||||
localCurrentLang.value = normalizeLang(currentLang.value);
|
||||
}
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleLanguageChange = async (targetLang: string) => {
|
||||
if (!targetLang || targetLang === currentLang.value) return;
|
||||
|
||||
uni.showLoading({
|
||||
title: t("Mobile.Common.switching"),
|
||||
});
|
||||
try {
|
||||
const ok = await setLanguage(targetLang);
|
||||
if (!ok) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Common.switchFailed"),
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: t("Mobile.Common.switchSuccess"),
|
||||
icon: "success",
|
||||
});
|
||||
|
||||
// 切换语言后刷新页面,确保全局语言包生效
|
||||
setTimeout(() => {
|
||||
// #ifdef H5
|
||||
window.location.reload();
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.relaunch({
|
||||
url: "/subpackages/pages/login/login",
|
||||
});
|
||||
// #endif
|
||||
}, 500);
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
visible.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-lang-wrapper {
|
||||
position: absolute;
|
||||
top: 40rpx;
|
||||
right: 32rpx;
|
||||
z-index: 999;
|
||||
|
||||
.login-lang-switcher {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 100rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
cursor: pointer;
|
||||
|
||||
/* 确保点击态响应 */
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.lang-text {
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.icon-a-Chevrondown {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<view class="container page-container">
|
||||
<!-- #ifdef H5 -->
|
||||
<login-lang-switcher />
|
||||
<!-- #endif -->
|
||||
|
||||
<safety-zone />
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<custom-nav-bar
|
||||
v-if="showNavBar"
|
||||
:show-back="showBack"
|
||||
:has-safety-zone="false"
|
||||
:transparent-background="true"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
<view class="content-weixin">
|
||||
<view class="header-content">
|
||||
<image
|
||||
v-if="!!logo"
|
||||
class="icon"
|
||||
:src="logo"
|
||||
mode="aspectFill"
|
||||
:alt="t('Mobile.Common.appLogoAlt')"
|
||||
/>
|
||||
<view class="title"> {{ title }} </view>
|
||||
</view>
|
||||
<view class="form-content">
|
||||
<slot name="default"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { useI18n } from "@/utils/i18n";
|
||||
import LoginLangSwitcher from "../login-lang-switcher/login-lang-switcher.uvue";
|
||||
const { t } = useI18n();
|
||||
|
||||
// 定义组件props
|
||||
interface Props {
|
||||
title?: string;
|
||||
logo?: string;
|
||||
showNavBar?: boolean;
|
||||
showBack?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: "",
|
||||
logo: "",
|
||||
showNavBar: true,
|
||||
showBack: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(180deg, rgb(150, 136, 230) 0%, #f8b1c5 100%);
|
||||
|
||||
.content-weixin {
|
||||
flex: 1;
|
||||
min-height: 0; /* 允许收缩 */
|
||||
overflow: auto;
|
||||
padding: 16rpx;
|
||||
/* 隐藏滚动条 */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
&::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari, Opera */
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 256rpx;
|
||||
|
||||
.icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-left: 20rpx;
|
||||
font-weight: 600;
|
||||
font-style: Semibold;
|
||||
font-size: 32rpx;
|
||||
leading-trim: NONE;
|
||||
line-height: 48rpx;
|
||||
letter-spacing: 0rpx;
|
||||
vertical-align: middle;
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.form-content {
|
||||
flex-shrink: 0; /* 防止内容被压缩 */
|
||||
padding: 48rpx;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<view class="captcha-verify">
|
||||
<view class="captcha-verify-header">
|
||||
<!-- 返回按钮 -->
|
||||
<view class="back-box" @tap="handleBack">
|
||||
<text class="iconfont icon-a-Chevronleft"></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="captcha-verify-content">
|
||||
<view class="title">{{ t("Mobile.Auth.setPasswordTitle") }}</view>
|
||||
<view class="phone-info">{{ t("Mobile.Auth.setPasswordDesc") }}</view>
|
||||
</view>
|
||||
<view class="password-input-wrapper">
|
||||
<input class="password-input" password :placeholder="t('Mobile.Auth.inputPassword')" v-model="password" cursor-color="#5147FF" />
|
||||
<view class="clear-btn" v-if="password" @click="clearPassword">
|
||||
<uni-icon class="iconfont icon-a-Xcircle-fill clear-icon" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="password-input-wrapper">
|
||||
<input class="password-input" password :placeholder="t('Mobile.Auth.inputPasswordAgain')" v-model="confirmPassword" cursor-color="#5147FF" />
|
||||
<view class="clear-btn" v-if="confirmPassword" @click="clearConfirmPassword">
|
||||
<uni-icon class="iconfont icon-a-Xcircle-fill clear-icon" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="reset-btn" :class="{ loading: loadingReset }" @click="handleReset" :disabled="loadingReset" :loading="loadingReset">
|
||||
<text>{{ loadingReset ? t("Mobile.Auth.setting") : t("Mobile.Common.ok") }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts" setup>
|
||||
import { apiSetPassword } from '@/servers/account'
|
||||
import { SUCCESS_CODE } from '@/constants/codes.constants'
|
||||
import type { ResetPasswordParams } from '@/types/interfaces/login'
|
||||
import { useI18n } from "@/utils/i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
phoneNumber: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
verificationCode: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['reset-success', 'back'])
|
||||
|
||||
// 密码
|
||||
const password = ref('')
|
||||
// 确认密码
|
||||
const confirmPassword = ref('')
|
||||
// 登录loading
|
||||
const loadingReset = ref(false)
|
||||
|
||||
// 清除密码
|
||||
const clearPassword = () => {
|
||||
password.value = ''
|
||||
}
|
||||
|
||||
// 清除确认密码
|
||||
const clearConfirmPassword = () => {
|
||||
confirmPassword.value = ''
|
||||
}
|
||||
|
||||
// 返回登录
|
||||
const handleBack = () => {
|
||||
emit('back')
|
||||
}
|
||||
|
||||
// 密码验证
|
||||
const validatePassword = () => {
|
||||
if (!password.value) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.inputPassword"),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (password.value.length < 6) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.passwordMinLength"),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!confirmPassword.value) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.confirmPassword"),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (password.value !== confirmPassword.value) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.passwordMismatch"),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
const handleReset = async () => {
|
||||
|
||||
if (!validatePassword()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loadingReset.value = true
|
||||
const { code, message } = await apiSetPassword({password: password.value})
|
||||
|
||||
if (code === SUCCESS_CODE) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Auth.passwordSetSuccess"),
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}, 1000)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: message || t("Mobile.Auth.passwordSetFailed"),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: t("Mobile.Common.networkRetry"),
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
loadingReset.value = false
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.captcha-verify {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.captcha-verify-header {
|
||||
|
||||
.back-box{
|
||||
display: inline-block;
|
||||
width: 58rpx;
|
||||
height: 58rpx;
|
||||
line-height: 58rpx;
|
||||
text-align: center;
|
||||
background-color: rgba(12, 20, 102, 0.04);
|
||||
border-radius: 50%;
|
||||
|
||||
.iconfont {
|
||||
font-size: 48rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.captcha-verify-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
line-height: 56rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(21, 23, 31, 1);
|
||||
margin-bottom: 24rpx;
|
||||
line-height: 64rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.phone-info {
|
||||
font-size: 28rpx;
|
||||
font-weight: 400;
|
||||
color: rgba(21, 23, 31, 0.7);
|
||||
margin-bottom: 80rpx;
|
||||
line-height: 44rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.password-input-wrapper {
|
||||
background: rgba(12, 20, 102, 0.04);
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
margin-bottom: 36rpx;
|
||||
position: relative;
|
||||
border: 2rpx solid transparent;
|
||||
transition: border-color 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
// justify-content: center;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #5147FF;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 32rpx;
|
||||
color: #15171f;
|
||||
font-weight: 400;
|
||||
line-height: 48rpx;
|
||||
padding-right: 60rpx;
|
||||
caret-color: #5147FF;
|
||||
cursor-color: #5147FF;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
position: absolute;
|
||||
right: 32rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
|
||||
.clear-icon {
|
||||
color: #b2b4b8;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
width: 100%;
|
||||
border-radius: 16rpx;
|
||||
margin-top: 20rpx;
|
||||
padding: 32rpx 23rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 400;
|
||||
margin-bottom: 32rpx;
|
||||
line-height: 48rpx;
|
||||
text-align: center;
|
||||
background: #5147ff;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.reset-btn:disabled {
|
||||
background-color: #cccccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user