diff --git a/server/src/main/resources/web/picture.ftl b/server/src/main/resources/web/picture.ftl index 648430a3..85464130 100644 --- a/server/src/main/resources/web/picture.ftl +++ b/server/src/main/resources/web/picture.ftl @@ -58,6 +58,17 @@ }); } + // 从原始图片 URL 提取文件名(优先用 data-original-url,避免受反代 / CORS 代理 URL 影响) + function getImageNameFromOriginalUrl(imageElement) { + var originalUrl = imageElement.getAttribute('data-original-url') || ''; + // 去除查询串与锚点 + var noQuery = originalUrl.split('?')[0].split('#')[0]; + // 取路径最后一段作为文件名(兼容 / 与 \) + var lastSep = Math.max(noQuery.lastIndexOf('/'), noQuery.lastIndexOf('\\')); + var name = lastSep >= 0 ? noQuery.substring(lastSep + 1) : noQuery; + return name || 'image'; + } + // 初始化图片查看器 function initImageViewer() { var viewer = new Viewer(document.getElementById('image'), { @@ -66,6 +77,10 @@ button: false, backdrop: false, loop: true, + // Viewer 会将克隆图片的 data-original-url 改为加载地址,因此从原始元素读取标题。 + title: function () { + return getImageNameFromOriginalUrl(this.images[this.index]); + } }); viewer.view(0); // 0 是图片的索引,如果你想点击第一张图片,索引为 0 } diff --git a/server/src/test/java/cn/keking/PictureFilePreviewTests.java b/server/src/test/java/cn/keking/PictureFilePreviewTests.java new file mode 100644 index 00000000..823377a3 --- /dev/null +++ b/server/src/test/java/cn/keking/PictureFilePreviewTests.java @@ -0,0 +1,52 @@ +package cn.keking; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * kkFileView #787 回归测试:直接图片预览(picture.ftl)必须在 Viewer.js 标题里显示文件名。 + * + * 根因:v4.3.0 之后模板把图片元素从 <img> 改成 <div src>,而 HTMLDivElement 没有 DOM 的 + * src 属性,Viewer.js 用 image.src 取标题得到 undefined,导致标题为空。修复方式是在 Viewer 初始化时 + * 配置 title 函数,从 data-original-url 派生文件名。 + * + * Viewer 会将内部克隆图片的 data-original-url 改为实际加载地址(可能是代理地址), + * 标题必须读取 this.images[this.index] 中保留原始 URL 的元素。 + */ +public class PictureFilePreviewTests { + + @Test + void shouldDeriveViewerTitleFromOriginalUrl() throws IOException { + String pictureTemplate = readResource("/web/picture.ftl"); + + // 模板仍保留 data-original-url 供标题派生使用 + assertTrue(pictureTemplate.contains("data-original-url"), + () -> "picture.ftl must keep data-original-url for title derivation (see #787)"); + + // Viewer 初始化必须配置 title 函数,否则
的 image.src 为 undefined、标题为空 + assertTrue(pictureTemplate.contains("title: function"), + () -> "picture.ftl must configure a Viewer title function to show the filename (see #787)"); + + // 标题派生逻辑必须读取 data-original-url(而非反代 / CORS 代理 URL) + assertTrue(pictureTemplate.contains("getImageNameFromOriginalUrl"), + () -> "picture.ftl must derive the title from data-original-url (see #787)"); + assertTrue(pictureTemplate.contains("getAttribute('data-original-url')"), + () -> "title derivation must read data-original-url attribute (see #787)"); + + assertTrue(pictureTemplate.contains("getImageNameFromOriginalUrl(this.images[this.index])"), + () -> "Viewer title must use the original element rather than its proxy-backed clone (see #787)"); + } + + private String readResource(String resourcePath) throws IOException { + try (InputStream inputStream = getClass().getResourceAsStream(resourcePath)) { + assertNotNull(inputStream); + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } + } +} diff --git a/tests/e2e/specs/picture-title.spec.ts b/tests/e2e/specs/picture-title.spec.ts new file mode 100644 index 00000000..aff9b6fa --- /dev/null +++ b/tests/e2e/specs/picture-title.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@playwright/test'; + +const fixtureBase = process.env.FIXTURE_BASE_URL || 'http://127.0.0.1:18080'; + +for (const proxy of [false, true]) { + test(`image title retains the source filename with kkagent=${proxy}`, async ({ page }) => { + const source = `${fixtureBase}/sample.png`; + const encoded = encodeURIComponent(Buffer.from(source).toString('base64')); + const response = await page.goto(`/onlinePreview?url=${encoded}&kkagent=${proxy}`); + + expect(response?.status()).toBe(200); + await expect(page.locator('.viewer-title')).toHaveText('sample.png'); + }); +}