feat: SSR 友链页与最小 Admin 审核

公开 /links 列表与申请/取消,导航页脚可配入口;后台品牌增删与通过/拒绝。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 06:30:01 +08:00
parent 204e7fdb32
commit 7bc50bfb80
19 changed files with 880 additions and 42 deletions

View File

@@ -537,7 +537,65 @@ points-only[data-locked="false"] {
margin: 0.75rem 0;
}
.j13-footer a { color: var(--j13-accent); text-decoration: none; }
.j13-footer a:hover { text-decoration: underline; }
.j13-check {
display: flex !important;
flex-direction: row !important;
align-items: center;
gap: 0.45rem;
}
.j13-check input { width: auto; margin: 0; }
.j13-admin-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
margin: 0.5rem 0 1rem;
}
.j13-admin-table th,
.j13-admin-table td {
text-align: left;
padding: 0.45rem 0.4rem;
border-bottom: 1px solid var(--j13-border);
vertical-align: top;
}
.j13-links { max-width: 720px; margin: 0 auto; padding: 1rem 1rem 2.5rem; }
.j13-links h1 { margin: 0 0 0.75rem; font-size: 1.4rem; }
.j13-links h2 { margin: 1.5rem 0 0.65rem; font-size: 1.05rem; }
.j13-links__grid {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.75rem;
}
.j13-links__item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
padding: 0.75rem 0.5rem;
border: 1px solid var(--j13-border);
border-radius: var(--j13-radius);
background: var(--j13-bg);
color: inherit;
text-decoration: none;
text-align: center;
font-size: 0.9rem;
}
.j13-links__item:hover { border-color: var(--j13-accent); color: var(--j13-accent); }
.j13-links__logo {
width: 48px;
height: 48px;
object-fit: contain;
border-radius: 6px;
background: #fff;
}
.j13-links__logo--sm {
width: 24px;
height: 24px;
vertical-align: middle;
margin-right: 0.25rem;
}

View File

@@ -176,3 +176,30 @@ document.documentElement.dataset.j13Ssr = "1";
}
});
})();
// 友链申请:选择文件后先上传并回填 LOGO URL
(function () {
const form = document.querySelector("form[data-logo-upload]");
if (!form) return;
const fileInput = form.querySelector("#fl-logo-file");
const urlInput = form.querySelector("#fl-logo-url");
const csrfInput = form.querySelector('input[name="_csrf"]');
if (!fileInput || !urlInput || !csrfInput) return;
const uploadURL = form.getAttribute("data-logo-upload") || "/links/logo";
fileInput.addEventListener("change", async () => {
const file = fileInput.files && fileInput.files[0];
if (!file) return;
const fd = new FormData();
fd.append("logo", file);
fd.append("_csrf", csrfInput.value);
try {
const res = await fetch(uploadURL, { method: "POST", body: fd, credentials: "same-origin" });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "上传失败");
if (data.url) urlInput.value = data.url;
} catch (e) {
alert(e.message || "上传失败");
fileInput.value = "";
}
});
})();