xsh_1997 1 месяц назад
Родитель
Сommit
cdc8ae1e2b

+ 121 - 83
ruoyi-screen/src/views/commonProsperity/chartOptions.js

@@ -1,13 +1,14 @@
1
-/** 共同富裕大屏 ECharts 配置(对齐 doc/共同富裕 设计稿) */
1
+/** 共同富裕大屏 ECharts 配置(对齐 doc/大屏/共同富裕 v1.1 滚动 12 月) */
2
 
2
 
3
 const AXIS_LABEL = { color: '#9fb0c3', fontSize: 10 }
3
 const AXIS_LABEL = { color: '#9fb0c3', fontSize: 10 }
4
 const AXIS_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.35)' } }
4
 const AXIS_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.35)' } }
5
 const SPLIT_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.12)' } }
5
 const SPLIT_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.12)' } }
6
 const GRID = { left: 40, right: 16, top: 28, bottom: 28, containLabel: true }
6
 const GRID = { left: 40, right: 16, top: 28, bottom: 28, containLabel: true }
7
-const MONTHS = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
8
 
7
 
9
 const PROJECT_TYPE_COLOR = ['#45f0b8', '#6eb5ff', '#ecd27b']
8
 const PROJECT_TYPE_COLOR = ['#45f0b8', '#6eb5ff', '#ecd27b']
10
 
9
 
