2026-04-02 11:36:05 +08:00
|
|
|
|
<template>
|
2026-05-07 16:17:10 +08:00
|
|
|
|
<div class="cassicLogin">
|
|
|
|
|
|
<!-- 背景:CASSIC logo + 红金渐变 + 波纹粒子(图片直接做底) -->
|
|
|
|
|
|
<div class="bgImage"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 中央登录卡 -->
|
|
|
|
|
|
<div class="loginCard">
|
|
|
|
|
|
<h2 class="title">登录注册中心</h2>
|
|
|
|
|
|
<div class="titleBar"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form
|
|
|
|
|
|
ref="formRef"
|
|
|
|
|
|
:model="form"
|
|
|
|
|
|
:rules="rules"
|
|
|
|
|
|
hide-required-asterisk
|
|
|
|
|
|
class="form"
|
|
|
|
|
|
@submit.prevent
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 账号 -->
|
|
|
|
|
|
<el-form-item prop="tel">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model.trim="form.tel"
|
|
|
|
|
|
:placeholder="loginMode === 'sms' ? '请输入手机号' : '请输入账号'"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<span class="iconBox"><el-icon><User /></el-icon></span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-input>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 密码 -->
|
|
|
|
|
|
<el-form-item v-if="loginMode === 'password'" prop="password">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model.trim="form.password"
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
placeholder="请输入密码"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
show-password
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<span class="iconBox"><el-icon><Lock /></el-icon></span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-input>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 验证码(图形 / 短信) -->
|
|
|
|
|
|
<el-form-item prop="captcha" v-if="loginMode === 'sms' || showCaptcha">
|
|
|
|
|
|
<div class="captchaRow">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model.trim="form.captcha"
|
|
|
|
|
|
:placeholder="loginMode === 'sms' ? '请输入短信验证码' : '请输入验证码'"
|
|
|
|
|
|
size="large"
|
2026-04-02 11:36:05 +08:00
|
|
|
|
>
|
2026-05-07 16:17:10 +08:00
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<span class="iconBox"><el-icon><Stamp /></el-icon></span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-input>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="captchaBtn"
|
|
|
|
|
|
:disabled="smsCountdown > 0"
|
|
|
|
|
|
@click="handleCaptcha"
|
2026-05-07 14:58:53 +08:00
|
|
|
|
>
|
2026-05-07 16:17:10 +08:00
|
|
|
|
{{ smsCountdown > 0 ? `${smsCountdown}s` : (loginMode === 'sms' ? '获取验证码' : '看不清') }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 切换模式 -->
|
|
|
|
|
|
<div class="switchModeRow">
|
|
|
|
|
|
<a class="modeLink" @click="toggleMode">
|
|
|
|
|
|
{{ loginMode === 'password' ? '短信验证登录' : '账号密码登录' }}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 主登录按钮 -->
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
class="primaryBtn"
|
|
|
|
|
|
:loading="submitting"
|
|
|
|
|
|
@click="handleSubmit"
|
|
|
|
|
|
>
|
|
|
|
|
|
登 录
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 统一身份登录 -->
|
|
|
|
|
|
<el-button class="casBtn" @click="goCasLogin">
|
|
|
|
|
|
统一身份登录(CASSIC)
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</el-form>
|
2026-04-02 11:36:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-05-07 16:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { onMounted, reactive, ref } from "vue";
|
|
|
|
|
|
import { useRouter } from "vue-router";
|
2026-04-02 11:36:05 +08:00
|
|
|
|
import type { FormInstance, FormRules } from "element-plus";
|
2026-05-07 16:17:10 +08:00
|
|
|
|
import { ElMessage } from "element-plus";
|
|
|
|
|
|
import { User, Lock, Stamp } from "@element-plus/icons-vue";
|
2026-04-02 11:36:05 +08:00
|
|
|
|
import { fetchVerify } from "@/api";
|
|
|
|
|
|
import { useAuthStore } from "@/store";
|
|
|
|
|
|
|
2026-05-07 16:17:10 +08:00
|
|
|
|
interface LoginForm {
|
2026-04-02 11:36:05 +08:00
|
|
|
|
tel: string;
|
|
|
|
|
|
password: string;
|
2026-05-07 16:17:10 +08:00
|
|
|
|
captcha: string;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
loginType: number;
|
|
|
|
|
|
}
|
2026-05-07 16:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
const form = reactive<LoginForm>({
|
2026-04-02 11:36:05 +08:00
|
|
|
|
tel: "",
|
|
|
|
|
|
password: "",
|
2026-05-07 16:17:10 +08:00
|
|
|
|
captcha: "",
|
2026-04-02 11:36:05 +08:00
|
|
|
|
loginType: 3,
|
|
|
|
|
|
});
|
2026-05-07 16:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
const formRef = ref<FormInstance>();
|
2026-04-02 11:36:05 +08:00
|
|
|
|
const router = useRouter();
|
2026-05-07 16:17:10 +08:00
|
|
|
|
const authStore = useAuthStore();
|
|
|
|
|
|
const submitting = ref(false);
|
|
|
|
|
|
const loginMode = ref<"password" | "sms">("password"); // password = 账号密码登录;sms = 短信验证登录
|
|
|
|
|
|
const showCaptcha = ref(false); // 账号密码模式下,可选图形验证码(暂未启用,留位)
|
|
|
|
|
|
const smsCountdown = ref(0);
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-05-07 16:17:10 +08:00
|
|
|
|
const rules = reactive<FormRules<LoginForm>>({
|
|
|
|
|
|
tel: [{ required: true, message: "请输入账号 / 手机号", trigger: "blur" }],
|
2026-04-02 11:36:05 +08:00
|
|
|
|
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
|
|
|
|
|
|
});
|
2026-05-07 16:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
const toggleMode = () => {
|
|
|
|
|
|
loginMode.value = loginMode.value === "password" ? "sms" : "password";
|
|
|
|
|
|
form.captcha = "";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let countdownTimer: number | null = null;
|
|
|
|
|
|
const handleCaptcha = () => {
|
|
|
|
|
|
if (loginMode.value === "sms") {
|
|
|
|
|
|
if (!form.tel) {
|
|
|
|
|
|
ElMessage.warning("请先输入手机号");
|
|
|
|
|
|
return;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-05-07 16:17:10 +08:00
|
|
|
|
// 触发短信发送(后端接口待联调;先做倒计时占位)
|
|
|
|
|
|
smsCountdown.value = 60;
|
|
|
|
|
|
countdownTimer = window.setInterval(() => {
|
|
|
|
|
|
smsCountdown.value -= 1;
|
|
|
|
|
|
if (smsCountdown.value <= 0 && countdownTimer) {
|
|
|
|
|
|
clearInterval(countdownTimer);
|
|
|
|
|
|
countdownTimer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
ElMessage.info("短信验证码功能尚未接入后端,先用账号密码模式登录");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.info("图形验证码暂未启用");
|
|
|
|
|
|
}
|
2026-04-02 11:36:05 +08:00
|
|
|
|
};
|
2026-05-07 16:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
|
if (!formRef.value) return;
|
|
|
|
|
|
await formRef.value.validate(async (valid) => {
|
|
|
|
|
|
if (!valid) return;
|
|
|
|
|
|
submitting.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetchVerify<{ token: string }>(form);
|
2026-04-02 11:36:05 +08:00
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
authStore.setToken(res.data.token);
|
2026-05-07 16:17:10 +08:00
|
|
|
|
router.push("/welcome");
|
2026-04-02 11:36:05 +08:00
|
|
|
|
} else {
|
2026-05-07 16:17:10 +08:00
|
|
|
|
ElMessage.error(res.msg || "登录失败");
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-05-07 16:17:10 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
submitting.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-04-02 11:36:05 +08:00
|
|
|
|
};
|
2026-05-06 16:51:00 +08:00
|
|
|
|
|
2026-05-07 14:58:53 +08:00
|
|
|
|
const goCasLogin = () => {
|
|
|
|
|
|
window.location.href = `/chat_web_backend/cas/login`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-07 16:17:10 +08:00
|
|
|
|
// CAS 回调:URL 含 cas_token → 存 token + 跳 welcome
|
2026-05-07 20:22:57 +08:00
|
|
|
|
// 注意 hash 路由下 token 在 hash 里(#/login?cas_token=xxx),不在 location.search
|
2026-05-07 14:58:53 +08:00
|
|
|
|
onMounted(() => {
|
2026-05-07 20:22:57 +08:00
|
|
|
|
const hash = window.location.hash || "";
|
|
|
|
|
|
const qIdx = hash.indexOf("?");
|
|
|
|
|
|
const queryStr = qIdx >= 0 ? hash.substring(qIdx + 1) : window.location.search.replace(/^\?/, "");
|
|
|
|
|
|
const params = new URLSearchParams(queryStr);
|
2026-05-07 14:58:53 +08:00
|
|
|
|
const casToken = params.get("cas_token");
|
|
|
|
|
|
if (casToken) {
|
|
|
|
|
|
authStore.setToken(casToken);
|
2026-05-07 20:22:57 +08:00
|
|
|
|
// 清掉 URL 上的 cas_token 参数
|
2026-05-07 16:17:10 +08:00
|
|
|
|
window.history.replaceState(
|
|
|
|
|
|
{},
|
|
|
|
|
|
document.title,
|
2026-05-07 20:22:57 +08:00
|
|
|
|
window.location.pathname + (qIdx >= 0 ? hash.substring(0, qIdx) : window.location.hash)
|
2026-05-07 16:17:10 +08:00
|
|
|
|
);
|
|
|
|
|
|
router.push("/welcome");
|
2026-05-07 14:58:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-04-02 11:36:05 +08:00
|
|
|
|
</script>
|
2026-05-07 16:17:10 +08:00
|
|
|
|
|
2026-04-02 11:36:05 +08:00
|
|
|
|
<style scoped lang="scss">
|
2026-05-07 16:17:10 +08:00
|
|
|
|
.cassicLogin {
|
|
|
|
|
|
position: relative;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
width: 100vw;
|
|
|
|
|
|
height: 100vh;
|
2026-05-07 16:17:10 +08:00
|
|
|
|
overflow: hidden;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
display: flex;
|
2026-05-07 16:17:10 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-05-07 16:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
.bgImage {
|
2026-04-02 11:36:05 +08:00
|
|
|
|
position: absolute;
|
2026-05-07 16:17:10 +08:00
|
|
|
|
inset: 0;
|
|
|
|
|
|
background-image: url("../../assets/images/login/cassicLoginBg.jpg");
|
|
|
|
|
|
background-size: cover;
|
|
|
|
|
|
background-position: center;
|
|
|
|
|
|
z-index: 0;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-05-07 16:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
.loginCard {
|
|
|
|
|
|
position: relative;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
z-index: 1;
|
2026-05-07 16:17:10 +08:00
|
|
|
|
width: 460px;
|
|
|
|
|
|
padding: 40px 40px 36px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.85);
|
|
|
|
|
|
box-shadow: 0 12px 40px rgba(120, 0, 0, 0.18);
|
|
|
|
|
|
backdrop-filter: blur(8px);
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-05-07 16:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
.title {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 26px;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
font-weight: 700;
|
2026-05-07 16:17:10 +08:00
|
|
|
|
color: #c10b08;
|
|
|
|
|
|
letter-spacing: 2px;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-05-07 16:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
.titleBar {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 3px;
|
|
|
|
|
|
background: #c10b08;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
margin: 12px auto 28px;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-05-07 16:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
.form {
|
|
|
|
|
|
:deep(.el-form-item) {
|
|
|
|
|
|
margin-bottom: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-input__wrapper) {
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
box-shadow: 0 0 0 1px #f3d2cf inset;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-input__wrapper.is-focus) {
|
|
|
|
|
|
box-shadow: 0 0 0 1px #c10b08 inset;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.iconBox {
|
|
|
|
|
|
display: inline-flex;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
align-items: center;
|
2026-05-07 16:17:10 +08:00
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 26px;
|
|
|
|
|
|
height: 26px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: #c10b08;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-style: normal;
|
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
|
i {
|
|
|
|
|
|
font-style: normal;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-07 16:17:10 +08:00
|
|
|
|
|
|
|
|
|
|
.captchaRow {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
.el-input {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.captchaBtn {
|
|
|
|
|
|
flex: 0 0 110px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: #f3d2cf;
|
|
|
|
|
|
color: #c10b08;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.captchaBtn:hover:not(:disabled) {
|
|
|
|
|
|
background: #e8b9b6;
|
|
|
|
|
|
}
|
|
|
|
|
|
.captchaBtn:disabled {
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.switchModeRow {
|
|
|
|
|
|
margin: 4px 0 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.modeLink {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #c10b08;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.primaryBtn {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 48px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: #c10b08;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
letter-spacing: 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: #a30907;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.casBtn {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 44px;
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
margin-left: 0;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
border: 1px solid #c10b08;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.6);
|
|
|
|
|
|
color: #c10b08;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.85);
|
|
|
|
|
|
border-color: #a30907;
|
|
|
|
|
|
}
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|