<template>
|
<div class="login-container" style="">
|
<!-- 动态粒子背景 -->
|
<canvas ref="canvasRef" class="particles-bg"></canvas>
|
|
<!-- 科技感光效装饰 -->
|
<div class="tech-decoration">
|
<div class="light-line line-1"></div>
|
<div class="light-line line-2"></div>
|
<div class="light-line line-3"></div>
|
<div class="light-line line-4"></div>
|
<div class="light-line line-5"></div>
|
</div>
|
|
<!-- 主登录框 -->
|
<div class="login-box">
|
<!-- 右上角语言切换 -->
|
<div class="language-switcher">
|
<el-dropdown trigger="click" @command="changeLanguage">
|
<i :class="'iconfont icon-zhongyingwen'" class="toolBar-icon"></i>
|
<template #dropdown>
|
<el-dropdown-menu>
|
<el-dropdown-item
|
v-for="item in languageList"
|
:key="item.value"
|
:command="item.value"
|
:disabled="language === item.value"
|
>
|
{{ item.label }}
|
</el-dropdown-item>
|
</el-dropdown-menu>
|
</template>
|
</el-dropdown>
|
</div>
|
|
<!-- 主题切换 -->
|
<!-- <SwitchDark class="dark" /> -->
|
|
<!-- 左侧科技感展示区 -->
|
<div class="login-left">
|
<div class="holographic-display">
|
<div class="holographic-circle"></div>
|
<div class="holographic-line"></div>
|
<div class="holographic-grid"></div>
|
</div>
|
</div>
|
|
<!-- 右侧登录表单 -->
|
<div class="login-form">
|
<div class="login-logo">
|
<h2 class="logo-text glitch" :data-text="title">{{ title }}</h2>
|
<div class="logo-subtitle">{{ t("login.subtitle") }}</div>
|
</div>
|
<LoginForm />
|
|
<!-- 底部科技感装饰 -->
|
<div class="tech-footer">
|
<div class="scan-line"></div>
|
<div class="binary-code">
|
{{ t("login.copyright") }}<el-divider direction="vertical" />{{ t("login.version") }}: {{ version
|
}}<el-divider direction="vertical" />{{ t("login.time") }}: {{ buildTime }}
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
import LoginForm from "./components/LoginForm.vue";
|
// import SwitchDark from "@/components/SwitchDark/index.vue";
|
const title = import.meta.env.VITE_GLOB_APP_TITLE;
|
const version = import.meta.env.VITE_APP_VERSION || "1.1.4";
|
const buildTime = import.meta.env.VITE_APP_BUILD_TIME || "2025-09-04";
|
|
import { onMounted, ref, computed } from "vue";
|
import { useI18n } from "vue-i18n";
|
import { useGlobalStore } from "@/stores/modules/global";
|
import { LanguageType } from "@/stores/interface";
|
import { setLanguage } from "@/api/modules/hxzk/util/util";
|
|
const i18n = useI18n();
|
// 使用国际化
|
const { t } = useI18n();
|
|
// 语言切换功能
|
const globalStore = useGlobalStore();
|
const language = computed(() => globalStore.language);
|
|
const languageList = [
|
{ label: t("login.languages.zh"), value: "zh" },
|
{ label: t("login.languages.en"), value: "en" }
|
];
|
const changeLanguage = async (lang: string) => {
|
try {
|
// 先调用后端接口设置语言
|
const response = await setLanguage({ lang });
|
console.log("后端语言设置结果:", response);
|
|
// 然后设置前端语言
|
i18n.locale.value = lang;
|
globalStore.setGlobalState("language", lang as LanguageType);
|
// 刷新页面以应用所有翻译变化
|
window.location.reload();
|
} catch (error) {
|
console.error("设置语言失败:", error);
|
// 如果后端调用失败,仍然设置前端语言
|
i18n.locale.value = lang;
|
globalStore.setGlobalState("language", lang as LanguageType);
|
window.location.reload();
|
}
|
};
|
// 增强的粒子系统
|
class Particle {
|
x: number;
|
y: number;
|
size: number;
|
speedX: number;
|
speedY: number;
|
color: string;
|
alpha: number;
|
life: number;
|
maxLife: number;
|
angle: number;
|
spin: number;
|
trail: { x: number; y: number }[];
|
|
constructor(width: number, height: number) {
|
this.x = Math.random() * width;
|
this.y = Math.random() * height;
|
this.size = Math.random() * 3 + 1;
|
this.speedX = Math.random() * 2 - 1;
|
this.speedY = Math.random() * 2 - 1;
|
this.color = `hsl(${Math.random() * 60 + 200}, 100%, 50%)`;
|
this.alpha = Math.random() * 0.6 + 0.1;
|
this.life = 0;
|
this.maxLife = Math.random() * 300 + 100;
|
this.angle = Math.random() * Math.PI * 2;
|
this.spin = Math.random() * 0.2 - 0.1;
|
this.trail = [];
|
}
|
|
update(width: number, height: number) {
|
this.x += this.speedX;
|
this.y += this.speedY;
|
this.angle += this.spin;
|
|
// 边界检查
|
if (this.x < 0 || this.x > width) this.speedX *= -1;
|
if (this.y < 0 || this.y > height) this.speedY *= -1;
|
|
// 生命周期
|
this.life++;
|
if (this.life > this.maxLife) {
|
this.life = 0;
|
this.x = Math.random() * width;
|
this.y = Math.random() * height;
|
}
|
|
// 记录轨迹
|
this.trail.push({ x: this.x, y: this.y });
|
if (this.trail.length > 10) this.trail.shift();
|
}
|
|
draw(ctx: CanvasRenderingContext2D) {
|
// 绘制轨迹
|
ctx.beginPath();
|
ctx.moveTo(this.trail[0]?.x || this.x, this.trail[0]?.y || this.y);
|
for (let i = 1; i < this.trail.length; i++) {
|
ctx.lineTo(this.trail[i].x, this.trail[i].y);
|
}
|
ctx.strokeStyle = `${this.color}${Math.floor(this.alpha * 255)
|
.toString(16)
|
.padStart(2, "0")}`;
|
ctx.lineWidth = this.size / 2;
|
ctx.stroke();
|
|
// 绘制粒子
|
ctx.save();
|
ctx.translate(this.x, this.y);
|
ctx.rotate(this.angle);
|
ctx.fillStyle = this.color;
|
ctx.globalAlpha = this.alpha;
|
|
// 绘制六边形粒子
|
ctx.beginPath();
|
for (let i = 0; i < 6; i++) {
|
const angle = (i * 2 * Math.PI) / 6;
|
ctx.lineTo(Math.cos(angle) * this.size, Math.sin(angle) * this.size);
|
}
|
ctx.closePath();
|
ctx.fill();
|
|
ctx.restore();
|
}
|
}
|
|
// 连接线系统
|
class ConnectionSystem {
|
particles: Particle[];
|
width: number;
|
height: number;
|
|
constructor(particles: Particle[], width: number, height: number) {
|
this.particles = particles;
|
this.width = width;
|
this.height = height;
|
}
|
|
draw(ctx: CanvasRenderingContext2D) {
|
for (let i = 0; i < this.particles.length; i++) {
|
for (let j = i + 1; j < this.particles.length; j++) {
|
const p1 = this.particles[i];
|
const p2 = this.particles[j];
|
|
const dx = p1.x - p2.x;
|
const dy = p1.y - p2.y;
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
if (distance < 150) {
|
const opacity = 1 - distance / 150;
|
ctx.strokeStyle = `rgba(100, 200, 255, ${opacity * 0.2})`;
|
ctx.lineWidth = 0.5;
|
ctx.beginPath();
|
ctx.moveTo(p1.x, p1.y);
|
ctx.lineTo(p2.x, p2.y);
|
ctx.stroke();
|
}
|
}
|
}
|
}
|
}
|
|
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
|
onMounted(() => {
|
if (canvasRef.value) {
|
const canvas = canvasRef.value;
|
const ctx = canvas.getContext("2d");
|
if (!ctx) return;
|
|
const resizeCanvas = () => {
|
canvas.width = window.innerWidth;
|
canvas.height = window.innerHeight;
|
};
|
|
resizeCanvas();
|
window.addEventListener("resize", resizeCanvas);
|
|
// 创建粒子
|
const particles: Particle[] = [];
|
const particleCount = Math.floor((canvas.width * canvas.height) / 10000);
|
|
for (let i = 0; i < particleCount; i++) {
|
particles.push(new Particle(canvas.width, canvas.height));
|
}
|
|
// 创建连接系统
|
const connectionSystem = new ConnectionSystem(particles, canvas.width, canvas.height);
|
|
// 动画循环
|
const animate = () => {
|
ctx.fillStyle = "rgba(8, 12, 26, 0.2)";
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
// 更新和绘制粒子
|
particles.forEach(particle => {
|
particle.update(canvas.width, canvas.height);
|
particle.draw(ctx);
|
});
|
|
// 绘制连接线
|
connectionSystem.draw(ctx);
|
|
requestAnimationFrame(animate);
|
};
|
|
animate();
|
|
// 添加鼠标交互
|
let mouseX = 0;
|
let mouseY = 0;
|
|
window.addEventListener("mousemove", e => {
|
mouseX = e.clientX;
|
mouseY = e.clientY;
|
|
// 鼠标附近的粒子会被排斥
|
particles.forEach(particle => {
|
const dx = particle.x - mouseX;
|
const dy = particle.y - mouseY;
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
if (distance < 100) {
|
const force = (100 - distance) / 50;
|
particle.speedX += (dx / distance) * force;
|
particle.speedY += (dy / distance) * force;
|
}
|
});
|
});
|
}
|
});
|
</script>
|
|
<style scoped lang="scss">
|
.login-container {
|
position: relative;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
width: 100vw;
|
height: 100vh;
|
overflow: hidden;
|
background: linear-gradient(135deg, #0a192f 0%, #172a45 100%);
|
.particles-bg {
|
position: fixed;
|
top: 0;
|
left: 0;
|
z-index: 0;
|
width: 100%;
|
height: 100%;
|
}
|
.tech-decoration {
|
position: absolute;
|
z-index: 1;
|
width: 100%;
|
height: 100%;
|
pointer-events: none;
|
.light-line {
|
position: absolute;
|
background: rgb(0 150 255 / 10%);
|
box-shadow: 0 0 10px rgb(0 200 255 / 50%);
|
&.line-1 {
|
top: 0;
|
left: 20%;
|
width: 1px;
|
height: 100%;
|
animation: scan 8s linear infinite;
|
}
|
&.line-2 {
|
top: 0;
|
left: 50%;
|
width: 1px;
|
height: 100%;
|
animation: scan 10s linear infinite 2s;
|
}
|
&.line-3 {
|
top: 0;
|
left: 80%;
|
width: 1px;
|
height: 100%;
|
animation: scan 12s linear infinite 4s;
|
}
|
&.line-4 {
|
top: 30%;
|
left: 0;
|
width: 100%;
|
height: 1px;
|
animation: scan-horizontal 9s linear infinite 1s;
|
}
|
&.line-5 {
|
top: 70%;
|
left: 0;
|
width: 100%;
|
height: 1px;
|
animation: scan-horizontal 11s linear infinite 3s;
|
}
|
}
|
}
|
.login-box {
|
position: relative;
|
z-index: 2;
|
display: flex;
|
width: 80%;
|
max-width: 1200px;
|
height: 70%;
|
min-height: 600px;
|
overflow: hidden;
|
background: rgb(10 25 47 / 80%);
|
backdrop-filter: blur(10px);
|
border: 1px solid rgb(0 200 255 / 20%);
|
border-radius: 10px;
|
box-shadow: 0 0 30px rgb(0 150 255 / 30%);
|
animation: float 6s ease-in-out infinite;
|
|
// 语言切换器样式
|
.language-switcher {
|
position: absolute;
|
top: 20px;
|
right: 20px;
|
z-index: 3;
|
cursor: pointer;
|
.toolBar-icon {
|
font-size: 20px;
|
color: #00c8ff;
|
transition: all 0.3s;
|
&:hover {
|
color: #ffffff;
|
text-shadow: 0 0 10px rgb(0 200 255 / 80%);
|
}
|
}
|
}
|
.dark {
|
position: absolute;
|
top: 20px;
|
right: 60px; // 调整位置,避免与语言切换器重叠
|
z-index: 3;
|
}
|
.login-left {
|
position: relative;
|
display: flex;
|
flex: 1;
|
align-items: center;
|
justify-content: center;
|
overflow: hidden;
|
background: linear-gradient(135deg, rgb(10 25 47 / 70%) 0%, rgb(23 42 69 / 70%) 100%);
|
.holographic-display {
|
position: relative;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
width: 300px;
|
height: 300px;
|
.holographic-circle {
|
width: 200px;
|
height: 200px;
|
border: 2px solid rgb(0 200 255 / 50%);
|
border-radius: 50%;
|
box-shadow: 0 0 30px rgb(0 200 255 / 30%);
|
animation: pulse 4s ease-in-out infinite;
|
&::before,
|
&::after {
|
position: absolute;
|
content: "";
|
border: 1px solid rgb(0 200 255 / 30%);
|
border-radius: 50%;
|
box-shadow: 0 0 10px rgb(0 200 255 / 20%);
|
}
|
&::before {
|
width: 250px;
|
height: 250px;
|
animation: pulse 5s ease-in-out infinite 1s;
|
}
|
&::after {
|
width: 300px;
|
height: 300px;
|
animation: pulse 6s ease-in-out infinite 2s;
|
}
|
}
|
.holographic-line {
|
position: absolute;
|
width: 300px;
|
height: 2px;
|
background: linear-gradient(90deg, transparent, rgb(0 200 255 / 50%), transparent);
|
animation: rotate 20s linear infinite;
|
}
|
.holographic-grid {
|
position: absolute;
|
width: 300px;
|
height: 300px;
|
background:
|
linear-gradient(rgb(0 200 255 / 10%) 1px, transparent 1px),
|
linear-gradient(90deg, rgb(0 200 255 / 10%) 1px, transparent 1px);
|
background-size: 20px 20px;
|
animation: rotate 60s linear infinite reverse;
|
}
|
}
|
}
|
.login-form {
|
position: relative;
|
display: flex;
|
flex: 1;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
padding: 40px;
|
&::before {
|
position: absolute;
|
top: 0;
|
left: 0;
|
z-index: -1;
|
width: 100%;
|
height: 100%;
|
content: "";
|
background: linear-gradient(135deg, rgb(0 150 255 / 10%) 0%, rgb(0 200 255 / 5%) 100%);
|
}
|
.login-logo {
|
margin-bottom: 40px;
|
text-align: center;
|
.logo-text {
|
position: relative;
|
margin-bottom: 10px;
|
font-size: 2.5rem;
|
font-weight: 700;
|
color: #00c8ff;
|
text-shadow: 0 0 10px rgb(0 200 255 / 50%);
|
&.glitch {
|
animation: glitch 2s linear infinite;
|
}
|
&::before,
|
&::after {
|
position: absolute;
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 100%;
|
content: attr(data-text);
|
}
|
&::before {
|
left: 2px;
|
clip: rect(44px, 450px, 56px, 0);
|
text-shadow: -2px 0 #ff00c1;
|
animation: glitch-anim 5s infinite linear alternate-reverse;
|
}
|
&::after {
|
left: -2px;
|
clip: rect(44px, 450px, 56px, 0);
|
text-shadow: -2px 0 #00fff9;
|
animation: glitch-anim2 5s infinite linear alternate-reverse;
|
}
|
}
|
.logo-subtitle {
|
font-size: 1rem;
|
color: rgb(0 200 255 / 70%);
|
text-transform: uppercase;
|
letter-spacing: 4px;
|
animation: flicker 3s linear infinite;
|
}
|
}
|
.tech-footer {
|
position: absolute;
|
bottom: 0;
|
left: 0;
|
width: 100%;
|
padding: 15px;
|
background: rgb(0 0 0 / 20%);
|
border-top: 1px solid rgb(0 200 255 / 10%);
|
.scan-line {
|
position: absolute;
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 1px;
|
background: linear-gradient(90deg, transparent, rgb(0 200 255 / 70%), transparent);
|
animation: scanline 5s linear infinite;
|
}
|
.binary-code {
|
font-family: monospace;
|
font-size: 0.8rem;
|
color: rgb(0 200 255 / 50%);
|
white-space: nowrap;
|
animation: scrollText 20s linear infinite;
|
}
|
}
|
}
|
}
|
}
|
|
@keyframes float {
|
0%,
|
100% {
|
transform: translateY(0);
|
}
|
50% {
|
transform: translateY(-20px);
|
}
|
}
|
|
@keyframes pulse {
|
0%,
|
100% {
|
opacity: 0.7;
|
transform: scale(1);
|
}
|
50% {
|
opacity: 1;
|
transform: scale(1.05);
|
}
|
}
|
|
@keyframes rotate {
|
0% {
|
transform: rotate(0deg);
|
}
|
100% {
|
transform: rotate(360deg);
|
}
|
}
|
|
@keyframes scan {
|
0% {
|
transform: translateY(-100%);
|
}
|
100% {
|
transform: translateY(100%);
|
}
|
}
|
|
@keyframes scan-horizontal {
|
0% {
|
transform: translateX(-100%);
|
}
|
100% {
|
transform: translateX(100%);
|
}
|
}
|
|
@keyframes glitch {
|
0% {
|
transform: translate(0);
|
}
|
20% {
|
transform: translate(-2px, 2px);
|
}
|
40% {
|
transform: translate(-2px, -2px);
|
}
|
60% {
|
transform: translate(2px, 2px);
|
}
|
80% {
|
transform: translate(2px, -2px);
|
}
|
100% {
|
transform: translate(0);
|
}
|
}
|
|
@keyframes glitch-anim {
|
0% {
|
clip: rect(31px, 9999px, 94px, 0);
|
}
|
10% {
|
clip: rect(112px, 9999px, 76px, 0);
|
}
|
20% {
|
clip: rect(85px, 9999px, 77px, 0);
|
}
|
30% {
|
clip: rect(27px, 9999px, 97px, 0);
|
}
|
40% {
|
clip: rect(64px, 9999px, 98px, 0);
|
}
|
50% {
|
clip: rect(61px, 9999px, 85px, 0);
|
}
|
60% {
|
clip: rect(99px, 9999px, 114px, 0);
|
}
|
70% {
|
clip: rect(34px, 9999px, 115px, 0);
|
}
|
80% {
|
clip: rect(98px, 9999px, 129px, 0);
|
}
|
90% {
|
clip: rect(43px, 9999px, 96px, 0);
|
}
|
100% {
|
clip: rect(82px, 9999px, 64px, 0);
|
}
|
}
|
|
@keyframes glitch-anim2 {
|
0% {
|
clip: rect(65px, 9999px, 119px, 0);
|
}
|
10% {
|
clip: rect(79px, 9999px, 66px, 0);
|
}
|
20% {
|
clip: rect(75px, 9999px, 103px, 0);
|
}
|
30% {
|
clip: rect(86px, 9999px, 114px, 0);
|
}
|
40% {
|
clip: rect(97px, 9999px, 129px, 0);
|
}
|
50% {
|
clip: rect(105px, 9999px, 57px, 0);
|
}
|
60% {
|
clip: rect(85px, 9999px, 87px, 0);
|
}
|
70% {
|
clip: rect(78px, 9999px, 47px, 0);
|
}
|
80% {
|
clip: rect(102px, 9999px, 122px, 0);
|
}
|
90% {
|
clip: rect(115px, 9999px, 37px, 0);
|
}
|
100% {
|
clip: rect(83px, 9999px, 131px, 0);
|
}
|
}
|
|
@keyframes flicker {
|
0%,
|
19.999%,
|
22%,
|
62.999%,
|
64%,
|
64.999%,
|
70%,
|
100% {
|
opacity: 1;
|
}
|
20%,
|
21.999%,
|
63%,
|
63.999%,
|
65%,
|
69.999% {
|
opacity: 0.4;
|
}
|
}
|
|
@keyframes scanline {
|
0% {
|
transform: translateX(-100%);
|
}
|
100% {
|
transform: translateX(100%);
|
}
|
}
|
|
@keyframes scrollText {
|
0% {
|
transform: translateX(100%);
|
}
|
100% {
|
transform: translateX(-100%);
|
}
|
}
|
</style>
|