diff --git a/static/css/components/modal.css b/static/css/components/modal.css
index a02a8575..2420a73f 100644
--- a/static/css/components/modal.css
+++ b/static/css/components/modal.css
@@ -29,6 +29,8 @@ body.modal-open {
border: 1px solid var(--lora-border);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
transform: translateZ(0); /* 创建新的堆叠上下文 */
+ max-height: 90vh; /* 限制最大高度为视窗高度的90% */
+ overflow-y: auto; /* 添加垂直滚动条 */
}
/* Delete Modal specific styles */
@@ -617,44 +619,66 @@ body.modal-open {
}
.carousel {
- display: flex;
- flex-direction: column; /* 改为垂直布局 */
- gap: var(--space-3);
- overflow-y: auto; /* 垂直滚动 */
+ transition: max-height 0.3s ease-in-out;
+ overflow: hidden;
}
-.carousel img,
-.carousel video {
+.carousel.collapsed {
+ max-height: 0;
+}
+
+.carousel-container {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+}
+
+.media-wrapper {
+ position: relative;
width: 100%;
- height: auto;
- max-height: 70vh; /* 限制单张图片最大高度 */
+ background: var(--lora-surface);
+ margin-bottom: var(--space-2);
+}
+
+.media-wrapper:last-child {
+ margin-bottom: 0;
+}
+
+.media-wrapper img,
+.media-wrapper video {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
object-fit: contain;
- border-radius: var(--border-radius-base);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Scroll Indicator */
.scroll-indicator {
- position: sticky;
- top: 0;
- left: 0;
- right: 0;
- text-align: center;
+ cursor: pointer;
padding: var(--space-2);
- background: linear-gradient(to bottom,
- var(--lora-surface) 0%,
- transparent 100%);
- opacity: 0.8;
- transition: opacity 0.3s;
- pointer-events: none;
- z-index: 1; /* 确保滚动提示在图片上层 */
+ background: var(--lora-surface);
+ border: 1px solid var(--lora-border);
+ border-radius: var(--border-radius-sm);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ margin-bottom: var(--space-2);
}
-/* Remove horizontal scrolling styles */
-.carousel {
- scroll-snap-type: none;
- grid-auto-flow: initial;
- overflow-x: initial;
+.scroll-indicator:hover {
+ background: oklch(var(--lora-accent) / 0.1);
+}
+
+.lazy {
+ opacity: 0;
+ transition: opacity 0.3s;
+}
+
+.lazy[src] {
+ opacity: 1;
}
/* Update Trigger Words styles */
diff --git a/static/js/components/LoraCard.js b/static/js/components/LoraCard.js
index fa648172..99a35660 100644
--- a/static/js/components/LoraCard.js
+++ b/static/js/components/LoraCard.js
@@ -171,15 +171,7 @@ export function showLoraModal(lora) {
-
-
-
- Scroll for more examples
-
-
- ${renderShowcaseImages(lora.civitai.images)}
-
-
+ ${renderShowcaseImages(lora.civitai.images)}
`;
@@ -275,21 +267,101 @@ function renderTriggerWords(words) {
function renderShowcaseImages(images) {
if (!images?.length) return '';
- return images.map(img => {
- if (img.type === 'video') {
- return `
-
- `;
+ return `
+
+
+
+ Show ${images.length} examples
+
+
+
+ ${images.map(img => {
+ // 计算适当的展示高度:
+ // 1. 保持原始宽高比
+ // 2. 限制最大高度为视窗高度的60%
+ // 3. 确保最小高度为容器宽度的40%
+ const aspectRatio = (img.height / img.width) * 100;
+ const containerWidth = 800; // modal content的最大宽度
+ const minHeightPercent = 40; // 最小高度为容器宽度的40%
+ const maxHeightPercent = (window.innerHeight * 0.6 / containerWidth) * 100;
+ const heightPercent = Math.max(
+ minHeightPercent,
+ Math.min(maxHeightPercent, aspectRatio)
+ );
+
+ if (img.type === 'video') {
+ return `
+
+
+
+ `;
+ }
+ return `
+
+ `;
+ }).join('')}
+
+
+
+ `;
+}
+
+// Add this to the window object for global access
+export function toggleShowcase(element) {
+ const carousel = element.nextElementSibling;
+ const isCollapsed = carousel.classList.contains('collapsed');
+ const indicator = element.querySelector('span');
+ const icon = element.querySelector('i');
+
+ carousel.classList.toggle('collapsed');
+
+ if (isCollapsed) {
+ indicator.textContent = 'Hide examples';
+ icon.classList.replace('fa-chevron-down', 'fa-chevron-up');
+ initLazyLoading(carousel);
+ } else {
+ const count = carousel.querySelectorAll('.media-wrapper').length;
+ indicator.textContent = `Show ${count} examples`;
+ icon.classList.replace('fa-chevron-up', 'fa-chevron-down');
+ }
+};
+
+// Add lazy loading initialization
+export function initLazyLoading(container) {
+ const lazyElements = container.querySelectorAll('.lazy');
+
+ const lazyLoad = (element) => {
+ if (element.tagName.toLowerCase() === 'video') {
+ element.src = element.dataset.src;
+ element.querySelector('source').src = element.dataset.src;
+ element.load();
+ } else {
+ element.src = element.dataset.src;
}
- return `
-
- `;
- }).join('');
+ element.classList.remove('lazy');
+ };
+
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ lazyLoad(entry.target);
+ observer.unobserve(entry.target);
+ }
+ });
+ });
+
+ lazyElements.forEach(element => observer.observe(element));
}
\ No newline at end of file
diff --git a/static/js/main.js b/static/js/main.js
index 2bc4ad1e..bbc2ab9b 100644
--- a/static/js/main.js
+++ b/static/js/main.js
@@ -2,7 +2,7 @@ import { debounce } from './utils/debounce.js';
import { LoadingManager } from './managers/LoadingManager.js';
import { modalManager } from './managers/ModalManager.js';
import { state } from './state/index.js';
-import { showLoraModal } from './components/LoraCard.js';
+import { showLoraModal, toggleShowcase, initLazyLoading } from './components/LoraCard.js';
import { loadMoreLoras, fetchCivitai, deleteModel, replacePreview, resetAndReload, refreshLoras } from './api/loraApi.js';
import {
showToast,
@@ -45,6 +45,7 @@ window.toggleFolderTags = toggleFolderTags;
window.settingsManager = new SettingsManager();
window.toggleApiKeyVisibility = toggleApiKeyVisibility;
window.moveManager = moveManager;
+window.toggleShowcase = toggleShowcase;
// Initialize everything when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
@@ -58,6 +59,7 @@ document.addEventListener('DOMContentLoaded', () => {
initTheme();
initFolderTagsVisibility();
initBackToTop();
+ initLazyLoading();
window.searchManager = new SearchManager();
new LoraContextMenu();
});