initial commit
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Icon',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: '18px',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#000000',
|
||||
},
|
||||
attr: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup(props:any) {
|
||||
// 添加isExternal函数定义
|
||||
const isExternal = (path: string): boolean => {
|
||||
return /^(https?:|mailto:|tel:)/.test(path)
|
||||
}
|
||||
|
||||
const iconStyle = computed((): CSSProperties => {
|
||||
const { size, color } = props
|
||||
// 修复:添加类型检查,确保size是字符串才能调用replace方法
|
||||
let fontSize = typeof size === 'string' ? `${size.replace('px', '')}px` : `${size}px`
|
||||
return {
|
||||
fontSize: fontSize,
|
||||
color: color,
|
||||
}
|
||||
})
|
||||
|
||||
if (props.name.startsWith('el-icon-')) {
|
||||
// 提取去掉前缀后的组件名
|
||||
const iconName = props.name.replace('el-icon-', '')
|
||||
return () =>
|
||||
h(resolveComponent('el-icon'), { class: [props.name, 'icon'], style: iconStyle.value, ...props.attr }, () =>
|
||||
h(resolveComponent(iconName))
|
||||
)
|
||||
} else if (props.name.startsWith('local-') || isExternal(props.name)) {
|
||||
const name = props.name.replace('local-', '')
|
||||
return () => h(resolveComponent('nuxt-icon'), { class: [props.name, 'icon'], name: name, style: iconStyle.value, ...props.attr })
|
||||
} else {
|
||||
// 处理fa前缀和其他普通图标
|
||||
return () => h('i', { class: [props.name, 'icon'], style: iconStyle.value, ...props.attr })
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="loginPopup">
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
align-center
|
||||
width="450"
|
||||
:before-close="beforeClose"
|
||||
custom-class="login !rounded-[var(--rounded-big)]"
|
||||
:show-close="false"
|
||||
append-to-body
|
||||
>
|
||||
<login v-if="type === 'login' && dialogVisible" @typeChange="typeChange" />
|
||||
<register v-if="type === 'register' && dialogVisible" @typeChange="typeChange" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from "vue";
|
||||
import login from "./login.vue";
|
||||
import register from "./register.vue";
|
||||
|
||||
const type = ref("login");
|
||||
|
||||
const memberStore = useMemberStore();
|
||||
//弹框状态
|
||||
const dialogVisible = computed(() => {
|
||||
return memberStore.loginPopup;
|
||||
});
|
||||
//弹框关闭
|
||||
const beforeClose = (next: any) => {
|
||||
memberStore.logClose();
|
||||
type.value = "login";
|
||||
next();
|
||||
};
|
||||
const handleClose = () => {
|
||||
memberStore.logClose();
|
||||
type.value = "login";
|
||||
};
|
||||
//判断当前是登录还是注册
|
||||
const typeChange = (val: any) => {
|
||||
type.value = val;
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.login .el-dialog__header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.login .el-dialog__body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.login .el-dialog__headerbtn {
|
||||
z-index: 99;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-form-item) {
|
||||
.el-input__wrapper {
|
||||
box-shadow: unset !important;
|
||||
border-radius: 0;
|
||||
&.is-focus {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-error {
|
||||
.el-input__wrapper {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-form-item__error) {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.text-color {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,649 @@
|
||||
<template>
|
||||
<div class="login-container" :class="{'dark-mode': isDarkMode}">
|
||||
<!-- 登录方式切换图标 -->
|
||||
<div class="login-mode-toggle">
|
||||
<Icon
|
||||
v-if="isAccountLoginActive"
|
||||
@click="toggleLoginMode"
|
||||
name="local-qr"
|
||||
:color="isDarkMode ? '#ffffff' : '#000000'"
|
||||
size="50px"
|
||||
class="login-toggle-icon"
|
||||
/>
|
||||
<Icon
|
||||
v-else
|
||||
@click="toggleLoginMode"
|
||||
name="local-pc"
|
||||
:color="isDarkMode ? '#ffffff' : '#000000'"
|
||||
size="50px"
|
||||
class="login-toggle-icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 账号密码登录表单 -->
|
||||
<div v-if="isAccountLoginActive" class="login-form-container">
|
||||
<div class="login-type-tabs">
|
||||
<div
|
||||
v-for="(item, index) in availableLoginTypes"
|
||||
:key="item.type"
|
||||
class="login-type-tab"
|
||||
:class="{
|
||||
'login-type-tab--active': currentLoginType === item.type,
|
||||
'login-type-tab--margin': index + 1 !== availableLoginTypes.length,
|
||||
}"
|
||||
@click="currentLoginType = item.type"
|
||||
>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
:model="formData"
|
||||
ref="formRef"
|
||||
:rules="formRules"
|
||||
:validate-on-rule-change="false"
|
||||
class="login-form"
|
||||
>
|
||||
<!-- 用户名密码登录 -->
|
||||
<div v-show="currentLoginType === 'username'" class="login-form-section">
|
||||
<el-form-item prop="username" class="form-item">
|
||||
<div class="input-container">
|
||||
<el-input
|
||||
v-model="formData.username"
|
||||
:placeholder="t('username_placeholder')"
|
||||
clearable
|
||||
:inline-message="true"
|
||||
:readonly="isUsernameInputReadonly"
|
||||
@click="isUsernameInputReadonly = false"
|
||||
@blur="isUsernameInputReadonly = true"
|
||||
class="login-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon
|
||||
name="el-icon-User"
|
||||
:color="isDarkMode ? '#ffffff' : '#000000'"
|
||||
class="input-prefix-icon"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password" class="form-item">
|
||||
<div class="input-container">
|
||||
<el-input
|
||||
v-model="formData.password"
|
||||
:placeholder="t('password_placeholder')"
|
||||
type="password"
|
||||
clearable
|
||||
show-password
|
||||
class="login-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon
|
||||
name="el-icon-Lock"
|
||||
:color="isDarkMode ? '#ffffff' : '#000000'"
|
||||
class="input-prefix-icon"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 手机号登录 -->
|
||||
<div v-show="currentLoginType === 'mobile'" class="login-form-section">
|
||||
<el-form-item prop="mobile" class="form-item">
|
||||
<div class="input-container">
|
||||
<el-input
|
||||
v-model="formData.mobile"
|
||||
:placeholder="t('mobile_placeholder')"
|
||||
clearable
|
||||
class="login-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon
|
||||
name="el-icon-Phone"
|
||||
:color="isDarkMode ? '#ffffff' : '#000000'"
|
||||
class="input-prefix-icon"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="mobile_code" class="form-item">
|
||||
<div class="input-container">
|
||||
<el-input
|
||||
v-model="formData.mobile_code"
|
||||
:placeholder="t('code_placeholder')"
|
||||
class="login-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon
|
||||
name="el-icon-Key"
|
||||
:color="isDarkMode ? '#ffffff' : '#000000'"
|
||||
class="input-prefix-icon"
|
||||
/>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<sms-code
|
||||
:mobile="formData.mobile"
|
||||
type="login"
|
||||
v-model="formData.mobile_key"
|
||||
@click="handleSendSmsCode"
|
||||
ref="smsCodeRef"
|
||||
></sms-code>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 注册链接 -->
|
||||
<div class="form-actions">
|
||||
<el-button type="primary" link @click="switchToRegister" class="register-link">
|
||||
{{ t("no_account") }},{{ t("to_register") }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<div class="login-button-container">
|
||||
<el-button
|
||||
type="primary"
|
||||
class="login-button"
|
||||
size="large"
|
||||
@click="handleLoginSubmit"
|
||||
:loading="isLoading"
|
||||
>
|
||||
{{ isLoading ? t("logining") : t("login") }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 协议同意 -->
|
||||
<div v-if="configStore.login.agreement_show" class="agreement-container">
|
||||
<span
|
||||
class="iconfont agreement-checkbox"
|
||||
:class="isAgreeProtocol ? 'icon-xuanze1' : 'icon-checkbox_nol'"
|
||||
@click="isAgreeProtocol = !isAgreeProtocol"
|
||||
></span>
|
||||
{{ t("agree_tips") }}
|
||||
<NuxtLink :to="protocolUrls.serviceUrl" target="_blank">
|
||||
<span class="agreement-link">{{ t("user_agreement") }}</span>
|
||||
</NuxtLink>
|
||||
{{ t("and") }}
|
||||
<NuxtLink :to="protocolUrls.privacyUrl" target="_blank">
|
||||
<span class="agreement-link">{{ t("privacy_agreement") }}</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 微信扫码登录 -->
|
||||
<div v-else class="wechat-login-container">
|
||||
<div class="wechat-login-title">{{ t("wechat_scan_login") }}</div>
|
||||
<div class="qrcode-container">
|
||||
<div class="qrcode-wrapper">
|
||||
<el-image v-if="wechatQrCode.url" :src="wechatQrCode.url" class="qrcode-image" />
|
||||
<div v-else class="qrcode-placeholder"></div>
|
||||
<div v-if="wechatQrCode.isExpired" class="qrcode-overlay">
|
||||
<span class="qrcode-error-text">{{ wechatQrCode.expiredMessage }}</span>
|
||||
<span @click="handleGenerateWechatQrCode" class="qrcode-refresh-link">{{ t("click_refresh") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wechat-tip">
|
||||
<span class="iconfont icon-weixin1 wechat-icon"></span>
|
||||
<span class="wechat-text">{{ t("wechat_scan_tip") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onUnmounted } from "vue";
|
||||
import { authenticateUser, authenticateWithMobile, authenticateWithWechat,generateWechatQrCode, checkWechatScanStatus } from "@/api/auth";
|
||||
import useConfigStore from "@/stores/config";
|
||||
import QRCode from "qrcode";
|
||||
import type { FormInstance } from "element-plus";
|
||||
|
||||
// Store 实例
|
||||
const memberStore = useMemberStore();
|
||||
const configStore = useConfigStore();
|
||||
|
||||
// 暗黑模式状态
|
||||
const isDarkMode = computed(() => {
|
||||
return configStore.isDarkMode;
|
||||
});
|
||||
|
||||
// 协议链接配置
|
||||
const protocolUrls = computed(() => {
|
||||
const basePath = location.pathname.includes("web") ? "/web/auth/agreement" : "/auth/agreement";
|
||||
return {
|
||||
serviceUrl: `${basePath}?key=service`,
|
||||
privacyUrl: `${basePath}?key=privacy`,
|
||||
};
|
||||
})as any;
|
||||
|
||||
// 登录模式状态
|
||||
const isAccountLoginActive = ref(true);
|
||||
const currentLoginType = ref("");
|
||||
let qrCodeTimer: any = null;
|
||||
|
||||
// 微信扫码登录状态
|
||||
const wechatQrCode = ref({
|
||||
url: "",
|
||||
key: "",
|
||||
isExpired: false,
|
||||
expiredMessage: t("qrcode_generate_failed"),
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const isLoading = ref(false);
|
||||
const isAgreeProtocol = ref(false);
|
||||
const isUsernameInputReadonly = ref(true);
|
||||
|
||||
const formData = reactive({
|
||||
username: "",
|
||||
password: "",
|
||||
mobile: "",
|
||||
mobile_code: "",
|
||||
mobile_key: "",
|
||||
});
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref<FormInstance>();
|
||||
const smsCodeRef = ref<AnyObject | null>(null);
|
||||
|
||||
// 计算属性
|
||||
const availableLoginTypes = computed(() => {
|
||||
const types = [];
|
||||
if (configStore.login.is_username) {
|
||||
types.push({ type: "username", title: t("username_login") });
|
||||
}
|
||||
if (configStore.login.is_mobile) {
|
||||
types.push({ type: "mobile", title: t("mobile_login") });
|
||||
}
|
||||
currentLoginType.value = types[0] ? types[0].type : "";
|
||||
return types;
|
||||
});
|
||||
|
||||
const formRules = computed(() => ({
|
||||
username: {
|
||||
required: currentLoginType.value === "username",
|
||||
message: t("username_placeholder"),
|
||||
trigger: ["blur", "change"],
|
||||
},
|
||||
password: {
|
||||
required: currentLoginType.value === "username",
|
||||
message: t("password_placeholder"),
|
||||
trigger: ["blur", "change"],
|
||||
},
|
||||
mobile: [
|
||||
{
|
||||
required: currentLoginType.value === "mobile",
|
||||
message: t("mobile_placeholder"),
|
||||
trigger: ["blur", "change"],
|
||||
},
|
||||
{
|
||||
validator(rule: any, value: string, callback: any) {
|
||||
if (currentLoginType.value !== "mobile") return true;
|
||||
return validate.mobile(value);
|
||||
},
|
||||
message: t("mobile_error"),
|
||||
trigger: ["blur"],
|
||||
},
|
||||
],
|
||||
mobile_code: {
|
||||
required: currentLoginType.value === "mobile",
|
||||
message: t("code_placeholder"),
|
||||
trigger: ["change"],
|
||||
},
|
||||
}));
|
||||
|
||||
// 生命周期
|
||||
onUnmounted(() => {
|
||||
clearTimeout(qrCodeTimer);
|
||||
});
|
||||
|
||||
// 初始化配置
|
||||
configStore.getLoginConfig();
|
||||
|
||||
// 登录模式切换
|
||||
const toggleLoginMode = () => {
|
||||
isAccountLoginActive.value = !isAccountLoginActive.value;
|
||||
if (!isAccountLoginActive.value) {
|
||||
handleGenerateWechatQrCode();
|
||||
} else {
|
||||
clearTimeout(qrCodeTimer);
|
||||
}
|
||||
};
|
||||
|
||||
// 微信扫码登录相关方法
|
||||
const handleGenerateWechatQrCode = async () => {
|
||||
try {
|
||||
const response = await generateWechatQrCode() as any;
|
||||
const data = response.data;
|
||||
|
||||
wechatQrCode.value.key = data.key;
|
||||
|
||||
if (data.url) {
|
||||
const qrCodeUrl = await QRCode.toDataURL(data.url, {
|
||||
errorCorrectionLevel: "L",
|
||||
margin: 0,
|
||||
width: 100,
|
||||
});
|
||||
|
||||
wechatQrCode.value.url = qrCodeUrl;
|
||||
wechatQrCode.value.isExpired = false;
|
||||
|
||||
setTimeout(() => {
|
||||
checkScan(wechatQrCode.value.key);
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
wechatQrCode.value.isExpired = true;
|
||||
wechatQrCode.value.expiredMessage = t("qrcode_generate_failed");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查微信扫码登录状态
|
||||
* @param key - 扫码登录密钥
|
||||
*/
|
||||
const checkScan = (key: string) => {
|
||||
checkWechatScanStatus({ key })
|
||||
.then((res: any) => {
|
||||
const data = res.data;
|
||||
switch (data.status) {
|
||||
case "wait":
|
||||
qrCodeTimer = setTimeout(() => {
|
||||
checkScan(wechatQrCode?.value.key);
|
||||
}, 1000);
|
||||
break;
|
||||
case "success":
|
||||
handleWechatLoginSuccess(data.login_data);
|
||||
break;
|
||||
case "fail":
|
||||
wechatQrCode.value.expiredMessage = data.fail_reason;
|
||||
wechatQrCode.value.isExpired = true;
|
||||
break;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
wechatQrCode.value.isExpired = true;
|
||||
wechatQrCode.value.expiredMessage = error.msg || t("scan_check_failed");
|
||||
});
|
||||
};
|
||||
|
||||
const handleWechatLoginSuccess = (loginData: any) => {
|
||||
if (!loginData.token) {
|
||||
useCookie("openId").value = loginData.openid;
|
||||
navigateTo("/auth/bind");
|
||||
memberStore.logClose();
|
||||
} else {
|
||||
memberStore.setToken(loginData.token);
|
||||
memberStore.logClose();
|
||||
}
|
||||
};
|
||||
|
||||
// 表单处理方法
|
||||
const handleLoginSubmit = async () => {
|
||||
const isValid = await formRef.value?.validate();
|
||||
if (!isValid) return;
|
||||
|
||||
if (configStore.login.agreement_show && !isAgreeProtocol.value) {
|
||||
ElMessage.error(t("is_agree_tips"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLoading.value) return;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const loginMethod = currentLoginType.value === "username" ? authenticateUser : authenticateWithMobile;
|
||||
const response = await loginMethod(formData) as any;
|
||||
|
||||
await memberStore.setToken(response?.data?.token || "");
|
||||
memberStore.logClose();
|
||||
} catch (error) {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendSmsCode = async () => {
|
||||
const isValid = await formRef.value?.validateField("mobile");
|
||||
if (isValid) {
|
||||
smsCodeRef.value?.send();
|
||||
}
|
||||
};
|
||||
|
||||
// 组件通信
|
||||
const emit = defineEmits(["typeChange"]);
|
||||
const switchToRegister = () => {
|
||||
emit("typeChange", "register");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&.dark-mode {
|
||||
background: var(--el-bg-color);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.login-mode-toggle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.login-toggle-icon {
|
||||
font-size: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-form-container {
|
||||
background: var(--el-bg-color);
|
||||
width: 100%;
|
||||
padding: 60px 30px;
|
||||
border-radius: var(--rounded-big);
|
||||
}
|
||||
|
||||
.login-type-tabs {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-type-tab {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 24px;
|
||||
font-family: "OppoSans-R";
|
||||
|
||||
&--active {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&--margin {
|
||||
margin-right: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
flex: 1;
|
||||
height: 50px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.login-input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
|
||||
.el-input__inner {
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-prefix-icon {
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.login-button-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
font-family: "OppoSans-M";
|
||||
}
|
||||
|
||||
.agreement-container {
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.agreement-checkbox {
|
||||
color: var(--el-color-primary);
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
color: var(--el-color-primary);
|
||||
margin: 0 4px;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary-light-3);
|
||||
}
|
||||
}
|
||||
|
||||
.wechat-login-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 60px 30px;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.wechat-login-title {
|
||||
font-size: 18px;
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
font-family: "OppoSans-R";
|
||||
}
|
||||
|
||||
.qrcode-container {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.qrcode-wrapper {
|
||||
padding: 20px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: var(--rounded-small);
|
||||
position: relative;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.qrcode-image {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.qrcode-placeholder {
|
||||
width: 202px;
|
||||
height: 202px;
|
||||
background: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.qrcode-overlay {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--el-bg-color-overlay);
|
||||
}
|
||||
|
||||
.qrcode-error-text {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.qrcode-refresh-link {
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: var(--el-color-primary);
|
||||
margin-top: 8px;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary-light-3);
|
||||
}
|
||||
}
|
||||
|
||||
.wechat-tip {
|
||||
margin-top: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wechat-icon {
|
||||
color: #00c22c;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.wechat-text {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-left: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,520 @@
|
||||
<template>
|
||||
<div class="register-container" :class="{'dark-mode': isDarkMode}">
|
||||
<div class="register-form-container">
|
||||
<div class="register-type-tabs">
|
||||
<div
|
||||
v-for="(item, index) in registerTypes"
|
||||
:key="item.type"
|
||||
class="register-type-tab"
|
||||
:class="{
|
||||
'register-type-tab--active': currentType === item.type,
|
||||
'register-type-tab--margin': index + 1 !== registerTypes.length
|
||||
}"
|
||||
@click="currentType = item.type"
|
||||
>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
:model="formData"
|
||||
ref="formRef"
|
||||
:rules="formRules"
|
||||
:validate-on-rule-change="false"
|
||||
class="register-form"
|
||||
>
|
||||
<!-- 用户名注册 -->
|
||||
<div v-show="currentType === 'username'" class="register-form-section">
|
||||
<el-form-item prop="username" class="form-item">
|
||||
<div class="input-container">
|
||||
<el-input
|
||||
v-model="formData.username"
|
||||
:placeholder="t('username_placeholder')"
|
||||
clearable
|
||||
:inline-message="true"
|
||||
:readonly="realNameInput"
|
||||
@click="realNameInput = false"
|
||||
@blur="realNameInput = true"
|
||||
class="register-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon
|
||||
name="el-icon-User"
|
||||
:color="isDarkMode ? '#ffffff' : '#000000'"
|
||||
class="input-prefix-icon"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password" class="form-item">
|
||||
<div class="input-container">
|
||||
<el-input
|
||||
v-model="formData.password"
|
||||
:placeholder="t('password_placeholder')"
|
||||
type="password"
|
||||
clearable
|
||||
show-password
|
||||
class="register-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon
|
||||
name="el-icon-Lock"
|
||||
:color="isDarkMode ? '#ffffff' : '#000000'"
|
||||
class="input-prefix-icon"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="confirmPassword" class="form-item">
|
||||
<div class="input-container">
|
||||
<el-input
|
||||
v-model="formData.confirmPassword"
|
||||
:placeholder="t('confirm_password_placeholder')"
|
||||
type="password"
|
||||
clearable
|
||||
show-password
|
||||
class="register-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon
|
||||
name="el-icon-Lock"
|
||||
:color="isDarkMode ? '#ffffff' : '#000000'"
|
||||
class="input-prefix-icon"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="captchaCode" class="form-item">
|
||||
<div class="input-container">
|
||||
<el-input
|
||||
v-model="formData.captchaCode"
|
||||
:placeholder="t('captcha_placeholder')"
|
||||
class="register-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon
|
||||
name="local-verify"
|
||||
:color="isDarkMode ? '#ffffff' : '#000000'"
|
||||
class="input-prefix-icon"
|
||||
/>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<div class="captcha-image-container">
|
||||
<el-image
|
||||
:src="captcha.image.value"
|
||||
class="captcha-image"
|
||||
@click="captcha.refresh()"
|
||||
></el-image>
|
||||
</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 手机号注册 -->
|
||||
<div v-show="currentType === 'mobile' || configStore.login.is_bind_mobile" class="register-form-section">
|
||||
<el-form-item prop="mobile" class="form-item">
|
||||
<div class="input-container">
|
||||
<el-input
|
||||
v-model="formData.mobile"
|
||||
:placeholder="t('mobile_placeholder')"
|
||||
clearable
|
||||
class="register-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon
|
||||
name="el-icon-Phone"
|
||||
:color="isDarkMode ? '#ffffff' : '#000000'"
|
||||
class="input-prefix-icon"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="mobileCode" class="form-item">
|
||||
<div class="input-container">
|
||||
<el-input
|
||||
v-model="formData.mobileCode"
|
||||
:placeholder="t('code_placeholder')"
|
||||
class="register-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon
|
||||
name="el-icon-Key"
|
||||
:color="isDarkMode ? '#ffffff' : '#000000'"
|
||||
class="input-prefix-icon"
|
||||
/>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<sms-code
|
||||
:mobile="formData.mobile"
|
||||
type="login"
|
||||
v-model="formData.mobileKey"
|
||||
@click="sendSmsCode"
|
||||
ref="smsCodeRef"
|
||||
></sms-code>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 登录链接 -->
|
||||
<div class="form-actions">
|
||||
<el-button type="primary" link @click="switchToLogin" class="login-link">
|
||||
{{ t('have_account') }},{{ t('to_login') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 注册按钮 -->
|
||||
<div class="register-button-container">
|
||||
<el-button
|
||||
type="primary"
|
||||
class="register-button"
|
||||
size="large"
|
||||
@click="handleRegister"
|
||||
:loading="isLoading"
|
||||
>
|
||||
{{ isLoading ? t('registering') : t('register') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 协议同意 -->
|
||||
<div
|
||||
v-if="configStore.login.agreement_show"
|
||||
class="agreement-container"
|
||||
>
|
||||
<span
|
||||
class="iconfont agreement-checkbox"
|
||||
:class="isAgreeChecked ? 'icon-xuanze1' : 'icon-checkbox_nol'"
|
||||
@click="toggleAgreement"
|
||||
></span>
|
||||
{{ t('register_agree_tips') }}
|
||||
<NuxtLink :to="protocolUrls.service" target="_blank">
|
||||
<span class="agreement-link">{{ t('user_agreement') }}</span>
|
||||
</NuxtLink>
|
||||
{{ t('and') }}
|
||||
<NuxtLink :to="protocolUrls.privacy" target="_blank">
|
||||
<span class="agreement-link">{{ t('privacy_agreement') }}</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { registerUser, registerWithMobile } from '@/api/auth'
|
||||
import useConfigStore from '@/stores/config'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
// 状态管理
|
||||
const memberStore = useMemberStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
// 暗黑模式状态
|
||||
const isDarkMode = computed(() => {
|
||||
return configStore.isDarkMode
|
||||
})
|
||||
|
||||
// 初始化配置
|
||||
configStore.getLoginConfig()
|
||||
|
||||
// 协议链接配置
|
||||
const protocolUrls = computed(() => {
|
||||
const basePath = location.pathname.includes('web') ? '/web' : ''
|
||||
return {
|
||||
service: `${basePath}/auth/agreement?key=service`,
|
||||
privacy: `${basePath}/auth/agreement?key=privacy`
|
||||
}
|
||||
})
|
||||
|
||||
// 注册类型配置
|
||||
const currentType = ref('')
|
||||
const registerTypes = computed(() => {
|
||||
const types = []
|
||||
if (configStore.login.is_username) {
|
||||
types.push({ type: 'username', title: t('username_register') })
|
||||
}
|
||||
if (configStore.login.is_mobile && !configStore.login.is_bind_mobile) {
|
||||
types.push({ type: 'mobile', title: t('mobile_register') })
|
||||
}
|
||||
currentType.value = types[0]?.type || ''
|
||||
return types
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const isLoading = ref(false)
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
mobile: '',
|
||||
mobileCode: '',
|
||||
mobileKey: '',
|
||||
captchaKey: '',
|
||||
captchaCode: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRef = ref<FormInstance>()
|
||||
const formRules = computed(() => ({
|
||||
username: {
|
||||
type: 'string',
|
||||
required: currentType.value === 'username',
|
||||
message: t('username_placeholder'),
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: currentType.value === 'username',
|
||||
message: t('password_placeholder'),
|
||||
trigger: ['blur', 'change']
|
||||
},
|
||||
confirmPassword: [
|
||||
{
|
||||
type: 'string',
|
||||
required: currentType.value === 'username',
|
||||
message: t('confirm_password_placeholder'),
|
||||
trigger: ['blur', 'change']
|
||||
},
|
||||
{
|
||||
validator(rule: any, value: string, callback: any) {
|
||||
return value === formData.password
|
||||
},
|
||||
message: t('confirm_password_error'),
|
||||
trigger: ['change', 'blur'],
|
||||
}
|
||||
],
|
||||
mobile: [
|
||||
{
|
||||
type: 'string',
|
||||
required: currentType.value === 'mobile' || configStore.login.is_bind_mobile,
|
||||
message: t('mobile_placeholder'),
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
{
|
||||
validator(rule: any, value: string, callback: any) {
|
||||
if (currentType.value !== 'mobile' && !configStore.login.is_bind_mobile) return true
|
||||
return validate.mobile(value)
|
||||
},
|
||||
message: t('mobile_error'),
|
||||
trigger: ['change', 'blur'],
|
||||
}
|
||||
],
|
||||
mobileCode: {
|
||||
type: 'string',
|
||||
required: currentType.value === 'mobile' || configStore.login.is_bind_mobile,
|
||||
message: t('code_placeholder'),
|
||||
trigger: ['blur', 'change']
|
||||
},
|
||||
captchaCode: {
|
||||
type: 'string',
|
||||
required: currentType.value === 'username',
|
||||
message: t('captcha_placeholder'),
|
||||
trigger: ['blur', 'change'],
|
||||
}
|
||||
}))
|
||||
|
||||
// 协议同意状态
|
||||
const isAgreeChecked = ref(false)
|
||||
const toggleAgreement = () => {
|
||||
isAgreeChecked.value = !isAgreeChecked.value
|
||||
}
|
||||
|
||||
// 注册处理
|
||||
const handleRegister = async () => {
|
||||
await formRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
// 检查协议同意
|
||||
if (configStore.login.agreement_show && !isAgreeChecked.value) {
|
||||
ElMessage.error(t('is_agree_tips'))
|
||||
return
|
||||
}
|
||||
|
||||
if (isLoading.value) return
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const registerApi = currentType.value === 'username' ? registerUser : registerWithMobile
|
||||
const res = await registerApi(formData)
|
||||
memberStore.setToken(res.data.token)
|
||||
memberStore.logClose()
|
||||
} catch (error) {
|
||||
isLoading.value = false
|
||||
captcha.refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 验证码功能
|
||||
const captcha = useCaptcha(formData)
|
||||
captcha.refresh()
|
||||
|
||||
// 短信验证码
|
||||
const smsCodeRef = ref<AnyObject | null>(null)
|
||||
const sendSmsCode = async () => {
|
||||
await formRef.value?.validateField('mobile', (valid) => {
|
||||
if (valid) {
|
||||
smsCodeRef.value?.send()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 切换到登录
|
||||
const emit = defineEmits(['typeChange'])
|
||||
const switchToLogin = () => {
|
||||
emit('typeChange', 'login')
|
||||
}
|
||||
|
||||
// 输入框状态
|
||||
const realNameInput = ref(true)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.register-container {
|
||||
width: 100%;
|
||||
|
||||
&.dark-mode {
|
||||
background: var(--el-bg-color);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.register-form-container {
|
||||
background: var(--el-bg-color);
|
||||
width: 100%;
|
||||
padding: 60px 30px;
|
||||
border-radius: var(--rounded-big);
|
||||
}
|
||||
|
||||
.register-type-tabs {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.register-type-tab {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 24px;
|
||||
font-family: 'OppoSans-R';
|
||||
|
||||
&--active {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&--margin {
|
||||
margin-right: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.register-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
flex: 1;
|
||||
height: 50px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.register-input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
|
||||
.el-input__inner {
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-prefix-icon {
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.captcha-image-container {
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.captcha-image {
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.register-button-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.register-button {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
font-family: 'OppoSans-M';
|
||||
}
|
||||
|
||||
.agreement-container {
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.agreement-checkbox {
|
||||
color: var(--el-color-primary);
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
color: var(--el-color-primary);
|
||||
margin: 0 4px;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary-light-3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<div class="sms-code-container">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:disabled="!canSendCode"
|
||||
@click="handleSendCode"
|
||||
class="sms-code-button"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="showCaptchaDialog"
|
||||
:title="t('captcha_title')"
|
||||
width="350px"
|
||||
:append-to-body="true"
|
||||
:align-center="true"
|
||||
class="captcha-dialog"
|
||||
>
|
||||
<el-form
|
||||
:model="captchaForm"
|
||||
ref="captchaFormRef"
|
||||
:rules="captchaRules"
|
||||
@submit.native.prevent
|
||||
class="captcha-form"
|
||||
>
|
||||
<el-form-item prop="captcha_code" class="captcha-form-item">
|
||||
<el-input
|
||||
v-model="captchaForm.captcha_code"
|
||||
:placeholder="t('captcha_placeholder')"
|
||||
class="captcha-input"
|
||||
>
|
||||
<template #suffix>
|
||||
<div class="captcha-image-wrapper">
|
||||
<el-image
|
||||
:src="captchaImage"
|
||||
class="captcha-image"
|
||||
@click="refreshCaptcha"
|
||||
></el-image>
|
||||
</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeCaptchaDialog" class="cancel-button">
|
||||
{{ t('cancel') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="isLoading"
|
||||
@click="confirmSendCode"
|
||||
class="confirm-button"
|
||||
>
|
||||
{{ t('confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { sendSmsVerificationCode } from '@/api/system'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
interface SmsCodeProps {
|
||||
mobile: string
|
||||
type: string
|
||||
modelValue: string
|
||||
}
|
||||
|
||||
interface CaptchaFormData {
|
||||
mobile: string
|
||||
captcha_code: string
|
||||
captcha_key: string
|
||||
type: string
|
||||
}
|
||||
|
||||
const props = defineProps<SmsCodeProps>()
|
||||
const emit = defineEmits(['update:modelValue', 'click'])
|
||||
|
||||
// 响应式数据
|
||||
const isLoading = ref(false)
|
||||
const showCaptchaDialog = ref(false)
|
||||
const canSendCode = ref(true)
|
||||
const buttonText = ref('')
|
||||
const seconds = ref(90)
|
||||
let countdownTimer: number | undefined = undefined
|
||||
|
||||
// 验证码相关
|
||||
const captchaForm = reactive<CaptchaFormData>({
|
||||
mobile: '',
|
||||
captcha_code: '',
|
||||
captcha_key: '',
|
||||
type: props.type
|
||||
})
|
||||
|
||||
const captchaImage = ref('')
|
||||
const captchaFormRef = ref<FormInstance>()
|
||||
|
||||
// 验证规则
|
||||
const captchaRules = reactive({
|
||||
captcha_code: {
|
||||
required: true,
|
||||
message: '请输入验证码',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const smsValue = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化按钮文本
|
||||
buttonText.value = t('get_sms_code')
|
||||
|
||||
// 发送短信验证码
|
||||
const sendSmsCode = async () => {
|
||||
captchaForm.mobile = props.mobile
|
||||
|
||||
if (!canSendCode.value) return
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await sendSmsVerificationCode(captchaForm) as any
|
||||
|
||||
if (response?.code !== -1) {
|
||||
smsValue.value = response.data.key
|
||||
startCountdown()
|
||||
closeCaptchaDialog()
|
||||
} else {
|
||||
refreshCaptcha()
|
||||
}
|
||||
} catch (error) {
|
||||
refreshCaptcha()
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 开始倒计时
|
||||
const startCountdown = () => {
|
||||
canSendCode.value = false
|
||||
countdownTimer = setInterval(() => {
|
||||
if (seconds.value > 0) {
|
||||
seconds.value -= 1
|
||||
buttonText.value = `${seconds.value}${t('sms_code_change_text')}`
|
||||
} else {
|
||||
resetCountdown()
|
||||
}
|
||||
}, 1000) as any
|
||||
}
|
||||
|
||||
// 重置倒计时
|
||||
const resetCountdown = () => {
|
||||
clearInterval(countdownTimer)
|
||||
seconds.value = 90
|
||||
canSendCode.value = true
|
||||
buttonText.value = t('get_sms_code')
|
||||
}
|
||||
|
||||
// 刷新验证码
|
||||
const refreshCaptcha = async () => {
|
||||
try {
|
||||
// 这里需要调用验证码API,暂时使用空字符串
|
||||
captchaImage.value = ''
|
||||
captchaForm.captcha_code = ''
|
||||
} catch (error) {
|
||||
console.error('刷新验证码失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理发送验证码点击
|
||||
const handleSendCode = () => {
|
||||
emit('click')
|
||||
showCaptchaDialog.value = true
|
||||
refreshCaptcha()
|
||||
}
|
||||
|
||||
// 确认发送验证码
|
||||
const confirmSendCode = async () => {
|
||||
await captchaFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
await sendSmsCode()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭验证码对话框
|
||||
const closeCaptchaDialog = () => {
|
||||
showCaptchaDialog.value = false
|
||||
captchaForm.captcha_code = ''
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
send: sendSmsCode
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sms-code-container {
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sms-code-button {
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.captcha-dialog {
|
||||
:deep(.el-dialog__header) {
|
||||
padding: 20px 20px 10px;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.captcha-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.captcha-input {
|
||||
:deep(.el-input__wrapper) {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-image-wrapper {
|
||||
padding: 5px 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.captcha-image {
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cancel-button,
|
||||
.confirm-button {
|
||||
min-width: 80px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user