bugfix; web tabs
This commit is contained in:
@@ -5,6 +5,7 @@ import SplitterPanel from 'primevue/splitterpanel'
|
||||
import ChartView from './components/ChartView.vue'
|
||||
import ChatPanel from './components/ChatPanel.vue'
|
||||
import LoginScreen from './components/LoginScreen.vue'
|
||||
import BottomTray from './components/BottomTray.vue'
|
||||
import { useChartStore } from './stores/chart'
|
||||
import { useShapeStore } from './stores/shapes'
|
||||
import { useIndicatorStore } from './stores/indicators'
|
||||
@@ -137,14 +138,19 @@ onBeforeUnmount(() => {
|
||||
:error-message="authError"
|
||||
@authenticate="handleAuthenticate"
|
||||
/>
|
||||
<Splitter v-else-if="!isMobile" class="main-splitter">
|
||||
<SplitterPanel :size="62" :minSize="40" class="chart-panel">
|
||||
<ChartView />
|
||||
</SplitterPanel>
|
||||
<SplitterPanel :size="38" :minSize="20" class="chat-panel">
|
||||
<ChatPanel />
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
<div v-else-if="!isMobile" class="desktop-layout">
|
||||
<div class="top-area">
|
||||
<Splitter class="main-splitter">
|
||||
<SplitterPanel :size="62" :minSize="40" class="chart-panel">
|
||||
<ChartView />
|
||||
</SplitterPanel>
|
||||
<SplitterPanel :size="38" :minSize="20" class="chat-panel">
|
||||
<ChatPanel />
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</div>
|
||||
<BottomTray />
|
||||
</div>
|
||||
<div v-else class="mobile-layout">
|
||||
<ChatPanel />
|
||||
</div>
|
||||
@@ -165,8 +171,21 @@ onBeforeUnmount(() => {
|
||||
background: #0f0f0f !important;
|
||||
}
|
||||
|
||||
.desktop-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.top-area {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-splitter {
|
||||
height: 100vh !important;
|
||||
height: 100% !important;
|
||||
background: #0f0f0f !important;
|
||||
}
|
||||
|
||||
@@ -200,7 +219,7 @@ onBeforeUnmount(() => {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
cursor: col-resize;
|
||||
cursor: auto;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
279
web/src/components/BottomTray.vue
Normal file
279
web/src/components/BottomTray.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onBeforeUnmount, 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'
|
||||
|
||||
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: MouseEvent) {
|
||||
e.preventDefault()
|
||||
resizeStartY = e.clientY
|
||||
resizeStartHeight = expandedHeight.value
|
||||
document.addEventListener('mousemove', onResizeMove)
|
||||
document.addEventListener('mouseup', stopResize)
|
||||
}
|
||||
|
||||
function onResizeMove(e: MouseEvent) {
|
||||
const delta = resizeStartY - e.clientY // dragging up increases height
|
||||
expandedHeight.value = Math.max(MIN_EXPANDED, resizeStartHeight + delta)
|
||||
}
|
||||
|
||||
function stopResize() {
|
||||
document.removeEventListener('mousemove', onResizeMove)
|
||||
document.removeEventListener('mouseup', stopResize)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('mousemove', onResizeMove)
|
||||
document.removeEventListener('mouseup', stopResize)
|
||||
})
|
||||
|
||||
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" @mousedown="startResize" />
|
||||
<Tabs :value="activeTab" class="tray-tabs">
|
||||
<TabList class="tray-tab-list">
|
||||
<Tab value="orders" @click="onTabClick('orders')">Orders</Tab>
|
||||
<Tab value="strategies" @click="onTabClick('strategies')">Strategies</Tab>
|
||||
<Tab value="positions" @click="onTabClick('positions')">Positions</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">✕</button>
|
||||
</TabList>
|
||||
<TabPanels v-if="isExpanded" class="tray-panels">
|
||||
<TabPanel value="orders" class="tray-panel"><OrdersTab /></TabPanel>
|
||||
<TabPanel value="strategies" class="tray-panel"><PlaceholderTab label="Strategies" /></TabPanel>
|
||||
<TabPanel value="positions" class="tray-panel"><PlaceholderTab label="Positions" /></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>
|
||||
69
web/src/components/tabs/OrdersTab.vue
Normal file
69
web/src/components/tabs/OrdersTab.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Column from 'primevue/column'
|
||||
import { useOrderStore } from '../../stores/orders'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const ordersStore = useOrderStore()
|
||||
const { orders } = storeToRefs(ordersStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DataTable
|
||||
:value="orders"
|
||||
scrollable
|
||||
scrollHeight="flex"
|
||||
size="small"
|
||||
class="orders-table"
|
||||
:empty-message="'No orders'"
|
||||
>
|
||||
<Column field="tokenIn" header="Token In" style="min-width: 90px" />
|
||||
<Column field="tokenOut" header="Token Out" style="min-width: 90px" />
|
||||
<Column field="route.exchange" header="Exchange" style="min-width: 100px" />
|
||||
<Column field="route.fee" header="Fee" style="min-width: 70px" />
|
||||
<Column field="amount" header="Amount" style="min-width: 100px" />
|
||||
<Column field="minFillAmount" header="Min Fill" style="min-width: 100px" />
|
||||
<Column field="amountIsInput" header="Amt Is Input" style="min-width: 100px">
|
||||
<template #body="{ data }">{{ data.amountIsInput ? 'Yes' : 'No' }}</template>
|
||||
</Column>
|
||||
<Column field="conditionalOrder" header="Condition" style="min-width: 100px" />
|
||||
</DataTable>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.orders-table {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.orders-table :deep(.p-datatable-header-cell) {
|
||||
background: #1a1a1a !important;
|
||||
color: #aaa !important;
|
||||
border-color: #2e2e2e !important;
|
||||
padding: 4px 8px !important;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.orders-table :deep(.p-datatable-row-cell) {
|
||||
background: #0f0f0f !important;
|
||||
color: #dbdbdb !important;
|
||||
border-color: #1e1e1e !important;
|
||||
padding: 3px 8px !important;
|
||||
}
|
||||
|
||||
.orders-table :deep(tr:hover .p-datatable-row-cell) {
|
||||
background: #1a1a1a !important;
|
||||
}
|
||||
|
||||
.orders-table :deep(.p-datatable-empty-message td) {
|
||||
background: #0f0f0f !important;
|
||||
color: #555 !important;
|
||||
text-align: center;
|
||||
padding: 16px !important;
|
||||
}
|
||||
</style>
|
||||
20
web/src/components/tabs/PlaceholderTab.vue
Normal file
20
web/src/components/tabs/PlaceholderTab.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ label: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="placeholder-tab">
|
||||
<span>{{ label }} — no data</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.placeholder-tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user