HomeCardTilesView.vue 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. <template>
  2. <div
  3. v-if="visibleCells.length > 0"
  4. class="tiles-view"
  5. :class="{ 'tiles-view--dense': props.cells.length > 4 }"
  6. >
  7. <div
  8. v-for="cell in visibleCells"
  9. :key="cell.key"
  10. class="tile"
  11. :class="{
  12. 'tile--good': cell.trendTone === 'good',
  13. 'tile--warn': cell.trendTone === 'warn',
  14. 'tile--bad': cell.trendTone === 'bad',
  15. 'tile--stale': cell.stale,
  16. }"
  17. >
  18. <span class="tile__accent" aria-hidden="true"></span>
  19. <div class="tile__body">
  20. <el-tooltip
  21. v-if="cell.formulaText"
  22. placement="top"
  23. :show-after="200"
  24. :content="cell.formulaText"
  25. >
  26. <div class="tile__label tile__label--formula">{{ cell.label }}</div>
  27. </el-tooltip>
  28. <div v-else class="tile__label">{{ cell.label }}</div>
  29. <div class="tile__target">
  30. 目标值: <span class="tile__target-val">{{ cell.targetDisplay || '—' }}</span>
  31. </div>
  32. <div class="tile__main">
  33. <span class="tile__value" :class="{ 'tile__value--status': cell.valueKind === 'status' }">{{ cell.value }}</span>
  34. <span v-if="cell.unit" class="tile__unit">{{ cell.unit }}</span>
  35. </div>
  36. <div v-if="cell.asOfText" class="tile__asof" :title="cell.staleHint || cell.asOfText">{{ cell.asOfText }}</div>
  37. <div v-if="cell.latestText" class="tile__asof">{{ cell.latestText }}</div>
  38. <div v-if="cell.policyText" class="tile__asof">{{ cell.policyText }}</div>
  39. </div>
  40. </div>
  41. <div v-if="overflowCount > 0" class="tiles-view__overflow" :title="`还有 ${overflowCount} 个指标未显示`">
  42. +{{ overflowCount }}
  43. </div>
  44. </div>
  45. <div v-else class="home-kpi-card__empty">
  46. <span v-if="loading">加载中…</span>
  47. <span v-else>暂无指标配置,点击卡片进入模块后到"运营指标建模"配置 L1 指标</span>
  48. </div>
  49. </template>
  50. <script setup>
  51. import { computed } from 'vue'
  52. const props = defineProps({
  53. moduleCode: { type: String, required: true },
  54. cells: { type: Array, required: true },
  55. loading: { type: Boolean, default: false },
  56. })
  57. /**
  58. * 上限与 card_grid 对齐:<=4 走 2x2 大卡;>4 自动进 dense(2x4 = 8 格),
  59. * 超出部分显示 +N 徽标,避免挤压与溢出。
  60. */
  61. const MAX_DENSE = 8
  62. const MAX_NORMAL = 4
  63. const visibleCells = computed(() => {
  64. const arr = Array.isArray(props.cells) ? props.cells : []
  65. const cap = arr.length > MAX_NORMAL ? MAX_DENSE : MAX_NORMAL
  66. return arr.slice(0, cap)
  67. })
  68. const overflowCount = computed(() => {
  69. const arr = Array.isArray(props.cells) ? props.cells : []
  70. const cap = arr.length > MAX_NORMAL ? MAX_DENSE : MAX_NORMAL
  71. return Math.max(0, arr.length - cap)
  72. })
  73. </script>
  74. <style scoped>
  75. .tiles-view {
  76. position: relative;
  77. flex: 1;
  78. min-height: 0;
  79. display: grid;
  80. grid-template-columns: 1fr 1fr;
  81. grid-template-rows: repeat(2, minmax(0, 1fr));
  82. grid-auto-rows: 0;
  83. gap: 10px;
  84. align-content: stretch;
  85. overflow: hidden;
  86. }
  87. .tiles-view--dense {
  88. grid-template-columns: repeat(4, minmax(0, 1fr));
  89. grid-template-rows: repeat(2, minmax(0, 1fr));
  90. gap: 8px;
  91. }
  92. .tile {
  93. position: relative;
  94. display: flex;
  95. align-items: stretch;
  96. min-height: 0;
  97. min-width: 0;
  98. background: rgba(15, 23, 42, 0.55);
  99. border: 1px solid rgba(51, 65, 85, 0.45);
  100. border-radius: 8px;
  101. overflow: hidden;
  102. /* 容器查询单位 cqi 让大数字字号随 tile 宽度自适应,避免窄 tile 下数字溢出压到左侧标签 */
  103. container-type: inline-size;
  104. --tile-accent: #64748b;
  105. }
  106. .tile--good {
  107. --tile-accent: #22c55e;
  108. }
  109. .tile--warn {
  110. --tile-accent: #eab308;
  111. }
  112. .tile--bad {
  113. --tile-accent: #ef4444;
  114. }
  115. .tile__accent {
  116. flex: 0 0 4px;
  117. background: var(--tile-accent);
  118. box-shadow: 0 0 6px color-mix(in srgb, var(--tile-accent) 55%, transparent);
  119. }
  120. .tile__body {
  121. flex: 1;
  122. min-width: 0;
  123. min-height: 0;
  124. display: flex;
  125. flex-direction: column;
  126. align-items: center;
  127. justify-content: center;
  128. gap: 4px;
  129. padding: 10px 10px 12px;
  130. text-align: center;
  131. }
  132. .tile__label {
  133. font-size: 12px;
  134. font-weight: 500;
  135. color: #cbd5e1;
  136. line-height: 1.25;
  137. max-width: 100%;
  138. white-space: nowrap;
  139. text-overflow: ellipsis;
  140. overflow: hidden;
  141. }
  142. .tile__label--formula {
  143. cursor: help;
  144. border-bottom: 1px dashed rgba(148, 163, 184, 0.55);
  145. padding-bottom: 1px;
  146. }
  147. .tile__target {
  148. font-size: 11px;
  149. color: #94a3b8;
  150. line-height: 1.25;
  151. max-width: 100%;
  152. white-space: nowrap;
  153. text-overflow: ellipsis;
  154. overflow: hidden;
  155. }
  156. .tile__target-val {
  157. color: #cbd5e1;
  158. font-weight: 500;
  159. }
  160. .tile__main {
  161. margin-top: 6px;
  162. display: flex;
  163. align-items: baseline;
  164. justify-content: center;
  165. gap: 3px;
  166. max-width: 100%;
  167. min-width: 0;
  168. overflow: hidden;
  169. }
  170. .tile__value {
  171. /* 18cqi = tile 宽度的 18%,窄 tile 自动缩小;垂直布局无横向挤压,允许更大字号 */
  172. font-size: clamp(18px, 18cqi, 30px);
  173. font-weight: 800;
  174. color: #ffffff;
  175. line-height: 1.05;
  176. letter-spacing: 0.02em;
  177. white-space: nowrap;
  178. }
  179. .tile__value.tile__value--status {
  180. font-size: 11px;
  181. white-space: nowrap;
  182. overflow: hidden;
  183. text-overflow: ellipsis;
  184. max-width: 100%;
  185. }
  186. .tile__unit {
  187. font-size: clamp(10px, 7cqi, 13px);
  188. color: #e2e8f0;
  189. font-weight: 500;
  190. white-space: nowrap;
  191. }
  192. .tile__asof {
  193. font-size: 10px;
  194. color: #94a3b8;
  195. line-height: 1.3;
  196. }
  197. .tile--stale {
  198. opacity: 0.55;
  199. }
  200. .tiles-view--dense .tile__body {
  201. gap: 2px;
  202. padding: 6px 8px 8px;
  203. }
  204. .tiles-view--dense .tile__value {
  205. font-size: clamp(14px, 15cqi, 20px);
  206. }
  207. .tiles-view--dense .tile__unit {
  208. font-size: clamp(9px, 6cqi, 11px);
  209. }
  210. .tiles-view--dense .tile__label {
  211. font-size: 11px;
  212. }
  213. .tiles-view--dense .tile__target {
  214. font-size: 10px;
  215. }
  216. .tiles-view--dense .tile__main {
  217. margin-top: 2px;
  218. }
  219. .tiles-view__overflow {
  220. position: absolute;
  221. right: 4px;
  222. bottom: 2px;
  223. font-size: 10px;
  224. font-weight: 700;
  225. color: #93c5fd;
  226. background: rgba(15, 23, 42, 0.75);
  227. border: 1px solid rgba(96, 165, 250, 0.55);
  228. border-radius: 10px;
  229. padding: 1px 6px;
  230. pointer-events: none;
  231. line-height: 1.2;
  232. }
  233. .home-kpi-card__empty {
  234. flex: 1;
  235. display: flex;
  236. align-items: center;
  237. justify-content: center;
  238. color: #94a3b8;
  239. font-size: 11px;
  240. padding: 8px;
  241. text-align: center;
  242. line-height: 1.4;
  243. }
  244. </style>