feat(login): CASSIC 红金风登录页 + 玻璃按钮欢迎页

需求:
1. 默认访问 /metalinfo 未登录时跳到 CASSIC 风格登录页
2. 登录页支持两种模式: 账号密码登录 / 统一身份登录(CAS)
3. 登录成功跳转 /welcome (原蓝色登录页布局,但右侧表单换成玻璃风"立即体验"按钮)

变更:
- 新增 chat_web_front/src/views/welcome/index.vue
  - 复用现有蓝色 Waves + projectLogo + "聚尖端之力" 文案
  - 玻璃磨砂按钮(backdrop-filter blur),圆角胶囊 + 圆形箭头
  - 点击 → router.push('/chat')
- 重写 chat_web_front/src/views/login/index.vue
  - CASSIC 红金背景图(cassicLoginBg.jpg, 1920x1080 by /Users/jayliu/gangyan/ui)
  - "登录注册中心" 标题 + 红色短分隔条
  - 账号/密码/验证码 三段式表单 (el-icon User/Lock/Stamp)
  - "短信验证登录" 链接切换到 SMS 模式
  - 主红登录按钮 + 次级"统一身份登录(CASSIC)"白底红边按钮
  - 登录成功后 router.push('/welcome')
  - CAS 回调 cas_token 处理保留
- chat_web_front/src/router/index.ts:
  - / → 默认重定向改为 /welcome (原来到 /chat)
  - 新增 /welcome 路由
- 复制 ui/微信图片_20260423163044_262_1531.jpg 到
  src/assets/images/login/cassicLoginBg.jpg

未做:
- SMS 验证码后端接口 (loginByTel) 仅留前端倒计时占位
- 图形验证码后端 (showCaptcha 默认关)
- 老素材 loginBg.png 仍由 welcome 页使用,未删

