feat: add avatar upload button to dashboard

This commit is contained in:
2025-12-29 00:16:45 +08:00
parent 98c80107e3
commit 45c865cef2
3 changed files with 137 additions and 5 deletions

View File

@@ -45,6 +45,7 @@ const { titleSvg } = Astro.props;
// Actually, let's keep state here. // Actually, let's keep state here.
let isLoggedIn = false; let isLoggedIn = false;
let currentQQ = ""; let currentQQ = "";
let currentUser = "";
let isPolling = false; let isPolling = false;
// DOM Helpers (Dynamic) // DOM Helpers (Dynamic)
@@ -128,7 +129,21 @@ const { titleSvg } = Astro.props;
function updateDashboard() { function updateDashboard() {
const displays = getDisplays(); const displays = getDisplays();
if (displays.dashAvatar) { if (displays.dashAvatar) {
if (currentQQ) { if (currentUser) {
displays.dashAvatar.src = `${BACKEND_API_BASE}/user/avatar/${currentUser}`;
// Add onerror fallback to QQ or default
displays.dashAvatar.onerror = () => {
if (currentQQ) {
displays.dashAvatar.src = `https://q.qlogo.cn/headimg_dl?dst_uin=${currentQQ}&spec=640&img_type=jpg`;
} else {
displays.dashAvatar.src =
"https://ui-avatars.com/api/?name=" +
(currentUser || "User") +
"&background=random";
}
displays.dashAvatar.onerror = null; // Prevent infinite loop
};
} else if (currentQQ) {
displays.dashAvatar.src = `https://q.qlogo.cn/headimg_dl?dst_uin=${currentQQ}&spec=640&img_type=jpg`; displays.dashAvatar.src = `https://q.qlogo.cn/headimg_dl?dst_uin=${currentQQ}&spec=640&img_type=jpg`;
} else { } else {
displays.dashAvatar.src = displays.dashAvatar.src =
@@ -139,6 +154,18 @@ const { titleSvg } = Astro.props;
function handleLoginSuccess(token: string, qq: string) { function handleLoginSuccess(token: string, qq: string) {
localStorage.setItem("token", token); localStorage.setItem("token", token);
// Extract username from token
try {
const payload = JSON.parse(atob(token.split(".")[1]));
if (payload.user_id) {
currentUser = payload.user_id;
localStorage.setItem("username", currentUser);
}
} catch (e) {
console.error("Failed to decode token", e);
}
if (qq) { if (qq) {
localStorage.setItem("qq", String(qq)); localStorage.setItem("qq", String(qq));
currentQQ = String(qq); currentQQ = String(qq);
@@ -332,8 +359,10 @@ const { titleSvg } = Astro.props;
function checkLoginState() { function checkLoginState() {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
const storedQQ = localStorage.getItem("qq"); const storedQQ = localStorage.getItem("qq");
const storedUser = localStorage.getItem("username");
isLoggedIn = !!token; isLoggedIn = !!token;
if (storedQQ) currentQQ = storedQQ; if (storedQQ) currentQQ = storedQQ;
if (storedUser) currentUser = storedUser;
if (isLoggedIn) { if (isLoggedIn) {
updateDashboard(); updateDashboard();

View File

@@ -25,11 +25,110 @@ const t = getTranslations(lang);
<div class="dashboard-avatar"> <div class="dashboard-avatar">
<img id="dashboard-avatar-img" src="" alt="User Avatar"> <img id="dashboard-avatar-img" src="" alt="User Avatar">
</div> </div>
<input type="file" id="avatar-upload-input" accept="image/*" class="hidden">
<button id="btn-change-avatar" class="change-avatar-btn">
{t("dashboard.change_avatar")}
</button>
<h2>{t("dashboard.logged_in")}</h2> <h2>{t("dashboard.logged_in")}</h2>
<p class="instruction">{t("dashboard.instruction")}</p> <p class="instruction">{t("dashboard.instruction")}</p>
</div> </div>
<script>
import { BACKEND_API_BASE } from "../../config/loginApiBaseUrl";
document.addEventListener("astro:page-load", () => {
const btnChange = document.getElementById("btn-change-avatar");
const input = document.getElementById(
"avatar-upload-input",
) as HTMLInputElement;
const img = document.getElementById(
"dashboard-avatar-img",
) as HTMLImageElement;
if (btnChange && input) {
btnChange.addEventListener("click", () => {
input.click();
});
input.addEventListener("change", async () => {
const file = input.files?.[0];
if (!file) return;
// Reset input value so same file can be selected again if needed
// but proceed first.
const token = localStorage.getItem("token");
if (!token) return;
const formData = new FormData();
formData.append("avatar", file);
// Show loading state
const originalText = btnChange.textContent;
btnChange.textContent = "...";
(btnChange as HTMLButtonElement).disabled = true;
try {
const res = await fetch(`${BACKEND_API_BASE}/user/avatar`, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
});
if (res.ok) {
// Update image source to force reload
if (img) {
const currentSrc = img.src.split("?")[0];
img.src = `${currentSrc}?t=${Date.now()}`;
}
// Dispatch event for other components (like navbar) if they listen
window.dispatchEvent(new CustomEvent("login-success"));
} else {
alert("Upload failed");
}
} catch (e) {
console.error("Avatar upload failed", e);
alert("Upload failed");
} finally {
btnChange.textContent = originalText;
(btnChange as HTMLButtonElement).disabled = false;
input.value = "";
}
});
}
});
</script>
<style> <style>
.change-avatar-btn {
margin-bottom: 1.5rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.9);
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s ease;
backdrop-filter: blur(4px);
}
.change-avatar-btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.change-avatar-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.hidden { .hidden {
/* biome-ignore lint/complexity/noImportantStyles: utility class */ /* biome-ignore lint/complexity/noImportantStyles: utility class */
display: none !important; display: none !important;
@@ -99,12 +198,12 @@ const t = getTranslations(lang);
padding-bottom: 3rem; padding-bottom: 3rem;
} }
.dashboard-avatar { .dashboard-avatar {
width: 96px; width: 128px;
height: 96px; height: 128px;
border-radius: 24px; border-radius: 32px;
overflow: hidden; overflow: hidden;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
border: 2px solid rgba(255, 255, 255, 0.2); border: 3px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
} }
.dashboard-avatar img { .dashboard-avatar img {

View File

@@ -43,6 +43,7 @@ export const ui = {
// Dashboard // Dashboard
"dashboard.logged_in": "Logged In", "dashboard.logged_in": "Logged In",
"dashboard.change_avatar": "Change Avatar",
"dashboard.instruction": "Development in progress...", "dashboard.instruction": "Development in progress...",
// Verification // Verification
@@ -98,6 +99,7 @@ export const ui = {
// Dashboard // Dashboard
"dashboard.logged_in": "已登录", "dashboard.logged_in": "已登录",
"dashboard.change_avatar": "修改头像",
"dashboard.instruction": "开发中...", "dashboard.instruction": "开发中...",
// Verification // Verification
@@ -152,6 +154,7 @@ export const ui = {
// Dashboard // Dashboard
"dashboard.logged_in": "已登入", "dashboard.logged_in": "已登入",
"dashboard.change_avatar": "修改頭像",
"dashboard.instruction": "開發中...", "dashboard.instruction": "開發中...",
// Verification // Verification
@@ -206,6 +209,7 @@ export const ui = {
// Dashboard // Dashboard
"dashboard.logged_in": "ログイン中", "dashboard.logged_in": "ログイン中",
"dashboard.change_avatar": "アバター変更",
"dashboard.instruction": "開発中...", "dashboard.instruction": "開発中...",
// Verification // Verification