Spaces:
Running
Running
Jimin Huang
commited on
Commit
·
9d1fe98
1
Parent(s):
5cec81c
Change settings
Browse files- src/components/AssetTabs.vue +32 -41
- src/lib/prices.ts +0 -87
src/components/AssetTabs.vue
CHANGED
|
@@ -1,62 +1,53 @@
|
|
| 1 |
-
<!-- src/components/AssetTabsPrice.vue -->
|
| 2 |
<template>
|
| 3 |
-
<div class="
|
| 4 |
-
<button
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
<
|
| 12 |
</button>
|
| 13 |
</div>
|
| 14 |
</template>
|
| 15 |
|
| 16 |
-
<script setup
|
| 17 |
-
import {
|
| 18 |
-
import { dataService } from '
|
| 19 |
-
import { pollPrices, subscribePrices, fmtUSD } from '../lib/prices'
|
| 20 |
|
| 21 |
-
const props = defineProps({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
const emit = defineEmits(['update:modelValue'])
|
| 23 |
|
| 24 |
-
|
| 25 |
-
const
|
|
|
|
|
|
|
| 26 |
|
| 27 |
const available = computed(() => {
|
| 28 |
-
const rows = Array.isArray(
|
| 29 |
-
return Array.from(new Set(rows.map(
|
| 30 |
})
|
| 31 |
-
|
| 32 |
const orderedAssets = computed(() => {
|
| 33 |
const present = new Set(available.value)
|
| 34 |
-
const primary =
|
| 35 |
-
const extras = [...present].filter(a => !
|
| 36 |
const list = [...primary, ...extras]
|
| 37 |
-
if (
|
| 38 |
return list
|
| 39 |
})
|
| 40 |
-
|
| 41 |
-
const prices = ref<Record<string, number>>({})
|
| 42 |
-
let unsub: null | (()=>void) = null
|
| 43 |
-
onMounted(async () => {
|
| 44 |
-
unsub = subscribePrices(m => (prices.value = m))
|
| 45 |
-
await pollPrices(orderedAssets.value) // starts polling; cheap even if called again
|
| 46 |
-
})
|
| 47 |
-
onBeforeUnmount(()=>unsub?.())
|
| 48 |
-
watch(orderedAssets, (arr)=>pollPrices(arr))
|
| 49 |
</script>
|
| 50 |
|
| 51 |
<style scoped>
|
| 52 |
-
.
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
padding:8px 12px; min-width:110px; display:flex; flex-direction:column; align-items:center; gap:2px
|
| 56 |
}
|
| 57 |
-
.tab--active{ background:#111827; color:#fff; border-color:#111827 }
|
| 58 |
-
.row{ display:flex; align-items:center; gap:6px; line-height:1 }
|
| 59 |
-
.icon{ width:18px; height:18px; flex:0 0 18px; object-fit:contain }
|
| 60 |
-
.code{ font-weight:700; letter-spacing:.02em }
|
| 61 |
-
.price{ font-variant-numeric:tabular-nums; font-weight:800; line-height:1.05; margin-top:2px }
|
| 62 |
</style>
|
|
|
|
|
|
|
| 1 |
<template>
|
| 2 |
+
<div class="flex gap-2 flex-wrap">
|
| 3 |
+
<button
|
| 4 |
+
v-for="code in orderedAssets" :key="code"
|
| 5 |
+
class="px-3 py-1 rounded-full border flex items-center gap-2"
|
| 6 |
+
:class="code===modelValue ? 'bg-black text-white' : 'bg-white'"
|
| 7 |
+
@click="$emit('update:modelValue', code)"
|
| 8 |
+
>
|
| 9 |
+
<img :src="iconFor(code)" alt="" class="asset-icon" decoding="async" loading="lazy" @error="hide($event)" />
|
| 10 |
+
<span>{{ code }}</span>
|
| 11 |
</button>
|
| 12 |
</div>
|
| 13 |
</template>
|
| 14 |
|
| 15 |
+
<script setup>
|
| 16 |
+
import { computed } from 'vue'
|
| 17 |
+
import { dataService } from '@/lib/dataService'
|
|
|
|
| 18 |
|
| 19 |
+
const props = defineProps({
|
| 20 |
+
modelValue: { type: String, default: '' },
|
| 21 |
+
preferredOrder: {
|
| 22 |
+
type: Array,
|
| 23 |
+
default: () => ['BTC','ETH','SOL','BNB','DOGE','XRP','AAPL','MSFT','BMRN','MRNA','TSLA']
|
| 24 |
+
}
|
| 25 |
+
})
|
| 26 |
const emit = defineEmits(['update:modelValue'])
|
| 27 |
|
| 28 |
+
// same pattern as your AssetsFilter.vue
|
| 29 |
+
const iconFor = (assetCode) =>
|
| 30 |
+
new URL(`../assets/images/assets_images/${assetCode}.png`, import.meta.url).href
|
| 31 |
+
const hide = (e) => { e.target.style.display = 'none' }
|
| 32 |
|
| 33 |
const available = computed(() => {
|
| 34 |
+
const rows = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
|
| 35 |
+
return Array.from(new Set(rows.map(r => r.asset)))
|
| 36 |
})
|
| 37 |
+
|
| 38 |
const orderedAssets = computed(() => {
|
| 39 |
const present = new Set(available.value)
|
| 40 |
+
const primary = props.preferredOrder.filter(a => present.has(a))
|
| 41 |
+
const extras = [...present].filter(a => !props.preferredOrder.includes(a)).sort()
|
| 42 |
const list = [...primary, ...extras]
|
| 43 |
+
if (!list.includes(props.modelValue) && list.length) emit('update:modelValue', list[0])
|
| 44 |
return list
|
| 45 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
</script>
|
| 47 |
|
| 48 |
<style scoped>
|
| 49 |
+
.asset-icon{
|
| 50 |
+
width: 18px; height: 18px; flex: 0 0 18px;
|
| 51 |
+
object-fit: contain; display: inline-block; vertical-align: middle;
|
|
|
|
| 52 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
</style>
|
src/lib/prices.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
| 1 |
-
// src/lib/prices.ts
|
| 2 |
-
type PriceMap = Record<string, number>
|
| 3 |
-
|
| 4 |
-
// Map asset code -> provider + symbol
|
| 5 |
-
const MAP: Record<string, { p: 'binance'|'yahoo'; s: string }> = {
|
| 6 |
-
// crypto (USDT pairs)
|
| 7 |
-
BTC: { p: 'binance', s: 'BTCUSDT' },
|
| 8 |
-
ETH: { p: 'binance', s: 'ETHUSDT' },
|
| 9 |
-
SOL: { p: 'binance', s: 'SOLUSDT' },
|
| 10 |
-
BNB: { p: 'binance', s: 'BNBUSDT' },
|
| 11 |
-
DOGE:{ p: 'binance', s: 'DOGEUSDT' },
|
| 12 |
-
XRP: { p: 'binance', s: 'XRPUSDT' },
|
| 13 |
-
|
| 14 |
-
// equities (Yahoo uses plain tickers)
|
| 15 |
-
AAPL:{ p: 'yahoo', s: 'AAPL' },
|
| 16 |
-
MSFT:{ p: 'yahoo', s: 'MSFT' },
|
| 17 |
-
BMRN:{ p: 'yahoo', s: 'BMRN' },
|
| 18 |
-
MRNA:{ p: 'yahoo', s: 'MRNA' },
|
| 19 |
-
TSLA:{ p: 'yahoo', s: 'TSLA' },
|
| 20 |
-
};
|
| 21 |
-
|
| 22 |
-
let latest: PriceMap = {};
|
| 23 |
-
const subs = new Set<(m: PriceMap)=>void>();
|
| 24 |
-
const push = () => subs.forEach(fn => fn({ ...latest }));
|
| 25 |
-
|
| 26 |
-
export function subscribePrices(fn: (m: PriceMap)=>void) {
|
| 27 |
-
subs.add(fn); fn({ ...latest });
|
| 28 |
-
return () => subs.delete(fn);
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
async function fetchBinance(symbol: string) {
|
| 32 |
-
const r = await fetch(`https://api.binance.com/api/v3/ticker/price?symbol=${encodeURIComponent(symbol)}`);
|
| 33 |
-
if (!r.ok) throw new Error(`binance ${symbol} ${r.status}`);
|
| 34 |
-
const j = await r.json(); const n = Number(j?.price);
|
| 35 |
-
return Number.isFinite(n) ? n : undefined;
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
async function fetchYahoo(symbolsCsv: string) {
|
| 39 |
-
const url = `https://query1.finance.yahoo.com/v7/finance/quote?symbols=${encodeURIComponent(symbolsCsv)}`
|
| 40 |
-
const r = await fetch(url);
|
| 41 |
-
if (!r.ok) throw new Error(`yahoo ${symbolsCsv} ${r.status}`);
|
| 42 |
-
const j = await r.json();
|
| 43 |
-
const out: Record<string, number> = {};
|
| 44 |
-
for (const it of (j?.quoteResponse?.result ?? [])) {
|
| 45 |
-
const sym = it?.symbol;
|
| 46 |
-
const px = Number(it?.regularMarketPrice ?? it?.postMarketPrice ?? it?.preMarketPrice);
|
| 47 |
-
if (sym && Number.isFinite(px)) out[sym] = px;
|
| 48 |
-
}
|
| 49 |
-
return out;
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
export async function pollPrices(codes: string[], intervalMs = 30000) {
|
| 53 |
-
const uniq = [...new Set(codes)].filter(c => MAP[c]);
|
| 54 |
-
|
| 55 |
-
// split by provider for efficient requests
|
| 56 |
-
const binanceSyms = uniq.filter(c => MAP[c].p === 'binance').map(c => MAP[c].s);
|
| 57 |
-
const yahooSyms = uniq.filter(c => MAP[c].p === 'yahoo').map(c => MAP[c].s);
|
| 58 |
-
|
| 59 |
-
async function tick() {
|
| 60 |
-
try {
|
| 61 |
-
// Binance: per-symbol (cheap for a handful)
|
| 62 |
-
await Promise.all(binanceSyms.map(async s => {
|
| 63 |
-
const px = await fetchBinance(s);
|
| 64 |
-
const code = Object.entries(MAP).find(([k,v]) => v.s === s)?.[0];
|
| 65 |
-
if (code && px !== undefined) latest[code] = px;
|
| 66 |
-
}));
|
| 67 |
-
|
| 68 |
-
// Yahoo: batch in one call
|
| 69 |
-
if (yahooSyms.length) {
|
| 70 |
-
const batch = await fetchYahoo(yahooSyms.join(','));
|
| 71 |
-
for (const [sym, px] of Object.entries(batch)) {
|
| 72 |
-
const code = Object.entries(MAP).find(([k,v]) => v.s === sym)?.[0];
|
| 73 |
-
if (code && px !== undefined) latest[code] = px;
|
| 74 |
-
}
|
| 75 |
-
}
|
| 76 |
-
} catch { /* ignore transient errors */ }
|
| 77 |
-
push();
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
await tick();
|
| 81 |
-
setInterval(tick, intervalMs);
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
export const fmtUSD = (n?: number) =>
|
| 85 |
-
typeof n === 'number'
|
| 86 |
-
? n.toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
|
| 87 |
-
: '—';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|