Browse Source

fix:拖拽优化,瀑布流优化

version/2.1.0
WIN-UGQIHHLSKBB\EDY 1 month ago
parent
commit
9e522ff02b
  1. 1
      src/hooks/index.ts
  2. 46
      src/hooks/useProbedImageDimensions.ts
  3. 4
      src/pages/Create/panels/InspirationLibraryPanel/BackgroundAssets/BackgroundAssets.scss
  4. 21
      src/pages/Create/panels/InspirationLibraryPanel/BackgroundAssets/BackgroundAssets.tsx
  5. 1
      src/pages/Create/panels/InspirationLibraryPanel/TemplateAssets/TemplateAssets.scss
  6. 10
      src/utils/imageDragTransfer.ts
  7. 9
      src/utils/index.ts
  8. 108
      src/utils/probeImageDimensions.ts
  9. 4
      vite.config.mts

1
src/hooks/index.ts

@ -1,3 +1,4 @@
/** 自定义 Hooks 统一导出 */
export { useImageDragTransfer, useWarmupImageDragUrls } from "./useImageDragTransfer";
export type { ImageDragStartOptions, ImageDragTransferHandle } from "./useImageDragTransfer";
export { useProbedImageDimensions } from "./useProbedImageDimensions";

46
src/hooks/useProbedImageDimensions.ts

@ -0,0 +1,46 @@
import { useCallback, useEffect, useState } from "react";
import {
getCachedImageSize,
normalizeImageProbeUrl,
probeImageSize,
type ProbedImageSize,
} from "@/utils/probeImageDimensions";
const PROBE_BATCH_SIZE = 8;
/** 列表图片 URL 批量探测尺寸;探测完成后触发重渲染,但不改变瀑布流分列。 */
export function useProbedImageDimensions(urls: readonly string[]) {
const [probeVersion, setProbeVersion] = useState(0);
useEffect(() => {
const uniqueUrls = [...new Set(urls.map(normalizeImageProbeUrl).filter(Boolean))];
const missingUrls = uniqueUrls.filter((url) => getCachedImageSize(url) == null);
if (missingUrls.length === 0) return;
let cancelled = false;
void (async () => {
for (let index = 0; index < missingUrls.length; index += PROBE_BATCH_SIZE) {
if (cancelled) return;
const batch = missingUrls.slice(index, index + PROBE_BATCH_SIZE);
await Promise.all(batch.map((url) => probeImageSize(url)));
if (!cancelled) {
setProbeVersion((value) => value + 1);
}
}
})();
return () => {
cancelled = true;
};
}, [urls]);
const getSize = useCallback(
(rawUrl: unknown): ProbedImageSize | undefined => {
void probeVersion;
return getCachedImageSize(rawUrl);
},
[probeVersion],
);
return { getSize };
}

4
src/pages/Create/panels/InspirationLibraryPanel/BackgroundAssets/BackgroundAssets.scss

