格式化前端代码
Some checks failed
Dart CI / build (push) Successful in 37s
Web CI / lint-test-build (push) Failing after 21s

This commit is contained in:
2026-04-09 21:34:33 +08:00
parent b05e77aae1
commit 710a0b6f8e
10 changed files with 275 additions and 221 deletions

View File

@@ -23,18 +23,18 @@ Loongyan 相册系统的前端应用,基于 [SvelteKit](https://kit.svelte.dev
## 🛠️ 技术栈 ## 🛠️ 技术栈
| 技术 | 说明 | | 技术 | 说明 |
|------|------| | ------------------------------------------------------------------------ | ---------------------- |
| [Svelte 5](https://svelte.dev/) | 前端框架Runes 模式) | | [Svelte 5](https://svelte.dev/) | 前端框架Runes 模式) |
| [SvelteKit 2](https://kit.svelte.dev/) | 全栈框架 | | [SvelteKit 2](https://kit.svelte.dev/) | 全栈框架 |
| [Vite 7](https://vitejs.dev/) | 构建工具 | | [Vite 7](https://vitejs.dev/) | 构建工具 |
| [Paraglide JS](https://inlang.com/m/gerre34r/library-inlang-paraglideJs) | 国际化 | | [Paraglide JS](https://inlang.com/m/gerre34r/library-inlang-paraglideJs) | 国际化 |
| [Better Auth](https://www.better-auth.com/) | 用户认证 | | [Better Auth](https://www.better-auth.com/) | 用户认证 |
| [Drizzle ORM](https://orm.drizzle.team/) | 数据库 ORM | | [Drizzle ORM](https://orm.drizzle.team/) | 数据库 ORM |
| [SQLite](https://www.sqlite.org/) | 嵌入式数据库 | | [SQLite](https://www.sqlite.org/) | 嵌入式数据库 |
| [Vitest](https://vitest.dev/) | 单元测试 | | [Vitest](https://vitest.dev/) | 单元测试 |
| [Playwright](https://playwright.dev/) | 浏览器测试 | | [Playwright](https://playwright.dev/) | 浏览器测试 |
| [mdsvex](https://mdsvex.pngwn.io/) | Markdown 组件 | | [mdsvex](https://mdsvex.pngwn.io/) | Markdown 组件 |
--- ---
@@ -139,14 +139,14 @@ const API_BASE = '/api/v1';
### 可用 API 方法 ### 可用 API 方法
| 方法 | 说明 | | 方法 | 说明 |
|------|------| | ------------------------- | -------------- |
| `getAlbums()` | 获取所有相册 | | `getAlbums()` | 获取所有相册 |
| `getAlbum(id)` | 获取相册详情 | | `getAlbum(id)` | 获取相册详情 |
| `getAlbumPhotos(albumId)` | 获取相册内照片 | | `getAlbumPhotos(albumId)` | 获取相册内照片 |
| `getPhoto(id)` | 获取照片详情 | | `getPhoto(id)` | 获取照片详情 |
| `getPhotoFileUrl(id)` | 获取原图 URL | | `getPhotoFileUrl(id)` | 获取原图 URL |
| `getPhotoPreviewUrl(id)` | 获取预览图 URL | | `getPhotoPreviewUrl(id)` | 获取预览图 URL |
--- ---
@@ -179,24 +179,24 @@ pnpm check:watch
### 基础 UI 组件 ### 基础 UI 组件
| 组件 | 说明 | | 组件 | 说明 |
|------|------| | ----------- | ---------- |
| `Button` | 按钮组件 | | `Button` | 按钮组件 |
| `Card` | 卡片容器 | | `Card` | 卡片容器 |
| `Container` | 页面容器 | | `Container` | 页面容器 |
| `Grid` | 网格布局 | | `Grid` | 网格布局 |
| `Loading` | 加载状态 | | `Loading` | 加载状态 |
| `Empty` | 空状态提示 | | `Empty` | 空状态提示 |
### 业务组件 ### 业务组件
| 组件 | 说明 | | 组件 | 说明 |
|------|------| | ------------ | -------- |
| `AlbumCard` | 相册卡片 | | `AlbumCard` | 相册卡片 |
| `AlbumList` | 相册列表 | | `AlbumList` | 相册列表 |
| `PhotoCard` | 照片卡片 | | `PhotoCard` | 照片卡片 |
| `PhotoGrid` | 照片网格 | | `PhotoGrid` | 照片网格 |
| `BackLink` | 返回链接 | | `BackLink` | 返回链接 |
| `PageHeader` | 页面标题 | | `PageHeader` | 页面标题 |
--- ---
@@ -228,7 +228,7 @@ pnpm check:watch
```svelte ```svelte
<script> <script>
import * as m from '$lib/paraglide/messages'; import * as m from '$lib/paraglide/messages';
</script> </script>
<h1>{m.welcome()}</h1> <h1>{m.welcome()}</h1>
@@ -262,16 +262,16 @@ pnpm drizzle-kit migrate
## 📜 脚本命令 ## 📜 脚本命令
| 命令 | 说明 | | 命令 | 说明 |
|------|------| | ------------------ | --------------- |
| `pnpm dev` | 启动开发服务器 | | `pnpm dev` | 启动开发服务器 |
| `pnpm build` | 构建生产版本 | | `pnpm build` | 构建生产版本 |
| `pnpm preview` | 预览生产构建 | | `pnpm preview` | 预览生产构建 |
| `pnpm test` | 运行测试 | | `pnpm test` | 运行测试 |
| `pnpm test:unit` | 运行单元测试 | | `pnpm test:unit` | 运行单元测试 |
| `pnpm check` | 类型检查 | | `pnpm check` | 类型检查 |
| `pnpm lint` | 代码检查 | | `pnpm lint` | 代码检查 |
| `pnpm format` | 格式化代码 | | `pnpm format` | 格式化代码 |
| `pnpm auth:schema` | 生成认证 Schema | | `pnpm auth:schema` | 生成认证 Schema |
--- ---
@@ -304,9 +304,9 @@ pnpm add -D @sveltejs/adapter-static
import adapter from '@sveltejs/adapter-node'; import adapter from '@sveltejs/adapter-node';
const config = { const config = {
kit: { kit: {
adapter: adapter() adapter: adapter()
} }
}; };
``` ```

View File

@@ -49,7 +49,10 @@ const handleApiProxy = async ({ event, resolve }) => {
const backendResponse = await fetch(targetUrl, { const backendResponse = await fetch(targetUrl, {
method: request.method, method: request.method,
headers: request.headers, headers: request.headers,
body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.arrayBuffer() : undefined body:
request.method !== 'GET' && request.method !== 'HEAD'
? await request.arrayBuffer()
: undefined
}); });
// 返回后端响应 // 返回后端响应

View File

@@ -52,9 +52,7 @@ export async function getAlbumPhotos(albumId, options = {}) {
if (options.order) params.set('order', String(options.order)); if (options.order) params.set('order', String(options.order));
const query = params.toString(); const query = params.toString();
const endpoint = query const endpoint = query ? `/album/${albumId}/photo?${query}` : `/album/${albumId}/photo`;
? `/album/${albumId}/photo?${query}`
: `/album/${albumId}/photo`;
return fetchApi(endpoint); return fetchApi(endpoint);
} }

View File

@@ -13,7 +13,15 @@
*/ */
/** @type {PhotoCardProps} */ /** @type {PhotoCardProps} */
let { photo, onUpgradeQuality, borderLess = false, waterfall = false, index = 0, sortMode = 'fileName', sortOrder = 'asc' } = $props(); let {
photo,
onUpgradeQuality,
borderLess = false,
waterfall = false,
index = 0,
sortMode = 'fileName',
sortOrder = 'asc'
} = $props();
// 使用 $derived 确保响应式更新 // 使用 $derived 确保响应式更新
let previewSrc = $derived(`/api/v1/photo/${photo.id}/preview`); let previewSrc = $derived(`/api/v1/photo/${photo.id}/preview`);
@@ -23,9 +31,7 @@
// 根据原始宽高比计算瀑布流显示比例,防止图片加载前容器塌陷 // 根据原始宽高比计算瀑布流显示比例,防止图片加载前容器塌陷
let aspectRatioStyle = $derived( let aspectRatioStyle = $derived(
waterfall && photo.width && photo.height waterfall && photo.width && photo.height ? `${photo.width} / ${photo.height}` : ''
? `${photo.width} / ${photo.height}`
: ''
); );
// 砖块模式:交错延迟(按网格行顺序);瀑布模式:同时入场(避免列填充顺序与延迟不匹配) // 砖块模式:交错延迟(按网格行顺序);瀑布模式:同时入场(避免列填充顺序与延迟不匹配)
@@ -44,7 +50,13 @@
} }
</script> </script>
<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};`}> <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}> <div class="photo-wrapper" class:photo-borderless={borderLess} class:photo-waterfall={waterfall}>
{#if photo.mimeType?.startsWith('video/')} {#if photo.mimeType?.startsWith('video/')}
<div class="video-indicator">🎬</div> <div class="video-indicator">🎬</div>
@@ -70,13 +82,13 @@
</a> </a>
<style> <style>
.photo-card { .photo-card {
display: block; display: block;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
animation: card-enter 0.35s cubic-bezier(0.4, 0, 0.2, 1) both; animation: card-enter 0.35s cubic-bezier(0.4, 0, 0.2, 1) both;
animation-delay: var(--wf-delay, 0ms); animation-delay: var(--wf-delay, 0ms);
} }
@keyframes card-enter { @keyframes card-enter {
from { from {
@@ -100,16 +112,18 @@
margin-bottom: 0; margin-bottom: 0;
} }
.photo-wrapper { .photo-wrapper {
position: relative; position: relative;
aspect-ratio: 1; aspect-ratio: 1;
background: linear-gradient(135deg, var(--color-bg-tertiary) 0%, var(--color-border) 100%); background: linear-gradient(135deg, var(--color-bg-tertiary) 0%, var(--color-border) 100%);
border-radius: var(--radius-md); border-radius: var(--radius-md);
overflow: hidden; overflow: hidden;
margin-bottom: var(--space-sm); margin-bottom: var(--space-sm);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
transition: border-radius 0.25s ease-out, margin-bottom 0.25s ease-out; transition:
} border-radius 0.25s ease-out,
margin-bottom 0.25s ease-out;
}
.photo-borderless { .photo-borderless {
border-radius: 0; border-radius: 0;
@@ -123,13 +137,13 @@
height: auto; height: auto;
} }
.photo-wrapper img { .photo-wrapper img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
transition: transform var(--transition-slow); transition: transform var(--transition-slow);
background: var(--color-border); background: var(--color-border);
} }
/* 瀑布流模式下图片高度自适应 */ /* 瀑布流模式下图片高度自适应 */
.photo-waterfall img { .photo-waterfall img {
@@ -138,51 +152,51 @@
height: auto; height: auto;
} }
.photo-card:hover .photo-wrapper img { .photo-card:hover .photo-wrapper img {
transform: scale(1.08); transform: scale(1.08);
} }
.photo-placeholder { .photo-placeholder {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
padding: var(--space-md); padding: var(--space-md);
text-align: center; text-align: center;
word-break: break-word; word-break: break-word;
} }
/* 瀑布流模式下 placeholder 默认 4:3 */ /* 瀑布流模式下 placeholder 默认 4:3 */
.photo-waterfall .photo-placeholder { .photo-waterfall .photo-placeholder {
aspect-ratio: 4/3; aspect-ratio: 4/3;
} }
.video-indicator { .video-indicator {
position: absolute; position: absolute;
top: var(--space-sm); top: var(--space-sm);
right: var(--space-sm); right: var(--space-sm);
background: var(--color-overlay-dark); background: var(--color-overlay-dark);
color: var(--color-text-inverse); color: var(--color-text-inverse);
padding: 0.35rem var(--space-sm); padding: 0.35rem var(--space-sm);
border-radius: var(--radius-md); border-radius: var(--radius-md);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
z-index: 1; z-index: 1;
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
} }
.photo-name { .photo-name {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
color: var(--color-text); color: var(--color-text);
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
padding: 0 var(--space-xs); padding: 0 var(--space-xs);
transition: visibility var(--transition-slow) cubic-bezier(0.4, 0, 0.2, 1); transition: visibility var(--transition-slow) cubic-bezier(0.4, 0, 0.2, 1);
} }
.photo-name-hide { .photo-name-hide {
display: none; display: none;
} }

View File

@@ -104,7 +104,14 @@
{#each splitToColumns(getVisiblePhotos(), columnCount) as col} {#each splitToColumns(getVisiblePhotos(), columnCount) as col}
<div class="waterfall-column"> <div class="waterfall-column">
{#each col as photo (photo.id)} {#each col as photo (photo.id)}
<PhotoCard {photo} {onUpgradeQuality} {borderLess} {waterfall} {sortMode} {sortOrder} /> <PhotoCard
{photo}
{onUpgradeQuality}
{borderLess}
{waterfall}
{sortMode}
{sortOrder}
/>
{/each} {/each}
</div> </div>
{/each} {/each}
@@ -112,7 +119,15 @@
{:else} {:else}
<div class="photo-grid" class:photo-grid-borderless={borderLess}> <div class="photo-grid" class:photo-grid-borderless={borderLess}>
{#each getVisiblePhotos() as photo, i (photo.id)} {#each getVisiblePhotos() as photo, i (photo.id)}
<PhotoCard {photo} {onUpgradeQuality} {borderLess} {waterfall} index={i} {sortMode} {sortOrder} /> <PhotoCard
{photo}
{onUpgradeQuality}
{borderLess}
{waterfall}
index={i}
{sortMode}
{sortOrder}
/>
{/each} {/each}
</div> </div>
{/if} {/if}
@@ -135,98 +150,98 @@
{/if} {/if}
<style> <style>
.photo-scroll-container { .photo-scroll-container {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
scroll-behavior: smooth; scroll-behavior: smooth;
min-height: 400px; min-height: 400px;
} }
.photo-scroll-container::-webkit-scrollbar { .photo-scroll-container::-webkit-scrollbar {
width: 8px; width: 8px;
} }
.photo-scroll-container::-webkit-scrollbar-track { .photo-scroll-container::-webkit-scrollbar-track {
background: var(--color-bg-tertiary); background: var(--color-bg-tertiary);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
} }
.photo-scroll-container::-webkit-scrollbar-thumb { .photo-scroll-container::-webkit-scrollbar-thumb {
background: var(--color-border); background: var(--color-border);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
} }
.photo-scroll-container::-webkit-scrollbar-thumb:hover { .photo-scroll-container::-webkit-scrollbar-thumb:hover {
background: var(--color-text-tertiary); background: var(--color-text-tertiary);
} }
.photo-grid { .photo-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: var(--space-md); gap: var(--space-md);
padding-bottom: var(--space-xl); padding-bottom: var(--space-xl);
transition: gap 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: gap 0.3s cubic-bezier(0.4, 0, 0.2, 1);
} }
.photo-grid-borderless { .photo-grid-borderless {
gap: 0; gap: 0;
} }
/* 瀑布流布局Flex 列分栏 */ /* 瀑布流布局Flex 列分栏 */
.photo-grid-waterfall { .photo-grid-waterfall {
display: flex; display: flex;
gap: var(--space-md); gap: var(--space-md);
padding-bottom: var(--space-xl); padding-bottom: var(--space-xl);
} }
.photo-grid-waterfall.photo-grid-borderless { .photo-grid-waterfall.photo-grid-borderless {
gap: 0; gap: 0;
} }
.waterfall-column { .waterfall-column {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
} }
.photo-grid-waterfall .waterfall-column > :global(.photo-card) { .photo-grid-waterfall .waterfall-column > :global(.photo-card) {
break-inside: avoid; break-inside: avoid;
margin-bottom: var(--space-md); margin-bottom: var(--space-md);
} }
.photo-grid-waterfall .waterfall-column > :global(.photo-card):last-child { .photo-grid-waterfall .waterfall-column > :global(.photo-card):last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.photo-grid-waterfall.photo-grid-borderless .waterfall-column > :global(.photo-card) { .photo-grid-waterfall.photo-grid-borderless .waterfall-column > :global(.photo-card) {
margin-bottom: 0; margin-bottom: 0;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.photo-grid { .photo-grid {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
} }
} }
.load-more-trigger, .load-more-trigger,
.loading-trigger { .loading-trigger {
text-align: center; text-align: center;
padding: var(--space-xl); padding: var(--space-xl);
} }
.loading-more { .loading-more {
display: inline-block; display: inline-block;
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-size: var(--font-size-base); font-size: var(--font-size-base);
} }
.load-complete { .load-complete {
text-align: center; text-align: center;
padding: var(--space-xl); padding: var(--space-xl);
color: var(--color-success); color: var(--color-success);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
} }
</style> </style>

View File

@@ -55,7 +55,8 @@
--radius-full: 9999px; --radius-full: 9999px;
/* ========== Typography ========== */ /* ========== Typography ========== */
--font-family: 'Inter', 'Noto Sans SC', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; --font-family:
'Inter', 'Noto Sans SC', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-size-xs: 0.75rem; /* 12px */ --font-size-xs: 0.75rem; /* 12px */
--font-size-sm: 0.875rem; /* 14px */ --font-size-sm: 0.875rem; /* 14px */
--font-size-base: 1rem; /* 16px */ --font-size-base: 1rem; /* 16px */

View File

@@ -33,7 +33,7 @@ export async function load({ params, fetch, url }) {
// 检测后端是否返回了分页信息 // 检测后端是否返回了分页信息
const hasMore = photos?.items const hasMore = photos?.items
? (photos.page * photos.size) < photos.total ? photos.page * photos.size < photos.total
: photos?.length === pageSize; : photos?.length === pageSize;
const totalPhotos = photos?.total ?? photos?.length ?? 0; const totalPhotos = photos?.total ?? photos?.length ?? 0;

View File

@@ -2,15 +2,31 @@
import { page } from '$app/state'; import { page } from '$app/state';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import { albums, back, photo_count, loading, import {
style_card, style_compact, albums,
layout_grid, layout_waterfall, back,
sort_name, sort_time, sort_size, photo_count,
sort_asc, sort_desc loading,
style_card,
style_compact,
layout_grid,
layout_waterfall,
sort_name,
sort_time,
sort_size,
sort_asc,
sort_desc
} from '$lib/paraglide/messages'; } from '$lib/paraglide/messages';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { SvelteSet } from 'svelte/reactivity'; import { SvelteSet } from 'svelte/reactivity';
import { Container, PageHeader, BackLink, PhotoGrid, Empty, SegmentedControl } from '$lib/components'; import {
Container,
PageHeader,
BackLink,
PhotoGrid,
Empty,
SegmentedControl
} from '$lib/components';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
let { data } = $props(); let { data } = $props();
@@ -49,9 +65,11 @@
let layoutMode = $state('grid'); let layoutMode = $state('grid');
// 排序模式 // 排序模式
let sortMode = $state(page.data.album ? (page.url.searchParams.get('sort') || 'fileName') : 'fileName'); 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'); let sortOrder = $state(page.data.album ? page.url.searchParams.get('order') || 'asc' : 'asc');
const styleOptions = $derived([ const styleOptions = $derived([
{ value: 'card', label: style_card(), icon: '🖼️' }, { value: 'card', label: style_card(), icon: '🖼️' },
@@ -133,7 +151,7 @@
allPhotos = [...allPhotos, ...newPhotos]; allPhotos = [...allPhotos, ...newPhotos];
// 根据后端返回的 total 判断是否还有更多 // 根据后端返回的 total 判断是否还有更多
const total = newData.totalPhotos ?? 0; const total = newData.totalPhotos ?? 0;
hasMore = (currentPage * PAGE_SIZE) < total; hasMore = currentPage * PAGE_SIZE < total;
displayedCount += BATCH_SIZE; displayedCount += BATCH_SIZE;
} }
} catch (error) { } catch (error) {
@@ -187,38 +205,43 @@
</svelte:head> </svelte:head>
<Container size="wide"> <Container size="wide">
<PageHeader <PageHeader title={album?.name || loading()}>
title={album?.name || loading()}
>
<BackLink href={resolve('/')} text={back()} /> <BackLink href={resolve('/')} text={back()} />
</PageHeader> </PageHeader>
{#if album && allPhotos.length > 0} {#if album && allPhotos.length > 0}
<div class="subtitle-bar"> <div class="subtitle-bar">
<span class="photo-count">{photo_count({ count: data.totalPhotos || allPhotos.length })}</span> <span class="photo-count">{photo_count({ count: data.totalPhotos || allPhotos.length })}</span
>
<div class="controls"> <div class="controls">
<SegmentedControl <SegmentedControl
value={styleMode} value={styleMode}
onchange={(v) => styleMode = v} onchange={(v) => (styleMode = v)}
options={styleOptions} options={styleOptions}
size="small" size="small"
/> />
<SegmentedControl <SegmentedControl
value={layoutMode} value={layoutMode}
onchange={(v) => layoutMode = v} onchange={(v) => (layoutMode = v)}
options={layoutOptions} options={layoutOptions}
size="small" size="small"
/> />
<SegmentedControl <SegmentedControl
value={sortMode} value={sortMode}
onchange={(v) => { sortMode = v; applySort(); }} onchange={(v) => {
sortMode = v;
applySort();
}}
options={sortOptions} options={sortOptions}
size="small" size="small"
/> />
<button <button
class="sort-order-toggle" class="sort-order-toggle"
type="button" type="button"
onclick={() => { sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'; applySort(); }} onclick={() => {
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
applySort();
}}
aria-label={sortOrder === 'asc' ? sort_desc() : sort_asc()} aria-label={sortOrder === 'asc' ? sort_desc() : sort_asc()}
> >
<span class="sort-icon">{sortOrder === 'asc' ? '↑' : '↓'}</span> <span class="sort-icon">{sortOrder === 'asc' ? '↑' : '↓'}</span>
@@ -241,8 +264,8 @@
bind:loadMoreTrigger bind:loadMoreTrigger
borderLess={styleMode === 'borderless'} borderLess={styleMode === 'borderless'}
waterfall={layoutMode === 'waterfall'} waterfall={layoutMode === 'waterfall'}
sortMode={sortMode} {sortMode}
sortOrder={sortOrder} {sortOrder}
/> />
{/if} {/if}
</Container> </Container>

View File

@@ -37,7 +37,7 @@ export async function load({ params, fetch, url }) {
`/album/${photo.albumId}/photo?sort=${sort}&order=${order}`, `/album/${photo.albumId}/photo?sort=${sort}&order=${order}`,
fetch fetch
); );
const photos = Array.isArray(albumPhotos) ? albumPhotos : albumPhotos?.items ?? []; const photos = Array.isArray(albumPhotos) ? albumPhotos : (albumPhotos?.items ?? []);
if (photos.length > 0) { if (photos.length > 0) {
const index = photos.findIndex((p) => String(p.id) === String(photoId)); const index = photos.findIndex((p) => String(p.id) === String(photoId));