创作者中心
概览
切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。
<script setup lang="ts">
import { ref } from 'vue'
import { FileText, House, Images, Settings } from '@lucide/vue'
import {
AppShell,
Avatar,
Heading,
Inline,
NavLink,
Sidebar,
SidebarGroup,
SidebarLabel,
SidebarTrigger,
Stack,
Text,
} from '@hina-ui/vue'
const selected = ref('overview')
const groups = [
{
label: '内容',
items: [
{ id: 'overview', label: '概览', icon: House },
{ id: 'articles', label: '文章', icon: FileText },
{ id: 'library', label: '媒体库', icon: Images },
],
},
{ label: '管理', items: [{ id: 'settings', label: '设置', icon: Settings }] },
]
</script>
<template>
<AppShell mobile-title="Hina UI" class="border-line h-112 w-full rounded-lg border">
<template #sidebar>
<Sidebar>
<template #icon>
<Avatar src="/favicon.png" name="Hina UI" class="rounded-md" />
</template>
<template #wordmark>
<Stack gap="none">
<Text weight="medium" class="truncate">Hina UI</Text>
<Text size="xs" tone="muted" class="truncate">工作空间</Text>
</Stack>
</template>
<SidebarGroup v-for="group in groups" :key="group.label" :label="group.label">
<NavLink
v-for="item in group.items"
:key="item.id"
:href="`#${item.id}`"
:label="item.label"
:active="selected === item.id"
@click.prevent="selected = item.id"
>
<template #icon><component :is="item.icon" /></template>
{{ item.label }}
</NavLink>
</SidebarGroup>
<template #footer>
<Inline gap="sm" align="center" :wrap="false">
<Avatar src="/avatars/paper.webp" name="星见书音" />
<SidebarLabel as="div" class="flex-1">
<Stack gap="none">
<Text size="sm" weight="medium" class="truncate">星见书音</Text>
<Text size="xs" tone="muted" class="truncate">管理员</Text>
</Stack>
</SidebarLabel>
</Inline>
</template>
</Sidebar>
</template>
<template #header>
<SidebarTrigger />
<Heading :level="2" size="sm" class="truncate">创作者中心</Heading>
</template>
<Stack gap="sm" class="p-6">
<Heading :level="3" size="sm">
{{ groups.flatMap(group => group.items).find(item => item.id === selected)?.label }}
</Heading>
<Text size="sm" tone="muted">切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。</Text>
</Stack>
</AppShell>
</template>
用法
import { AppShell } from '@hina-ui/vue'
AppShell 占满视口,把界面分成三块:sidebar 插槽是左侧栏,header 插槽是顶部条,默认插槽是主区域。三块都是可选的。此外还有 banner 插槽,位于最顶部,横贯侧栏与主区域,用于放置 Banner。
它同时是侧栏状态的提供方。Sidebar、SidebarGroup、SidebarTrigger 以及侧栏内的 NavLink 都从这里取状态,脱离 AppShell 时它们退回展开形态,SidebarTrigger 不会渲染。
主区域的内容放在默认插槽里。
<script setup lang="ts">
import { AppShell, NavLink, Sidebar, Text } from '@hina-ui/vue'
</script>
<template>
<AppShell class="border-line h-72 w-full rounded-lg border">
<template #sidebar>
<Sidebar>
<NavLink href="#" active label="概览">概览</NavLink>
<NavLink href="#" label="我的书架">我的书架</NavLink>
</Sidebar>
</template>
<Text size="sm" tone="muted" class="block p-6">主区域的内容放在默认插槽里。</Text>
</AppShell>
</template>
组件默认高度为一屏。嵌在页面中的局部框架可以用 class 覆盖高度,本页示例用的就是这个办法。
主区域是独立的滚动容器。默认插槽的直接子元素加上 min-h-full,就至少占满主区域的可视高度,有公告条时也成立。顶部条的高度由根元素上的 --hn-app-shell-header-h 给出,壳内任意层级都可以读取。
<AppShell>
<template #header>…</template>
<div class="flex min-h-full flex-col">
<article class="flex-1">…</article>
<footer>…</footer>
</div>
</AppShell>
示例
收起方式
collapsible 决定桌面端收起后的形态:rail 只留图标一条,hidden 让整条侧栏退场。默认为 rail。
侧栏状态可以用 v-model:sidebar 双向绑定,取值为 expanded、rail、hidden。需要在别处读出或写入当前形态时绑定它,否则交给组件自己维护即可。
收起过程中标识、头像和已展开条目的位置保持不变。Sidebar 的品牌插槽 自动处理图标与字标的显隐;自定义页眉与页脚内容可通过 SidebarLabel 使用相同过渡。
collapsible="rail" · expanded
创作者中心
概览
切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。
collapsible="hidden" · expanded
创作者中心
概览
切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。
<script setup lang="ts">
import { ref } from 'vue'
import { FileText, House, Images, Settings } from '@lucide/vue'
import {
AppShell,
Avatar,
Heading,
Inline,
NavLink,
Sidebar,
SidebarGroup,
SidebarLabel,
SidebarTrigger,
Stack,
Text,
type SidebarState,
} from '@hina-ui/vue'
const selected = ref('overview')
const groups = [
{
label: '内容',
items: [
{ id: 'overview', label: '概览', icon: House },
{ id: 'articles', label: '文章', icon: FileText },
{ id: 'library', label: '媒体库', icon: Images },
],
},
{ label: '管理', items: [{ id: 'settings', label: '设置', icon: Settings }] },
]
const examples = ref<{ mode: 'rail' | 'hidden'; state: SidebarState }[]>([
{ mode: 'rail', state: 'expanded' },
{ mode: 'hidden', state: 'expanded' },
])
</script>
<template>
<Stack gap="lg" class="w-full">
<Stack v-for="example in examples" :key="example.mode" gap="sm">
<Text size="sm" tone="faint">collapsible="{{ example.mode }}" · {{ example.state }}</Text>
<AppShell
v-model:sidebar="example.state"
:collapsible="example.mode"
mobile-title="Hina UI"
class="border-line h-112 w-full rounded-lg border"
>
<template #sidebar>
<Sidebar>
<template #icon>
<Avatar src="/favicon.png" name="Hina UI" class="rounded-md" />
</template>
<template #wordmark>
<Stack gap="none">
<Text weight="medium" class="truncate">Hina UI</Text>
<Text size="xs" tone="muted" class="truncate">工作空间</Text>
</Stack>
</template>
<SidebarGroup v-for="group in groups" :key="group.label" :label="group.label">
<NavLink
v-for="item in group.items"
:key="item.id"
:href="`#${item.id}`"
:label="item.label"
:active="selected === item.id"
@click.prevent="selected = item.id"
>
<template #icon><component :is="item.icon" /></template>
{{ item.label }}
</NavLink>
</SidebarGroup>
<template #footer>
<Inline gap="sm" align="center" :wrap="false">
<Avatar src="/avatars/paper.webp" name="星见书音" />
<SidebarLabel as="div" class="flex-1">
<Stack gap="none">
<Text size="sm" weight="medium" class="truncate">星见书音</Text>
<Text size="xs" tone="muted" class="truncate">管理员</Text>
</Stack>
</SidebarLabel>
</Inline>
</template>
</Sidebar>
</template>
<template #header>
<SidebarTrigger />
<Heading :level="2" size="sm" class="truncate">创作者中心</Heading>
</template>
<Stack gap="sm" class="p-6">
<Heading :level="3" size="sm">
{{ groups.flatMap(group => group.items).find(item => item.id === selected)?.label }}
</Heading>
<Text size="sm" tone="muted">切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。</Text>
</Stack>
</AppShell>
</Stack>
</Stack>
</template>
桌面侧栏切换完成后触发一次 @size-stable,无动画的切换也会触发。图表等测量成本较高的内容可以在此时调整尺寸;使用这个事件时,应关闭图表自身的持续尺寸监听,避免重复响应。内部的 ScrollArea 会在侧栏过渡期间暂缓测量,结束后统一更新,期间仍可正常滚动。
主区域滚动
主区域自带滚动容器,内容再长也只在其内部滚动,页眉与侧栏保持不动。
标题栏不随内容滚动
第 1 段正文。
第 2 段正文。
第 3 段正文。
第 4 段正文。
第 5 段正文。
第 6 段正文。
第 7 段正文。
第 8 段正文。
第 9 段正文。
第 10 段正文。
第 11 段正文。
第 12 段正文。
<script setup lang="ts">
import { AppShell, Heading, NavLink, Sidebar, Stack, Text } from '@hina-ui/vue'
const paragraphs = Array.from({ length: 12 }, (_, i) => `第 ${i + 1} 段正文。`)
</script>
<template>
<AppShell class="border-line h-72 w-full rounded-lg border">
<template #sidebar>
<Sidebar>
<NavLink href="#" active label="正文">正文</NavLink>
<NavLink href="#" label="注释">注释</NavLink>
</Sidebar>
</template>
<template #header>
<Heading :level="2" size="sm">标题栏不随内容滚动</Heading>
</template>
<Stack gap="sm" class="p-6">
<Text v-for="p in paragraphs" :key="p" size="sm" tone="muted">{{ p }}</Text>
</Stack>
</AppShell>
</template>
行为
- 视口宽度达到 1024 像素时为桌面布局,侧栏常驻左侧;低于此宽度时侧栏改由抽屉承载,
sidebar插槽的内容原样搬进抽屉。 - 桌面端点击
SidebarTrigger在展开与collapsible指定的形态之间切换;窄屏则切换抽屉开合。 - 抽屉的开合状态可用
v-model:mobileOpen绑定。 - 移动端 Drawer 不额外显示标题栏,关闭按钮默认位于 Sidebar 页眉内,可在
Sidebar上设置:closable="false"隐藏。mobileTitle仅设置抽屉的无障碍名,默认取自界面语言。 - 抽屉内侧栏占满可用高度,页眉和页脚固定在两端,导航区填满剩余空间并独立滚动。
autoClose为真时路由变化会关闭抽屉,避免跳转后抽屉仍挡在内容前面。默认开启。- 设置
restoreKey后,组件自行保存并恢复主区域的滚动位置:位置按历史记录条目写入history.state,刷新、后退与前进时恢复,进入新页面回到顶部,带#hash的新页面滚动到对应元素。同一页面上有多个壳时使用不同的键。 - 服务端输出带一段内联脚本,在激活之前把位置设好,刷新时不会先停在顶部再跳动。内容安全策略禁止内联脚本时,这段脚本不执行,位置在激活后恢复。
- 恢复不依赖具体的路由库:通过历史记录的变化和主区域内容的更新判断页面切换,内容尚未渲染到足够高度时会等待,最多 3 秒。
keepScrollPosition()让接下来的一次页面变化保持当前位置,不回到顶部,用于切换标签页、筛选或排序这类会改变路径或查询的导航。在发起导航之前调用,只生效一次;后退与前进仍然恢复各自保存的位置。- 组件内置浮层提供方,侧栏在 rail 形态下的悬停提示不需要另行包裹。
无障碍
- 页眉渲染为
header,主区域渲染为main,侧栏的导航地标由Sidebar自行提供。 SidebarTrigger带有随界面语言给出的无障碍名(简体中文为「切换侧栏」)。- 抽屉形态下焦点被限制在抽屉内,关闭后归还给触发按钮,这由
Drawer保证。
API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
collapsible | 'rail' | 'hidden' | 'rail' | 桌面端收起后的形态 |
autoClose | boolean | true | 路由变化时是否关闭移动端抽屉 |
mobileTitle | string | 取自界面语言 | 移动端抽屉的无障碍名,不显示标题栏 |
restoreKey | string | — | 设置后保存并恢复主区域的滚动位置,值是这个滚动容器的标识 |
class | string | — | 追加到根元素的类 |
双向绑定
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
sidebar | SidebarState | 'expanded' | 桌面端侧栏形态 |
mobileOpen | boolean | false | 移动端抽屉是否打开 |
事件
| 事件 | 参数 | 说明 |
|---|---|---|
size-stable | — | 侧栏状态变化后的布局已稳定;中途反向切换只在最终布局稳定后通知 |
CSS 变量
| 变量 | 默认值 | 说明 |
|---|---|---|
--hn-app-shell-header-h | calc(3.5rem + 1px) | 顶部条的高度,含下边线;在 class 中改写会同时改变顶部条 |
Slots
| 插槽 | 说明 |
|---|---|
banner | 最顶部的公告条,横贯整个壳 |
sidebar | 侧栏内容,窄屏时搬入抽屉 |
header | 顶部条内容 |
default | 主区域内容 |
Expose
| 名称 | 类型 | 说明 |
|---|---|---|
mainViewport | HTMLElement | undefined | 主区域滚动容器的视口元素 |
mainArea | ScrollArea | undefined | 主区域的滚动容器组件 |
函数
| 函数 | 说明 |
|---|---|
keepScrollPosition(key?: string) | 让下一次页面变化保持主区域的滚动位置;传入 restoreKey 时只作用于对应的壳 |
import { keepScrollPosition } from '@hina-ui/vue'
function sort(order: string) {
keepScrollPosition()
router.replace({ query: { ...route.query, order } })
}