[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:
Kunthawat Greethong
2026-08-25 15:59:02 +07:00
parent 7643764679
commit 90e63e3384
2 changed files with 62 additions and 1 deletions

View File

@@ -5,6 +5,7 @@ const summary = ref(null)
const observations = ref(null)
const signalData = ref(null)
const factorData = ref(null)
const themeData = ref(null)
const dividendOnly = ref(false)
const sortKey = ref('signal_score')
const sortDirection = ref('desc')
@@ -34,6 +35,11 @@ const factorAvailable = computed(() => factorData.value?.available ?? false)
const dividendCount = computed(() => factorData.value?.dividend_count ?? 0)
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.
const filteredFactorRows = computed(() => {
let rows = factorRows.value
@@ -187,11 +193,12 @@ async function loadDashboard() {
loading.value = true
error.value = ''
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/factors/tourism/observations'),
fetchJson('/api/v1/signals'),
fetchJson('/api/v1/factors'),
fetchJson('/api/v1/themes'),
fetchJson('/api/v1/paper/ledger'),
fetchJson('/api/v1/auth/paper', { credentials: 'include' }),
fetchBacktestReadiness(),
@@ -201,6 +208,7 @@ async function loadDashboard() {
observations.value = observationBody
signalData.value = signalBody
factorData.value = factorBody
themeData.value = themeBody
ledger.value = ledgerBody
paperAuthenticated.value = Boolean(sessionBody.authenticated)
paperAuthMode.value = sessionBody.mode || 'token'
@@ -362,6 +370,44 @@ onMounted(loadDashboard)
</article>
</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">
<div class="panel-header signal-header">
<div>

View File

@@ -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.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; }
/* ---- 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:hover { color: var(--mint); border-color: var(--mint); }