760 lines
18 KiB
Vue
760 lines
18 KiB
Vue
<script setup lang="ts">
|
||
import { ref, onMounted, computed } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import { getEvalRuns, getLeaderboard, type EvalRun, type LeaderboardRow, type LeaderboardTiles } from '@/api/factor'
|
||
import { useFactorCompareStore } from '@/stores/factorCompare'
|
||
|
||
const router = useRouter()
|
||
const compareStore = useFactorCompareStore()
|
||
const runs = ref<EvalRun[]>([])
|
||
const runId = ref<string>('')
|
||
const period = ref<'1' | '5' | '10'>('1')
|
||
const category = ref<string>('')
|
||
const search = ref('')
|
||
const sort = ref('abs_icir')
|
||
const loading = ref(false)
|
||
const tiles = ref<LeaderboardTiles | null>(null)
|
||
const rows = ref<LeaderboardRow[]>([])
|
||
|
||
const CATEGORY_CHIPS = [
|
||
{ key: '', label: '全部' },
|
||
{ key: 'alpha101', label: 'Alpha101' },
|
||
{ key: 'alpha158', label: 'Alpha158' },
|
||
{ key: 'builtin', label: '内置' },
|
||
{ key: 'fundamental', label: '财务基本面' },
|
||
{ key: 'composite', label: '合成因子' },
|
||
]
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
const data = await getLeaderboard({
|
||
...(runId.value ? { run_id: runId.value } : {}),
|
||
period: period.value,
|
||
...(category.value ? { category: category.value } : {}),
|
||
...(search.value ? { search: search.value } : {}),
|
||
sort: sort.value,
|
||
})
|
||
tiles.value = data.tiles
|
||
rows.value = data.rows
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function pct(v: number | null): string {
|
||
return v == null ? '—' : `${(v * 100).toFixed(1)}%`
|
||
}
|
||
|
||
function num(v: number | null, digits = 3): string {
|
||
return v == null ? '—' : v.toFixed(digits)
|
||
}
|
||
|
||
function sparkPath(points: { ic: number }[]): string {
|
||
const pts = points.slice(-24)
|
||
if (!pts.length) return ''
|
||
const w = 88
|
||
const h = 20
|
||
const maxAbs = Math.max(0.02, ...pts.map((p) => Math.abs(p.ic)))
|
||
return pts
|
||
.map((p, i) => {
|
||
const x = (i / Math.max(pts.length - 1, 1)) * w
|
||
const y = h / 2 - (p.ic / maxAbs) * (h / 2 - 2)
|
||
return `${x},${y}`
|
||
})
|
||
.join(' ')
|
||
}
|
||
|
||
const CONCLUSION: Record<string, { label: string; color: string }> = {
|
||
effective: { label: '有效', color: 'var(--lamp-ok)' },
|
||
watch: { label: '观察', color: 'var(--lamp-warn)' },
|
||
eliminated: { label: '淘汰', color: 'var(--lamp-idle)' },
|
||
}
|
||
|
||
const selectedRun = computed(() => runs.value.find((r) => r.run_id === runId.value))
|
||
|
||
function goToDetail(row: LeaderboardRow) {
|
||
if (row.error) return
|
||
router.push(`/factor/leaderboard/${row.factor}?run=${runId.value}`)
|
||
}
|
||
|
||
function toggleCompare(row: LeaderboardRow) {
|
||
const ok = compareStore.toggle(row.factor, runId.value || undefined)
|
||
if (!ok) ElMessage.warning('对比托盘已满(6 个),先移除再添加')
|
||
}
|
||
|
||
function getCategoryClass(cat: string): string {
|
||
const map: Record<string, string> = {
|
||
alpha101: 'cat-alpha101',
|
||
alpha158: 'cat-alpha158',
|
||
builtin: 'cat-builtin',
|
||
fundamental: 'cat-fundamental',
|
||
composite: 'cat-composite',
|
||
}
|
||
return map[cat] || 'cat-builtin'
|
||
}
|
||
|
||
onMounted(async () => {
|
||
runs.value = await getEvalRuns()
|
||
if (runs.value.length) runId.value = runs.value[0].run_id
|
||
await load()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="leaderboard-page">
|
||
<div class="page-head">
|
||
<div>
|
||
<h1 class="page-title">IC 排行榜</h1>
|
||
<p v-if="selectedRun" class="page-sub">
|
||
{{ selectedRun.label }} · {{ selectedRun.universe }} {{ selectedRun.symbols_count.toLocaleString() }} 只 ·
|
||
{{ selectedRun.start }} ~ {{ selectedRun.end }}
|
||
</p>
|
||
</div>
|
||
<router-link to="/factor/batch" class="btn primary">发起新评估</router-link>
|
||
</div>
|
||
|
||
<div v-if="tiles" class="stats">
|
||
<div class="stat">
|
||
<div class="k">参评因子</div>
|
||
<div class="v cy">{{ tiles.factors_total }}</div>
|
||
<div class="d">alpha101 ×100 · alpha158 ×158</div>
|
||
</div>
|
||
<div class="stat ok">
|
||
<div class="k">有效因子</div>
|
||
<div class="v" :style="{ color: 'var(--lamp-ok)' }">{{ tiles.effective }}</div>
|
||
<div class="d">|IC| > 0.02 且 ICIR > 0.3</div>
|
||
</div>
|
||
<div class="stat warn">
|
||
<div class="k">观察中</div>
|
||
<div class="v" :style="{ color: 'var(--amber)' }">{{ tiles.watch }}</div>
|
||
<div class="d">指标边缘 · 待分年验证</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="k">TOP1 多空年化</div>
|
||
<div class="v up">{{ pct(tiles.top_ls_annual) }}</div>
|
||
<div class="d">{{ tiles.top_ls_factor || '—' }} · 十组多空 · 扣费前</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filterbar">
|
||
<el-select v-model="runId" placeholder="选择批次" @change="load" class="run-select">
|
||
<el-option v-for="run in runs" :key="run.run_id" :label="`${run.label} · ${run.start}~${run.end}`" :value="run.run_id" />
|
||
</el-select>
|
||
|
||
<div class="chips">
|
||
<button v-for="chip in CATEGORY_CHIPS" :key="chip.key" class="chip-btn" :class="{ on: category === chip.key }" @click="category = chip.key; load()">
|
||
{{ chip.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<el-input v-model="search" placeholder="搜索因子名…" class="search-input" @keyup.enter="load" />
|
||
|
||
<div class="seg">
|
||
<button :class="{ on: period === '1' }" @click="period = '1'; load()">1D</button>
|
||
<button :class="{ on: period === '5' }" @click="period = '5'; load()">5D</button>
|
||
<button :class="{ on: period === '10' }" @click="period = '10'; load()">10D</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-loading="loading" class="tablewrap">
|
||
<div v-if="!loading && rows.length === 0" class="empty-state">
|
||
<p>暂无评估结果</p>
|
||
<router-link to="/factor/batch">去批量评估发起</router-link>
|
||
</div>
|
||
|
||
<div v-else class="tablescroll">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th class="l">#</th>
|
||
<th class="l">因子</th>
|
||
<th class="l">类别</th>
|
||
<th>IC 均值</th>
|
||
<th>ICIR</th>
|
||
<th>t 值</th>
|
||
<th>胜率</th>
|
||
<th>多空年化</th>
|
||
<th>换手/日</th>
|
||
<th>IC 趋势</th>
|
||
<th>结论</th>
|
||
<th class="l">对比</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="row in rows" :key="row.factor" :class="{ 'top3': row.rank <= 3, 'error-row': row.error }" @click="goToDetail(row)">
|
||
<td class="l rank">{{ row.rank }}</td>
|
||
<td class="l">
|
||
<span class="fname">{{ row.factor }}</span>
|
||
<span class="fexpr">{{ row.expression?.slice(0, 48) }}{{ row.expression?.length > 48 ? '…' : '' }}</span>
|
||
</td>
|
||
<td class="l">
|
||
<span :class="['cat-chip', getCategoryClass(row.category)]">{{ row.category }}</span>
|
||
</td>
|
||
<td>
|
||
<span v-if="row.ic_mean != null" class="icbar">
|
||
<span class="track">
|
||
<span class="fill" :class="row.ic_mean >= 0 ? 'pos' : 'neg'" :style="{ width: `${Math.abs(row.ic_mean) * 500}%` }"></span>
|
||
</span>
|
||
<span :class="row.ic_mean >= 0 ? 'pos' : 'neg'">{{ row.ic_mean.toFixed(3) }}</span>
|
||
</span>
|
||
<span v-else>—</span>
|
||
</td>
|
||
<td :class="row.icir != null ? (row.icir >= 0 ? 'pos' : 'neg') : ''">{{ num(row.icir, 2) }}</td>
|
||
<td :class="row.t_stat != null ? (row.t_stat >= 0 ? 'pos' : 'neg') : ''">{{ num(row.t_stat, 1) }}</td>
|
||
<td :class="row.win_rate != null ? (row.win_rate >= 0 ? 'pos' : 'neg') : ''">{{ pct(row.win_rate) }}</td>
|
||
<td :class="row.ls_annual != null ? (row.ls_annual >= 0 ? 'pos' : 'neg') : ''">{{ pct(row.ls_annual) }}</td>
|
||
<td>{{ pct(row.turnover) }}</td>
|
||
<td>
|
||
<svg v-if="row.monthly_ic?.length" width="88" height="20" class="sparkline">
|
||
<polyline :points="sparkPath(row.monthly_ic)" fill="none" stroke="var(--up)" stroke-width="1.5" />
|
||
</svg>
|
||
<span v-else>—</span>
|
||
</td>
|
||
<td>
|
||
<span v-if="row.error" class="st st-out" :title="row.error">
|
||
<span class="lamp"></span>错误
|
||
</span>
|
||
<span v-else class="st" :class="`st-${row.conclusion}`">
|
||
<span class="lamp"></span>{{ CONCLUSION[row.conclusion]?.label || row.conclusion }}
|
||
</span>
|
||
</td>
|
||
<td class="l">
|
||
<button
|
||
class="addbtn"
|
||
:class="{ on: compareStore.isSelected(row.factor) }"
|
||
:disabled="!!row.error"
|
||
:title="compareStore.isSelected(row.factor) ? '移出对比' : '加入对比'"
|
||
@click.stop="toggleCompare(row)"
|
||
>
|
||
{{ compareStore.isSelected(row.factor) ? '✓ 已选' : '+对比' }}
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div v-if="rows.length" class="tbl-foot">
|
||
<span>显示 {{ rows.length }} / {{ tiles?.factors_total || 0 }} · 按 |IC| 降序</span>
|
||
<span>口径:日频 RankIC · 红正绿负 · 多空=Q10−Q1 等权 · 未做行业中性化</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.leaderboard-page {
|
||
padding: 22px 24px 60px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.page-head {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
margin-bottom: var(--sp-4);
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.04em;
|
||
color: var(--text);
|
||
}
|
||
|
||
.page-sub {
|
||
color: var(--text-2);
|
||
font-size: 12px;
|
||
margin-top: 2px;
|
||
font-family: var(--mono);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.btn {
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
border-radius: var(--r-md);
|
||
padding: 7px 16px;
|
||
border: 1px solid var(--border);
|
||
color: var(--text-2);
|
||
background: var(--bg-card);
|
||
transition: all 0.15s var(--ease);
|
||
cursor: pointer;
|
||
text-decoration: none;
|
||
display: inline-block;
|
||
}
|
||
|
||
.btn:hover {
|
||
color: var(--text);
|
||
border-color: var(--text-3);
|
||
}
|
||
|
||
.btn.primary {
|
||
/* 对齐 el-button--primary 观感(新建模拟盘同款):青软底+荧光青字,
|
||
* 深底亮字对比清楚;青实底配冷白字对比不足(用户 08-30 反馈) */
|
||
color: var(--brand);
|
||
background: var(--cyan-soft);
|
||
border-color: rgba(0, 229, 255, 0.5);
|
||
font-weight: 600;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
|
||
.btn.primary:hover {
|
||
color: var(--brand);
|
||
background: rgba(0, 229, 255, 0.22);
|
||
border-color: rgba(0, 229, 255, 0.65);
|
||
}
|
||
|
||
.stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: var(--sp-3);
|
||
margin-bottom: var(--sp-4);
|
||
}
|
||
|
||
@media (max-width: 1100px) {
|
||
.stats {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
|
||
.stat {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r-lg);
|
||
padding: 13px 16px 11px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.stat::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 2px;
|
||
background: var(--brand);
|
||
opacity: 0.55;
|
||
}
|
||
|
||
.stat.warn::before {
|
||
background: var(--amber);
|
||
}
|
||
|
||
.stat.ok::before {
|
||
background: var(--lamp-ok);
|
||
}
|
||
|
||
.stat .k {
|
||
color: var(--text-3);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.14em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.stat .v {
|
||
font-family: var(--mono);
|
||
font-variant-numeric: tabular-nums;
|
||
font-size: 23px;
|
||
font-weight: 600;
|
||
margin-top: 3px;
|
||
color: var(--text);
|
||
}
|
||
|
||
.stat .v.up {
|
||
color: var(--up);
|
||
}
|
||
|
||
.stat .v.cy {
|
||
color: var(--brand);
|
||
}
|
||
|
||
.stat .d {
|
||
font-size: 11px;
|
||
margin-top: 2px;
|
||
color: var(--text-3);
|
||
font-family: var(--mono);
|
||
}
|
||
|
||
.filterbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r-lg);
|
||
padding: 9px 12px;
|
||
margin-bottom: var(--sp-3);
|
||
}
|
||
|
||
.run-select {
|
||
width: 200px;
|
||
}
|
||
|
||
.run-select :deep(.el-input__wrapper) {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r-sm);
|
||
box-shadow: none;
|
||
}
|
||
|
||
.run-select :deep(.el-input__inner) {
|
||
font-family: var(--mono);
|
||
font-size: 11.5px;
|
||
color: var(--text-2);
|
||
}
|
||
|
||
.chips {
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
|
||
.chip-btn {
|
||
font-family: var(--mono);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: var(--text-2);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r-sm);
|
||
padding: 2px 10px;
|
||
letter-spacing: 0.3px;
|
||
transition: all 0.15s var(--ease);
|
||
background: transparent;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.chip-btn:hover {
|
||
color: var(--text);
|
||
border-color: var(--text-3);
|
||
}
|
||
|
||
.chip-btn.on {
|
||
color: var(--brand);
|
||
background: rgba(0, 229, 255, 0.1);
|
||
border-color: rgba(0, 229, 255, 0.3);
|
||
}
|
||
|
||
.search-input {
|
||
width: 170px;
|
||
}
|
||
|
||
.search-input :deep(.el-input__wrapper) {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r-sm);
|
||
box-shadow: none;
|
||
}
|
||
|
||
.search-input :deep(.el-input__inner) {
|
||
font-family: var(--mono);
|
||
font-size: 11px;
|
||
color: var(--text-3);
|
||
}
|
||
|
||
.seg {
|
||
display: flex;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r-sm);
|
||
overflow: hidden;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.seg button {
|
||
font-family: var(--mono);
|
||
font-size: 11px;
|
||
padding: 4px 12px;
|
||
color: var(--text-2);
|
||
background: transparent;
|
||
border: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.seg button.on {
|
||
background: rgba(0, 229, 255, 0.12);
|
||
color: var(--brand);
|
||
}
|
||
|
||
.tablewrap {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r-lg);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tablescroll {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.empty-state {
|
||
padding: 40px;
|
||
text-align: center;
|
||
color: var(--text-3);
|
||
}
|
||
|
||
.empty-state a {
|
||
color: var(--brand);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.empty-state a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
min-width: 1050px;
|
||
}
|
||
|
||
thead th {
|
||
text-align: right;
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.12em;
|
||
color: var(--text-3);
|
||
text-transform: uppercase;
|
||
background: var(--bg-hover);
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 8px 12px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
thead th.l,
|
||
tbody td.l {
|
||
text-align: left;
|
||
}
|
||
|
||
tbody td {
|
||
text-align: right;
|
||
padding: 7px 12px;
|
||
border-bottom: 1px solid var(--border-2);
|
||
font-family: var(--mono);
|
||
font-variant-numeric: tabular-nums;
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
tbody tr:last-child td {
|
||
border-bottom: none;
|
||
}
|
||
|
||
tbody tr {
|
||
cursor: pointer;
|
||
transition: background 0.12s;
|
||
}
|
||
|
||
tbody tr:hover {
|
||
background: var(--bg-hover);
|
||
}
|
||
|
||
tbody tr.error-row {
|
||
cursor: not-allowed;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
tbody tr.error-row:hover {
|
||
background: transparent;
|
||
}
|
||
|
||
td .fname {
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
|
||
td .fname:hover {
|
||
color: var(--brand);
|
||
}
|
||
|
||
td .fexpr {
|
||
color: var(--text-3);
|
||
font-size: 10.5px;
|
||
display: block;
|
||
margin-top: 0;
|
||
max-width: 250px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.cat-chip {
|
||
display: inline-block;
|
||
padding: 1px 8px;
|
||
border-radius: var(--r-sm);
|
||
font-family: var(--mono);
|
||
font-size: 10.5px;
|
||
font-weight: 600;
|
||
line-height: 17px;
|
||
letter-spacing: 0.3px;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.cat-alpha101 {
|
||
color: var(--brand);
|
||
background: rgba(0, 229, 255, 0.1);
|
||
border-color: rgba(0, 229, 255, 0.3);
|
||
}
|
||
|
||
.cat-alpha158 {
|
||
color: var(--purple-dim);
|
||
background: rgba(180, 140, 255, 0.1);
|
||
border-color: rgba(180, 140, 255, 0.3);
|
||
}
|
||
|
||
.cat-builtin {
|
||
color: var(--amber);
|
||
background: rgba(255, 176, 0, 0.1);
|
||
border-color: rgba(255, 176, 0, 0.3);
|
||
}
|
||
|
||
.cat-fundamental {
|
||
color: var(--teal);
|
||
background: rgba(45, 212, 191, 0.1);
|
||
border-color: rgba(45, 212, 191, 0.3);
|
||
}
|
||
|
||
.cat-composite {
|
||
color: var(--magenta);
|
||
background: rgba(232, 121, 249, 0.1);
|
||
border-color: rgba(232, 121, 249, 0.3);
|
||
}
|
||
|
||
.pos {
|
||
color: var(--up);
|
||
}
|
||
|
||
.neg {
|
||
color: var(--down);
|
||
}
|
||
|
||
.icbar {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.icbar .track {
|
||
width: 64px;
|
||
height: 4px;
|
||
border-radius: 2px;
|
||
background: var(--border);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.icbar .fill {
|
||
position: absolute;
|
||
top: 0;
|
||
bottom: 0;
|
||
}
|
||
|
||
.icbar .fill.pos {
|
||
background: var(--up);
|
||
right: 0;
|
||
box-shadow: 0 0 5px rgba(255, 77, 94, 0.5);
|
||
}
|
||
|
||
.icbar .fill.neg {
|
||
background: var(--down);
|
||
left: 0;
|
||
box-shadow: 0 0 5px rgba(46, 230, 138, 0.5);
|
||
}
|
||
|
||
.st {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
font-family: var(--mono);
|
||
font-size: 10.5px;
|
||
letter-spacing: 0.08em;
|
||
}
|
||
|
||
.st-ok .lamp {
|
||
background: var(--lamp-ok);
|
||
box-shadow: 0 0 6px rgba(46, 230, 138, 0.7);
|
||
}
|
||
|
||
.st-watch .lamp {
|
||
background: var(--lamp-warn);
|
||
box-shadow: 0 0 6px rgba(255, 176, 0, 0.7);
|
||
}
|
||
|
||
.st-out .lamp {
|
||
background: var(--lamp-idle);
|
||
box-shadow: none;
|
||
}
|
||
|
||
.lamp {
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 50%;
|
||
display: inline-block;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.rank {
|
||
color: var(--text-3);
|
||
font-size: 11px;
|
||
}
|
||
|
||
tr.top3 td.rank {
|
||
color: var(--brand);
|
||
font-weight: 700;
|
||
text-shadow: 0 0 10px rgba(0, 229, 255, 0.5);
|
||
}
|
||
|
||
.tbl-foot {
|
||
padding: 9px 14px;
|
||
color: var(--text-3);
|
||
font-family: var(--mono);
|
||
font-size: 10.5px;
|
||
letter-spacing: 0.04em;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
border-top: 1px solid var(--border);
|
||
background: var(--panel);
|
||
}
|
||
|
||
.sparkline {
|
||
display: block;
|
||
}
|
||
|
||
.addbtn {
|
||
font-family: var(--mono);
|
||
font-size: 10.5px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.3px;
|
||
color: var(--text-2);
|
||
background: transparent;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r-sm);
|
||
padding: 2px 9px;
|
||
cursor: pointer;
|
||
transition: all 0.15s var(--ease);
|
||
}
|
||
|
||
.addbtn:hover:not(:disabled) {
|
||
color: var(--brand);
|
||
border-color: rgba(0, 229, 255, 0.4);
|
||
}
|
||
|
||
.addbtn.on {
|
||
color: var(--brand);
|
||
background: rgba(0, 229, 255, 0.1);
|
||
border-color: rgba(0, 229, 255, 0.35);
|
||
}
|
||
|
||
.addbtn:disabled {
|
||
opacity: 0.35;
|
||
cursor: not-allowed;
|
||
}
|
||
</style>
|