Files
what-to-eat/apps/web/app/components/layout/AppRail.vue
T
toor 7408591fac feat: 落地 Warm Kitchen Ledger 原型,宽屏左导航 + 双栏布局
新增 ledger.css 设计令牌与桌面端布局,AppRail 左导航、PageHead 桌面页头、ResultsColumn 常驻结果栏;做菜/菜谱/发现/我的/设置/帮助/关于各页做桌面窄屏双形态适配,窄屏仍走原有底部标签栏与弹层交互。
2026-10-22 03:30:56 +08:00

119 lines
4.3 KiB
Vue

<script lang="ts" setup>
/**
* 桌面端左侧固定导航栏(原型 prototype/cook-desktop 的 .app-rail)。
* 窄屏由 .ledger-rail 的 display:none 隐藏,导航仍走底部标签栏。
*/
/**
* 左导航挂在 app.vue 上,不在页面组件内,
* 而 useRoute() 在页面外拿到的是 nuxtApp._route,直接进页面(如 /library)时还是旧路径。
* 所以这里改用 router.currentRoute,它始终是当前路由。
*/
const router = useRouter()
const currentPath = computed(() => router.currentRoute.value.path)
const { isDark, toggleDark } = useDarkMode()
const rStore = useRecipeStore()
const primary = [
{ key: 'cook', label: '做菜', path: '/home', icon: ioniconsBookOutline },
{ key: 'library', label: '菜谱', path: '/library', icon: ioniconsLibraryOutline },
{ key: 'discover', label: '发现', path: '/apps', icon: ioniconsRestaurantOutline },
{ key: 'me', label: '我的', path: '/my', icon: ioniconsPersonOutline },
]
const more = [
{ key: 'settings', label: '设置', path: '/settings', icon: ioniconsSettingsOutline },
{ key: 'help', label: '帮助', path: '/help', icon: ioniconsHelpCircleOutline },
{ key: 'about', label: '关于', path: '/about', icon: ioniconsInformationCircleOutline },
]
/** 菜谱库和「做菜」都有别名,这里按前缀归类,保证左侧高亮和底部标签一致 */
function isActive(item: { key: string, path: string }) {
const path = currentPath.value
if (item.key === 'cook')
return path === '/' || path === '/home' || path === '/tabs'
if (item.key === 'library')
return path.startsWith('/library') || path.startsWith('/recipes') || path.startsWith('/cookbooks')
if (item.key === 'discover')
return path.startsWith('/apps')
if (item.key === 'me')
return path.startsWith('/my')
return path.startsWith(item.path)
}
/** 是否正停留在这一页(做菜页有 / 、/home、/tabs 三个等价路径) */
function isCurrentPage(item: { key: string, path: string }) {
if (item.key === 'cook')
return currentPath.value === '/' || currentPath.value === '/home' || currentPath.value === '/tabs'
return currentPath.value === item.path
}
/** 已经在这一页时,拦下跳转改成滚回顶部,和底部标签栏的行为一致 */
function onLinkClick(item: { key: string, path: string }, event: MouseEvent) {
if (!isCurrentPage(item))
return
event.preventDefault()
scrollTop()
}
/** 和底部标签栏一样:再点一次当前项,页面滚回顶部 */
function scrollTop() {
const content = document.querySelector<HTMLIonContentElement>('.ion-page:not(.ion-page-hidden) ion-content')
void content?.scrollToTop(300)
}
</script>
<template>
<aside class="ledger-rail" aria-label="主导航">
<div class="ledger-rail-brand">
<span class="ledger-rail-mark">
<ion-icon :icon="ioniconsRestaurantOutline" aria-hidden="true" />
</span>
<span class="ledger-rail-brand-text">
<b>食用手册</b>
<small>冰箱里有什么就做什么</small>
</span>
</div>
<nav class="ledger-rail-nav">
<NuxtLink
v-for="item in primary"
:key="item.key"
:to="item.path"
class="ledger-rail-link"
:class="{ 'is-on': isActive(item) }"
:aria-current="isActive(item) ? 'page' : undefined"
@click="onLinkClick(item, $event)"
>
<ion-icon :icon="item.icon" aria-hidden="true" />
<span>{{ item.label }}</span>
</NuxtLink>
</nav>
<nav class="ledger-rail-nav">
<span class="ledger-rail-label">更多</span>
<NuxtLink
v-for="item in more"
:key="item.key"
:to="item.path"
class="ledger-rail-link"
:class="{ 'is-on': isActive(item) }"
:aria-current="isActive(item) ? 'page' : undefined"
@click="onLinkClick(item, $event)"
>
<ion-icon :icon="item.icon" aria-hidden="true" />
<span>{{ item.label }}</span>
</NuxtLink>
</nav>
<div class="ledger-rail-foot">
<button type="button" class="ledger-rail-theme" @click="toggleDark()">
<ion-icon :icon="isDark ? ioniconsSunnyOutline : ioniconsMoonOutline" aria-hidden="true" />
<span>{{ isDark ? '浅色模式' : '深色模式' }}</span>
</button>
<p class="ledger-rail-stat">
菜谱库收录 <b>{{ rStore.recipesLength }}</b> 道菜
</p>
</div>
</aside>
</template>