добавил на страницы загрузок эффект матрицы
This commit is contained in:
@@ -2,109 +2,434 @@
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>WFL Admin Login</title>
|
||||
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<title>Вход</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f1115;
|
||||
--panel: #171a21;
|
||||
--panel-2: #1d222b;
|
||||
--border: #2b3240;
|
||||
--text: #e8ecf3;
|
||||
--muted: #9aa4b2;
|
||||
--accent: #4f8cff;
|
||||
--accent-hover: #3e78e6;
|
||||
--danger-bg: rgba(220, 53, 69, 0.14);
|
||||
--danger-border: rgba(220, 53, 69, 0.45);
|
||||
--info-bg: rgba(79, 140, 255, 0.14);
|
||||
--info-border: rgba(79, 140, 255, 0.4);
|
||||
--shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
||||
--radius: 16px;
|
||||
--bg: #000000;
|
||||
--green: #00ff66;
|
||||
--green-soft: #00cc55;
|
||||
--green-dim: rgba(0, 255, 102, 0.7);
|
||||
--panel: rgba(0, 20, 0, 0.22);
|
||||
--border: rgba(0, 255, 102, 0.22);
|
||||
--danger: #ff5f56;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: Arial, sans-serif;
|
||||
color: var(--green);
|
||||
font-family: "Courier New", monospace;
|
||||
}
|
||||
.card {
|
||||
|
||||
body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#matrixCanvas {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 28px;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
opacity: 0.22;
|
||||
}
|
||||
h1 { margin: 0 0 8px; font-size: 28px; }
|
||||
.subtitle { color: var(--muted); margin-bottom: 22px; }
|
||||
.alert {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.alert-error { background: var(--danger-bg); border-color: var(--danger-border); }
|
||||
.alert-info { background: var(--info-bg); border-color: var(--info-border); }
|
||||
label { display: block; font-size: 14px; margin-bottom: 8px; }
|
||||
input {
|
||||
|
||||
.screen {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
margin-bottom: 14px;
|
||||
padding: 0 12px;
|
||||
border-radius: 10px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
width: min(900px, 100%);
|
||||
min-height: 420px;
|
||||
padding: 32px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel-2);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 18, 0, 0.35) 0%,
|
||||
rgba(0, 0, 0, 0.72) 100%
|
||||
);
|
||||
box-shadow:
|
||||
0 0 24px rgba(0, 255, 102, 0.08),
|
||||
inset 0 0 30px rgba(0, 255, 102, 0.04);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
input:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(79, 140, 255, 0.15);
|
||||
|
||||
.terminal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
min-height: 46px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 8px rgba(0, 255, 102, 0.5);
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
margin-left: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--green-dim);
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.terminal-body {
|
||||
font-size: 28px;
|
||||
line-height: 1.8;
|
||||
text-shadow: 0 0 10px rgba(0, 255, 102, 0.18);
|
||||
}
|
||||
|
||||
.term-line {
|
||||
display: block;
|
||||
min-height: 1.8em;
|
||||
}
|
||||
|
||||
.prompt-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: var(--green);
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.input-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.fake-input {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.typed-value {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.caret {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
margin-left: 2px;
|
||||
color: var(--green);
|
||||
animation: blink 1s steps(1, end) infinite;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sys-message {
|
||||
margin-top: 18px;
|
||||
font-size: 16px;
|
||||
color: var(--green-dim);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.sys-error {
|
||||
color: var(--danger);
|
||||
text-shadow: 0 0 8px rgba(255, 95, 86, 0.25);
|
||||
}
|
||||
|
||||
.subtle {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.scanline::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0.02),
|
||||
rgba(255, 255, 255, 0.00) 3px
|
||||
);
|
||||
background-size: 100% 4px;
|
||||
opacity: 0.08;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 49% { opacity: 1; }
|
||||
50%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.terminal {
|
||||
padding: 22px;
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.terminal-body {
|
||||
font-size: 20px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
.sys-message {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
button:hover { background: var(--accent-hover); }
|
||||
.hint { color: var(--muted); font-size: 13px; margin-top: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>WFL Admin</h1>
|
||||
<div class="subtitle">Вход в административный интерфейс</div>
|
||||
<body class="scanline">
|
||||
<canvas id="matrixCanvas"></canvas>
|
||||
|
||||
{% if message %}
|
||||
<div class="alert alert-info">{{ message }}</div>
|
||||
{% endif %}
|
||||
<div class="screen">
|
||||
<div class="terminal">
|
||||
<div class="terminal-header">
|
||||
<span class="dot"></span>
|
||||
<span class="dot" style="opacity: 0.7;"></span>
|
||||
<span class="dot" style="opacity: 0.45;"></span>
|
||||
<div class="terminal-title">wfl access terminal</div>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-error">{{ error }}</div>
|
||||
{% endif %}
|
||||
<div class="terminal-body" id="terminalBody">
|
||||
<div class="term-line" id="welcomeLine"></div>
|
||||
|
||||
<form method="post" action="/login">
|
||||
<label for="username">Логин</label>
|
||||
<input id="username" name="username" type="text" autocomplete="username" required>
|
||||
<div class="term-line hidden" id="loginLine">
|
||||
<span class="prompt">login:</span>
|
||||
<span id="usernameText" class="typed-value"></span>
|
||||
<span id="usernameCaret" class="caret">█</span>
|
||||
<input id="usernameInput" class="fake-input" type="text" autocomplete="username" spellcheck="false">
|
||||
</div>
|
||||
|
||||
<label for="password">Пароль</label>
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required>
|
||||
<div class="term-line hidden" id="passwordLine">
|
||||
<span class="prompt">password:</span>
|
||||
<span id="passwordText" class="typed-value"></span>
|
||||
<span id="passwordCaret" class="caret hidden">█</span>
|
||||
<input id="passwordInput" class="fake-input" type="password" autocomplete="current-password" spellcheck="false">
|
||||
</div>
|
||||
|
||||
<button type="submit">Войти</button>
|
||||
</form>
|
||||
<div class="term-line hidden subtle" id="submitLine">
|
||||
authorizing...
|
||||
</div>
|
||||
|
||||
<div class="hint">Сессия автоматически завершится после 2 часов без активности.</div>
|
||||
{% if message %}
|
||||
<div class="sys-message">{{ message }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if error %}
|
||||
<div class="sys-message sys-error">{{ error }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="realLoginForm" method="post" action="/login" class="hidden">
|
||||
<input type="hidden" name="username" id="realUsername">
|
||||
<input type="hidden" name="password" id="realPassword">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const canvas = document.getElementById("matrixCanvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
let matrixAnimationId = null;
|
||||
let drops = [];
|
||||
const fontSize = 18;
|
||||
const chars = "アイウエオカキクケコサシスセソ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZЖФЛ<>+*#";
|
||||
let columns = 0;
|
||||
let lastTime = 0;
|
||||
const fps = 18;
|
||||
const interval = 1000 / fps;
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
columns = Math.floor(canvas.width / fontSize);
|
||||
drops = Array(columns).fill(1);
|
||||
}
|
||||
|
||||
function drawMatrix(time = 0) {
|
||||
if (time - lastTime < interval) {
|
||||
matrixAnimationId = requestAnimationFrame(drawMatrix);
|
||||
return;
|
||||
}
|
||||
|
||||
lastTime = time;
|
||||
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.12)";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.fillStyle = "#00ff66";
|
||||
ctx.font = fontSize + "px monospace";
|
||||
|
||||
for (let i = 0; i < drops.length; i++) {
|
||||
const text = chars[Math.floor(Math.random() * chars.length)];
|
||||
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
|
||||
|
||||
if (drops[i] * fontSize > canvas.height && Math.random() > 0.985) {
|
||||
drops[i] = 0;
|
||||
}
|
||||
|
||||
drops[i] += 0.65;
|
||||
}
|
||||
|
||||
matrixAnimationId = requestAnimationFrame(drawMatrix);
|
||||
}
|
||||
|
||||
resizeCanvas();
|
||||
drawMatrix();
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
|
||||
const welcomeText = "добро пожаловать в ЖФЛ";
|
||||
const typingSpeed = 75;
|
||||
|
||||
const welcomeLine = document.getElementById("welcomeLine");
|
||||
const loginLine = document.getElementById("loginLine");
|
||||
const passwordLine = document.getElementById("passwordLine");
|
||||
const submitLine = document.getElementById("submitLine");
|
||||
|
||||
const usernameInput = document.getElementById("usernameInput");
|
||||
const passwordInput = document.getElementById("passwordInput");
|
||||
|
||||
const usernameText = document.getElementById("usernameText");
|
||||
const passwordText = document.getElementById("passwordText");
|
||||
|
||||
const usernameCaret = document.getElementById("usernameCaret");
|
||||
const passwordCaret = document.getElementById("passwordCaret");
|
||||
|
||||
const realLoginForm = document.getElementById("realLoginForm");
|
||||
const realUsername = document.getElementById("realUsername");
|
||||
const realPassword = document.getElementById("realPassword");
|
||||
|
||||
let phase = "typing_welcome";
|
||||
|
||||
function focusCurrentInput() {
|
||||
if (phase === "login") {
|
||||
usernameInput.focus();
|
||||
} else if (phase === "password") {
|
||||
passwordInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function showLoginStep() {
|
||||
phase = "login";
|
||||
loginLine.classList.remove("hidden");
|
||||
usernameCaret.classList.remove("hidden");
|
||||
passwordCaret.classList.add("hidden");
|
||||
setTimeout(() => usernameInput.focus(), 30);
|
||||
}
|
||||
|
||||
function showPasswordStep() {
|
||||
phase = "password";
|
||||
passwordLine.classList.remove("hidden");
|
||||
usernameCaret.classList.add("hidden");
|
||||
passwordCaret.classList.remove("hidden");
|
||||
setTimeout(() => passwordInput.focus(), 30);
|
||||
}
|
||||
|
||||
function submitLogin() {
|
||||
phase = "submitting";
|
||||
passwordCaret.classList.add("hidden");
|
||||
submitLine.classList.remove("hidden");
|
||||
|
||||
realUsername.value = usernameInput.value.trim();
|
||||
realPassword.value = passwordInput.value;
|
||||
|
||||
setTimeout(() => {
|
||||
realLoginForm.submit();
|
||||
}, 350);
|
||||
}
|
||||
|
||||
function typeWelcome(index = 0) {
|
||||
if (index < welcomeText.length) {
|
||||
welcomeLine.textContent += welcomeText[index];
|
||||
setTimeout(() => typeWelcome(index + 1), typingSpeed);
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(showLoginStep, 220);
|
||||
}
|
||||
|
||||
usernameInput.addEventListener("input", function () {
|
||||
usernameText.textContent = usernameInput.value;
|
||||
});
|
||||
|
||||
passwordInput.addEventListener("input", function () {
|
||||
passwordText.textContent = "*".repeat(passwordInput.value.length);
|
||||
});
|
||||
|
||||
usernameInput.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
|
||||
if (!usernameInput.value.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
showPasswordStep();
|
||||
}
|
||||
});
|
||||
|
||||
passwordInput.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
|
||||
if (!usernameInput.value.trim() || !passwordInput.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitLogin();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", function () {
|
||||
focusCurrentInput();
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (phase === "typing_welcome" || phase === "submitting") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.activeElement === usernameInput || document.activeElement === passwordInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
focusCurrentInput();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key.length === 1 || event.key === "Backspace") {
|
||||
focusCurrentInput();
|
||||
}
|
||||
});
|
||||
|
||||
typeWelcome();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user