228 lines
5.8 KiB
Vue
228 lines
5.8 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
|
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'
|
|
import { useIndicatorTypesStore } from './stores/indicatorTypes'
|
|
import { useChannelStore } from './stores/channel'
|
|
import { useResearchTypesStore } from './stores/researchTypes'
|
|
import { useStrategyTypesStore } from './stores/strategyTypes'
|
|
import { useStateSync } from './composables/useStateSync'
|
|
import { wsManager } from './composables/useWebSocket'
|
|
import { authService } from './composables/useAuth'
|
|
|
|
const isAuthenticated = authService.isAuthenticated
|
|
const authError = ref<string>()
|
|
const isMobile = ref(false)
|
|
let stateSyncCleanup: (() => void) | null = null
|
|
|
|
// Horizontal split: chart width in pixels (initialized on mount)
|
|
const CHART_MIN_PX = 300
|
|
const CHAT_MIN_PX = 240
|
|
const CHART_DEFAULT_RATIO = 0.62
|
|
const chartWidth = ref(0)
|
|
let hDragStartX = 0
|
|
let hDragStartWidth = 0
|
|
|
|
function initChartWidth() {
|
|
chartWidth.value = Math.round(window.innerWidth * CHART_DEFAULT_RATIO)
|
|
}
|
|
|
|
function startHDrag(e: PointerEvent) {
|
|
e.preventDefault()
|
|
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
|
hDragStartX = e.clientX
|
|
hDragStartWidth = chartWidth.value
|
|
}
|
|
|
|
function onHDragMove(e: PointerEvent) {
|
|
if (!e.buttons) return
|
|
const delta = e.clientX - hDragStartX
|
|
const maxWidth = window.innerWidth - CHAT_MIN_PX - 4
|
|
chartWidth.value = Math.max(CHART_MIN_PX, Math.min(maxWidth, hDragStartWidth + delta))
|
|
}
|
|
|
|
// Clamp chartWidth so chart + chat always fit within the window
|
|
function clampChartWidth() {
|
|
const maxWidth = window.innerWidth - CHAT_MIN_PX - 4
|
|
if (maxWidth >= CHART_MIN_PX) {
|
|
chartWidth.value = Math.max(CHART_MIN_PX, Math.min(maxWidth, chartWidth.value))
|
|
}
|
|
}
|
|
|
|
// Check screen width for mobile layout
|
|
const checkMobile = () => {
|
|
isMobile.value = window.innerWidth < 768
|
|
if (!isMobile.value) clampChartWidth()
|
|
}
|
|
|
|
const chartStore = useChartStore()
|
|
|
|
// Watch mobile state and update ChartStore
|
|
watch(isMobile, (mobile) => {
|
|
if (mobile) {
|
|
chartStore.symbol = null as any
|
|
chartStore.start_time = null
|
|
chartStore.end_time = null
|
|
chartStore.period = null as any
|
|
}
|
|
})
|
|
|
|
// Check if user is already authenticated on page load
|
|
onMounted(async () => {
|
|
initChartWidth()
|
|
checkMobile()
|
|
window.addEventListener('resize', checkMobile)
|
|
|
|
const token = authService.getToken()
|
|
if (token) {
|
|
const sessionValid = await authService.checkAuth()
|
|
if (sessionValid) {
|
|
isAuthenticated.value = true
|
|
try {
|
|
await wsManager.connect(token)
|
|
await initializeApp()
|
|
} catch (err) {
|
|
console.error('Failed to connect WebSocket on session restore:', err)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
const handleAuthenticate = async (email: string, password: string) => {
|
|
authError.value = undefined
|
|
try {
|
|
const result = await authService.login(email, password)
|
|
if (!result.success) {
|
|
authError.value = result.error || 'Login failed'
|
|
return
|
|
}
|
|
if (!result.token) {
|
|
authError.value = 'No token received from server'
|
|
return
|
|
}
|
|
await wsManager.connect(result.token)
|
|
await initializeApp()
|
|
} catch (err: any) {
|
|
authError.value = err.message || 'Authentication failed'
|
|
console.error('Authentication error:', err)
|
|
}
|
|
}
|
|
|
|
const initializeApp = async () => {
|
|
const chartStore = useChartStore()
|
|
const shapeStore = useShapeStore()
|
|
const indicatorStore = useIndicatorStore()
|
|
const indicatorTypesStore = useIndicatorTypesStore()
|
|
const channelStore = useChannelStore()
|
|
const researchTypesStore = useResearchTypesStore()
|
|
const strategyTypesStore = useStrategyTypesStore()
|
|
const stateSync = useStateSync({
|
|
chartState: chartStore,
|
|
shapes: shapeStore,
|
|
indicators: indicatorStore,
|
|
indicator_types: indicatorTypesStore,
|
|
research_types: researchTypesStore,
|
|
strategy_types: strategyTypesStore,
|
|
channelState: channelStore
|
|
})
|
|
stateSyncCleanup = stateSync.cleanup
|
|
}
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('resize', checkMobile)
|
|
if (stateSyncCleanup) stateSyncCleanup()
|
|
wsManager.disconnect()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="app-container dark">
|
|
<LoginScreen
|
|
v-if="!isAuthenticated"
|
|
:error-message="authError"
|
|
@authenticate="handleAuthenticate"
|
|
/>
|
|
<div v-else-if="!isMobile" class="desktop-layout">
|
|
<div class="top-area">
|
|
<div class="chart-panel" :style="{ width: chartWidth + 'px' }">
|
|
<ChartView />
|
|
</div>
|
|
<div class="h-grabber" @pointerdown="startHDrag" @pointermove="onHDragMove" />
|
|
<div class="chat-panel">
|
|
<ChatPanel />
|
|
</div>
|
|
</div>
|
|
<BottomTray />
|
|
</div>
|
|
<div v-else class="mobile-layout">
|
|
<ChatPanel />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.app-container {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
background: #0f0f0f;
|
|
}
|
|
|
|
.desktop-layout {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.top-area {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: row;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.chart-panel {
|
|
flex-shrink: 0;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.h-grabber {
|
|
width: 4px;
|
|
flex-shrink: 0;
|
|
height: 100%;
|
|
background: #2e2e2e;
|
|
cursor: col-resize;
|
|
}
|
|
|
|
.h-grabber:hover {
|
|
background: #444;
|
|
}
|
|
|
|
.chat-panel {
|
|
flex: 1;
|
|
min-width: 240px;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.mobile-layout {
|
|
width: 100%;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
</style>
|