[verified] Add Thai multi-theme panel to dashboard (3 themes + combined summary)
- Theme panel (ธีม): shows tourism/auto_credit/refining_energy with Thai labels, frequency chips (monthly/quarterly), and real factor reads (tourism surprise, car sales YoY, TOP net profit) from /api/v1/themes - Combined count badge; responsive 1-col on mobile - Loads /api/v1/themes in loadDashboard (already wired backend) - Verified: npm build passes, browser shows 3 Thai cards with live values
This commit is contained in:
@@ -5,6 +5,7 @@ const summary = ref(null)
|
|||||||
const observations = ref(null)
|
const observations = ref(null)
|
||||||
const signalData = ref(null)
|
const signalData = ref(null)
|
||||||
const factorData = ref(null)
|
const factorData = ref(null)
|
||||||
|
const themeData = ref(null)
|
||||||
const dividendOnly = ref(false)
|
const dividendOnly = ref(false)
|
||||||
const sortKey = ref('signal_score')
|
const sortKey = ref('signal_score')
|
||||||
const sortDirection = ref('desc')
|
const sortDirection = ref('desc')
|
||||||
@@ -34,6 +35,11 @@ const factorAvailable = computed(() => factorData.value?.available ?? false)
|
|||||||
const dividendCount = computed(() => factorData.value?.dividend_count ?? 0)
|
const dividendCount = computed(() => factorData.value?.dividend_count ?? 0)
|
||||||
const signalScoreCount = computed(() => factorRows.value.filter((f) => f.signal_side === 'LONG' || f.signal_side === 'SHORT').length)
|
const signalScoreCount = computed(() => factorRows.value.filter((f) => f.signal_side === 'LONG' || f.signal_side === 'SHORT').length)
|
||||||
|
|
||||||
|
// Multi-theme combined data from /api/v1/themes.
|
||||||
|
const themes = computed(() => themeData.value?.themes ?? [])
|
||||||
|
const combinedBoard = computed(() => themeData.value?.board ?? [])
|
||||||
|
const combinedCount = computed(() => themeData.value?.combined_count ?? 0)
|
||||||
|
|
||||||
// Rows filtered to dividend-paying names only when the toggle is on.
|
// Rows filtered to dividend-paying names only when the toggle is on.
|
||||||
const filteredFactorRows = computed(() => {
|
const filteredFactorRows = computed(() => {
|
||||||
let rows = factorRows.value
|
let rows = factorRows.value
|
||||||
@@ -187,11 +193,12 @@ async function loadDashboard() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
try {
|
try {
|
||||||
const [summaryBody, observationBody, signalBody, factorBody, ledgerBody, sessionBody, backtestBody, researchBody] = await Promise.all([
|
const [summaryBody, observationBody, signalBody, factorBody, themeBody, ledgerBody, sessionBody, backtestBody, researchBody] = await Promise.all([
|
||||||
fetchJson('/api/v1/dashboard/summary'),
|
fetchJson('/api/v1/dashboard/summary'),
|
||||||
fetchJson('/api/v1/factors/tourism/observations'),
|
fetchJson('/api/v1/factors/tourism/observations'),
|
||||||
fetchJson('/api/v1/signals'),
|
fetchJson('/api/v1/signals'),
|
||||||
fetchJson('/api/v1/factors'),
|
fetchJson('/api/v1/factors'),
|
||||||
|
fetchJson('/api/v1/themes'),
|
||||||
fetchJson('/api/v1/paper/ledger'),
|
fetchJson('/api/v1/paper/ledger'),
|
||||||
fetchJson('/api/v1/auth/paper', { credentials: 'include' }),
|
fetchJson('/api/v1/auth/paper', { credentials: 'include' }),
|
||||||
fetchBacktestReadiness(),
|
fetchBacktestReadiness(),
|
||||||
@@ -201,6 +208,7 @@ async function loadDashboard() {
|
|||||||
observations.value = observationBody
|
observations.value = observationBody
|
||||||
signalData.value = signalBody
|
signalData.value = signalBody
|
||||||
factorData.value = factorBody
|
factorData.value = factorBody
|
||||||
|
themeData.value = themeBody
|
||||||
ledger.value = ledgerBody
|
ledger.value = ledgerBody
|
||||||
paperAuthenticated.value = Boolean(sessionBody.authenticated)
|
paperAuthenticated.value = Boolean(sessionBody.authenticated)
|
||||||
paperAuthMode.value = sessionBody.mode || 'token'
|
paperAuthMode.value = sessionBody.mode || 'token'
|
||||||
@@ -362,6 +370,44 @@ onMounted(loadDashboard)
|
|||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="panel theme-panel" id="themes">
|
||||||
|
<div class="panel-header signal-header">
|
||||||
|
<div>
|
||||||
|
<div class="section-kicker">Alternative factors · ข้อมูลไทย</div>
|
||||||
|
<h2>ธีม (Themes)</h2>
|
||||||
|
<p class="panel-subtitle">ภาพรวม alternative factors ของไทย — แต่ละธีมมีความถี่ข้อมูลต่างกัน (monthly / quarterly) ดังนั้นอย่าเทียบเป็นจุดเวลาเดียวกัน.</p>
|
||||||
|
</div>
|
||||||
|
<span class="status-tag">รวม {{ combinedCount }} symbols</span>
|
||||||
|
</div>
|
||||||
|
<div class="theme-grid">
|
||||||
|
<article v-for="theme in themes" :key="theme.id" class="theme-card">
|
||||||
|
<div class="theme-card-head">
|
||||||
|
<span class="theme-chip">{{ theme.frequency }}</span>
|
||||||
|
<span class="theme-label-en">{{ theme.label_en }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="theme-label-th">{{ theme.label_th }}</div>
|
||||||
|
<div class="theme-source">{{ theme.source }}</div>
|
||||||
|
<div class="theme-read">
|
||||||
|
<template v-if="theme.read && !theme.read.error">
|
||||||
|
<div v-if="theme.id === 'tourism'">
|
||||||
|
<div class="theme-read-value">{{ theme.read.surprise != null ? '+' + formatNumber(theme.read.surprise, 2) + 'σ' : '—' }}</div>
|
||||||
|
<div class="theme-read-meta">theme surprise · as of {{ theme.read.as_of }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="theme.id === 'auto_credit'">
|
||||||
|
<div class="theme-read-value">{{ theme.read.new_car_sales_yoy != null ? (theme.read.new_car_sales_yoy >= 0 ? '+' : '') + formatNumber(theme.read.new_car_sales_yoy) + '%' : '—' }}</div>
|
||||||
|
<div class="theme-read-meta">ยอดขายรถ YoY · {{ theme.read.total_vehicle_sales ? formatNumber(theme.read.total_vehicle_sales, 0) + ' คัน' : '' }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="theme.id === 'refining_energy'">
|
||||||
|
<div class="theme-read-value">{{ theme.read.net_profit != null ? formatNumber(theme.read.net_profit / 1000, 1) + ' พันล้าน' : '—' }}</div>
|
||||||
|
<div class="theme-read-meta">TOP กำไรสุทธิ (ล้านบาท) · {{ theme.read.as_of }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="theme-read-error">{{ theme.read?.error || 'ไม่พบข้อมูล' }}</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="panel stock-panel" id="stocks">
|
<section class="panel stock-panel" id="stocks">
|
||||||
<div class="panel-header signal-header">
|
<div class="panel-header signal-header">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -109,6 +109,21 @@ tbody tr:hover { background: rgba(255,255,255,.025); }
|
|||||||
.side-pill.short { color: var(--red); background: var(--red-soft); }
|
.side-pill.short { color: var(--red); background: var(--red-soft); }
|
||||||
.side-pill.neutral { color: var(--amber); background: var(--amber-soft); }
|
.side-pill.neutral { color: var(--amber); background: var(--amber-soft); }
|
||||||
.reason-code { display: inline-block; margin: 2px 3px 2px 0; padding: 4px 5px; border: 1px solid var(--line-bright); border-radius: 4px; color: var(--faint); font: 9px 'DM Mono', monospace; }
|
.reason-code { display: inline-block; margin: 2px 3px 2px 0; padding: 4px 5px; border: 1px solid var(--line-bright); border-radius: 4px; color: var(--faint); font: 9px 'DM Mono', monospace; }
|
||||||
|
|
||||||
|
/* ---- Theme panel (multi-theme) ---- */
|
||||||
|
.theme-panel { margin-top: 18px; }
|
||||||
|
.theme-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||||
|
.theme-card { border: 1px solid var(--line-bright); border-radius: 10px; padding: 14px; background: var(--card); }
|
||||||
|
.theme-card-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||||
|
.theme-chip { background: var(--accent-soft, rgba(63,161,255,0.12)); color: var(--accent); font: 9px 'DM Mono', monospace; padding: 2px 6px; border-radius: 4px; text-transform: uppercase; }
|
||||||
|
.theme-label-en { font: 600 12px/1.4 var(--font); color: var(--foreground); }
|
||||||
|
.theme-label-th { font-weight: 700; margin-bottom: 2px; }
|
||||||
|
.theme-source { font: 10px 'DM Mono', monospace; color: var(--faint); margin-bottom: 10px; }
|
||||||
|
.theme-read-value { font-size: 22px; font-weight: 700; color: var(--foreground); }
|
||||||
|
.theme-read-meta { font-size: 11px; color: var(--faint); margin-top: 2px; }
|
||||||
|
.theme-read-error { font-size: 11px; color: var(--danger, #e5484d); }
|
||||||
|
|
||||||
|
@media (max-width: 720px) { .theme-grid { grid-template-columns: 1fr; } }
|
||||||
.row-action, .primary-button { padding: 8px 10px; border: 1px solid var(--line-bright); border-radius: 5px; color: var(--text); background: transparent; font-size: 10px; white-space: nowrap; transition: .2s ease; }
|
.row-action, .primary-button { padding: 8px 10px; border: 1px solid var(--line-bright); border-radius: 5px; color: var(--text); background: transparent; font-size: 10px; white-space: nowrap; transition: .2s ease; }
|
||||||
.row-action:hover { color: var(--mint); border-color: var(--mint); }
|
.row-action:hover { color: var(--mint); border-color: var(--mint); }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user