支持缩放和分辨率适配

This commit is contained in:
2026-03-20 14:01:41 +08:00
parent e76a1dff71
commit ca084cd229
8 changed files with 969 additions and 97 deletions

View File

@@ -208,6 +208,18 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="36" />
<option name="brand" value="samsung" />
<option name="codename" value="a15xtfn" />
<option name="id" value="a15xtfn" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy A15 5G" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="samsung" />
@@ -244,6 +256,18 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="samsung" />
<option name="codename" value="a16xeea" />
<option name="id" value="a16xeea" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A16 5G" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
@@ -436,6 +460,18 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2640" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="36" />
<option name="brand" value="samsung" />
<option name="codename" value="b6qsqw" />
<option name="id" value="b6qsqw" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Flip 6" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2640" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="36" />
<option name="brand" value="google" />

View File

@@ -44,6 +44,5 @@
"vitest": "^4.0.18",
"vitest-browser-svelte": "^2.0.2"
},
"dependencies": {
}
"dependencies": {}
}

View File

@@ -5,7 +5,7 @@ import { svelteKitHandler } from 'better-auth/svelte-kit';
import { getTextDirection } from '$lib/paraglide/runtime';
import { paraglideMiddleware } from '$lib/paraglide/server';
/** @type {import('@sveltejs/kit').Handle} */
/** @type {import('@sveltejs/kit').Handle} */
const handleParaglide = ({ event, resolve }) =>
paraglideMiddleware(event.request, ({ request, locale }) => {
event.request = request;
@@ -18,11 +18,8 @@ const handleParaglide = ({ event, resolve }) =>
});
});
/** @type {import('@sveltejs/kit').Handle} */
const handleBetterAuth = async ({
event,
resolve
}) => {
/** @type {import('@sveltejs/kit').Handle} */
const handleBetterAuth = async ({ event, resolve }) => {
const session = await auth.api.getSession({
/** @type {import('@sveltejs/kit').Handle} */ headers: event.request.headers
});
@@ -38,14 +35,14 @@ const handleBetterAuth = async ({
/** @type {import('@sveltejs/kit').Handle} */
const handleCache = async ({ event, resolve }) => {
const response = await resolve(event);
// 克隆响应以便修改头
const newResponse = new Response(response.body, response);
// 为静态资源添加缓存头
const url = new URL(event.request.url);
const pathname = url.pathname;
// 静态资源缓存 1 年
if (pathname.startsWith('/_app/') || pathname.match(/\.[a-f0-9]+\.(css|js)$/)) {
newResponse.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
@@ -62,16 +59,11 @@ const handleCache = async ({ event, resolve }) => {
else if (pathname.startsWith('/album/')) {
// 相册页面:短时缓存,快速更新
newResponse.headers.set('Cache-Control', 'public, max-age=30, stale-while-revalidate=300');
}
else {
} else {
newResponse.headers.set('Cache-Control', 'public, max-age=0, stale-while-revalidate=60');
}
return newResponse;
};
export const handle = sequence(
handleParaglide,
handleBetterAuth,
handleCache
);
export const handle = sequence(handleParaglide, handleBetterAuth, handleCache);

View File

@@ -6,7 +6,7 @@ import { getRequestEvent } from '$app/server';
export const auth = betterAuth({
baseURL: env.ORIGIN,
secret: env.BETTER_AUTH_SECRET,
// database: drizzleAdapter(db, { provider: 'sqlite' }),
// database: drizzleAdapter(db, { provider: 'sqlite' }),
emailAndPassword: { enabled: true },
plugins: [sveltekitCookies(getRequestEvent)] // make sure this is the last plugin in the array
});

View File

@@ -3,7 +3,7 @@ import { fetchApi } from '$lib/api/client';
/** @type {import('./$types').PageServerLoad} */
export async function load({ params, fetch, url }) {
const albumId = params.id;
// 支持分页参数,默认每页 100 张照片
const page = parseInt(url.searchParams.get('page') || '1', 10);
const pageSize = parseInt(url.searchParams.get('pageSize') || '100', 10);
@@ -28,8 +28,8 @@ export async function load({ params, fetch, url }) {
const hasMore = photos?.length === pageSize;
const totalPhotos = photos?.total ?? photos?.length ?? 0;
return {
album,
return {
album,
photos: photos?.items ?? photos ?? [],
hasMore,
totalPhotos

View File

@@ -11,14 +11,14 @@
/** @type {import('$lib/api/types').Photo[]} */
let initialPhotos = $derived(data.photos ?? []);
let hasMore = $derived(data.hasMore ?? false);
// 所有已加载的照片(支持分页追加)
let allPhotos = $state([...initialPhotos]);
// 分批加载配置 - 前端虚拟滚动
const BATCH_SIZE = 50;
let displayedCount = $state(BATCH_SIZE);
// 当前页码
let currentPage = $state(1);
const PAGE_SIZE = 100;
@@ -30,6 +30,9 @@
/** @type {boolean} */
let isLoading = $state(false);
// 已升级质量的图片 ID 集合,避免重复升级
let upgradedPhotoIds = $state(new Set());
// 监听数据变化,重置状态
$effect(() => {
if (data.photos) {
@@ -37,6 +40,7 @@
displayedCount = BATCH_SIZE;
currentPage = 1;
isLoading = false;
upgradedPhotoIds = new Set();
}
});
@@ -77,16 +81,16 @@
// 加载下一页(从服务器)
async function loadNextPage() {
if (isLoading) return;
isLoading = true;
currentPage++;
try {
const response = await fetch(
`${page.url.pathname}?page=${currentPage}&pageSize=${PAGE_SIZE}`
);
const newData = await response.json();
if (newData.photos) {
const newPhotos = newData.photos.items ?? newData.photos;
allPhotos = [...allPhotos, ...newPhotos];
@@ -103,6 +107,34 @@
function getVisiblePhotos() {
return allPhotos.slice(0, displayedCount);
}
/**
* 当用户悬停或聚焦图片时,升级图片质量
* @param {number} photoId
* @param {string} previewSrc
*/
function upgradePhotoQuality(photoId, previewSrc) {
// 避免重复升级
if (upgradedPhotoIds.has(photoId)) return;
console.log('upgradePhotoQuality', photoId);
upgradedPhotoIds.add(photoId);
// 创建新图片对象预加载高分辨率版本
const highResImg = new Image();
highResImg.src = `${previewSrc}?w=1000`;
highResImg.fetchPriority = 'low';
// 找到对应的 img 元素并更新 srcset
const imgElement = document.querySelector(`img[data-photo-id="${photoId}"]`);
if (imgElement && !imgElement.dataset.upgraded) {
imgElement.dataset.upgraded = 'true';
imgElement.srcset = `${previewSrc}?w=1000 1000w, ${previewSrc}?w=1200 1200w`;
imgElement.sizes = '(max-width: 768px) 150px, (max-width: 1200px) 200px, 300px';
// 触发浏览器重新加载更高分辨率的图片
imgElement.src = `${previewSrc}?w=1000`;
}
}
</script>
<svelte:head>
@@ -134,15 +166,19 @@
<span>{photo.fileName}</span>
</div>
{:else}
{@const previewBase = getPhotoPreviewUrl(photo.id)}
{@const photoId = photo.id}
{@const previewSrc = `/api/v1/photo/${photoId}/preview`}
<img
src={previewBase}
srcset="{previewBase}?w=400 400w, {previewBase}?w=800 800w"
src={`${previewSrc}?w=800`}
srcset={`${previewSrc}?w=600 600w, ${previewSrc}?w=800 800w`}
sizes="(max-width: 768px) 150px, (max-width: 1200px) 200px, 250px"
alt={photo.fileName}
loading="lazy"
decoding="async"
class="photo-image"
onmouseenter={() => upgradePhotoQuality(photoId, previewSrc)}
onfocus={() => upgradePhotoQuality(photoId, previewSrc)}
data-photo-id={photo.id}
/>
{/if}
</div>
@@ -335,8 +371,13 @@
}
@keyframes pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
.load-complete {

View File

@@ -31,9 +31,9 @@ export async function load({ params, fetch }) {
try {
const albumPhotos = await fetchApi(`/album/${photo.albumId}/photo`, fetch);
const photos = Array.isArray(albumPhotos) ? albumPhotos : [];
if (photos.length > 0) {
const index = photos.findIndex(p => String(p.id) === String(photoId));
const index = photos.findIndex((p) => String(p.id) === String(photoId));
if (index >= 0) {
currentIndex = index;
totalPhotos = photos.length;

File diff suppressed because it is too large Load Diff