格式化前端代码
This commit is contained in:
@@ -24,7 +24,7 @@ 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/) | 构建工具 |
|
||||||
@@ -140,7 +140,7 @@ const API_BASE = '/api/v1';
|
|||||||
### 可用 API 方法
|
### 可用 API 方法
|
||||||
|
|
||||||
| 方法 | 说明 |
|
| 方法 | 说明 |
|
||||||
|------|------|
|
| ------------------------- | -------------- |
|
||||||
| `getAlbums()` | 获取所有相册 |
|
| `getAlbums()` | 获取所有相册 |
|
||||||
| `getAlbum(id)` | 获取相册详情 |
|
| `getAlbum(id)` | 获取相册详情 |
|
||||||
| `getAlbumPhotos(albumId)` | 获取相册内照片 |
|
| `getAlbumPhotos(albumId)` | 获取相册内照片 |
|
||||||
@@ -180,7 +180,7 @@ pnpm check:watch
|
|||||||
### 基础 UI 组件
|
### 基础 UI 组件
|
||||||
|
|
||||||
| 组件 | 说明 |
|
| 组件 | 说明 |
|
||||||
|------|------|
|
| ----------- | ---------- |
|
||||||
| `Button` | 按钮组件 |
|
| `Button` | 按钮组件 |
|
||||||
| `Card` | 卡片容器 |
|
| `Card` | 卡片容器 |
|
||||||
| `Container` | 页面容器 |
|
| `Container` | 页面容器 |
|
||||||
@@ -191,7 +191,7 @@ pnpm check:watch
|
|||||||
### 业务组件
|
### 业务组件
|
||||||
|
|
||||||
| 组件 | 说明 |
|
| 组件 | 说明 |
|
||||||
|------|------|
|
| ------------ | -------- |
|
||||||
| `AlbumCard` | 相册卡片 |
|
| `AlbumCard` | 相册卡片 |
|
||||||
| `AlbumList` | 相册列表 |
|
| `AlbumList` | 相册列表 |
|
||||||
| `PhotoCard` | 照片卡片 |
|
| `PhotoCard` | 照片卡片 |
|
||||||
@@ -263,7 +263,7 @@ pnpm drizzle-kit migrate
|
|||||||
## 📜 脚本命令
|
## 📜 脚本命令
|
||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
|------|------|
|
| ------------------ | --------------- |
|
||||||
| `pnpm dev` | 启动开发服务器 |
|
| `pnpm dev` | 启动开发服务器 |
|
||||||
| `pnpm build` | 构建生产版本 |
|
| `pnpm build` | 构建生产版本 |
|
||||||
| `pnpm preview` | 预览生产构建 |
|
| `pnpm preview` | 预览生产构建 |
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
// 返回后端响应
|
// 返回后端响应
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -108,7 +120,9 @@
|
|||||||
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 {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user