|
|
@@ -1,8 +1,10 @@
|
|
1
|
1
|
<template>
|
|
2
|
2
|
<div class="screen-page ts-page" :class="{ 'is-loading': loading }">
|
|
3
|
|
- <div v-if="loadError" class="ts-error">{{ loadError }}</div>
|
|
4
|
|
-
|
|
5
|
|
- <!-- <div class="ts-year-bar">
|
|
|
3
|
+ <div v-if="loadError" class="ts-error" @click="onRetry">
|
|
|
4
|
+ {{ loadError }}(点击重试)
|
|
|
5
|
+ </div>
|
|
|
6
|
+<!--
|
|
|
7
|
+ <div class="ts-year-bar">
|
|
6
|
8
|
<label class="ts-year-bar__label">统计年份</label>
|
|
7
|
9
|
<select
|
|
8
|
10
|
v-model="statYear"
|
|
|
@@ -13,9 +15,17 @@
|
|
13
|
15
|
<option v-for="y in availableYears" :key="y" :value="String(y)">{{ y }}年</option>
|
|
14
|
16
|
</select>
|
|
15
|
17
|
<span v-if="statDate" class="ts-year-bar__date">统计日 {{ statDate }}</span>
|
|
|
18
|
+ <button
|
|
|
19
|
+ type="button"
|
|
|
20
|
+ class="ts-year-bar__refresh"
|
|
|
21
|
+ :disabled="loading || refreshing"
|
|
|
22
|
+ @click="onManualRefresh"
|
|
|
23
|
+ >
|
|
|
24
|
+ {{ refreshing ? '刷新中…' : '刷新' }}
|
|
|
25
|
+ </button>
|
|
16
|
26
|
</div> -->
|
|
17
|
27
|
|
|
18
|
|
- <!-- 左栏:牦牛交易(对接 §3.1) -->
|
|
|
28
|
+ <!-- 左栏:牦牛交易 -->
|
|
19
|
29
|
<div class="screen-page--home-column">
|
|
20
|
30
|
<div class="screen-page--home-column-top">
|
|
21
|
31
|
<div class="top_title">交易总览 ཚོང་འདོན་ཀྱི་རྒྱུན་བསྡུས།</div>
|
|
|
@@ -59,6 +69,12 @@
|
|
59
|
69
|
<strong>{{ formatPrice(originQuote?.maxPrice7d) }}</strong>
|
|
60
|
70
|
</div>
|
|
61
|
71
|
</div>
|
|
|
72
|
+ <div class="quote-summary__item">
|
|
|
73
|
+ <div class="quote-summary__label">最近行情日</div>
|
|
|
74
|
+ <div class="quote-summary__val quote-summary__val--date">
|
|
|
75
|
+ <strong>{{ originQuote?.lastQuoteDate || '—' }}</strong>
|
|
|
76
|
+ </div>
|
|
|
77
|
+ </div>
|
|
62
|
78
|
</div>
|
|
63
|
79
|
<div class="quote-chart">
|
|
64
|
80
|
<ScreenChart :option="originQuoteChartOption" />
|
|
|
@@ -89,10 +105,13 @@
|
|
89
|
105
|
</div>
|
|
90
|
106
|
</div>
|
|
91
|
107
|
|
|
92
|
|
- <!-- 右栏:农资商城(对接 §8~13) -->
|
|
93
|
|
- <div class="screen-page--home-column">
|
|
|
108
|
+ <!-- 右栏:农资商城 -->
|
|
|
109
|
+ <div class="screen-page--home-column" :class="{ 'ts-mall-unavailable': !mallStatsAvailable }">
|
|
94
|
110
|
<div class="screen-page--home-column-right ts-mall-panel">
|
|
95
|
|
- <div class="top_title">农资品类销售 ཞིང་ལས་རྒྱུ་ཆ་རིགས་ཀྱི་ཚོང་འདོན།</div>
|
|
|
111
|
+ <div class="top_title">
|
|
|
112
|
+ 农资品类销售 ཞིང་ལས་རྒྱུ་ཆ་རིགས་ཀྱི་ཚོང་འདོན།
|
|
|
113
|
+ <span v-if="categoryStatDate" class="ts-mall-stat-date">({{ categoryStatDate }})</span>
|
|
|
114
|
+ </div>
|
|
96
|
115
|
<div class="ts-mall-top">
|
|
97
|
116
|
<div class="ts-mall-top__pie">
|
|
98
|
117
|
<ScreenChart :option="categorySalesChartOption" />
|
|
|
@@ -103,7 +122,7 @@
|
|
103
|
122
|
<li v-for="item in hotCategoryItems" :key="item.rank" class="ts-mall-hot-list__row">
|
|
104
|
123
|
<span class="ts-mall-hot-list__rank">{{ item.rank }}</span>
|
|
105
|
124
|
<span class="ts-mall-hot-list__name" :title="item.categoryName">{{ item.categoryName }}</span>
|
|
106
|
|
- <span class="ts-mall-hot-list__qty">{{ formatNum(item.qty) }}</span>
|
|
|
125
|
+ <span class="ts-mall-hot-list__qty">{{ formatNum(item.qty) }} 件</span>
|
|
107
|
126
|
</li>
|
|
108
|
127
|
</ul>
|
|
109
|
128
|
<span v-else class="ts-placeholder ts-placeholder--sm">{{ mallEmptyHint }}</span>
|
|
|
@@ -143,7 +162,7 @@
|
|
143
|
162
|
</template>
|
|
144
|
163
|
|
|
145
|
164
|
<script setup>
|
|
146
|
|
-import { computed, onMounted, ref } from 'vue'
|
|
|
165
|
+import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
147
|
166
|
import ScreenChart from '@/components/ScreenChart.vue'
|
|
148
|
167
|
import { getTradeSalesDashboard } from '@/api/tradeSales'
|
|
149
|
168
|
import {
|
|
|
@@ -158,11 +177,15 @@ import {
|
|
158
|
177
|
buildTradeMonthlyOption
|
|
159
|
178
|
} from './chartOptions'
|
|
160
|
179
|
|
|
|
180
|
+/** 自动轮播刷新周期(对齐功能需求 §7.3,默认 3 分钟) */
|
|
|
181
|
+const DASHBOARD_REFRESH_MS = 3 * 60 * 1000
|
|
|
182
|
+
|
|
161
|
183
|
const loading = ref(false)
|
|
|
184
|
+const refreshing = ref(false)
|
|
162
|
185
|
const loadError = ref('')
|
|
163
|
186
|
const statYear = ref(String(new Date().getFullYear()))
|
|
164
|
187
|
const statDate = ref('')
|
|
165
|
|
-const availableYears = ref([])
|
|
|
188
|
+const availableYears = ref([new Date().getFullYear()])
|
|
166
|
189
|
|
|
167
|
190
|
const tradeOverview = ref(null)
|
|
168
|
191
|
const originQuote = ref(null)
|
|
|
@@ -177,15 +200,25 @@ const shopEntry = ref(null)
|
|
177
|
200
|
const regionRank = ref(null)
|
|
178
|
201
|
const reviewWordCloud = ref(null)
|
|
179
|
202
|
|
|
|
203
|
+let dashboardTimer = null
|
|
|
204
|
+
|
|
|
205
|
+const mallAvailable = computed(() => mallStatsAvailable.value)
|
|
|
206
|
+
|
|
180
|
207
|
const originQuoteChartOption = computed(() => buildOriginQuoteOption(originQuote.value))
|
|
181
|
208
|
const monthlyTrendChartOption = computed(() => buildTradeMonthlyOption(tradeMonthlyTrend.value))
|
|
182
|
209
|
const destinationChartOption = computed(() => buildSalesDestinationBarOption(salesDestination.value))
|
|
183
|
210
|
const qualityGradeChartOption = computed(() => buildQualityGradePieOption(qualityGrade.value))
|
|
184
|
|
-const categorySalesChartOption = computed(() => buildCategorySalesPieOption(categorySales.value))
|
|
185
|
|
-const mallOrderTrendChartOption = computed(() => buildMallOrderTrendOption(mallOrderTrend.value))
|
|
186
|
|
-const shopEntryChartOption = computed(() => buildShopEntryPieOption(shopEntry.value))
|
|
187
|
|
-const regionRankChartOption = computed(() => buildRegionRankBarOption(regionRank.value))
|
|
188
|
|
-const reviewWordCloudChartOption = computed(() => buildReviewWordCloudOption(reviewWordCloud.value))
|
|
|
211
|
+const categorySalesChartOption = computed(() =>
|
|
|
212
|
+ buildCategorySalesPieOption(categorySales.value, mallAvailable.value)
|
|
|
213
|
+)
|
|
|
214
|
+const mallOrderTrendChartOption = computed(() =>
|
|
|
215
|
+ buildMallOrderTrendOption(mallOrderTrend.value, mallAvailable.value)
|
|
|
216
|
+)
|
|
|
217
|
+const shopEntryChartOption = computed(() => buildShopEntryPieOption(shopEntry.value, mallAvailable.value))
|
|
|
218
|
+const regionRankChartOption = computed(() => buildRegionRankBarOption(regionRank.value, mallAvailable.value))
|
|
|
219
|
+const reviewWordCloudChartOption = computed(() =>
|
|
|
220
|
+ buildReviewWordCloudOption(reviewWordCloud.value, mallAvailable.value)
|
|
|
221
|
+)
|
|
189
|
222
|
|
|
190
|
223
|
const hotCategoryItems = computed(() => {
|
|
191
|
224
|
const items = hotCategoryRank.value?.items || []
|
|
|
@@ -194,13 +227,17 @@ const hotCategoryItems = computed(() => {
|
|
194
|
227
|
|
|
195
|
228
|
const mallEmptyHint = computed(() => (mallStatsAvailable.value ? '暂无热销数据' : '商城统计未接入'))
|
|
196
|
229
|
|
|
|
230
|
+const categoryStatDate = computed(
|
|
|
231
|
+ () => categorySales.value?.statDate || hotCategoryRank.value?.statDate || ''
|
|
|
232
|
+)
|
|
|
233
|
+
|
|
197
|
234
|
const tradeOverviewItems = computed(() => {
|
|
198
|
235
|
const o = tradeOverview.value
|
|
199
|
236
|
return [
|
|
200
|
237
|
{
|
|
201
|
238
|
key: 'orderCount',
|
|
202
|
239
|
label: '牦牛交易订单数',
|
|
203
|
|
- unit: '笔',
|
|
|
240
|
+ unit: '单',
|
|
204
|
241
|
value: display(o?.orderCount),
|
|
205
|
242
|
placeholder: false
|
|
206
|
243
|
},
|
|
|
@@ -293,6 +330,21 @@ function formatPrice(val) {
|
|
293
|
330
|
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
294
|
331
|
}
|
|
295
|
332
|
|
|
|
333
|
+function resetDashboard() {
|
|
|
334
|
+ tradeOverview.value = null
|
|
|
335
|
+ originQuote.value = null
|
|
|
336
|
+ tradeMonthlyTrend.value = []
|
|
|
337
|
+ salesDestination.value = []
|
|
|
338
|
+ qualityGrade.value = null
|
|
|
339
|
+ mallStatsAvailable.value = false
|
|
|
340
|
+ categorySales.value = null
|
|
|
341
|
+ hotCategoryRank.value = null
|
|
|
342
|
+ mallOrderTrend.value = null
|
|
|
343
|
+ shopEntry.value = null
|
|
|
344
|
+ regionRank.value = null
|
|
|
345
|
+ reviewWordCloud.value = null
|
|
|
346
|
+}
|
|
|
347
|
+
|
|
296
|
348
|
function applyDashboard(data) {
|
|
297
|
349
|
if (!data) {
|
|
298
|
350
|
return
|
|
|
@@ -309,7 +361,7 @@ function applyDashboard(data) {
|
|
309
|
361
|
tradeMonthlyTrend.value = data.tradeMonthlyTrend || []
|
|
310
|
362
|
salesDestination.value = data.salesDestination || []
|
|
311
|
363
|
qualityGrade.value = data.qualityGrade || null
|
|
312
|
|
- mallStatsAvailable.value = !!data.mallStatsAvailable
|
|
|
364
|
+ mallStatsAvailable.value = data.mallStatsAvailable || false
|
|
313
|
365
|
categorySales.value = data.categorySales || null
|
|
314
|
366
|
hotCategoryRank.value = data.hotCategoryRank || null
|
|
315
|
367
|
mallOrderTrend.value = data.mallOrderTrend || null
|
|
|
@@ -318,16 +370,25 @@ function applyDashboard(data) {
|
|
318
|
370
|
reviewWordCloud.value = data.reviewWordCloud || null
|
|
319
|
371
|
}
|
|
320
|
372
|
|
|
321
|
|
-async function fetchDashboard() {
|
|
322
|
|
- loading.value = true
|
|
|
373
|
+async function fetchDashboard(forceRefresh = false) {
|
|
|
374
|
+ if (forceRefresh) {
|
|
|
375
|
+ if (refreshing.value) {
|
|
|
376
|
+ return
|
|
|
377
|
+ }
|
|
|
378
|
+ refreshing.value = true
|
|
|
379
|
+ } else {
|
|
|
380
|
+ loading.value = true
|
|
|
381
|
+ resetDashboard()
|
|
|
382
|
+ }
|
|
323
|
383
|
loadError.value = ''
|
|
324
|
384
|
try {
|
|
325
|
|
- const res = await getTradeSalesDashboard(statYear.value)
|
|
|
385
|
+ const res = await getTradeSalesDashboard(statYear.value, forceRefresh)
|
|
326
|
386
|
applyDashboard(res.data)
|
|
327
|
387
|
} catch (e) {
|
|
328
|
388
|
loadError.value = e?.message || '看板数据加载失败'
|
|
329
|
389
|
} finally {
|
|
330
|
390
|
loading.value = false
|
|
|
391
|
+ refreshing.value = false
|
|
331
|
392
|
}
|
|
332
|
393
|
}
|
|
333
|
394
|
|
|
|
@@ -335,8 +396,30 @@ function onYearChange() {
|
|
335
|
396
|
fetchDashboard()
|
|
336
|
397
|
}
|
|
337
|
398
|
|
|
|
399
|
+function onManualRefresh() {
|
|
|
400
|
+ fetchDashboard(true)
|
|
|
401
|
+}
|
|
|
402
|
+
|
|
|
403
|
+function onRetry() {
|
|
|
404
|
+ fetchDashboard()
|
|
|
405
|
+}
|
|
|
406
|
+
|
|
|
407
|
+function startDashboardTimer() {
|
|
|
408
|
+ dashboardTimer = setInterval(() => {
|
|
|
409
|
+ fetchDashboard(true)
|
|
|
410
|
+ }, DASHBOARD_REFRESH_MS)
|
|
|
411
|
+}
|
|
|
412
|
+
|
|
338
|
413
|
onMounted(() => {
|
|
339
|
414
|
fetchDashboard()
|
|
|
415
|
+ startDashboardTimer()
|
|
|
416
|
+})
|
|
|
417
|
+
|
|
|
418
|
+onUnmounted(() => {
|
|
|
419
|
+ if (dashboardTimer) {
|
|
|
420
|
+ clearInterval(dashboardTimer)
|
|
|
421
|
+ dashboardTimer = null
|
|
|
422
|
+ }
|
|
340
|
423
|
})
|
|
341
|
424
|
</script>
|
|
342
|
425
|
|
|
|
@@ -367,6 +450,7 @@ onMounted(() => {
|
|
367
|
450
|
color: #ffb4b4;
|
|
368
|
451
|
background: rgba(80, 20, 20, 0.75);
|
|
369
|
452
|
border-radius: 4px;
|
|
|
453
|
+ cursor: pointer;
|
|
370
|
454
|
}
|
|
371
|
455
|
|
|
372
|
456
|
.ts-year-bar {
|
|
|
@@ -401,6 +485,22 @@ onMounted(() => {
|
|
401
|
485
|
color: rgba(168, 212, 200, 0.75);
|
|
402
|
486
|
}
|
|
403
|
487
|
|
|
|
488
|
+.ts-year-bar__refresh {
|
|
|
489
|
+ height: 28px;
|
|
|
490
|
+ padding: 0 10px;
|
|
|
491
|
+ font-size: 12px;
|
|
|
492
|
+ color: #e8eef5;
|
|
|
493
|
+ background: rgba(4, 48, 40, 0.85);
|
|
|
494
|
+ border: 1px solid var(--screen-line-dim, rgba(61, 217, 176, 0.42));
|
|
|
495
|
+ border-radius: 4px;
|
|
|
496
|
+ cursor: pointer;
|
|
|
497
|
+}
|
|
|
498
|
+
|
|
|
499
|
+.ts-year-bar__refresh:disabled {
|
|
|
500
|
+ opacity: 0.6;
|
|
|
501
|
+ cursor: not-allowed;
|
|
|
502
|
+}
|
|
|
503
|
+
|
|
404
|
504
|
.screen-page--home-column {
|
|
405
|
505
|
width: 617px;
|
|
406
|
506
|
height: 100%;
|
|
|
@@ -431,6 +531,12 @@ onMounted(() => {
|
|
431
|
531
|
flex-direction: column;
|
|
432
|
532
|
}
|
|
433
|
533
|
|
|
|
534
|
+.ts-mall-stat-date {
|
|
|
535
|
+ font-size: 12px;
|
|
|
536
|
+ color: rgba(168, 212, 200, 0.8);
|
|
|
537
|
+ margin-left: 4px;
|
|
|
538
|
+}
|
|
|
539
|
+
|
|
434
|
540
|
.ts-mall-top {
|
|
435
|
541
|
flex: 1;
|
|
436
|
542
|
min-height: 0;
|
|
|
@@ -613,7 +719,6 @@ onMounted(() => {
|
|
613
|
719
|
}
|
|
614
|
720
|
|
|
615
|
721
|
.content_title {
|
|
616
|
|
-
|
|
617
|
722
|
font-size: 11px;
|
|
618
|
723
|
color: #a8d4c8;
|
|
619
|
724
|
line-height: 1.3;
|
|
|
@@ -646,13 +751,15 @@ onMounted(() => {
|
|
646
|
751
|
.quote-summary {
|
|
647
|
752
|
display: flex;
|
|
648
|
753
|
flex-direction: row;
|
|
649
|
|
- gap: 6px;
|
|
650
|
|
- height: 42px;
|
|
|
754
|
+ flex-wrap: wrap;
|
|
|
755
|
+ gap: 4px;
|
|
|
756
|
+ min-height: 42px;
|
|
651
|
757
|
flex-shrink: 0;
|
|
652
|
758
|
}
|
|
653
|
759
|
|
|
654
|
760
|
.quote-summary__item {
|
|
655
|
|
- flex: 1;
|
|
|
761
|
+ flex: 1 1 calc(50% - 4px);
|
|
|
762
|
+ min-width: 0;
|
|
656
|
763
|
display: flex;
|
|
657
|
764
|
flex-direction: column;
|
|
658
|
765
|
align-items: center;
|
|
|
@@ -660,28 +767,33 @@ onMounted(() => {
|
|
660
|
767
|
background: rgba(4, 48, 40, 0.45);
|
|
661
|
768
|
border-radius: 4px;
|
|
662
|
769
|
border: 1px solid rgba(61, 217, 176, 0.2);
|
|
|
770
|
+ padding: 2px 0;
|
|
663
|
771
|
}
|
|
664
|
772
|
|
|
665
|
773
|
.quote-summary__label {
|
|
666
|
|
- font-size: 10px;
|
|
|
774
|
+ font-size: 9px;
|
|
667
|
775
|
color: #a8d4c8;
|
|
668
|
776
|
}
|
|
669
|
777
|
|
|
670
|
778
|
.quote-summary__val {
|
|
671
|
|
- font-size: 11px;
|
|
|
779
|
+ font-size: 10px;
|
|
672
|
780
|
color: #fff;
|
|
673
|
781
|
}
|
|
674
|
782
|
|
|
675
|
783
|
.quote-summary__val strong {
|
|
676
|
|
- font-size: 14px;
|
|
|
784
|
+ font-size: 12px;
|
|
677
|
785
|
background: linear-gradient(to bottom, #98e9aa, #ecd27b);
|
|
678
|
786
|
-webkit-background-clip: text;
|
|
679
|
787
|
background-clip: text;
|
|
680
|
788
|
color: transparent;
|
|
681
|
789
|
}
|
|
682
|
790
|
|
|
683
|
|
-.quote-summary__unit {
|
|
|
791
|
+.quote-summary__val--date strong {
|
|
684
|
792
|
font-size: 10px;
|
|
|
793
|
+}
|
|
|
794
|
+
|
|
|
795
|
+.quote-summary__unit {
|
|
|
796
|
+ font-size: 9px;
|
|
685
|
797
|
color: #a8d4c8;
|
|
686
|
798
|
margin-left: 2px;
|
|
687
|
799
|
}
|
|
|
@@ -690,10 +802,4 @@ onMounted(() => {
|
|
690
|
802
|
flex: 1;
|
|
691
|
803
|
min-height: 0;
|
|
692
|
804
|
}
|
|
693
|
|
-
|
|
694
|
|
-.flex_content--placeholder {
|
|
695
|
|
- display: flex;
|
|
696
|
|
- align-items: center;
|
|
697
|
|
- justify-content: center;
|
|
698
|
|
-}
|
|
699
|
805
|
</style>
|