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: '/',
name: '/',
redirect: '/chat'
redirect: '/welcome'
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
},
{
path: '/welcome',
name: 'Welcome',
component: () => import('@/views/welcome/index.vue'),
},
{
path: '/chat',
name: 'Chat',

View File

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

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>