style: improve frontend interaction details

This commit is contained in:
2025-12-23 23:28:03 +08:00
parent 658361d297
commit a892cbb044
5 changed files with 276 additions and 69 deletions

View File

@@ -49,9 +49,13 @@ const { titleSvg } = Astro.props;
// Displays
const displays = {
loginError: document.getElementById("login-error"),
qqError: document.getElementById("qq-error"),
regError: document.getElementById("reg-error"),
loginError: document.getElementById("login-password-error"),
loginUserError: document.getElementById("login-username-error"),
loginPassError: document.getElementById("login-password-error"),
qqError: document.getElementById("qq-id-error"),
regError: document.getElementById("reg-password-error"),
regUserError: document.getElementById("reg-username-error"),
regPassError: document.getElementById("reg-password-error"),
verifyCode: document.getElementById("verification-code-display"),
dashAvatar: document.getElementById(
"dashboard-avatar-img",
@@ -81,9 +85,13 @@ const { titleSvg } = Astro.props;
}
function clearErrors() {
if (displays.loginError) displays.loginError.classList.add("hidden");
if (displays.qqError) displays.qqError.classList.add("hidden");
if (displays.regError) displays.regError.classList.add("hidden");
[
displays.loginUserError,
displays.loginPassError,
displays.qqError,
displays.regUserError,
displays.regPassError,
].forEach((el) => el?.classList.add("hidden"));
}
function updateDashboard() {
@@ -109,14 +117,38 @@ const { titleSvg } = Astro.props;
switchView("dashboard");
}
function setLoading(btnId: string, isLoading: boolean, originalText: string) {
const btn = document.getElementById(btnId) as HTMLButtonElement | null;
if (!btn) return;
if (isLoading) {
btn.dataset.originalText = originalText;
btn.innerHTML = '<span class="btn-spinner"></span>';
btn.disabled = true;
} else {
btn.innerHTML = btn.dataset.originalText || originalText;
btn.disabled = false;
}
}
// --- Auth Logic ---
async function performLogin() {
const u = inputs.loginUser?.value;
const p = inputs.loginPass?.value;
if (!u || !p)
return showError(displays.loginError, "Please fill all fields");
clearErrors();
let hasError = false;
if (!u) {
showError(displays.loginUserError, "Username required");
hasError = true;
}
if (!p) {
showError(displays.loginPassError, "Password required");
hasError = true;
}
if (hasError) return;
setLoading("btn-login-submit", true, "Login");
try {
const res = await fetch(`${BACKEND_API_BASE}/login`, {
method: "POST",
@@ -129,18 +161,23 @@ const { titleSvg } = Astro.props;
if (inputs.loginUser) inputs.loginUser.value = "";
if (inputs.loginPass) inputs.loginPass.value = "";
} else {
showError(displays.loginError, data.error || "Login failed");
showError(displays.loginPassError, data.error || "Login failed");
}
} catch (e) {
showError(displays.loginError, "Network error");
showError(displays.loginPassError, "Network error");
} finally {
setLoading("btn-login-submit", false, "Login");
}
}
async function startQQAuth() {
const qq = inputs.qqId?.value;
if (!qq) return showError(displays.qqError, "Enter your QQ");
clearErrors();
if (!qq) return showError(displays.qqError, "Enter your QQ");
if (!/^\d{5,11}$/.test(qq))
return showError(displays.qqError, "QQ number must be 5-11 digits");
setLoading("btn-qq-next", true, "Next");
try {
const res = await fetch(`${BACKEND_API_BASE}/auth/qq/start`, {
method: "POST",
@@ -149,7 +186,8 @@ const { titleSvg } = Astro.props;
});
const data = await res.json();
if (data.code) {
if (displays.verifyCode) displays.verifyCode.textContent = data.code;
if (displays.verifyCode)
displays.verifyCode.textContent = `/login ${data.code}`;
currentQQ = qq;
switchView("verification");
startPolling(qq);
@@ -158,6 +196,8 @@ const { titleSvg } = Astro.props;
}
} catch (e) {
showError(displays.qqError, "Network error");
} finally {
setLoading("btn-qq-next", false, "Next");
}
}
@@ -188,9 +228,20 @@ const { titleSvg } = Astro.props;
async function performRegister() {
const u = inputs.regUser?.value;
const p = inputs.regPass?.value;
if (!u || !p) return showError(displays.regError, "Please fill all fields");
clearErrors();
let hasError = false;
if (!u) {
showError(displays.regUserError, "Username required");
hasError = true;
}
if (!p) {
showError(displays.regPassError, "Password required");
hasError = true;
}
if (hasError) return;
setLoading("btn-register-submit", true, "Register");
try {
const res = await fetch(`${BACKEND_API_BASE}/register`, {
method: "POST",
@@ -211,10 +262,12 @@ const { titleSvg } = Astro.props;
switchView("login");
}
} else {
showError(displays.regError, data.error || "Registration failed");
showError(displays.regPassError, data.error || "Registration failed");
}
} catch (e) {
showError(displays.regError, "Network error");
showError(displays.regPassError, "Network error");
} finally {
setLoading("btn-register-submit", false, "Register");
}
}
@@ -266,9 +319,20 @@ const { titleSvg } = Astro.props;
document
.getElementById("btn-qq-mode")
?.addEventListener("click", () => switchView("qqInput"));
document
.getElementById("btn-qq-next")
?.addEventListener("click", startQQAuth);
const btnQQNext = document.getElementById("btn-qq-next") as HTMLButtonElement;
if (btnQQNext) {
// Initial state
btnQQNext.disabled = true;
btnQQNext.addEventListener("click", startQQAuth);
}
if (inputs.qqId) {
inputs.qqId.addEventListener("input", () => {
const val = inputs.qqId.value;
const isValid = /^\d{5,11}$/.test(val);
if (btnQQNext) btnQQNext.disabled = !isValid;
});
}
document
.getElementById("btn-cancel-verify")
?.addEventListener("click", () => {
@@ -317,6 +381,25 @@ const { titleSvg } = Astro.props;
checkLoginState();
</script>
<style is:global>
.btn-spinner {
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
display: inline-block;
vertical-align: middle;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
<style>
.island-container {
display: grid;

View File

@@ -20,18 +20,26 @@ import qqIconRaw from "../../assets/icons/qq.svg?raw";
</svg>
</button>
<h2>Login</h2>
<input
type="text"
id="login-username"
placeholder="Username"
class="input-field"
>
<input
type="password"
id="login-password"
placeholder="Password"
class="input-field"
>
<div class="input-group">
<input
type="text"
id="login-username"
placeholder="Username"
class="input-field"
/>
<div id="login-username-error" class="input-error hidden"></div>
</div>
<div class="input-group">
<input
type="password"
id="login-password"
placeholder="Password"
class="input-field"
/>
<div id="login-password-error" class="input-error hidden"></div>
</div>
<button id="btn-login-submit" class="submit-btn">Login</button>
<div class="divider">
@@ -39,9 +47,8 @@ import qqIconRaw from "../../assets/icons/qq.svg?raw";
</div>
<button id="btn-qq-mode" class="qq-btn" title="Login with QQ">
<span set:html={qqIconRaw} style="display: flex; align-items: center;"/>
<span set:html={qqIconRaw} style="display: flex; align-items: center;" />
</button>
<p class="error-msg hidden" id="login-error"></p>
</div>
<style>
@@ -99,6 +106,11 @@ import qqIconRaw from "../../assets/icons/qq.svg?raw";
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.input-group {
width: 100%;
margin-bottom: 1rem;
}
.input-field {
width: 100%;
padding: 0.8rem 1rem;
@@ -111,7 +123,7 @@ import qqIconRaw from "../../assets/icons/qq.svg?raw";
transition:
border-color 0.2s,
background 0.2s;
margin-bottom: 1rem;
/* margin-bottom removed */
font-family:
monospace, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New";
@@ -125,6 +137,20 @@ import qqIconRaw from "../../assets/icons/qq.svg?raw";
color: rgba(255, 255, 255, 0.4);
}
/* Specific styles for input errors */
.input-error {
color: #ff3333; /* Stronger red */
font-size: 0.85rem;
margin-top: 6px; /* Space between input and error */
text-align: center;
font-weight: 500;
animation: fadeIn 0.3s ease;
}
/* Remove invalid style if present */
.error-msg {
display: none;
}
.submit-btn {
width: 100%;
padding: 1rem;
@@ -206,11 +232,6 @@ import qqIconRaw from "../../assets/icons/qq.svg?raw";
width: 40px;
height: 40px;
}
.error-msg {
color: #ff6b6b;
margin-top: 10px;
font-size: 0.9rem;
}
.fade-in {
animation: fadeIn 0.5s ease forwards;
}

View File

@@ -17,10 +17,20 @@
</button>
<h2>QQ Login</h2>
<p class="instruction">Enter your QQ id to start</p>
<input type="text" id="qq-id-input" placeholder="QQ" class="input-field">
<div class="input-group">
<input
type="text"
id="qq-id-input"
placeholder="QQ"
class="input-field"
minlength="5"
maxlength="11"
/>
<div id="qq-id-error" class="input-error hidden"></div>
</div>
<button id="btn-qq-next" class="submit-btn">Next</button>
<button class="back-text-btn" data-target="view-login">Back to Login</button>
<p class="error-msg hidden" id="qq-error"></p>
</div>
<style>
@@ -84,6 +94,11 @@
font-size: 0.9rem;
}
.input-group {
width: 100%;
margin-bottom: 1rem;
}
.input-field {
width: 100%;
padding: 0.8rem 1rem;
@@ -96,7 +111,7 @@
transition:
border-color 0.2s,
background 0.2s;
margin-bottom: 1rem;
/* margin-bottom removed */
font-family:
monospace, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New";
@@ -110,6 +125,20 @@
color: rgba(255, 255, 255, 0.4);
}
/* Specific styles for input errors */
.input-error {
color: #ff3333; /* Stronger red */
font-size: 0.85rem;
margin-top: 6px; /* Space between input and error */
text-align: center;
font-weight: 500;
animation: fadeIn 0.3s ease;
}
/* Remove invalid style if present */
.error-msg {
display: none;
}
.submit-btn {
width: 100%;
padding: 1rem;
@@ -153,12 +182,6 @@
.back-text-btn:hover {
color: white;
}
.error-msg {
color: #ff6b6b;
margin-top: 10px;
font-size: 0.9rem;
}
.fade-in {
animation: fadeIn 0.5s ease forwards;
}

View File

@@ -17,20 +17,27 @@
</button>
<h2>Complete Registration</h2>
<p class="instruction">Set up your username and password</p>
<input
type="text"
id="reg-username"
placeholder="Username"
class="input-field"
>
<input
type="password"
id="reg-password"
placeholder="Password"
class="input-field"
>
<div class="input-group">
<input
type="text"
id="reg-username"
placeholder="Username"
class="input-field"
/>
<div id="reg-username-error" class="input-error hidden"></div>
</div>
<div class="input-group">
<input
type="password"
id="reg-password"
placeholder="Password"
class="input-field"
/>
<div id="reg-password-error" class="input-error hidden"></div>
</div>
<button id="btn-register-submit" class="submit-btn">Register</button>
<p class="error-msg hidden" id="reg-error"></p>
</div>
<style>
@@ -94,6 +101,11 @@
font-size: 0.9rem;
}
.input-group {
width: 100%;
margin-bottom: 1rem;
}
.input-field {
width: 100%;
padding: 0.8rem 1rem;
@@ -106,7 +118,7 @@
transition:
border-color 0.2s,
background 0.2s;
margin-bottom: 1rem;
/* margin-bottom removed */
font-family:
monospace, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New";
@@ -120,6 +132,20 @@
color: rgba(255, 255, 255, 0.4);
}
/* Specific styles for input errors */
.input-error {
color: #ff3333; /* Stronger red */
font-size: 0.85rem;
margin-top: 6px; /* Space between input and error */
text-align: center;
font-weight: 500;
animation: fadeIn 0.3s ease;
}
/* Remove invalid style if present */
.error-msg {
display: none;
}
.submit-btn {
width: 100%;
padding: 1rem;
@@ -151,12 +177,6 @@
cursor: not-allowed;
}
.error-msg {
color: #ff6b6b;
margin-top: 10px;
font-size: 0.9rem;
}
.fade-in {
animation: fadeIn 0.5s ease forwards;
}

View File

@@ -94,8 +94,33 @@
border-radius: 12px;
border: 1px dashed rgba(255, 255, 255, 0.3);
margin: 1rem 0;
user-select: all;
user-select: none;
color: #7cfbff;
cursor: pointer;
}
.shake {
animation: shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
@keyframes shake {
10%,
90% {
transform: translate3d(-1px, 0, 0);
}
20%,
80% {
transform: translate3d(2px, 0, 0);
}
30%,
50%,
70% {
transform: translate3d(-4px, 0, 0);
}
40%,
60% {
transform: translate3d(4px, 0, 0);
}
}
.status-text {
@@ -143,3 +168,38 @@
}
}
</style>
<script>
const codeDisplay = document.querySelector(
"#verification-code-display",
) as HTMLElement;
const statusText = document.querySelector(".status-text") as HTMLElement;
if (codeDisplay && statusText) {
codeDisplay.addEventListener("click", async () => {
const code = codeDisplay.innerText;
if (!code || code === "......") return;
try {
await navigator.clipboard.writeText(code);
// Visual feedback
const originalStatus = statusText.innerText;
statusText.innerText = "Copied to clipboard!";
statusText.style.color = "#7cfbff";
codeDisplay.classList.remove("shake");
// Trigger reflow
void codeDisplay.offsetWidth;
codeDisplay.classList.add("shake");
setTimeout(() => {
statusText.innerText = originalStatus;
statusText.style.color = "";
}, 2000);
} catch (err) {
console.error("Failed to copy:", err);
}
});
}
</script>