refactor: move supporters loading to separate API endpoint

- Add SupportersHandler in misc_handlers.py to serve /api/lm/supporters
- Register new endpoint in misc_route_registrar.py
- Remove supporters from page load template context in model_handlers.py
- Create supportersService.js for frontend data fetching
- Update Header.js to fetch supporters when support modal opens
- Modify support_modal.html to use client-side rendering

This change improves page load performance by loading supporters data
on-demand instead of during initial page render.
This commit is contained in:
Will Miao
2026-02-28 20:14:20 +08:00
parent 77a2215e62
commit 78b55d10ba
20 changed files with 1206 additions and 89 deletions

View File

@@ -68,6 +68,7 @@ body {
--space-1: calc(8px * 1);
--space-2: calc(8px * 2);
--space-3: calc(8px * 3);
--space-4: calc(8px * 4);
/* Z-index Scale */
--z-base: 10;
@@ -77,6 +78,7 @@ body {
/* Border Radius */
--border-radius-base: 12px;
--border-radius-md: 12px;
--border-radius-sm: 8px;
--border-radius-xs: 4px;

View File

@@ -1,6 +1,26 @@
/* Support Modal Styles */
.support-modal {
max-width: 570px;
max-width: 1000px;
width: 90vw;
}
/* Two-column layout */
.support-container {
display: flex;
gap: var(--space-3);
min-height: 500px;
}
.support-left {
flex: 0 0 42%;
min-width: 0;
}
.support-right {
flex: 1;
min-width: 0;
border-left: 1px solid var(--lora-border);
padding-left: var(--space-4);
}
.support-header {
@@ -214,6 +234,11 @@
.support-links {
flex-direction: column;
}
.support-modal {
width: 95vw;
max-width: 95vw;
}
}
/* Civitai link styles */
@@ -239,4 +264,181 @@
.folder-item:hover {
border-color: var(--lora-accent);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Supporters Section Styles */
.supporters-section {
height: 100%;
display: flex;
flex-direction: column;
}
.supporters-header {
margin-bottom: var(--space-4);
}
.supporters-title {
display: flex;
align-items: center;
gap: var(--space-2);
margin: 0 0 var(--space-1) 0;
font-size: 1.3em !important;
color: var(--lora-accent) !important;
}
.supporters-title i {
opacity: 0.9;
}
.supporters-subtitle {
margin: 0;
font-size: 0.95em;
color: var(--text-color);
opacity: 0.6;
}
.supporters-group {
margin-bottom: var(--space-3);
}
.supporters-group-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 var(--space-2) 0;
font-size: 1em;
color: var(--text-color);
opacity: 0.8;
font-weight: 500;
}
.supporters-group-title i {
color: var(--lora-accent);
opacity: 0.7;
}
/* Special Thanks - Clean Card Style */
.special-thanks-group {
margin-bottom: var(--space-4);
}
.special-thanks-group .supporters-group-title {
margin-bottom: var(--space-3);
}
.special-thanks-group .supporters-group-title i {
color: #fbbf24;
}
.all-supporters-group .supporters-group-title i {
color: var(--lora-error);
opacity: 0.9;
}
.supporters-special-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-2);
}
.supporter-special-card {
display: flex;
align-items: center;
padding: var(--space-2) var(--space-3);
background: var(--card-bg);
border: 1px solid var(--border-color);
border-left: 3px solid var(--lora-accent);
border-radius: var(--border-radius-sm);
transition: all 0.2s ease;
cursor: default;
}
.supporter-special-card:hover {
border-color: var(--lora-accent);
border-left-color: var(--lora-accent);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transform: translateX(4px);
}
.supporter-special-card .supporter-special-name {
font-size: 1em;
font-weight: 500;
color: var(--text-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.supporter-special-card:hover .supporter-special-name {
color: var(--lora-accent);
}
/* All Supporters - Elegant Text Flow */
.all-supporters-group {
flex: 1;
display: flex;
flex-direction: column;
}
.all-supporters-group .supporters-group-title {
margin-bottom: var(--space-2);
}
.supporters-all-list {
display: flex;
flex-wrap: wrap;
align-items: baseline;
line-height: 2.2;
max-height: 550px;
overflow-y: auto;
padding: var(--space-2) 0;
color: var(--text-color);
}
.supporter-name-item {
font-size: 0.95em;
color: var(--text-color);
opacity: 0.85;
transition: all 0.2s ease;
white-space: nowrap;
cursor: default;
}
.supporter-name-item:hover {
opacity: 1;
color: var(--lora-accent);
}
.supporter-separator {
margin: 0 10px;
color: var(--text-color);
opacity: 0.25;
font-weight: 300;
user-select: none;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.support-container {
flex-direction: column;
}
.support-left {
flex: 1;
}
.support-right {
border-left: none;
border-top: 1px solid var(--lora-border);
padding-left: 0;
padding-top: var(--space-3);
}
.supporters-all-list {
max-height: 200px;
}
.supporters-special-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -5,6 +5,7 @@ import { FilterManager } from '../managers/FilterManager.js';
import { initPageState } from '../state/index.js';
import { getStorageItem } from '../utils/storageHelpers.js';
import { updateElementAttribute } from '../utils/i18nHelpers.js';
import { renderSupporters } from '../services/supportersService.js';
/**
* Header.js - Manages the application header behavior across different pages
@@ -85,9 +86,15 @@ export class HeaderManager {
// Handle support toggle
const supportToggle = document.getElementById('supportToggleBtn');
if (supportToggle) {
supportToggle.addEventListener('click', () => {
supportToggle.addEventListener('click', async () => {
if (window.modalManager) {
window.modalManager.toggleModal('supportModal');
// Load supporters data when modal opens
try {
await renderSupporters();
} catch (error) {
console.error('Error loading supporters:', error);
}
}
});
}

View File

@@ -0,0 +1,104 @@
/**
* Supporters service - Fetches and manages supporters data
*/
let supportersData = null;
let isLoading = false;
let loadPromise = null;
/**
* Fetch supporters data from the API
* @returns {Promise<Object>} Supporters data
*/
export async function fetchSupporters() {
// Return cached data if available
if (supportersData) {
return supportersData;
}
// Return existing promise if already loading
if (isLoading && loadPromise) {
return loadPromise;
}
isLoading = true;
loadPromise = fetch('/api/lm/supporters')
.then(response => {
if (!response.ok) {
throw new Error(`Failed to fetch supporters: ${response.statusText}`);
}
return response.json();
})
.then(data => {
if (data.success && data.supporters) {
supportersData = data.supporters;
return supportersData;
}
throw new Error(data.error || 'Failed to load supporters data');
})
.catch(error => {
console.error('Error loading supporters:', error);
// Return empty data on error
return {
specialThanks: [],
allSupporters: [],
totalCount: 0
};
})
.finally(() => {
isLoading = false;
loadPromise = null;
});
return loadPromise;
}
/**
* Clear cached supporters data
*/
export function clearSupportersCache() {
supportersData = null;
}
/**
* Render supporters in the support modal
*/
export async function renderSupporters() {
const supporters = await fetchSupporters();
// Update subtitle with total count
const subtitleEl = document.getElementById('supportersSubtitle');
if (subtitleEl) {
// Get the translation key and replace count
const originalText = subtitleEl.textContent;
// Replace the count in the text (simple approach)
subtitleEl.textContent = originalText.replace(/\d+/, supporters.totalCount);
}
// Render special thanks
const specialThanksGrid = document.getElementById('specialThanksGrid');
if (specialThanksGrid && supporters.specialThanks) {
specialThanksGrid.innerHTML = supporters.specialThanks
.map(supporter => `
<div class="supporter-special-card" title="${supporter}">
<span class="supporter-special-name">${supporter}</span>
</div>
`)
.join('');
}
// Render all supporters
const supportersGrid = document.getElementById('supportersGrid');
if (supportersGrid && supporters.allSupporters) {
supportersGrid.innerHTML = supporters.allSupporters
.map((supporter, index, array) => {
const separator = index < array.length - 1
? '<span class="supporter-separator">·</span>'
: '';
return `
<span class="supporter-name-item" title="${supporter}">${supporter}</span>${separator}
`;
})
.join('');
}
}