index.vue 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090
  1. <template>
  2. <div class="us-page">
  3. <header class="page-hd">
  4. <div>
  5. <div class="page-title">美股分析</div>
  6. <div class="page-sub">
  7. 美股资产评分表 · 政治与宏观 · 估值与盈利 · 市场情绪与动量 · 数据截至 {{ summary.asOf }}
  8. </div>
  9. </div>
  10. <div class="page-actions">
  11. <span class="chip" :class="summaryCls">
  12. 综合 · {{ summaryBand.label }}
  13. </span>
  14. <button class="btn" @click="reload" :disabled="loading">
  15. <span :class="['ic-refresh', { spinning: loading }]">↻</span> 刷新
  16. </button>
  17. </div>
  18. </header>
  19. <!-- KPI 概览条 -->
  20. <section class="kpi-grid">
  21. <div class="kpi-card hero" :class="summaryCls">
  22. <div class="label">综合加权得分</div>
  23. <div class="value">
  24. {{ formatScore(summary.current) }}<span class="unit"> / 10</span>
  25. </div>
  26. <div class="chg">
  27. <span class="arrow">{{ summaryDelta.arrow }}</span>
  28. 较上期 {{ summaryDelta.text }}
  29. · {{ summaryBand.label }}
  30. </div>
  31. </div>
  32. <div class="kpi-card">
  33. <div class="label">政治与宏观</div>
  34. <div class="value">{{ formatScore(catScore('macro')) }}</div>
  35. <div class="chg">权重 33.33% · 4 项简单平均</div>
  36. </div>
  37. <div class="kpi-card">
  38. <div class="label">估值与盈利</div>
  39. <div class="value">{{ formatScore(catScore('profit')) }}</div>
  40. <div class="chg">权重 33.33% · 4 项简单平均</div>
  41. </div>
  42. <div class="kpi-card">
  43. <div class="label">市场情绪与动量</div>
  44. <div class="value">{{ formatScore(catScore('momentum')) }}</div>
  45. <div class="chg">权重 33.33% · 4 项简单平均</div>
  46. </div>
  47. <div class="kpi-card">
  48. <div class="label">投资小组评分</div>
  49. <div class="value">{{ formatScore(summary.groupScore) }}</div>
  50. <div class="chg">较上期 +{{ summary.groupDelta.toFixed(2) }} · 中性偏多</div>
  51. </div>
  52. </section>
  53. <!-- 综合得分仪表 + 评分区间说明 -->
  54. <section class="section">
  55. <div class="section-hd">
  56. <span class="dot"></span>
  57. <h2>综合得分仪表 · 评分区间映射</h2>
  58. <span class="meta">{{ summary.asOf }} · 上期对比 {{ summary.prevAsOf }}</span>
  59. </div>
  60. <div class="card-grid cols-2-1">
  61. <div class="card">
  62. <div class="card-head">
  63. <div class="title">
  64. <span class="ic">图</span>
  65. <span class="title-text">综合加权得分 · 仪表盘</span>
  66. </div>
  67. <div class="acts">
  68. <span class="meta">本期 {{ formatScore(summary.current) }} · 上期 {{ formatScore(summary.previous) }}</span>
  69. </div>
  70. </div>
  71. <div class="card-body">
  72. <Chart :option="gaugeOpt" height="320px" />
  73. </div>
  74. </div>
  75. <div class="card">
  76. <div class="card-head">
  77. <div class="title">
  78. <span class="ic">说</span>
  79. <span class="title-text">评分区间说明</span>
  80. </div>
  81. </div>
  82. <div class="card-body">
  83. <div class="band-list">
  84. <div
  85. v-for="b in bands"
  86. :key="b.label"
  87. class="band-row"
  88. :class="{ on: summaryBand.label === b.label }"
  89. >
  90. <div class="band-bar" :style="{ background: b.axisColor }"></div>
  91. <div class="band-label">{{ b.label }}</div>
  92. <div class="band-range">
  93. <template v-if="b.lo === 0">(0, {{ b.hi }}]</template>
  94. <template v-else-if="b.hi === 10">[{{ b.lo }}, 10)</template>
  95. <template v-else-if="b.hi === 8">[{{ b.lo }}, {{ b.hi }})</template>
  96. <template v-else>({{ b.lo }}, {{ b.hi }}]</template>
  97. </div>
  98. </div>
  99. </div>
  100. <div class="note">
  101. <div class="note-row">
  102. <span class="note-k">综合评分</span>
  103. <span class="note-v">{{ formatScore(summary.current) }} 分</span>
  104. </div>
  105. <div class="note-row">
  106. <span class="note-k">所在区间</span>
  107. <span class="note-v" :style="{ color: summaryBand.fg }">{{ summaryBand.label }}</span>
  108. </div>
  109. <div class="note-row">
  110. <span class="note-k">上期得分</span>
  111. <span class="note-v">{{ formatScore(summary.previous) }} 分</span>
  112. </div>
  113. <div class="note-row">
  114. <span class="note-k">环比变动</span>
  115. <span class="note-v">{{ summaryDelta.arrow }} {{ summaryDelta.text }}</span>
  116. </div>
  117. </div>
  118. </div>
  119. </div>
  120. </div>
  121. </section>
  122. <!-- 一级类别横向对比 + 贡献度 + 本月解读 -->
  123. <section class="section">
  124. <div class="section-hd">
  125. <span class="dot"></span>
  126. <h2>一级类别综合得分 · 类别贡献度 · 本月解读</h2>
  127. <span class="meta">12 项因子简单平均 · 含投资小组 / 综合评分</span>
  128. </div>
  129. <div class="card-grid cols-2-1-1">
  130. <div class="card">
  131. <div class="card-head">
  132. <div class="title">
  133. <span class="ic">图</span>
  134. <span class="title-text">一级类别综合得分 · 本期 vs 上期</span>
  135. </div>
  136. </div>
  137. <div class="card-body">
  138. <Chart :option="barOpt" height="300px" />
  139. </div>
  140. </div>
  141. <div class="card">
  142. <div class="card-head">
  143. <div class="title">
  144. <span class="ic">图</span>
  145. <span class="title-text">类别贡献度 · 横向对比</span>
  146. </div>
  147. </div>
  148. <div class="card-body">
  149. <Chart :option="contribOpt" height="300px" />
  150. </div>
  151. </div>
  152. <div class="card">
  153. <div class="card-head">
  154. <div class="title">
  155. <span class="ic">图</span>
  156. <span class="title-text">本月主要变化 · 评级</span>
  157. </div>
  158. </div>
  159. <div class="card-body">
  160. <div class="rating-block">
  161. <div class="rating-row">
  162. <span class="r-k">评级</span>
  163. <span class="chip" :class="summaryCls">{{ summary.rating }}</span>
  164. </div>
  165. <div class="rating-row">
  166. <span class="r-k">投资小组评分</span>
  167. <span class="r-v">
  168. {{ formatScore(summary.groupScore) }}
  169. <span class="r-delta up">+{{ summary.groupDelta.toFixed(2) }}</span>
  170. </span>
  171. </div>
  172. <div class="rating-row">
  173. <span class="r-k">综合评分(12 项简单平均)</span>
  174. <span class="r-v">{{ formatScore(summary.current) }}</span>
  175. </div>
  176. <div class="highlight">
  177. <div class="hl-title">本月主要变化</div>
  178. <ul class="hl-list">
  179. <li v-for="(h, i) in summary.highlights" :key="i">{{ h }}</li>
  180. </ul>
  181. </div>
  182. </div>
  183. </div>
  184. </div>
  185. </div>
  186. </section>
  187. <!-- 分项因子明细 -->
  188. <section class="section">
  189. <div class="section-hd">
  190. <span class="dot"></span>
  191. <h2>分项因子明细 · 评分对比</h2>
  192. <span class="meta">{{ allFactors.length }} 项因子 · 雷达对比本期与上期</span>
  193. </div>
  194. <div class="card-grid cols-1">
  195. <div class="card">
  196. <div class="card-head">
  197. <div class="title">
  198. <span class="ic">图</span>
  199. <span class="title-text">分项因子 · 雷达图(本期 vs 上期)</span>
  200. </div>
  201. <div class="acts">
  202. <span class="meta">满值 10 · 中性 5</span>
  203. </div>
  204. </div>
  205. <div class="card-body">
  206. <Chart :option="radarOpt" height="380px" />
  207. </div>
  208. </div>
  209. <div class="card" style="margin-top:14px">
  210. <div class="card-head">
  211. <div class="title">
  212. <span class="ic">表</span>
  213. <span class="title-text">分项因子 · 明细表</span>
  214. </div>
  215. <div class="acts">
  216. <span class="meta">Δ = 本期 − 上期</span>
  217. </div>
  218. </div>
  219. <div class="card-body">
  220. <div class="table-wrap">
  221. <table class="score-tbl">
  222. <thead>
  223. <tr>
  224. <th>类别</th>
  225. <th>指标因子</th>
  226. <th>指标释义</th>
  227. <th>评分规则</th>
  228. <th class="num">权重</th>
  229. <th class="num">本期</th>
  230. <th class="num">上期</th>
  231. <th class="num">Δ</th>
  232. <th>区间</th>
  233. </tr>
  234. </thead>
  235. <tbody>
  236. <tr v-for="(f, i) in factorCompare" :key="i">
  237. <td>
  238. <span class="cat-pill" :class="f.categoryKey">{{ f.category }}</span>
  239. </td>
  240. <td><b>{{ f.name }}</b></td>
  241. <td class="def">{{ f.definition }}</td>
  242. <td class="rule">{{ f.rule }}</td>
  243. <td class="num">{{ f.weight }}%</td>
  244. <td class="num">
  245. <span class="score-pill" :class="bandCls(f.current)">{{ f.current }}</span>
  246. </td>
  247. <td class="num">{{ f.previous }}</td>
  248. <td class="num">
  249. <span :class="['delta', deltaCls(f.delta)]">
  250. {{ f.delta > 0 ? '+' : '' }}{{ f.delta }}
  251. </span>
  252. </td>
  253. <td>
  254. <span class="band-tag" :class="bandCls(f.current)">
  255. {{ classify(f.current).label }}
  256. </span>
  257. </td>
  258. </tr>
  259. </tbody>
  260. </table>
  261. </div>
  262. </div>
  263. </div>
  264. </div>
  265. </section>
  266. <!-- 类别分组详情卡 -->
  267. <section class="section">
  268. <div class="section-hd">
  269. <span class="dot"></span>
  270. <h2>类别分组 · 子因子快速浏览</h2>
  271. <span class="meta">点击卡片展开 / 收起</span>
  272. </div>
  273. <div class="cat-grid">
  274. <div
  275. v-for="cat in categories"
  276. :key="cat.key"
  277. class="cat-card"
  278. :style="{ borderTopColor: cat.color }"
  279. >
  280. <div class="cat-head" @click="toggle(cat.key)">
  281. <div class="cat-title">
  282. <span class="cat-dot" :style="{ background: cat.color }"></span>
  283. <span class="cat-name">{{ cat.name }}</span>
  284. <span class="cat-weight">{{ totalWeight(cat) }}%</span>
  285. </div>
  286. <div class="cat-score">
  287. <span class="cat-score-val" :style="{ color: cat.color }">
  288. {{ formatScore(catScore(cat.key)) }}
  289. </span>
  290. <span class="cat-score-delta">
  291. <span :class="['arrow', deltaCls(catDelta(cat.key))]">
  292. {{ catDeltaArrow(cat.key) }}
  293. </span>
  294. {{ Math.abs(catDelta(cat.key)).toFixed(2) }}
  295. </span>
  296. <span class="cat-toggle">{{ expanded[cat.key] ? '收起' : '展开' }}</span>
  297. </div>
  298. </div>
  299. <div v-show="expanded[cat.key]" class="cat-body">
  300. <div
  301. v-for="f in cat.factors"
  302. :key="f.name"
  303. class="factor-row"
  304. >
  305. <div class="factor-meta">
  306. <div class="factor-name">
  307. <span class="factor-name-t">{{ f.name }}</span>
  308. <span class="factor-w">{{ f.weight }}%</span>
  309. </div>
  310. <div class="factor-def">{{ f.definition }}</div>
  311. <div class="factor-rule">规则:{{ f.rule }}</div>
  312. </div>
  313. <div class="factor-score">
  314. <div class="factor-cur" :class="bandCls(f.current)">
  315. {{ f.current }}
  316. </div>
  317. <div class="factor-prev">{{ f.previous }}</div>
  318. <div class="factor-bar">
  319. <div
  320. class="factor-bar-fill"
  321. :style="{
  322. width: (f.current / 10 * 100) + '%',
  323. background: bandColor(f.current),
  324. }"
  325. ></div>
  326. </div>
  327. </div>
  328. </div>
  329. </div>
  330. </div>
  331. </div>
  332. </section>
  333. <footer class="footer">
  334. 数据源:内部权益部模型 · 4.3.1-美股打分表202609 · 截至 {{ summary.asOf }} · 评分区间 (0,3] 看空 / (3,4] 中性偏空 / (4,6) 中性 / [6,8) 中性偏多 / [8,10) 看多
  335. </footer>
  336. </div>
  337. </template>
  338. <script setup>
  339. import { computed, reactive, ref } from 'vue'
  340. import Chart from '@/components/Chart.vue'
  341. import {
  342. scoreSummary,
  343. categories,
  344. allFactors,
  345. categoryWeightedCurrent,
  346. categoryWeightedPrev,
  347. factorCompare,
  348. SCORE_BANDS,
  349. classifyScore,
  350. } from './data'
  351. import {
  352. scoreGaugeOption,
  353. categoryBarOption,
  354. categoryContribOption,
  355. factorRadarOption,
  356. } from './charts'
  357. const loading = ref(false)
  358. const summary = scoreSummary
  359. const bands = SCORE_BANDS
  360. const summaryBand = computed(() => classifyScore(summary.current))
  361. const summaryCls = computed(() => summaryBand.value.cls)
  362. const summaryDelta = computed(() => {
  363. const d = +(summary.current - summary.previous).toFixed(3)
  364. return {
  365. arrow: d > 0.005 ? '▲' : (d < -0.005 ? '▼' : '—'),
  366. text: (d > 0 ? '+' : '') + d.toFixed(2) + ' 分',
  367. }
  368. })
  369. function catScore(key) {
  370. const c = categoryWeightedCurrent.find((x) => x.key === key)
  371. return c ? c.score : 0
  372. }
  373. function catDelta(key) {
  374. const cur = categoryWeightedCurrent.find((x) => x.key === key)
  375. const pre = categoryWeightedPrev.find((x) => x.key === key)
  376. if (!cur || !pre) return 0
  377. return +(cur.score - pre.score).toFixed(2)
  378. }
  379. function catDeltaArrow(key) {
  380. const d = catDelta(key)
  381. return d > 0.005 ? '▲' : (d < -0.005 ? '▼' : '—')
  382. }
  383. function totalWeight(cat) {
  384. return cat.factors.reduce((s, f) => s + f.weight, 0).toFixed(2)
  385. }
  386. function formatScore(v) {
  387. if (v == null) return '—'
  388. return Number(v).toFixed(2)
  389. }
  390. function bandCls(v) {
  391. return classifyScore(v).cls
  392. }
  393. function bandColor(v) {
  394. return classifyScore(v).axisColor
  395. }
  396. function classify(v) {
  397. return classifyScore(v)
  398. }
  399. function deltaCls(d) {
  400. if (d > 0.005) return 'up'
  401. if (d < -0.005) return 'down'
  402. return 'flat'
  403. }
  404. const expanded = reactive({
  405. macro: true,
  406. profit: true,
  407. momentum: false,
  408. })
  409. function toggle(k) {
  410. expanded[k] = !expanded[k]
  411. }
  412. const gaugeOpt = computed(() => scoreGaugeOption(summary.current, summary.previous))
  413. const barOpt = computed(() => categoryBarOption(categoryWeightedCurrent, categoryWeightedPrev))
  414. const contribOpt = computed(() => categoryContribOption(categoryWeightedCurrent))
  415. const radarOpt = computed(() => factorRadarOption(allFactors))
  416. function reload() {
  417. loading.value = true
  418. setTimeout(() => { loading.value = false }, 600)
  419. }
  420. </script>
  421. <style scoped>
  422. .us-page {
  423. padding: 18px 24px 32px;
  424. color: var(--ink, #1A2438);
  425. }
  426. /* === 顶部 === */
  427. .page-hd {
  428. display: flex;
  429. align-items: flex-end;
  430. justify-content: space-between;
  431. margin-bottom: 14px;
  432. flex-wrap: wrap;
  433. gap: 12px;
  434. }
  435. .page-title {
  436. font-size: 18px;
  437. font-weight: 600;
  438. color: var(--ink, #1A2438);
  439. margin-bottom: 3px;
  440. }
  441. .page-sub {
  442. font-size: 12px;
  443. color: var(--mut, #6B7A96);
  444. }
  445. .page-actions {
  446. display: flex;
  447. gap: 8px;
  448. align-items: center;
  449. }
  450. .btn {
  451. height: 30px;
  452. padding: 0 14px;
  453. border: 1px solid var(--line, #E4E9F2);
  454. border-radius: 6px;
  455. background: #fff;
  456. color: var(--ink, #1A2438);
  457. font-size: 12.5px;
  458. cursor: pointer;
  459. display: inline-flex;
  460. align-items: center;
  461. gap: 5px;
  462. transition: 0.15s;
  463. }
  464. .btn:hover {
  465. border-color: var(--brand, #1552D0);
  466. color: var(--brand, #1552D0);
  467. }
  468. .chip {
  469. display: inline-block;
  470. padding: 4px 11px;
  471. border-radius: 12px;
  472. font-size: 12px;
  473. font-weight: 600;
  474. white-space: nowrap;
  475. }
  476. .chip.bear { background: #FDEAEA; color: #E13B3B; }
  477. .chip.mild-bear { background: #FFE9D6; color: #E8912D; }
  478. .chip.neutral { background: #EEF2F7; color: #6B7A96; }
  479. .chip.mild-bull { background: #E4F5EE; color: #189A65; }
  480. .chip.bull { background: #FDEAEA; color: #E13B3B; }
  481. .ic-refresh {
  482. display: inline-block;
  483. transition: transform 0.6s;
  484. }
  485. .ic-refresh.spinning { animation: spin 0.8s linear infinite; }
  486. @keyframes spin {
  487. from { transform: rotate(0deg); }
  488. to { transform: rotate(360deg); }
  489. }
  490. /* === KPI === */
  491. .kpi-grid {
  492. display: grid;
  493. grid-template-columns: 1.4fr repeat(4, 1fr);
  494. gap: 10px;
  495. margin-bottom: 16px;
  496. }
  497. .kpi-card {
  498. background: #fff;
  499. border: 1px solid var(--line, #E4E9F2);
  500. border-radius: 8px;
  501. padding: 13px 14px;
  502. position: relative;
  503. transition: 0.15s;
  504. min-width: 0;
  505. }
  506. .kpi-card:hover {
  507. box-shadow: 0 1px 3px rgba(16, 32, 64, 0.06);
  508. border-color: #D1D8E5;
  509. }
  510. .kpi-card.hero {
  511. background: linear-gradient(135deg, #0E3A96, #1552D0);
  512. color: #fff;
  513. border-color: #0E3A96;
  514. }
  515. .kpi-card.hero .label { color: rgba(255,255,255,0.78); }
  516. .kpi-card.hero .unit { color: rgba(255,255,255,0.7); }
  517. .kpi-card.hero .chg { color: rgba(255,255,255,0.85); }
  518. .kpi-card.hero.bear { background: linear-gradient(135deg, #B72828, #E13B3B); border-color: #B72828; }
  519. .kpi-card.hero.mild-bear { background: linear-gradient(135deg, #B86A1F, #E8912D); border-color: #B86A1F; }
  520. .kpi-card.hero.neutral { background: linear-gradient(135deg, #4D5A75, #6B7A96); border-color: #4D5A75; }
  521. .kpi-card.hero.mild-bull { background: linear-gradient(135deg, #13754C, #189A65); border-color: #13754C; }
  522. .kpi-card.hero.bull { background: linear-gradient(135deg, #B72828, #E13B3B); border-color: #B72828; }
  523. .kpi-card .label {
  524. font-size: 11.5px;
  525. color: var(--mut, #6B7A96);
  526. margin-bottom: 6px;
  527. display: flex;
  528. align-items: center;
  529. gap: 4px;
  530. }
  531. .kpi-card .value {
  532. font-size: 22px;
  533. font-weight: 600;
  534. color: var(--ink, #1A2438);
  535. font-variant-numeric: tabular-nums;
  536. letter-spacing: -0.5px;
  537. line-height: 1.1;
  538. }
  539. .kpi-card .value .unit {
  540. font-size: 12px;
  541. color: var(--mut, #6B7A96);
  542. font-weight: 500;
  543. margin-left: 2px;
  544. }
  545. .kpi-card .chg {
  546. font-size: 11px;
  547. margin-top: 5px;
  548. color: var(--mut, #6B7A96);
  549. font-variant-numeric: tabular-nums;
  550. display: flex;
  551. align-items: center;
  552. gap: 3px;
  553. }
  554. .kpi-card .chg .arrow { font-size: 9px; }
  555. /* === 章节 === */
  556. .section {
  557. margin-top: 18px;
  558. }
  559. .section-hd {
  560. display: flex;
  561. align-items: center;
  562. gap: 8px;
  563. margin-bottom: 12px;
  564. }
  565. .section-hd .dot {
  566. width: 4px;
  567. height: 18px;
  568. background: var(--brand, #1552D0);
  569. border-radius: 2px;
  570. }
  571. .section-hd h2 {
  572. font-size: 14.5px;
  573. font-weight: 600;
  574. color: var(--ink, #1A2438);
  575. margin: 0;
  576. }
  577. .section-hd .meta {
  578. font-size: 11.5px;
  579. color: #98A5BD;
  580. margin-left: 8px;
  581. }
  582. /* === 卡片网格 === */
  583. .card-grid {
  584. display: grid;
  585. gap: 14px;
  586. }
  587. .card-grid.cols-2-1 { grid-template-columns: 2fr 1fr; }
  588. .card-grid.cols-2-1-1 { grid-template-columns: 1.6fr 1fr 1fr; }
  589. .card-grid.cols-1-1 { grid-template-columns: 1fr 1fr; }
  590. .card-grid.cols-1 { grid-template-columns: 1fr; }
  591. .card-grid.cols-2 { grid-template-columns: 1fr 1fr; }
  592. .card {
  593. background: #fff;
  594. border: 1px solid var(--line, #E4E9F2);
  595. border-radius: 8px;
  596. overflow: hidden;
  597. display: flex;
  598. flex-direction: column;
  599. }
  600. .card:hover { box-shadow: 0 1px 3px rgba(16, 32, 64, 0.06); }
  601. .card-head {
  602. padding: 11px 16px;
  603. border-bottom: 1px solid var(--line, #E4E9F2);
  604. display: flex;
  605. align-items: center;
  606. gap: 8px;
  607. }
  608. .card-head .title {
  609. font-size: 13.5px;
  610. font-weight: 600;
  611. color: var(--ink, #1A2438);
  612. display: flex;
  613. align-items: center;
  614. gap: 6px;
  615. flex: 1;
  616. min-width: 0;
  617. }
  618. .card-head .title .ic {
  619. display: inline-grid;
  620. place-items: center;
  621. width: 18px;
  622. height: 18px;
  623. background: var(--brand-lt, #EAF1FF);
  624. color: var(--brand, #1552D0);
  625. font-size: 10px;
  626. border-radius: 4px;
  627. font-weight: 700;
  628. flex: none;
  629. }
  630. .card-head .title-text {
  631. white-space: nowrap;
  632. overflow: hidden;
  633. text-overflow: ellipsis;
  634. }
  635. .card-head .acts {
  636. display: flex;
  637. align-items: center;
  638. gap: 6px;
  639. flex: none;
  640. font-size: 11px;
  641. color: var(--mut, #6B7A96);
  642. }
  643. .card-body {
  644. padding: 14px 16px;
  645. flex: 1;
  646. min-height: 0;
  647. }
  648. /* === 评分区间说明 === */
  649. .band-list {
  650. display: flex;
  651. flex-direction: column;
  652. gap: 6px;
  653. margin-bottom: 12px;
  654. }
  655. .band-row {
  656. display: flex;
  657. align-items: center;
  658. gap: 10px;
  659. padding: 7px 10px;
  660. border-radius: 6px;
  661. background: #FAFBFE;
  662. border: 1px solid transparent;
  663. transition: 0.15s;
  664. }
  665. .band-row.on {
  666. background: #fff;
  667. border-color: var(--brand-lt, #EAF1FF);
  668. box-shadow: 0 1px 3px rgba(16,32,64,0.06);
  669. }
  670. .band-bar {
  671. width: 4px;
  672. height: 18px;
  673. border-radius: 2px;
  674. }
  675. .band-label {
  676. font-size: 13px;
  677. font-weight: 600;
  678. color: var(--ink);
  679. flex: 1;
  680. }
  681. .band-range {
  682. font-size: 11.5px;
  683. color: var(--mut, #6B7A96);
  684. font-variant-numeric: tabular-nums;
  685. }
  686. .note {
  687. padding-top: 12px;
  688. border-top: 1px dashed var(--line, #E4E9F2);
  689. font-size: 12px;
  690. display: flex;
  691. flex-direction: column;
  692. gap: 6px;
  693. }
  694. .note-row {
  695. display: flex;
  696. justify-content: space-between;
  697. align-items: center;
  698. }
  699. .note-k {
  700. color: var(--mut, #6B7A96);
  701. }
  702. .note-v {
  703. font-weight: 600;
  704. font-variant-numeric: tabular-nums;
  705. color: var(--ink, #1A2438);
  706. }
  707. /* === 评级解读块 === */
  708. .rating-block {
  709. display: flex;
  710. flex-direction: column;
  711. gap: 10px;
  712. }
  713. .rating-row {
  714. display: flex;
  715. justify-content: space-between;
  716. align-items: center;
  717. font-size: 12.5px;
  718. padding: 6px 0;
  719. border-bottom: 1px dashed var(--line, #E4E9F2);
  720. }
  721. .rating-row:last-of-type {
  722. border-bottom: none;
  723. }
  724. .r-k {
  725. color: var(--mut, #6B7A96);
  726. }
  727. .r-v {
  728. font-weight: 600;
  729. font-variant-numeric: tabular-nums;
  730. color: var(--ink, #1A2438);
  731. display: inline-flex;
  732. align-items: center;
  733. gap: 6px;
  734. }
  735. .r-delta {
  736. font-size: 11px;
  737. padding: 1px 6px;
  738. border-radius: 8px;
  739. font-weight: 600;
  740. }
  741. .r-delta.up { background: #E4F5EE; color: #189A65; }
  742. .r-delta.down { background: #FDEAEA; color: #E13B3B; }
  743. .highlight {
  744. margin-top: 4px;
  745. padding: 10px 12px;
  746. background: #FAFBFE;
  747. border: 1px dashed var(--line, #E4E9F2);
  748. border-radius: 6px;
  749. }
  750. .hl-title {
  751. font-size: 12px;
  752. font-weight: 700;
  753. color: var(--brand, #1552D0);
  754. margin-bottom: 6px;
  755. display: flex;
  756. align-items: center;
  757. gap: 4px;
  758. }
  759. .hl-title::before {
  760. content: '▸';
  761. color: var(--brand, #1552D0);
  762. }
  763. .hl-list {
  764. margin: 0;
  765. padding: 0 0 0 16px;
  766. font-size: 12px;
  767. color: var(--ink, #1A2438);
  768. line-height: 1.7;
  769. }
  770. .hl-list li {
  771. list-style: disc;
  772. }
  773. /* === 表格 === */
  774. .table-wrap {
  775. overflow-x: auto;
  776. }
  777. .score-tbl {
  778. width: 100%;
  779. border-collapse: collapse;
  780. font-size: 12px;
  781. }
  782. .score-tbl th,
  783. .score-tbl td {
  784. padding: 9px 10px;
  785. border-bottom: 1px solid var(--line, #E4E9F2);
  786. text-align: left;
  787. vertical-align: top;
  788. }
  789. .score-tbl th {
  790. background: #F5F7FB;
  791. color: var(--mut, #6B7A96);
  792. font-weight: 600;
  793. font-size: 11.5px;
  794. }
  795. .score-tbl td.num,
  796. .score-tbl th.num {
  797. text-align: right;
  798. font-variant-numeric: tabular-nums;
  799. white-space: nowrap;
  800. }
  801. .score-tbl td.def {
  802. color: var(--mut, #6B7A96);
  803. font-size: 11.5px;
  804. max-width: 280px;
  805. line-height: 1.5;
  806. }
  807. .score-tbl td.rule {
  808. color: var(--mut, #6B7A96);
  809. font-size: 11.5px;
  810. max-width: 260px;
  811. line-height: 1.5;
  812. }
  813. .score-tbl tbody tr:hover {
  814. background: #FAFBFE;
  815. }
  816. .cat-pill {
  817. display: inline-block;
  818. padding: 2px 8px;
  819. border-radius: 9px;
  820. font-size: 11px;
  821. font-weight: 600;
  822. white-space: nowrap;
  823. }
  824. .cat-pill.macro { background: #EAF1FF; color: #1552D0; }
  825. .cat-pill.profit { background: #EAF1FF; color: #0E3A96; }
  826. .cat-pill.momentum { background: #FDEAEA; color: #E13B3B; }
  827. .score-pill {
  828. display: inline-block;
  829. min-width: 28px;
  830. padding: 2px 8px;
  831. border-radius: 4px;
  832. font-size: 11.5px;
  833. font-weight: 700;
  834. text-align: center;
  835. font-variant-numeric: tabular-nums;
  836. }
  837. .score-pill.bear { background: #FDEAEA; color: #E13B3B; }
  838. .score-pill.mild-bear { background: #FFE9D6; color: #E8912D; }
  839. .score-pill.neutral { background: #EEF2F7; color: #6B7A96; }
  840. .score-pill.mild-bull { background: #E4F5EE; color: #189A65; }
  841. .score-pill.bull { background: #FDEAEA; color: #E13B3B; }
  842. .delta {
  843. display: inline-block;
  844. font-weight: 600;
  845. font-variant-numeric: tabular-nums;
  846. }
  847. .delta.up { color: #189A65; }
  848. .delta.down { color: #E13B3B; }
  849. .delta.flat { color: var(--mut, #6B7A96); }
  850. .band-tag {
  851. display: inline-block;
  852. padding: 2px 8px;
  853. border-radius: 4px;
  854. font-size: 11px;
  855. font-weight: 600;
  856. white-space: nowrap;
  857. }
  858. .band-tag.bear { background: #FDEAEA; color: #E13B3B; }
  859. .band-tag.mild-bear { background: #FFE9D6; color: #E8912D; }
  860. .band-tag.neutral { background: #EEF2F7; color: #6B7A96; }
  861. .band-tag.mild-bull { background: #E4F5EE; color: #189A65; }
  862. .band-tag.bull { background: #FDEAEA; color: #E13B3B; }
  863. /* === 类别分组卡片 === */
  864. .cat-grid {
  865. display: grid;
  866. grid-template-columns: repeat(2, minmax(0, 1fr));
  867. gap: 14px;
  868. }
  869. .cat-card {
  870. background: #fff;
  871. border: 1px solid var(--line, #E4E9F2);
  872. border-top: 3px solid var(--brand);
  873. border-radius: 8px;
  874. overflow: hidden;
  875. display: flex;
  876. flex-direction: column;
  877. }
  878. .cat-head {
  879. padding: 12px 16px;
  880. display: flex;
  881. justify-content: space-between;
  882. align-items: center;
  883. cursor: pointer;
  884. user-select: none;
  885. transition: 0.15s;
  886. }
  887. .cat-head:hover { background: #FAFBFE; }
  888. .cat-title {
  889. display: flex;
  890. align-items: center;
  891. gap: 8px;
  892. }
  893. .cat-dot {
  894. width: 8px;
  895. height: 8px;
  896. border-radius: 50%;
  897. display: inline-block;
  898. }
  899. .cat-name {
  900. font-size: 14px;
  901. font-weight: 600;
  902. color: var(--ink, #1A2438);
  903. }
  904. .cat-weight {
  905. font-size: 11px;
  906. color: var(--mut, #6B7A96);
  907. background: #F2F5FA;
  908. padding: 1px 7px;
  909. border-radius: 8px;
  910. font-weight: 600;
  911. }
  912. .cat-score {
  913. display: flex;
  914. align-items: center;
  915. gap: 10px;
  916. }
  917. .cat-score-val {
  918. font-size: 18px;
  919. font-weight: 700;
  920. font-variant-numeric: tabular-nums;
  921. letter-spacing: -0.5px;
  922. }
  923. .cat-score-delta {
  924. font-size: 11px;
  925. color: var(--mut, #6B7A96);
  926. font-variant-numeric: tabular-nums;
  927. min-width: 50px;
  928. text-align: right;
  929. }
  930. .cat-score-delta .arrow.up { color: #189A65; }
  931. .cat-score-delta .arrow.down { color: #E13B3B; }
  932. .cat-score-delta .arrow.flat { color: var(--mut, #6B7A96); }
  933. .cat-toggle {
  934. font-size: 11px;
  935. color: var(--mut, #6B7A96);
  936. padding-left: 10px;
  937. border-left: 1px dashed var(--line);
  938. }
  939. .cat-body {
  940. border-top: 1px solid var(--line, #E4E9F2);
  941. padding: 4px 16px 8px;
  942. }
  943. .factor-row {
  944. display: flex;
  945. align-items: center;
  946. gap: 14px;
  947. padding: 11px 0;
  948. border-bottom: 1px dashed var(--line, #E4E9F2);
  949. }
  950. .factor-row:last-child { border-bottom: none; }
  951. .factor-meta { flex: 1; min-width: 0; }
  952. .factor-name {
  953. display: flex;
  954. align-items: center;
  955. gap: 8px;
  956. margin-bottom: 3px;
  957. }
  958. .factor-name-t {
  959. font-size: 12.5px;
  960. font-weight: 600;
  961. color: var(--ink, #1A2438);
  962. }
  963. .factor-w {
  964. font-size: 10.5px;
  965. color: var(--mut, #6B7A96);
  966. background: #F2F5FA;
  967. padding: 1px 6px;
  968. border-radius: 8px;
  969. font-weight: 600;
  970. }
  971. .factor-def {
  972. font-size: 11.5px;
  973. color: var(--mut, #6B7A96);
  974. line-height: 1.5;
  975. margin-bottom: 2px;
  976. }
  977. .factor-rule {
  978. font-size: 11px;
  979. color: #98A5BD;
  980. line-height: 1.4;
  981. }
  982. .factor-score {
  983. display: flex;
  984. align-items: center;
  985. gap: 10px;
  986. flex: none;
  987. }
  988. .factor-cur {
  989. min-width: 36px;
  990. height: 36px;
  991. border-radius: 6px;
  992. display: inline-grid;
  993. place-items: center;
  994. font-size: 16px;
  995. font-weight: 700;
  996. font-variant-numeric: tabular-nums;
  997. border: 1px solid currentColor;
  998. }
  999. .factor-cur.bear { color: #E13B3B; background: #FDEAEA; }
  1000. .factor-cur.mild-bear { color: #E8912D; background: #FFE9D6; }
  1001. .factor-cur.neutral { color: #6B7A96; background: #EEF2F7; }
  1002. .factor-cur.mild-bull { color: #189A65; background: #E4F5EE; }
  1003. .factor-cur.bull { color: #E13B3B; background: #FDEAEA; }
  1004. .factor-prev {
  1005. font-size: 11px;
  1006. color: var(--mut, #6B7A96);
  1007. font-variant-numeric: tabular-nums;
  1008. min-width: 24px;
  1009. text-align: center;
  1010. }
  1011. .factor-bar {
  1012. width: 80px;
  1013. height: 6px;
  1014. background: #F2F5FA;
  1015. border-radius: 3px;
  1016. overflow: hidden;
  1017. }
  1018. .factor-bar-fill {
  1019. height: 100%;
  1020. border-radius: 3px;
  1021. transition: width 0.6s;
  1022. }
  1023. .footer {
  1024. padding: 18px 24px 12px;
  1025. color: #98A5BD;
  1026. font-size: 11.5px;
  1027. text-align: center;
  1028. border-top: 1px solid var(--line, #E4E9F2);
  1029. background: #FAFBFE;
  1030. margin-top: 14px;
  1031. border-radius: 8px;
  1032. }
  1033. @media (max-width: 1280px) {
  1034. .kpi-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  1035. .kpi-card.hero { grid-column: span 2; }
  1036. }
  1037. @media (max-width: 980px) {
  1038. .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  1039. .kpi-card.hero { grid-column: span 2; }
  1040. .card-grid.cols-2-1,
  1041. .card-grid.cols-2-1-1,
  1042. .card-grid.cols-1-2,
  1043. .card-grid.cols-1-1,
  1044. .card-grid.cols-2 { grid-template-columns: 1fr; }
  1045. .cat-grid { grid-template-columns: 1fr; }
  1046. }
  1047. </style>