|
|
@@ -0,0 +1,817 @@
|
|
|
+<template>
|
|
|
+ <div class="gdp-page">
|
|
|
+ <header class="page-hd">
|
|
|
+ <div>
|
|
|
+ <div class="page-title">GDP 看板</div>
|
|
|
+ <div class="page-sub">基本面 · GDP · 现价 / 不变价 / 周度预测 / 平减指数 · 数据源 Wind · 国家统计局 · 自研 MM6</div>
|
|
|
+ </div>
|
|
|
+ <div class="page-actions">
|
|
|
+ <button class="btn" @click="reload" :disabled="loading">
|
|
|
+ <span :class="['ic-refresh', { spinning: loading }]">↻</span> 刷新
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <!-- KPI 概览条 -->
|
|
|
+ <section class="kpi-grid">
|
|
|
+ <div class="kpi-card" :class="{ forecast: k.forecast }" v-for="k in kpis" :key="k.label">
|
|
|
+ <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 }} · {{ k.tag }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- §1.1 现价 GDP 增速 -->
|
|
|
+ <section class="section">
|
|
|
+ <div class="section-hd">
|
|
|
+ <span class="dot"></span>
|
|
|
+ <h2>§1.1 现价 GDP 增速 · 当季同比</h2>
|
|
|
+ <span class="meta">季度 · Wind</span>
|
|
|
+ <span class="meta-spacer"></span>
|
|
|
+ <span class="meta">最新 {{ formatPct(gdpNominalLatest) }} · {{ formatDate(gdpNominalMeta?.data_end) }}</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: nomPill === 'yoy' }" @click="nomPill = 'yoy'">同比</a>
|
|
|
+ <a :class="{ on: nomPill === 'trend' }" @click="nomPill = 'trend'">趋势线</a>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <Chart :option="gdpNominalOpt" height="300px" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-head">
|
|
|
+ <div class="title">
|
|
|
+ <span class="ic">说</span>
|
|
|
+ <span class="title-text">指标解读</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <div class="kv-list">
|
|
|
+ <div class="kv"><span class="k">指标编码</span><span class="v">{{ gdpNominalMeta?.wind_code || '—' }}</span></div>
|
|
|
+ <div class="kv"><span class="k">数据源</span><span class="v">{{ gdpNominalMeta?.source || 'Wind' }}</span></div>
|
|
|
+ <div class="kv"><span class="k">频率</span><span class="v">{{ gdpNominalMeta?.frequency || '季' }}</span></div>
|
|
|
+ <div class="kv"><span class="k">起始日期</span><span class="v">{{ gdpNominalMeta?.data_start || '—' }}</span></div>
|
|
|
+ <div class="kv"><span class="k">最近观测</span><span class="v">{{ gdpNominalMeta?.data_end || '—' }}</span></div>
|
|
|
+ <div class="kv"><span class="k">最新读数</span><span class="v strong">{{ formatPct(gdpNominalLatest) }}</span></div>
|
|
|
+ </div>
|
|
|
+ <div class="note">
|
|
|
+ 现价 GDP 同比反映名义增长(含价格因素)。近 4 季度呈温和回升态势,{{ gdpNominalComment }}。
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- §1.2 不变价 GDP 增速 -->
|
|
|
+ <section class="section">
|
|
|
+ <div class="section-hd">
|
|
|
+ <span class="dot"></span>
|
|
|
+ <h2>§1.2 不变价 GDP 增速 · 当季同比</h2>
|
|
|
+ <span class="meta">季度 · 国家统计局</span>
|
|
|
+ <span class="meta-spacer"></span>
|
|
|
+ <span class="meta">最新 {{ formatPct(gdpRealLatest) }} · {{ formatDate(gdpRealMeta?.data_end) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="card-grid cols-2-1">
|
|
|
+ <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 tag-r" v-if="gdpRealMeta?.data_end">观察期 {{ formatDate(gdpRealMeta.data_end) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <Chart :option="gdpRealOpt" height="300px" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-head">
|
|
|
+ <div class="title">
|
|
|
+ <span class="ic">对</span>
|
|
|
+ <span class="title-text">现价 vs 不变价 · 价 vs 量</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <Chart :option="gdpNominalVsRealOpt" height="220px" />
|
|
|
+ <div class="legend-row">
|
|
|
+ <span class="dot-c brand"></span><span>现价(量×价)</span>
|
|
|
+ <span class="dot-c warn"></span><span>不变价(实际量)</span>
|
|
|
+ <span class="dot-c faint"></span><span>GDP 平减指数 = 现价 − 不变价</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- §1.3 GDP 周度预测 -->
|
|
|
+ <section class="section">
|
|
|
+ <div class="section-hd">
|
|
|
+ <span class="dot"></span>
|
|
|
+ <h2>§1.3 GDP 周度预测 (MM6)</h2>
|
|
|
+ <span class="meta">周频 · 自研模型 · MM6 · R² ≈ 0.91</span>
|
|
|
+ <span class="meta-spacer"></span>
|
|
|
+ <span class="meta">最近预测 {{ formatPct(mm6ForecastLatest) }} · {{ formatDate(mm6ForecastMeta?.data_end) }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="card-grid cols-2-1">
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-head">
|
|
|
+ <div class="title">
|
|
|
+ <span class="ic">1.3</span>
|
|
|
+ <span class="title-text">实际 vs 预测 · 周频</span>
|
|
|
+ </div>
|
|
|
+ <div class="acts">
|
|
|
+ <span class="meta">最近更新 {{ formatDate(mm6ForecastMeta?.data_end) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <Chart :option="mm6WeeklyOpt" height="300px" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="card fc-card">
|
|
|
+ <div class="head">
|
|
|
+ <div>
|
|
|
+ <div class="name">🔮 MM6 下期预测</div>
|
|
|
+ <div style="font-size:11px;color:var(--mut);margin-top:3px">基于 22 个高频指标</div>
|
|
|
+ </div>
|
|
|
+ <span class="badge" v-if="mm6R2">R²={{ mm6R2 }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="pred">
|
|
|
+ <span class="v" style="color:var(--ink)">{{ mm6ForecastLatest?.value?.toFixed?.(2) || '—' }}<span style="font-size:13px;color:var(--mut)">%</span></span>
|
|
|
+ <span :class="mm6Delta.dirCls"><span class="arrow">{{ mm6Delta.arrow }}</span> {{ mm6Delta.text }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="range">
|
|
|
+ <span>{{ mm6RangeLow?.toFixed?.(2) }}%</span>
|
|
|
+ <span>{{ formatDate(mm6ForecastLatest?.date) }} 预测 ± {{ mm6Range?.toFixed?.(2) }}pct</span>
|
|
|
+ <span>{{ mm6RangeHigh?.toFixed?.(2) }}%</span>
|
|
|
+ </div>
|
|
|
+ <div class="hrange"><div class="pin" :style="{ left: mm6PinLeft + '%' }"></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)">输入高频指标 TOP</span>
|
|
|
+ <span style="font-size:11px;color:var(--mut)">最近一期权重</span>
|
|
|
+ </div>
|
|
|
+ <div v-for="c in mm6Contribs" :key="c.name" class="contrib-row">
|
|
|
+ <div class="top"><span>{{ c.name }}</span><span style="color:var(--brand);font-weight:600">{{ c.weight }}</span></div>
|
|
|
+ <div class="bar"><div class="fill" :style="{ width: c.pct + '%' }"></div></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="card-grid cols-1" style="margin-top:14px">
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-head">
|
|
|
+ <div class="title">
|
|
|
+ <span class="ic">1.3b</span>
|
|
|
+ <span class="title-text">实际值 / 预测 / 95% 置信区间</span>
|
|
|
+ </div>
|
|
|
+ <div class="acts">
|
|
|
+ <span class="meta">最近更新 {{ formatDate(mm6ForecastMeta?.data_end) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <Chart :option="mm6ForecastOpt" height="320px" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- §1.4 GDP 平减指数 -->
|
|
|
+ <section class="section">
|
|
|
+ <div class="section-hd">
|
|
|
+ <span class="dot"></span>
|
|
|
+ <h2>§1.4 GDP 平减指数 · 当季同比</h2>
|
|
|
+ <span class="meta">季度 · Wind</span>
|
|
|
+ <span class="meta-spacer"></span>
|
|
|
+ <span class="meta">最新 {{ formatPct(gdpDeflatorLatest) }} · {{ formatDate(gdpDeflatorMeta?.data_end) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="card-grid cols-2-1">
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-head">
|
|
|
+ <div class="title">
|
|
|
+ <span class="ic">1.4</span>
|
|
|
+ <span class="title-text">GDP 平减指数 · 同比</span>
|
|
|
+ </div>
|
|
|
+ <div class="acts">
|
|
|
+ <span class="meta tag-r" v-if="gdpDeflatorLatest">
|
|
|
+ {{ gdpDeflatorLatest.value >= 0 ? '通胀贡献为正' : '通缩压力' }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <Chart :option="gdpDeflatorOpt" height="280px" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-head">
|
|
|
+ <div class="title">
|
|
|
+ <span class="ic">说</span>
|
|
|
+ <span class="title-text">指标解读</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <div class="kv-list">
|
|
|
+ <div class="kv"><span class="k">指标编码</span><span class="v">{{ gdpDeflatorMeta?.wind_code || '—' }}</span></div>
|
|
|
+ <div class="kv"><span class="k">计算</span><span class="v">现价 − 不变价</span></div>
|
|
|
+ <div class="kv"><span class="k">最新读数</span><span class="v strong">{{ formatPct(gdpDeflatorLatest) }}</span></div>
|
|
|
+ <div class="kv"><span class="k">方向</span>
|
|
|
+ <span :class="['v', gdpDeflatorLatest?.value >= 0 ? 'up' : 'down']">
|
|
|
+ {{ gdpDeflatorLatest?.value >= 0 ? '↑ 价格因素贡献为正' : '↓ 价格因素贡献为负' }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div class="kv"><span class="k">起始日期</span><span class="v">{{ gdpDeflatorMeta?.data_start || '—' }}</span></div>
|
|
|
+ <div class="kv"><span class="k">最近观测</span><span class="v">{{ gdpDeflatorMeta?.data_end || '—' }}</span></div>
|
|
|
+ </div>
|
|
|
+ <div class="note">
|
|
|
+ GDP 平减指数 = 现价增速 − 不变价增速,反映经济体整体价格水平变化。该值由 {{ gdpDeflatorMeta?.source || 'Wind' }} 提供。
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <footer class="footer">
|
|
|
+ 数据源:Wind · 国家统计局 · 自研 MM6 模型 · {{ updatedAt }} · GDP 看板 v1.0
|
|
|
+ </footer>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { computed, onMounted, ref as vRef } from 'vue'
|
|
|
+import Chart from '@/components/Chart.vue'
|
|
|
+import {
|
|
|
+ loadGdpNominal, loadGdpReal, loadGdpDeflator,
|
|
|
+ loadMm6Actual, loadMm6Forecast, loadLatestAll,
|
|
|
+} from './data'
|
|
|
+import {
|
|
|
+ gdpQuarterOption, gdpDeflatorOption, gdpDeflatorQuarterOption,
|
|
|
+ mm6ForecastOption, mm6WeeklyOption,
|
|
|
+} from './charts'
|
|
|
+
|
|
|
+const loading = vRef(false)
|
|
|
+const updatedAt = vRef('')
|
|
|
+
|
|
|
+const gdpNominalSeries = vRef([])
|
|
|
+const gdpRealSeries = vRef([])
|
|
|
+const gdpDeflatorSeries = vRef([])
|
|
|
+const mm6ActualSeries = vRef([])
|
|
|
+const mm6Forecast = vRef({ series: [], upper: [], lower: [] })
|
|
|
+
|
|
|
+const gdpNominalMeta = vRef(null)
|
|
|
+const gdpRealMeta = vRef(null)
|
|
|
+const gdpDeflatorMeta = vRef(null)
|
|
|
+const mm6ActualMeta = vRef(null)
|
|
|
+const mm6ForecastMeta = vRef(null)
|
|
|
+
|
|
|
+const gdpNominalLatest = vRef(null)
|
|
|
+const gdpRealLatest = vRef(null)
|
|
|
+const gdpDeflatorLatest = vRef(null)
|
|
|
+const mm6ActualLatest = vRef(null)
|
|
|
+
|
|
|
+const nomPill = vRef('yoy')
|
|
|
+
|
|
|
+const mm6R2 = vRef(0.91)
|
|
|
+const mm6Contribs = [
|
|
|
+ { name: '工业增加值 (当月同比)', weight: '高', pct: 88 },
|
|
|
+ { name: '社会消费品零售总额', weight: '中高', pct: 72 },
|
|
|
+ { name: 'PMI 制造业', weight: '中', pct: 60 },
|
|
|
+ { name: '固定资产投资完成额', weight: '中', pct: 56 },
|
|
|
+ { name: '进出口金额 (美元口径)', weight: '中低', pct: 42 },
|
|
|
+]
|
|
|
+
|
|
|
+function formatNum(v) {
|
|
|
+ if (typeof v !== 'number') return v ?? '—'
|
|
|
+ if (Math.abs(v) >= 100) return v.toFixed(0)
|
|
|
+ return v.toFixed(2)
|
|
|
+}
|
|
|
+function formatPct(latest) {
|
|
|
+ if (!latest || typeof latest.value !== 'number') return '—'
|
|
|
+ return `${latest.value.toFixed(2)}%`
|
|
|
+}
|
|
|
+function formatDate(d) {
|
|
|
+ if (!d) return '—'
|
|
|
+ return String(d).slice(0, 10)
|
|
|
+}
|
|
|
+function chgCls(v, sign = true) {
|
|
|
+ if (typeof v !== 'number' || Math.abs(v) < 0.005) return 'flat'
|
|
|
+ const isUp = sign ? v > 0 : v < 0
|
|
|
+ return isUp ? 'up' : 'down'
|
|
|
+}
|
|
|
+function arrow(v) {
|
|
|
+ if (typeof v !== 'number' || Math.abs(v) < 0.005) return '—'
|
|
|
+ return v > 0 ? '▲' : '▼'
|
|
|
+}
|
|
|
+
|
|
|
+async function reload() {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const [n, r, d, a, f, latestAll] = await Promise.all([
|
|
|
+ loadGdpNominal(),
|
|
|
+ loadGdpReal(),
|
|
|
+ loadGdpDeflator(),
|
|
|
+ loadMm6Actual(),
|
|
|
+ loadMm6Forecast(),
|
|
|
+ loadLatestAll(),
|
|
|
+ ])
|
|
|
+ gdpNominalSeries.value = n.series
|
|
|
+ gdpNominalMeta.value = n.meta
|
|
|
+ gdpRealSeries.value = r.series
|
|
|
+ gdpRealMeta.value = r.meta
|
|
|
+ gdpDeflatorSeries.value = d.series
|
|
|
+ gdpDeflatorMeta.value = d.meta
|
|
|
+ mm6ActualSeries.value = a.series
|
|
|
+ mm6ActualMeta.value = a.meta
|
|
|
+ mm6Forecast.value = f
|
|
|
+ mm6ForecastMeta.value = f.meta
|
|
|
+
|
|
|
+ gdpNominalLatest.value = latestAll.gdpNominal.latest
|
|
|
+ gdpRealLatest.value = latestAll.gdpReal.latest
|
|
|
+ gdpDeflatorLatest.value = latestAll.gdpDeflator.latest
|
|
|
+ mm6ActualLatest.value = latestAll.mm6Actual.latest
|
|
|
+
|
|
|
+ updatedAt.value = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(reload)
|
|
|
+
|
|
|
+// ===== KPI =====
|
|
|
+const kpis = computed(() => ([
|
|
|
+ { label: '现价 GDP 同比', icon: '📈', value: gdpNominalLatest.value?.value, unit: '%', date: formatDate(gdpNominalLatest.value?.date), tag: '名义增速', sign: true, forecast: false },
|
|
|
+ { label: '不变价 GDP 同比', icon: '📊', value: gdpRealLatest.value?.value, unit: '%', date: formatDate(gdpRealLatest.value?.date), tag: '实际增速', sign: true, forecast: false },
|
|
|
+ { label: 'GDP 平减指数', icon: '🧮', value: gdpDeflatorLatest.value?.value, unit: '%', date: formatDate(gdpDeflatorLatest.value?.date), tag: gdpDeflatorLatest.value?.value >= 0 ? '价格因素为正' : '价格因素为负', sign: false, forecast: false },
|
|
|
+ { label: 'MM6 周频预测', icon: '🔮', value: mm6ForecastLatest.value?.value, unit: '%', date: formatDate(mm6ForecastLatest.value?.date), tag: '下期预测', sign: true, forecast: true },
|
|
|
+ { label: 'MM6 实际增速', icon: '🎯', value: mm6ActualLatest.value?.value, unit: '%', date: formatDate(mm6ActualLatest.value?.date), tag: '季度内最新', sign: true, forecast: false },
|
|
|
+]))
|
|
|
+
|
|
|
+// ===== Charts =====
|
|
|
+const gdpNominalOpt = computed(() => gdpQuarterOption(
|
|
|
+ gdpNominalSeries.value,
|
|
|
+ { title: '现价 GDP 同比', color: '#1552D0', unit: '%' },
|
|
|
+))
|
|
|
+const gdpRealOpt = computed(() => gdpQuarterOption(
|
|
|
+ gdpRealSeries.value,
|
|
|
+ { title: '不变价 GDP 同比', color: '#E8912D', unit: '%' },
|
|
|
+))
|
|
|
+const gdpDeflatorOpt = computed(() => gdpDeflatorQuarterOption(
|
|
|
+ gdpDeflatorSeries.value,
|
|
|
+))
|
|
|
+
|
|
|
+// 现价 vs 不变价 叠加(量 vs 价)
|
|
|
+const gdpNominalVsRealOpt = computed(() => {
|
|
|
+ const nomMap = new Map(gdpNominalSeries.value)
|
|
|
+ const realMap = new Map(gdpRealSeries.value)
|
|
|
+ const cats = gdpNominalSeries.value.map((x) => String(x[0]).slice(2, 10))
|
|
|
+ return {
|
|
|
+ tooltip: { backgroundColor: 'rgba(12,43,94,.94)', borderWidth: 0, textStyle: { color: '#fff', fontSize: 12 }, trigger: 'axis' },
|
|
|
+ legend: { data: ['现价', '不变价'], top: 2, textStyle: { color: '#6B7A96', fontSize: 11 }, itemWidth: 14, itemHeight: 8 },
|
|
|
+ grid: { top: 22, left: 46, right: 18, bottom: 26 },
|
|
|
+ xAxis: { type: 'category', data: cats, axisLine: { lineStyle: { color: '#E4E9F2' } }, axisTick: { show: false }, axisLabel: { color: '#6B7A96', fontSize: 10 } },
|
|
|
+ yAxis: { type: 'value', name: '%', axisLine: { lineStyle: { color: '#E4E9F2' } }, axisTick: { show: false }, axisLabel: { color: '#6B7A96', fontSize: 11 }, splitLine: { lineStyle: { color: '#F2F5FA', type: 'dashed' } } },
|
|
|
+ series: [
|
|
|
+ { name: '现价', type: 'line', smooth: true, showSymbol: false, data: gdpNominalSeries.value.map((x) => x[1]), lineStyle: { color: '#1552D0', width: 2.2 }, itemStyle: { color: '#1552D0' } },
|
|
|
+ { name: '不变价', type: 'line', smooth: true, showSymbol: false, data: gdpRealSeries.value.map((x) => x[1]), lineStyle: { color: '#E8912D', width: 2.2, type: 'dashed' }, itemStyle: { color: '#E8912D' } },
|
|
|
+ ],
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// ===== MM6 =====
|
|
|
+const mm6ForecastLatest = computed(() => {
|
|
|
+ const arr = mm6Forecast.value.series
|
|
|
+ if (!arr || !arr.length) return null
|
|
|
+ const last = arr[arr.length - 1]
|
|
|
+ return { date: last[0], value: last[1] }
|
|
|
+})
|
|
|
+const mm6Range = computed(() => {
|
|
|
+ // CI 宽度来自 upper - lower 的平均值
|
|
|
+ const u = new Map(mm6Forecast.value.upper)
|
|
|
+ const l = new Map(mm6Forecast.value.lower)
|
|
|
+ const widths = [...u.entries()].map(([d, uv]) => {
|
|
|
+ const lv = l.get(d)
|
|
|
+ return lv != null ? uv - lv : null
|
|
|
+ }).filter((x) => x != null)
|
|
|
+ if (!widths.length) return 0.3
|
|
|
+ return widths[widths.length - 1] / 2
|
|
|
+})
|
|
|
+const mm6RangeLow = computed(() => (mm6ForecastLatest.value ? mm6ForecastLatest.value.value - mm6Range.value : null))
|
|
|
+const mm6RangeHigh = computed(() => (mm6ForecastLatest.value ? mm6ForecastLatest.value.value + mm6Range.value : null))
|
|
|
+const mm6PinLeft = computed(() => {
|
|
|
+ if (!mm6ForecastLatest.value) return 50
|
|
|
+ const min = mm6RangeLow.value - 0.3
|
|
|
+ const max = mm6RangeHigh.value + 0.3
|
|
|
+ return Math.max(0, Math.min(100, ((mm6ForecastLatest.value.value - min) / (max - min)) * 100))
|
|
|
+})
|
|
|
+const mm6Delta = computed(() => {
|
|
|
+ const lastActual = mm6ActualSeries.value[mm6ActualSeries.value.length - 1]
|
|
|
+ if (!lastActual || !mm6ForecastLatest.value) return { dirCls: 'flat', arrow: '—', text: '—' }
|
|
|
+ const delta = mm6ForecastLatest.value.value - lastActual[1]
|
|
|
+ const dir = delta > 0.01 ? 'up' : (delta < -0.01 ? 'down' : 'flat')
|
|
|
+ const ar = delta > 0.01 ? '▲' : (delta < -0.01 ? '▼' : '—')
|
|
|
+ return { dirCls: dir, arrow: ar, text: `${ar} ${Math.abs(delta).toFixed(2)}pct 同比` }
|
|
|
+})
|
|
|
+
|
|
|
+const mm6WeeklyOpt = computed(() => mm6WeeklyOption(
|
|
|
+ mm6ActualSeries.value,
|
|
|
+ mm6Forecast.value.series,
|
|
|
+))
|
|
|
+const mm6ForecastOpt = computed(() => mm6ForecastOption(
|
|
|
+ mm6ActualSeries.value,
|
|
|
+ mm6Forecast.value.series,
|
|
|
+ mm6Forecast.value.upper,
|
|
|
+ mm6Forecast.value.lower,
|
|
|
+))
|
|
|
+
|
|
|
+const gdpNominalComment = computed(() => {
|
|
|
+ const v = gdpNominalLatest.value?.value
|
|
|
+ if (typeof v !== 'number') return '增长动能持续观察'
|
|
|
+ if (v >= 6) return '名义增长动能较强'
|
|
|
+ if (v >= 4) return '名义增长温和'
|
|
|
+ if (v >= 0) return '名义增长偏弱'
|
|
|
+ return '名义增长承压'
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.gdp-page {
|
|
|
+ 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:disabled { opacity: .6; cursor: not-allowed; }
|
|
|
+.ic-refresh.spinning { display: inline-block; animation: spin 1s linear infinite; }
|
|
|
+@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
|
+
|
|
|
+/* === KPI === */
|
|
|
+.kpi-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(5, 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;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+.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;
|
|
|
+}
|
|
|
+.section-hd .meta-spacer { flex: 1; }
|
|
|
+
|
|
|
+/* === 卡片网格 === */
|
|
|
+.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;
|
|
|
+ min-width: 18px;
|
|
|
+ height: 18px;
|
|
|
+ padding: 0 4px;
|
|
|
+ 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);
|
|
|
+}
|
|
|
+
|
|
|
+/* === KV 列表 === */
|
|
|
+.kv-list { padding: 4px 0; }
|
|
|
+.kv {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 7px 0;
|
|
|
+ border-bottom: 1px dashed var(--line, #E4E9F2);
|
|
|
+ font-size: 12.5px;
|
|
|
+}
|
|
|
+.kv:last-child { border-bottom: none; }
|
|
|
+.kv .k { color: var(--mut, #6B7A96); }
|
|
|
+.kv .v { color: var(--ink, #1A2438); font-variant-numeric: tabular-nums; }
|
|
|
+.kv .v.strong { font-weight: 600; }
|
|
|
+.note {
|
|
|
+ margin-top: 12px;
|
|
|
+ padding: 10px 12px;
|
|
|
+ background: #F7F9FD;
|
|
|
+ border-left: 3px solid var(--brand, #1552D0);
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.7;
|
|
|
+ color: var(--mut, #6B7A96);
|
|
|
+}
|
|
|
+
|
|
|
+/* === tag-r === */
|
|
|
+.tag-r { color: var(--brand-d, #0E3A96) !important; font-weight: 600; }
|
|
|
+.meta { color: #98A5BD; font-size: 11.5px; }
|
|
|
+
|
|
|
+/* === 预测卡 === */
|
|
|
+.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;
|
|
|
+}
|
|
|
+
|
|
|
+/* === Legend row === */
|
|
|
+.legend-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ font-size: 11.5px;
|
|
|
+ color: var(--mut, #6B7A96);
|
|
|
+ margin-top: 6px;
|
|
|
+}
|
|
|
+.dot-c {
|
|
|
+ display: inline-block;
|
|
|
+ width: 10px;
|
|
|
+ height: 2px;
|
|
|
+ border-radius: 1px;
|
|
|
+ margin-right: 4px;
|
|
|
+}
|
|
|
+.dot-c.brand { background: #1552D0; }
|
|
|
+.dot-c.warn { background: #E8912D; }
|
|
|
+.dot-c.faint { background: #98A5BD; border-top: 1px dashed #98A5BD; height: 0; }
|
|
|
+
|
|
|
+/* === 颜色工具 === */
|
|
|
+.up { color: #E13B3B !important; }
|
|
|
+.down { color: #189A65 !important; }
|
|
|
+.flat { color: var(--mut, #6B7A96) !important; }
|
|
|
+
|
|
|
+.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(3, minmax(0, 1fr)); }
|
|
|
+}
|
|
|
+@media (max-width: 980px) {
|
|
|
+ .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
|
+ .card-grid.cols-2-1,
|
|
|
+ .card-grid.cols-1-1,
|
|
|
+ .card-grid.cols-2 { grid-template-columns: 1fr; }
|
|
|
+}
|
|
|
+</style>
|