收缩按钮

This commit is contained in:
tianmo committed 2026-10-27 16:02:18 +08:00
1 parent 0f578d29a4
commit 67bf3921cc
9 files changed
+241 -86

No files matched your search

+149 -5
View File
@@ -13,7 +13,11 @@
<!-- 右列:上=画布区,下=时间轴区(高度可拖) -->
<div class="right">
<div class="stage-area">
<div
class="stage-area"
:class="{ 'timeline-collapsed': timelineCollapsed, 'operation-bar-clipped': operationBarHeight !== null }"
:style="stageStyle"
>
<Stage />
<nav
v-if="store.settings.floatingMenuEnabled"
@@ -38,9 +42,24 @@
<span class="floating-section-label">{{ $t(section.titleKey) }}</span>
</button>
</nav>
<button
type="button"
class="timeline-collapse-toggle"
:class="{ collapsed: !bottomPanelExpanded }"
:aria-expanded="bottomPanelExpanded"
:title="timelineCollapsed ? '展开时间轴' : '点击收起,拖动调整高度'"
@pointerdown="onTimelineTogglePointerDown"
@click="toggleTimeline"
>
<svg class="timeline-collapse-shape" viewBox="0 0 160 34" preserveAspectRatio="none" aria-hidden="true">
<path class="timeline-collapse-fill" d="M0 34 C 24 34 35 32 48 25 C 60 19 68 4 80 4 C 92 4 100 19 112 25 C 125 32 136 34 160 34 Z" />
<path class="timeline-collapse-outline" vector-effect="non-scaling-stroke" d="M0 34 C 24 34 35 32 48 25 C 60 19 68 4 80 4 C 92 4 100 19 112 25 C 125 32 136 34 160 34" />
</svg>
<span class="timeline-collapse-lines"><i></i><i></i><i></i></span>
</button>
</div>
<div class="resizer-row" @pointerdown="onTimelineDrag" :title="$t('app.resizeTimeline')"></div>
<div class="timeline-area" :style="{ height: timelineH + 'px' }">
<div v-show="!timelineCollapsed && timelineH > 0" class="resizer-row" @pointerdown="onTimelineDrag" :title="$t('app.resizeTimeline')"></div>
<div v-show="!timelineCollapsed && timelineH > 0" class="timeline-area" :style="{ height: timelineH + 'px' }">
<Timeline />
</div>
</div>
@@ -65,7 +84,100 @@ installSystemSettingsCache(store)
const { activeSection, activateSection, canUseSection } = provideParticlePanel()
const showParameterPanel = computed(() => !store.settings.floatingMenuEnabled || activeSection.value !== null)
const panelW = ref(360)
const timelineH = ref(180)
const TIMELINE_HEIGHT_KEY = 'SpineParticle.TimelineHeight.v1'
function loadTimelineHeight() {
try {
const height = Number(localStorage.getItem(TIMELINE_HEIGHT_KEY))
if (!Number.isFinite(height) || height <= 0) return 180
return Math.min(height, window.innerHeight - 200)
} catch {
return 180
}
}
function saveTimelineHeight() {
if (timelineH.value <= 0) return
try { localStorage.setItem(TIMELINE_HEIGHT_KEY, String(timelineH.value)) } catch { /* 缓存不可用时保留当前会话高度。 */ }
}
const timelineH = ref(loadTimelineHeight())
const timelineCollapsed = ref(false)
const operationBarHeight = ref<number | null>(null)
let lastExpandedTimelineHeight = timelineH.value
let fullOperationBarHeight = 84
const stageStyle = computed<Record<string, string> | undefined>(() =>
operationBarHeight.value == null
? undefined
: { '--operation-bar-height': `${operationBarHeight.value}px` },
)
const bottomPanelExpanded = computed(() =>
!timelineCollapsed.value
&& (timelineH.value > 0 || operationBarHeight.value == null || operationBarHeight.value > 0),
)
let ignoreTimelineToggleClick = false
function toggleTimeline() {
if (ignoreTimelineToggleClick) return
if (!bottomPanelExpanded.value) {
operationBarHeight.value = null
timelineH.value = lastExpandedTimelineHeight
timelineCollapsed.value = false
return
}
const operationBar = document.querySelector<HTMLElement>('.top-operation-stack')
if (operationBar) fullOperationBarHeight = Math.max(operationBar.getBoundingClientRect().height, operationBar.scrollHeight)
if (timelineH.value > 0) lastExpandedTimelineHeight = timelineH.value
saveTimelineHeight()
operationBarHeight.value = null
timelineCollapsed.value = true
}
function onTimelineTogglePointerDown(e: PointerEvent) {
if (e.button !== 0) return
const startY = e.clientY
const operationBar = document.querySelector<HTMLElement>('.top-operation-stack')
const wasCollapsed = timelineCollapsed.value
if (!wasCollapsed && timelineH.value > 0) lastExpandedTimelineHeight = timelineH.value
const baseOperationHeight = wasCollapsed
? fullOperationBarHeight
: operationBar
? Math.max(operationBar.getBoundingClientRect().height, operationBar.scrollHeight)
: fullOperationBarHeight
fullOperationBarHeight = baseOperationHeight
const startAdjustment = wasCollapsed
? -baseOperationHeight
: timelineH.value + (operationBarHeight.value ?? baseOperationHeight) - baseOperationHeight
let dragging = false
const move = (event: PointerEvent) => {
const deltaY = startY - event.clientY
if (!dragging && Math.abs(deltaY) < 4) return
dragging = true
const nextAdjustment = Math.min(startAdjustment + deltaY, window.innerHeight - 200)
if (nextAdjustment <= 0) {
timelineCollapsed.value = false
timelineH.value = 0
operationBarHeight.value = Math.max(0, baseOperationHeight + nextAdjustment)
} else {
timelineCollapsed.value = false
timelineH.value = nextAdjustment
operationBarHeight.value = null
}
event.preventDefault()
}
const finish = () => {
window.removeEventListener('pointermove', move)
window.removeEventListener('pointerup', finish)
window.removeEventListener('pointercancel', finish)
if (!dragging) return
if (timelineH.value > 0) saveTimelineHeight()
// 拖拽后的 click 与本次手势属于同一操作,不能再次切换折叠。
ignoreTimelineToggleClick = true
setTimeout(() => { ignoreTimelineToggleClick = false })
}
window.addEventListener('pointermove', move)
window.addEventListener('pointerup', finish)
window.addEventListener('pointercancel', finish)
}
// 参数面板宽度拖拽:面板在右侧时,从视口右边缘反向计算宽度。
function onPanelDrag(e: PointerEvent) {
@@ -91,7 +203,11 @@ function onTimelineDrag(e: PointerEvent) {
const h = Math.max(80, Math.min(startH + (startY - ev.clientY), window.innerHeight - 200))
timelineH.value = h
}
const up = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up) }
const up = () => {
window.removeEventListener('pointermove', move)
window.removeEventListener('pointerup', up)
saveTimelineHeight()
}
window.addEventListener('pointermove', move)
window.addEventListener('pointerup', up)
}
@@ -134,6 +250,34 @@ function onTimelineDrag(e: PointerEvent) {
}
.floating-section-tab.selected .floating-section-index { background: #7771e8; color: #fff; }
.floating-section-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stage-area.timeline-collapsed :deep(.top-operation-stack) { display: none; }
.stage-area.operation-bar-clipped :deep(.top-operation-stack) {
height: var(--operation-bar-height);
padding-top: min(8px, calc(var(--operation-bar-height) * .1));
padding-bottom: min(8px, calc(var(--operation-bar-height) * .1));
overflow: hidden;
box-sizing: border-box;
}
.timeline-collapse-toggle {
position: absolute; left: 50%; bottom: calc(var(--operation-bar-height, 84px) - 1px); z-index: 21;
width: 96px; height: 20.4px; padding: 0; transform: translateX(-50%);
border: 0; background: transparent; color: #8792a6; cursor: ns-resize;
touch-action: none; user-select: none;
}
.timeline-collapse-toggle.collapsed { bottom: 0; }
.timeline-collapse-shape {
position: absolute; inset: 0; width: 100%; height: 100%;
fill: none;
}
.timeline-collapse-fill { fill: #12121c; }
.timeline-collapse-outline { fill: none; stroke: #2a2a3c; stroke-width: .75; }
.timeline-collapse-lines {
position: absolute; top: 7px; left: 50%; display: grid;
gap: 2px; transform: translateX(-50%);
}
.timeline-collapse-lines i { display: block; width: 11px; height: 1px; border-radius: 1px; background: currentColor; }
.timeline-collapse-toggle:hover { color: #b3bdce; }
.timeline-collapse-toggle:focus-visible { outline: 2px solid #8b86ff; outline-offset: -2px; }
.resizer-row { height: 4px; cursor: row-resize; background: #1c1c2a; border-top: 1px solid #2a2a3c; border-bottom: 1px solid #2a2a3c; flex-shrink: 0; }
.resizer-row:hover { background: #3a5a8c; }
.timeline-area { position: relative; z-index: 10; flex-shrink: 0; overflow: visible; }