feat(embed): photo click opens album page in a new tab (user decision 2026-09-24)
Reverts the in-host detail modal interaction: click now window.open's the album page (/a/:token?lang=, no /p/ deep link) with noopener — same target for middle-click/right-click via the real <a> href. The loader's open-photo modal code remains but is no longer triggered. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -309,44 +309,24 @@ test.describe.serial('E04 interactions: pause, click lock, keyboard, destroy', (
|
||||
expect(after).toBeLessThan(resumedAt); // 恢复滚动(translateY 更负)
|
||||
});
|
||||
|
||||
test('photo click opens viewer deep link for the clicked photo (§4.1)', async ({ page }) => {
|
||||
test('photo click opens album page in a new tab (user decision 2026-09-24)', async ({ page }) => {
|
||||
await openHost(page, { width: 1440, height: 900 });
|
||||
const frame = page.locator('iframe[src*="/embed/"][src*="v=2"]');
|
||||
await frame.waitFor();
|
||||
|
||||
// 悬停冻结 + 坐标直击当前命中的照片(.first() 可能已滚出视口——flaky 源)。
|
||||
// 悬停冻结(点击防错图语义下稳定命中)。
|
||||
const box = await frame.boundingBox();
|
||||
const cx = (box!.x ?? 0) + 90;
|
||||
const cy = (box!.y ?? 0) + 140;
|
||||
await page.mouse.move(cx, cy);
|
||||
await page.mouse.move((box?.x ?? 0) + 90, (box?.y ?? 0) + 140);
|
||||
await page.waitForTimeout(400);
|
||||
const embedFrame0 = page.frames().find((f) => f.url().includes('v=2'));
|
||||
expect(embedFrame0).toBeDefined();
|
||||
const photoId = await embedFrame0!.evaluate(([_x, _y]) => {
|
||||
const el = document.elementFromPoint(_x, _y)?.closest('a.stream-photo');
|
||||
return el?.getAttribute('data-photo-id') ?? '';
|
||||
}, [90, 140] as const);
|
||||
expect(photoId).not.toBe('');
|
||||
// 宿主内弹框(用户决策 2026-09-23):不新开标签,宿主页覆盖层 + 详情 iframe。
|
||||
let popupOpened = false;
|
||||
page.once('popup', () => {
|
||||
popupOpened = true;
|
||||
});
|
||||
await page.mouse.click(cx, cy);
|
||||
const modalFrame = page.locator('iframe[title="PhotoLive photo detail"]');
|
||||
await modalFrame.waitFor({ timeout: 5000 });
|
||||
expect(popupOpened).toBe(false);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const f = page.frames().find((fr) => (fr.url().includes('/p/') ?? false));
|
||||
return f?.url() ?? '';
|
||||
})
|
||||
.toContain(`/p/${photoId}`);
|
||||
await page.screenshot({ path: `${OUT_ROOT}/actual/server/photo-modal.png` });
|
||||
await page
|
||||
.locator('button[aria-label*="Close photo detail"], button[aria-label*="关闭照片详情"]')
|
||||
.click();
|
||||
await expect(modalFrame).toHaveCount(0);
|
||||
const popupPromise = page.waitForEvent('popup', { timeout: 6000 });
|
||||
await page.mouse.click((box?.x ?? 0) + 90, (box?.y ?? 0) + 140);
|
||||
const popup = await popupPromise;
|
||||
// 新标签打开的是相册页(/a/:token),不含照片深链 /p/。
|
||||
expect(popup.url()).toMatch(/\/a\/[^/]+\?lang=/);
|
||||
expect(popup.url()).not.toContain('/p/');
|
||||
await popup.close();
|
||||
// 宿主内不出现弹框。
|
||||
await page.waitForTimeout(600);
|
||||
expect(await page.locator('iframe[title="PhotoLive photo detail"]').count()).toBe(0);
|
||||
});
|
||||
|
||||
test('drag beyond 8px cancels click (§4.1)', async ({ page }) => {
|
||||
|
||||
@@ -18,7 +18,6 @@ import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useEmbedFeed } from '../embed/useEmbedFeed';
|
||||
import { clampEmbedIntervalMs, EMBED_CAROUSEL_DEFAULT_MS } from '../embed/feed';
|
||||
import { postEmbedMessage } from '../embed/postMessage';
|
||||
import EmbedCarousel from './embed/EmbedCarousel.vue';
|
||||
import EmbedStream from './embed/EmbedStream.vue';
|
||||
|
||||
@@ -118,28 +117,13 @@ const albumUrl = computed(() => {
|
||||
return url.toString();
|
||||
});
|
||||
|
||||
/** v2 照片深链构造器(真实 <a> href;受控打开同 openPhoto)。 */
|
||||
function streamPhotoUrl(photoId: string): string {
|
||||
const albumToken = feed.album?.publicToken;
|
||||
const base = albumToken ?? '';
|
||||
const url = new URL(`/a/${encodeURIComponent(base)}/p/${encodeURIComponent(photoId)}`, window.location.origin);
|
||||
url.searchParams.set('lang', feed.resolvedLocale ?? 'zh-CN');
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function openPhoto(photoId: string): void {
|
||||
const albumToken = feed.album?.publicToken;
|
||||
if (albumToken === undefined) return;
|
||||
const url = new URL(`/a/${encodeURIComponent(albumToken)}/p/${encodeURIComponent(photoId)}`, window.location.origin);
|
||||
url.searchParams.set('lang', feed.resolvedLocale ?? 'zh-CN');
|
||||
// 宿主内弹框(用户决策 2026-09-23):有父窗(loader 嵌入)→ postMessage
|
||||
// 由宿主弹框内嵌详情页,不跳出当前网站;无父窗(直接打开/预览)→ 新标签
|
||||
// noopener(sandbox allow-popups-to-escape-sandbox 配合)。
|
||||
if (window.parent !== window) {
|
||||
postEmbedMessage({ photoliveEmbed: true, type: 'open-photo', url: url.toString() });
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
// 用户决策 2026-09-24:点击照片 → 新标签页打开观众页(相册页),不定位到
|
||||
// 具体照片、不在当前页弹框——v1 与 v2 统一该语义(此前的宿主内弹框与照片
|
||||
// 深链均退役;loader v2 的 open-photo 弹框逻辑保留但不再被触发)。
|
||||
function openPhoto(): void {
|
||||
if (albumUrl.value === null) return;
|
||||
// noopener:新标签页不持有 opener(sandbox allow-popups-to-escape-sandbox 配合)。
|
||||
window.open(albumUrl.value, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -151,7 +135,7 @@ function openPhoto(photoId: string): void {
|
||||
:photos="feed.status === 'ready' ? feed.photos : []"
|
||||
:album-token="feed.album?.publicToken ?? null"
|
||||
:album-title="feed.album?.title ?? ''"
|
||||
:photo-url="streamPhotoUrl"
|
||||
:photo-url="() => albumUrl ?? '#'"
|
||||
:speed="streamSpeed"
|
||||
:reactions="streamReactions"
|
||||
:active="feed.active"
|
||||
|
||||
Reference in New Issue
Block a user