diff --git a/DESIGN.md b/DESIGN.md
index 4feb893..efa8b2d 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -226,7 +226,7 @@ Pokemon 详情页展示:
- 关联喜欢的东西的物品
- 出现的栖息地
- 最后编辑信息
-- 编辑历史:保留在右侧 Sidebar 展示
+- 编辑历史:通过详情页 Tabs 展示
## 物品
@@ -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 标签筛选,在移动端展示紧凑筛选条;包含 All 和后台配置的 Life 标签;点击标签后按该标签筛选,搜索和标签筛选可以同时生效。
+- Feed 使用 Tabs 展示 Life 标签筛选;包含 All 和后台配置的 Life 标签;点击标签后按该标签筛选,搜索和标签筛选可以同时生效。
- 信息流分页加载,初始展示最新一页,滚动到底部自动加载更多。
- 当前没有图片上传、转发、置顶或单独审核流程。
- Life Post 是用户生成内容,正文按作者输入展示,不进入 `entity_translations`。
@@ -408,6 +408,8 @@ API 暴露边界:
- UI 风格以 `DesignGuidelines.html` 为准。
- 页面结构以 `AppShell`、`PageHeader`、列表、详情区和管理区为核心。
+- 全局主导航使用 `AppShell` 侧边栏;移动端通过导航按钮打开侧边栏抽屉。
+- 页面级分类、筛选或辅助内容切换使用 Tabs,避免在内容页继续增加侧边栏。
- 导航和主要操作使用图标增强识别。
- 数据加载状态使用 Skeleton,避免裸文本 loading。
- 分类切换使用 Tabs。
diff --git a/frontend/src/components/AppShell.vue b/frontend/src/components/AppShell.vue
index 9df9c26..84bfe49 100644
--- a/frontend/src/components/AppShell.vue
+++ b/frontend/src/components/AppShell.vue
@@ -1,8 +1,9 @@
-
-
+
diff --git a/frontend/src/components/EditHistoryPanel.vue b/frontend/src/components/EditHistoryPanel.vue
index 9b1c464..393041c 100644
--- a/frontend/src/components/EditHistoryPanel.vue
+++ b/frontend/src/components/EditHistoryPanel.vue
@@ -107,7 +107,7 @@ function formatDateTime(value: string): string {
-
+
diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts
index 728a527..1f22202 100644
--- a/frontend/src/i18n.ts
+++ b/frontend/src/i18n.ts
@@ -17,6 +17,7 @@ const messages = {
create: 'Create',
delete: 'Delete',
edit: 'Edit',
+ details: 'Details',
filters: 'Filters',
loading: 'Loading',
name: 'Name',
@@ -47,6 +48,8 @@ const messages = {
life: 'Life',
admin: 'Admin',
main: 'Main navigation',
+ openMenu: 'Open navigation',
+ closeMenu: 'Close navigation',
language: 'Language',
login: 'Log in',
logout: 'Log out',
@@ -375,6 +378,7 @@ const messages = {
create: '创建',
delete: '删除',
edit: '编辑',
+ details: '详情',
filters: '筛选',
loading: '加载中',
name: '名称',
@@ -405,6 +409,8 @@ const messages = {
life: 'Life',
admin: '管理',
main: '主导航',
+ openMenu: '打开导航',
+ closeMenu: '关闭导航',
language: '语言',
login: '登录',
logout: '退出',
diff --git a/frontend/src/icons.ts b/frontend/src/icons.ts
index 21dcdf6..62888f0 100644
--- a/frontend/src/icons.ts
+++ b/frontend/src/icons.ts
@@ -20,6 +20,7 @@ export const iconLife: AppIcon = 'mdi:post-outline';
export const iconLogin: AppIcon = 'mdi:login';
export const iconLogout: AppIcon = 'mdi:logout';
export const iconMail: AppIcon = 'mdi:email-fast-outline';
+export const iconMenu: AppIcon = 'mdi:menu';
export const iconNoRecipe: AppIcon = 'mdi:file-document-remove-outline';
export const iconPokemon: AppIcon = 'mdi:pokeball';
export const iconRecipe: AppIcon = 'mdi:book-open-page-variant-outline';
diff --git a/frontend/src/styles/main.css b/frontend/src/styles/main.css
index 31441b3..a0d23c7 100644
--- a/frontend/src/styles/main.css
+++ b/frontend/src/styles/main.css
@@ -107,6 +107,8 @@ svg {
.app-shell {
min-height: 100vh;
+ display: grid;
+ grid-template-columns: 252px minmax(0, 1fr);
}
.container {
@@ -115,28 +117,36 @@ svg {
padding: 0 24px;
}
-.site-header {
+.mobile-topbar,
+.site-sidebar-scrim {
+ display: none;
+}
+
+.site-sidebar {
position: sticky;
top: 0;
z-index: 50;
- border-bottom: 1px solid rgba(31, 42, 59, 0.12);
+ align-self: start;
+ height: 100dvh;
+ border-right: 1px solid rgba(31, 42, 59, 0.12);
background: color-mix(in srgb, var(--surface) 88%, transparent);
backdrop-filter: blur(18px);
}
-.top-nav {
- min-height: 70px;
+.site-sidebar__inner {
+ height: 100%;
display: grid;
- grid-template-columns: auto minmax(0, 1fr) auto;
- align-items: center;
- gap: 22px;
+ grid-template-rows: auto minmax(0, 1fr) auto;
+ gap: 18px;
+ padding: 18px 14px;
}
.brand-lockup {
- min-width: 216px;
+ min-width: 0;
display: inline-flex;
align-items: center;
gap: 12px;
+ width: fit-content;
}
.pokemon-word {
@@ -159,53 +169,72 @@ svg {
text-transform: uppercase;
}
-.nav-links {
- display: flex;
- justify-content: center;
- gap: 4px;
- overflow-x: auto;
+.side-nav {
+ min-height: 0;
+ display: grid;
+ align-content: start;
+ gap: 6px;
+ overflow-y: auto;
+ padding: 2px 0;
}
-.nav-links a {
- min-height: 38px;
- display: inline-flex;
+.side-nav__link {
+ min-height: 44px;
+ display: flex;
align-items: center;
- justify-content: center;
- gap: 6px;
- padding: 8px 10px;
+ justify-content: flex-start;
+ gap: 10px;
+ padding: 9px 10px;
border-radius: var(--radius-control);
color: var(--ink-soft);
- font-size: 14px;
+ font-size: 15px;
font-weight: 850;
+ line-height: 1.2;
white-space: nowrap;
+ transition:
+ background 0.14s ease,
+ color 0.14s ease,
+ box-shadow 0.14s ease;
}
-.nav-links a:hover {
+.side-nav__link:hover {
background: rgba(255, 203, 5, 0.24);
color: var(--ink);
}
-.nav-links a.router-link-active {
+.side-nav__link.router-link-active {
background: var(--pokemon-blue);
color: #ffffff;
+ box-shadow: 0 2px 0 var(--line-strong);
}
-.nav-links__icon {
- width: 17px;
- height: 17px;
+.side-nav__icon {
+ width: 19px;
+ height: 19px;
}
.auth-actions {
- display: flex;
- align-items: center;
- justify-content: flex-end;
+ display: grid;
+ align-content: end;
gap: 8px;
+ min-width: 0;
+}
+
+.auth-actions .ui-button {
+ width: 100%;
+ justify-content: flex-start;
}
.language-menu {
position: relative;
}
+.site-sidebar .language-menu__trigger {
+ width: 100%;
+ min-height: 44px;
+ justify-content: flex-start;
+}
+
.language-menu__trigger {
min-height: 38px;
display: inline-flex;
@@ -265,6 +294,14 @@ svg {
box-shadow: var(--shadow-raised);
}
+.site-sidebar .language-menu__dropdown {
+ top: auto;
+ bottom: calc(100% + 6px);
+ right: auto;
+ left: 0;
+ width: min(220px, calc(100vw - 40px));
+}
+
.language-menu__item {
display: flex;
align-items: center;
@@ -306,7 +343,7 @@ svg {
}
.auth-user {
- max-width: 180px;
+ max-width: 100%;
overflow: hidden;
color: var(--ink-soft);
font-size: 14px;
@@ -1245,75 +1282,6 @@ button:disabled,
min-height: 44px;
}
-.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,
.life-post {
display: grid;
@@ -2034,7 +2002,6 @@ button:disabled,
.life-page .ui-button,
.life-icon-button,
.life-metric-button,
- .life-tag-filter__button,
.life-reaction-option,
.life-action-tooltip,
.life-search-control__clear,
@@ -2094,14 +2061,8 @@ button:disabled,
grid-template-columns: 1fr;
}
-.detail-with-sidebar {
- display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(260px, 320px);
- gap: 16px;
- align-items: start;
-}
-
-.pokemon-detail-sidebar {
+.detail-tabs,
+.detail-tab-panel {
display: grid;
gap: 16px;
min-width: 0;
@@ -2157,8 +2118,6 @@ button:disabled,
}
.edit-history-panel {
- position: sticky;
- top: 92px;
display: grid;
gap: 16px;
padding: 18px;
@@ -3027,26 +2986,79 @@ button:disabled,
}
@media (max-width: 900px) {
- .top-nav {
- grid-template-columns: 1fr;
+ .app-shell {
+ display: block;
+ padding-top: 64px;
+ }
+
+ .mobile-topbar {
+ position: fixed;
+ inset: 0 0 auto;
+ z-index: 55;
+ min-height: 64px;
+ display: flex;
+ align-items: center;
gap: 12px;
- padding-top: 14px;
- padding-bottom: 14px;
+ padding: 10px 16px;
+ border-bottom: 1px solid rgba(31, 42, 59, 0.12);
+ background: color-mix(in srgb, var(--surface) 90%, transparent);
+ backdrop-filter: blur(18px);
}
- .brand-lockup,
- .auth-actions,
- .nav-links {
- justify-content: flex-start;
- min-width: 0;
+ .sidebar-toggle {
+ width: 44px;
+ min-height: 44px;
+ display: inline-grid;
+ place-items: center;
+ border: 2px solid var(--line);
+ border-radius: var(--radius-control);
+ background: var(--surface);
+ color: var(--ink-soft);
+ cursor: pointer;
}
- .nav-links {
- width: 100%;
+ .sidebar-toggle:hover {
+ border-color: var(--pokemon-blue);
+ color: var(--pokemon-blue-deep);
}
- .auth-actions {
- flex-wrap: wrap;
+ .brand-lockup--mobile .pokemon-word {
+ font-size: 24px;
+ }
+
+ .brand-lockup--mobile .brand-subtitle {
+ font-size: 11px;
+ }
+
+ .site-sidebar {
+ position: fixed;
+ inset: 0 auto 0 0;
+ z-index: 70;
+ width: min(82vw, 300px);
+ max-width: calc(100vw - 48px);
+ transform: translateX(-100%);
+ box-shadow: var(--shadow-raised);
+ transition: transform 0.18s ease;
+ }
+
+ .app-shell--sidebar-open .site-sidebar {
+ transform: translateX(0);
+ }
+
+ .site-sidebar-scrim {
+ position: fixed;
+ inset: 0;
+ z-index: 60;
+ display: block;
+ background: rgba(21, 25, 35, 0.42);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.18s ease;
+ }
+
+ .app-shell--sidebar-open .site-sidebar-scrim {
+ opacity: 1;
+ pointer-events: auto;
}
.page-header {
@@ -3058,44 +3070,13 @@ 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,
.pokemon-profile-row,
.admin-layout {
grid-template-columns: 1fr;
}
- .edit-history-panel {
- position: static;
- }
-
.appearance-row__main {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
diff --git a/frontend/src/views/HabitatDetail.vue b/frontend/src/views/HabitatDetail.vue
index 2b4041b..7eff835 100644
--- a/frontend/src/views/HabitatDetail.vue
+++ b/frontend/src/views/HabitatDetail.vue
@@ -8,6 +8,7 @@ import EditHistoryPanel from '../components/EditHistoryPanel.vue';
import EntityChips from '../components/EntityChips.vue';
import PageHeader from '../components/PageHeader.vue';
import Skeleton from '../components/Skeleton.vue';
+import Tabs, { type TabOption } from '../components/Tabs.vue';
import { iconBack, iconEdit } from '../icons';
import { api, type HabitatDetail } from '../services/api';
import HabitatEdit from './HabitatEdit.vue';
@@ -15,9 +16,14 @@ import HabitatEdit from './HabitatEdit.vue';
const route = useRoute();
const { t } = useI18n();
const habitat = ref(null);
+const detailTab = ref('details');
const timeOfDays = ['早晨', '中午', '傍晚', '晚上'];
const weathers = ['晴天', '阴天', '雨天'];
const showEditor = computed(() => route.name === 'habitat-edit');
+const detailTabs = computed(() => [
+ { value: 'details', label: t('common.details') },
+ { value: 'history', label: t('history.editHistory') }
+]);
type PokemonRow = {
id: number;
@@ -121,6 +127,7 @@ watch(
() => route.params.id,
() => {
habitat.value = null;
+ detailTab.value = 'details';
void loadHabitatDetail();
}
);
@@ -187,8 +194,10 @@ watch(
-