279 lines
6.8 KiB
Vue
279 lines
6.8 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed, type Component } from 'vue'
|
||
import Tabs from 'primevue/tabs'
|
||
import TabList from 'primevue/tablist'
|
||
import Tab from 'primevue/tab'
|
||
import TabPanels from 'primevue/tabpanels'
|
||
import TabPanel from 'primevue/tabpanel'
|
||
import OrdersTab from './tabs/OrdersTab.vue'
|
||
import PlaceholderTab from './tabs/PlaceholderTab.vue'
|
||
import ResearchTab from './tabs/ResearchTab.vue'
|
||
import StrategiesTab from './tabs/StrategiesTab.vue'
|
||
import IndicatorsTab from './tabs/IndicatorsTab.vue'
|
||
|
||
interface TempTab {
|
||
id: string
|
||
label: string
|
||
component: Component
|
||
props?: Record<string, any>
|
||
}
|
||
|
||
const COLLAPSED_HEIGHT = 34
|
||
const DEFAULT_EXPANDED = 260
|
||
const MIN_EXPANDED = 80
|
||
|
||
const isExpanded = ref(false)
|
||
const expandedHeight = ref(DEFAULT_EXPANDED)
|
||
const activeTab = ref('orders')
|
||
const tempTabs = ref<TempTab[]>([])
|
||
|
||
const trayStyle = computed(() => ({
|
||
height: isExpanded.value ? `${expandedHeight.value}px` : `${COLLAPSED_HEIGHT}px`,
|
||
}))
|
||
|
||
function onTabClick(tabId: string) {
|
||
if (!isExpanded.value) {
|
||
activeTab.value = tabId
|
||
isExpanded.value = true
|
||
} else if (activeTab.value === tabId) {
|
||
isExpanded.value = false
|
||
} else {
|
||
activeTab.value = tabId
|
||
}
|
||
}
|
||
|
||
function closeTab(tabId: string) {
|
||
const idx = tempTabs.value.findIndex(t => t.id === tabId)
|
||
if (idx === -1) return
|
||
tempTabs.value.splice(idx, 1)
|
||
if (activeTab.value === tabId) {
|
||
activeTab.value = tempTabs.value[idx - 1]?.id ?? tempTabs.value[0]?.id ?? 'orders'
|
||
if (tempTabs.value.length === 0) isExpanded.value = false
|
||
}
|
||
}
|
||
|
||
// Resize handle drag
|
||
let resizeStartY = 0
|
||
let resizeStartHeight = 0
|
||
|
||
function startResize(e: PointerEvent) {
|
||
e.preventDefault()
|
||
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
||
resizeStartY = e.clientY
|
||
resizeStartHeight = expandedHeight.value
|
||
}
|
||
|
||
function onResizeMove(e: PointerEvent) {
|
||
if (!e.buttons) return
|
||
const delta = resizeStartY - e.clientY // dragging up increases height
|
||
expandedHeight.value = Math.max(MIN_EXPANDED, resizeStartHeight + delta)
|
||
}
|
||
|
||
defineExpose({
|
||
openTab(id: string, label: string, component: Component, props?: Record<string, any>) {
|
||
const existing = tempTabs.value.find(t => t.id === id)
|
||
if (!existing) {
|
||
tempTabs.value.push({ id, label, component, props })
|
||
}
|
||
activeTab.value = id
|
||
isExpanded.value = true
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="bottom-tray" :style="trayStyle">
|
||
<div v-if="isExpanded" class="tray-resize-handle" @pointerdown="startResize" @pointermove="onResizeMove" />
|
||
<Tabs :value="isExpanded ? activeTab : null" class="tray-tabs">
|
||
<TabList class="tray-tab-list">
|
||
<Tab value="positions" @click="onTabClick('positions')">Positions</Tab>
|
||
<Tab value="orders" @click="onTabClick('orders')">Orders</Tab>
|
||
<Tab value="indicators" @click="onTabClick('indicators')">Indicators</Tab>
|
||
<Tab value="research" @click="onTabClick('research')">Research</Tab>
|
||
<Tab value="strategies" @click="onTabClick('strategies')">Strategies</Tab>
|
||
<Tab
|
||
v-for="tab in tempTabs"
|
||
:key="tab.id"
|
||
:value="tab.id"
|
||
class="tab-closeable"
|
||
@click="onTabClick(tab.id)"
|
||
>
|
||
{{ tab.label }}
|
||
<button class="tab-close-btn" @click.stop="closeTab(tab.id)">×</button>
|
||
</Tab>
|
||
<div class="tray-spacer" />
|
||
<button v-if="isExpanded" class="tray-close-btn" @click="isExpanded = false" title="Minimize">
|
||
<i class="pi pi-chevron-down" />
|
||
</button>
|
||
</TabList>
|
||
<TabPanels v-if="isExpanded" class="tray-panels">
|
||
<TabPanel value="positions" class="tray-panel"><PlaceholderTab label="Positions" /></TabPanel>
|
||
<TabPanel value="orders" class="tray-panel"><OrdersTab /></TabPanel>
|
||
<TabPanel value="research" class="tray-panel"><ResearchTab /></TabPanel>
|
||
<TabPanel value="indicators" class="tray-panel"><IndicatorsTab /></TabPanel>
|
||
<TabPanel value="strategies" class="tray-panel"><StrategiesTab /></TabPanel>
|
||
<TabPanel
|
||
v-for="tab in tempTabs"
|
||
:key="tab.id"
|
||
:value="tab.id"
|
||
class="tray-panel"
|
||
>
|
||
<component :is="tab.component" v-bind="tab.props" />
|
||
</TabPanel>
|
||
</TabPanels>
|
||
</Tabs>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.bottom-tray {
|
||
flex-shrink: 0;
|
||
width: 100%;
|
||
background: #0f0f0f;
|
||
border-top: 1px solid #2e2e2e;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
transition: height 0.15s ease;
|
||
}
|
||
|
||
.tray-resize-handle {
|
||
height: 4px;
|
||
flex-shrink: 0;
|
||
background: #2e2e2e;
|
||
cursor: row-resize;
|
||
width: 100%;
|
||
}
|
||
|
||
.tray-resize-handle:hover {
|
||
background: #444;
|
||
}
|
||
|
||
.tray-tabs {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
min-height: 0;
|
||
}
|
||
|
||
.tray-tab-list {
|
||
height: 34px !important;
|
||
min-height: 34px !important;
|
||
flex-shrink: 0;
|
||
display: flex !important;
|
||
align-items: center;
|
||
padding: 0 4px;
|
||
background: #141414 !important;
|
||
border-bottom: 1px solid #2e2e2e !important;
|
||
gap: 2px;
|
||
}
|
||
|
||
/* Override PrimeVue Tab default styles */
|
||
.tray-tab-list :deep(.p-tab) {
|
||
padding: 0 12px !important;
|
||
height: 28px !important;
|
||
line-height: 28px !important;
|
||
font-size: 12px !important;
|
||
color: #888 !important;
|
||
background: transparent !important;
|
||
border: none !important;
|
||
border-radius: 4px !important;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.tray-tab-list :deep(.p-tab:hover) {
|
||
color: #dbdbdb !important;
|
||
background: #1e1e1e !important;
|
||
}
|
||
|
||
.tray-tab-list :deep(.p-tab-active) {
|
||
color: #dbdbdb !important;
|
||
background: #1e1e1e !important;
|
||
}
|
||
|
||
/* Hide the PrimeVue active indicator bar */
|
||
.tray-tab-list :deep(.p-tablist-active-bar) {
|
||
display: none !important;
|
||
}
|
||
|
||
.tab-close-btn {
|
||
background: none;
|
||
border: none;
|
||
color: #666;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
line-height: 1;
|
||
padding: 0 2px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.tab-close-btn:hover {
|
||
color: #dbdbdb;
|
||
}
|
||
|
||
.tray-spacer {
|
||
flex: 1;
|
||
}
|
||
|
||
.tray-close-btn {
|
||
background: none;
|
||
border: none;
|
||
color: #666;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
padding: 0 8px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
border-radius: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.tray-close-btn:hover {
|
||
color: #dbdbdb;
|
||
background: #1e1e1e;
|
||
}
|
||
|
||
.tray-panels {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
padding: 0 !important;
|
||
background: #0f0f0f !important;
|
||
}
|
||
|
||
.tray-panels :deep(.p-tabpanels) {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 0 !important;
|
||
background: #0f0f0f !important;
|
||
}
|
||
|
||
.tray-panel {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
padding: 0 !important;
|
||
}
|
||
|
||
.tray-panel :deep(.p-tabpanel-content) {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
padding: 0 !important;
|
||
background: #0f0f0f !important;
|
||
}
|
||
</style>
|