|
@@ -0,0 +1,891 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="fundamentals">
|
|
|
|
|
+ <header class="page-hd">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div class="page-title">基本面看板</div>
|
|
|
|
|
+ <div class="page-sub">宏观经济跟踪 · 数据源 Wind / 国家统计局 / 海关总署 </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </header>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- <nav class="subnav">
|
|
|
|
|
+ <div class="subnav-tab">
|
|
|
|
|
+ <a :class="{ on: tab === 'overview' }" @click="tab = 'overview'">总览</a>
|
|
|
|
|
+ <a :class="{ on: tab === 'gdp' }" @click="tab = 'gdp'">GDP</a>
|
|
|
|
|
+ <a :class="{ on: tab === 'inv' }" @click="tab = 'inv'">投资</a>
|
|
|
|
|
+ <a :class="{ on: tab === 'exp' }" @click="tab = 'exp'">出口</a>
|
|
|
|
|
+ <a :class="{ on: tab === 'cons' }" @click="tab = 'cons'">消费</a>
|
|
|
|
|
+ <a :class="{ on: tab === 'cpi' }" @click="tab = 'cpi'">通胀</a>
|
|
|
|
|
+ <a :class="{ on: tab === 'sup' }" @click="tab = 'sup'">供给</a>
|
|
|
|
|
+ <a :class="{ on: tab === 'pmi' }" @click="tab = 'pmi'">景气度</a>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="subnav-time">
|
|
|
|
|
+ <span>时间</span><span class="chip">近 6 年 · 月度</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </nav> -->
|
|
|
|
|
+
|
|
|
|
|
+ <!-- KPI 概览条 -->
|
|
|
|
|
+ <section class="kpi-grid">
|
|
|
|
|
+ <div v-for="k in kpis" :key="k.label" class="kpi-card" :class="{ forecast: k.forecast }">
|
|
|
|
|
+ <div class="label">{{ k.icon }} {{ k.label }}</div>
|
|
|
|
|
+ <div class="value">
|
|
|
|
|
+ {{ formatNum(k.value) }}<span class="unit"> {{ k.unit }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div :class="['chg', chgCls(k.value, k.sign)]">
|
|
|
|
|
+ <span class="arrow">{{ arrow(k.value) }}</span> {{ k.date }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- §1 GDP 与增长 -->
|
|
|
|
|
+ <section class="section">
|
|
|
|
|
+ <div class="section-hd">
|
|
|
|
|
+ <span class="dot"></span>
|
|
|
|
|
+ <h2>§1 GDP 与增长</h2>
|
|
|
|
|
+ <span class="meta">季度数据 · Wind / 国家统计局</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card-grid cols-2-1">
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <div class="card-head">
|
|
|
|
|
+ <div class="title">
|
|
|
|
|
+ <span class="ic">1.1</span>
|
|
|
|
|
+ <span class="title-text">GDP 现价 · 当季同比</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="acts">
|
|
|
|
|
+ <div class="pill-group">
|
|
|
|
|
+ <a :class="{ on: gdpPill === 'yoy' }" @click="gdpPill = 'yoy'">同比</a>
|
|
|
|
|
+ <a :class="{ on: gdpPill === 'qoq' }" @click="gdpPill = 'qoq'">季环比</a>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-body">
|
|
|
|
|
+ <Chart :option="gdpNomOpt" height="280px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card fc-card">
|
|
|
|
|
+ <div class="head">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div class="name">🔮 GDP 模型预测</div>
|
|
|
|
|
+ <div style="font-size:11px;color:var(--mut);margin-top:3px">基于 MM6 周频 + 高频指标</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span class="badge">R²={{ gdpForecast.meta.r2 }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="pred">
|
|
|
|
|
+ <span class="v" style="color:var(--ink)">{{ gdpForecast.meta.nextValue.toFixed(1) }}<span style="font-size:13px;color:var(--mut)">%</span></span>
|
|
|
|
|
+ <span :class="forecastDelta.dirCls"><span class="arrow">{{ forecastDelta.arrow }}</span> {{ forecastDelta.text }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="range">
|
|
|
|
|
+ <span>{{ (gdpForecast.meta.nextValue - gdpForecast.meta.range).toFixed(1) }}%</span>
|
|
|
|
|
+ <span>{{ gdpForecast.meta.nextPeriod }} 预测 {{ gdpForecast.meta.nextValue.toFixed(1) }}% ±{{ gdpForecast.meta.range.toFixed(1) }}</span>
|
|
|
|
|
+ <span>{{ (gdpForecast.meta.nextValue + gdpForecast.meta.range).toFixed(1) }}%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="hrange"><div class="pin" :style="{ left: forecastPinLeft + '%' }"></div></div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="contribs">
|
|
|
|
|
+ <div class="top" style="display:flex;justify-content:space-between;margin-bottom:10px">
|
|
|
|
|
+ <span style="font-size:12px;font-weight:600;color:var(--ink)">分项贡献度</span>
|
|
|
|
|
+ <span style="font-size:11px;color:var(--mut)">最新一期</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-for="c in gdpForecast.meta.contributions" :key="c.name" class="contrib-row">
|
|
|
|
|
+ <div class="top"><span>{{ c.name }}</span><span style="color:var(--brand);font-weight:600">{{ c.value }}%</span></div>
|
|
|
|
|
+ <div class="bar"><div class="fill" :style="{ width: c.value + '%' }"></div></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+<!--
|
|
|
|
|
+ <div class="card-grid cols-1-1" style="margin-top:14px">
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <div class="card-head">
|
|
|
|
|
+ <div class="title">
|
|
|
|
|
+ <span class="ic">1.2</span>
|
|
|
|
|
+ <span class="title-text">GDP 模型预测 · 实际值 / 预测 / 置信区间</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="acts">
|
|
|
|
|
+ <span class="meta" style="font-size:11px">最近更新 2026-09-12</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-body">
|
|
|
|
|
+ <Chart :option="gdpFcOpt" height="300px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div> -->
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- §2 投资分析 -->
|
|
|
|
|
+ <section class="section">
|
|
|
|
|
+ <div class="section-hd">
|
|
|
|
|
+ <span class="dot"></span>
|
|
|
|
|
+ <h2>§2 投资分析</h2>
|
|
|
|
|
+ <span class="meta">累计同比 · 国家统计局</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card-grid cols-1">
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <div class="card-head">
|
|
|
|
|
+ <div class="title">
|
|
|
|
|
+ <span class="ic">2.1</span>
|
|
|
|
|
+ <span class="title-text">固定资产投资分项 · 累计同比</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- <div class="acts">
|
|
|
|
|
+ <div class="pill-group">
|
|
|
|
|
+ <a :class="{ on: invPill === 'all' }" @click="invPill = 'all'">全部</a>
|
|
|
|
|
+ <a :class="{ on: invPill === 'estate' }" @click="invPill = 'estate'">地产</a>
|
|
|
|
|
+ <a :class="{ on: invPill === 'infra' }" @click="invPill = 'infra'">基建</a>
|
|
|
|
|
+ <a :class="{ on: invPill === 'mfg' }" @click="invPill = 'mfg'">制造业</a>
|
|
|
|
|
+ <a :class="{ on: invPill === 'hi' }" @click="invPill = 'hi'">高技术</a>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div> -->
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-body">
|
|
|
|
|
+ <Chart :option="invOpt" height="320px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- §3 进出口贸易 -->
|
|
|
|
|
+ <section class="section">
|
|
|
|
|
+ <div class="section-hd">
|
|
|
|
|
+ <span class="dot"></span>
|
|
|
|
|
+ <h2>§3 进出口贸易</h2>
|
|
|
|
|
+ <span class="meta">月度 · 海关总署</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card-grid cols-1">
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <div class="card-head">
|
|
|
|
|
+ <div class="title">
|
|
|
|
|
+ <span class="ic">3.1</span>
|
|
|
|
|
+ <span class="title-text">出口金额 vs 出口增速 (双轴)</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="acts">
|
|
|
|
|
+ <div class="pill-group">
|
|
|
|
|
+ <a :class="{ on: expPill === 'm' }" @click="expPill = 'm'">当月</a>
|
|
|
|
|
+ <a :class="{ on: expPill === 'acc' }" @click="expPill = 'acc'">累计</a>
|
|
|
|
|
+ <a :class="{ on: expPill === 'usd' }" @click="expPill = 'usd'">美元</a>
|
|
|
|
|
+ <a :class="{ on: expPill === 'cny' }" @click="expPill = 'cny'">人民币</a>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-body">
|
|
|
|
|
+ <Chart :option="expOpt" height="320px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- §4 消费与就业 -->
|
|
|
|
|
+ <section class="section">
|
|
|
|
|
+ <div class="section-hd">
|
|
|
|
|
+ <span class="dot"></span>
|
|
|
|
|
+ <h2>§4 消费与就业</h2>
|
|
|
|
|
+ <span class="meta">月度 · 国家统计局</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card-grid cols-1">
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <div class="card-head">
|
|
|
|
|
+ <div class="title">
|
|
|
|
|
+ <span class="ic">4.1</span>
|
|
|
|
|
+ <span class="title-text">社会消费品零售总额 · 当月同比</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="acts">
|
|
|
|
|
+ <span class="meta" style="font-size:11px">最新 {{ retail.values[retail.values.length - 1] }}%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-body">
|
|
|
|
|
+ <Chart :option="retailOpt" height="280px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- §5 通胀 CPI / PPI -->
|
|
|
|
|
+ <section class="section">
|
|
|
|
|
+ <div class="section-hd">
|
|
|
|
|
+ <span class="dot"></span>
|
|
|
|
|
+ <h2>§5 通胀 · CPI / PPI</h2>
|
|
|
|
|
+ <span class="meta">月度 · 国家统计局</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card-grid cols-2">
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <div class="card-head">
|
|
|
|
|
+ <div class="title">
|
|
|
|
|
+ <span class="ic">5.1</span>
|
|
|
|
|
+ <span class="title-text">CPI 当月同比 / 环比</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="acts">
|
|
|
|
|
+ <span class="meta" style="font-size:11px">最新 {{ cpi.yoy[cpi.yoy.length - 1] }}% / {{ cpi.mom[cpi.mom.length - 1] }}%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-body">
|
|
|
|
|
+ <Chart :option="cpiOpt" height="280px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <div class="card-head">
|
|
|
|
|
+ <div class="title">
|
|
|
|
|
+ <span class="ic">5.2</span>
|
|
|
|
|
+ <span class="title-text">PPI 当月同比 / 环比</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="acts">
|
|
|
|
|
+ <span class="meta" style="font-size:11px">最新 {{ ppi.yoy[ppi.yoy.length - 1] }}% / {{ ppi.mom[ppi.mom.length - 1] }}%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-body">
|
|
|
|
|
+ <Chart :option="ppiOpt" height="280px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- §6 工业与供给 -->
|
|
|
|
|
+ <section class="section">
|
|
|
|
|
+ <div class="section-hd">
|
|
|
|
|
+ <span class="dot"></span>
|
|
|
|
|
+ <h2>§6 工业与供给</h2>
|
|
|
|
|
+ <span class="meta">月度 · 国家统计局</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card-grid cols-1">
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <div class="card-head">
|
|
|
|
|
+ <div class="title">
|
|
|
|
|
+ <span class="ic">6.1</span>
|
|
|
|
|
+ <span class="title-text">工业增加值 · 同比 / 环比</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="acts">
|
|
|
|
|
+ <span class="meta" style="font-size:11px">最新 {{ industrial.yoy[industrial.yoy.length - 1] }}% / {{ industrial.mom[industrial.mom.length - 1] }}%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-body">
|
|
|
|
|
+ <Chart :option="industrialOpt" height="300px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- §7 景气度 -->
|
|
|
|
|
+ <section class="section">
|
|
|
|
|
+ <div class="section-hd">
|
|
|
|
|
+ <span class="dot"></span>
|
|
|
|
|
+ <h2>§7 景气度</h2>
|
|
|
|
|
+ <span class="meta">月度 · 国家统计局</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card-grid cols-2-1">
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <div class="card-head">
|
|
|
|
|
+ <div class="title">
|
|
|
|
|
+ <span class="ic">7.1</span>
|
|
|
|
|
+ <span class="title-text">制造业 PMI</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="acts">
|
|
|
|
|
+ <span class="meta" style="font-size:11px">最新 {{ pmi.values[pmi.values.length - 1] }} · {{ pmiLatestTag }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-body">
|
|
|
|
|
+ <div class="pmi-hero">
|
|
|
|
|
+ <div class="mut">最新读数</div>
|
|
|
|
|
+ <div :class="['pmi-val', pmiLatestCls]">{{ pmi.values[pmi.values.length - 1] }}</div>
|
|
|
|
|
+ <span :class="['tag', pmiLatestTagCls]">{{ pmiLatestTag }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Chart :option="pmiOpt" height="220px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <div class="card-head">
|
|
|
|
|
+ <div class="title">
|
|
|
|
|
+ <span class="ic">7.2</span>
|
|
|
|
|
+ <span class="title-text">景气度结构 · PMI 5 项扩散指数</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-body">
|
|
|
|
|
+ <div class="stat-list">
|
|
|
|
|
+ <div v-for="row in pmiSub" :key="row.name" class="stat-row">
|
|
|
|
|
+ <span class="name">{{ row.name }}</span>
|
|
|
|
|
+ <span class="num">{{ row.value.toFixed(1) }}</span>
|
|
|
|
|
+ <span :class="['delta', row.cls]">{{ row.arrow }} {{ row.delta.toFixed(1) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <footer class="footer">
|
|
|
|
|
+ 数据源:Wind · 国家统计局 · 海关总署 · 更新于 2026-09-21 18:00 · 基本面看板 v2.0
|
|
|
|
|
+ </footer>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { computed, ref } from 'vue'
|
|
|
|
|
+import Chart from '@/components/Chart.vue'
|
|
|
|
|
+import {
|
|
|
|
|
+ gdpNominal, gdpForecast, fixedInvestment, exports, retail, cpi, ppi, industrial, pmi,
|
|
|
|
|
+} from './data'
|
|
|
|
|
+import {
|
|
|
|
|
+ gdpNominalOption, gdpForecastOption, fixedInvestmentOption, exportsDualOption,
|
|
|
|
|
+ retailOption, cpiDualOption, ppiDualOption, industrialDualOption, pmiOption,
|
|
|
|
|
+} from './charts'
|
|
|
|
|
+
|
|
|
|
|
+const tab = ref('overview')
|
|
|
|
|
+const gdpPill = ref('yoy')
|
|
|
|
|
+const invPill = ref('all')
|
|
|
|
|
+const expPill = ref('m')
|
|
|
|
|
+
|
|
|
|
|
+const kpis = computed(() => ([
|
|
|
|
|
+ { label: 'GDP 现价同比', icon: '📈', value: gdpNominal.value, unit: '%', date: gdpNominal.latest, sign: true, forecast: false },
|
|
|
|
|
+ { label: 'CPI 同比', icon: '📊', value: cpi.yoy[cpi.yoy.length - 1], unit: '%', date: cpi.categories[cpi.categories.length - 1], sign: true, forecast: false },
|
|
|
|
|
+ { label: 'PPI 同比', icon: '🏷', value: ppi.yoy[ppi.yoy.length - 1], unit: '%', date: ppi.categories[ppi.categories.length - 1], sign: false, forecast: false },
|
|
|
|
|
+ { label: '固定资产投资', icon: '🏗', value: fixedInvestment.series.total[fixedInvestment.series.total.length - 1], unit: '%', date: fixedInvestment.categories[fixedInvestment.categories.length - 1], sign: false, forecast: false },
|
|
|
|
|
+ { label: '出口金额同比', icon: '📦', value: exports.yoy[exports.yoy.length - 1], unit: '%', date: exports.categories[exports.categories.length - 1], sign: true, forecast: false },
|
|
|
|
|
+ { label: '社零总额同比', icon: '🛒', value: retail.values[retail.values.length - 1], unit: '%', date: retail.categories[retail.categories.length - 1], sign: true, forecast: false },
|
|
|
|
|
+ { label: '制造业 PMI', icon: '📋', value: pmi.values[pmi.values.length - 1], unit: '', date: pmi.categories[pmi.categories.length - 1], sign: true, forecast: false },
|
|
|
|
|
+]))
|
|
|
|
|
+
|
|
|
|
|
+const gdpNomOpt = computed(() => gdpNominalOption(gdpNominal.series))
|
|
|
|
|
+const gdpFcOpt = computed(() => gdpForecastOption(gdpForecast.actual, gdpForecast.forecast, gdpForecast.upper, gdpForecast.lower))
|
|
|
|
|
+
|
|
|
|
|
+const invOpt = computed(() => fixedInvestmentOption(fixedInvestment))
|
|
|
|
|
+const expOpt = computed(() => exportsDualOption(exports))
|
|
|
|
|
+const retailOpt = computed(() => retailOption(retail))
|
|
|
|
|
+const cpiOpt = computed(() => cpiDualOption(cpi))
|
|
|
|
|
+const ppiOpt = computed(() => ppiDualOption(ppi))
|
|
|
|
|
+const industrialOpt = computed(() => industrialDualOption(industrial))
|
|
|
|
|
+const pmiOpt = computed(() => pmiOption(pmi))
|
|
|
|
|
+
|
|
|
|
|
+// KPI helpers
|
|
|
|
|
+function formatNum(v) {
|
|
|
|
|
+ if (typeof v !== 'number') return v
|
|
|
|
|
+ if (Math.abs(v) >= 100) return v.toFixed(0)
|
|
|
|
|
+ return v.toFixed(2)
|
|
|
|
|
+}
|
|
|
|
|
+function chgCls(v, sign = true) {
|
|
|
|
|
+ if (Math.abs(v) < 0.005) return 'flat'
|
|
|
|
|
+ // For non-sign KPIs (e.g., PPI, 固投 期望下行/上升方向按用户视角)
|
|
|
|
|
+ const isUp = sign ? v > 0 : v < 0
|
|
|
|
|
+ return isUp ? 'up' : 'down'
|
|
|
|
|
+}
|
|
|
|
|
+function arrow(v) {
|
|
|
|
|
+ if (Math.abs(v) < 0.005) return '—'
|
|
|
|
|
+ return v > 0 ? '▲' : '▼'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// GDP forecast pin position (0..100)
|
|
|
|
|
+const forecastPinLeft = computed(() => {
|
|
|
|
|
+ const lo = gdpForecast.meta.nextValue - gdpForecast.meta.range
|
|
|
|
|
+ const hi = gdpForecast.meta.nextValue + gdpForecast.meta.range
|
|
|
|
|
+ const min = lo - 0.5
|
|
|
|
|
+ const max = hi + 0.5
|
|
|
|
|
+ const pct = (gdpForecast.meta.nextValue - min) / (max - min)
|
|
|
|
|
+ return Math.max(0, Math.min(100, pct * 100))
|
|
|
|
|
+})
|
|
|
|
|
+const forecastDelta = computed(() => {
|
|
|
|
|
+ // 预测相对上一期实际值的方向
|
|
|
|
|
+ const lastActual = gdpForecast.actual[gdpForecast.actual.length - 1][1]
|
|
|
|
|
+ const next = gdpForecast.meta.nextValue
|
|
|
|
|
+ const delta = next - lastActual
|
|
|
|
|
+ const dir = delta > 0.01 ? 'up' : (delta < -0.01 ? 'down' : 'flat')
|
|
|
|
|
+ const arrow_ = delta > 0.01 ? '▲' : (delta < -0.01 ? '▼' : '—')
|
|
|
|
|
+ return {
|
|
|
|
|
+ dirCls: dir,
|
|
|
|
|
+ arrow: arrow_,
|
|
|
|
|
+ text: `${arrow_} ${Math.abs(delta).toFixed(1)}pct 同比`,
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// PMI 摘要
|
|
|
|
|
+const pmiLatestCls = computed(() => {
|
|
|
|
|
+ const v = pmi.values[pmi.values.length - 1]
|
|
|
|
|
+ return v >= 50 ? 'up' : 'down'
|
|
|
|
|
+})
|
|
|
|
|
+const pmiLatestTagCls = computed(() => pmiLatestCls.value === 'up' ? 'r' : 'd')
|
|
|
|
|
+const pmiLatestTag = computed(() => {
|
|
|
|
|
+ const v = pmi.values[pmi.values.length - 1]
|
|
|
|
|
+ return v >= 50 ? '扩张区间 · 荣枯线上' : '荣枯线下 · 收缩压力'
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// PMI 五项扩散指数(mock)
|
|
|
|
|
+const pmiSub = computed(() => {
|
|
|
|
|
+ const subItems = [
|
|
|
|
|
+ { name: '生产指数', value: 50.6 },
|
|
|
|
|
+ { name: '新订单', value: 49.5 },
|
|
|
|
|
+ { name: '原材料库存', value: 47.8 },
|
|
|
|
|
+ { name: '从业人员', value: 48.4 },
|
|
|
|
|
+ { name: '供应商配送时间', value: 50.2 },
|
|
|
|
|
+ ]
|
|
|
|
|
+ return subItems.map((it, i) => {
|
|
|
|
|
+ const delta = it.value - 50
|
|
|
|
|
+ const cls = delta > 0.01 ? 'up-bg' : (delta < -0.01 ? 'down-bg' : 'flat-bg')
|
|
|
|
|
+ const arrow_ = delta > 0.01 ? '▲' : (delta < -0.01 ? '▼' : '—')
|
|
|
|
|
+ return { ...it, delta, cls, arrow: arrow_ }
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.fundamentals {
|
|
|
|
|
+ padding: 18px 24px 32px;
|
|
|
|
|
+ color: var(--ink, #1A2438);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* === 顶部 === */
|
|
|
|
|
+.page-hd {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-end;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-bottom: 14px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.page-title {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: var(--ink, #1A2438);
|
|
|
|
|
+ margin-bottom: 3px;
|
|
|
|
|
+}
|
|
|
|
|
+.page-sub {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--mut, #6B7A96);
|
|
|
|
|
+}
|
|
|
|
|
+.page-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.btn {
|
|
|
|
|
+ height: 30px;
|
|
|
|
|
+ padding: 0 14px;
|
|
|
|
|
+ border: 1px solid var(--line, #E4E9F2);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ color: var(--ink, #1A2438);
|
|
|
|
|
+ font-size: 12.5px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 5px;
|
|
|
|
|
+ transition: 0.15s;
|
|
|
|
|
+}
|
|
|
|
|
+.btn:hover {
|
|
|
|
|
+ border-color: var(--brand, #1552D0);
|
|
|
|
|
+ color: var(--brand, #1552D0);
|
|
|
|
|
+}
|
|
|
|
|
+.btn.primary {
|
|
|
|
|
+ background: var(--brand, #1552D0);
|
|
|
|
|
+ border-color: var(--brand, #1552D0);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+.btn.primary:hover {
|
|
|
|
|
+ background: var(--brand-d, #0E3A96);
|
|
|
|
|
+ border-color: var(--brand-d, #0E3A96);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* === 子导航 === */
|
|
|
|
|
+.subnav {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 1px solid var(--line, #E4E9F2);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 4px;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ margin-bottom: 14px;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+.subnav-tab {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+}
|
|
|
|
|
+.subnav-tab a {
|
|
|
|
|
+ padding: 6px 12px;
|
|
|
|
|
+ border-radius: 5px;
|
|
|
|
|
+ font-size: 12.5px;
|
|
|
|
|
+ color: var(--mut, #6B7A96);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: 0.15s;
|
|
|
|
|
+}
|
|
|
|
|
+.subnav-tab a:hover {
|
|
|
|
|
+ background: #F0F3F8;
|
|
|
|
|
+ color: var(--ink, #1A2438);
|
|
|
|
|
+}
|
|
|
|
|
+.subnav-tab a.on {
|
|
|
|
|
+ background: var(--brand-lt, #EAF1FF);
|
|
|
|
|
+ color: var(--brand-d, #0E3A96);
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+.subnav-time {
|
|
|
|
|
+ margin-left: auto;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--mut, #6B7A96);
|
|
|
|
|
+ padding: 0 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.subnav-time .chip {
|
|
|
|
|
+ padding: 3px 9px;
|
|
|
|
|
+ background: var(--brand-lt, #EAF1FF);
|
|
|
|
|
+ color: var(--brand-d, #0E3A96);
|
|
|
|
|
+ font-size: 11.5px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* === KPI === */
|
|
|
|
|
+.kpi-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+.kpi-card {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 1px solid var(--line, #E4E9F2);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 13px 14px;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ transition: 0.15s;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.kpi-card:hover {
|
|
|
|
|
+ box-shadow: 0 1px 3px rgba(16, 32, 64, 0.06);
|
|
|
|
|
+ border-color: #D1D8E5;
|
|
|
|
|
+}
|
|
|
|
|
+.kpi-card.forecast {
|
|
|
|
|
+ background: linear-gradient(180deg, #EFF4FC, #FFF);
|
|
|
|
|
+ border-color: #D1D8E5;
|
|
|
|
|
+}
|
|
|
|
|
+.kpi-card .label {
|
|
|
|
|
+ font-size: 11.5px;
|
|
|
|
|
+ color: var(--mut, #6B7A96);
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+.kpi-card .value {
|
|
|
|
|
+ font-size: 21px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: var(--ink, #1A2438);
|
|
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
|
|
+ letter-spacing: -0.5px;
|
|
|
|
|
+ line-height: 1.1;
|
|
|
|
|
+}
|
|
|
|
|
+.kpi-card .value .unit {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--mut, #6B7A96);
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ margin-left: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+.kpi-card .chg {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ margin-top: 5px;
|
|
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 3px;
|
|
|
|
|
+}
|
|
|
|
|
+.kpi-card .chg.up { color: #E13B3B; }
|
|
|
|
|
+.kpi-card .chg.down { color: #189A65; }
|
|
|
|
|
+.kpi-card .chg.flat { color: var(--mut, #6B7A96); }
|
|
|
|
|
+.kpi-card .chg .arrow { font-size: 9px; }
|
|
|
|
|
+
|
|
|
|
|
+/* === 章节 === */
|
|
|
|
|
+.section {
|
|
|
|
|
+ margin-top: 18px;
|
|
|
|
|
+}
|
|
|
|
|
+.section-hd {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.section-hd .dot {
|
|
|
|
|
+ width: 4px;
|
|
|
|
|
+ height: 18px;
|
|
|
|
|
+ background: var(--brand, #1552D0);
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+.section-hd h2 {
|
|
|
|
|
+ font-size: 14.5px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: var(--ink, #1A2438);
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.section-hd .meta {
|
|
|
|
|
+ font-size: 11.5px;
|
|
|
|
|
+ color: #98A5BD;
|
|
|
|
|
+ margin-left: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* === 卡片网格 === */
|
|
|
|
|
+.card-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ gap: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+.card-grid.cols-2-1 { grid-template-columns: 2fr 1fr; }
|
|
|
|
|
+.card-grid.cols-1-1 { grid-template-columns: 1fr 1fr; }
|
|
|
|
|
+.card-grid.cols-1 { grid-template-columns: 1fr; }
|
|
|
|
|
+.card-grid.cols-2 { grid-template-columns: 1fr 1fr; }
|
|
|
|
|
+
|
|
|
|
|
+/* === 卡片 === */
|
|
|
|
|
+.card {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 1px solid var(--line, #E4E9F2);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+.card:hover { box-shadow: 0 1px 3px rgba(16, 32, 64, 0.06); }
|
|
|
|
|
+.card-head {
|
|
|
|
|
+ padding: 11px 16px;
|
|
|
|
|
+ border-bottom: 1px solid var(--line, #E4E9F2);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.card-head .title {
|
|
|
|
|
+ font-size: 13.5px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: var(--ink, #1A2438);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.card-head .title .ic {
|
|
|
|
|
+ display: inline-grid;
|
|
|
|
|
+ place-items: center;
|
|
|
|
|
+ width: 18px;
|
|
|
|
|
+ height: 18px;
|
|
|
|
|
+ background: var(--brand-lt, #EAF1FF);
|
|
|
|
|
+ color: var(--brand, #1552D0);
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+}
|
|
|
|
|
+.card-head .title-text {
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+}
|
|
|
|
|
+.card-head .acts {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+}
|
|
|
|
|
+.card-body {
|
|
|
|
|
+ padding: 14px 16px;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.pill-group {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ background: #F2F5FA;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ padding: 2px;
|
|
|
|
|
+ gap: 1px;
|
|
|
|
|
+}
|
|
|
|
|
+.pill-group a {
|
|
|
|
|
+ padding: 3px 10px;
|
|
|
|
|
+ font-size: 11.5px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ color: var(--mut, #6B7A96);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: 0.15s;
|
|
|
|
|
+}
|
|
|
|
|
+.pill-group a:hover { color: var(--ink, #1A2438); }
|
|
|
|
|
+.pill-group a.on {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ color: var(--brand-d, #0E3A96);
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ box-shadow: 0 1px 2px rgba(16, 32, 64, 0.06);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* === 预测卡 === */
|
|
|
|
|
+.fc-card {
|
|
|
|
|
+ padding: 14px 16px;
|
|
|
|
|
+}
|
|
|
|
|
+.fc-card .head {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+.fc-card .name {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+.fc-card .badge {
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ background: var(--brand-lt, #EAF1FF);
|
|
|
|
|
+ color: var(--brand-d, #0E3A96);
|
|
|
|
|
+ padding: 2px 6px;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+.fc-card .pred {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: baseline;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.fc-card .pred .v {
|
|
|
|
|
+ font-size: 26px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
|
|
+ letter-spacing: -0.5px;
|
|
|
|
|
+}
|
|
|
|
|
+.fc-card .range {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: var(--mut, #6B7A96);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+.fc-card .hrange {
|
|
|
|
|
+ height: 8px;
|
|
|
|
|
+ background: linear-gradient(90deg, #189A65 0%, #E4F5EE 20%, #F2F5FA 50%, #FDEAEA 80%, #E13B3B 100%);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ margin: 6px 0 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.fc-card .hrange .pin {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: -4px;
|
|
|
|
|
+ width: 2px;
|
|
|
|
|
+ height: 16px;
|
|
|
|
|
+ background: var(--nav, #0C2B5E);
|
|
|
|
|
+ border-radius: 1px;
|
|
|
|
|
+ transition: left 0.6s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.contribs {
|
|
|
|
|
+ margin-top: 14px;
|
|
|
|
|
+ padding-top: 14px;
|
|
|
|
|
+ border-top: 1px dashed var(--line, #E4E9F2);
|
|
|
|
|
+}
|
|
|
|
|
+.contrib-row {
|
|
|
|
|
+ margin-bottom: 9px;
|
|
|
|
|
+}
|
|
|
|
|
+.contrib-row .top {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ font-size: 11.5px;
|
|
|
|
|
+ color: var(--mut, #6B7A96);
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+.contrib-row .bar {
|
|
|
|
|
+ height: 5px;
|
|
|
|
|
+ background: #F2F5FA;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+.contrib-row .fill {
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background: linear-gradient(90deg, #1552D0, #3980E0);
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ transition: width 0.8s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* === 颜色工具 === */
|
|
|
|
|
+.up { color: #E13B3B !important; }
|
|
|
|
|
+.down { color: #189A65 !important; }
|
|
|
|
|
+.flat { color: var(--mut, #6B7A96) !important; }
|
|
|
|
|
+
|
|
|
|
|
+/* === PMI hero === */
|
|
|
|
|
+.pmi-hero {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 10px 0 14px;
|
|
|
|
|
+ border-bottom: 1px dashed var(--line, #E4E9F2);
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.pmi-hero .mut {
|
|
|
|
|
+ font-size: 11.5px;
|
|
|
|
|
+ color: var(--mut, #6B7A96);
|
|
|
|
|
+}
|
|
|
|
|
+.pmi-val {
|
|
|
|
|
+ font-size: 32px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
|
|
+ margin: 4px 0 6px;
|
|
|
|
|
+ letter-spacing: -1px;
|
|
|
|
|
+}
|
|
|
|
|
+.pmi-val.up { color: #189A65; }
|
|
|
|
|
+.pmi-val.down { color: #E13B3B; }
|
|
|
|
|
+
|
|
|
|
|
+.tag {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ font-size: 10.5px;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+}
|
|
|
|
|
+.tag.r { background: #FDEAEA; color: #E13B3B; }
|
|
|
|
|
+.tag.d { background: #E4F5EE; color: #189A65; }
|
|
|
|
|
+.tag.b { background: #EAF1FF; color: #0E3A96; }
|
|
|
|
|
+
|
|
|
|
|
+/* === stat-list === */
|
|
|
|
|
+.stat-list { padding: 6px 0; }
|
|
|
|
|
+.stat-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 9px 0;
|
|
|
|
|
+ border-bottom: 1px dashed var(--line, #E4E9F2);
|
|
|
|
|
+ font-size: 12.5px;
|
|
|
|
|
+}
|
|
|
|
|
+.stat-row:last-child { border-bottom: none; }
|
|
|
|
|
+.stat-row .name { flex: 1; color: var(--mut, #6B7A96); }
|
|
|
|
|
+.stat-row .num {
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
|
|
+ color: var(--ink, #1A2438);
|
|
|
|
|
+ margin-right: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.stat-row .delta {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ padding: 1px 6px;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
|
|
+}
|
|
|
|
|
+.up-bg { background: #FDEAEA; color: #E13B3B; }
|
|
|
|
|
+.down-bg { background: #E4F5EE; color: #189A65; }
|
|
|
|
|
+.flat-bg { background: #EEF2F7; color: var(--mut, #6B7A96); }
|
|
|
|
|
+
|
|
|
|
|
+.footer {
|
|
|
|
|
+ padding: 18px 24px 12px;
|
|
|
|
|
+ color: #98A5BD;
|
|
|
|
|
+ font-size: 11.5px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ border-top: 1px solid var(--line, #E4E9F2);
|
|
|
|
|
+ background: #FAFBFE;
|
|
|
|
|
+ margin-top: 14px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* === 响应式 === */
|
|
|
|
|
+@media (max-width: 1280px) {
|
|
|
|
|
+ .kpi-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
|
|
|
+}
|
|
|
|
|
+@media (max-width: 980px) {
|
|
|
|
|
+ .kpi-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
|
|
|
+ .card-grid.cols-2-1,
|
|
|
|
|
+ .card-grid.cols-1-2,
|
|
|
|
|
+ .card-grid.cols-1-1,
|
|
|
|
|
+ .card-grid.cols-2 { grid-template-columns: 1fr; }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|