fix: 相册改用绝对定位百分比布局,彻底消除底部黑边

CSS grid + aspect-ratio 下 fr 行在部分浏览器不撑满,留出 bg-black。
改为 Telegram 同款:算法输出每块 {left,top,width,height} 百分比坐标,
图块绝对定位铺满容器。顺带修复竖主图在左时右侧堆叠图高度算错未填满列。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
TerryM
2026-05-29 22:56:20 +08:00
parent 789920d2b9
commit 471d29bec9
3 changed files with 123 additions and 137 deletions

View File

@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest";
import { computeAlbumLayout } from "./albumLayout";
import { computeAlbumLayout, type AlbumTile } from "./albumLayout";
/** Right/bottom edge of a tile, as fractions of the box. */
const right = (t: AlbumTile) => t.left + t.width;
const bottom = (t: AlbumTile) => t.top + t.height;
describe("computeAlbumLayout", () => {
it("returns null for fewer than 2 images", () => {
@@ -7,48 +11,49 @@ describe("computeAlbumLayout", () => {
expect(computeAlbumLayout([1.5])).toBeNull();
});
it("places two portraits side by side (1 row, 2 cols)", () => {
it("places two portraits side by side, splitting the full width", () => {
const layout = computeAlbumLayout([0.7, 0.8])!;
expect(layout.gridTemplateRows).toBe("1fr");
expect(layout.tiles).toHaveLength(2);
// Both tiles occupy the single row, adjacent columns.
expect(layout.tiles[0]).toMatchObject({ colStart: 1, colEnd: 2 });
expect(layout.tiles[1]).toMatchObject({ colStart: 2, colEnd: 3 });
// Album width = sum of ratios when height = 1.
expect(layout.aspectRatio).toBeCloseTo(0.7 + 0.8, 4);
expect(layout.tiles).toHaveLength(2);
// Full height each, adjacent columns covering the whole width.
expect(layout.tiles[0]).toMatchObject({ left: 0, top: 0, height: 1 });
expect(layout.tiles[1].top).toBe(0);
expect(layout.tiles[1].height).toBe(1);
expect(right(layout.tiles[0])).toBeCloseTo(layout.tiles[1].left, 6);
expect(right(layout.tiles[1])).toBeCloseTo(1, 6);
});
it("stacks two landscapes vertically (2 rows, 1 col)", () => {
it("stacks two landscapes vertically, splitting the full height", () => {
const layout = computeAlbumLayout([1.6, 1.4])!;
expect(layout.gridTemplateColumns).toBe("1fr");
expect(layout.tiles[0]).toMatchObject({ rowStart: 1, rowEnd: 2 });
expect(layout.tiles[1]).toMatchObject({ rowStart: 2, rowEnd: 3 });
expect(layout.aspectRatio).toBeCloseTo(1 / (1 / 1.6 + 1 / 1.4), 4);
expect(layout.tiles[0]).toMatchObject({ left: 0, top: 0, width: 1 });
expect(layout.tiles[1].left).toBe(0);
expect(layout.tiles[1].width).toBe(1);
expect(bottom(layout.tiles[0])).toBeCloseTo(layout.tiles[1].top, 6);
expect(bottom(layout.tiles[1])).toBeCloseTo(1, 6);
});
it("puts a portrait primary on the left with the rest stacked right", () => {
it("portrait primary on the left, rest stacked on the right, fully covering", () => {
const layout = computeAlbumLayout([0.6, 1.2, 1.3])!;
// Primary spans both right-hand rows in column 1.
expect(layout.tiles[0]).toEqual({
colStart: 1,
colEnd: 2,
rowStart: 1,
rowEnd: 3,
});
expect(layout.tiles[1]).toMatchObject({ colStart: 2, rowStart: 1 });
expect(layout.tiles[2]).toMatchObject({ colStart: 2, rowStart: 2 });
// Primary occupies the full-height left column.
expect(layout.tiles[0]).toMatchObject({ left: 0, top: 0, height: 1 });
// Secondary tiles share the right column and fill it top-to-bottom.
expect(layout.tiles[1].left).toBeCloseTo(layout.tiles[0].width, 6);
expect(layout.tiles[1].top).toBe(0);
expect(bottom(layout.tiles[1])).toBeCloseTo(layout.tiles[2].top, 6);
expect(bottom(layout.tiles[2])).toBeCloseTo(1, 6);
expect(right(layout.tiles[2])).toBeCloseTo(1, 6);
});
it("puts a landscape primary on top with the rest in a row below", () => {
it("landscape primary on top, rest in a bottom row, fully covering", () => {
const layout = computeAlbumLayout([1.8, 0.9, 1.1])!;
// Primary spans both bottom columns in row 1.
expect(layout.tiles[0]).toEqual({
colStart: 1,
colEnd: 3,
rowStart: 1,
rowEnd: 2,
});
expect(layout.tiles[1]).toMatchObject({ rowStart: 2, colStart: 1 });
expect(layout.tiles[2]).toMatchObject({ rowStart: 2, colStart: 2 });
// Primary occupies the full-width top row.
expect(layout.tiles[0]).toMatchObject({ left: 0, top: 0, width: 1 });
// Secondary tiles share the bottom row and fill it left-to-right.
expect(layout.tiles[1].top).toBeCloseTo(layout.tiles[0].height, 6);
expect(layout.tiles[1].left).toBe(0);
expect(right(layout.tiles[1])).toBeCloseTo(layout.tiles[2].left, 6);
expect(right(layout.tiles[2])).toBeCloseTo(1, 6);
expect(bottom(layout.tiles[2])).toBeCloseTo(1, 6);
});
});