详情切换动画与侧栏折叠动画
详情换选中:内容 out-in 淡切,容器高度从旧值滑到新值。auto 高度不可 插值,做法是离开前钉住当前高度,新内容进场时量出目标值过渡过去,结束 后清掉内联样式交还给内容。 侧栏折叠改成裁切式:只过渡外层宽度,内层内容按展开宽度定死、被 overflow 裁掉,文字用透明度淡出(比宽度动画快,先消失再收边,不露半截字)。原来 用 display: none 藏文字加居中图标,动画中途会重排跳位。折叠宽度取 60px 是因为图标中心恰好在 30px,收拢后天然居中,全程不用挪任何东西;折叠 按钮钉在右下角,跟着边缘走。
This commit is contained in:
+40
-22
@@ -137,14 +137,22 @@ const current = computed(() => NAV.find((n) => n.key === active.value))
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/*
|
||||||
|
* 侧栏折叠的做法:只过渡外层宽度,内层内容保持展开时的固定宽度、被 overflow 裁切,
|
||||||
|
* 文字用透明度淡出。不用 display: none 藏文字——那会让内容在动画中途重排、图标跳位。
|
||||||
|
* 60px 这个折叠宽度是算出来的:图标中心在 12+1+8+9=30px,正好是 60 的一半,
|
||||||
|
* 收拢后图标视觉居中,全程不需要挪动任何东西。
|
||||||
|
*/
|
||||||
.shell {
|
.shell {
|
||||||
|
--side-w: 184px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 184px 1fr;
|
grid-template-columns: var(--side-w) 1fr;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
transition: grid-template-columns 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell.is-compact {
|
.shell.is-compact {
|
||||||
grid-template-columns: 56px 1fr;
|
--side-w: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 侧栏 ───────────────────────────────────── */
|
/* ── 侧栏 ───────────────────────────────────── */
|
||||||
@@ -163,6 +171,14 @@ const current = computed(() => NAV.find((n) => n.key === active.value))
|
|||||||
padding: var(--s-4) var(--s-3);
|
padding: var(--s-4) var(--s-3);
|
||||||
border-right: 1px solid var(--c-border);
|
border-right: 1px solid var(--c-border);
|
||||||
background: var(--c-surface);
|
background: var(--c-surface);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容按展开宽度定死(184 - 24 内边距 - 1 边框),收拢时被裁掉而不是挤压换行 */
|
||||||
|
.brand,
|
||||||
|
.nav,
|
||||||
|
.side-foot {
|
||||||
|
min-width: 159px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
@@ -204,6 +220,19 @@ const current = computed(() => NAV.find((n) => n.key === active.value))
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 收拢时淡出的文字。比外层宽度的 0.3s 快,字先消失、再收边,不露半截字 */
|
||||||
|
.brand-name,
|
||||||
|
.nav-label,
|
||||||
|
.kernel {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-compact .brand-name,
|
||||||
|
.is-compact .nav-label,
|
||||||
|
.is-compact .kernel {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -249,9 +278,9 @@ const current = computed(() => NAV.find((n) => n.key === active.value))
|
|||||||
.side-foot {
|
.side-foot {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--s-2);
|
gap: var(--s-2);
|
||||||
padding: 0 var(--s-2);
|
/* 右边给绝对定位的折叠按钮留位置 */
|
||||||
|
padding: 0 44px 0 var(--s-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.kernel {
|
.kernel {
|
||||||
@@ -281,10 +310,14 @@ const current = computed(() => NAV.find((n) => n.key === active.value))
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 钉在侧栏右下角:宽度过渡时自动跟着边缘走。right 取 17 是为了收拢到 60px 时正好居中 */
|
||||||
.densify {
|
.densify {
|
||||||
flex: none;
|
position: absolute;
|
||||||
display: grid;
|
right: 17px;
|
||||||
place-items: center;
|
bottom: var(--s-4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
@@ -298,21 +331,6 @@ const current = computed(() => NAV.find((n) => n.key === active.value))
|
|||||||
color: var(--c-text);
|
color: var(--c-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 紧凑模式:文字收起来,只留图标 */
|
|
||||||
.is-compact .brand-name,
|
|
||||||
.is-compact .nav-label,
|
|
||||||
.is-compact .kernel-text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.is-compact .brand,
|
|
||||||
.is-compact .nav-item {
|
|
||||||
justify-content: center;
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
.is-compact .side-foot {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── 主区 ───────────────────────────────────── */
|
/* ── 主区 ───────────────────────────────────── */
|
||||||
.main {
|
.main {
|
||||||
|
|||||||
@@ -147,6 +147,42 @@ function toggleOpen() {
|
|||||||
open.value = !open.value
|
open.value = !open.value
|
||||||
if (open.value) void revealBottom()
|
if (open.value) void revealBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换选中的入站时,内容淡出淡入,容器高度从旧值滑到新值。
|
||||||
|
* 高度过渡没法纯 CSS 做:auto 不可插值。所以离开前把当前高度钉住,
|
||||||
|
* 新内容进场时量出目标高度、过渡过去,结束后清掉内联样式让内容重新做主。
|
||||||
|
*/
|
||||||
|
function lockHeight() {
|
||||||
|
const el = rootEl.value
|
||||||
|
if (!el) return
|
||||||
|
el.style.height = `${String(el.offsetHeight)}px`
|
||||||
|
el.style.overflow = 'hidden'
|
||||||
|
}
|
||||||
|
|
||||||
|
function settleHeight() {
|
||||||
|
const el = rootEl.value
|
||||||
|
if (!el || !el.style.height) return
|
||||||
|
const from = el.style.height
|
||||||
|
el.style.height = 'auto'
|
||||||
|
const to = `${String(el.offsetHeight)}px`
|
||||||
|
if (to === from) {
|
||||||
|
el.style.height = ''
|
||||||
|
el.style.overflow = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
el.style.height = from
|
||||||
|
void el.offsetHeight
|
||||||
|
el.style.transition = 'height 0.15s ease'
|
||||||
|
el.style.height = to
|
||||||
|
el.addEventListener('transitionend', function done(event) {
|
||||||
|
if (event.propertyName !== 'height') return
|
||||||
|
el.removeEventListener('transitionend', done)
|
||||||
|
el.style.transition = ''
|
||||||
|
el.style.height = ''
|
||||||
|
el.style.overflow = ''
|
||||||
|
})
|
||||||
|
}
|
||||||
const form = ref<OutboundSetup>({ proxy: { sources: [], filter: null }, chain: null })
|
const form = ref<OutboundSetup>({ proxy: { sources: [], filter: null }, chain: null })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -338,6 +374,15 @@ const complete = computed(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside ref="rootEl" class="detail">
|
<aside ref="rootEl" class="detail">
|
||||||
|
<!-- 换一路入站:淡出旧内容、淡入新内容,高度的过渡由上面两个钩子做 -->
|
||||||
|
<Transition
|
||||||
|
name="swap"
|
||||||
|
mode="out-in"
|
||||||
|
@before-leave="lockHeight"
|
||||||
|
@enter="settleHeight"
|
||||||
|
@after-enter="moveInk"
|
||||||
|
>
|
||||||
|
<div :key="inbound.id">
|
||||||
<header class="top">
|
<header class="top">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<span class="port mono">{{ inbound.port }}</span>
|
<span class="port mono">{{ inbound.port }}</span>
|
||||||
@@ -588,6 +633,8 @@ const complete = computed(
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -779,6 +826,17 @@ h3 {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 切换入站的内容淡切。高度的过渡不在这里,由 lockHeight/settleHeight 用内联样式做 */
|
||||||
|
.swap-enter-active,
|
||||||
|
.swap-leave-active {
|
||||||
|
transition: opacity 0.12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swap-enter-from,
|
||||||
|
.swap-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
|
||||||
|
|||||||
Reference in New Issue
Block a user