xsh_1997 hace 2 meses
padre
commit
d68aa13e3b

BIN
doc/大屏/畜牧资源/畜牧资源.png


+ 15 - 0
ruoyi-screen/package-lock.json

@@ -10,6 +10,7 @@
10 10
       "dependencies": {
11 11
         "axios": "^1.16.1",
12 12
         "echarts": "^5.5.1",
13
+        "echarts-wordcloud": "^2.1.0",
13 14
         "vue": "^3.5.13",
14 15
         "vue-router": "^4.6.4"
15 16
       },
@@ -614,6 +615,14 @@
614 615
         "zrender": "5.6.1"
615 616
       }
616 617
     },
618
+    "node_modules/echarts-wordcloud": {
619
+      "version": "2.1.0",
620
+      "resolved": "https://registry.npmmirror.com/echarts-wordcloud/-/echarts-wordcloud-2.1.0.tgz",
621
+      "integrity": "sha512-Kt1JmbcROgb+3IMI48KZECK2AP5lG6bSsOEs+AsuwaWJxQom31RTNd6NFYI01E/YaI1PFZeueaupjlmzSQasjQ==",
622
+      "peerDependencies": {
623
+        "echarts": "^5.0.1"
624
+      }
625
+    },
617 626
     "node_modules/entities": {
618 627
       "version": "7.0.1",
619 628
       "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
@@ -1456,6 +1465,12 @@
1456 1465
         "zrender": "5.6.1"
1457 1466
       }
1458 1467
     },
