diff --git a/web/README.md b/web/README.md index c93cb31..ffd1add 100644 --- a/web/README.md +++ b/web/README.md @@ -23,18 +23,18 @@ Loongyan 相册系统的前端应用,基于 [SvelteKit](https://kit.svelte.dev ## 🛠️ 技术栈 -| 技术 | 说明 | -|------|------| -| [Svelte 5](https://svelte.dev/) | 前端框架(Runes 模式) | -| [SvelteKit 2](https://kit.svelte.dev/) | 全栈框架 | -| [Vite 7](https://vitejs.dev/) | 构建工具 | -| [Paraglide JS](https://inlang.com/m/gerre34r/library-inlang-paraglideJs) | 国际化 | -| [Better Auth](https://www.better-auth.com/) | 用户认证 | -| [Drizzle ORM](https://orm.drizzle.team/) | 数据库 ORM | -| [SQLite](https://www.sqlite.org/) | 嵌入式数据库 | -| [Vitest](https://vitest.dev/) | 单元测试 | -| [Playwright](https://playwright.dev/) | 浏览器测试 | -| [mdsvex](https://mdsvex.pngwn.io/) | Markdown 组件 | +| 技术 | 说明 | +| ------------------------------------------------------------------------ | ---------------------- | +| [Svelte 5](https://svelte.dev/) | 前端框架(Runes 模式) | +| [SvelteKit 2](https://kit.svelte.dev/) | 全栈框架 | +| [Vite 7](https://vitejs.dev/) | 构建工具 | +| [Paraglide JS](https://inlang.com/m/gerre34r/library-inlang-paraglideJs) | 国际化 | +| [Better Auth](https://www.better-auth.com/) | 用户认证 | +| [Drizzle ORM](https://orm.drizzle.team/) | 数据库 ORM | +| [SQLite](https://www.sqlite.org/) | 嵌入式数据库 | +| [Vitest](https://vitest.dev/) | 单元测试 | +| [Playwright](https://playwright.dev/) | 浏览器测试 | +| [mdsvex](https://mdsvex.pngwn.io/) | Markdown 组件 | --- @@ -139,14 +139,14 @@ const API_BASE = '/api/v1'; ### 可用 API 方法 -| 方法 | 说明 | -|------|------| -| `getAlbums()` | 获取所有相册 | -| `getAlbum(id)` | 获取相册详情 | +| 方法 | 说明 | +| ------------------------- | -------------- | +| `getAlbums()` | 获取所有相册 | +| `getAlbum(id)` | 获取相册详情 | | `getAlbumPhotos(albumId)` | 获取相册内照片 | -| `getPhoto(id)` | 获取照片详情 | -| `getPhotoFileUrl(id)` | 获取原图 URL | -| `getPhotoPreviewUrl(id)` | 获取预览图 URL | +| `getPhoto(id)` | 获取照片详情 | +| `getPhotoFileUrl(id)` | 获取原图 URL | +| `getPhotoPreviewUrl(id)` | 获取预览图 URL | --- @@ -179,24 +179,24 @@ pnpm check:watch ### 基础 UI 组件 -| 组件 | 说明 | -|------|------| -| `Button` | 按钮组件 | -| `Card` | 卡片容器 | -| `Container` | 页面容器 | -| `Grid` | 网格布局 | -| `Loading` | 加载状态 | -| `Empty` | 空状态提示 | +| 组件 | 说明 | +| ----------- | ---------- | +| `Button` | 按钮组件 | +| `Card` | 卡片容器 | +| `Container` | 页面容器 | +| `Grid` | 网格布局 | +| `Loading` | 加载状态 | +| `Empty` | 空状态提示 | ### 业务组件 -| 组件 | 说明 | -|------|------| -| `AlbumCard` | 相册卡片 | -| `AlbumList` | 相册列表 | -| `PhotoCard` | 照片卡片 | -| `PhotoGrid` | 照片网格 | -| `BackLink` | 返回链接 | +| 组件 | 说明 | +| ------------ | -------- | +| `AlbumCard` | 相册卡片 | +| `AlbumList` | 相册列表 | +| `PhotoCard` | 照片卡片 | +| `PhotoGrid` | 照片网格 | +| `BackLink` | 返回链接 | | `PageHeader` | 页面标题 | --- @@ -228,7 +228,7 @@ pnpm check:watch ```svelte

{m.welcome()}

@@ -262,16 +262,16 @@ pnpm drizzle-kit migrate ## 📜 脚本命令 -| 命令 | 说明 | -|------|------| -| `pnpm dev` | 启动开发服务器 | -| `pnpm build` | 构建生产版本 | -| `pnpm preview` | 预览生产构建 | -| `pnpm test` | 运行测试 | -| `pnpm test:unit` | 运行单元测试 | -| `pnpm check` | 类型检查 | -| `pnpm lint` | 代码检查 | -| `pnpm format` | 格式化代码 | +| 命令 | 说明 | +| ------------------ | --------------- | +| `pnpm dev` | 启动开发服务器 | +| `pnpm build` | 构建生产版本 | +| `pnpm preview` | 预览生产构建 | +| `pnpm test` | 运行测试 | +| `pnpm test:unit` | 运行单元测试 | +| `pnpm check` | 类型检查 | +| `pnpm lint` | 代码检查 | +| `pnpm format` | 格式化代码 | | `pnpm auth:schema` | 生成认证 Schema | --- @@ -304,9 +304,9 @@ pnpm add -D @sveltejs/adapter-static import adapter from '@sveltejs/adapter-node'; const config = { - kit: { - adapter: adapter() - } + kit: { + adapter: adapter() + } }; ``` @@ -322,4 +322,4 @@ const config = {
Made with ❤️ using SvelteKit -
\ No newline at end of file + diff --git a/web/src/hooks.server.js b/web/src/hooks.server.js index e76c601..11f5976 100644 --- a/web/src/hooks.server.js +++ b/web/src/hooks.server.js @@ -49,7 +49,10 @@ const handleApiProxy = async ({ event, resolve }) => { const backendResponse = await fetch(targetUrl, { method: request.method, 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 }); // 返回后端响应 @@ -96,4 +99,4 @@ const handleCache = async ({ event, resolve }) => { return newResponse; }; -export const handle = sequence(handleApiProxy, handleParaglide, handleBetterAuth, handleCache); \ No newline at end of file +export const handle = sequence(handleApiProxy, handleParaglide, handleBetterAuth, handleCache); diff --git a/web/src/lib/api/client.js b/web/src/lib/api/client.js index 78490bf..bce9ed2 100644 --- a/web/src/lib/api/client.js +++ b/web/src/lib/api/client.js @@ -50,11 +50,9 @@ export async function getAlbumPhotos(albumId, options = {}) { 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 - ? `/album/${albumId}/photo?${query}` - : `/album/${albumId}/photo`; + const endpoint = query ? `/album/${albumId}/photo?${query}` : `/album/${albumId}/photo`; return fetchApi(endpoint); } diff --git a/web/src/lib/components/photo/PhotoCard.svelte b/web/src/lib/components/photo/PhotoCard.svelte index 1699582..db2049b 100644 --- a/web/src/lib/components/photo/PhotoCard.svelte +++ b/web/src/lib/components/photo/PhotoCard.svelte @@ -13,7 +13,15 @@ */ /** @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 确保响应式更新 let previewSrc = $derived(`/api/v1/photo/${photo.id}/preview`); @@ -23,9 +31,7 @@ // 根据原始宽高比计算瀑布流显示比例,防止图片加载前容器塌陷 let aspectRatioStyle = $derived( - waterfall && photo.width && photo.height - ? `${photo.width} / ${photo.height}` - : '' + waterfall && photo.width && photo.height ? `${photo.width} / ${photo.height}` : '' ); // 砖块模式:交错延迟(按网格行顺序);瀑布模式:同时入场(避免列填充顺序与延迟不匹配) @@ -44,7 +50,13 @@ } - +
{#if photo.mimeType?.startsWith('video/')}
🎬
@@ -70,13 +82,13 @@
diff --git a/web/src/lib/styles/theme.css b/web/src/lib/styles/theme.css index 666c857..5a1fd35 100644 --- a/web/src/lib/styles/theme.css +++ b/web/src/lib/styles/theme.css @@ -55,7 +55,8 @@ --radius-full: 9999px; /* ========== 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-sm: 0.875rem; /* 14px */ --font-size-base: 1rem; /* 16px */ diff --git a/web/src/routes/album/[id]/+page.server.js b/web/src/routes/album/[id]/+page.server.js index a3183c1..be6f9cc 100644 --- a/web/src/routes/album/[id]/+page.server.js +++ b/web/src/routes/album/[id]/+page.server.js @@ -33,7 +33,7 @@ export async function load({ params, fetch, url }) { // 检测后端是否返回了分页信息 const hasMore = photos?.items - ? (photos.page * photos.size) < photos.total + ? photos.page * photos.size < photos.total : photos?.length === pageSize; const totalPhotos = photos?.total ?? photos?.length ?? 0; diff --git a/web/src/routes/album/[id]/+page.svelte b/web/src/routes/album/[id]/+page.svelte index c7d25fb..0dddbec 100644 --- a/web/src/routes/album/[id]/+page.svelte +++ b/web/src/routes/album/[id]/+page.svelte @@ -2,15 +2,31 @@ import { page } from '$app/state'; import { browser } from '$app/environment'; import { m } from '$lib/paraglide/messages'; - import { albums, back, photo_count, loading, - style_card, style_compact, - layout_grid, layout_waterfall, - sort_name, sort_time, sort_size, - sort_asc, sort_desc + import { + albums, + back, + photo_count, + loading, + style_card, + style_compact, + layout_grid, + layout_waterfall, + sort_name, + sort_time, + sort_size, + sort_asc, + sort_desc } from '$lib/paraglide/messages'; import { resolve } from '$app/paths'; 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'; let { data } = $props(); @@ -49,9 +65,11 @@ 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([ { value: 'card', label: style_card(), icon: '🖼️' }, @@ -133,7 +151,7 @@ allPhotos = [...allPhotos, ...newPhotos]; // 根据后端返回的 total 判断是否还有更多 const total = newData.totalPhotos ?? 0; - hasMore = (currentPage * PAGE_SIZE) < total; + hasMore = currentPage * PAGE_SIZE < total; displayedCount += BATCH_SIZE; } } catch (error) { @@ -187,38 +205,43 @@ - + {#if album && allPhotos.length > 0}
- {photo_count({ count: data.totalPhotos || allPhotos.length })} + {photo_count({ count: data.totalPhotos || allPhotos.length })}
styleMode = v} + onchange={(v) => (styleMode = v)} options={styleOptions} size="small" /> layoutMode = v} + onchange={(v) => (layoutMode = v)} options={layoutOptions} size="small" /> { sortMode = v; applySort(); }} + onchange={(v) => { + sortMode = v; + applySort(); + }} options={sortOptions} size="small" />