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 @@ -
-
+
+ + +
@@ -220,7 +229,9 @@ watch(
- +
+ +
diff --git a/frontend/src/views/ItemDetail.vue b/frontend/src/views/ItemDetail.vue index a6796a7..ec6b6bd 100644 --- a/frontend/src/views/ItemDetail.vue +++ b/frontend/src/views/ItemDetail.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 { iconAdd, iconBack, iconEdit } from '../icons'; import { api, type ItemDetail } from '../services/api'; import ItemEdit from './ItemEdit.vue'; @@ -15,7 +16,12 @@ import ItemEdit from './ItemEdit.vue'; const route = useRoute(); const { t } = useI18n(); const item = ref(null); +const detailTab = ref('details'); const showEditor = computed(() => route.name === 'item-edit'); +const detailTabs = computed(() => [ + { value: 'details', label: t('common.details') }, + { value: 'history', label: t('history.editHistory') } +]); const customization = computed(() => { if (!item.value) { @@ -50,6 +56,7 @@ watch( () => route.params.id, () => { item.value = null; + detailTab.value = 'details'; void loadItemDetail(); } ); @@ -123,8 +130,10 @@ watch( -
-
+
+ + +
@@ -186,7 +195,9 @@ watch(
- +
+ +
diff --git a/frontend/src/views/LifeView.vue b/frontend/src/views/LifeView.vue index 7f2801e..97ae4f7 100644 --- a/frontend/src/views/LifeView.vue +++ b/frontend/src/views/LifeView.vue @@ -7,6 +7,7 @@ import Modal from '../components/Modal.vue'; import PageHeader from '../components/PageHeader.vue'; import Skeleton from '../components/Skeleton.vue'; import StatusMessage from '../components/StatusMessage.vue'; +import Tabs, { type TabOption } from '../components/Tabs.vue'; import TagsSelect from '../components/TagsSelect.vue'; import { iconAdd, @@ -93,7 +94,7 @@ const selectedFeedTagId = computed(() => { const tagId = Number(activeTagId.value); return activeTagId.value === allTagValue || !Number.isInteger(tagId) || tagId <= 0 ? undefined : tagId; }); -const tagFilterOptions = computed(() => [ +const tagFilterOptions = computed(() => [ { value: allTagValue, label: t('pages.life.allTags') }, ...lifeTags.value.map((tag) => ({ value: String(tag.id), label: tag.name })) ]); @@ -244,12 +245,6 @@ 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; @@ -783,27 +778,9 @@ onUnmounted(() => {
-
- + -
+
@@ -1149,7 +1126,6 @@ onUnmounted(() => { {{ t('pages.life.newPost') }}
-
-
+ diff --git a/frontend/src/views/PokemonDetail.vue b/frontend/src/views/PokemonDetail.vue index f6aae41..fc331da 100644 --- a/frontend/src/views/PokemonDetail.vue +++ b/frontend/src/views/PokemonDetail.vue @@ -18,6 +18,7 @@ const route = useRoute(); const { t } = useI18n(); const pokemon = ref(null); const itemCategoryTab = ref(''); +const detailTab = ref('details'); const timeOfDays = ['早晨', '中午', '傍晚', '晚上']; const weathers = ['晴天', '阴天', '雨天']; @@ -103,6 +104,10 @@ const habitatRows = computed(() => { }); const skillDropRows = computed(() => pokemon.value?.skills.filter((skill) => skill.itemDrop) ?? []); const showEditor = computed(() => route.name === 'pokemon-edit'); +const detailTabs = computed(() => [ + { value: 'details', label: t('common.details') }, + { value: 'history', label: t('history.editHistory') } +]); const itemCategoryTabs = computed(() => { const categories = new Map(); @@ -163,6 +168,7 @@ watch( () => route.params.id, () => { pokemon.value = null; + detailTab.value = 'details'; void loadPokemonDetail(); } ); @@ -240,8 +246,10 @@ watch( -
-
+
+ + +
@@ -351,9 +359,9 @@ watch(
- +
diff --git a/frontend/src/views/RecipeDetail.vue b/frontend/src/views/RecipeDetail.vue index 05f9674..04488ae 100644 --- a/frontend/src/views/RecipeDetail.vue +++ b/frontend/src/views/RecipeDetail.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 RecipeDetail } from '../services/api'; import RecipeEdit from './RecipeEdit.vue'; @@ -15,7 +16,12 @@ import RecipeEdit from './RecipeEdit.vue'; const route = useRoute(); const { t } = useI18n(); const recipe = ref(null); +const detailTab = ref('details'); const showEditor = computed(() => route.name === 'recipe-edit'); +const detailTabs = computed(() => [ + { value: 'details', label: t('common.details') }, + { value: 'history', label: t('history.editHistory') } +]); async function loadRecipeDetail() { recipe.value = await api.recipeDetail(String(route.params.id)); @@ -38,6 +44,7 @@ watch( () => route.params.id, () => { recipe.value = null; + detailTab.value = 'details'; void loadRecipeDetail(); } ); @@ -85,8 +92,10 @@ watch( -
-
+
+ + +
@@ -96,7 +105,9 @@ watch(
- +
+ +