1468
+    "echarts-wordcloud": {
1469
+      "version": "2.1.0",
1470
+      "resolved": "https://registry.npmmirror.com/echarts-wordcloud/-/echarts-wordcloud-2.1.0.tgz",
1471
+      "integrity": "sha512-Kt1JmbcROgb+3IMI48KZECK2AP5lG6bSsOEs+AsuwaWJxQom31RTNd6NFYI01E/YaI1PFZeueaupjlmzSQasjQ==",
1472
+      "requires": {}
1473
+    },
1459 1474
     "entities": {
1460 1475
       "version": "7.0.1",
1461 1476
       "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",

+ 1 - 0
ruoyi-screen/package.json

@@ -14,6 +14,7 @@
14 14
   "dependencies": {
15 15
     "axios": "^1.16.1",
16 16
     "echarts": "^5.5.1",
17
+    "echarts-wordcloud": "^2.1.0",
17 18
     "vue": "^3.5.13",
18 19
     "vue-router": "^4.6.4"
19 20
   },

+ 10 - 0
ruoyi-screen/src/api/livestockResources.js

@@ -0,0 +1,10 @@
1
+import request from '@/utils/request'
2
+
3
+/** 畜牧资源看板主数据 GET /bigScreen/livestockResource/dashboard */
4
+export function getLivestockResourceDashboard(statYear) {
5
+  return request({
6
+    url: '/bigScreen/livestockResource/dashboard',
7
+    method: 'get',
8
+    params: { statYear }
9
+  })
10
+}

BIN
ruoyi-screen/src/assets/live/live.png


+ 14 - 5
ruoyi-screen/src/components/ScreenChart.vue

@@ -1,6 +1,7 @@
1 1
 <script setup>
2
-import { onMounted, onUnmounted, ref, watch } from "vue"
2
+import { nextTick, onMounted, onUnmounted, ref, watch } from "vue"
3 3
 import * as echarts from "echarts"
4
+import "echarts-wordcloud"
4 5
 
5 6
 const props = defineProps({
6 7
   /** 与 ECharts 官方 option 一致,由父组件传入 */
@@ -14,6 +15,15 @@ const chartRef = ref(null)
14 15
 let chartInstance = null
15 16
 let resizeObserver = null
16 17
 
18
+/** 写入配置并强制整表替换,避免从「暂无数据」切到真实图表时残留 title */
19
+function applyOption(opt) {
20
+  if (!chartInstance || !opt) {
21
+    return
22
+  }
23
+  chartInstance.setOption(opt, { notMerge: true })
24
+  chartInstance.resize()
25
+}
26
+
17 27
 /** 在挂载点创建实例并塞入配置 */
18 28
 function initChart() {
19 29
   if (!chartRef.value) {
@@ -23,7 +33,7 @@ function initChart() {
23 33
     chartInstance.dispose()
24 34
   }
25 35
   chartInstance = echarts.init(chartRef.value)
26
-  chartInstance.setOption(props.option)
36
+  applyOption(props.option)
27 37
 }
28 38
 
29 39
 /** 外层 div 尺寸变化时重绘,避免缩放过后面糊或留白 */
@@ -42,9 +52,8 @@ onMounted(() => {
42 52
 watch(
43 53
   () => props.option,
44 54
   (opt) => {
45
-    if (chartInstance && opt) {
46
-      chartInstance.setOption(opt, true)
47
-    }
55
+    applyOption(opt)
56
+    nextTick(() => resize())
48 57
   },
49 58
   { deep: true }
50 59
 )

+ 460 - 0
ruoyi-screen/src/views/livestockResources/chartOptions.js

@@ -0,0 +1,460 @@
1
+/** 畜牧资源大屏 ECharts 配置(对齐 doc/大屏/畜牧资源 技术方案) */
2
+import 'echarts-wordcloud'
3
+
4
+const AXIS_LABEL = { color: '#9fb0c3', fontSize: 10 }
5
+const AXIS_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.35)' } }
6
+const SPLIT_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.12)' } }
7
+const GRID = { left: 40, right: 16, top: 36, bottom: 28, containLabel: true }
8
+
9
+const PIE_COLORS = ['#5ef0c8', '#ecd27b', '#6eb5ff', '#0f8f72', '#8a9fb0']
10
+const BAR_COLORS = ['#5ef0c8', '#ecd27b', '#6eb5ff', '#98e9aa']
11
+const WORD_COLORS = ['#5ef0c8', '#98e9aa', '#ecd27b', '#6eb5ff', '#a8d4c8', '#0f8f72']
12
+
13
+function emptyOption(text = '暂无数据') {
14
+  return {
15
+    title: {
16
+      text,
17
+      left: 'center',
18
+      top: 'middle',
19
+      textStyle: { color: '#6a9f90', fontSize: 13, fontWeight: 'normal' }
20
+    }
21
+  }
22
+}
23
+
24
+function formatShortDate(dateStr) {
25
+  if (!dateStr) {
26
+    return ''
27
+  }
28
+  const parts = String(dateStr).split('-')
29
+  if (parts.length >= 3) {
30
+    return `${parts[1]}-${parts[2]}`
31
+  }
32
+  return dateStr
33
+}
34
+
35
+function toNum(val) {
36
+  const n = Number(val)
37
+  return Number.isNaN(n) ? 0 : n
38
+}
39
+
40
+function chartBase() {
41
+  return { title: { show: false } }
42
+}
43
+
44
+/** 活跃度趋势 — 7 日会话数 + 提问量堆叠面积图 */
45
+export function buildActivityTrendOption(activityTrend) {
46
+  const series = activityTrend?.dailySeries || []
47
+  if (!series.length) {
48
+    return emptyOption('暂无活跃数据')
49
+  }
50
+  const dates = series.map((d) => formatShortDate(d.date))
51
+  const sessions = series.map((d) => toNum(d.sessionCount))
52
+  const questions = series.map((d) => toNum(d.questionCount))
53
+  return {
54
+    ...chartBase(),
55
+    color: ['#5ef0c8', '#ecd27b'],
56
+    tooltip: { trigger: 'axis' },
57
+    legend: {
58
+      top: 0,
59
+      right: 8,
60
+      textStyle: { color: '#a8d4c8', fontSize: 10 },
61
+      itemWidth: 12,
62
+      itemHeight: 8
63
+    },
64
+    grid: { ...GRID, top: 28, bottom: 24 },
65
+    xAxis: {
66
+      type: 'category',
67
+      boundaryGap: false,
68
+      data: dates,
69
+      axisLabel: { ...AXIS_LABEL, fontSize: 9 },
70
+      axisLine: AXIS_LINE
71
+    },
72
+    yAxis: {
73
+      type: 'value',
74
+      minInterval: 1,
75
+      axisLabel: AXIS_LABEL,
76
+      axisLine: AXIS_LINE,
77
+      splitLine: SPLIT_LINE
78
+    },
79
+    series: [
80
+      {
81
+        name: '会话数',
82
+        type: 'line',
83
+        stack: 'total',
84
+        smooth: true,
85
+        symbol: 'circle',
86
+        symbolSize: 4,
87
+        areaStyle: { color: 'rgba(94, 240, 200, 0.35)' },
88
+        data: sessions
89
+      },
90
+      {
91
+        name: '提问量',
92
+        type: 'line',
93
+        stack: 'total',
94
+        smooth: true,
95
+        symbol: 'circle',
96
+        symbolSize: 4,
97
+        areaStyle: { color: 'rgba(236, 210, 123, 0.35)' },
98
+        data: questions
99
+      }
100
+    ]
101
+  }
102
+}
103
+
104
+/** 用户结构 — 新老用户对比柱图 */
105
+export function buildUserStructureOption(userStructure) {
106
+  if (!userStructure) {
107
+    return emptyOption('暂无用户数据')
108
+  }
109
+  const newCount = toNum(userStructure.newUserCount)
110
+  const oldCount = toNum(userStructure.oldUserCount)
111
+  // if (newCount === 0 && oldCount === 0) {
112
+  //   return emptyOption('暂无用户数据')
113
+  // }
114
+  const newRatio = userStructure?.newUserRatio != null ? Number(userStructure.newUserRatio).toFixed(1) : null
115
+  const oldRatio = userStructure?.oldUserRatio != null ? Number(userStructure.oldUserRatio).toFixed(1) : null
116
+  return {
117
+    ...chartBase(),
118
+    color: ['#5ef0c8', '#ecd27b'],
119
+    tooltip: {
120
+      trigger: 'axis',
121
+      axisPointer: { type: 'shadow' },
122
+      formatter: (params) => {
123
+        const p = params[0]
124
+        const ratio = p.dataIndex === 0 ? newRatio : oldRatio
125
+        const ratioText = ratio != null ? ` (${ratio}%)` : ''
126
+        return `${p.name}<br/>${p.value} 人${ratioText}`
127
+      }
128
+    },
129
+    grid: { ...GRID, top: 16, bottom: 24 },
130
+    xAxis: {
131
+      type: 'category',
132
+      data: ['新用户', '老用户'],
133
+      axisLabel: { ...AXIS_LABEL, fontSize: 10 },
134
+      axisLine: AXIS_LINE
135
+    },
136
+    yAxis: {
137
+      type: 'value',
138
+      minInterval: 1,
139
+      axisLabel: AXIS_LABEL,
140
+      axisLine: AXIS_LINE,
141
+      splitLine: SPLIT_LINE
142
+    },
143
+    series: [
144
+      {
145
+        type: 'bar',
146
+        barMaxWidth: 48,
147
+        itemStyle: {
148
+          borderRadius: [4, 4, 0, 0],
149
+          color: (params) => BAR_COLORS[params.dataIndex % BAR_COLORS.length]
150
+        },
151
+        data: [newCount, oldCount]
152
+      }
153
+    ]
154
+  }
155
+}
156
+
157
+/** 提问分类占比 — 饼图 */
158
+export function buildCategoryShareOption(categoryShare) {
159
+  const items = categoryShare?.items || []
160
+  const data = items
161
+    .map((item, i) => ({
162
+      name: item.categoryName || item.categoryCode,
163
+      value: item.count ?? 0,
164
+      itemStyle: { color: PIE_COLORS[i % PIE_COLORS.length] }
165
+    }))
166
+  if (!data.length) {
167
+    return emptyOption('暂无分类数据')
168
+  }
169
+  return {
170
+    ...chartBase(),
171
+    tooltip: {
172
+      trigger: 'item',
173
+      formatter: (p) => {
174
+        const row = items.find((it) => (it.categoryName || it.categoryCode) === p.name)
175
+        const ratio = row?.ratio != null ? `${Number(row.ratio).toFixed(1)}%` : `${p.percent}%`
176
+        return `${p.name}<br/>${p.value} 条 (${ratio})`
177
+      }
178
+    },
179
+    series: [
180
+      {
181
+        type: 'pie',
182
+        radius: ['0%', '62%'],
183
+        center: ['50%', '54%'],
184
+        label: {
185
+          color: '#e8eef5',
186
+          fontSize: 9,
187
+          formatter: (p) => {
188
+            const row = items.find((it) => (it.categoryName || it.categoryCode) === p.name)
189
+            const ratio = row?.ratio != null ? Number(row.ratio).toFixed(1) : p.percent
190
+            return `${p.name}\n${ratio}%`
191
+          }
192
+        },
193
+        labelLine: { length: 6, length2: 4 },
194
+        data
195
+      }
196
+    ]
197
+  }
198
+}
199
+
200
+/** 模型调用分析 — 分类条数柱图 */
201
+export function buildModelCallOption(modelCallAnalysis) {
202
+  const list = modelCallAnalysis || []
203
+  if (!list.length) {
204
+    return emptyOption('暂无调用数据')
205
+  }
206
+  const names = list.map((r) => r.categoryName || r.categoryCode || '')
207
+  const values = list.map((r) => toNum(r.count))
208
+  return {
209
+    ...chartBase(),
210
+    color: BAR_COLORS,
211
+    tooltip: {
212
+      trigger: 'axis',
213
+      axisPointer: { type: 'shadow' }
214
+    },
215
+    grid: { ...GRID, top: 12, bottom: 28 },
216
+    xAxis: {
217
+      type: 'category',
218
+      data: names,
219
+      axisLabel: {
220
+        ...AXIS_LABEL,
221
+        interval: 0,
222
+        fontSize: 9,
223
+        rotate: names.some((n) => n.length > 4) ? 20 : 0
224
+      },
225
+      axisLine: AXIS_LINE
226
+    },
227
+    yAxis: {
228
+      type: 'value',
229
+      minInterval: 1,
230
+      axisLabel: AXIS_LABEL,
231
+      axisLine: AXIS_LINE,
232
+      splitLine: SPLIT_LINE
233
+    },
234
+    series: [
235
+      {
236
+        type: 'bar',
237
+        barMaxWidth: 28,
238
+        itemStyle: {
239
+          borderRadius: [4, 4, 0, 0],
240
+          color: (params) => BAR_COLORS[params.dataIndex % BAR_COLORS.length]
241
+        },
242
+        data: values
243
+      }
244
+    ]
245
+  }
246
+}
247
+
248
+/** 分时使用热度 — 会话柱 + 提问量折线(24 小时) */
249
+export function buildHourlyHeatOption(hourlyHeat) {
250
+  const series = hourlyHeat?.hourlySeries || []
251
+  if (!series.length) {
252
+    return emptyOption('暂无热度数据')
253
+  }
254
+  const labels = series.map((d) => d.bucketStart || '')
255
+  const sessions = series.map((d) => toNum(d.sessionCount))
256
+  const questions = series.map((d) => toNum(d.questionCount))
257
+  return {
258
+    ...chartBase(),
259
+    color: ['#5ef0c8', '#ecd27b'],
260
+    tooltip: { trigger: 'axis' },
261
+    legend: {
262
+      top: 0,
263
+      right: 8,
264
+      textStyle: { color: '#a8d4c8', fontSize: 10 },
265
+      itemWidth: 12,
266
+      itemHeight: 8
267
+    },
268
+    grid: { ...GRID, top: 28, bottom: 24, left: 36, right: 36 },
269
+    xAxis: {
270
+      type: 'category',
271
+      data: labels,
272
+      axisLabel: {
273
+        ...AXIS_LABEL,
274
+        fontSize: 8,
275
+        interval: (idx) => idx % 4 === 0
276
+      },
277
+      axisLine: AXIS_LINE
278
+    },
279
+    yAxis: [
280
+      {
281
+        type: 'value',
282
+        minInterval: 1,
283
+        axisLabel: { ...AXIS_LABEL, fontSize: 9 },
284
+        axisLine: AXIS_LINE,
285
+        splitLine: SPLIT_LINE
286
+      },
287
+      {
288
+        type: 'value',
289
+        minInterval: 1,
290
+        axisLabel: { ...AXIS_LABEL, fontSize: 9 },
291
+        axisLine: AXIS_LINE,
292
+        splitLine: { show: false }
293
+      }
294
+    ],
295
+    series: [
296
+      {
297
+        name: '会话数',
298
+        type: 'bar',
299
+        barMaxWidth: 8,
300
+        itemStyle: { color: 'rgba(94, 240, 200, 0.75)', borderRadius: [2, 2, 0, 0] },
301
+        data: sessions
302
+      },
303
+      {
304
+        name: '提问量',
305
+        type: 'line',
306
+        yAxisIndex: 1,
307
+        smooth: true,
308
+        symbol: 'none',
309
+        lineStyle: { width: 2, color: '#ecd27b' },
310
+        data: questions
311
+      }
312
+    ]
313
+  }
314
+}
315
+
316
+/** 单次会话提问数量分布 — 环形图 */
317
+export function buildSessionReplyOption(sessionReplyDistribution) {
318
+  const list = sessionReplyDistribution || []
319
+  if (!list.length) {
320
+    return emptyOption('暂无分布数据')
321
+  }
322
+  const data = list
323
+    .map((item, i) => ({
324
+      name: item.bandName || item.bandCode,
325
+      value: toNum(item.sessionCount),
326
+      itemStyle: { color: PIE_COLORS[i % PIE_COLORS.length] }
327
+    }))
328
+  if (!data.length) {
329
+    return emptyOption('暂无分布数据')
330
+  }
331
+  return {
332
+    ...chartBase(),
333
+    tooltip: {
334
+      trigger: 'item',
335
+      formatter: (p) => {
336
+        const row = list.find((it) => (it.bandName || it.bandCode) === p.name)
337
+        const ratio = row?.ratio != null ? `${Number(row.ratio).toFixed(1)}%` : `${p.percent}%`
338
+        return `${p.name}<br/>${p.value} 会话 (${ratio})`
339
+      }
340
+    },
341
+    series: [
342
+      {
343
+        type: 'pie',
344
+        radius: ['42%', '68%'],
345
+        center: ['50%', '54%'],
346
+        label: {
347
+          color: '#e8eef5',
348
+          fontSize: 9,
349
+          formatter: (p) => {
350
+            const row = list.find((it) => (it.bandName || it.bandCode) === p.name)
351
+            const ratio = row?.ratio != null ? Number(row.ratio).toFixed(1) : p.percent
352
+            return `${p.name}\n${ratio}%`
353
+          }
354
+        },
355
+        labelLine: { length: 6, length2: 4 },
356
+        data
357
+      }
358
+    ]
359
+  }
360
+}
361
+
362
+/** 会话状态分布 — 柱图(正常应答 / 调用失败) */
363
+export function buildReplyStatusOption(replyStatusDistribution) {
364
+  const items = replyStatusDistribution?.items || []
365
+  if (!items.length) {
366
+    return emptyOption('暂无状态数据')
367
+  }
368
+  const names = items.map((r) => r.statusName || r.statusCode || '')
369
+  const values = items.map((r) => toNum(r.count))
370
+  return {
371
+    ...chartBase(),
372
+    color: ['#5ef0c8', '#f56c6c'],
373
+    tooltip: {
374
+      trigger: 'axis',
375
+      axisPointer: { type: 'shadow' },
376
+      formatter: (params) => {
377
+        const p = params[0]
378
+        const row = items[p.dataIndex]
379
+        const ratio = row?.ratio != null ? `${Number(row.ratio).toFixed(1)}%` : ''
380
+        return `${p.name}<br/>${p.value} 条${ratio ? ` (${ratio})` : ''}`
381
+      }
382
+    },
383
+    grid: { ...GRID, top: 12, bottom: 28 },
384
+    xAxis: {
385
+      type: 'category',
386
+      data: names,
387
+      axisLabel: { ...AXIS_LABEL, interval: 0, fontSize: 9 },
388
+      axisLine: AXIS_LINE
389
+    },
390
+    yAxis: {
391
+      type: 'value',
392
+      minInterval: 1,
393
+      axisLabel: AXIS_LABEL,
394
+      axisLine: AXIS_LINE,
395
+      splitLine: SPLIT_LINE
396
+    },
397
+    series: [
398
+      {
399
+        type: 'bar',
400
+        barMaxWidth: 36,
401
+        itemStyle: {
402
+          borderRadius: [4, 4, 0, 0],
403
+          color: (params) => (params.dataIndex === 0 ? '#5ef0c8' : '#f56c6c')
404
+        },
405
+        data: values
406
+      }
407
+    ]
408
+  }
409
+}
410
+
411
+/** 高频关键词 — ECharts 词云 */
412
+export function buildWordCloudOption(topKeywords) {
413
+  const list = topKeywords || []
414
+  if (!list.length) {
415
+    return emptyOption('暂无关键词')
416
+  }
417
+  const data = list
418
+    .filter((item) => item.keyword)
419
+    .map((item) => ({
420
+      name: item.keyword,
421
+      value: toNum(item.count)
422
+    }))
423
+  if (!data.length) {
424
+    return emptyOption('暂无关键词')
425
+  }
426
+  return {
427
+    ...chartBase(),
428
+    tooltip: {
429
+      show: true,
430
+      formatter: (p) => `${p.name}: ${p.value}`
431
+    },
432
+    series: [
433
+      {
434
+        type: 'wordCloud',
435
+        shape: 'circle',
436
+        left: 'center',
437
+        top: 'center',
438
+        width: '96%',
439
+        height: '96%',
440
+        sizeRange: [10, 26],
441
+        rotationRange: [-15, 15],
442
+        rotationStep: 15,
443
+        gridSize: 7,
444
+        drawOutOfBound: false,
445
+        textStyle: {
446
+          fontFamily: 'Microsoft YaHei, sans-serif',
447
+          fontWeight: 'bold',
448
+          color: () => WORD_COLORS[Math.floor(Math.random() * WORD_COLORS.length)]
449
+        },
450
+        emphasis: {
451
+          textStyle: {
452
+            shadowBlur: 8,
453
+            shadowColor: 'rgba(94, 240, 200, 0.6)'
454
+          }
455
+        },
456
+        data
457
+      }
458
+    ]
459
+  }
460
+}

+ 466 - 146
ruoyi-screen/src/views/livestockResources/index.vue

@@ -1,30 +1,16 @@
1 1
 <template>
2
-  <div class="screen-page ts-page" :class="{ 'is-loading': loading }">
3
-    <div v-if="loadError" class="ts-error">{{ loadError }}</div>
4
-
5
-    <!-- <div class="ts-year-bar">
6
-      <label class="ts-year-bar__label">统计年份</label>
7
-      <select
8
-        v-model="statYear"
9
-        class="ts-year-bar__select"
10
-        :disabled="loading"
11
-        @change="onYearChange"
12
-      >
13
-        <option v-for="y in availableYears" :key="y" :value="String(y)">{{ y }}年</option>
14
-      </select>
15
-      <span v-if="statDate" class="ts-year-bar__date">统计日 {{ statDate }}</span>
16
-    </div> -->
17
-
18
-    <!-- 左栏:核心数据总览( -->
2
+  <div class="screen-page lr-page" :class="{ 'is-loading': loading }">
3
+    <div v-if="loadError" class="lr-error">{{ loadError }}</div>
4
+
5
+    <!-- 左栏:核心总览 + 活跃/用户/分类/模型 -->
19 6
     <div class="screen-page--home-column">
20 7
       <div class="screen-page--home-column-top">
21 8
         <div class="top_title">核心数据总览 གཙོ་བོའི་གནས་ཚད་བསྡུས་གསལ།</div>
22
-        <div class="ts-overview-grid">
9
+        <div class="lr-overview-grid">
23 10
           <div
24
-            v-for="item in liveStock"
11
+            v-for="item in overviewItems"
25 12
             :key="item.key"
26
-            class="ts-overview-cell"
27
-            :class="{ 'ts-overview-cell--placeholder': item.placeholder }"
13
+            class="lr-overview-cell"
28 14
           >
29 15
             <div class="content_title"><div>{{ item.label }}</div></div>
30 16
             <div class="content_num">
@@ -38,17 +24,45 @@
38 24
       <div class="screen-page--home-column-flex">
39 25
         <div class="screen-page--home-column-flex-item">
40 26
           <div class="flex_title">活跃度趋势 འགྲོ་འདུག་གོམ་སྟངས་རྒྱུན་སྟངས།</div>
41
-          <div class="flex_content flex_content--quote">
42
-           
43
-            <div class="quote-chart">
44
-              
45
-            </div>
27
+          <div class="flex_content">
28
+            <ScreenChart :option="activityTrendOption" />
46 29
           </div>
47 30
         </div>
48 31
         <div class="screen-page--home-column-flex-item">
49 32
           <div class="flex_title">用户结构分析 སྤྱོད་མིའི་གོ་སྐབས་དཔྱད་པ།</div>
50
-          <div class="flex_content">
51
-           
33
+          <div class="flex_content flex_content--user">
34
+            <div class="lr-rings">
35
+              <div class="lr-ring">
36
+                <div class="lr-ring__figure">
37
+                  <div class="lr-ring__outer">
38
+                    <div class="lr-ring__inner">
39
+                      <div class="lr-ring__val">
40
+                        <strong>{{ userStructureDisplay.newCount }}</strong>
41
+                      </div>
42
+                    </div>
43
+                  </div>
44
+                </div>
45
+                <div class="lr-ring__label">新用户人数</div>
46
+                <div v-if="userStructureDisplay.newRatio" class="lr-ring__ratio">
47
+                  占比 {{ userStructureDisplay.newRatio }}
48
+                </div>
49
+              </div>
50
+              <div class="lr-ring">
51
+                <div class="lr-ring__figure">
52
+                  <div class="lr-ring__outer">
53
+                    <div class="lr-ring__inner">
54
+                      <div class="lr-ring__val">
55
+                        <strong>{{ userStructureDisplay.oldCount }}</strong>
56
+                      </div>
57
+                    </div>
58
+                  </div>
59
+                </div>
60
+                <div class="lr-ring__label">老用户人数</div>
61
+                <div v-if="userStructureDisplay.oldRatio" class="lr-ring__ratio">
62
+                  占比 {{ userStructureDisplay.oldRatio }}
63
+                </div>
64
+              </div>
65
+            </div>
52 66
           </div>
53 67
         </div>
54 68
       </div>
@@ -57,49 +71,63 @@
57 71
         <div class="screen-page--home-column-flex-item">
58 72
           <div class="flex_title">提问分类占比 དྲིས་ལན་དབྱེ་རྩིས་བགོ་སྟངས།</div>
59 73
           <div class="flex_content">
60
-          
74
+            <ScreenChart :option="categoryShareOption" />
61 75
           </div>
62 76
         </div>
63 77
         <div class="screen-page--home-column-flex-item">
64 78
           <div class="flex_title">模型调用分析 དཔེ་གཞི་འབྲི་རྩོམ་དཔྱད་པ།</div>
65 79
           <div class="flex_content">
66
-           
80
+            <ScreenChart :option="modelCallOption" />
67 81
           </div>
68 82
         </div>
69 83
       </div>
70 84
     </div>
71 85
 
72
-    <!-- 右栏:农资商城本期不实现,保留占位 -->
86
+    <!-- 右栏:分时热度 + 分布/榜单/词云 -->
73 87
     <div class="screen-page--home-column">
74
-      <div class="screen-page--home-column-right ts-mall-panel">
88
+      <div class="screen-page--home-column-right lr-heat-panel">
75 89
         <div class="top_title">分时使用热度 དུས་སྐོར་སྤྱོད་སྟོབས་ཚད།</div>
76
-        
90
+        <div class="lr-heat-chart">
91
+          <ScreenChart :option="hourlyHeatOption" />
92
+        </div>
77 93
       </div>
94
+
78 95
       <div class="screen-page--home-column-flex">
79 96
         <div class="screen-page--home-column-flex-item">
80 97
           <div class="flex_title">会话提问数量分布 གོ་སྐབས་དྲིས་གྲངས་ཁྱབ་སྤེལ།</div>
81
-          <div class="flex_content flex_content--placeholder">
82
-          
98
+          <div class="flex_content">
99
+            <ScreenChart :option="sessionReplyOption" />
83 100
           </div>
84 101
         </div>
85 102
         <div class="screen-page--home-column-flex-item">
86 103
           <div class="flex_title">会话状态分布 གོ་སྐབས་གནས་སྟངས་ཁྱབ་སྤེལ།</div>
87
-          <div class="flex_content flex_content--placeholder">
88
-          
104
+          <div class="flex_content">
105
+            <ScreenChart :option="replyStatusOption" />
89 106
           </div>
90 107
         </div>
91 108
       </div>
109
+
92 110
       <div class="screen-page--home-column-flex">
93 111
         <div class="screen-page--home-column-flex-item">
94 112
           <div class="flex_title">热门问题榜单 ཡོངས་གྲགས་དྲིས་ལན་གསལ་ཐོ།</div>
95
-          <div class="flex_content flex_content--placeholder">
96
-           
113
+          <div class="flex_content flex_content--list">
114
+            <ul v-if="hotTopics.length" class="lr-hot-list">
115
+              <li v-for="(item, idx) in hotTopics" :key="item.categoryCode + '-' + idx" class="lr-hot-list__row">
116
+                <span class="lr-hot-list__rank" :class="'lr-hot-list__rank--' + (idx + 1)">{{ idx + 1 }}</span>
117
+                <span class="lr-hot-list__name" :title="item.categoryName">{{ item.categoryName }}</span>
118
+                <span class="lr-hot-list__meta">
119
+                  <strong>{{ formatNum(item.count) }}</strong>
120
+                  <em v-if="item.ratio != null">{{ formatRatio(item.ratio) }}%</em>
121
+                </span>
122
+              </li>
123
+            </ul>
124
+            <span v-else class="lr-empty">暂无榜单数据</span>
97 125
           </div>
98 126
         </div>
99 127
         <div class="screen-page--home-column-flex-item">
100 128
           <div class="flex_title">高频关键词 མང་དུ་སྦྱོང་བའི་ཚིག་གཙོ།</div>
101
-          <div class="flex_content flex_content--placeholder">
102
-           
129
+          <div class="flex_content">
130
+            <ScreenChart :option="wordCloudOption" />
103 131
           </div>
104 132
         </div>
105 133
       </div>
@@ -110,19 +138,206 @@
110 138
 <script setup>
111 139
 import { computed, onMounted, ref } from 'vue'
112 140
 import ScreenChart from '@/components/ScreenChart.vue'
141
+import { getLivestockResourceDashboard } from '@/api/livestockResources'
142
+import {
143
+  buildActivityTrendOption,
144
+  buildCategoryShareOption,
145
+  buildHourlyHeatOption,
146
+  buildModelCallOption,
147
+  buildReplyStatusOption,
148
+  buildSessionReplyOption,
149
+  buildWordCloudOption
150
+} from './chartOptions'
113 151
 
114 152
 const loading = ref(false)
115 153
 const loadError = ref('')
116 154
 const statYear = ref(String(new Date().getFullYear()))
117
-const statDate = ref('')
118
-
119
-const liveStock = ref([])
120
-
121 155
 
156
+const overview = ref(null)
157
+const activityTrend = ref(null)
158
+const userStructure = ref(null)
159
+const categoryShare = ref(null)
160
+const modelCallAnalysis = ref([])
161
+const hourlyHeat = ref(null)
162
+const sessionReplyDistribution = ref([])
163
+const replyStatusDistribution = ref(null)
164
+const hotTopics = ref([])
165
+const topKeywords = ref([])
166
+
167
+const activityTrendOption = computed(() => buildActivityTrendOption(activityTrend.value))
168
+const categoryShareOption = computed(() => buildCategoryShareOption(categoryShare.value))
169
+const modelCallOption = computed(() => buildModelCallOption(modelCallAnalysis.value))
170
+const hourlyHeatOption = computed(() => buildHourlyHeatOption(hourlyHeat.value))
171
+const sessionReplyOption = computed(() => buildSessionReplyOption(sessionReplyDistribution.value))
172
+const replyStatusOption = computed(() => buildReplyStatusOption(replyStatusDistribution.value))
173
+const wordCloudOption = computed(() => buildWordCloudOption(topKeywords.value))
174
+
175
+/** 核心总览 8 项(布局对齐交易总览,4 列 × 2 行) */
176
+const overviewItems = computed(() => {
177
+  const o = overview.value
178
+  return [
179
+    { key: 'totalUsers', label: '累计使用用户', unit: '人', value: display(o?.totalUsers) },
180
+    { key: 'todayActiveUsers', label: '今日活跃用户', unit: '人', value: display(o?.todayActiveUsers) },
181
+    { key: 'todayNewUsers', label: '日新增用户', unit: '人', value: display(o?.todayNewUsers) },
182
+    { key: 'totalSessions', label: '累计会话数', unit: '次', value: display(o?.totalSessions) },
183
+    { key: 'todaySessions', label: '今日会话数', unit: '次', value: display(o?.todaySessions) },
184
+    { key: 'totalQuestionCount', label: '累计提问量', unit: '次', value: display(o?.totalQuestionCount) },
185
+    { key: 'todayQuestionCount', label: '今日提问量', unit: '次', value: display(o?.todayQuestionCount) },
186
+    {
187
+      key: 'avgResponseSeconds',
188
+      label: '平均响应时长',
189
+      unit: '秒',
190
+      value: displaySeconds(o?.avgResponseSeconds)
191
+    }
192
+  ]
193
+})
194
+
195
+/** 用户结构双圆环:人数 + 占比(对齐共同富裕「数字赋能」圆环样式) */
196
+const userStructureDisplay = computed(() => {
197
+  const u = userStructure.value
198
+  const newRatio = u?.newUserRatio != null ? `${formatRatio(u.newUserRatio)}%` : ''
199
+  const oldRatio = u?.oldUserRatio != null ? `${formatRatio(u.oldUserRatio)}%` : ''
200
+  return {
201
+    newCount: display(u?.newUserCount),
202
+    oldCount: display(u?.oldUserCount),
203
+    newRatio,
204
+    oldRatio
205
+  }
206
+})
207
+
208
+function formatNum(val) {
209
+  if (val === null || val === undefined || val === '') {
210
+    return '0'
211
+  }
212
+  const n = Number(val)
213
+  if (Number.isNaN(n)) {
214
+    return '0'
215
+  }
216
+  return n.toLocaleString('zh-CN')
217
+}
218
+
219
+function display(val) {
220
+  return formatNum(val)
221
+}
222
+
223
+function displaySeconds(val) {
224
+  if (val === null || val === undefined || val === '') {
225
+    return '0'
226
+  }
227
+  const n = Number(val)
228
+  if (Number.isNaN(n)) {
229
+    return '0'
230
+  }
231
+  return n.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 1 })
232
+}
233
+
234
+function formatRatio(val) {
235
+  const n = Number(val)
236
+  if (Number.isNaN(n)) {
237
+    return '0.0'
238
+  }
239
+  return n.toFixed(1)
240
+}
241
+
242
+/** 兼容拦截器返回 AjaxResult,或直接返回 data 本体 */
243
+function resolveDashboard(res) {
244
+  if (!res) {
245
+    return null
246
+  }
247
+  const payload = res.data !== undefined ? res.data : res
248
+  if (payload && payload.data && payload.overview == null && payload.data.overview) {
249
+    return payload.data
250
+  }
251
+  return payload
252
+}
253
+
254
+function normalizeActivityTrend(raw) {
255
+  if (!raw) {
256
+    return null
257
+  }
258
+  if (Array.isArray(raw)) {
259
+    return { dailySeries: raw }
260
+  }
261
+  return raw
262
+}
263
+
264
+function normalizeHourlyHeat(raw) {
265
+  if (!raw) {
266
+    return null
267
+  }
268
+  if (Array.isArray(raw)) {
269
+    return { hourlySeries: raw }
270
+  }
271
+  return raw
272
+}
273
+
274
+function normalizeReplyStatus(raw) {
275
+  if (!raw) {
276
+    return null
277
+  }
278
+  if (Array.isArray(raw)) {
279
+    return { items: raw }
280
+  }
281
+  return raw
282
+}
283
+
284
+function pickHotTopics(data, categoryItems) {
285
+  if (data.hotTopics?.length) {
286
+    return data.hotTopics
287
+  }
288
+  return [...(categoryItems || [])]
289
+    .filter((item) => toNum(item?.count) > 0)
290
+    .sort((a, b) => toNum(b.count) - toNum(a.count))
291
+    .slice(0, 5)
292
+}
293
+
294
+function toNum(val) {
295
+  const n = Number(val)
296
+  return Number.isNaN(n) ? 0 : n
297
+}
298
+
299
+function applyDashboard(data) {
300
+  if (!data) {
301
+    return
302
+  }
303
+  if (data.statYear) {
304
+    statYear.value = String(data.statYear)
305
+  }
306
+  overview.value = data.overview || null
307
+  activityTrend.value = normalizeActivityTrend(data.activityTrend)
308
+  userStructure.value = data.userStructure || null
309
+  categoryShare.value = data.categoryShare || null
310
+  const categoryItems = data.categoryShare?.items || []
311
+  modelCallAnalysis.value = data.modelCallAnalysis?.length
312
+    ? data.modelCallAnalysis
313
+    : categoryItems
314
+  hourlyHeat.value = normalizeHourlyHeat(data.hourlyHeat)
315
+  sessionReplyDistribution.value = data.sessionReplyDistribution || []
316
+  replyStatusDistribution.value = normalizeReplyStatus(data.replyStatusDistribution)
317
+  hotTopics.value = pickHotTopics(data, categoryItems)
318
+  topKeywords.value = data.topKeywords || []
319
+}
320
+
321
+async function fetchDashboard() {
322
+  loading.value = true
323
+  loadError.value = ''
324
+  try {
325
+    const res = await getLivestockResourceDashboard(statYear.value)
326
+    applyDashboard(resolveDashboard(res))
327
+  } catch (e) {
328
+    loadError.value = e?.message || '看板数据加载失败'
329
+  } finally {
330
+    loading.value = false
331
+  }
332
+}
333
+
334
+onMounted(() => {
335
+  fetchDashboard()
336
+})
122 337
 </script>
123 338
 
124 339
 <style scoped>
125
-.ts-page {
340
+.lr-page {
126 341
   position: relative;
127 342
   width: 100%;
128 343
   height: 100%;
@@ -133,11 +348,11 @@ const liveStock = ref([])
133 348
   justify-content: space-between;
134 349
 }
135 350
 
136
-.ts-page.is-loading {
351
+.lr-page.is-loading {
137 352
   opacity: 0.92;
138 353
 }
139 354
 
140
-.ts-error {
355
+.lr-error {
141 356
   position: absolute;
142 357
   top: 4px;
143 358
   left: 50%;
@@ -150,38 +365,6 @@ const liveStock = ref([])
150 365
   border-radius: 4px;
151 366
 }
152 367
 
153
-.ts-year-bar {
154
-  position: absolute;
155
-  top: 8px;
156
-  right: 24px;
157
-  z-index: 5;
158
-  display: flex;
159
-  align-items: center;
160
-  gap: 8px;
161
-}
162
-
163
-.ts-year-bar__label {
164
-  font-size: 12px;
165
-  color: var(--screen-text-secondary, #a8d4c8);
166
-}
167
-
168
-.ts-year-bar__select {
169
-  min-width: 88px;
170
-  height: 28px;
171
-  padding: 0 8px;
172
-  font-size: 13px;
173
-  color: #e8eef5;
174
-  background: rgba(4, 48, 40, 0.85);
175
-  border: 1px solid var(--screen-line-dim, rgba(61, 217, 176, 0.42));
176
-  border-radius: 4px;
177
-  outline: none;
178
-}
179
-
180
-.ts-year-bar__date {
181
-  font-size: 11px;
182
-  color: rgba(168, 212, 200, 0.75);
183
-}
184
-
185 368
 .screen-page--home-column {
186 369
   width: 617px;
187 370
   height: 100%;
@@ -196,35 +379,29 @@ const liveStock = ref([])
196 379
   display: flex;
197 380
   flex-direction: column;
198 381
   box-sizing: border-box;
199
-  background: url('../../assets/sale/sale.png') no-repeat center center;
382
+  background: url('../../assets/live/live.png') no-repeat center center;
200 383
   background-size: 100% 100%;
201 384
 }
202 385
 
203 386
 .screen-page--home-column-right {
204 387
   width: 617px;
205 388
   height: 201px;
389
+  display: flex;
390
+  flex-direction: column;
391
+  box-sizing: border-box;
206 392
   background: url('../../assets/pro/pro.png') no-repeat center center;
207 393
   background-size: 100% 100%;
208 394
 }
209 395
 
210
-.ts-mall-panel {
211
-  display: flex;
212
-  flex-direction: column;
396
+.lr-heat-panel {
397
+  overflow: hidden;
213 398
 }
214 399
 
215
-.ts-placeholder {
400
+.lr-heat-chart {
216 401
   flex: 1;
217
-  display: flex;
218
-  align-items: center;
219
-  justify-content: center;
220
-  padding: 0 24px;
221
-  text-align: center;
222
-  font-size: 14px;
223
-  color: rgba(168, 212, 200, 0.75);
224
-}
225
-
226
-.ts-placeholder--sm {
227
-  font-size: 13px;
402
+  min-height: 0;
403
+  box-sizing: border-box;
404
+  padding: 0 8px 8px;
228 405
 }
229 406
 
230 407
 .screen-page--home-column-flex {
@@ -253,18 +430,19 @@ const liveStock = ref([])
253 430
   flex-shrink: 0;
254 431
 }
255 432
 
256
-.ts-overview-grid {
433
+/* 核心总览:4 列 × 2 行,对齐交易总览样式 */
434
+.lr-overview-grid {
257 435
   flex: 1;
258 436
   min-height: 0;
259 437
   box-sizing: border-box;
260
-  padding: 6px 20px 14px 32px;
438
+  padding: 4px 20px 12px 28px;
261 439
   display: grid;
262
-  grid-template-columns: repeat(3, 1fr);
440
+  grid-template-columns: repeat(4, 1fr);
263 441
   grid-template-rows: repeat(2, 1fr);
264
-  gap: 4px 40px;
442
+  gap: 2px 16px;
265 443
 }
266 444
 
267
-.ts-overview-cell {
445
+.lr-overview-cell {
268 446
   display: flex;
269 447
   flex-direction: column;
270 448
   align-items: flex-end;
@@ -273,29 +451,29 @@ const liveStock = ref([])
273 451
   width: 100%;
274 452
   text-align: right;
275 453
   box-sizing: border-box;
276
-  padding-right: 10px;
454
+  padding-right: 6px;
277 455
 }
278 456
 
279
-.ts-overview-cell .content_title {
457
+.lr-overview-cell .content_title {
280 458
   width: 100%;
281 459
   text-align: right;
282 460
 }
283 461
 
284
-.ts-overview-cell .content_num {
462
+.lr-overview-cell .content_num {
285 463
   width: 100%;
286 464
   display: flex;
287 465
   align-items: baseline;
288 466
   justify-content: flex-end;
289
-  gap: 6px;
290
-  margin: 4px 0 0;
291
-  line-height: 1.3;
467
+  gap: 4px;
468
+  margin: 2px 0 0;
469
+  line-height: 1.2;
292 470
 }
293 471
 
294 472
 .content_num__value {
295 473
   flex: 1;
296 474
   min-width: 0;
297 475
   text-align: right;
298
-  font-size: 24px;
476
+  font-size: 20px;
299 477
   font-weight: 600;
300 478
   background: linear-gradient(to bottom, #98e9aa, #ecd27b);
301 479
   -webkit-background-clip: text;
@@ -305,25 +483,15 @@ const liveStock = ref([])
305 483
 
306 484
 .content_num__unit {
307 485
   flex-shrink: 0;
308
-  font-size: 12px;
486
+  font-size: 11px;
309 487
   color: #a8d4c8;
310 488
   line-height: 1.2;
311
-  text-align: right;
312
-}
313
-
314
-.ts-overview-cell--placeholder .content_num__value {
315
-  font-size: 18px;
316
-  background: none;
317
-  -webkit-background-clip: unset;
318
-  background-clip: unset;
319
-  color: #6a9f90;
320 489
 }
321 490
 
322 491
 .content_title {
323
-
324
-  font-size: 11px;
492
+  font-size: 10px;
325 493
   color: #a8d4c8;
326
-  line-height: 1.3;
494
+  line-height: 1.25;
327 495
 }
328 496
 
329 497
 .flex_title {
@@ -344,63 +512,215 @@ const liveStock = ref([])
344 512
   padding: 5px;
345 513
 }
346 514
 
347
-.flex_content--quote {
515
+.flex_content--user {
348 516
   display: flex;
349
-  flex-direction: column;
350
-  padding: 4px 6px;
517
+  align-items: center;
518
+  justify-content: center;
519
+  padding: 4px 8px 8px;
351 520
 }
352 521
 
353
-.quote-summary {
522
+/* 用户结构双圆环(对齐 commonProsperity 数字赋能 cp-ring) */
523
+.lr-rings {
524
+  width: 100%;
525
+  height: 100%;
354 526
   display: flex;
355 527
   flex-direction: row;
356
-  gap: 6px;
357
-  height: 42px;
358
-  flex-shrink: 0;
528
+  align-items: center;
529
+  justify-content: space-around;
530
+  gap: 8px;
359 531
 }
360 532
 
361
-.quote-summary__item {
533
+.lr-ring {
362 534
   flex: 1;
535
+  min-width: 0;
536
+  max-width: 130px;
363 537
   display: flex;
364 538
   flex-direction: column;
365 539
   align-items: center;
366 540
   justify-content: center;
367
-  background: rgba(4, 48, 40, 0.45);
368
-  border-radius: 4px;
369
-  border: 1px solid rgba(61, 217, 176, 0.2);
370 541
 }
371 542
 
372
-.quote-summary__label {
543
+.lr-ring__figure {
544
+  width: 104px;
545
+  height: 104px;
546
+  flex-shrink: 0;
547
+  display: flex;
548
+  align-items: center;
549
+  justify-content: center;
550
+}
551
+
552
+.lr-ring__outer {
553
+  width: 100%;
554
+  height: 100%;
555
+  border-radius: 50%;
556
+  display: flex;
557
+  align-items: center;
558
+  justify-content: center;
559
+  box-sizing: border-box;
560
+  border: 2px solid rgba(236, 210, 123, 0.22);
561
+  background: transparent;
562
+}
563
+
564
+.lr-ring__inner {
565
+  position: relative;
566
+  width: 82%;
567
+  height: 82%;
568
+  display: flex;
569
+  align-items: center;
570
+  justify-content: center;
571
+  background: transparent;
572
+}
573
+
574
+.lr-ring__inner::before {
575
+  content: '';
576
+  position: absolute;
577
+  inset: 0;
578
+  border-radius: 50%;
579
+  padding: 3px;
580
+  background: linear-gradient(160deg, #45f0b8 0%, #1d9a7a 42%, #ecd27b 100%);
581
+  -webkit-mask:
582
+    linear-gradient(#fff 0 0) content-box,
583
+    linear-gradient(#fff 0 0);
584
+  -webkit-mask-composite: xor;
585
+  mask:
586
+    linear-gradient(#fff 0 0) content-box,
587
+    linear-gradient(#fff 0 0);
588
+  mask-composite: exclude;
589
+  pointer-events: none;
590
+}
591
+
592
+.lr-ring__val {
593
+  position: relative;
594
+  z-index: 1;
595
+  display: flex;
596
+  align-items: center;
597
+  justify-content: center;
598
+  width: 100%;
599
+  padding: 0 4px;
600
+  box-sizing: border-box;
601
+  text-align: center;
602
+  line-height: 1.15;
603
+  color: #e8eef5;
604
+}
605
+
606
+.lr-ring__val strong {
607
+  font-size: 22px;
608
+  font-weight: 600;
609
+  letter-spacing: 0.02em;
610
+  color: #ecd27b;
611
+}
612
+
613
+.lr-ring__label {
614
+  margin-top: 6px;
615
+  width: 100%;
616
+  padding: 0 2px;
617
+  box-sizing: border-box;
373 618
   font-size: 10px;
374 619
   color: #a8d4c8;
620
+  text-align: center;
621
+  line-height: 1.35;
622
+}
623
+
624
+.lr-ring__ratio {
625
+  margin-top: 2px;
626
+  font-size: 9px;
627
+  color: #5ef0c8;
628
+  text-align: center;
629
+  line-height: 1.3;
630
+}
631
+
632
+.flex_content--list {
633
+  padding: 6px 10px;
634
+  overflow: hidden;
635
+}
636
+
637
+.lr-hot-list {
638
+  list-style: none;
639
+  margin: 0;
640
+  padding: 0;
641
+  height: 100%;
642
+  display: flex;
643
+  flex-direction: column;
644
+  justify-content: space-between;
375 645
 }
376 646
 
377
-.quote-summary__val {
647
+.lr-hot-list__row {
648
+  display: flex;
649
+  align-items: center;
650
+  gap: 8px;
651
+  height: 30px;
652
+  padding: 0 4px;
653
+  border-bottom: 1px solid rgba(61, 217, 176, 0.12);
654
+}
655
+
656
+.lr-hot-list__row:last-child {
657
+  border-bottom: none;
658
+}
659
+
660
+.lr-hot-list__rank {
661
+  flex-shrink: 0;
662
+  width: 18px;
663
+  height: 18px;
664
+  border-radius: 3px;
665
+  background: rgba(61, 217, 176, 0.25);
666
+  color: #a8d4c8;
378 667
   font-size: 11px;
668
+  line-height: 18px;
669
+  text-align: center;
670
+}
671
+
672
+.lr-hot-list__rank--1 {
673
+  background: linear-gradient(135deg, #ecd27b, #c9a030);
674
+  color: #1a2e28;
675
+}
676
+
677
+.lr-hot-list__rank--2 {
678
+  background: rgba(236, 210, 123, 0.55);
379 679
   color: #fff;
380 680
 }
381 681
 
382
-.quote-summary__val strong {
383
-  font-size: 14px;
384
-  background: linear-gradient(to bottom, #98e9aa, #ecd27b);
385
-  -webkit-background-clip: text;
386
-  background-clip: text;
387
-  color: transparent;
682
+.lr-hot-list__rank--3 {
683
+  background: rgba(110, 181, 255, 0.45);
684
+  color: #fff;
388 685
 }
389 686
 
390
-.quote-summary__unit {
687
+.lr-hot-list__name {
688
+  flex: 1;
689
+  min-width: 0;
690
+  font-size: 11px;
691
+  color: #e8eef5;
692
+  white-space: nowrap;
693
+  overflow: hidden;
694
+  text-overflow: ellipsis;
695
+}
696
+
697
+.lr-hot-list__meta {
698
+  flex-shrink: 0;
699
+  display: flex;
700
+  align-items: baseline;
701
+  gap: 6px;
391 702
   font-size: 10px;
392 703
   color: #a8d4c8;
393
-  margin-left: 2px;
394 704
 }
395 705
 
396
-.quote-chart {
397
-  flex: 1;
398
-  min-height: 0;
706
+.lr-hot-list__meta strong {
707
+  font-size: 12px;
708
+  color: #5ef0c8;
709
+  font-weight: 600;
710
+}
711
+
712
+.lr-hot-list__meta em {
713
+  font-style: normal;
714
+  color: #ecd27b;
399 715
 }
400 716
 
401
-.flex_content--placeholder {
717
+.lr-empty {
402 718
   display: flex;
403 719
   align-items: center;
404 720
   justify-content: center;
721
+  width: 100%;
722
+  height: 100%;
723
+  font-size: 13px;
724
+  color: rgba(168, 212, 200, 0.75);
405 725
 }
406 726
 </style>