10
+const ACHIEVEMENT_EMPTY = '暂无该年度共富成果数据'
11
+
11
 function emptyOption(text = '暂无数据') {
12
 function emptyOption(text = '暂无数据') {
12
   return {
13
   return {
13
     title: {
14
     title: {
@@ -19,45 +20,72 @@ function emptyOption(text = '暂无数据') {
19
   }
20
   }
20
 }
21
 }
21
 
22
 
22
-function monthSeries(monthly, field) {
23
-  if (!monthly?.length) {
24
-    return MONTHS.map(() => 0)
23
+function pickNumber(row, fields) {
24
+  for (const key of fields) {
25
+    const raw = row?.[key]
26
+    if (raw != null && raw !== '') {
27
+      const n = Number(raw)
28
+      if (!Number.isNaN(n)) {
29
+        return n
30
+      }
31
+    }
25
   }
32
   }
26
-  const map = new Map(monthly.map((row) => [row.month, row[field] ?? 0]))
27
-  return MONTHS.map((_, i) => {
28
-    const val = map.get(i + 1) ?? 0
29
-    return typeof val === 'object' && val !== null ? Number(val) : Number(val)
33
+  return 0
34
+}
35
+
36
+/** 滚动最近 12 月:按 statYear、month 升序 */
37
+function sortRollingMonths(list) {
38
+  return [...(list || [])].sort((a, b) => {
39
+    const ak = (Number(a.statYear) || 0) * 100 + (Number(a.month) || 0)
40
+    const bk = (Number(b.statYear) || 0) * 100 + (Number(b.month) || 0)
41
+    return ak - bk
30
   })
42
   })
31
 }
43
 }
32
 
44
 
33
-function hasMonthlyData(monthly, field) {
34
-  return monthly?.length && monthSeries(monthly, field).some((v) => v > 0)
45
+function rollingMonthLabel(row) {
46
+  return `${Number(row.month)}月`
47
+}
48
+
49
+function rollingTooltipTitle(row) {
50
+  if (!row) {
51
+    return ''
52
+  }
53
+  return `${row.statYear}年${Number(row.month)}月`
54
+}
55
+
56
+function buildRollingAxis(list) {
57
+  const rows = sortRollingMonths(list)
58
+  return {
59
+    rows,
60
+    categories: rows.map(rollingMonthLabel),
61
+    titleAt: (dataIndex) => rollingTooltipTitle(rows[dataIndex])
62
+  }
63
+}
64
+
65
+function rollingSeries(list, field, altFields = []) {
66
+  const fields = [field, ...altFields]
67
+  return sortRollingMonths(list).map((row) => pickNumber(row, fields))
35
 }
68
 }
36
 
69
 
37
-/** 横轴为去掉「月」的类目时,tooltip 标题补回「月」 */
38
-function formatMonthAxisTooltip(params) {
39
-  const list = Array.isArray(params) ? params : [params]
40
-  const raw = list[0]?.axisValue
41
-  const title =
42
-    raw != null && raw !== ''
43
-      ? String(raw).endsWith('月')
44
-        ? String(raw)
45
-        : `${raw}月`
46
-      : ''
47
-  const body = list
48
-    .map((p) =>
49
-      p.seriesName ? `${p.marker}${p.seriesName}:${p.value}` : `${p.marker}${p.value}`
50
-    )
51
-    .join('<br/>')
52
-  return title ? `${title}<br/>${body}` : body
70
+function rollingAxisTooltip(params, axis, valueFormatter) {
71
+  if (!Array.isArray(params) || !params.length) {
72
+    return ''
73
+  }
74
+  const title = axis.titleAt(params[0].dataIndex)
75
+  const lines = params.map((p) => {
76
+    const val = valueFormatter ? valueFormatter(p) : `${p.value ?? 0}`
77
+    return `${p.marker}${p.seriesName}:${val}`
78
+  })
79
+  return `${title}<br/>${lines.join('<br/>')}`
53
 }
80
 }
54
 
81
 
55
-/** 经济收入对比 — 横向柱(集体经济收入,万元) */
82
+/** 经济收入对比 — 横向柱(集体经济收入,万元,滚动 12 月) */
56
 export function buildCollectiveIncomeBarOption(monthly, hasData) {
83
 export function buildCollectiveIncomeBarOption(monthly, hasData) {
57
-  if (!hasData || !hasMonthlyData(monthly, 'collectiveEconomyIncome')) {
58
-    return emptyOption('暂无收入数据')
84
+  if (!hasData || !monthly?.length) {
85
+    return emptyOption(ACHIEVEMENT_EMPTY)
59
   }
86
   }
60
-  const values = monthSeries(monthly, 'collectiveEconomyIncome')
87
+  const axis = buildRollingAxis(monthly)
88
+  const values = rollingSeries(monthly, 'collectiveEconomyIncome')
61
   const maxVal = Math.max(...values, 1)
89
   const maxVal = Math.max(...values, 1)
62
   return {
90
   return {
63
     color: ['#ecd27b'],
91
     color: ['#ecd27b'],
@@ -70,7 +98,8 @@ export function buildCollectiveIncomeBarOption(monthly, hasData) {
70
         if (!row) {
98
         if (!row) {
71
           return ''
99
           return ''
72
         }
100
         }
73
-        return `${row.name}<br/>收入:${Number(row.value).toFixed(1)} 万元`
101
+        const title = axis.titleAt(row.dataIndex)
102
+        return `${title}<br/>收入:${Number(row.value).toFixed(1)} 万元`
74
       }
103
       }
75
     },
104
     },
76
     grid: { left: 4, right: 36, top: 10, bottom: 0, containLabel: true },
105
     grid: { left: 4, right: 36, top: 10, bottom: 0, containLabel: true },
@@ -81,7 +110,7 @@ export function buildCollectiveIncomeBarOption(monthly, hasData) {
81
     },
110
     },
82
     yAxis: {
111
     yAxis: {
83
       type: 'category',
112
       type: 'category',
84
-      data: MONTHS,
113
+      data: axis.categories,
85
       inverse: true,
114
       inverse: true,
86
       boundaryGap: false,
115
       boundaryGap: false,
87
       axisLabel: {
116
       axisLabel: {
@@ -104,7 +133,7 @@ export function buildCollectiveIncomeBarOption(monthly, hasData) {
104
         silent: true,
133
         silent: true,
105
         tooltip: { show: false },
134
         tooltip: { show: false },
106
         itemStyle: { color: 'rgba(15, 80, 68, 0.55)', borderRadius: [0, 4, 4, 0] },
135
         itemStyle: { color: 'rgba(15, 80, 68, 0.55)', borderRadius: [0, 4, 4, 0] },
107
-        data: MONTHS.map(() => maxVal),
136
+        data: axis.categories.map(() => maxVal),
108
         z: 1
137
         z: 1
109
       },
138
       },
110
       {
139
       {
@@ -139,18 +168,18 @@ export function buildCollectiveIncomeBarOption(monthly, hasData) {
139
   }
168
   }
140
 }
169
 }
141
 
170
 
142
-/** 就业带动 — 就业人数 + 就业岗位双折线 */
171
+/** 就业带动 — 带动就业人数 + 新增就业岗位双折线(滚动 12 月) */
143
 export function buildEmploymentOption(monthly, hasData) {
172
 export function buildEmploymentOption(monthly, hasData) {
144
-  if (
145
-    !hasData ||
146
-    (!hasMonthlyData(monthly, 'employmentDrivenCount') &&
147
-      !hasMonthlyData(monthly, 'newJobPositions'))
148
-  ) {
149
-    return emptyOption('暂无就业数据')
173
+  if (!hasData || !monthly?.length) {
174
+    return emptyOption(ACHIEVEMENT_EMPTY)
150
   }
175
   }
176
+  const axis = buildRollingAxis(monthly)
151
   return {
177
   return {
152
     color: ['#ecd27b', '#5ef0c8'],
178
     color: ['#ecd27b', '#5ef0c8'],
153
-    tooltip: { trigger: 'axis', formatter: formatMonthAxisTooltip },
179
+    tooltip: {
180
+      trigger: 'axis',
181
+      formatter: (params) => rollingAxisTooltip(params, axis)
182
+    },
154
     legend: {
183
     legend: {
155
       top: 4,
184
       top: 4,
156
       left: 'center',
185
       left: 'center',
@@ -161,7 +190,7 @@ export function buildEmploymentOption(monthly, hasData) {
161
     grid: { ...GRID, left: 10, top: 36, bottom: 8 },
190
     grid: { ...GRID, left: 10, top: 36, bottom: 8 },
162
     xAxis: {
191
     xAxis: {
163
       type: 'category',
192
       type: 'category',
164
-      data: MONTHS.map((m) => m.replace('月', '')),
193
+      data: axis.categories,
165
       axisLabel: { ...AXIS_LABEL, fontSize: 9, interval: 0 },
194
       axisLabel: { ...AXIS_LABEL, fontSize: 9, interval: 0 },
166
       axisLine: AXIS_LINE
195
       axisLine: AXIS_LINE
167
     },
196
     },
@@ -187,40 +216,44 @@ export function buildEmploymentOption(monthly, hasData) {
187
     ],
216
     ],
188
     series: [
217
     series: [
189
       {
218
       {
190
-        name: '就业人数',
219
+        name: '带动就业人数',
191
         type: 'line',
220
         type: 'line',
192
         smooth: true,
221
         smooth: true,
193
         symbol: 'circle',
222
         symbol: 'circle',
194
         symbolSize: 4,
223
         symbolSize: 4,
195
         areaStyle: { color: 'rgba(236, 210, 123, 0.2)' },
224
         areaStyle: { color: 'rgba(236, 210, 123, 0.2)' },
196
-        data: monthSeries(monthly, 'employmentDrivenCount')
225
+        data: rollingSeries(monthly, 'employmentDrivenCount')
197
       },
226
       },
198
       {
227
       {
199
-        name: '就业岗位',
228
+        name: '新增就业岗位',
200
         type: 'line',
229
         type: 'line',
201
         yAxisIndex: 1,
230
         yAxisIndex: 1,
202
         smooth: true,
231
         smooth: true,
203
         symbol: 'circle',
232
         symbol: 'circle',
204
         symbolSize: 4,
233
         symbolSize: 4,
205
         areaStyle: { color: 'rgba(94, 240, 200, 0.15)' },
234
         areaStyle: { color: 'rgba(94, 240, 200, 0.15)' },
206
-        data: monthSeries(monthly, 'newJobPositions')
235
+        data: rollingSeries(monthly, 'newJobPositions')
207
       }
236
       }
208
     ]
237
     ]
209
   }
238
   }
210
 }
239
 }
211
 
240
 
212
-/** 环境治理 — 项目数月度柱图 */
241
+/** 环境治理 — 项目数月度柱图(滚动 12 月) */
213
 export function buildEnvGovernanceOption(monthly, hasData) {
242
 export function buildEnvGovernanceOption(monthly, hasData) {
214
-  if (!hasData || !hasMonthlyData(monthly, 'envProjectCount')) {
215
-    return emptyOption('暂无环境治理数据')
243
+  if (!hasData || !monthly?.length) {
244
+    return emptyOption(ACHIEVEMENT_EMPTY)
216
   }
245
   }
246
+  const axis = buildRollingAxis(monthly)
217
   return {
247
   return {
218
     color: ['#ecd27b'],
248
     color: ['#ecd27b'],
219
-    tooltip: { trigger: 'axis', formatter: formatMonthAxisTooltip },
249
+    tooltip: {
250
+      trigger: 'axis',
251
+      formatter: (params) => rollingAxisTooltip(params, axis, (p) => `${p.value ?? 0} 个`)
252
+    },
220
     grid: { ...GRID, left: 10, right: 8, top: 36, bottom: 20, containLabel: true },
253
     grid: { ...GRID, left: 10, right: 8, top: 36, bottom: 20, containLabel: true },
221
     xAxis: {
254
     xAxis: {
222
       type: 'category',
255
       type: 'category',
223
-      data: MONTHS.map((m) => m.replace('月', '')),
256
+      data: axis.categories,
224
       axisLabel: {
257
       axisLabel: {
225
         ...AXIS_LABEL,
258
         ...AXIS_LABEL,
226
         fontSize: 9,
259
         fontSize: 9,
@@ -244,24 +277,28 @@ export function buildEnvGovernanceOption(monthly, hasData) {
244
         barMaxWidth: 8,
277
         barMaxWidth: 8,
245
         barCategoryGap: '35%',
278
         barCategoryGap: '35%',
246
         itemStyle: { color: '#ecd27b', borderRadius: [3, 3, 0, 0] },
279
         itemStyle: { color: '#ecd27b', borderRadius: [3, 3, 0, 0] },
247
-        data: monthSeries(monthly, 'envProjectCount')
280
+        data: rollingSeries(monthly, 'envProjectCount')
248
       }
281
       }
249
     ]
282
     ]
250
   }
283
   }
251
 }
284
 }
252
 
285
 
253
-/** 绿化建设 — 新增绿化面积折线面积图 */
286
+/** 绿化建设 — 新增绿化面积折线面积图(滚动 12 月) */
254
 export function buildGreenAreaOption(monthly, hasData) {
287
 export function buildGreenAreaOption(monthly, hasData) {
255
-  if (!hasData || !hasMonthlyData(monthly, 'newGreenArea')) {
256
-    return emptyOption('暂无绿化数据')
288
+  if (!hasData || !monthly?.length) {
289
+    return emptyOption(ACHIEVEMENT_EMPTY)
257
   }
290
   }
291
+  const axis = buildRollingAxis(monthly)
258
   return {
292
   return {
259
     color: ['#5ef0c8'],
293
     color: ['#5ef0c8'],
260
-    tooltip: { trigger: 'axis' },
261
-    grid: {...GRID, left: 10,  bottom: 8},
294
+    tooltip: {
295
+      trigger: 'axis',
296
+      formatter: (params) => rollingAxisTooltip(params, axis, (p) => `${p.value ?? 0} 亩`)
297
+    },
298
+    grid: { ...GRID, left: 10, bottom: 8 },
262
     xAxis: {
299
     xAxis: {
263
       type: 'category',
300
       type: 'category',
264
-      data: MONTHS,
301
+      data: axis.categories,
265
       axisLabel: { ...AXIS_LABEL, rotate: 35, interval: 0, fontSize: 9 },
302
       axisLabel: { ...AXIS_LABEL, rotate: 35, interval: 0, fontSize: 9 },
266
       axisLine: AXIS_LINE
303
       axisLine: AXIS_LINE
267
     },
304
     },
@@ -275,30 +312,34 @@ export function buildGreenAreaOption(monthly, hasData) {
275
     },
312
     },
276
     series: [
313
     series: [
277
       {
314
       {
278
-        name: '绿化面积',
315
+        name: '新增绿化面积',
279
         type: 'line',
316
         type: 'line',
280
         smooth: true,
317
         smooth: true,
281
         symbol: 'circle',
318
         symbol: 'circle',
282
         symbolSize: 4,
319
         symbolSize: 4,
283
         areaStyle: { color: 'rgba(94, 240, 200, 0.3)' },
320
         areaStyle: { color: 'rgba(94, 240, 200, 0.3)' },
284
-        data: monthSeries(monthly, 'newGreenArea')
321
+        data: rollingSeries(monthly, 'newGreenArea')
285
       }
322
       }
286
     ]
323
     ]
287
   }
324
   }
288
 }
325
 }
289
 
326
 
290
-/** 文化传承 — 月度柱图 */
327
+/** 文化传承 — 月度柱图(滚动 12 月) */
291
 export function buildCulturalHeritageOption(monthly, hasData) {
328
 export function buildCulturalHeritageOption(monthly, hasData) {
292
-  if (!hasData || !hasMonthlyData(monthly, 'culturalHeritageCount')) {
293
-    return emptyOption('暂无文化传承数据')
329
+  if (!hasData || !monthly?.length) {
330
+    return emptyOption(ACHIEVEMENT_EMPTY)
294
   }
331
   }
332
+  const axis = buildRollingAxis(monthly)
295
   return {
333
   return {
296
     color: ['#5ef0c8'],
334
     color: ['#5ef0c8'],
297
-    tooltip: { trigger: 'axis', formatter: formatMonthAxisTooltip },
335
+    tooltip: {
336
+      trigger: 'axis',
337
+      formatter: (params) => rollingAxisTooltip(params, axis, (p) => `${p.value ?? 0} 人`)
338
+    },
298
     grid: { ...GRID, left: 10, right: 8, bottom: 20, containLabel: true },
339
     grid: { ...GRID, left: 10, right: 8, bottom: 20, containLabel: true },
299
     xAxis: {
340
     xAxis: {
300
       type: 'category',
341
       type: 'category',
301
-      data: MONTHS.map((m) => m.replace('月', '')),
342
+      data: axis.categories,
302
       axisLabel: {
343
       axisLabel: {
303
         ...AXIS_LABEL,
344
         ...AXIS_LABEL,
304
         fontSize: 9,
345
         fontSize: 9,
@@ -317,32 +358,29 @@ export function buildCulturalHeritageOption(monthly, hasData) {
317
     },
358
     },
318
     series: [
359
     series: [
319
       {
360
       {
320
-        name: '人数',
361
+        name: '文化传承人数',
321
         type: 'bar',
362
         type: 'bar',
322
         barMaxWidth: 8,
363
         barMaxWidth: 8,
323
         barCategoryGap: '35%',
364
         barCategoryGap: '35%',
324
         itemStyle: { color: '#5ef0c8', borderRadius: [3, 3, 0, 0] },
365
         itemStyle: { color: '#5ef0c8', borderRadius: [3, 3, 0, 0] },
325
-        data: monthSeries(monthly, 'culturalHeritageCount')
366
+        data: rollingSeries(monthly, 'culturalHeritageCount')
326
       }
367
       }
327
     ]
368
     ]
328
   }
369
   }
329
 }
370
 }
330
 
371
 
331
-/** 民族团结 — 堆叠柱 */
372
+/** 民族团结 — 堆叠柱(滚动 12 月) */
332
 export function buildEthnicUnityStackedOption(monthly, hasData) {
373
 export function buildEthnicUnityStackedOption(monthly, hasData) {
333
-  if (
334
-    !hasData ||
335
-    (!hasMonthlyData(monthly, 'ethnicIntegrationProjectCount') &&
336
-      !hasMonthlyData(monthly, 'ethnicUnityActivityCount'))
337
-  ) {
338
-    return emptyOption('暂无民族团结数据')
374
+  if (!hasData || !monthly?.length) {
375
+    return emptyOption(ACHIEVEMENT_EMPTY)
339
   }
376
   }
377
+  const axis = buildRollingAxis(monthly)
340
   return {
378
   return {
341
     color: ['#5ef0c8', '#1a4a6e'],
379
     color: ['#5ef0c8', '#1a4a6e'],
342
     tooltip: {
380
     tooltip: {
343
       trigger: 'axis',
381
       trigger: 'axis',
344
       axisPointer: { type: 'shadow' },
382
       axisPointer: { type: 'shadow' },
345
-      formatter: formatMonthAxisTooltip
383
+      formatter: (params) => rollingAxisTooltip(params, axis, (p) => `${p.value ?? 0} 个`)
346
     },
384
     },
347
     legend: {
385
     legend: {
348
       top: 4,
386
       top: 4,
@@ -354,7 +392,7 @@ export function buildEthnicUnityStackedOption(monthly, hasData) {
354
     grid: { ...GRID, left: 10, right: 8, top: 36, bottom: 20, containLabel: true },
392
     grid: { ...GRID, left: 10, right: 8, top: 36, bottom: 20, containLabel: true },
355
     xAxis: {
393
     xAxis: {
356
       type: 'category',
394
       type: 'category',
357
-      data: MONTHS.map((m) => m.replace('月', '')),
395
+      data: axis.categories,
358
       axisLabel: {
396
       axisLabel: {
359
         ...AXIS_LABEL,
397
         ...AXIS_LABEL,
360
         fontSize: 9,
398
         fontSize: 9,
@@ -373,19 +411,19 @@ export function buildEthnicUnityStackedOption(monthly, hasData) {
373
     },
411
     },
374
     series: [
412
     series: [
375
       {
413
       {
376
-        name: '民族融合案例',
414
+        name: '民族融合项目数',
377
         type: 'bar',
415
         type: 'bar',
378
         stack: 'ethnic',
416
         stack: 'ethnic',
379
         barMaxWidth: 8,
417
         barMaxWidth: 8,
380
         barCategoryGap: '35%',
418
         barCategoryGap: '35%',
381
-        data: monthSeries(monthly, 'ethnicIntegrationProjectCount')
419
+        data: rollingSeries(monthly, 'ethnicIntegrationProjectCount')
382
       },
420
       },
383
       {
421
       {
384
-        name: '民族团结活动',
422
+        name: '民族团结活动',
385
         type: 'bar',
423
         type: 'bar',
386
         stack: 'ethnic',
424
         stack: 'ethnic',
387
         barMaxWidth: 8,
425
         barMaxWidth: 8,
388
-        data: monthSeries(monthly, 'ethnicUnityActivityCount')
426
+        data: rollingSeries(monthly, 'ethnicUnityActivityCount')
389
       }
427
       }
390
     ]
428
     ]
391
   }
429
   }
@@ -394,7 +432,7 @@ export function buildEthnicUnityStackedOption(monthly, hasData) {
394
 /** 共富项目分类 — 饼图 */
432
 /** 共富项目分类 — 饼图 */
395
 export function buildProjectTypePieOption(projectTypeStats, hasProjectData) {
433
 export function buildProjectTypePieOption(projectTypeStats, hasProjectData) {
396
   if (!hasProjectData || !projectTypeStats?.length) {
434
   if (!hasProjectData || !projectTypeStats?.length) {
397
-    return emptyOption('暂无项目数据')
435
+    return emptyOption('本年度暂无共富项目')
398
   }
436
   }
399
   const data = projectTypeStats
437
   const data = projectTypeStats
400
     .filter((item) => (item.count ?? 0) > 0)
438
     .filter((item) => (item.count ?? 0) > 0)
@@ -404,7 +442,7 @@ export function buildProjectTypePieOption(projectTypeStats, hasProjectData) {
404
       itemStyle: { color: PROJECT_TYPE_COLOR[i % PROJECT_TYPE_COLOR.length] }
442
       itemStyle: { color: PROJECT_TYPE_COLOR[i % PROJECT_TYPE_COLOR.length] }
405
     }))
443
     }))
406
   if (!data.length) {
444
   if (!data.length) {
407
-    return emptyOption('暂无项目数据')
445
+    return emptyOption('本年度暂无共富项目')
408
   }
446
   }
409
   return {
447
   return {
410
     tooltip: {
448
     tooltip: {

+ 103 - 60
ruoyi-screen/src/views/commonProsperity/index.vue

@@ -1,6 +1,6 @@
1
 <template>
1
 <template>
2
   <div class="screen-page cp-page" :class="{ 'is-loading': loading }">
2
   <div class="screen-page cp-page" :class="{ 'is-loading': loading }">
3
-    <div v-if="loadError" class="cp-error">{{ loadError }}</div>
3
+    <div v-if="loadError" class="cp-error" @click="fetchDashboard">{{ loadError }}(点击重试)</div>
4
 
4
 
5
     <!-- <div class="cp-year-bar">
5
     <!-- <div class="cp-year-bar">
6
       <label class="cp-year-bar__label">统计年份</label>
6
       <label class="cp-year-bar__label">统计年份</label>
@@ -17,67 +17,72 @@
17
     <div class="screen-page--home-column">
17
     <div class="screen-page--home-column">
18
       <div class="screen-page--home-column-top">
18
       <div class="screen-page--home-column-top">
19
         <div class="top_title">共同富裕 མཉམ་འདུས་འབྲིག་སྐྱིད།</div>
19
         <div class="top_title">共同富裕 མཉམ་འདུས་འབྲིག་སྐྱིད།</div>
20
-        <div class="top_content">
21
-          <div class="content_1">
22
-            <div class="content_num">
23
-              <ScreenScrollNumber :value="summary?.collectiveEconomyIncome" format="money" />
24
-              万元
20
+        <template v-if="hasAchievementData">
21
+          <div class="top_content">
22
+            <div class="content_1">
23
+              <div class="content_num">
24
+                <ScreenScrollNumber :value="summary?.collectiveEconomyIncome" format="money" />
25
+                万元
26
+              </div>
27
+              <div class="content_title"><div>集体经济收入</div></div>
25
             </div>
28
             </div>
26
-            <div class="content_title"><div>集体经济收入</div></div>
27
-          </div>
28
-          <div class="content_2">
29
-            <div class="content_num">
30
-              <ScreenScrollNumber :value="summary?.employmentDrivenCount" />
31
-              人
29
+            <div class="content_2">
30
+              <div class="content_num">
31
+                <ScreenScrollNumber :value="summary?.employmentDrivenCount" />
32
+            
33
+              </div>
34
+              <div class="content_title"><div>带动就业数</div></div>
32
             </div>
35
             </div>
33
-            <div class="content_title"><div>就业人数</div></div>
34
-          </div>
35
-          <div class="content_3">
36
-            <div class="content_num">
37
-              <ScreenScrollNumber :value="summary?.newJobPositions" />
38
-              
36
+            <div class="content_3">
37
+              <div class="content_num">
38
+                <ScreenScrollNumber :value="summary?.newJobPositions" />
39
+            
40
+              </div>
41
+              <div class="content_title"><div>新增就业岗位</div></div>
39
             </div>
42
             </div>
40
-            <div class="content_title"><div>就业岗位数</div></div>
41
-          </div>
42
-          <div class="content_4">
43
-            <div class="content_num">
44
-              <ScreenScrollNumber :value="summary?.envProjectCount" />
45
-              
43
+            <div class="content_4">
44
+              <div class="content_num">
45
+                <ScreenScrollNumber :value="summary?.envProjectCount" />
46
+            
47
+              </div>
48
+              <div class="content_title"><div>环境治理项目数</div></div>
46
             </div>
49
             </div>
47
-            <div class="content_title"><div>环境治理项目数</div></div>
48
           </div>
50
           </div>
49
-        </div>
50
-        <div class="top_content">
51
-          <div class="content_1">
52
-            <div class="content_num">
53
-              <ScreenScrollNumber :value="summary?.newGreenArea" format="area" />
54
-              亩
51
+          <div class="top_content">
52
+            <div class="content_1">
53
+              <div class="content_num">
54
+                <ScreenScrollNumber :value="summary?.newGreenArea" format="area" />
55
+                亩
56
+              </div>
57
+              <div class="content_title"><div>新增绿化面积</div></div>
55
             </div>
58
             </div>
56
-            <div class="content_title"><div>绿化面积</div></div>
57
-          </div>
58
-          <div class="content_2">
59
-            <div class="content_num">
60
-              <ScreenScrollNumber :value="summary?.culturalHeritageCount" />
61
-              人
59
+            <div class="content_2">
60
+              <div class="content_num">
61
+                <ScreenScrollNumber :value="summary?.culturalHeritageCount" />
62
+            
63
+              </div>
64
+              <div class="content_title"><div>文化传承数</div></div>
62
             </div>
65
             </div>
63
-            <div class="content_title"><div>文化传承人数</div></div>
64
-          </div>
65
-          <div class="content_3">
66
-            <div class="content_num">
67
-              <ScreenScrollNumber :value="summary?.ethnicIntegrationProjectCount" />
68
-              
66
+            <div class="content_3">
67
+              <div class="content_num">
68
+                <ScreenScrollNumber :value="summary?.ethnicIntegrationProjectCount" />
69
+            
70
+              </div>
71
+              <div class="content_title"><div>民族融合项目数</div></div>
69
             </div>
72
             </div>
70
-            <div class="content_title"><div>民族团结项目数</div></div>
71
-          </div>
72
-          <div class="content_4">
73
-            <div class="content_num">
74
-              <ScreenScrollNumber :value="summary?.regionalPublicBrandCount" />
75
-              
73
+            <div class="content_4">
74
+              <div class="content_num">
75
+                <ScreenScrollNumber :value="summary?.regionalPublicBrandCount" />
76
+            
77
+              </div>
78
+              <div class="content_title"><div>区域公共品牌数</div></div>
76
             </div>
79
             </div>
77
-            <div class="content_title"><div>区域公共品牌数</div></div>
78
           </div>
80
           </div>
81
+        </template>
82
+        <div v-else class="cp-overview-empty">
83
+          <p>暂无该年度共富成果数据</p>
84
+          <p class="cp-overview-empty__hint">请在共同富裕成果中维护</p>
79
         </div>
85
         </div>
80
-        <p v-if="achievement && !achievement.hasData" class="cp-no-achievement">本年度暂无成果台账数据</p>
81
       </div>
86
       </div>
82
 
87
 
83
       <div class="screen-page--home-column-flex">
88
       <div class="screen-page--home-column-flex">
@@ -114,7 +119,7 @@
114
     <div class="screen-page--home-column">
119
     <div class="screen-page--home-column">
115
       <div class="screen-page--home-column-right cp-digital-panel">
120
       <div class="screen-page--home-column-right cp-digital-panel">
116
         <div class="top_title">数字赋能 གྲངས་ཀ་སྟོབས་སྦྱོང་།</div>
121
         <div class="top_title">数字赋能 གྲངས་ཀ་སྟོབས་སྦྱོང་།</div>
117
-        <div class="cp-digital-body">
122
+        <div v-if="hasAchievementData" class="cp-digital-body">
118
           <div class="cp-digital-group">
123
           <div class="cp-digital-group">
119
             <div class="cp-digital-group__title">品牌增值</div>
124
             <div class="cp-digital-group__title">品牌增值</div>
120
             <div class="cp-rings">
125
             <div class="cp-rings">
@@ -162,6 +167,10 @@
162
             </div>
167
             </div>
163
           </div>
168
           </div>
164
         </div>
169
         </div>
170
+        <div v-else class="cp-digital-empty">
171
+          <p>暂无该年度共富成果数据</p>
172
+          <p class="cp-digital-empty__hint">请在共同富裕成果中维护</p>
173
+        </div>
165
       </div>
174
       </div>
166
 
175
 
167
       <div class="screen-page--home-column-flex">
176
       <div class="screen-page--home-column-flex">
@@ -191,7 +200,7 @@
191
           <div class="flex_content flex_content--projects">
200
           <div class="flex_content flex_content--projects">
192
             <div class="cp-project-scroll" @scroll="onProjectScroll">
201
             <div class="cp-project-scroll" @scroll="onProjectScroll">
193
               <div v-if="projectLoading && !projectRows.length" class="cp-project-empty">加载中…</div>
202
               <div v-if="projectLoading && !projectRows.length" class="cp-project-empty">加载中…</div>
194
-              <div v-else-if="!projectRows.length" class="cp-project-empty">暂无共富项目</div>
203
+              <div v-else-if="!projectRows.length" class="cp-project-empty">本年度暂无共富项目</div>
195
               <div
204
               <div
196
                 v-for="row in projectRows"
205
                 v-for="row in projectRows"
197
                 :key="row.id"
206
                 :key="row.id"
@@ -637,6 +646,7 @@ onUnmounted(() => {
637
   color: #ffb4b4;
646
   color: #ffb4b4;
638
   background: rgba(80, 20, 20, 0.75);
647
   background: rgba(80, 20, 20, 0.75);
639
   border-radius: 4px;
648
   border-radius: 4px;
649
+  cursor: pointer;
640
 }
650
 }
641
 
651
 
642
 .cp-year-bar {
652
 .cp-year-bar {
@@ -682,15 +692,48 @@ onUnmounted(() => {
682
   position: relative;
692
   position: relative;
683
 }
693
 }
684
 
694
 
685
-.cp-no-achievement {
686
-  position: absolute;
687
-  bottom: 4px;
688
-  left: 0;
689
-  right: 0;
695
+.cp-overview-empty {
696
+  flex: 1;
697
+  display: flex;
698
+  flex-direction: column;
699
+  align-items: center;
700
+  justify-content: center;
701
+  padding: 0 24px 12px;
690
   text-align: center;
702
   text-align: center;
691
-  font-size: 11px;
692
-  color: rgba(168, 212, 200, 0.7);
703
+}
704
+
705
+.cp-overview-empty p {
693
   margin: 0;
706
   margin: 0;
707
+  font-size: 13px;
708
+  color: rgba(168, 212, 200, 0.85);
709
+}
710
+
711
+.cp-overview-empty__hint {
712
+  margin-top: 6px !important;
713
+  font-size: 11px !important;
714
+  color: rgba(106, 159, 144, 0.9) !important;
715
+}
716
+
717
+.cp-digital-empty {
718
+  flex: 1;
719
+  display: flex;
720
+  flex-direction: column;
721
+  align-items: center;
722
+  justify-content: center;
723
+  padding: 20px 16px;
724
+  text-align: center;
725
+}
726
+
727
+.cp-digital-empty p {
728
+  margin: 0;
729
+  font-size: 13px;
730
+  color: rgba(168, 212, 200, 0.85);
731
+}
732
+
733
+.cp-digital-empty__hint {
734
+  margin-top: 6px !important;
735
+  font-size: 11px !important;
736
+  color: rgba(106, 159, 144, 0.9) !important;
694
 }
737
 }
695
 
738
 
696
 .screen-page--home-column-right {
739
 .screen-page--home-column-right {

+ 51 - 18
ruoyi-screen/src/views/epidemicRisk/chartOptions.js

@@ -6,7 +6,8 @@ const SPLIT_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.12)' } }
6
 const GRID = { left: 40, right: 16, top: 36, bottom: 28, containLabel: true }
6
 const GRID = { left: 40, right: 16, top: 36, bottom: 28, containLabel: true }
7
 /** 解除时效柱图:预留 y 轴单位「天」空间,下边距收紧 */
7
 /** 解除时效柱图:预留 y 轴单位「天」空间,下边距收紧 */
8
 const RELIEVE_DURATION_GRID = { left: 40, right: 12, top: 30, bottom: 4, containLabel: true }
8
 const RELIEVE_DURATION_GRID = { left: 40, right: 12, top: 30, bottom: 4, containLabel: true }
9
-const MONTHS = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
9
+const LAB_GRID = { left: 28, right: 10, top: 30, bottom: 6, containLabel: true }
10
+const LAB_MONTH_AXIS_LABEL = { color: '#9fb0c3', fontSize: 9, margin: 4 }
10
 
11
 
11
 /** 样本来源展示顺序:日常→场点→调运→上报(与设计稿一致) */
12
 /** 样本来源展示顺序:日常→场点→调运→上报(与设计稿一致) */
12
 const SAMPLE_SOURCE_ORDER = [4, 1, 3, 2]
13
 const SAMPLE_SOURCE_ORDER = [4, 1, 3, 2]
@@ -169,13 +170,38 @@ function pickAvgRelieveDays(item) {
169
   return null
170
   return null
170
 }
171
 }
171
 
172
 
172
-function sortByMonth(list) {
173
-  return [...(list || [])].sort((a, b) => (a.month || 0) - (b.month || 0))
173
+function sortRollingMonths(list) {
174
+  return [...(list || [])].sort((a, b) => {
175
+    const ak = (Number(a.statYear) || 0) * 100 + (Number(a.month) || 0)
176
+    const bk = (Number(b.statYear) || 0) * 100 + (Number(b.month) || 0)
177
+    return ak - bk
178
+  })
174
 }
179
 }
175
 
180
 
176
-function monthSeries(list, field) {
177
-  const map = new Map(sortByMonth(list).map((row) => [row.month, row[field] ?? 0]))
178
-  return MONTHS.map((_, i) => map.get(i + 1) ?? 0)
181
+function buildRollingAxis(list) {
182
+  const rows = sortRollingMonths(list)
183
+  return {
184
+    rows,
185
+    categories: rows.map((row) => `${Number(row.month)}月`),
186
+    titleAt: (dataIndex) => {
187
+      const row = rows[dataIndex]
188
+      if (!row) {
189
+        return ''
190
+      }
191
+      return `${row.statYear}年${Number(row.month)}月`
192
+    }
193
+  }
194
+}
195
+
196
+function rollingSeries(list, field) {
197
+  return sortRollingMonths(list).map((row) => {
198
+    const val = row?.[field]
199
+    if (val == null || val === '') {
200
+      return 0
201
+    }
202
+    const n = Number(val)
203
+    return Number.isNaN(n) ? 0 : n
204
+  })
179
 }
205
 }
180
 
206
 
181
 function pickSampleSourceRows(stats) {
207
 function pickSampleSourceRows(stats) {
@@ -264,19 +290,26 @@ export function buildSampleSourceBarOption(sampleSourceStats) {
264
   }
290
   }
265
 }
291
 }
266
 
292
 
267
-/** 实验室检测专用 grid(仅本图,收紧左/下留白) */
268
-const LAB_GRID = { left: 28, right: 10, top: 30, bottom: 6, containLabel: true }
269
-const LAB_MONTH_AXIS_LABEL = { color: '#9fb0c3', fontSize: 9, margin: 4 }
270
-
271
-/** 实验室检测 — 检测量折线 + 阳性数面积 */
272
-export function buildLabDetectionOption(labDetection, hasLabData) {
273
-  if (!hasLabData || !labDetection?.monthly?.length) {
293
+/** 实验室检测 — 检测量折线 + 阳性数面积(滚动最近 12 月) */
294
+export function buildLabDetectionOption(labDetection) {
295
+  const monthly = labDetection?.monthly
296
+  if (!monthly?.length) {
274
     return emptyOption('暂无实验室数据')
297
     return emptyOption('暂无实验室数据')
275
   }
298
   }
276
-  const monthly = labDetection.monthly
299
+  const axis = buildRollingAxis(monthly)
277
   return {
300
   return {
278
     color: ['#5ef0c8', '#ecd27b'],
301
     color: ['#5ef0c8', '#ecd27b'],
279
-    tooltip: { trigger: 'axis' },
302
+    tooltip: {
303
+      trigger: 'axis',
304
+      formatter: (params) => {
305
+        if (!Array.isArray(params) || !params.length) {
306
+          return ''
307
+        }
308
+        const title = axis.titleAt(params[0].dataIndex)
309
+        const lines = params.map((p) => `${p.marker}${p.seriesName}: ${p.value ?? 0} 份`)
310
+        return `${title}<br/>${lines.join('<br/>')}`
311
+      }
312
+    },
280
     legend: {
313
     legend: {
281
       top: 2,
314
       top: 2,
282
       left: 'center',
315
       left: 'center',
@@ -287,7 +320,7 @@ export function buildLabDetectionOption(labDetection, hasLabData) {
287
     grid: LAB_GRID,
320
     grid: LAB_GRID,
288
     xAxis: {
321
     xAxis: {
289
       type: 'category',
322
       type: 'category',
290
-      data: MONTHS,
323
+      data: axis.categories,
291
       axisLabel: { ...LAB_MONTH_AXIS_LABEL, interval: 0 },
324
       axisLabel: { ...LAB_MONTH_AXIS_LABEL, interval: 0 },
292
       axisLine: AXIS_LINE,
325
       axisLine: AXIS_LINE,
293
       axisTick: { show: false }
326
       axisTick: { show: false }
@@ -306,7 +339,7 @@ export function buildLabDetectionOption(labDetection, hasLabData) {
306
         smooth: true,
339
         smooth: true,
307
         symbol: 'circle',
340
         symbol: 'circle',
308
         symbolSize: 4,
341
         symbolSize: 4,
309
-        data: monthSeries(monthly, 'testQuantity')
342
+        data: rollingSeries(monthly, 'testQuantity')
310
       },
343
       },
311
       {
344
       {
312
         name: '阳性数',
345
         name: '阳性数',
@@ -315,7 +348,7 @@ export function buildLabDetectionOption(labDetection, hasLabData) {
315
         symbol: 'circle',
348
         symbol: 'circle',
316
         symbolSize: 4,
349
         symbolSize: 4,
317
         areaStyle: { color: 'rgba(236, 210, 123, 0.25)' },
350
         areaStyle: { color: 'rgba(236, 210, 123, 0.25)' },
318
-        data: monthSeries(monthly, 'positiveCount')
351
+        data: rollingSeries(monthly, 'positiveCount')
319
       }
352
       }
320
     ]
353
     ]
321
   }
354
   }

+ 16 - 11
ruoyi-screen/src/views/epidemicRisk/index.vue

@@ -1,6 +1,6 @@
1
 <template>
1
 <template>
2
   <div class="screen-page er-page" :class="{ 'is-loading': loading }">
2
   <div class="screen-page er-page" :class="{ 'is-loading': loading }">
3
-    <div v-if="loadError" class="er-error">{{ loadError }}</div>
3
+    <div v-if="loadError" class="er-error" @click="fetchDashboard">{{ loadError }}(点击重试)</div>
4
 
4
 
5
     <!-- <div class="er-year-bar">
5
     <!-- <div class="er-year-bar">
6
       <label class="er-year-bar__label">统计年份</label>
6
       <label class="er-year-bar__label">统计年份</label>
@@ -67,13 +67,19 @@
67
           <div class="flex_content flex_content--lab">
67
           <div class="flex_content flex_content--lab">
68
             <div class="lab-summary">
68
             <div class="lab-summary">
69
               <div class="lab-summary__item">
69
               <div class="lab-summary__item">
70
-                <div class="lab-summary__label">样本检测数</div>
70
+                <div class="lab-summary__label">检测量</div>
71
                 <div class="lab-summary__val">
71
                 <div class="lab-summary__val">
72
                   <strong>{{ display(labDetection?.testQuantityTotal) }}</strong> 份
72
                   <strong>{{ display(labDetection?.testQuantityTotal) }}</strong> 份
73
                 </div>
73
                 </div>
74
               </div>
74
               </div>
75
+              <div class="lab-summary__item">
76
+                <div class="lab-summary__label">阳性检出数</div>
77
+                <div class="lab-summary__val">
78
+                  <strong>{{ display(labDetection?.positiveCountTotal) }}</strong> 份
79
+                </div>
80
+              </div>
75
               <div class="lab-summary__item lab-summary__item--rate">
81
               <div class="lab-summary__item lab-summary__item--rate">
76
-                <div class="lab-summary__label">阳性检测率</div>
82
+                <div class="lab-summary__label">阳性检率</div>
77
                 <div class="lab-summary__val">
83
                 <div class="lab-summary__val">
78
                   <strong>{{ formatRate(labDetection?.positiveRate) }}</strong>
84
                   <strong>{{ formatRate(labDetection?.positiveRate) }}</strong>
79
                 </div>
85
                 </div>
@@ -261,9 +267,7 @@ const reportLoadingMore = ref(false)
261
 const reportScrollLoading = ref(false)
267
 const reportScrollLoading = ref(false)
262
 
268
 
263
 const sampleSourceChartOption = computed(() => buildSampleSourceBarOption(sampleSourceStats.value))
269
 const sampleSourceChartOption = computed(() => buildSampleSourceBarOption(sampleSourceStats.value))
264
-const labChartOption = computed(() =>
265
-  buildLabDetectionOption(labDetection.value, hasLabData.value)
266
-)
270
+const labChartOption = computed(() => buildLabDetectionOption(labDetection.value))
267
 const riskLevelChartOption = computed(() =>
271
 const riskLevelChartOption = computed(() =>
268
   buildRiskLevelPieOption(riskLevelStats.value, hasEpidemicData.value)
272
   buildRiskLevelPieOption(riskLevelStats.value, hasEpidemicData.value)
269
 )
273
 )
@@ -547,6 +551,7 @@ onUnmounted(() => {
547
   color: #ffb4b4;
551
   color: #ffb4b4;
548
   background: rgba(80, 20, 20, 0.75);
552
   background: rgba(80, 20, 20, 0.75);
549
   border-radius: 4px;
553
   border-radius: 4px;
554
+  cursor: pointer;
550
 }
555
 }
551
 
556
 
552
 .er-year-bar {
557
 .er-year-bar {
@@ -950,8 +955,8 @@ onUnmounted(() => {
950
 .lab-summary {
955
 .lab-summary {
951
   display: flex;
956
   display: flex;
952
   flex-direction: row;
957
   flex-direction: row;
953
-  gap: 6px;
954
-  height: 28px;
958
+  gap: 4px;
959
+  height: 32px;
955
   flex-shrink: 0;
960
   flex-shrink: 0;
956
 }
961
 }
957
 
962
 
@@ -967,9 +972,9 @@ onUnmounted(() => {
967
 }
972
 }
968
 
973
 
969
 .lab-summary__label {
974
 .lab-summary__label {
970
-  font-size: 10px;
975
+  font-size: 9px;
971
   color: #a8d4c8;
976
   color: #a8d4c8;
972
-  line-height: 14px;
977
+  line-height: 13px;
973
 }
978
 }
974
 
979
 
975
 .lab-summary__val {
980
 .lab-summary__val {
@@ -979,7 +984,7 @@ onUnmounted(() => {
979
 }
984
 }
980
 
985
 
981
 .lab-summary__val strong {
986
 .lab-summary__val strong {
982
-  font-size: 14px;
987
+  font-size: 13px;
983
   background: linear-gradient(to bottom, #98e9aa, #ecd27b);
988
   background: linear-gradient(to bottom, #98e9aa, #ecd27b);
984
   -webkit-background-clip: text;
989
   -webkit-background-clip: text;
985
   background-clip: text;
990
   background-clip: text;

+ 2 - 2
ruoyi-screen/src/views/home/index.vue

@@ -2,7 +2,7 @@
2
   <div class="screen-page screen-page--home" :class="{ 'is-loading': loading }">
2
   <div class="screen-page screen-page--home" :class="{ 'is-loading': loading }">
3
     <div v-if="loadError" class="home-error" @click="fetchDashboard">{{ loadError }}(点击重试)</div>
3
     <div v-if="loadError" class="home-error" @click="fetchDashboard">{{ loadError }}(点击重试)</div>
4
 
4
 
5
-    <div class="home-year-bar">
5
+    <!-- <div class="home-year-bar">
6
       <label class="home-year-bar__label">统计年份</label>
6
       <label class="home-year-bar__label">统计年份</label>
7
       <select
7
       <select
8
         v-model="statYear"
8
         v-model="statYear"
@@ -13,7 +13,7 @@
13
         <option v-for="y in availableYears" :key="y" :value="String(y)">{{ y }}年</option>
13
         <option v-for="y in availableYears" :key="y" :value="String(y)">{{ y }}年</option>
14
       </select>
14
       </select>
15
       <span v-if="statDate" class="home-year-bar__date">统计日 {{ statDate }}</span>
15
       <span v-if="statDate" class="home-year-bar__date">统计日 {{ statDate }}</span>
16
-    </div>
16
+    </div> -->
17
 
17
 
18
     <div class="screen-page--home-column">
18
     <div class="screen-page--home-column">
19
       <div class="screen-page--home-column-top">
19
       <div class="screen-page--home-column-top">

+ 31 - 9
ruoyi-screen/src/views/livestockResources/chartOptions.js

@@ -54,7 +54,19 @@ export function buildActivityTrendOption(activityTrend) {
54
   return {
54
   return {
55
     ...chartBase(),
55
     ...chartBase(),
56
     color: ['#5ef0c8', '#ecd27b'],
56
     color: ['#5ef0c8', '#ecd27b'],
57
-    tooltip: { trigger: 'axis' },
57
+    tooltip: {
58
+      trigger: 'axis',
59
+      formatter: (params) => {
60
+        if (!Array.isArray(params) || !params.length) {
61
+          return ''
62
+        }
63
+        const idx = params[0].dataIndex
64
+        const row = series[idx] || {}
65
+        const title = row.date || dates[idx] || ''
66
+        const lines = params.map((p) => `${p.marker}${p.seriesName}: ${p.value ?? 0}`)
67
+        return `${title}<br/>${lines.join('<br/>')}`
68
+      }
69
+    },
58
     legend: {
70
     legend: {
59
       top: 0,
71
       top: 0,
60
       right: 8,
72
       right: 8,
@@ -81,7 +93,6 @@ export function buildActivityTrendOption(activityTrend) {
81
       {
93
       {
82
         name: '会话数',
94
         name: '会话数',
83
         type: 'line',
95
         type: 'line',
84
-        stack: 'total',
85
         smooth: true,
96
         smooth: true,
86
         symbol: 'circle',
97
         symbol: 'circle',
87
         symbolSize: 4,
98
         symbolSize: 4,
@@ -91,7 +102,6 @@ export function buildActivityTrendOption(activityTrend) {
91
       {
102
       {
92
         name: '提问量',
103
         name: '提问量',
93
         type: 'line',
104
         type: 'line',
94
-        stack: 'total',
95
         smooth: true,
105
         smooth: true,
96
         symbol: 'circle',
106
         symbol: 'circle',
97
         symbolSize: 4,
107
         symbolSize: 4,
@@ -235,19 +245,31 @@ export function buildModelCallOption(modelCallAnalysis) {
235
   }
245
   }
236
 }
246
 }
237
 
247
 
238
-/** 分时使用热度 — 会话柱 + 提问量折线(24 小时) */
248
+/** 分时使用热度 — 最近 24 整点小时(会话柱 + 提问量折线) */
239
 export function buildHourlyHeatOption(hourlyHeat) {
249
 export function buildHourlyHeatOption(hourlyHeat) {
240
   const series = hourlyHeat?.hourlySeries || []
250
   const series = hourlyHeat?.hourlySeries || []
241
   if (!series.length) {
251
   if (!series.length) {
242
     return emptyOption('暂无热度数据')
252
     return emptyOption('暂无热度数据')
243
   }
253
   }
244
-  const labels = series.map((d) => d.bucketStart || '')
245
-  const sessions = series.map((d) => toNum(d.sessionCount))
246
-  const questions = series.map((d) => toNum(d.questionCount))
254
+  const labels = series.map((d) => d.bucketStart || d.bucket_start || '')
255
+  const sessions = series.map((d) => toNum(d.sessionCount ?? d.session_count))
256
+  const questions = series.map((d) => toNum(d.questionCount ?? d.question_count))
247
   return {
257
   return {
248
     ...chartBase(),
258
     ...chartBase(),
249
     color: ['#5ef0c8', '#ecd27b'],
259
     color: ['#5ef0c8', '#ecd27b'],
250
-    tooltip: { trigger: 'axis' },
260
+    tooltip: {
261
+      trigger: 'axis',
262
+      formatter: (params) => {
263
+        if (!Array.isArray(params) || !params.length) {
264
+          return ''
265
+        }
266
+        const idx = params[0].dataIndex
267
+        const row = series[idx] || {}
268
+        const title = row.bucketDateTime || row.bucket_date_time || row.bucketStart || ''
269
+        const lines = params.map((p) => `${p.marker}${p.seriesName}: ${p.value ?? 0}`)
270
+        return `${title}<br/>${lines.join('<br/>')}`
271
+      }
272
+    },
251
     legend: {
273
     legend: {
252
       top: 0,
274
       top: 0,
253
       right: 8,
275
       right: 8,
@@ -262,7 +284,7 @@ export function buildHourlyHeatOption(hourlyHeat) {
262
       axisLabel: {
284
       axisLabel: {
263
         ...AXIS_LABEL,
285
         ...AXIS_LABEL,
264
         fontSize: 8,
286
         fontSize: 8,
265
-        interval: (idx) => idx % 4 === 0
287
+        interval: (idx) => idx % 3 === 0
266
       },
288
       },
267
       axisLine: AXIS_LINE
289
       axisLine: AXIS_LINE
268
     },
290
     },

+ 57 - 1
ruoyi-screen/src/views/livestockResources/index.vue

@@ -1,6 +1,19 @@
1
 <template>
1
 <template>
2
   <div class="screen-page lr-page" :class="{ 'is-loading': loading }">
2
   <div class="screen-page lr-page" :class="{ 'is-loading': loading }">
3
-    <div v-if="loadError" class="lr-error">{{ loadError }}</div>
3
+    <div v-if="loadError" class="lr-error" @click="fetchDashboard">{{ loadError }}(点击重试)</div>
4
+
5
+    <!-- <div class="lr-year-bar">
6
+      <label class="lr-year-bar__label">统计年份</label>
7
+      <select
8
+        v-model="statYear"
9
+        class="lr-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="lr-year-bar__date">统计日 {{ statDate }}</span>
16
+    </div> -->
4
 
17
 
5
     <!-- 左栏:核心总览 + 活跃/用户/分类/模型 -->
18
     <!-- 左栏:核心总览 + 活跃/用户/分类/模型 -->
6
     <div class="screen-page--home-column">
19
     <div class="screen-page--home-column">
@@ -157,6 +170,8 @@ import {
157
 const loading = ref(false)
170
 const loading = ref(false)
158
 const loadError = ref('')
171
 const loadError = ref('')
159
 const statYear = ref(String(new Date().getFullYear()))
172
 const statYear = ref(String(new Date().getFullYear()))
173
+const statDate = ref('')
174
+const availableYears = ref([Number(statYear.value)])
160
 
175
 
161
 const overview = ref(null)
176
 const overview = ref(null)
162
 const activityTrend = ref(null)
177
 const activityTrend = ref(null)
@@ -298,6 +313,10 @@ function applyDashboard(data) {
298
   if (data.statYear) {
313
   if (data.statYear) {
299
     statYear.value = String(data.statYear)
314
     statYear.value = String(data.statYear)
300
   }
315
   }
316
+  statDate.value = data.statDate || ''
317
+  availableYears.value = data.availableYears?.length
318
+    ? [...data.availableYears]
319
+    : [Number(statYear.value)]
301
   overview.value = data.overview || null
320
   overview.value = data.overview || null
302
   activityTrend.value = normalizeActivityTrend(data.activityTrend)
321
   activityTrend.value = normalizeActivityTrend(data.activityTrend)
303
   userStructure.value = data.userStructure || null
322
   userStructure.value = data.userStructure || null
@@ -326,6 +345,10 @@ async function fetchDashboard() {
326
   }
345
   }
327
 }
346
 }
328
 
347
 
348
+function onYearChange() {
349
+  fetchDashboard()
350
+}
351
+
329
 onMounted(() => {
352
 onMounted(() => {
330
   fetchDashboard()
353
   fetchDashboard()
331
 })
354
 })
@@ -358,6 +381,39 @@ onMounted(() => {
358
   color: #ffb4b4;
381
   color: #ffb4b4;
359
   background: rgba(80, 20, 20, 0.75);
382
   background: rgba(80, 20, 20, 0.75);
360
   border-radius: 4px;
383
   border-radius: 4px;
384
+  cursor: pointer;
385
+}
386
+
387
+.lr-year-bar {
388
+  position: absolute;
389
+  top: 8px;
390
+  right: 24px;
391
+  z-index: 5;
392
+  display: flex;
393
+  align-items: center;
394
+  gap: 8px;
395
+}
396
+
397
+.lr-year-bar__label {
398
+  font-size: 12px;
399
+  color: var(--screen-text-secondary, #a8d4c8);
400
+}
401
+
402
+.lr-year-bar__select {
403
+  min-width: 88px;
404
+  height: 28px;
405
+  padding: 0 8px;
406
+  font-size: 13px;
407
+  color: #e8eef5;
408
+  background: rgba(4, 48, 40, 0.85);
409
+  border: 1px solid var(--screen-line-dim, rgba(61, 217, 176, 0.42));
410
+  border-radius: 4px;
411
+  outline: none;
412
+}
413
+
414
+.lr-year-bar__date {
415
+  font-size: 11px;
416
+  color: var(--screen-text-secondary, #a8d4c8);
361
 }
417
 }
362
 
418
 
363
 .screen-page--home-column {
419
 .screen-page--home-column {

+ 95 - 36
ruoyi-screen/src/views/tradeSales/chartOptions.js

@@ -1,4 +1,4 @@
1
-/** 交易销售大屏 ECharts 配置(对齐 doc/交易销售 设计稿) */
1
+/** 交易销售大屏 ECharts 配置(对齐 doc/大屏/交易销售 v1.3 滚动 12 月) */
2
 
2
 
3
 import { buildMallPie2DOption } from '@/utils/mallPie2d'
3
 import { buildMallPie2DOption } from '@/utils/mallPie2d'
4
 
4
 
@@ -6,7 +6,6 @@ const AXIS_LABEL = { color: '#9fb0c3', fontSize: 10 }
6
 const AXIS_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.35)' } }
6
 const AXIS_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.35)' } }
7
 const SPLIT_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.12)' } }
7
 const SPLIT_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.12)' } }
8
 const GRID = { left: 40, right: 16, top: 36, bottom: 28, containLabel: true }
8
 const GRID = { left: 40, right: 16, top: 36, bottom: 28, containLabel: true }
9
-const MONTHS = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
10
 
9
 
11
 const DESTINATION_COLOR = ['#5ef0c8', '#ecd27b', '#6eb5ff']
10
 const DESTINATION_COLOR = ['#5ef0c8', '#ecd27b', '#6eb5ff']
12
 const GRADE_COLOR = ['#6eb5ff', '#ecd27b', '#5ef0c8', '#0f8f72']
11
 const GRADE_COLOR = ['#6eb5ff', '#ecd27b', '#5ef0c8', '#0f8f72']
@@ -22,16 +21,63 @@ function emptyOption(text = '暂无数据') {
22
   }
21
   }
23
 }
22
 }
24
 
23
 
25
-function sortByMonth(list) {
26
-  return [...(list || [])].sort((a, b) => (a.month || 0) - (b.month || 0))
24
+function pickNumber(row, fields) {
25
+  for (const key of fields) {
26
+    const raw = row?.[key]
27
+    if (raw != null && raw !== '') {
28
+      const n = Number(raw)
29
+      if (!Number.isNaN(n)) {
30
+        return n
31
+      }
32
+    }
33
+  }
34
+  return 0
35
+}
36
+
37
+/** 滚动最近 12 月:按 statYear、month 升序 */
38
+function sortRollingMonths(list) {
39
+  return [...(list || [])].sort((a, b) => {
40
+    const ak = (Number(a.statYear) || 0) * 100 + (Number(a.month) || 0)
41
+    const bk = (Number(b.statYear) || 0) * 100 + (Number(b.month) || 0)
42
+    return ak - bk
43
+  })
44
+}
45
+
46
+function rollingMonthLabel(row) {
47
+  return `${Number(row.month)}月`
48
+}
49
+
50
+function rollingTooltipTitle(row) {
51
+  if (!row) {
52
+    return ''
53
+  }
54
+  return `${row.statYear}年${Number(row.month)}月`
55
+}
56
+
57
+function buildRollingAxis(list) {
58
+  const rows = sortRollingMonths(list)
59
+  return {
60
+    rows,
61
+    categories: rows.map(rollingMonthLabel),
62
+    titleAt: (dataIndex) => rollingTooltipTitle(rows[dataIndex])
63
+  }
64
+}
65
+
66
+function rollingSeries(list, field, altFields = []) {
67
+  const fields = [field, ...altFields]
68
+  return sortRollingMonths(list).map((row) => pickNumber(row, fields))
27
 }
69
 }
28
 
70
 
29
-function monthSeries(list, field) {
30
-  const map = new Map(sortByMonth(list).map((row) => [row.month, row[field] ?? 0]))
31
-  return MONTHS.map((_, i) => {
32
-    const val = map.get(i + 1) ?? 0
33
-    return typeof val === 'object' && val !== null ? Number(val) : Number(val)
71
+function rollingAxisTooltip(params, axis, valueFormatter) {
72
+  if (!Array.isArray(params) || !params.length) {
73
+    return ''
74
+  }
75
+  const title = axis.titleAt(params[0].dataIndex)
76
+  const lines = params.map((p) => {
77
+    const val = valueFormatter ? valueFormatter(p) : `${p.value ?? 0}`
78
+    return `${p.marker}${p.seriesName}:${val}`
34
   })
79
   })
80
+  return `${title}<br/>${lines.join('<br/>')}`
35
 }
81
 }
36
 
82
 
37
 function formatQuoteDate(quoteDate) {
83
 function formatQuoteDate(quoteDate) {
@@ -40,7 +86,9 @@ function formatQuoteDate(quoteDate) {
40
   }
86
   }
41
   const parts = String(quoteDate).split('-')
87
   const parts = String(quoteDate).split('-')
42
   if (parts.length >= 3) {
88
   if (parts.length >= 3) {
43
-    return `${Number(parts[1])}/${Number(parts[2])}`
89
+    const mm = String(parts[1]).padStart(2, '0')
90
+    const dd = String(parts[2]).padStart(2, '0')
91
+    return `${mm}-${dd}`
44
   }
92
   }
45
   return quoteDate
93
   return quoteDate
46
 }
94
 }
@@ -63,11 +111,13 @@ export function buildOriginQuoteOption(originQuote) {
63
     tooltip: {
111
     tooltip: {
64
       trigger: 'axis',
112
       trigger: 'axis',
65
       formatter: (params) => {
113
       formatter: (params) => {
66
-        const lines = [params[0]?.name || '']
67
-        params.forEach((p) => {
68
-          lines.push(`${p.marker}${p.seriesName} ${p.value}${unit}`)
69
-        })
70
-        return lines.join('<br/>')
114
+        if (!Array.isArray(params) || !params.length) {
115
+          return ''
116
+        }
117
+        const row = originQuote.dailyTrend[params[0].dataIndex]
118
+        const title = row?.quoteDate || params[0]?.name || ''
119
+        const lines = params.map((p) => `${p.marker}${p.seriesName} ${p.value}${unit}`)
120
+        return `${title}<br/>${lines.join('<br/>')}`
71
       }
121
       }
72
     },
122
     },
73
     legend: {
123
     legend: {
@@ -120,19 +170,26 @@ export function buildOriginQuoteOption(originQuote) {
120
   }
170
   }
121
 }
171
 }
122
 
172
 
123
-/** 牦牛交易数额趋势 — 销售量(头) + 销售额(元) */
173
+/** 牦牛交易数额趋势 — 销售量(头) + 销售额(元)折线(滚动 12 月) */
124
 export function buildTradeMonthlyOption(tradeMonthlyTrend) {
174
 export function buildTradeMonthlyOption(tradeMonthlyTrend) {
125
   if (!tradeMonthlyTrend?.length) {
175
   if (!tradeMonthlyTrend?.length) {
126
-    return emptyOption()
127
-  }
128
-  const heads = monthSeries(tradeMonthlyTrend, 'tradeHeads')
129
-  const amounts = monthSeries(tradeMonthlyTrend, 'tradeAmount')
130
-  if (!heads.some((v) => v > 0) && !amounts.some((v) => v > 0)) {
131
     return emptyOption('暂无交易数据')
176
     return emptyOption('暂无交易数据')
132
   }
177
   }
178
+  const axis = buildRollingAxis(tradeMonthlyTrend)
179
+  const heads = rollingSeries(tradeMonthlyTrend, 'tradeHeads')
180
+  const amounts = rollingSeries(tradeMonthlyTrend, 'tradeAmount')
133
   return {
181
   return {
134
     color: ['#5ef0c8', '#ecd27b'],
182
     color: ['#5ef0c8', '#ecd27b'],
135
-    tooltip: { trigger: 'axis' },
183
+    tooltip: {
184
+      trigger: 'axis',
185
+      formatter: (params) =>
186
+        rollingAxisTooltip(params, axis, (p) => {
187
+          if (p.seriesName === '销售额') {
188
+            return `${Number(p.value).toFixed(2)} 万元`
189
+          }
190
+          return `${p.value ?? 0} 头`
191
+        })
192
+    },
136
     legend: {
193
     legend: {
137
       top: 0,
194
       top: 0,
138
       left: 'center',
195
       left: 'center',
@@ -143,7 +200,7 @@ export function buildTradeMonthlyOption(tradeMonthlyTrend) {
143
     grid: { ...GRID, bottom: 10 },
200
     grid: { ...GRID, bottom: 10 },
144
     xAxis: {
201
     xAxis: {
145
       type: 'category',
202
       type: 'category',
146
-      data: MONTHS,
203
+      data: axis.categories,
147
       axisLabel: { ...AXIS_LABEL, rotate: 35, interval: 0, fontSize: 9 },
204
       axisLabel: { ...AXIS_LABEL, rotate: 35, interval: 0, fontSize: 9 },
148
       axisLine: AXIS_LINE
205
       axisLine: AXIS_LINE
149
     },
206
     },
@@ -172,11 +229,9 @@ export function buildTradeMonthlyOption(tradeMonthlyTrend) {
172
     series: [
229
     series: [
173
       {
230
       {
174
         name: '销售量',
231
         name: '销售量',
175
-        type: 'line',
176
-        smooth: true,
177
-        symbol: 'circle',
178
-        symbolSize: 4,
179
-        areaStyle: { color: 'rgba(94, 240, 200, 0.2)' },
232
+        type: 'bar',
233
+        barMaxWidth: 10,
234
+        itemStyle: { color: 'rgba(94, 240, 200, 0.75)', borderRadius: [3, 3, 0, 0] },
180
         data: heads
235
         data: heads
181
       },
236
       },
182
       {
237
       {
@@ -186,6 +241,7 @@ export function buildTradeMonthlyOption(tradeMonthlyTrend) {
186
         smooth: true,
241
         smooth: true,
187
         symbol: 'circle',
242
         symbol: 'circle',
188
         symbolSize: 4,
243
         symbolSize: 4,
244
+        lineStyle: { width: 2, color: '#ecd27b' },
189
         data: amounts.map((v) => Number(v) / 10000)
245
         data: amounts.map((v) => Number(v) / 10000)
190
       }
246
       }
191
     ]
247
     ]
@@ -331,25 +387,28 @@ export function buildCategorySalesPieOption(categorySales, mallStatsAvailable =
331
   })
387
   })
332
 }
388
 }
333
 
389
 
334
-/** 商城订单趋势 — 曲线 */
390
+/** 商城订单趋势 — 曲线(滚动 12 月) */
335
 export function buildMallOrderTrendOption(mallOrderTrend, mallStatsAvailable = true) {
391
 export function buildMallOrderTrendOption(mallOrderTrend, mallStatsAvailable = true) {
336
   if (mallStatsAvailable === false) {
392
   if (mallStatsAvailable === false) {
337
     return emptyOption(MALL_UNAVAILABLE_TEXT)
393
     return emptyOption(MALL_UNAVAILABLE_TEXT)
338
   }
394
   }
339
-  if (!mallOrderTrend?.items?.length) {
395
+  const items = mallOrderTrend?.items || []
396
+  if (!items.length) {
340
     return emptyOption(resolveMallEmptyText(mallStatsAvailable, '暂无订单数据'))
397
     return emptyOption(resolveMallEmptyText(mallStatsAvailable, '暂无订单数据'))
341
   }
398
   }
342
-  const counts = monthSeries(mallOrderTrend.items, 'orderCount')
343
-  if (!counts.some((v) => v > 0)) {
344
-    return emptyOption('暂无订单数据')
345
-  }
399
+  const axis = buildRollingAxis(items)
400
+  const counts = rollingSeries(items, 'orderCount')
346
   return {
401
   return {
347
     color: ['#6eb5ff'],
402
     color: ['#6eb5ff'],
348
-    tooltip: { trigger: 'axis' },
403
+    tooltip: {
404
+      trigger: 'axis',
405
+      formatter: (params) =>
406
+        rollingAxisTooltip(params, axis, (p) => `${p.value ?? 0} 单`)
407
+    },
349
     grid: { ...GRID, top: 12, bottom: 10 },
408
     grid: { ...GRID, top: 12, bottom: 10 },
350
     xAxis: {
409
     xAxis: {
351
       type: 'category',
410
       type: 'category',
352
-      data: MONTHS,
411
+      data: axis.categories,
353
       axisLabel: { ...AXIS_LABEL, rotate: 35, interval: 0, fontSize: 9 },
412
       axisLabel: { ...AXIS_LABEL, rotate: 35, interval: 0, fontSize: 9 },
354
       axisLine: AXIS_LINE
413
       axisLine: AXIS_LINE
355
     },
414
     },

+ 1 - 3
ruoyi-screen/src/views/tradeSales/index.vue

@@ -3,8 +3,7 @@
3
     <div v-if="loadError" class="ts-error" @click="onRetry">
3
     <div v-if="loadError" class="ts-error" @click="onRetry">
4
       {{ loadError }}(点击重试)
4
       {{ loadError }}(点击重试)
5
     </div>
5
     </div>
6
-<!-- 
7
-    <div class="ts-year-bar">
6
+    <!-- <div class="ts-year-bar">
8
       <label class="ts-year-bar__label">统计年份</label>
7
       <label class="ts-year-bar__label">统计年份</label>
9
       <select
8
       <select
10
         v-model="statYear"
9
         v-model="statYear"
@@ -116,7 +115,6 @@
116
       <div class="screen-page--home-column-right ts-mall-panel">
115
       <div class="screen-page--home-column-right ts-mall-panel">
117
         <div class="top_title">
116
         <div class="top_title">
118
           农资品类销售 ཞིང་ལས་རྒྱུ་ཆ་རིགས་ཀྱི་ཚོང་འདོན།
117
           农资品类销售 ཞིང་ལས་རྒྱུ་ཆ་རིགས་ཀྱི་ཚོང་འདོན།
119
-          <!-- <span v-if="categoryStatYearLabel" class="ts-mall-stat-year">({{ categoryStatYearLabel }})</span> -->
120
         </div>
118
         </div>
121
         <div class="ts-mall-top">
119
         <div class="ts-mall-top">
122
           <div class="ts-mall-top__pie">
120
           <div class="ts-mall-top__pie">