import PhotoSwipeLightbox from 'https://dpangzi.com/library/photoswipe/photoswipe.esm.min.js';
import PhotoSwipe from 'https://dpangzi.com/library/photoswipe/photoswipe.esm.min.js';
import {dialog} from "./dialog.js";
export class Albums {
constructor() {
this.container = document.querySelector('.albums');
if (!this.container) return;
this.grid = this.container.querySelector('.albums__grid');
this.loadingIndicator = this.container.querySelector('.albums__loading');
this.noMoreIndicator = this.container.querySelector('.albums__no-more');
// 统计栏中「第 X / Y 页」的文字节点所在的 span
this.statPageEl = this.container.querySelector('.albums__stats span:last-child');
// State
this.pageIndex = parseInt(this.container.dataset.pageIndex) || 1;
this.pageSize = parseInt(this.container.dataset.pageSize) || 20;
this.totalPageCount = parseInt(this.container.dataset.totalPages) || 0;
this.hasMore = this.container.dataset.hasMore === 'true';
this.isLoading = false;
this.items = []; // PhotoSwipe items
this._isClosedByNavigation = false;
this._isClosing = false;
this.lightbox = null;
// Bind functions
this.onHashChange = this.onHashChange.bind(this);
this.init();
}
init() {
this.initMasonry();
this.initInitialItems();
this.initObserver();
this.bindEvents();
// Hash 导航
window.addEventListener('hashchange', this.onHashChange);
// 处理初始化时已存在的 Hash
this.onHashChange();
}
initMasonry() {
if (typeof Masonry === 'undefined') return;
// 相册图片带有 width/height 属性,浏览器可自动推断宽高比,
// Masonry 初始化时卡片高度已知,无需等待图片加载
this._masonry = new Masonry(this.grid, {
itemSelector: '.albums__item', // 瀑布流条目选择器
columnWidth: '.albums__grid-sizer', // 列宽基准元素
percentPosition: true, // 百分比定位,适配响应式
gutter: 16, // 卡片水平间距(px)
transitionDuration: '0.3s', // 卡片移动动画时长
});
// 排列完成后显示网格,触发淡入效果
this.grid.classList.add('albums__grid--ready');
}
initInitialItems() {
const cards = this.grid.querySelectorAll('.albums__card');
cards.forEach(card => {
this.addItemFromCard(card);
});
}
addItemFromCard(card) {
const img = card.querySelector('.albums__image');
if (!img) return;
const src = img.getAttribute('data-src-original') || img.src.replace('!albums', '');
const id = img.getAttribute('data-id') || ''; // Get ID
const width = parseInt(img.getAttribute('width')) || 0;
const height = parseInt(img.getAttribute('height')) || 0;
const desc = img.alt || '';
this.items.push({
id: id,
src: src,
w: width,
h: height,
alt: desc,
// Reference to DOM element
element: img
});
// Bind click to open PhotoSwipe
// We use delegation on grid, so no individual binding needed here,
// but we need to know the index.
card.dataset.index = (this.items.length - 1).toString();
if (id) {
card.dataset.id = id;
}
}
initObserver() {
if (!this.hasMore) {
if (this.loadingIndicator) this.loadingIndicator.style.display = 'none';
if (this.noMoreIndicator) this.noMoreIndicator.style.display = 'block';
return;
}
const options = {
root: null,
rootMargin: '200px',
threshold: 0.1
};
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !this.isLoading && this.hasMore) {
void this.loadMore();
}
});
}, options);
// Observe the loading indicator (which is at the bottom)
if (this.loadingIndicator) {
this.observer.observe(this.loadingIndicator);
}
}
bindEvents() {
// Event delegation for clicks
this.grid.addEventListener('click', (e) => {
const card = e.target.closest('.albums__card');
const actionBtn = e.target.closest('.albums__action-btn');
if (actionBtn) {
// Handle action buttons (stop propagation to prevent opening gallery)
e.stopPropagation();
this.handleAction(actionBtn);
return;
}
if (card) {
const index = parseInt(card.dataset.index);
if (!isNaN(index)) {
this.openGallery(index);
}
}
});
}
parseHash() {
const hash = window.location.hash;
const match = hash.match(/#pid=([^&]+)/);
if (match) {
return match[1];
}
return null;
}
findItemIndexById(id) {
if (!id) return -1;
return this.items.findIndex(item => item.id === id);
}
onHashChange() {
const id = this.parseHash();
if (id !== null) {
const index = this.findItemIndexById(id);
// Check if index is valid
if (index >= 0) {
this.openGallery(index, true);
}
} else {
// Hash cleared, close if open
if (this.lightbox && !this._isClosing) {
this._isClosedByNavigation = true;
if (this.lightbox.pswp) {
this.lightbox.pswp.close();
}
}
}
}
async loadMore() {
if (this.isLoading || !this.hasMore) return;
this.isLoading = true;
try {
const nextPage = this.pageIndex + 1;
const url = `/pages/albums?pageIndex=${nextPage}&pageSize=${this.pageSize}`;
const response = await fetch(url);
if (!response.ok) {
dialog.toast("网络响应错误", "error");
return;
}
const data = await response.json();
const items = data.items || data.Items || [];
if (items.length > 0) {
this.appendItems(items);
this.pageIndex = nextPage;
this.hasMore = nextPage < (data.totalPageCount || data.TotalPageCount);
// 同步更新页头的页码显示
this.updateStatPage();
} else {
this.hasMore = false;
}
} catch (error) {
console.error('Error loading more albums:', error);
// Optionally show error UI
} finally {
this.isLoading = false;
this.updateUI();
}
}
appendItems(items) {
const fragment = document.createDocumentFragment();
// 记录新增的 DOM 节点,用于通知 Masonry
const newNodes = [];
items.forEach(item => {
// 统一属性名(兼容大小写)
const id = item.Id || item.id;
const url = item.AccessUrl || item.accessUrl;
const desc = item.Description || item.description;
const width = item.Width || item.width;
const height = item.Height || item.height;
const creator = item.Creator || item.creator;
const uploadTime = item.UploadTime || item.uploadTime;
const length = item.Length || item.length;
const cardNode = this.createCardNode({
id, url, desc, width, height, creator, uploadTime, length
});
fragment.appendChild(cardNode);
newNodes.push(cardNode);
// 同步追加到 PhotoSwipe 数据源
this.items.push({
id: id,
src: url,
w: width,
h: height,
alt: desc
});
// 为刚创建的卡片设置 PhotoSwipe 索引
const card = cardNode.querySelector('.albums__card');
card.dataset.index = (this.items.length - 1).toString();
if (id) {
card.dataset.id = id;
}
});
this.grid.appendChild(fragment);
// 通知 Masonry 有新卡片追加,触发重新布局
if (this._masonry) {
this._masonry.appended(newNodes);
}
}
createCardNode(data) {
const div = document.createElement('div');
div.className = 'albums__item';
// 格式化文件大小
const sizeMB = (data.length / 1024 / 1024).toFixed(2);
// 格式化上传日期
const date = new Date(data.uploadTime).toLocaleDateString();
const userName = (data.creator && (data.creator.Name || data.creator.name)) || 'Unknown';
div.innerHTML = `
<div class="albums__card">
<div class="albums__image-wrapper">
<img class="albums__image is-loaded"
src="${data.url}!albums"
data-src-original="${data.url}"
data-id="${data.id || ''}"
alt="${this.escapeHtml(data.desc || '')}"
width="${data.width}"
height="${data.height}"
loading="lazy">
<div class="albums__overlay">
<div class="albums__actions">
<button class="albums__action-btn" data-action="download" data-url="${data.url}" title="下载">
<i class="fa fa-download"></i>
</button>
<button class="albums__action-btn" data-action="share" data-url="${data.url}" title="分享">
<i class="fa fa-share"></i>
</button>
</div>
<div class="albums__info">
<div class="albums__desc">${this.escapeHtml(data.desc || '')}</div>
<div class="albums__meta">
<div class="albums__meta-row">
<span>${this.escapeHtml(userName)}</span>
<span>${date}</span>
</div>
<div class="albums__meta-row">
<span>${data.width} × ${data.height}</span>
<span>${sizeMB} MB</span>
</div>
</div>
</div>
</div>
</div>
</div>
`;
return div;
}
updateUI() {
if (!this.hasMore) {
if (this.observer && this.loadingIndicator) {
this.observer.unobserve(this.loadingIndicator);
}
if (this.loadingIndicator) this.loadingIndicator.style.display = 'none';
if (this.noMoreIndicator) this.noMoreIndicator.style.display = 'block';
}
}
/** 更新页头「第 X / Y 页」文字 */
updateStatPage() {
if (this.statPageEl) {
this.statPageEl.textContent = `第 ${this.pageIndex} / ${this.totalPageCount} 页`;
}
}
openGallery(index, fromHash = false) {
if (this.lightbox) {
// If already open, just update index
if (this.lightbox.pswp) {
this.lightbox.pswp.goTo(index);
}
return;
}
this._isClosing = false;
const lightbox = new PhotoSwipeLightbox({
dataSource: this.items,
pswpModule: PhotoSwipe,
index: index,
bgOpacity: 0.9,
showHideAnimationType: 'zoom'
});
this.lightbox = lightbox;
lightbox.on('change', () => {
const pswp = lightbox.pswp;
if (pswp) {
const currItem = pswp.currSlide.data;
if (currItem && currItem.id) {
const newHash = `#pid=${currItem.id}`;
if (window.location.hash !== newHash) {
history.replaceState(null, null, newHash);
}
}
}
});
lightbox.on('close', () => {
this._isClosing = true;
// If hash is present and we are not closing by navigation (back button),
// then we should go back to clear the hash.
if (!this._isClosedByNavigation && this.parseHash() !== null) {
history.back();
}
this.lightbox = null;
this._isClosedByNavigation = false;
});
// If not opened from hash, push the initial state
if (!fromHash) {
const item = this.items[index];
if (item && item.id) {
history.pushState(null, '', `#pid=${item.id}`);
}
}
lightbox.init();
}
handleAction(btn) {
const action = btn.dataset.action;
const url = btn.dataset.url;
if (action === 'download') {
this.downloadImage(url);
} else if (action === 'share') {
this.shareImage(url);
}
}
downloadImage(url) {
const link = document.createElement('a');
link.href = url;
link.download = ''; // Browser handles filename
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
shareImage(url) {
if (navigator.share) {
navigator.share({
title: '分享图片',
url: url
}).catch(console.error);
} else {
// Fallback to clipboard
navigator.clipboard.writeText(url).then(() => {
alert('图片链接已复制到剪贴板');
}).catch(() => {
prompt('复制链接:', url);
});
}
}
escapeHtml(text) {
if (!text) return '';
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
}
评论加载中...