Ver código fonte

大屏后台修改

xsh_1997 4 semanas atrás
pai
commit
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 3
 const AXIS_LABEL = { color: '#9fb0c3', fontSize: 10 }
4 4
 const AXIS_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.35)' } }
5 5
 const SPLIT_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.12)' } }
6 6
 const GRID = { left: 40, right: 16, top: 28, bottom: 28, containLabel: true }
7
-const MONTHS = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
8 7
 
9 8
 const PROJECT_TYPE_COLOR = ['#45f0b8', '#6eb5ff', '#ecd27b']
10 9
 
10
+const ACHIEVEMENT_EMPTY = '暂无该年度共富成果数据'
11
+
11 12
 function emptyOption(text = '暂无数据') {
12 13
   return {
13 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 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 89
   const maxVal = Math.max(...values, 1)
62 90
   return {
63 91
     color: ['#ecd27b'],
@@ -70,7 +98,8 @@ export function buildCollectiveIncomeBarOption(monthly, hasData) {
70 98
         if (!row) {
71 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 105
     grid: { left: 4, right: 36, top: 10, bottom: 0, containLabel: true },
@@ -81,7 +110,7 @@ export function buildCollectiveIncomeBarOption(monthly, hasData) {
81 110
     },
82 111
     yAxis: {
83 112
       type: 'category',
84
-      data: MONTHS,
113
+      data: axis.categories,
85 114
       inverse: true,
86 115
       boundaryGap: false,
87 116
       axisLabel: {
@@ -104,7 +133,7 @@ export function buildCollectiveIncomeBarOption(monthly, hasData) {
104 133
         silent: true,
105 134
         tooltip: { show: false },
106 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 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 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 177
   return {
152 178
     color: ['#ecd27b', '#5ef0c8'],
153
-    tooltip: { trigger: 'axis', formatter: formatMonthAxisTooltip },
179
+    tooltip: {
180
+      trigger: 'axis',
181
+      formatter: (params) => rollingAxisTooltip(params, axis)
182
+    },
154 183
     legend: {
155 184
       top: 4,
156 185
       left: 'center',
@@ -161,7 +190,7 @@ export function buildEmploymentOption(monthly, hasData) {
161 190
     grid: { ...GRID, left: 10, top: 36, bottom: 8 },
162 191
     xAxis: {
163 192
       type: 'category',
164
-      data: MONTHS.map((m) => m.replace('月', '')),
193
+      data: axis.categories,
165 194
       axisLabel: { ...AXIS_LABEL, fontSize: 9, interval: 0 },
166 195
       axisLine: AXIS_LINE
167 196
     },
@@ -187,40 +216,44 @@ export function buildEmploymentOption(monthly, hasData) {
187 216
     ],
188 217
     series: [
189 218
       {
190
-        name: '就业人数',
219
+        name: '带动就业人数',
191 220
         type: 'line',
192 221
         smooth: true,
193 222
         symbol: 'circle',
194 223
         symbolSize: 4,
195 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 229
         type: 'line',
201 230
         yAxisIndex: 1,
202 231
         smooth: true,
203 232
         symbol: 'circle',
204 233
         symbolSize: 4,
205 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 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 247
   return {
218 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 253
     grid: { ...GRID, left: 10, right: 8, top: 36, bottom: 20, containLabel: true },
221 254
     xAxis: {
222 255
       type: 'category',
223
-      data: MONTHS.map((m) => m.replace('月', '')),
256
+      data: axis.categories,
224 257
       axisLabel: {
225 258
         ...AXIS_LABEL,
226 259
         fontSize: 9,
@@ -244,24 +277,28 @@ export function buildEnvGovernanceOption(monthly, hasData) {
244 277
         barMaxWidth: 8,
245 278
         barCategoryGap: '35%',
246 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 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 292
   return {
259 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 299
     xAxis: {
263 300
       type: 'category',
264
-      data: MONTHS,
301
+      data: axis.categories,
265 302
       axisLabel: { ...AXIS_LABEL, rotate: 35, interval: 0, fontSize: 9 },
266 303
       axisLine: AXIS_LINE
267 304
     },
@@ -275,30 +312,34 @@ export function buildGreenAreaOption(monthly, hasData) {
275 312
     },
276 313
     series: [
277 314
       {
278
-        name: '绿化面积',
315
+        name: '新增绿化面积',
279 316
         type: 'line',
280 317
         smooth: true,
281 318
         symbol: 'circle',
282 319
         symbolSize: 4,
283 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 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 333
   return {
296 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 339
     grid: { ...GRID, left: 10, right: 8, bottom: 20, containLabel: true },
299 340
     xAxis: {
300 341
       type: 'category',
301
-      data: MONTHS.map((m) => m.replace('月', '')),
342
+      data: axis.categories,
302 343
       axisLabel: {
303 344
         ...AXIS_LABEL,
304 345
         fontSize: 9,
@@ -317,32 +358,29 @@ export function buildCulturalHeritageOption(monthly, hasData) {
317 358
     },
318 359
     series: [
319 360
       {
320
-        name: '人数',
361
+        name: '文化传承人数',
321 362
         type: 'bar',
322 363
         barMaxWidth: 8,
323 364
         barCategoryGap: '35%',
324 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 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 378
   return {
341 379
     color: ['#5ef0c8', '#1a4a6e'],
342 380
     tooltip: {
343 381
       trigger: 'axis',
344 382
       axisPointer: { type: 'shadow' },
345
-      formatter: formatMonthAxisTooltip
383
+      formatter: (params) => rollingAxisTooltip(params, axis, (p) => `${p.value ?? 0} 个`)
346 384
     },
347 385
     legend: {
348 386
       top: 4,
@@ -354,7 +392,7 @@ export function buildEthnicUnityStackedOption(monthly, hasData) {
354 392
     grid: { ...GRID, left: 10, right: 8, top: 36, bottom: 20, containLabel: true },
355 393
     xAxis: {
356 394
       type: 'category',
357
-      data: MONTHS.map((m) => m.replace('月', '')),
395
+      data: axis.categories,
358 396
       axisLabel: {
359 397
         ...AXIS_LABEL,
360 398
         fontSize: 9,
@@ -373,19 +411,19 @@ export function buildEthnicUnityStackedOption(monthly, hasData) {
373 411
     },
374 412
     series: [
375 413
       {
376
-        name: '民族融合案例',
414
+        name: '民族融合项目数',
377 415
         type: 'bar',
378 416
         stack: 'ethnic',
379 417
         barMaxWidth: 8,
380 418
         barCategoryGap: '35%',
381
-        data: monthSeries(monthly, 'ethnicIntegrationProjectCount')
419
+        data: rollingSeries(monthly, 'ethnicIntegrationProjectCount')
382 420
       },
383 421
       {
384
-        name: '民族团结活动',
422
+        name: '民族团结活动',
385 423
         type: 'bar',
386 424
         stack: 'ethnic',
387 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 433
 export function buildProjectTypePieOption(projectTypeStats, hasProjectData) {
396 434
   if (!hasProjectData || !projectTypeStats?.length) {
397
-    return emptyOption('暂无项目数据')
435
+    return emptyOption('本年度暂无共富项目')
398 436
   }
399 437
   const data = projectTypeStats
400 438
     .filter((item) => (item.count ?? 0) > 0)
@@ -404,7 +442,7 @@ export function buildProjectTypePieOption(projectTypeStats, hasProjectData) {
404 442
       itemStyle: { color: PROJECT_TYPE_COLOR[i % PROJECT_TYPE_COLOR.length] }
405 443
     }))
406 444
   if (!data.length) {
407
-    return emptyOption('暂无项目数据')
445
+    return emptyOption('本年度暂无共富项目')
408 446
   }
409 447
   return {
410 448
     tooltip: {

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

@@ -1,6 +1,6 @@
1 1
 <template>
2 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 5
     <!-- <div class="cp-year-bar">
6 6
       <label class="cp-year-bar__label">统计年份</label>
@@ -17,67 +17,72 @@
17 17
     <div class="screen-page--home-column">
18 18
       <div class="screen-page--home-column-top">
19 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 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 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 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 49
             </div>
47
-            <div class="content_title"><div>环境治理项目数</div></div>
48 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 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 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 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 79
             </div>
77
-            <div class="content_title"><div>区域公共品牌数</div></div>
78 80
           </div>
81
+        </template>
82
+        <div v-else class="cp-overview-empty">
83
+          <p>暂无该年度共富成果数据</p>
84
+          <p class="cp-overview-empty__hint">请在共同富裕成果中维护</p>
79 85
         </div>
80
-        <p v-if="achievement && !achievement.hasData" class="cp-no-achievement">本年度暂无成果台账数据</p>
81 86
       </div>
82 87
 
83 88
       <div class="screen-page--home-column-flex">
@@ -114,7 +119,7 @@
114 119
     <div class="screen-page--home-column">
115 120
       <div class="screen-page--home-column-right cp-digital-panel">
116 121
         <div class="top_title">数字赋能 གྲངས་ཀ་སྟོབས་སྦྱོང་།</div>
117
-        <div class="cp-digital-body">
122
+        <div v-if="hasAchievementData" class="cp-digital-body">
118 123
           <div class="cp-digital-group">
119 124
             <div class="cp-digital-group__title">品牌增值</div>
120 125
             <div class="cp-rings">
@@ -162,6 +167,10 @@
162 167
             </div>
163 168
           </div>
164 169
         </div>
170
+        <div v-else class="cp-digital-empty">
171
+          <p>暂无该年度共富成果数据</p>
172
+          <p class="cp-digital-empty__hint">请在共同富裕成果中维护</p>
173
+        </div>
165 174
       </div>
166 175
 
167 176
       <div class="screen-page--home-column-flex">
@@ -191,7 +200,7 @@
191 200
           <div class="flex_content flex_content--projects">
192 201
             <div class="cp-project-scroll" @scroll="onProjectScroll">
193 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 204
               <div
196 205
                 v-for="row in projectRows"
197 206
                 :key="row.id"
@@ -637,6 +646,7 @@ onUnmounted(() => {
637 646
   color: #ffb4b4;
638 647
   background: rgba(80, 20, 20, 0.75);
639 648
   border-radius: 4px;
649
+  cursor: pointer;
640 650
 }
641 651
 
642 652
 .cp-year-bar {
@@ -682,15 +692,48 @@ onUnmounted(() => {
682 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 702
   text-align: center;
691
-  font-size: 11px;
692
-  color: rgba(168, 212, 200, 0.7);
703
+}
704
+
705
+.cp-overview-empty p {
693 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 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 6
 const GRID = { left: 40, right: 16, top: 36, bottom: 28, containLabel: true }
7 7
 /** 解除时效柱图:预留 y 轴单位「天」空间,下边距收紧 */
8 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 13
 const SAMPLE_SOURCE_ORDER = [4, 1, 3, 2]
@@ -169,13 +170,38 @@ function pickAvgRelieveDays(item) {
169 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 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 297
     return emptyOption('暂无实验室数据')
275 298
   }
276
-  const monthly = labDetection.monthly
299
+  const axis = buildRollingAxis(monthly)
277 300
   return {
278 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 313
     legend: {
281 314
       top: 2,
282 315
       left: 'center',
@@ -287,7 +320,7 @@ export function buildLabDetectionOption(labDetection, hasLabData) {
287 320
     grid: LAB_GRID,
288 321
     xAxis: {
289 322
       type: 'category',
290
-      data: MONTHS,
323
+      data: axis.categories,
291 324
       axisLabel: { ...LAB_MONTH_AXIS_LABEL, interval: 0 },
292 325
       axisLine: AXIS_LINE,
293 326
       axisTick: { show: false }
@@ -306,7 +339,7 @@ export function buildLabDetectionOption(labDetection, hasLabData) {
306 339
         smooth: true,
307 340
         symbol: 'circle',
308 341
         symbolSize: 4,
309
-        data: monthSeries(monthly, 'testQuantity')
342
+        data: rollingSeries(monthly, 'testQuantity')
310 343
       },
311 344
       {
312 345
         name: '阳性数',
@@ -315,7 +348,7 @@ export function buildLabDetectionOption(labDetection, hasLabData) {
315 348
         symbol: 'circle',
316 349
         symbolSize: 4,
317 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 1
 <template>
2 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 5
     <!-- <div class="er-year-bar">
6 6
       <label class="er-year-bar__label">统计年份</label>
@@ -67,13 +67,19 @@
67 67
           <div class="flex_content flex_content--lab">
68 68
             <div class="lab-summary">
69 69
               <div class="lab-summary__item">
70
-                <div class="lab-summary__label">样本检测数</div>
70
+                <div class="lab-summary__label">检测量</div>
71 71
                 <div class="lab-summary__val">
72 72
                   <strong>{{ display(labDetection?.testQuantityTotal) }}</strong> 份
73 73
                 </div>
74 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 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 83
                 <div class="lab-summary__val">
78 84
                   <strong>{{ formatRate(labDetection?.positiveRate) }}</strong>
79 85
                 </div>
@@ -261,9 +267,7 @@ const reportLoadingMore = ref(false)
261 267
 const reportScrollLoading = ref(false)
262 268
 
263 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 271
 const riskLevelChartOption = computed(() =>
268 272
   buildRiskLevelPieOption(riskLevelStats.value, hasEpidemicData.value)
269 273
 )
@@ -547,6 +551,7 @@ onUnmounted(() => {
547 551
   color: #ffb4b4;
548 552
   background: rgba(80, 20, 20, 0.75);
549 553
   border-radius: 4px;
554
+  cursor: pointer;
550 555
 }
551 556
 
552 557
 .er-year-bar {
@@ -950,8 +955,8 @@ onUnmounted(() => {
950 955
 .lab-summary {
951 956
   display: flex;
952 957
   flex-direction: row;
953
-  gap: 6px;
954
-  height: 28px;
958
+  gap: 4px;
959
+  height: 32px;
955 960
   flex-shrink: 0;
956 961
 }
957 962
 
@@ -967,9 +972,9 @@ onUnmounted(() => {
967 972
 }
968 973
 
969 974
 .lab-summary__label {
970
-  font-size: 10px;
975
+  font-size: 9px;
971 976
   color: #a8d4c8;
972
-  line-height: 14px;
977
+  line-height: 13px;
973 978
 }
974 979
 
975 980
 .lab-summary__val {
@@ -979,7 +984,7 @@ onUnmounted(() => {
979 984
 }
980 985
 
981 986
 .lab-summary__val strong {
982
-  font-size: 14px;
987
+  font-size: 13px;
983 988
   background: linear-gradient(to bottom, #98e9aa, #ecd27b);
984 989
   -webkit-background-clip: text;
985 990
   background-clip: text;

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

@@ -2,7 +2,7 @@
2 2
   <div class="screen-page screen-page--home" :class="{ 'is-loading': loading }">
3 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 6
       <label class="home-year-bar__label">统计年份</label>
7 7
       <select
8 8
         v-model="statYear"
@@ -13,7 +13,7 @@
13 13
         <option v-for="y in availableYears" :key="y" :value="String(y)">{{ y }}年</option>
14 14
       </select>
15 15
       <span v-if="statDate" class="home-year-bar__date">统计日 {{ statDate }}</span>
16
-    </div>
16
+    </div> -->
17 17
 
18 18
     <div class="screen-page--home-column">
19 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 54
   return {
55 55
     ...chartBase(),
56 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 70
     legend: {
59 71
       top: 0,
60 72
       right: 8,
@@ -81,7 +93,6 @@ export function buildActivityTrendOption(activityTrend) {
81 93
       {
82 94
         name: '会话数',
83 95
         type: 'line',
84
-        stack: 'total',
85 96
         smooth: true,
86 97
         symbol: 'circle',
87 98
         symbolSize: 4,
@@ -91,7 +102,6 @@ export function buildActivityTrendOption(activityTrend) {
91 102
       {
92 103
         name: '提问量',
93 104
         type: 'line',
94
-        stack: 'total',
95 105
         smooth: true,
96 106
         symbol: 'circle',
97 107
         symbolSize: 4,
@@ -235,19 +245,31 @@ export function buildModelCallOption(modelCallAnalysis) {
235 245
   }
236 246
 }
237 247
 
238
-/** 分时使用热度 — 会话柱 + 提问量折线(24 小时) */
248
+/** 分时使用热度 — 最近 24 整点小时(会话柱 + 提问量折线) */
239 249
 export function buildHourlyHeatOption(hourlyHeat) {
240 250
   const series = hourlyHeat?.hourlySeries || []
241 251
   if (!series.length) {
242 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 257
   return {
248 258
     ...chartBase(),
249 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 273
     legend: {
252 274
       top: 0,
253 275
       right: 8,
@@ -262,7 +284,7 @@ export function buildHourlyHeatOption(hourlyHeat) {
262 284
       axisLabel: {
263 285
         ...AXIS_LABEL,
264 286
         fontSize: 8,
265
-        interval: (idx) => idx % 4 === 0
287
+        interval: (idx) => idx % 3 === 0
266 288
       },
267 289
       axisLine: AXIS_LINE
268 290
     },

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

@@ -1,6 +1,19 @@
1 1
 <template>
2 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 19
     <div class="screen-page--home-column">
@@ -157,6 +170,8 @@ import {
157 170
 const loading = ref(false)
158 171
 const loadError = ref('')
159 172
 const statYear = ref(String(new Date().getFullYear()))
173
+const statDate = ref('')
174
+const availableYears = ref([Number(statYear.value)])
160 175
 
161 176
 const overview = ref(null)
162 177
 const activityTrend = ref(null)
@@ -298,6 +313,10 @@ function applyDashboard(data) {
298 313
   if (data.statYear) {
299 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 320
   overview.value = data.overview || null
302 321
   activityTrend.value = normalizeActivityTrend(data.activityTrend)
303 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 352
 onMounted(() => {
330 353
   fetchDashboard()
331 354
 })
@@ -358,6 +381,39 @@ onMounted(() => {
358 381
   color: #ffb4b4;
359 382
   background: rgba(80, 20, 20, 0.75);
360 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 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 3
 import { buildMallPie2DOption } from '@/utils/mallPie2d'
4 4
 
@@ -6,7 +6,6 @@ const AXIS_LABEL = { color: '#9fb0c3', fontSize: 10 }
6 6
 const AXIS_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.35)' } }
7 7
 const SPLIT_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.12)' } }
8 8
 const GRID = { left: 40, right: 16, top: 36, bottom: 28, containLabel: true }
9
-const MONTHS = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
10 9
 
11 10
 const DESTINATION_COLOR = ['#5ef0c8', '#ecd27b', '#6eb5ff']
12 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 83
 function formatQuoteDate(quoteDate) {
@@ -40,7 +86,9 @@ function formatQuoteDate(quoteDate) {
40 86
   }
41 87
   const parts = String(quoteDate).split('-')
42 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 93
   return quoteDate
46 94
 }
@@ -63,11 +111,13 @@ export function buildOriginQuoteOption(originQuote) {
63 111
     tooltip: {
64 112
       trigger: 'axis',
65 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 123
     legend: {
@@ -120,19 +170,26 @@ export function buildOriginQuoteOption(originQuote) {
120 170
   }
121 171
 }
122 172
 
123
-/** 牦牛交易数额趋势 — 销售量(头) + 销售额(元) */
173
+/** 牦牛交易数额趋势 — 销售量(头) + 销售额(元)折线(滚动 12 月) */
124 174
 export function buildTradeMonthlyOption(tradeMonthlyTrend) {
125 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 176
     return emptyOption('暂无交易数据')
132 177
   }
178
+  const axis = buildRollingAxis(tradeMonthlyTrend)
179
+  const heads = rollingSeries(tradeMonthlyTrend, 'tradeHeads')
180
+  const amounts = rollingSeries(tradeMonthlyTrend, 'tradeAmount')
133 181
   return {
134 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 193
     legend: {
137 194
       top: 0,
138 195
       left: 'center',
@@ -143,7 +200,7 @@ export function buildTradeMonthlyOption(tradeMonthlyTrend) {
143 200
     grid: { ...GRID, bottom: 10 },
144 201
     xAxis: {
145 202
       type: 'category',
146
-      data: MONTHS,
203
+      data: axis.categories,
147 204
       axisLabel: { ...AXIS_LABEL, rotate: 35, interval: 0, fontSize: 9 },
148 205
       axisLine: AXIS_LINE
149 206
     },
@@ -172,11 +229,9 @@ export function buildTradeMonthlyOption(tradeMonthlyTrend) {
172 229
     series: [
173 230
       {
174 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 235
         data: heads
181 236
       },
182 237
       {
@@ -186,6 +241,7 @@ export function buildTradeMonthlyOption(tradeMonthlyTrend) {
186 241
         smooth: true,
187 242
         symbol: 'circle',
188 243
         symbolSize: 4,
244
+        lineStyle: { width: 2, color: '#ecd27b' },
189 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 391
 export function buildMallOrderTrendOption(mallOrderTrend, mallStatsAvailable = true) {
336 392
   if (mallStatsAvailable === false) {
337 393
     return emptyOption(MALL_UNAVAILABLE_TEXT)
338 394
   }
339
-  if (!mallOrderTrend?.items?.length) {
395
+  const items = mallOrderTrend?.items || []
396
+  if (!items.length) {
340 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 401
   return {
347 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 408
     grid: { ...GRID, top: 12, bottom: 10 },
350 409
     xAxis: {
351 410
       type: 'category',
352
-      data: MONTHS,
411
+      data: axis.categories,
353 412
       axisLabel: { ...AXIS_LABEL, rotate: 35, interval: 0, fontSize: 9 },
354 413
       axisLine: AXIS_LINE
355 414
     },

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

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