@@ -40,12 +40,16 @@ export async function getAlbum(id) {
|
||||
* @param {Object} [options]
|
||||
* @param {number} [options.page] - 页码(从 1 开始)
|
||||
* @param {number} [options.size] - 每页数量
|
||||
* @param {string} [options.sort] - 排序字段
|
||||
* @param {string} [options.order] - 排序方向 (asc/desc)
|
||||
* @returns {Promise<import('./types').Photo[] | { items: import('./types').Photo[], total: number, page: number, size: number }>}
|
||||
*/
|
||||
export async function getAlbumPhotos(albumId, options = {}) {
|
||||
const params = new URLSearchParams();
|
||||
if (options.page) params.set('page', String(options.page));
|
||||
if (options.size) params.set('size', String(options.size));
|
||||
if (options.sort) params.set('sort', String(options.sort));
|
||||
if (options.order) params.set('order', String(options.order));
|
||||
|
||||
const query = params.toString();
|
||||
const endpoint = query
|
||||
@@ -73,7 +77,7 @@ export function getPhotoFileUrl(id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get photo preview URL
|
||||
* and photo preview URL
|
||||
* @param {number} id
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
@@ -8,14 +8,19 @@
|
||||
* @property {boolean} [borderLess]
|
||||
* @property {boolean} [waterfall]
|
||||
* @property {number} [index]
|
||||
* @property {string} [sortMode]
|
||||
* @property {string} [sortOrder]
|
||||
*/
|
||||
|
||||
/** @type {PhotoCardProps} */
|
||||
let { photo, onUpgradeQuality, borderLess = false, waterfall = false, index = 0 } = $props();
|
||||
let { photo, onUpgradeQuality, borderLess = false, waterfall = false, index = 0, sortMode = 'fileName', sortOrder = 'asc' } = $props();
|
||||
|
||||
// 使用 $derived 确保响应式更新
|
||||
let previewSrc = $derived(`/api/v1/photo/${photo.id}/preview`);
|
||||
|
||||
// 构建带排序参数的照片详情页链接
|
||||
let photoLink = $derived(resolve(`/photo/${photo.id}?sort=${sortMode}&order=${sortOrder}`));
|
||||
|
||||
// 根据原始宽高比计算瀑布流显示比例,防止图片加载前容器塌陷
|
||||
let aspectRatioStyle = $derived(
|
||||
waterfall && photo.width && photo.height
|
||||
@@ -39,7 +44,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<a href={resolve(`/photo/${photo.id}`)} class="photo-card" class:photo-card-waterfall={waterfall} class:photo-card-compact-waterfall={borderLess && waterfall} style={`--wf-aspect: ${aspectRatioStyle}; --wf-delay: ${animationDelay};`}>
|
||||
<a href={photoLink} class="photo-card" class:photo-card-waterfall={waterfall} class:photo-card-compact-waterfall={borderLess && waterfall} style={`--wf-aspect: ${aspectRatioStyle}; --wf-delay: ${animationDelay};`}>
|
||||
<div class="photo-wrapper" class:photo-borderless={borderLess} class:photo-waterfall={waterfall}>
|
||||
{#if photo.mimeType?.startsWith('video/')}
|
||||
<div class="video-indicator">🎬</div>
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
* @property {HTMLDivElement | null} [loadMoreTrigger]
|
||||
* @property {boolean} [borderLess]
|
||||
* @property {boolean} [waterfall]
|
||||
* @property {string} [sortMode]
|
||||
* @property {string} [sortOrder]
|
||||
*/
|
||||
|
||||
/** @type {PhotoGridProps} */
|
||||
@@ -29,7 +31,9 @@
|
||||
scrollContainer = $bindable(null),
|
||||
loadMoreTrigger = $bindable(null),
|
||||
borderLess = false,
|
||||
waterfall = false
|
||||
waterfall = false,
|
||||
sortMode = 'fileName',
|
||||
sortOrder = 'asc'
|
||||
} = $props();
|
||||
|
||||
function getVisiblePhotos() {
|
||||
@@ -100,7 +104,7 @@
|
||||
{#each splitToColumns(getVisiblePhotos(), columnCount) as col}
|
||||
<div class="waterfall-column">
|
||||
{#each col as photo (photo.id)}
|
||||
<PhotoCard {photo} {onUpgradeQuality} {borderLess} {waterfall} />
|
||||
<PhotoCard {photo} {onUpgradeQuality} {borderLess} {waterfall} {sortMode} {sortOrder} />
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -108,7 +112,7 @@
|
||||
{:else}
|
||||
<div class="photo-grid" class:photo-grid-borderless={borderLess}>
|
||||
{#each getVisiblePhotos() as photo, i (photo.id)}
|
||||
<PhotoCard {photo} {onUpgradeQuality} {borderLess} {waterfall} index={i} />
|
||||
<PhotoCard {photo} {onUpgradeQuality} {borderLess} {waterfall} index={i} {sortMode} {sortOrder} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -8,11 +8,18 @@ export async function load({ params, fetch, url }) {
|
||||
const page = parseInt(url.searchParams.get('page') || '1', 10);
|
||||
const pageSize = parseInt(url.searchParams.get('pageSize') || '100', 10);
|
||||
|
||||
// 支持排序参数
|
||||
const sort = url.searchParams.get('sort') || 'fileName';
|
||||
const order = url.searchParams.get('order') || 'asc';
|
||||
|
||||
try {
|
||||
const [album, photos] = await Promise.all([
|
||||
fetchApi(`/album/${albumId}`, fetch),
|
||||
// 后端使用 size 作为分页参数名
|
||||
fetchApi(`/album/${albumId}/photo?page=${page}&size=${pageSize}`, fetch)
|
||||
fetchApi(
|
||||
`/album/${albumId}/photo?page=${page}&size=${pageSize}&sort=${sort}&order=${order}`,
|
||||
fetch
|
||||
)
|
||||
]);
|
||||
|
||||
if (!album) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { resolve } from '$app/paths';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import { Container, PageHeader, BackLink, PhotoGrid, Empty, SegmentedControl } from '$lib/components';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -42,6 +43,11 @@
|
||||
// 布局模式:砖块 / 瀑布(Grid / Columns)
|
||||
let layoutMode = $state('grid');
|
||||
|
||||
// 排序模式
|
||||
let sortMode = $state(page.data.album ? (page.url.searchParams.get('sort') || 'fileName') : 'fileName');
|
||||
// 排序方向
|
||||
let sortOrder = $state(page.data.album ? (page.url.searchParams.get('order') || 'asc') : 'asc');
|
||||
|
||||
const styleOptions = [
|
||||
{ value: 'card', label: '卡片', icon: '🖼️' },
|
||||
{ value: 'borderless', label: '紧凑', icon: '📐' }
|
||||
@@ -52,6 +58,12 @@
|
||||
{ value: 'waterfall', label: '瀑布', icon: '🌊' }
|
||||
];
|
||||
|
||||
const sortOptions = [
|
||||
{ value: 'fileName', label: '名称' },
|
||||
{ value: 'createdAt', label: '时间' },
|
||||
{ value: 'fileSize', label: '大小' }
|
||||
];
|
||||
|
||||
// 监听数据变化,重置状态
|
||||
$effect(() => {
|
||||
if (data.photos) {
|
||||
@@ -105,9 +117,9 @@
|
||||
currentPage++;
|
||||
|
||||
try {
|
||||
// 后端使用 size 作为分页参数名
|
||||
// 后端使用 size 作为分页参数名,同时传递排序参数
|
||||
const response = await fetch(
|
||||
`${page.url.pathname}?page=${currentPage}&pageSize=${PAGE_SIZE}`
|
||||
`${page.url.pathname}?page=${currentPage}&pageSize=${PAGE_SIZE}&sort=${sortMode}&order=${sortOrder}`
|
||||
);
|
||||
const newData = await response.json();
|
||||
|
||||
@@ -127,6 +139,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 处理排序变化
|
||||
function applySort() {
|
||||
// 重置分页状态并导航到新 URL
|
||||
goto(`${page.url.pathname}?page=1&pageSize=${PAGE_SIZE}&sort=${sortMode}&order=${sortOrder}`, {
|
||||
replaceState: true,
|
||||
keepFocus: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 当用户悬停或聚焦图片时,升级图片质量
|
||||
* @param {number} photoId
|
||||
@@ -183,6 +204,20 @@
|
||||
options={layoutOptions}
|
||||
size="small"
|
||||
/>
|
||||
<SegmentedControl
|
||||
value={sortMode}
|
||||
onchange={(v) => { sortMode = v; applySort(); }}
|
||||
options={sortOptions}
|
||||
size="small"
|
||||
/>
|
||||
<button
|
||||
class="sort-order-toggle"
|
||||
type="button"
|
||||
onclick={() => { sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'; applySort(); }}
|
||||
aria-label={sortOrder === 'asc' ? '降序' : '升序'}
|
||||
>
|
||||
<span class="sort-icon">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -201,6 +236,8 @@
|
||||
bind:loadMoreTrigger
|
||||
borderLess={styleMode === 'borderless'}
|
||||
waterfall={layoutMode === 'waterfall'}
|
||||
sortMode={sortMode}
|
||||
sortOrder={sortOrder}
|
||||
/>
|
||||
{/if}
|
||||
</Container>
|
||||
@@ -221,5 +258,30 @@
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sort-order-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: var(--font-size-sm);
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.sort-order-toggle:hover {
|
||||
color: var(--color-text);
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.sort-icon {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { fetchApi } from '$lib/api/client';
|
||||
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export async function load({ params, fetch }) {
|
||||
export async function load({ params, fetch, url }) {
|
||||
const photoId = params.id;
|
||||
|
||||
// 保留排序参数用于 prev/next 导航
|
||||
const sort = url.searchParams.get('sort') || 'fileName';
|
||||
const order = url.searchParams.get('order') || 'asc';
|
||||
|
||||
try {
|
||||
// 获取当前照片信息
|
||||
const photo = await fetchApi(`/photo/${photoId}`, fetch);
|
||||
@@ -26,11 +30,14 @@ export async function load({ params, fetch }) {
|
||||
let prevPhotoId = null;
|
||||
let nextPhotoId = null;
|
||||
|
||||
// 如果有 albumId,尝试获取相册中的照片列表
|
||||
// 如果有 albumId,尝试获取相册中的照片列表(使用相同排序)
|
||||
if (photo.albumId) {
|
||||
try {
|
||||
const albumPhotos = await fetchApi(`/album/${photo.albumId}/photo`, fetch);
|
||||
const photos = Array.isArray(albumPhotos) ? albumPhotos : [];
|
||||
const albumPhotos = await fetchApi(
|
||||
`/album/${photo.albumId}/photo?sort=${sort}&order=${order}`,
|
||||
fetch
|
||||
);
|
||||
const photos = Array.isArray(albumPhotos) ? albumPhotos : albumPhotos?.items ?? [];
|
||||
|
||||
if (photos.length > 0) {
|
||||
const index = photos.findIndex((p) => String(p.id) === String(photoId));
|
||||
|
||||
Reference in New Issue
Block a user