fix: avoid applying thumbnail proxy twice in gallery details

This commit is contained in:
root
2026-10-07 23:50:52 +08:00
parent 97d635fbc7
commit de7971fb23
2 changed files with 178 additions and 2 deletions
+3 -2
View File
@@ -51,11 +51,12 @@ const resolutions = computed(() => {
}));
});
// Remote galleries carry a raw path; the proxy URL needs the active auth code.
// A task coming back from the task list already has its thumbnail proxied by
// normalizeTask. Only a raw upstream path (thumbnailUrl) still needs the proxy.
const remoteThumb = computed(() => {
const gallery = remote.value;
if (!gallery) return "";
return ehThumbnailUrl(gallery.thumb_link || gallery.thumbnailUrl, store.state.AuthCode);
return gallery.thumb_link || ehThumbnailUrl(gallery.thumbnailUrl, store.state.AuthCode);
});
function closeInspector() {
+175
View File
@@ -0,0 +1,175 @@
import test from "node:test"
import assert from "node:assert/strict"
import fs from "node:fs"
import {registerHooks} from "node:module"
import {compileScript, parse} from "@vue/compiler-sfc"
import {createRenderer, nextTick} from "vue"
import {ehThumbnailUrl} from "lionwebsite-frontend-shared"
// 桌面详情面板的缩略图规则必须与共享 store 的规范化保持一致:
// normalizeTask 已经代理过 thumb_link,组件再用 ehThumbnailUrl 套一层就会把
// 代理地址当成原始 path 再代理一次。这里直接执行仓库里的 .vue 源码来盯住它。
registerHooks({
load(url, context, nextLoad) {
if (!url.endsWith(".vue")) return nextLoad(url, context)
const source = fs.readFileSync(new URL(url), "utf8")
const {descriptor, errors} = parse(source, {filename: url})
assert.deepEqual(errors, [], "SFC 解析必须无错")
return {
format: "module",
shortCircuit: true,
source: compileScript(descriptor, {id: "panel-test", inlineTemplate: true, isProd: false}).content,
}
},
})
const {default: DetailPanel} = await import("../src/components/DetailPanel.vue")
const store = (await import("../src/store/index.js")).default
// 最小渲染器:能插入节点、读回属性、处理 v-show 的 style 即可。
function fakeElement(tag) {
return {nodeType: 1, tag, children: [], parentNode: null, style: {}, attrs: {}, listeners: {}}
}
function nodeOps() {
const remove = node => {
const parent = node.parentNode
if (!parent) return
const index = parent.children.indexOf(node)
if (index >= 0) parent.children.splice(index, 1)
node.parentNode = null
}
const insert = (node, parent, anchor = null) => {
if (node.parentNode) remove(node)
const index = anchor ? parent.children.indexOf(anchor) : -1
if (index >= 0) parent.children.splice(index, 0, node)
else parent.children.push(node)
node.parentNode = parent
}
return {
insert,
remove,
createElement: fakeElement,
createText: text => ({nodeType: 3, text, parentNode: null}),
createComment: text => ({nodeType: 8, text, parentNode: null}),
setText: (node, text) => { node.text = text },
setElementText: (node, text) => { node.children = [{nodeType: 3, text, parentNode: node}] },
parentNode: node => node.parentNode,
nextSibling: node => {
const parent = node.parentNode
if (!parent) return null
const index = parent.children.indexOf(node)
return parent.children[index + 1] || null
},
querySelector: () => null,
setScopeId: (el, id) => { el.scopeId = id },
cloneNode: el => ({...fakeElement(el.tag), attrs: {...el.attrs}}),
insertStaticContent: (content, parent, anchor) => {
const node = fakeElement("static")
insert(node, parent, anchor)
return [node, node]
},
patchProp: (el, key, previous, next) => {
if (key === "style") el.style = next || {}
else if (/^on[A-Z]/.test(key)) el.listeners[key] = next
else el.attrs[key] = next
},
}
}
function mountPanel() {
const app = createRenderer(nodeOps()).createApp(DetailPanel)
// 真实入口在 main.js 里全局注册 Element Plus;这里只需要占位节点。
app.component("el-button", {render: () => null})
app.component("el-progress", {render: () => null})
const root = fakeElement("root")
app.mount(root)
return root
}
function descendants(node, out = []) {
for (const child of node.children || []) {
if (child.nodeType === 1) {
out.push(child)
descendants(child, out)
}
}
return out
}
function textOf(root) {
const parts = []
const walk = node => {
if (node.nodeType === 3) { parts.push(node.text); return }
for (const child of node.children || []) walk(child)
}
walk(root)
return parts.join(" ")
}
const previewImage = root => descendants(root).find(node => node.tag === "img") || null
const remoteGallery = () => ({
gid: 9, name: "远端画廊", link: "https://example.org/g/9/k/", thumbnailUrl: "/t/9.jpg",
})
function reset() {
store.state.AuthCode = "auth"
store.state.userId = 7
store.state.selectedGallery = null
store.state.chosenGallery = false
}
test("已规范化的 thumb_link 直接作为 src,外层 path 仍是原始图片路径", async () => {
reset()
const root = mountPanel()
const proxied = ehThumbnailUrl("/t/9.jpg", "auth")
store.state.chosenGallery = {
...remoteGallery(), thumb_link: proxied, thumbnailUrl: "https://example.org/raw.jpg",
}
await nextTick()
const image = previewImage(root)
assert.ok(image, "远端面板必须渲染缩略图")
assert.equal(image.attrs.src, proxied)
assert.equal(image.attrs.src.split("ehThumbnail").length - 1, 1)
assert.equal(new URL(image.attrs.src).searchParams.get("path"), "/t/9.jpg")
})
test("只有原始上游地址时才在这里代理一次", async () => {
reset()
const root = mountPanel()
store.state.chosenGallery = {...remoteGallery(), thumbnailUrl: "https://example.org/raw.jpg"}
await nextTick()
const image = previewImage(root)
assert.ok(image)
assert.equal(image.attrs.src, ehThumbnailUrl("https://example.org/raw.jpg", "auth"))
assert.equal(new URL(image.attrs.src).searchParams.get("path"), "https://example.org/raw.jpg")
})
test("清空 chosenGallery 后远端面板收起,不会残留上一张缩略图", async () => {
reset()
const root = mountPanel()
store.state.chosenGallery = {...remoteGallery(), thumb_link: ehThumbnailUrl("/t/9.jpg", "auth")}
await nextTick()
assert.ok(previewImage(root), "先确认远端面板已打开")
store.state.chosenGallery = false
await nextTick()
assert.equal(previewImage(root), null)
assert.match(textOf(root), /选择一条任务查看详情/)
})
test("列表任务已带代理缩略图,详情面板不再重复代理", async () => {
reset()
const root = mountPanel()
store.commit("_updateGalleryTasks", [{
gid: 3, name: "任务", status: "已提交", pages: 1, proceeding: 0,
thumb_link: "/t/3.jpg", createTime: 1, downloader: 7,
}])
store.commit("_selectGallery", store.state.totalGalleryTask[0])
await nextTick()
const image = previewImage(root)
assert.ok(image)
assert.equal(new URL(image.attrs.src).searchParams.get("path"), "/t/3.jpg")
assert.equal(image.attrs.src.split("ehThumbnail").length - 1, 1)
})