实现排序功能
Some checks failed
Dart CI / build (push) Failing after 9s

This commit is contained in:
2026-04-05 19:39:37 +08:00
parent 90f7159746
commit 2bb8a83bbc
9 changed files with 193 additions and 80 deletions

View File

@@ -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}
*/

View File

@@ -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>

View File

@@ -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}

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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));