测试:
- 访问 /metalinfo → 未登录跳 /login (CASSIC 红金)
- 账号密码登录 / CAS 登录 → 跳 /welcome
- /welcome 点"立即体验" → /chat
This commit is contained in:
2026-05-07 16:17:10 +08:00
parent 846380879b
commit cc54b24a77
4 changed files with 414 additions and 162 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -7,13 +7,18 @@ const routes: RouteRecordRaw[] = [
{ {
path: '/', path: '/',
name: '/', name: '/',
redirect: '/chat' redirect: '/welcome'
}, },
{ {
path: '/login', path: '/login',
name: 'Login', name: 'Login',
component: () => import('@/views/login/index.vue'), component: () => import('@/views/login/index.vue'),
}, },
{
path: '/welcome',
name: 'Welcome',
component: () => import('@/views/welcome/index.vue'),
},
{ {
path: '/chat', path: '/chat',
name: 'Chat', name: 'Chat',

View File

@@ -1,215 +1,351 @@
<template> <template>
<div class="loginPage"> <div class="cassicLogin">
<Waves></Waves> <!-- 背景CASSIC logo + 红金渐变 + 波纹粒子图片直接做底 -->
<div> <div class="bgImage"></div>
<img :src="projectLogo" alt="" class="loginLogo" />
</div> <!-- 中央登录卡 -->
<div class="loginContent"> <div class="loginCard">
<div class="loginTitle"> <h2 class="title">登录注册中心</h2>
<div> <div class="titleBar"></div>
聚尖端之力创多维平台
<br />
<span class="loginInfo">
聚合科技动能扩展创新疆界引领行业跃迁升级
</span>
</div>
</div>
<div class="loginOperate">
<img
:src="projectLogo2"
class="logoImg"
alt=""
/>
<el-form <el-form
ref="ruleFormRef" ref="formRef"
label-position="top" :model="form"
label-width="auto"
:model="ruleForm"
:rules="rules" :rules="rules"
style="width: 20vw; margin-top: 5%"
hide-required-asterisk hide-required-asterisk
class="form"
@submit.prevent
> >
<el-form-item label="手机号" prop="tel"> <!-- 账号 -->
<el-input v-model.trim="ruleForm.tel" /> <el-form-item prop="tel">
</el-form-item> <el-input
<el-form-item label="密码" prop="password"> v-model.trim="form.tel"
<el-input v-model.trim="ruleForm.password" show-password /> :placeholder="loginMode === 'sms' ? '请输入手机号' : '请输入账号'"
</el-form-item> size="large"
<el-form-item prop="checked">
<el-checkbox v-model="ruleForm.checked" label="" size="large" />
<span
>勾选即代表您阅读并同意<a
target="_blank"
href="http://www.metalinfo.cn/agreement.html?pageId=c03923c64e6b4d0896488212054b1742"
style="color: #0969da"
>用户协议</a
></span
> >
<template #prefix>
<span class="iconBox"><el-icon><User /></el-icon></span>
</template>
</el-input>
</el-form-item> </el-form-item>
<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"
>
<template #prefix>
<span class="iconBox"><el-icon><Stamp /></el-icon></span>
</template>
</el-input>
<button
type="button"
class="captchaBtn"
:disabled="smsCountdown > 0"
@click="handleCaptcha"
>
{{ 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 <el-button
type="primary" class="primaryBtn"
style="width: 100%; margin-top: 20px" :loading="submitting"
@click="submitForm(ruleFormRef)" @click="handleSubmit"
:disabled="isDisabled"
> >
{{ loginTip }}
</el-button> </el-button>
</el-form-item>
<el-form-item> <!-- 统一身份登录 -->
<el-button <el-button class="casBtn" @click="goCasLogin">
style="width: 100%; background: #FF2500; color: #fff; border-color: #FF2500;" 统一身份登录CASSIC
@click="goCasLogin"
>
通过统一身份登录
</el-button> </el-button>
</el-form-item>
</el-form> </el-form>
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup lang='ts'>
import { computed, ref, onMounted, reactive } from "vue"; <script setup lang="ts">
import projectLogo from "@/assets/images/login/projectLogo-white.svg"; import { onMounted, reactive, ref } from "vue";
import projectLogo2 from "@/assets/images/login/projectLogo.svg"; import { useRouter } from "vue-router";
import type { FormInstance, FormRules } from "element-plus"; import type { FormInstance, FormRules } from "element-plus";
import { ElMessage } from "element-plus";
import { User, Lock, Stamp } from "@element-plus/icons-vue";
import { fetchVerify } from "@/api"; import { fetchVerify } from "@/api";
import { useAuthStore } from "@/store"; import { useAuthStore } from "@/store";
import { useRouter } from "vue-router";
import Waves from "../../components/Waves.vue";
interface RuleForm { interface LoginForm {
tel: string; tel: string;
password: string; password: string;
checked: boolean; captcha: string;
loginType: number; loginType: number;
} }
interface Token {
token: string; const form = reactive<LoginForm>({
refreshToken: string;
expiresIn: number;
}
const ruleForm = reactive<RuleForm>({
tel: "", tel: "",
password: "", password: "",
checked: true, captcha: "",
loginType: 3, loginType: 3,
}); });
const ruleFormRef = ref<FormInstance>();
const authStore = useAuthStore();
const router = useRouter();
const isDisabled = ref<Boolean>(false);
const loginTip = ref<string>("登录");
const validateChecked = (rule: any, value: any, callback: any) => { const formRef = ref<FormInstance>();
if (!value) { const router = useRouter();
callback(new Error("请勾选“用户协议”")); const authStore = useAuthStore();
} else { const submitting = ref(false);
callback(); const loginMode = ref<"password" | "sms">("password"); // password = 账号密码登录sms = 短信验证登录
} const showCaptcha = ref(false); // 账号密码模式下,可选图形验证码(暂未启用,留位)
}; const smsCountdown = ref(0);
const rules = reactive<FormRules<RuleForm>>({
tel: [{ required: true, message: "请输入手机号", trigger: "blur" }], const rules = reactive<FormRules<LoginForm>>({
tel: [{ required: true, message: "请输入账号 / 手机号", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }], password: [{ required: true, message: "请输入密码", trigger: "blur" }],
checked: {
required: true,
message: "请勾选“用户协议”",
trigger: "blur",
validator: validateChecked,
},
}); });
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return; const toggleMode = () => {
await formEl.validate((valid, fields) => { loginMode.value = loginMode.value === "password" ? "sms" : "password";
if (valid) { form.captcha = "";
isDisabled.value = true;
loginTip.value = "登录中...";
handleVerify();
} else {
console.log("error submit!", fields);
}
});
}; };
const handleVerify = async () => {
let countdownTimer: number | null = null;
const handleCaptcha = () => {
if (loginMode.value === "sms") {
if (!form.tel) {
ElMessage.warning("请先输入手机号");
return;
}
// 触发短信发送(后端接口待联调;先做倒计时占位)
smsCountdown.value = 60;
countdownTimer = window.setInterval(() => {
smsCountdown.value -= 1;
if (smsCountdown.value <= 0 && countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
}, 1000);
ElMessage.info("短信验证码功能尚未接入后端,先用账号密码模式登录");
} else {
ElMessage.info("图形验证码暂未启用");
}
};
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (!valid) return;
submitting.value = true;
try { try {
fetchVerify<Token>(ruleForm).then((res) => { const res = await fetchVerify<{ token: string }>(form);
if (res.code === 200) { if (res.code === 200) {
authStore.setToken(res.data.token); authStore.setToken(res.data.token);
router.push("/chat"); router.push("/welcome");
} else { } else {
isDisabled.value = false; ElMessage.error(res.msg || "登录失败");
loginTip.value = "登录"; }
ElMessage.error(res.msg); } finally {
submitting.value = false;
} }
}); });
} finally {
}
}; };
// 跳后端 CAS 入口;后端 302 跳 CAS 服务器
const goCasLogin = () => { const goCasLogin = () => {
window.location.href = `/chat_web_backend/cas/login`; window.location.href = `/chat_web_backend/cas/login`;
}; };
// 处理 CAS 回调URL 含 cas_token 参数 → 存 token 后跳首页 // CAS 回调URL 含 cas_token → 存 token + 跳 welcome
onMounted(() => { onMounted(() => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const casToken = params.get("cas_token"); const casToken = params.get("cas_token");
if (casToken) { if (casToken) {
authStore.setToken(casToken); authStore.setToken(casToken);
window.history.replaceState({}, document.title, window.location.pathname + window.location.hash.split("?")[0]); window.history.replaceState(
router.push("/chat"); {},
document.title,
window.location.pathname + window.location.hash.split("?")[0]
);
router.push("/welcome");
} }
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.loginPage { .cassicLogin {
position: relative;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background-image: url("../../assets/images/login/loginBg.png"); overflow: hidden;
background-size: cover;
display: flex; display: flex;
flex-direction: column; align-items: center;
justify-content: center;
} }
.loginLogo {
.bgImage {
position: absolute; position: absolute;
height: 6.1vh; inset: 0;
margin: 2rem; background-image: url("../../assets/images/login/cassicLoginBg.jpg");
background-size: cover;
background-position: center;
z-index: 0;
} }
.loginContent {
height: 100vh; .loginCard {
display: flex; position: relative;
justify-content: space-around;
align-items: center;
z-index: 1; z-index: 1;
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);
} }
.loginTitle {
.title {
margin: 0;
text-align: center;
font-size: 26px;
font-weight: 700; font-weight: 700;
font-size: 7vh; color: #c10b08;
color: white; letter-spacing: 2px;
display: flex;
flex-direction: column;
} }
.loginInfo {
font-weight: 300; .titleBar {
font-size: 2vh; width: 36px;
height: 3px;
background: #c10b08;
border-radius: 2px;
margin: 12px auto 28px;
} }
.loginOperate {
background: white; .form {
display: flex; :deep(.el-form-item) {
flex-direction: column; margin-bottom: 18px;
border-radius: 0.75rem; }
align-items: center;
padding: 4vh; :deep(.el-input__wrapper) {
.logoImg { border-radius: 4px;
height: 5vh; 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;
} }
} }
::v-deep .is-required .el-form-item__label::after {
content: "*"; .iconBox {
color: #ff0000; display: inline-flex;
margin-left: 4px; align-items: center;
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;
}
}
.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;
}
} }
</style> </style>

View File

@@ -0,0 +1,111 @@
<template>
<div class="welcomePage">
<Waves></Waves>
<img :src="projectLogo" alt="" class="welcomeLogo" />
<div class="welcomeContent">
<div class="welcomeTitle">
<div>
聚尖端之力创多维平台
<br />
<span class="welcomeInfo">
聚合科技动能扩展创新疆界引领行业跃迁升级
</span>
</div>
</div>
<div class="enterAction">
<button class="glassBtn" @click="goChat">
<span>立即体验</span>
<i class="arrow"></i>
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
import projectLogo from "@/assets/images/login/projectLogo-white.svg";
import Waves from "../../components/Waves.vue";
const router = useRouter();
const goChat = () => {
router.push("/chat");
};
</script>
<style scoped lang="scss">
.welcomePage {
width: 100vw;
height: 100vh;
background-image: url("../../assets/images/login/loginBg.png");
background-size: cover;
display: flex;
flex-direction: column;
}
.welcomeLogo {
position: absolute;
height: 6.1vh;
margin: 2rem;
}
.welcomeContent {
height: 100vh;
display: flex;
justify-content: space-around;
align-items: center;
z-index: 1;
}
.welcomeTitle {
font-weight: 700;
font-size: 7vh;
color: white;
display: flex;
flex-direction: column;
}
.welcomeInfo {
font-weight: 300;
font-size: 2vh;
}
.enterAction {
display: flex;
align-items: center;
justify-content: center;
}
.glassBtn {
display: flex;
align-items: center;
gap: 14px;
padding: 18px 44px;
font-size: 22px;
font-weight: 500;
color: #ffffff;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 999px;
cursor: pointer;
transition: all 0.25s ease;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.25),
0 8px 32px rgba(0, 0, 0, 0.18);
}
.glassBtn:hover {
background: rgba(255, 255, 255, 0.18);
border-color: rgba(255, 255, 255, 0.6);
transform: translateY(-1px);
}
.glassBtn:active {
transform: translateY(0);
}
.glassBtn .arrow {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
font-style: normal;
font-size: 18px;
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 50%;
}
</style>