支持缩放和分辨率适配
This commit is contained in:
36
.idea/caches/deviceStreaming.xml
generated
36
.idea/caches/deviceStreaming.xml
generated
@@ -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" />
|
||||
|
||||
@@ -44,6 +44,5 @@
|
||||
"vitest": "^4.0.18",
|
||||
"vitest-browser-svelte": "^2.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
}
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user