@ -53,8 +53,8 @@
line-height: 0;
}
.backgroundLibrary__cardMedia_placeholder {
aspect-ratio: 4 / 3;
.backgroundLibrary__cardMedia_pending {
min-height: 24px;
}
.backgroundLibrary__cardImage {

21
src/pages/Create/panels/InspirationLibraryPanel/BackgroundAssets/BackgroundAssets.tsx

@ -7,12 +7,13 @@ import type { AssetLibraryModel } from "../CharacterAssetsPanel/CharacterAssetsP
import "./BackgroundAssets.scss";
import { useGlobalPromptImageStore } from "@/store/useMakeSameStore";
import { useImageDragTransfer, useWarmupImageDragUrls } from "@/hooks/useImageDragTransfer";
import { useProbedImageDimensions } from "@/hooks/useProbedImageDimensions";
import {
appendToShortestColumns,
distributeToShortestColumns,
estimateRecordAspectWeight,
estimateAspectWeightWithProbe,
resolveColumnCountFromWidth,
resolveRecordImageDimensions,
resolveMediaDimensionsWithProbe,
} from "@/utils";
const gridRoot = "backgroundLibrary";
@ -29,8 +30,8 @@ type MasonryLayoutCache = {
columns: unknown[][];
};
function itemWeight(row: unknown): number {
return estimateRecordAspectWeight(row as Record<string, unknown>);
function itemWeight(row: unknown, getProbedSize: (url: string) => { width: number; height: number } | undefined): number {
return estimateAspectWeightWithProbe(row as Record<string, unknown>, getProbedSize);
}
function responseDataList(res: unknown): unknown[] {
@ -204,6 +205,9 @@ export function BackgroundAssetsPanel({
[list],
);
useWarmupImageDragUrls(warmupPreviewUrls, previewUrls);
const { getSize: getProbedSize } = useProbedImageDimensions(previewUrls);
const getProbedSizeRef = useRef(getProbedSize);
getProbedSizeRef.current = getProbedSize;
loadingStateRef.current = { loading, loadingMore };
@ -238,18 +242,19 @@ export function BackgroundAssetsPanel({
}, [loading, list.length]);
const masonryColumns = useMemo(() => {
const getWeight = (row: unknown) => itemWeight(row, (url) => getProbedSizeRef.current(url));
const cache = masonryCacheRef.current;
const canIncrementalAppend =
cache != null && cache.columnCount === columnCount && list.length > cache.listLength;
if (canIncrementalAppend) {
const newItems = list.slice(cache.listLength);
const columns = appendToShortestColumns(cache.columns, newItems, itemWeight);
const columns = appendToShortestColumns(cache.columns, newItems, getWeight);
masonryCacheRef.current = { columnCount, listLength: list.length, columns };
return columns;
}
const columns = distributeToShortestColumns(list, columnCount, itemWeight);
const columns = distributeToShortestColumns(list, columnCount, getWeight);
masonryCacheRef.current = { columnCount, listLength: list.length, columns };
return columns;
}, [columnCount, list]);
@ -288,7 +293,7 @@ export function BackgroundAssetsPanel({
<div className='backgroundLibrary__masonryColumn' key={`col-${columnIdx}`}>
{column.map((row) => {
const item = row as Record<string, unknown>;
const imageDims = resolveRecordImageDimensions(item);
const imageDims = resolveMediaDimensionsWithProbe(item, getProbedSize);
const mediaStyle: CSSProperties | undefined = imageDims.aspectRatio
? { aspectRatio: imageDims.aspectRatio }
: undefined;
@ -302,7 +307,7 @@ export function BackgroundAssetsPanel({
<div
className={[
"backgroundLibrary__cardMedia",
imageDims.aspectRatio ? "" : "backgroundLibrary__cardMedia_placeholder",
imageDims.aspectRatio ? "" : "backgroundLibrary__cardMedia_pending",
]
.filter(Boolean)
.join(" ")}

1
src/pages/Create/panels/InspirationLibraryPanel/TemplateAssets/TemplateAssets.scss

@ -39,7 +39,6 @@
inset: 0;
border-radius: 7px;
overflow: hidden;
pointer-events: none;
}
.templateLibrary__cardImage {

10
src/utils/imageDragTransfer.ts

@ -8,7 +8,7 @@ export interface StartImageDragParams {
dragUrl?: unknown;
/** 无法使用 DOM 图时的预览 URL */
previewUrl?: unknown;
/** 为 true 时只用拖拽源 DOM 图做预览(如三视图数据 + 头像展示) */
/** 为 true 时只用拖拽源 DOM 图做预览(如三视图 URL + 头像展示) */
preferDomDragImage?: boolean;
}
@ -252,12 +252,11 @@ export function createImageDragTransferManager(options?: CreateImageDragTransfer
evt.dataTransfer.setData("text/uri-list", dataUrl);
evt.dataTransfer.setData("text/html", `<img src="${dataUrl}" />`);
if (preferDomDragImage) {
const domImage = pickLoadedImageFromDragTarget(evt);
if (domImage && trySetDragImageFromElement(evt.dataTransfer, domImage)) {
return true;
}
if (preferDomDragImage) {
return true;
}
@ -267,6 +266,11 @@ export function createImageDragTransferManager(options?: CreateImageDragTransfer
return true;
}
const domImage = pickLoadedImageFromDragTarget(evt);
if (domImage && trySetDragImageFromElement(evt.dataTransfer, domImage)) {
return true;
}
return true;
};

9
src/utils/index.ts

@ -11,6 +11,15 @@ export {
resolveRecordImageDimensions,
} from "./masonryLayout";
export type { RecordImageDimensions } from "./masonryLayout";
export {
estimateAspectWeightWithProbe,
getCachedImageSize,
normalizeImageProbeUrl,
probeImageSize,
probeImageSizes,
resolveMediaDimensionsWithProbe,
} from "./probeImageDimensions";
export type { ProbedImageSize } from "./probeImageDimensions";
/** 安全解析 JSON,失败返回 null */
export function safeParse<T>(str: string, fallback: T): T {

108
src/utils/probeImageDimensions.ts

@ -0,0 +1,108 @@
import { resolveRecordImageDimensions, type RecordImageDimensions } from "./masonryLayout";
export type ProbedImageSize = {
width: number;
height: number;
};
const sizeCache = new Map<string, ProbedImageSize>();
const pendingProbes = new Map<string, Promise<ProbedImageSize | null>>();
export function normalizeImageProbeUrl(rawUrl: unknown): string {
return typeof rawUrl === "string" ? rawUrl.trim() : "";
}
/** 读取已探测过的图片尺寸(跨组件、跨渲染复用)。 */
export function getCachedImageSize(rawUrl: unknown): ProbedImageSize | undefined {
const url = normalizeImageProbeUrl(rawUrl);
if (!url) return undefined;
return sizeCache.get(url);
}
/** 预加载图片并读取 naturalWidth / naturalHeight。 */
export function probeImageSize(rawUrl: unknown): Promise<ProbedImageSize | null> {
const url = normalizeImageProbeUrl(rawUrl);
if (!url) return Promise.resolve(null);
const cached = sizeCache.get(url);
if (cached) return Promise.resolve(cached);
const pending = pendingProbes.get(url);
if (pending) return pending;
const task = new Promise<ProbedImageSize | null>((resolve) => {
const img = new Image();
const finish = (size: ProbedImageSize | null) => {
pendingProbes.delete(url);
if (size) sizeCache.set(url, size);
resolve(size);
};
const onLoad = () => {
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
finish({ width: img.naturalWidth, height: img.naturalHeight });
return;
}
finish(null);
};
const onError = () => finish(null);
img.addEventListener("load", onLoad, { once: true });
img.addEventListener("error", onError, { once: true });
img.decoding = "async";
img.src = url;
});
pendingProbes.set(url, task);
return task;
}
const PROBE_BATCH_SIZE = 8;
/** 批量探测尚未缓存的图片尺寸。 */
export async function probeImageSizes(
rawUrls: readonly unknown[],
options?: { batchSize?: number; signal?: AbortSignal },
): Promise<void> {
const batchSize = Math.max(1, options?.batchSize ?? PROBE_BATCH_SIZE);
const uniqueUrls = [...new Set(rawUrls.map(normalizeImageProbeUrl).filter(Boolean))].filter(
(url) => !sizeCache.has(url),
);
if (uniqueUrls.length === 0) return;
for (let index = 0; index < uniqueUrls.length; index += batchSize) {
if (options?.signal?.aborted) return;
const batch = uniqueUrls.slice(index, index + batchSize);
await Promise.all(batch.map((url) => probeImageSize(url)));
}
}
/** 合并接口字段与前端探测结果,优先使用接口宽高。 */
export function resolveMediaDimensionsWithProbe(
row: Record<string, unknown>,
getProbedSize: (url: string) => ProbedImageSize | undefined,
): RecordImageDimensions {
const fromApi = resolveRecordImageDimensions(row);
if (fromApi.aspectRatio) return fromApi;
const url = normalizeImageProbeUrl(row.url);
if (!url) return {};
const probed = getProbedSize(url);
if (probed == null) return {};
return {
width: probed.width,
height: probed.height,
aspectRatio: `${probed.width} / ${probed.height}`,
};
}
/** 估算卡片高度权重:接口或探测尺寸优先,否则 fallback。 */
export function estimateAspectWeightWithProbe(
row: Record<string, unknown>,
getProbedSize: (url: string) => ProbedImageSize | undefined,
fallback = 0.75,
): number {
const dims = resolveMediaDimensionsWithProbe(row, getProbedSize);
if (dims.width != null && dims.height != null) return dims.height / dims.width;
return fallback;
}

4
vite.config.mts

@ -39,9 +39,9 @@ export default defineConfig(({ mode }) => {
proxy: isDev
? {
"/api_client": {
target: "http://192.168.77.111:8080/",
// target: "http://192.168.77.111:8080/",
// target: "https://wwwtest.popi.art/",
// target: "https://www.popi.art/",
target: "https://www.popi.art/",
secure: false,
changeOrigin: true,
},

Loading…
Cancel
Save