feat: introduce mnie
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
<script setup lang="ts">
|
||||
import { AnimatePresence } from 'motion-v'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { revokeApiKey } from './api'
|
||||
import AppHeader from './components/layout/AppHeader.vue'
|
||||
import AppSidebar from './components/layout/AppSidebar.vue'
|
||||
import SearchDialog from './features/search/SearchDialog.vue'
|
||||
import AuthGate from './features/auth/AuthGate.vue'
|
||||
import { useAuthAdmin } from './features/auth/useAuthAdmin'
|
||||
import HistoryView from './features/history/HistoryView.vue'
|
||||
import OAuthApprovalPanel from './features/oauth/OAuthApprovalPanel.vue'
|
||||
import { useOAuthApproval } from './features/oauth/useOAuthApproval'
|
||||
import OrderDialogs from './features/orders/OrderDialogs.vue'
|
||||
import PortfolioView from './features/portfolio/PortfolioView.vue'
|
||||
import SettingsView from './features/settings/SettingsView.vue'
|
||||
import TradeView from './features/trade/TradeView.vue'
|
||||
import { useTradingSession } from './features/trading/useTradingSession'
|
||||
import { stockIdFromTradeRouteId, tradeRouteIdFromStockId } from './constants/trade'
|
||||
import { routeNames, type RouteName } from './router'
|
||||
import { ui } from './styles/ui'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const activeTab = computed<RouteName>(() => {
|
||||
return routeNames.includes(route.name as RouteName) ? (route.name as RouteName) : 'portfolio'
|
||||
})
|
||||
const isOAuthRoute = computed(() => route.name === 'oauthAuthorize')
|
||||
const showAuthGate = computed(() => true)
|
||||
const decodedRouteParam = (value: string) => {
|
||||
try {
|
||||
return decodeURIComponent(value)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
const tradePath = (id: string) => `/trade/${encodeURIComponent(id)}`
|
||||
const tradeId = computed(() =>
|
||||
typeof route.params.id === 'string'
|
||||
? stockIdFromTradeRouteId(decodedRouteParam(route.params.id))
|
||||
: '',
|
||||
)
|
||||
const navigateTrade = (id = selectedStock.value.symbol || selectedStockCode.value) => {
|
||||
if (!id) return
|
||||
void router.push(tradePath(tradeRouteIdFromStockId(id)))
|
||||
}
|
||||
const navigate = (name: RouteName) => {
|
||||
if (name === 'settings') return router.push('/settings/api-keys')
|
||||
if (name === 'trade') return navigateTrade()
|
||||
return router.push({ name })
|
||||
}
|
||||
|
||||
const {
|
||||
status,
|
||||
apiKeys,
|
||||
sbiPasskeys,
|
||||
selectedPasskeyId,
|
||||
setupPassword,
|
||||
authBusy,
|
||||
apiKeyLabel,
|
||||
newApiKeySettings,
|
||||
newApiToken,
|
||||
sbiLabel,
|
||||
sbiCredentialJson,
|
||||
tradePassword,
|
||||
sbiDeviceId,
|
||||
refresh,
|
||||
addApiKey,
|
||||
saveApiKeySettings,
|
||||
setupOwnerPasskey,
|
||||
loginWithPasskey,
|
||||
addSbiPasskey,
|
||||
removeSbiPasskey,
|
||||
} = useAuthAdmin()
|
||||
|
||||
const {
|
||||
selectedStockCode,
|
||||
selectedStockId,
|
||||
tradeSide,
|
||||
orderKind,
|
||||
cashOrderAccountType,
|
||||
cashOrderMarket,
|
||||
cashOrderPriceCondition,
|
||||
cashOrderTerm,
|
||||
cashOrderDateInput,
|
||||
cashOrderMethod,
|
||||
cashOrderTriggerZone,
|
||||
cashOrderTriggerPriceInput,
|
||||
cashOrderSecondaryPriceCondition,
|
||||
cashOrderSecondaryPriceInput,
|
||||
quantityInput,
|
||||
priceInput,
|
||||
chartMode,
|
||||
chartRange,
|
||||
showSearch,
|
||||
searchQuery,
|
||||
countryFilter,
|
||||
marketFilter,
|
||||
showEstimateDialog,
|
||||
showOrderDialog,
|
||||
lastCashEstimate,
|
||||
connected,
|
||||
dataLoading,
|
||||
searchLoading,
|
||||
buyingPower,
|
||||
holdingsMarketValue,
|
||||
totalProfitLoss,
|
||||
totalProfitLossRate,
|
||||
marketIndexes,
|
||||
orders,
|
||||
cancelingOrderKey,
|
||||
orderHistoryLoaded,
|
||||
orderHistoryNotice,
|
||||
positions,
|
||||
chartPricePoints,
|
||||
chartNotice,
|
||||
pricePolling,
|
||||
selectedStock,
|
||||
orderQuantity,
|
||||
orderPrice,
|
||||
cashOrderPrimaryRequiresPrice,
|
||||
cashOrderTriggerPrice,
|
||||
cashOrderSecondaryPrice,
|
||||
cashOrderAccountTypeOptions,
|
||||
cashOrderMarketOptions,
|
||||
cashOrderTermOptions,
|
||||
cashOrderDateOptions,
|
||||
cashOrderPriceStep,
|
||||
estimatedAmount,
|
||||
showPortfolioSpinner,
|
||||
canRequestCashEstimate,
|
||||
canPlaceCashOrder,
|
||||
countries,
|
||||
markets,
|
||||
viewedStocks,
|
||||
filteredStocks,
|
||||
selectedPosition,
|
||||
recentOrders,
|
||||
totalAssetValue,
|
||||
stockAssetRatio,
|
||||
cashAssetRatio,
|
||||
hasQuote,
|
||||
selectStock,
|
||||
selectStockByCode,
|
||||
connect,
|
||||
loadTradingData,
|
||||
estimateCashOrder,
|
||||
askPlaceOrder,
|
||||
placeCashOrder,
|
||||
cancelOrder,
|
||||
loadOrderDetail,
|
||||
loadTradeRecords,
|
||||
loadPositionDetail,
|
||||
estimateOrderCorrection,
|
||||
placeOrderCorrection,
|
||||
downloadCsv,
|
||||
openTradeForStock,
|
||||
openTradeForPosition,
|
||||
} = useTradingSession(selectedPasskeyId)
|
||||
|
||||
const { oauthApproval, oauthSettings, loadOAuthApproval, approveOAuth } = useOAuthApproval()
|
||||
|
||||
const refreshAndMaybeConnect = () =>
|
||||
refresh({
|
||||
autoConnect: true,
|
||||
connect: () => {
|
||||
if (!connected.value) connect()
|
||||
},
|
||||
})
|
||||
|
||||
const addSbiPasskeyAndConnect = async () => {
|
||||
await addSbiPasskey()
|
||||
connect()
|
||||
}
|
||||
|
||||
const revokeAndRefresh = async (id: string) => {
|
||||
await revokeApiKey(id)
|
||||
await refresh()
|
||||
}
|
||||
|
||||
watch(
|
||||
tradeId,
|
||||
(id) => {
|
||||
if (activeTab.value !== 'trade' || !id) return
|
||||
selectStockByCode(id)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
[activeTab, () => selectedStock.value.symbol, selectedStockId, selectedStockCode],
|
||||
([tab]) => {
|
||||
const id = selectedStock.value.symbol || selectedStockId.value || selectedStockCode.value
|
||||
if (tab !== 'trade' || !id || tradeId.value === id) return
|
||||
void router.replace(tradePath(tradeRouteIdFromStockId(id)))
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await refreshAndMaybeConnect()
|
||||
} catch {
|
||||
status.value = { configured: false, authenticated: false }
|
||||
}
|
||||
await loadOAuthApproval()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main :class="ui.appShell">
|
||||
<AppSidebar v-if="!isOAuthRoute" :active-tab="activeTab" @navigate="navigate" />
|
||||
|
||||
<section :class="ui.workspace">
|
||||
<AppHeader v-if="!isOAuthRoute && activeTab !== 'settings'" :active-tab="activeTab" />
|
||||
|
||||
<OAuthApprovalPanel
|
||||
v-if="oauthApproval.active && status.authenticated"
|
||||
v-model:settings="oauthSettings"
|
||||
:approval="oauthApproval"
|
||||
@approve="approveOAuth"
|
||||
/>
|
||||
|
||||
<AuthGate
|
||||
v-else-if="showAuthGate && !status.authenticated"
|
||||
v-model:setup-password="setupPassword"
|
||||
:status="status"
|
||||
:auth-busy="authBusy"
|
||||
@login="loginWithPasskey"
|
||||
@setup="setupOwnerPasskey"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<PortfolioView
|
||||
v-if="activeTab === 'portfolio'"
|
||||
:show-portfolio-spinner="showPortfolioSpinner"
|
||||
:total-asset-value="totalAssetValue"
|
||||
:buying-power="buyingPower"
|
||||
:holdings-market-value="holdingsMarketValue"
|
||||
:total-profit-loss="totalProfitLoss"
|
||||
:total-profit-loss-rate="totalProfitLossRate"
|
||||
:market-indexes="marketIndexes"
|
||||
:stock-asset-ratio="stockAssetRatio"
|
||||
:cash-asset-ratio="cashAssetRatio"
|
||||
:positions="positions"
|
||||
:recent-orders="recentOrders"
|
||||
:canceling-order-key="cancelingOrderKey"
|
||||
:data-loading="dataLoading"
|
||||
:connected="connected"
|
||||
:order-history-loaded="orderHistoryLoaded"
|
||||
:order-history-notice="orderHistoryNotice"
|
||||
:load-position-detail="loadPositionDetail"
|
||||
@connect="connect"
|
||||
@open-position="
|
||||
(code) =>
|
||||
openTradeForPosition(code, () => void navigateTrade(selectedStock.symbol || code))
|
||||
"
|
||||
@cancel-order="cancelOrder"
|
||||
/>
|
||||
|
||||
<TradeView
|
||||
v-if="activeTab === 'trade'"
|
||||
v-model:trade-side="tradeSide"
|
||||
v-model:order-kind="orderKind"
|
||||
v-model:cash-order-account-type="cashOrderAccountType"
|
||||
v-model:cash-order-market="cashOrderMarket"
|
||||
v-model:cash-order-price-condition="cashOrderPriceCondition"
|
||||
v-model:cash-order-term="cashOrderTerm"
|
||||
v-model:cash-order-date-input="cashOrderDateInput"
|
||||
v-model:cash-order-method="cashOrderMethod"
|
||||
v-model:cash-order-trigger-zone="cashOrderTriggerZone"
|
||||
v-model:cash-order-trigger-price-input="cashOrderTriggerPriceInput"
|
||||
v-model:cash-order-secondary-price-condition="cashOrderSecondaryPriceCondition"
|
||||
v-model:cash-order-secondary-price-input="cashOrderSecondaryPriceInput"
|
||||
v-model:quantity-input="quantityInput"
|
||||
v-model:price-input="priceInput"
|
||||
v-model:chart-mode="chartMode"
|
||||
v-model:chart-range="chartRange"
|
||||
:viewed-stocks="viewedStocks"
|
||||
:selected-stock="selectedStock"
|
||||
:selected-position="selectedPosition"
|
||||
:connected="connected"
|
||||
:order-quantity="orderQuantity"
|
||||
:estimated-amount="estimatedAmount"
|
||||
:cash-order-account-type-options="cashOrderAccountTypeOptions"
|
||||
:cash-order-market-options="cashOrderMarketOptions"
|
||||
:cash-order-term-options="cashOrderTermOptions"
|
||||
:cash-order-date-options="cashOrderDateOptions"
|
||||
:cash-order-price-step="cashOrderPriceStep"
|
||||
:can-request-cash-estimate="canRequestCashEstimate"
|
||||
:can-place-cash-order="canPlaceCashOrder"
|
||||
:realtime-price-points="chartPricePoints"
|
||||
:chart-notice="chartNotice"
|
||||
:price-polling="pricePolling"
|
||||
:has-quote="hasQuote"
|
||||
@open-search="showSearch = true"
|
||||
@select-stock="
|
||||
(stock) => {
|
||||
selectStock(stock)
|
||||
navigateTrade(stock.symbol || stock.code)
|
||||
}
|
||||
"
|
||||
@download-csv="downloadCsv"
|
||||
@estimate="estimateCashOrder"
|
||||
@confirm-order="askPlaceOrder"
|
||||
/>
|
||||
|
||||
<HistoryView
|
||||
v-if="activeTab === 'history'"
|
||||
:orders="orders"
|
||||
:connected="connected"
|
||||
:data-loading="dataLoading"
|
||||
:canceling-order-key="cancelingOrderKey"
|
||||
:order-history-loaded="orderHistoryLoaded"
|
||||
:order-history-notice="orderHistoryNotice"
|
||||
:load-order-detail="loadOrderDetail"
|
||||
:load-trade-records="loadTradeRecords"
|
||||
:estimate-order-correction="estimateOrderCorrection"
|
||||
:place-order-correction="placeOrderCorrection"
|
||||
@refresh="loadTradingData"
|
||||
@cancel="cancelOrder"
|
||||
/>
|
||||
|
||||
<SettingsView
|
||||
v-if="activeTab === 'settings'"
|
||||
v-model:api-key-label="apiKeyLabel"
|
||||
v-model:new-api-key-settings="newApiKeySettings"
|
||||
v-model:new-api-token="newApiToken"
|
||||
v-model:sbi-label="sbiLabel"
|
||||
v-model:sbi-credential-json="sbiCredentialJson"
|
||||
v-model:trade-password="tradePassword"
|
||||
v-model:sbi-device-id="sbiDeviceId"
|
||||
v-model:selected-passkey-id="selectedPasskeyId"
|
||||
:api-keys="apiKeys"
|
||||
:sbi-passkeys="sbiPasskeys"
|
||||
@add-api-key="addApiKey"
|
||||
@refresh="refresh"
|
||||
@save-api-key-settings="saveApiKeySettings"
|
||||
@revoke-api-key="revokeAndRefresh"
|
||||
@add-sbi-passkey="addSbiPasskeyAndConnect"
|
||||
@connect="connect"
|
||||
@remove-sbi-passkey="removeSbiPasskey"
|
||||
/>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<AnimatePresence>
|
||||
<SearchDialog
|
||||
v-if="showSearch"
|
||||
key="search-dialog"
|
||||
v-model:search-query="searchQuery"
|
||||
v-model:country-filter="countryFilter"
|
||||
v-model:market-filter="marketFilter"
|
||||
:stocks="filteredStocks"
|
||||
:selected-stock-code="selectedStockCode"
|
||||
:countries="countries"
|
||||
:markets="markets"
|
||||
:loading="searchLoading"
|
||||
@close="showSearch = false"
|
||||
@select="
|
||||
(stock) => openTradeForStock(stock, () => void navigateTrade(stock.symbol || stock.code))
|
||||
"
|
||||
/>
|
||||
</AnimatePresence>
|
||||
|
||||
<OrderDialogs
|
||||
:estimate="lastCashEstimate"
|
||||
:show-estimate="showEstimateDialog"
|
||||
:show-order="showOrderDialog"
|
||||
:stock-name="selectedStock.name"
|
||||
:stock-market="selectedStock.market"
|
||||
:side="tradeSide"
|
||||
:kind="orderKind"
|
||||
:account-type="cashOrderAccountType"
|
||||
:market="cashOrderMarket"
|
||||
:price-condition="cashOrderPriceCondition"
|
||||
:price="cashOrderPrimaryRequiresPrice ? orderPrice : 0"
|
||||
:order-term="cashOrderTerm"
|
||||
:order-date="cashOrderDateInput"
|
||||
:order-method="cashOrderMethod"
|
||||
:trigger-zone="cashOrderTriggerZone"
|
||||
:trigger-price="cashOrderTriggerPrice"
|
||||
:secondary-price-condition="cashOrderSecondaryPriceCondition"
|
||||
:secondary-price="cashOrderSecondaryPrice"
|
||||
:quantity="orderQuantity"
|
||||
:amount="estimatedAmount"
|
||||
@close-estimate="showEstimateDialog = false"
|
||||
@proceed="askPlaceOrder"
|
||||
@close-order="showOrderDialog = false"
|
||||
@place="placeCashOrder"
|
||||
/>
|
||||
</main>
|
||||
</template>
|
||||
Reference in New Issue
Block a user