2026-01-16 22:04:12 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="login-container" :class="{'dark-mode': isDarkMode}">
|
|
|
|
|
|
<!-- 登录方式切换图标 -->
|
|
|
|
|
|
<div class="login-mode-toggle">
|
|
|
|
|
|
<Icon
|
|
|
|
|
|
v-if="isAccountLoginActive"
|
|
|
|
|
|
@click="toggleLoginMode"
|
2026-04-07 11:01:11 +08:00
|
|
|
|
icon="ant-design:qrcode-outlined"
|
2026-01-16 22:04:12 +08:00
|
|
|
|
:color="isDarkMode ? '#ffffff' : '#000000'"
|
|
|
|
|
|
size="50px"
|
|
|
|
|
|
class="login-toggle-icon"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Icon
|
|
|
|
|
|
v-else
|
|
|
|
|
|
@click="toggleLoginMode"
|
2026-04-07 11:01:11 +08:00
|
|
|
|
icon="ant-design:desktop-outlined"
|
2026-01-16 22:04:12 +08:00
|
|
|
|
: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"
|
2026-04-07 11:01:11 +08:00
|
|
|
|
:placeholder="t('auth.login.username_placeholder')"
|
2026-01-16 22:04:12 +08:00
|
|
|
|
clearable
|
|
|
|
|
|
:inline-message="true"
|
|
|
|
|
|
:readonly="isUsernameInputReadonly"
|
|
|
|
|
|
@click="isUsernameInputReadonly = false"
|
|
|
|
|
|
@blur="isUsernameInputReadonly = true"
|
|
|
|
|
|
class="login-input"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<Icon
|
2026-04-07 11:01:11 +08:00
|
|
|
|
icon="ant-design:user-outlined"
|
2026-01-16 22:04:12 +08:00
|
|
|
|
: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"
|
2026-04-07 11:01:11 +08:00
|
|
|
|
:placeholder="t('auth.login.password_placeholder')"
|
2026-01-16 22:04:12 +08:00
|
|
|
|
type="password"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
show-password
|
|
|
|
|
|
class="login-input"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<Icon
|
2026-04-07 11:01:11 +08:00
|
|
|
|
icon="ant-design:lock-outlined"
|
2026-01-16 22:04:12 +08:00
|
|
|
|
: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"
|
2026-04-07 11:01:11 +08:00
|
|
|
|
:placeholder="t('auth.login.mobile_placeholder')"
|
2026-01-16 22:04:12 +08:00
|
|
|
|
clearable
|
|
|
|
|
|
class="login-input"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<Icon
|
2026-04-07 11:01:11 +08:00
|
|
|
|
icon="ant-design:phone-outlined"
|
2026-01-16 22:04:12 +08:00
|
|
|
|
: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"
|
2026-04-07 11:01:11 +08:00
|
|
|
|
:placeholder="t('auth.login.code_placeholder')"
|
2026-01-16 22:04:12 +08:00
|
|
|
|
class="login-input"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<Icon
|
2026-04-07 11:01:11 +08:00
|
|
|
|
icon="ant-design:key-outlined"
|
2026-01-16 22:04:12 +08:00
|
|
|
|
: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>
|
|
|
|
|
|
|
2026-04-07 11:01:11 +08:00
|
|
|
|
<!-- 注册链接和忘记密码 -->
|
2026-01-16 22:04:12 +08:00
|
|
|
|
<div class="form-actions">
|
|
|
|
|
|
<el-button type="primary" link @click="switchToRegister" class="register-link">
|
2026-04-07 11:01:11 +08:00
|
|
|
|
{{ t("auth.login.no_account") }},{{ t("auth.login.to_register") }}
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="currentLoginType === 'username'"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
link
|
|
|
|
|
|
@click="switchToForgetPassword"
|
|
|
|
|
|
class="forget-password-link"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t("auth.login.resetpwd") }}
|
2026-01-16 22:04:12 +08:00
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 登录按钮 -->
|
|
|
|
|
|
<div class="login-button-container">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
class="login-button"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
@click="handleLoginSubmit"
|
|
|
|
|
|
:loading="isLoading"
|
|
|
|
|
|
>
|
2026-04-07 11:01:11 +08:00
|
|
|
|
{{ isLoading ? t("auth.login.logining") : t("auth.login.login") }}
|
2026-01-16 22:04:12 +08:00
|
|
|
|
</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">
|
2026-04-07 11:01:11 +08:00
|
|
|
|
<span class="agreement-link">{{ t("auth.register.user_agreement") }}</span>
|
2026-01-16 22:04:12 +08:00
|
|
|
|
</NuxtLink>
|
|
|
|
|
|
{{ t("and") }}
|
|
|
|
|
|
<NuxtLink :to="protocolUrls.privacyUrl" target="_blank">
|
2026-04-07 11:01:11 +08:00
|
|
|
|
<span class="agreement-link">{{ t("auth.register.privacy_agreement") }}</span>
|
2026-01-16 22:04:12 +08:00
|
|
|
|
</NuxtLink>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 微信扫码登录 -->
|
|
|
|
|
|
<div v-else class="wechat-login-container">
|
2026-04-07 11:01:11 +08:00
|
|
|
|
<div class="wechat-login-title">{{ t("auth.login.wechat_scan_login") }}</div>
|
2026-01-16 22:04:12 +08:00
|
|
|
|
<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>
|
2026-04-07 11:01:11 +08:00
|
|
|
|
<span @click="handleGenerateWechatQrCode" class="qrcode-refresh-link">{{ t("auth.login.click_refresh") }}</span>
|
2026-01-16 22:04:12 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="wechat-tip">
|
|
|
|
|
|
<span class="iconfont icon-weixin1 wechat-icon"></span>
|
2026-04-07 11:01:11 +08:00
|
|
|
|
<span class="wechat-text">{{ t("auth.login.wechat_scan_tip") }}</span>
|
2026-01-16 22:04:12 +08:00
|
|
|
|
</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";
|
2026-04-07 11:01:11 +08:00
|
|
|
|
import { useMemberStore } from "@/stores/member";
|
2026-01-16 22:04:12 +08:00
|
|
|
|
import QRCode from "qrcode";
|
|
|
|
|
|
import type { FormInstance } from "element-plus";
|
2026-04-07 11:01:11 +08:00
|
|
|
|
import { Icon } from '~/components/icon'
|
|
|
|
|
|
import {t}from "~/composables/lang";
|
|
|
|
|
|
import { ElMessage } from "element-plus";
|
|
|
|
|
|
import validate from "~/utils/validate";
|
2026-01-16 22:04:12 +08:00
|
|
|
|
|
|
|
|
|
|
// Store 实例
|
|
|
|
|
|
const memberStore = useMemberStore();
|
|
|
|
|
|
const configStore = useConfigStore();
|
|
|
|
|
|
|
|
|
|
|
|
// 暗黑模式状态
|
|
|
|
|
|
const isDarkMode = computed(() => {
|
2026-04-07 11:01:11 +08:00
|
|
|
|
const root = document.querySelector('html')
|
|
|
|
|
|
return root?.classList.contains('dark') || false
|
2026-01-16 22:04:12 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 协议链接配置
|
|
|
|
|
|
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) {
|
2026-04-07 11:01:11 +08:00
|
|
|
|
types.push({ type: "username", title: t("auth.login.username_login") });
|
2026-01-16 22:04:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (configStore.login.is_mobile) {
|
2026-04-07 11:01:11 +08:00
|
|
|
|
types.push({ type: "mobile", title: t("auth.login.mobile_login") });
|
2026-01-16 22:04:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
currentLoginType.value = types[0] ? types[0].type : "";
|
|
|
|
|
|
return types;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const formRules = computed(() => ({
|
|
|
|
|
|
username: {
|
|
|
|
|
|
required: currentLoginType.value === "username",
|
2026-04-07 11:01:11 +08:00
|
|
|
|
message: t("auth.login.username_placeholder"),
|
2026-01-16 22:04:12 +08:00
|
|
|
|
trigger: ["blur", "change"],
|
|
|
|
|
|
},
|
|
|
|
|
|
password: {
|
|
|
|
|
|
required: currentLoginType.value === "username",
|
2026-04-07 11:01:11 +08:00
|
|
|
|
message: t("auth.login.password_placeholder"),
|
2026-01-16 22:04:12 +08:00
|
|
|
|
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);
|
|
|
|
|
|
},
|
2026-04-07 11:01:11 +08:00
|
|
|
|
message: t("auth.login.mobile_error"),
|
2026-01-16 22:04:12 +08:00
|
|
|
|
trigger: ["blur"],
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
mobile_code: {
|
|
|
|
|
|
required: currentLoginType.value === "mobile",
|
2026-04-07 11:01:11 +08:00
|
|
|
|
message: t("auth.login.sms_code_placeholder"),
|
2026-01-16 22:04:12 +08:00
|
|
|
|
trigger: ["change"],
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 生命周期
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
clearTimeout(qrCodeTimer);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化配置
|
2026-04-07 11:01:11 +08:00
|
|
|
|
configStore.getWebGroupConfigs();
|
2026-01-16 22:04:12 +08:00
|
|
|
|
|
|
|
|
|
|
// 登录模式切换
|
|
|
|
|
|
const toggleLoginMode = () => {
|
|
|
|
|
|
if (!isAccountLoginActive.value) {
|
2026-04-07 11:01:11 +08:00
|
|
|
|
isAccountLoginActive.value = !isAccountLoginActive.value;
|
2026-01-16 22:04:12 +08:00
|
|
|
|
clearTimeout(qrCodeTimer);
|
2026-04-07 11:01:11 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 检查是否开启微信登录
|
|
|
|
|
|
if (!configStore.login?.is_wechat) {
|
|
|
|
|
|
ElMessage.error(t('auth.login.wechat_login_not_available'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
isAccountLoginActive.value = !isAccountLoginActive.value;
|
|
|
|
|
|
handleGenerateWechatQrCode();
|
2026-01-16 22:04:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 微信扫码登录相关方法
|
|
|
|
|
|
const handleGenerateWechatQrCode = async () => {
|
|
|
|
|
|
try {
|
2026-04-07 11:01:11 +08:00
|
|
|
|
const data = await generateWechatQrCode() as any;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
wechatQrCode.value.key = data.scene_id;
|
2026-01-16 22:04:12 +08:00
|
|
|
|
|
2026-04-07 11:01:11 +08:00
|
|
|
|
if (data.qr_code_url) {
|
|
|
|
|
|
const qrCodeUrl = await QRCode.toDataURL(data.qr_code_url, {
|
2026-01-16 22:04:12 +08:00
|
|
|
|
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 - 扫码登录密钥
|
|
|
|
|
|
*/
|
2026-04-07 11:01:11 +08:00
|
|
|
|
const checkScan = (scene_id: string) => {
|
|
|
|
|
|
checkWechatScanStatus({ scene_id })
|
|
|
|
|
|
.then((data: any) => {
|
2026-01-16 22:04:12 +08:00
|
|
|
|
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) => {
|
2026-04-07 11:01:11 +08:00
|
|
|
|
if (!loginData.access_token) {
|
2026-01-16 22:04:12 +08:00
|
|
|
|
useCookie("openId").value = loginData.openid;
|
|
|
|
|
|
navigateTo("/auth/bind");
|
|
|
|
|
|
memberStore.logClose();
|
|
|
|
|
|
} else {
|
2026-04-07 11:01:11 +08:00
|
|
|
|
memberStore.setToken(loginData.access_token, loginData.refresh_token || '');
|
2026-01-16 22:04:12 +08:00
|
|
|
|
memberStore.logClose();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 表单处理方法
|
|
|
|
|
|
const handleLoginSubmit = async () => {
|
|
|
|
|
|
const isValid = await formRef.value?.validate();
|
|
|
|
|
|
if (!isValid) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (configStore.login.agreement_show && !isAgreeProtocol.value) {
|
2026-04-07 11:01:11 +08:00
|
|
|
|
ElMessage.error(t("auth.login.is_agree_tips"));
|
2026-01-16 22:04:12 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isLoading.value) return;
|
|
|
|
|
|
isLoading.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const loginMethod = currentLoginType.value === "username" ? authenticateUser : authenticateWithMobile;
|
2026-04-07 11:01:11 +08:00
|
|
|
|
const data = await loginMethod(formData) as any;
|
2026-01-16 22:04:12 +08:00
|
|
|
|
|
2026-04-07 11:01:11 +08:00
|
|
|
|
await memberStore.setToken(data?.access_token || '', data?.refresh_token || '');
|
2026-01-16 22:04:12 +08:00
|
|
|
|
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");
|
|
|
|
|
|
};
|
2026-04-07 11:01:11 +08:00
|
|
|
|
const switchToForgetPassword = () => {
|
|
|
|
|
|
emit("typeChange", "forget-password");
|
|
|
|
|
|
};
|
2026-01-16 22:04:12 +08:00
|
|
|
|
</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;
|
2026-04-07 11:01:11 +08:00
|
|
|
|
align-items: center;
|
2026-01-16 22:04:12 +08:00
|
|
|
|
margin-bottom: 20px;
|
2026-04-07 11:01:11 +08:00
|
|
|
|
gap: 10px;
|
2026-01-16 22:04:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 11:01:11 +08:00
|
|
|
|
.register-link,
|
|
|
|
|
|
.forget-password-link {
|
2026-01-16 22:04:12 +08:00
|
|
|
|
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>
|