feat(viewer): hide like feature — action bar like slot, photo like badges, hot sort tab, others' like bubbles (user request 2026-09-23)

- PhotoFeed: cell like badge not rendered
- AlbumHeader: sort tabs row removed (hot depends on likes; latest-only has no toggle value)
- PhotoActionBar: like slot v-if=false (share/download remain, auto re-center)
- ActivityPage: LikeBubbles hidden
- specs updated to hidden assertions; 244 green

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-23 14:29:22 +08:00
parent e44b35db28
commit 2babd364ba
9 changed files with 40 additions and 108 deletions
+3 -13
View File
@@ -233,21 +233,11 @@ describe('activity page (Task 6)', () => {
// —— UI-02 V02:排序/分类经 AlbumHeader 上抛,沿用本页序列重置机制 ——
it('selecting the Popular tab refetches the sequence with sort=hot (active state moves)', async () => {
it('sort tabs removed per user request (2026-09-23): no popular tab, sequence stays latest', async () => {
const wrapper = mountPage();
await flushPromises();
mockedGetPublicPhotos.mockClear();
const tabs = wrapper.findAll('.sort-tab');
expect(tabs.map((tab) => tab.text())).toEqual(['最新', '热门']);
expect(tabs[0].classes()).toContain('active');
expect(tabs[0].attributes('aria-pressed')).toBe('true');
await tabs[1].trigger('click');
await flushPromises();
expect(mockedGetPublicPhotos).toHaveBeenLastCalledWith('tok-activity', expect.objectContaining({ sort: 'hot' }));
expect(wrapper.findAll('.sort-tab')[1].classes()).toContain('active');
expect(wrapper.findAll('.sort-tab')).toHaveLength(0);
expect(wrapper.find('.sort-tabs').exists()).toBe(false);
});
it('category chips filter the sequence and the selection survives a locale switch', async () => {
+4 -13
View File
@@ -81,7 +81,7 @@ describe('album header info area (UI-02 V02)', () => {
expect(wrapper.find('.meta').text()).toContain('10月1日');
// 工具栏:排序 tab + 分类胶囊(All + 接口分类)。
expect(wrapper.findAll('.sort-tab').map((tab) => tab.text())).toEqual(['最新', '热门']);
expect(wrapper.findAll('.sort-tab')).toHaveLength(0); // 排序 tab 已移除(2026-09-23)
expect(wrapper.findAll('.categories .chip').map((chip) => chip.text())).toEqual(['全部', '签到墙', '开场']);
// jsdom 无布局:未溢出 → 不渲染展开按钮。
expect(wrapper.findAll('.text-expand')).toHaveLength(0);
@@ -153,19 +153,10 @@ describe('album header info area (UI-02 V02)', () => {
expect(wrapper.find('.title').classes()).not.toContain('clamped');
});
it('sort tabs: latest active by default, clicking popular emits select-sort hot, locked disables', async () => {
it('sort tabs removed per user request (2026-09-23): whole row hidden, mode stays latest', () => {
const wrapper = mountHeader({ album: makeAlbum(), viewCount: null });
const tabs = wrapper.findAll('.sort-tab');
expect(tabs[0].classes()).toContain('active');
expect(tabs[0].attributes('aria-pressed')).toBe('true');
expect(tabs[1].classes()).not.toContain('active');
await tabs[1].trigger('click');
expect(wrapper.emitted('selectSort')).toEqual([['hot']]);
const locked = mountHeader({ album: makeAlbum(), viewCount: null, sortLocked: true });
expect(locked.findAll('.sort-tab').every((tab) => tab.attributes('disabled') !== undefined)).toBe(true);
expect(wrapper.find('.sort-tabs').exists()).toBe(false);
expect(wrapper.find('.sort-tab').exists()).toBe(false);
});
it('category chips: selected state from prop, clicks emit the category id and All emits null', async () => {
+6 -29
View File
@@ -343,37 +343,14 @@ describe('photo feed (Task 7 / Task 8 sequence props)', () => {
expect(wrapper.findAll('.cell')).toHaveLength(1);
});
it('renders the like badge overlay with the real count and no card chrome', async () => {
const likes = {
displayFor: (photo: PublicPhoto) => photo.likeCount,
popNonce: () => 0,
like: vi.fn(),
} as unknown as LikesController;
mockedGetPublicPhotos.mockResolvedValueOnce(
makeList([{ ...makePhoto('p1'), likeCount: 1286 }, makePhoto('p2')], null),
);
const { wrapper } = mountFeed({ likes });
await flushPromises();
const badge = wrapper.find('.cell .cell-like');
expect(badge.exists()).toBe(true);
expect(badge.find('.heart').exists()).toBe(true);
// 真实计数(useLikes displayFor 值)经 Intl 分组。
expect(badge.find('.count').text()).toBe('1,286');
// 无白色卡片包裹/无常驻文件名/无额外标题:单元格可见文本只有点赞计数
//(图片 alt 非可见文本,懒加载绑定不影响该断言)。
expect(wrapper.findAll('.cell')[0].text()).toBe('1,286');
it('like badge overlay hidden per user request (2026-09-23)', () => {
const { wrapper } = mountFeed();
expect(wrapper.find('.cell-like').exists()).toBe(false);
});
it('clicking the like badge likes the photo without opening the lightbox', async () => {
const like = vi.fn();
const likes = { displayFor: () => 0, popNonce: () => 0, like } as unknown as LikesController;
const { wrapper, opened } = mountFeed({ likes });
await flushPromises();
await wrapper.find('.cell-like').trigger('click');
expect(like).toHaveBeenCalledWith('p1');
expect(opened).toEqual([]);
it('like badge interactions removed with the badge (hidden 2026-09-23)', () => {
const { wrapper } = mountFeed();
expect(wrapper.find('.cell-like').exists()).toBe(false);
});
it('shows the new-photo pill on hasNew; clicking refreshes without scrolling to top', async () => {
+5 -7
View File
@@ -139,9 +139,9 @@ describe('photo lightbox (Task 8)', () => {
expect(img(wrapper).attributes('src')).toBe('http://img.test/p2-browse.jpg?s=1');
// 3 张全部加载完成(无更多页)→ 筛选序列 i / n(§5.2)。
expect(wrapper.find('.position-indicator').text()).toBe('2 / 3');
// 底部操作栏(V04):Like 计数 + Like/Share 标签(无下载控制器 → 下载位省略)。
expect(wrapper.find('.like-count').text()).toBe('7');
expect(wrapper.findAll('.action-label').map((label) => label.text())).toEqual(['点赞', '分享']);
// 底部操作栏:点赞已按用户要求隐藏(2026-09-23)→ 仅分享(无下载控制器)。
expect(wrapper.find('.like-count').exists()).toBe(false);
expect(wrapper.findAll('.action-label').map((label) => label.text())).toEqual(['分享']);
});
it('navigates with buttons and arrow keys, keeping the list order', async () => {
@@ -553,14 +553,12 @@ describe('photo lightbox interactions (UI-02 V05)', () => {
document.documentElement.className = '';
});
it('bursts heart particles for a local like click on the current photo', async () => {
it('like entry hidden (2026-09-23): no local like click path, no particles', async () => {
const { wrapper } = mountV05('p1');
await flushPromises();
expect(wrapper.find('.like-slot').exists()).toBe(false);
expect(wrapper.findAll('.heart-particles-zone .particle')).toHaveLength(0);
await wrapper.find('.like-slot .action-btn').trigger('click');
await nextTick();
expect(wrapper.findAll('.heart-particles-zone .particle')).toHaveLength(2);
});
it('routes realtime bursts: particles for the current photo, a notice for others; notice click navigates', async () => {
@@ -39,36 +39,30 @@ describe('photo action bar (UI-02 V04)', () => {
vi.restoreAllMocks();
});
it('renders like/share/download as evenly distributed slots with formatted count (no tap-more hint)', () => {
it('like slot hidden (2026-09-23): share/download slots remain, no like count', () => {
const wrapper = mountBar({ likeCount: 1286, canDownload: true });
// 三个 action-slot flex 均分(下载隐藏时由剩余 slot 自然重排,见下例)。
expect(wrapper.findAll('.action-slot')).toHaveLength(3);
expect(wrapper.findAll('.action-slot')).toHaveLength(2);
const labels = wrapper.findAll('.action-label').map((label) => label.text());
expect(labels).toEqual(['点赞', '分享', '下载']);
// 计数 Intl 分组 + tabular-nums 容器;「连续点击」提示已按用户要求移除。
expect(wrapper.find('.like-count').text()).toBe('1,286');
expect(wrapper.find('.like-hint').exists()).toBe(false);
expect(labels).toEqual(['分享', '下载']);
expect(wrapper.find('.like-slot').exists()).toBe(false);
expect(wrapper.find('.like-count').exists()).toBe(false);
});
it('removes the download action entirely when downloads are disabled', () => {
it('download disabled leaves share only (like hidden 2026-09-23)', () => {
const wrapper = mountBar({ likeCount: 0, canDownload: false });
expect(wrapper.find('.download-group').exists()).toBe(false);
expect(wrapper.findAll('.action-slot')).toHaveLength(2); // 剩余两键均分栏宽
expect(wrapper.findAll('.action-label').map((label) => label.text())).toEqual(['点赞', '分享']);
expect(wrapper.findAll('.action-slot')).toHaveLength(1);
expect(wrapper.findAll('.action-label').map((label) => label.text())).toEqual(['分享']);
});
it('emits like and share from the large tap targets', async () => {
const wrapper = mountBar({ likeCount: 3 });
const buttons = wrapper.findAll('.action-btn');
expect(buttons).toHaveLength(2);
it('emits share from the large tap target (like hidden 2026-09-23)', async () => {
const wrapper = mountBar({ likeCount: 0 });
await buttons[0].trigger('click');
await buttons[1].trigger('click');
expect(wrapper.emitted('like')).toHaveLength(1);
await wrapper.get('.action-btn[aria-label="分享照片"]').trigger('click');
expect(wrapper.emitted('share')).toHaveLength(1);
expect(wrapper.emitted('download')).toBeUndefined();
expect(wrapper.emitted('like')).toBeUndefined();
});
it('downloads the browse variant directly when original download is disabled', async () => {
+3 -3
View File
@@ -256,16 +256,16 @@ onBeforeUnmount(() => {
decoding="async"
@error="onImgError(photos[placement.index])"
>
<!-- 点赞角标:独立交互区(@click.stop),不随图片点击打开大图。 -->
<!-- 点赞角标:已按用户要求隐藏(2026-09-23,先隐藏点赞功能)。 -->
<button
v-if="likes !== undefined"
v-if="false && likes !== undefined"
type="button"
class="cell-like"
:aria-label="t('viewer.like')"
@click.stop="likeCell(photos[placement.index])"
>
<svg
:key="likes.popNonce(photos[placement.index].photoId)"
:key="likes?.popNonce(photos[placement.index].photoId) ?? 0"
class="heart"
viewBox="0 0 24 24"
aria-hidden="true"
@@ -220,29 +220,8 @@ watch(
</div>
</header>
<!-- 排序 tab(§4.2):高 44px;选中蓝字 + 底部 2px 线;大图打开期间锁定。 -->
<nav class="sort-tabs" :aria-label="t('album.sortLabel')">
<button
type="button"
class="sort-tab"
:class="{ active: props.sortMode === 'latest' }"
:aria-pressed="props.sortMode === 'latest'"
:disabled="props.sortLocked"
@click="emit('selectSort', 'latest')"
>
{{ t('viewer.latest') }}
</button>
<button
type="button"
class="sort-tab"
:class="{ active: props.sortMode === 'hot' }"
:aria-pressed="props.sortMode === 'hot'"
:disabled="props.sortLocked"
@click="emit('selectSort', 'hot')"
>
{{ t('viewer.popular') }}
</button>
</nav>
<!-- 排序 tab:热门已按用户要求移除(2026-09-23,其排序依赖点赞数);
仅剩「最新」单一排序无切换意义 → 整条隐藏(sortMode 恒 latest)。 -->
<!-- 分类胶囊:来自接口;无分类省略整条;All = 不传分类筛选。 -->
<nav v-if="album.categories.length > 0" class="categories" :aria-label="t('album.categories')">
@@ -75,7 +75,9 @@ onBeforeUnmount(cancelPress);
<template>
<div class="action-bar">
<div class="action-slot like-slot">
<!-- 点赞槽:已按用户要求隐藏(2026-09-23,先隐藏点赞功能——含计数)。
v-if 编译期常量 false,构建后整槽不进 DOM。 -->
<div v-if="false" class="action-slot like-slot">
<button
type="button"
class="action-btn"
+2 -1
View File
@@ -372,7 +372,8 @@ onBeforeUnmount(() => {
<!-- 他人点赞气泡(Task 10):点击定位打开该照片大图。大图打开期间改由
查看器内互动提示承载(§6.2:避免页面级气泡被模态框盖住或遮住下载)。 -->
<LikeBubbles :bubbles="lightboxPhotoId === null ? realtime.bubbles : []" @open="openFromFeed" />
<!-- 他人点赞气泡:已按用户要求隐藏(2026-09-23,先隐藏点赞功能)。 -->
<LikeBubbles v-if="false" :bubbles="lightboxPhotoId === null ? realtime.bubbles : []" @open="openFromFeed" />
<!-- 照片下线轻提示(大图 404 刷新 / WS hidden/deleted 推送移除时)。 -->
<p v-if="photoRemovedNotice" class="removed-notice" role="status">{{ t('viewer.photoRemoved') }}</p>