From 6462ed23de4c33bff2a11ca2a0ab8398b7294e0d Mon Sep 17 00:00:00 2001 From: xiaomai Date: Sat, 2 May 2026 00:54:07 +0800 Subject: [PATCH] feat(life): redesign feed layout with sidebar and icon buttons Replace tag tabs with a responsive sidebar for filtering Convert post and comment action buttons to icon-only with tooltips Standardize engagement buttons into reusable icon and metric components --- DESIGN.md | 2 +- frontend/src/styles/main.css | 261 +++++++----- frontend/src/views/LifeView.vue | 692 +++++++++++++++++--------------- 3 files changed, 539 insertions(+), 416 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 9685f59..4feb893 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -387,7 +387,7 @@ Life Post 可配置: - 已注册并完成邮箱验证的用户可以对每条 Life Post 选择一个 Reaction;普通点击默认设置 `like`,再次点击 `like` 会取消,当前为其他 Reaction 时普通点击会替换为 `like`。 - Life Reaction 的其他类型通过右键 / context menu 或可见展开按钮打开 Popup 选择;再次选择当前 Reaction 会取消,选择其他 Reaction 会替换原 Reaction。 - 支持按 Life Post 正文搜索;用户按 Enter 或点击 Search 按钮后提交搜索,不随输入实时请求;搜索结果仍按创建时间倒序展示并分页加载。 -- Feed 顶部展示 Life 标签 Tabs,包含 All 和后台配置的 Life 标签;点击标签后按该标签筛选,搜索和标签筛选可以同时生效。 +- Feed 在桌面端通过侧边栏展示 Life 标签筛选,在移动端展示紧凑筛选条;包含 All 和后台配置的 Life 标签;点击标签后按该标签筛选,搜索和标签筛选可以同时生效。 - 信息流分页加载,初始展示最新一页,滚动到底部自动加载更多。 - 当前没有图片上传、转发、置顶或单独审核流程。 - Life Post 是用户生成内容,正文按作者输入展示,不进入 `entity_translations`。 diff --git a/frontend/src/styles/main.css b/frontend/src/styles/main.css index 5a5e475..31441b3 100644 --- a/frontend/src/styles/main.css +++ b/frontend/src/styles/main.css @@ -1245,8 +1245,73 @@ button:disabled, min-height: 44px; } -.life-tag-tabs { - max-width: 100%; +.life-layout { + display: grid; + grid-template-columns: minmax(184px, 240px) minmax(0, 1fr); + align-items: start; + gap: 16px; +} + +.life-sidebar { + position: sticky; + top: 92px; + display: grid; + gap: 12px; + padding: 14px; + border: 2px solid var(--line-strong); + border-radius: var(--radius-card); + background: var(--surface); + box-shadow: var(--shadow-control); +} + +.life-sidebar__header h2 { + margin: 0; + color: var(--ink); + font-family: var(--font-display); + font-size: 18px; + font-weight: 950; + line-height: 1.15; +} + +.life-tag-filter { + display: grid; + gap: 8px; +} + +.life-tag-filter__button { + min-height: 44px; + display: flex; + align-items: center; + justify-content: flex-start; + min-width: 0; + padding: 8px 10px; + border: 2px solid var(--line); + border-radius: var(--radius-control); + background: var(--surface-soft); + color: var(--ink-soft); + font-weight: 900; + cursor: pointer; + text-align: left; + transition: + background 0.14s ease, + border-color 0.14s ease, + color 0.14s ease, + box-shadow 0.14s ease; +} + +.life-tag-filter__button span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.life-tag-filter__button:hover, +.life-tag-filter__button.is-active { + border-color: var(--line-strong); + background: var(--pokemon-yellow); + box-shadow: 0 2px 0 var(--line-strong); + color: #172036; } .life-composer, @@ -1291,11 +1356,12 @@ button:disabled, .life-feed { display: grid; + min-width: 0; } .life-feed__list { - width: min(100%, 920px); - justify-self: center; + width: 100%; + justify-self: stretch; } .life-feed__sentinel { @@ -1395,10 +1461,6 @@ button:disabled, gap: 8px; } -.life-post__actions .ui-button { - min-height: 44px; -} - .life-post__body { max-width: 72ch; margin: 0; @@ -1456,40 +1518,72 @@ button:disabled, min-width: 0; } -.life-post__engagement-button, -.life-post__comment-count { +.life-icon-button, +.life-metric-button { + position: relative; min-height: 44px; - display: inline-flex; - align-items: center; - gap: 7px; - padding: 7px 9px; + border: 1px solid var(--line); border-radius: var(--radius-control); - background: transparent; + background: var(--surface-soft); color: var(--ink-soft); - font-weight: 900; cursor: pointer; + font-weight: 900; transition: background 0.14s ease, border-color 0.14s ease, - color 0.14s ease; + color 0.14s ease, + box-shadow 0.14s ease; } -.life-post__engagement-button:hover, -.life-post__comment-count:hover, -.life-post__engagement-button[aria-expanded="true"], -.life-post__engagement-button.is-active { +.life-icon-button { + width: 44px; + min-width: 44px; + display: inline-grid; + place-items: center; + padding: 0; +} + +.life-metric-button { + display: inline-flex; + align-items: center; + gap: 7px; + justify-content: center; + padding: 7px 10px; +} + +.life-icon-button:hover, +.life-icon-button[aria-expanded="true"], +.life-icon-button.is-active, +.life-metric-button:hover, +.life-metric-button[aria-expanded="true"] { + border-color: color-mix(in srgb, var(--pokemon-blue) 45%, var(--line)); background: color-mix(in srgb, var(--pokemon-blue) 10%, var(--surface-soft)); color: var(--pokemon-blue-deep); } -.life-post__engagement-button .ui-icon { - width: 18px; - height: 18px; +.life-icon-button--flat { + border-color: transparent; + background: transparent; } -.life-post__comment-count { - color: var(--muted); - font-size: 14px; +.life-icon-button--danger:hover, +.life-icon-button--danger:focus-visible { + border-color: color-mix(in srgb, var(--danger) 45%, var(--line)); + background: color-mix(in srgb, var(--danger) 10%, var(--surface-soft)); + color: var(--danger); +} + +.life-icon-button:disabled, +.life-metric-button:disabled { + cursor: not-allowed; + opacity: 0.54; + box-shadow: none; +} + +.life-icon-button .ui-icon, +.life-metric-button .ui-icon { + width: 20px; + height: 20px; } .life-reactions { @@ -1499,48 +1593,31 @@ button:disabled, .life-reaction-control { display: inline-flex; align-items: stretch; - overflow: hidden; + overflow: visible; border: 1px solid var(--line); border-radius: var(--radius-control); background: var(--surface-soft); } -.life-reaction-trigger { - position: relative; - min-width: 96px; - justify-content: flex-start; - padding: 7px 10px; -} - -.life-reaction-trigger__label { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; +.life-reaction-control .life-icon-button { + border: 0; + border-radius: 0; + background: transparent; } .life-reaction-menu-button { - width: 44px; - min-height: 44px; - display: inline-grid; - place-items: center; border-left: 1px solid var(--line); - background: transparent; - color: var(--ink-soft); - cursor: pointer; } +.life-reaction-control .life-icon-button:hover, +.life-reaction-control .life-icon-button[aria-expanded="true"], +.life-reaction-control .life-icon-button.is-active, .life-reaction-menu-button:hover, .life-reaction-menu-button[aria-expanded="true"] { background: color-mix(in srgb, var(--pokemon-blue) 10%, var(--surface-soft)); color: var(--pokemon-blue-deep); } -.life-post__engagement-button:disabled, -.life-reaction-menu-button:disabled { - cursor: not-allowed; - opacity: 0.54; -} - .life-reaction-picker { position: absolute; z-index: 10; @@ -1621,7 +1698,7 @@ button:disabled, color: var(--ink-soft); } -.life-reaction-tooltip { +.life-action-tooltip { position: absolute; z-index: 30; bottom: calc(100% + 8px); @@ -1646,7 +1723,7 @@ button:disabled, white-space: nowrap; } -.life-reaction-tooltip::after { +.life-action-tooltip::after { position: absolute; top: 100%; left: 50%; @@ -1659,11 +1736,11 @@ button:disabled, transform: translate(-50%, -4px) rotate(45deg); } -.life-reaction-trigger:hover .life-reaction-tooltip, -.life-reaction-trigger:focus-visible .life-reaction-tooltip, -.life-reaction-option:hover .life-reaction-tooltip, -.life-reaction-option:focus-visible .life-reaction-tooltip, -.life-reaction-summary__item:hover .life-reaction-tooltip { +.life-icon-button:hover .life-action-tooltip, +.life-icon-button:focus-visible .life-action-tooltip, +.life-metric-button:hover .life-action-tooltip, +.life-metric-button:focus-visible .life-action-tooltip, +.life-reaction-summary__item:hover .life-action-tooltip { opacity: 1; transform: translate(-50%, 0); visibility: visible; @@ -1810,30 +1887,6 @@ button:disabled, gap: 8px; } -.life-comment__link-button { - min-height: 44px; - display: inline-flex; - align-items: center; - gap: 5px; - padding: 8px 10px; - border-radius: var(--radius-control); - background: transparent; - color: var(--pokemon-blue); - font-size: 13px; - font-weight: 900; - cursor: pointer; -} - -.life-comment__link-button:hover { - background: color-mix(in srgb, var(--pokemon-blue) 9%, transparent); - color: var(--pokemon-blue-deep); -} - -.life-comment__link-button .ui-icon { - width: 15px; - height: 15px; -} - .life-comments__empty { margin: 0; } @@ -1979,10 +2032,11 @@ button:disabled, @media (prefers-reduced-motion: reduce) { .life-page .ui-button, - .life-post__engagement-button, - .life-reaction-menu-button, + .life-icon-button, + .life-metric-button, + .life-tag-filter__button, .life-reaction-option, - .life-reaction-tooltip, + .life-action-tooltip, .life-search-control__clear, .reorderable-row, .reorderable-list-move, @@ -3004,6 +3058,32 @@ button:disabled, justify-content: flex-start; } + .life-layout { + grid-template-columns: 1fr; + } + + .life-sidebar { + position: static; + } + + .life-sidebar__header { + display: none; + } + + .life-tag-filter { + display: flex; + gap: 8px; + max-width: 100%; + overflow-x: auto; + padding-bottom: 2px; + scrollbar-width: thin; + } + + .life-tag-filter__button { + flex: 0 0 auto; + max-width: 180px; + } + .detail-grid, .detail-with-sidebar, .pokemon-profile-grid, @@ -3093,14 +3173,9 @@ button:disabled, align-items: stretch; } - .life-reactions, - .life-reaction-control { + .life-reactions { min-width: 0; - flex: 1 1 auto; - } - - .life-reaction-trigger { - flex: 1 1 auto; + flex: 0 0 auto; } .life-reaction-picker { diff --git a/frontend/src/views/LifeView.vue b/frontend/src/views/LifeView.vue index 58b863c..7f2801e 100644 --- a/frontend/src/views/LifeView.vue +++ b/frontend/src/views/LifeView.vue @@ -8,7 +8,6 @@ import PageHeader from '../components/PageHeader.vue'; import Skeleton from '../components/Skeleton.vue'; import StatusMessage from '../components/StatusMessage.vue'; import TagsSelect from '../components/TagsSelect.vue'; -import Tabs, { type TabOption } from '../components/Tabs.vue'; import { iconAdd, iconCancel, @@ -94,7 +93,7 @@ const selectedFeedTagId = computed(() => { const tagId = Number(activeTagId.value); return activeTagId.value === allTagValue || !Number.isInteger(tagId) || tagId <= 0 ? undefined : tagId; }); -const tagTabs = computed(() => [ +const tagFilterOptions = computed(() => [ { value: allTagValue, label: t('pages.life.allTags') }, ...lifeTags.value.map((tag) => ({ value: String(tag.id), label: tag.name })) ]); @@ -245,6 +244,12 @@ function retryLoadMore() { void loadMorePosts(); } +function selectTagFilter(value: string) { + if (value !== activeTagId.value) { + activeTagId.value = value; + } +} + function matchesCurrentFilters(post: LifePost) { const keyword = searchQuery.value.toLowerCase(); const tagId = selectedFeedTagId.value; @@ -715,8 +720,6 @@ onUnmounted(() => { - - {{ loadError }} { -
-
-
-
- - -
- - - -
-
- -
-
-
- - - -
- - -
-
- -

{{ post.body }}

- - - - - - - -
+
-
- -
-
- - -
- - - -
- - -
-
-
+ -
-
+ +
+
+
+ + + +
+ + +
+
+ +

{{ post.body }}

+ + + + + + + +
+
+

{{ t('pages.life.comments') }}

+ {{ commentCount(post) }} +
+ +
+
+ + +
+ + +
+ +
+
+
+ +
+
+ {{ commentAuthorName(comment) }} + +
+

{{ comment.body }}

+ +
+ + +
+ + + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ {{ commentAuthorName(reply) }} + +
+

{{ reply.body }}

+
+ +
+ +
+
+
+
+
+
+
+ +

{{ t('pages.life.noComments') }}

+
+
+ +
+
+ + +
+ + + +
+ + +
+ +
+
+ +
+
+ +