xsh_1997 před 1 měsícem
rodič
revize
b47e2a8d5e
31 změnil soubory, kde provedl 1155 přidání a 243 odebrání
  1. 161 0
      ruoyi-screen/src/utils/mallPie2d.js
  2. 20 4
      ruoyi-screen/src/views/epidemicRisk/chartOptions.js
  3. 63 14
      ruoyi-screen/src/views/epidemicRisk/index.vue
  4. 21 12
      ruoyi-screen/src/views/home/index.vue
  5. 23 33
      ruoyi-screen/src/views/livestockResources/chartOptions.js
  6. 31 20
      ruoyi-screen/src/views/livestockResources/index.vue
  7. 31 64
      ruoyi-screen/src/views/tradeSales/chartOptions.js
  8. 36 36
      ruoyi-screen/src/views/tradeSales/index.vue
  9. 9 0
      ruoyi-ui-app/api/livestockResource.js
  10. 51 0
      ruoyi-ui-app/locale/bo.js
  11. 51 0
      ruoyi-ui-app/locale/zh.js
  12. 414 0
      ruoyi-ui-app/package-a/livestock-resource-detail/index.vue
  13. 19 33
      ruoyi-ui-app/package-a/livestock-resources/index.vue
  14. 6 0
      ruoyi-ui-app/pages.json
  15. 1 1
      ruoyi-ui-app/pages/mine/index.vue
  16. 165 0
      ruoyi-ui-app/utils/livestockResourceDetail.js
  17. 1 0
      ruoyi-ui/src/lang/bo/breedingStd.js
  18. 1 0
      ruoyi-ui/src/lang/bo/techService.js
  19. 1 0
      ruoyi-ui/src/lang/zh/breedingStd.js
  20. 1 0
      ruoyi-ui/src/lang/zh/techService.js
  21. 16 0
      ruoyi-ui/src/mixins/breedingStdLocaleMixin.js
  22. 16 0
      ruoyi-ui/src/mixins/techServiceLocaleMixin.js
  23. 1 3
      ruoyi-ui/src/views/breedingStandards/breedingManage/index.vue
  24. 1 3
      ruoyi-ui/src/views/breedingStandards/diseaseTreatment/drugIncompatibility/index.vue
  25. 1 3
      ruoyi-ui/src/views/breedingStandards/diseaseTreatment/epidemicTreatment/index.vue
  26. 1 3
      ruoyi-ui/src/views/breedingStandards/diseaseTreatment/withdrawalPeriod/index.vue
  27. 1 3
      ruoyi-ui/src/views/breedingStandards/equipmentOperation/index.vue
  28. 1 3
      ruoyi-ui/src/views/breedingStandards/feeding/index.vue
  29. 1 3
      ruoyi-ui/src/views/breedingStandards/growthOutbound/index.vue
  30. 1 3
      ruoyi-ui/src/views/techService/knowledge/index.vue
  31. 9 2
      ruoyi-ui/src/views/tool/gen/index.vue

+ 161 - 0
ruoyi-screen/src/utils/mallPie2d.js

@@ -0,0 +1,161 @@
1
+/** 大屏 2D 环形饼图(渐变 + 中心汇总 + 悬停放大,交易销售/畜牧资源等复用) */
2
+
3
+export const MALL_PIE_COLORS = [
4
+  '#5ef0c8',
5
+  '#ecd27b',
6
+  '#6eb5ff',
7
+  '#22a85a',
8
+  '#c9a227',
9
+  '#8b7cf6',
10
+  '#ffb88c',
11
+  '#3d7dd8'
12
+]
13
+
14
+const MALL_PIE_LEGEND = {
15
+  type: 'scroll',
16
+  orient: 'vertical',
17
+  right: 2,
18
+  top: 'middle',
19
+  height: '72%',
20
+  itemWidth: 8,
21
+  itemHeight: 8,
22
+  itemGap: 5,
23
+  pageIconColor: '#5ef0c8',
24
+  pageIconInactiveColor: '#4a6b62',
25
+  pageTextStyle: { color: '#a8d4c8', fontSize: 9 },
26
+  textStyle: { color: '#a8d4c8', fontSize: 9 }
27
+}
28
+
29
+const MALL_PIE_TOOLTIP = {
30
+  trigger: 'item',
31
+  backgroundColor: 'rgba(4, 30, 26, 0.92)',
32
+  borderColor: 'rgba(61, 217, 176, 0.35)',
33
+  borderWidth: 1,
34
+  textStyle: { color: '#e8eef5', fontSize: 11 }
35
+}
36
+
37
+function clampByte(n) {
38
+  return Math.min(255, Math.max(0, Math.round(n)))
39
+}
40
+
41
+function tintHex(hex, ratio) {
42
+  const h = String(hex || '').replace('#', '')
43
+  if (h.length !== 6) {
44
+    return hex
45
+  }
46
+  const r = parseInt(h.slice(0, 2), 16)
47
+  const g = parseInt(h.slice(2, 4), 16)
48
+  const b = parseInt(h.slice(4, 6), 16)
49
+  const target = ratio >= 0 ? 255 : 0
50
+  const p = Math.abs(ratio)
51
+  const nr = clampByte(r + (target - r) * p)
52
+  const ng = clampByte(g + (target - g) * p)
53
+  const nb = clampByte(b + (target - b) * p)
54
+  return `#${nr.toString(16).padStart(2, '0')}${ng.toString(16).padStart(2, '0')}${nb.toString(16).padStart(2, '0')}`
55
+}
56
+
57
+function pieSliceStyle(color) {
58
+  return {
59
+    color: {
60
+      type: 'linear',
61
+      x: 0.2,
62
+      y: 0,
63
+      x2: 0.85,
64
+      y2: 1,
65
+      colorStops: [
66
+        { offset: 0, color: tintHex(color, 0.42) },
67
+        { offset: 0.45, color },
68
+        { offset: 1, color: tintHex(color, -0.28) }
69
+      ]
70
+    },
71
+    borderRadius: 6,
72
+    borderColor: 'rgba(4, 30, 26, 0.85)',
73
+    borderWidth: 2,
74
+    shadowBlur: 14,
75
+    shadowColor: 'rgba(0, 0, 0, 0.38)'
76
+  }
77
+}
78
+
79
+/** 2D 环形饼图(渐变 + 中心汇总 + 悬停放大) */
80
+export function buildMallPie2DOption(data, opts = {}) {
81
+  const {
82
+    colors = MALL_PIE_COLORS,
83
+    legend = {},
84
+    summaryTitle = '',
85
+    summarySubtext = '',
86
+    tooltipFormatter,
87
+    center = ['36%', '50%'],
88
+    radius = ['46%', '70%']
89
+  } = opts
90
+
91
+  const pieData = data.map((item, i) => {
92
+    const base = colors[i % colors.length]
93
+    return {
94
+      name: item.name,
95
+      value: item.value,
96
+      itemStyle: pieSliceStyle(base)
97
+    }
98
+  })
99
+
100
+  const hasSummary = summaryTitle !== ''
101
+  const centerLabelFormatter = () => `{title|${summaryTitle}}\n{sub|${summarySubtext}}`
102
+  const centerLabel = hasSummary
103
+    ? {
104
+        show: true,
105
+        position: 'center',
106
+        silent: true,
107
+        formatter: (params) => (params.dataIndex === 0 ? centerLabelFormatter() : ''),
108
+        rich: {
109
+          title: { color: '#5ef0c8', fontSize: 14, fontWeight: 700, lineHeight: 20 },
110
+          sub: { color: '#a8d4c8', fontSize: 9, lineHeight: 14 }
111
+        }
112
+      }
113
+    : { show: false }
114
+
115
+  return {
116
+    color: colors,
117
+    tooltip: {
118
+      ...MALL_PIE_TOOLTIP,
119
+      formatter: tooltipFormatter
120
+    },
121
+    legend: {
122
+      ...MALL_PIE_LEGEND,
123
+      data: data.map((d) => d.name),
124
+      ...legend
125
+    },
126
+    series: [
127
+      {
128
+        type: 'pie',
129
+        radius,
130
+        center,
131
+        minAngle: 4,
132
+        avoidLabelOverlap: true,
133
+        padAngle: 1.5,
134
+        itemStyle: {
135
+          shadowBlur: 10,
136
+          shadowColor: 'rgba(0, 0, 0, 0.35)'
137
+        },
138
+        emphasis: {
139
+          scale: true,
140
+          scaleSize: 12,
141
+          label: hasSummary
142
+            ? {
143
+                show: true,
144
+                position: 'center',
145
+                silent: true,
146
+                formatter: centerLabelFormatter,
147
+                rich: centerLabel.rich
148
+              }
149
+            : { show: false },
150
+          itemStyle: {
151
+            shadowBlur: 22,
152
+            shadowColor: 'rgba(94, 240, 200, 0.42)'
153
+          }
154
+        },
155
+        label: centerLabel,
156
+        labelLine: { show: false },
157
+        data: pieData
158
+      }
159
+    ]
160
+  }
161
+}

+ 20 - 4
ruoyi-screen/src/views/epidemicRisk/chartOptions.js

@@ -197,7 +197,18 @@ export function buildSampleSourceBarOption(sampleSourceStats) {
197 197
   const maxVal = Math.max(...values, 1)
198 198
   return {
199 199
     color: ['#ecd27b'],
200
-    tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
200
+    tooltip: {
201
+      trigger: 'axis',
202
+      axisPointer: { type: 'shadow' },
203
+      formatter: (params) => {
204
+        const items = Array.isArray(params) ? params : [params]
205
+        const row = items.find((p) => p.seriesName === '检测量')
206
+        if (!row) {
207
+          return ''
208
+        }
209
+        return `${row.name}<br/>检测量: ${row.value}`
210
+      }
211
+    },
201 212
     grid: { left: 72, right: 48, top: 8, bottom: 8, containLabel: false },
202 213
     xAxis: {
203 214
       type: 'value',
@@ -545,7 +556,7 @@ export function buildRiskFactorRadarOption(riskFactorWeights) {
545 556
   const items = riskFactorWeights.items
546 557
   const indicators = items.map((item) => ({
547 558
     name: item.factorName,
548
-    max: 100
559
+    max: 1
549 560
   }))
550 561
   const values = items.map((item) => Number(item.weight ?? 0))
551 562
   return {
@@ -562,13 +573,18 @@ export function buildRiskFactorRadarOption(riskFactorWeights) {
562 573
     series: [
563 574
       {
564 575
         type: 'radar',
576
+        symbol: 'none',
577
+        emphasis: {
578
+          symbol: 'none',
579
+          lineStyle: { color: '#5ef0c8' }
580
+        },
565 581
         data: [
566 582
           {
567 583
             value: values,
568 584
             name: '权重',
585
+            symbol: 'none',
569 586
             areaStyle: { color: 'rgba(94, 240, 200, 0.25)' },
570
-            lineStyle: { color: '#5ef0c8' },
571
-            itemStyle: { color: '#5ef0c8' }
587
+            lineStyle: { color: '#5ef0c8' }
572 588
           }
573 589
         ]
574 590
       }

+ 63 - 14
ruoyi-screen/src/views/epidemicRisk/index.vue

@@ -43,8 +43,13 @@
43 43
               class="er-weather__cell"
44 44
               :class="'er-weather__cell--' + item.key"
45 45
             >
46
-              <span class="er-weather__val">{{ item.value }}</span>
47
-
46
+              <ScreenScrollNumber
47
+                v-if="item.numeric"
48
+                class="er-weather__val"
49
+                :value="item.rawValue"
50
+                :format="item.format"
51
+              />
52
+              <span v-else class="er-weather__val">{{ item.value }}</span>
48 53
             </div>
49 54
           </div>
50 55
         </div>
@@ -209,6 +214,7 @@
209 214
 <script setup>
210 215
 import { computed, onMounted, onUnmounted, ref } from 'vue'
211 216
 import ScreenChart from '@/components/ScreenChart.vue'
217
+import ScreenScrollNumber from '@/components/ScreenScrollNumber.vue'
212 218
 import { getEpidemicRiskDashboard, getEpidemicRiskReports } from '@/api/epidemicRisk'
213 219
 import { getBaqingWeather, parseWinMeterToMs } from '@/api/weather'
214 220
 import {
@@ -290,24 +296,54 @@ const weatherDisplay = computed(() => {
290 296
   return { tem: tem === '—' ? tem : `${tem}°C`, range }
291 297
 })
292 298
 
299
+function isNumericWeatherValue(val) {
300
+  if (val === null || val === undefined || val === '') {
301
+    return false
302
+  }
303
+  const text = String(val).trim()
304
+  if (text === '—' || text === '--' || text === '-') {
305
+    return false
306
+  }
307
+  return !Number.isNaN(Number(text))
308
+}
309
+
310
+function weatherNumberFormat(val) {
311
+  const n = Number(val)
312
+  if (Number.isNaN(n)) {
313
+    return 'int'
314
+  }
315
+  return Number.isInteger(n) ? 'int' : 'area'
316
+}
317
+
293 318
 const weatherMetrics = computed(() => {
294 319
   const w = weather.value
295 320
   if (!w) {
296 321
     return []
297 322
   }
298
-  const humidity = w.humidity != null && w.humidity !== '' ? String(w.humidity) : '—'
299
-  const rain =
300
-    w.rain_pcpn != null && w.rain_pcpn !== '' ? String(w.rain_pcpn) : '0'
301
-  const uv =
302
-    w.uvIndex != null && w.uvIndex !== '' ? String(w.uvIndex) : '—'
323
+  const humidity =
324
+    w.humidity != null && w.humidity !== '' ? String(w.humidity).replace(/%$/, '') : '—'
325
+  const rain = w.rain_pcpn != null && w.rain_pcpn !== '' ? String(w.rain_pcpn) : '0'
326
+  const uv = w.uvIndex != null && w.uvIndex !== '' ? String(w.uvIndex) : '—'
327
+  const speed = parseWinMeterToMs(w.win_meter)
328
+  const tem = w.tem != null && w.tem !== '' ? String(w.tem) : '—'
329
+  const win = w.win || '—'
330
+
303 331
   return [
304
-    { key: 'win', label: '风向', value: w.win || '—' },
305
-    { key: 'speed', label: '风速', value: parseWinMeterToMs(w.win_meter) },
306
-    { key: 'uv', label: '光照强度', value: uv },
307
-    { key: 'humidity', label: '空气湿度', value: humidity.replace(/%$/, '') || '—' },
308
-    { key: 'rain', label: '时降雨量', value: rain },
309
-    { key: 'tem', label: '空气温度', value: w.tem != null && w.tem !== '' ? w.tem : '—' }
310
-  ]
332
+    { key: 'win', value: win },
333
+    { key: 'speed', value: speed },
334
+    { key: 'uv', value: uv },
335
+    { key: 'humidity', value: humidity || '—' },
336
+    { key: 'rain', value: rain },
337
+    { key: 'tem', value: tem }
338
+  ].map((item) => {
339
+    const numeric = item.key !== 'win' && isNumericWeatherValue(item.value)
340
+    return {
341
+      ...item,
342
+      numeric,
343
+      rawValue: numeric ? Number(item.value) : null,
344
+      format: numeric ? weatherNumberFormat(item.value) : 'int'
345
+    }
346
+  })
311 347
 })
312 348
 
313 349
 function formatNum(val) {
@@ -657,6 +693,19 @@ onUnmounted(() => {
657 693
   text-overflow: ellipsis;
658 694
   text-shadow: 0 0 10px rgba(69, 240, 184, 0.35);
659 695
 }
696
+
697
+.er-weather__val :deep(.screen-scroll-number) {
698
+  font-size: inherit;
699
+  font-weight: inherit;
700
+  color: inherit;
701
+  text-shadow: inherit;
702
+}
703
+
704
+.er-weather__val :deep(.screen-scroll-number__strip),
705
+.er-weather__val :deep(.screen-scroll-number__sep) {
706
+  color: inherit;
707
+  text-shadow: inherit;
708
+}
660 709
 .er-weather__cell--speed .er-weather__val {
661 710
   right: 25%;
662 711
 }

+ 21 - 12
ruoyi-screen/src/views/home/index.vue

@@ -20,25 +20,25 @@
20 20
         <div class="top_content">
21 21
           <div class="content_1">
22 22
             <div class="content_num">
23
-              <strong>{{ display(overview?.inventoryTotal) }}</strong> 头
23
+              <ScreenScrollNumber :value="overview?.inventoryTotal" /> 头
24 24
             </div>
25 25
             <div class="content_title"><div>牦牛存栏总量</div></div>
26 26
           </div>
27 27
           <div class="content_2">
28 28
             <div class="content_num">
29
-              <strong>{{ display(overview?.annualOutbound) }}</strong> 头
29
+              <ScreenScrollNumber :value="overview?.annualOutbound" /> 头
30 30
             </div>
31 31
             <div class="content_title"><div>牦牛年出栏量</div></div>
32 32
           </div>
33 33
           <div class="content_3">
34 34
             <div class="content_num">
35
-              <strong>{{ display(overview?.pastureCount) }}</strong> 个
35
+              <ScreenScrollNumber :value="overview?.pastureCount" /> 个
36 36
             </div>
37 37
             <div class="content_title"><div>牧场数量</div></div>
38 38
           </div>
39 39
           <div class="content_4">
40 40
             <div class="content_num">
41
-              <strong>{{ display(overview?.grasslandCount) }}</strong> 个
41
+              <ScreenScrollNumber :value="overview?.grasslandCount" /> 个
42 42
             </div>
43 43
             <div class="content_title"><div>草场数量</div></div>
44 44
           </div>
@@ -46,25 +46,25 @@
46 46
         <div class="top_content">
47 47
           <div class="content_1">
48 48
             <div class="content_num">
49
-              <strong>{{ displayArea(overview?.grasslandAreaMu) }}</strong> 亩
49
+              <ScreenScrollNumber :value="overview?.grasslandAreaMu" format="area" /> 亩
50 50
             </div>
51 51
             <div class="content_title"><div>草场面积</div></div>
52 52
           </div>
53 53
           <div class="content_2">
54 54
             <div class="content_num">
55
-              <strong>{{ display(overview?.supplierCount) }}</strong> 个
55
+              <ScreenScrollNumber :value="overview?.supplierCount" /> 个
56 56
             </div>
57 57
             <div class="content_title"><div>供应商数量</div></div>
58 58
           </div>
59 59
           <div class="content_3">
60 60
             <div class="content_num">
61
-              <strong>{{ display(overview?.distributorCount) }}</strong> 个
61
+              <ScreenScrollNumber :value="overview?.distributorCount" /> 个
62 62
             </div>
63 63
             <div class="content_title"><div>承销商数量</div></div>
64 64
           </div>
65 65
           <div class="content_4">
66 66
             <div class="content_num">
67
-              <strong>{{ display(overview?.tradeMarketCount) }}</strong> 个
67
+              <ScreenScrollNumber :value="overview?.tradeMarketCount" /> 个
68 68
             </div>
69 69
             <div class="content_title"><div>交易市场数量</div></div>
70 70
           </div>
@@ -209,6 +209,7 @@
209 209
 <script setup>
210 210
 import { computed, onMounted, ref } from 'vue'
211 211
 import ScreenChart from '@/components/ScreenChart.vue'
212
+import ScreenScrollNumber from '@/components/ScreenScrollNumber.vue'
212 213
 import StandardScrollRow from '@/components/StandardScrollRow.vue'
213 214
 import { getHomeDashboard, getHomeStandards } from '@/api/home'
214 215
 import {
@@ -291,10 +292,6 @@ function display(val) {
291 292
   return formatNum(val)
292 293
 }
293 294
 
294
-function displayArea(val) {
295
-  return formatNum(val, 2)
296
-}
297
-
298 295
 /** 应用看板主数据 */
299 296
 function applyDashboard(data) {
300 297
   if (!data) {
@@ -514,6 +511,18 @@ onMounted(() => {
514 511
   margin: 10px 0;
515 512
 }
516 513
 
514
+.content_num :deep(.screen-scroll-number) {
515
+  font-size: 20px;
516
+}
517
+
518
+.content_num :deep(.screen-scroll-number__strip),
519
+.content_num :deep(.screen-scroll-number__sep) {
520
+  background: linear-gradient(to bottom, #98e9aa, #ecd27b);
521
+  -webkit-background-clip: text;
522
+  background-clip: text;
523
+  color: transparent;
524
+}
525
+
517 526
 .content_num strong {
518 527
   font-size: 20px;
519 528
   background: linear-gradient(to bottom, #98e9aa, #ecd27b);

+ 23 - 33
ruoyi-screen/src/views/livestockResources/chartOptions.js

@@ -1,5 +1,6 @@
1 1
 /** 畜牧资源大屏 ECharts 配置(对齐 doc/大屏/畜牧资源 技术方案) */
2 2
 import 'echarts-wordcloud'
3
+import { buildMallPie2DOption } from '@/utils/mallPie2d'
3 4
 
4 5
 const AXIS_LABEL = { color: '#9fb0c3', fontSize: 10 }
5 6
 const AXIS_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.35)' } }
@@ -154,47 +155,36 @@ export function buildUserStructureOption(userStructure) {
154 155
   }
155 156
 }
156 157
 
157
-/** 提问分类占比 — 饼图 */
158
+/** 提问分类占比 — 2D 环形饼图(对齐交易销售店铺入驻样式) */
158 159
 export function buildCategoryShareOption(categoryShare) {
159 160
   const items = categoryShare?.items || []
160 161
   const data = items
161
-    .map((item, i) => ({
162
-      name: item.categoryName || item.categoryCode,
163
-      value: item.count ?? 0,
164
-      itemStyle: { color: PIE_COLORS[i % PIE_COLORS.length] }
162
+    .filter((item) => (item.count ?? 0) > 0)
163
+    .map((item) => ({
164
+      name: item.categoryName || item.categoryCode || '未分类',
165
+      value: item.count ?? 0
165 166
     }))
166 167
   if (!data.length) {
167 168
     return emptyOption('暂无分类数据')
168 169
   }
169
-  return {
170
-    ...chartBase(),
171
-    tooltip: {
172
-      trigger: 'item',
173
-      formatter: (p) => {
174
-        const row = items.find((it) => (it.categoryName || it.categoryCode) === p.name)
175
-        const ratio = row?.ratio != null ? `${Number(row.ratio).toFixed(1)}%` : `${p.percent}%`
176
-        return `${p.name}<br/>${p.value} 条 (${ratio})`
177
-      }
178
-    },
179
-    series: [
180
-      {
181
-        type: 'pie',
182
-        radius: ['0%', '62%'],
183
-        center: ['50%', '54%'],
184
-        label: {
185
-          color: '#e8eef5',
186
-          fontSize: 9,
187
-          formatter: (p) => {
188
-            const row = items.find((it) => (it.categoryName || it.categoryCode) === p.name)
189
-            const ratio = row?.ratio != null ? Number(row.ratio).toFixed(1) : p.percent
190
-            return `${p.name}\n${ratio}%`
191
-          }
192
-        },
193
-        labelLine: { length: 6, length2: 4 },
194
-        data
195
-      }
196
-    ]
170
+  const total =
171
+    categoryShare?.totalAskerMessages ?? data.reduce((sum, item) => sum + item.value, 0)
172
+  if (!(total > 0)) {
173
+    return emptyOption('暂无分类数据')
197 174
   }
175
+  return buildMallPie2DOption(data, {
176
+    summaryTitle: String(total),
177
+    summarySubtext: '提问量(条)',
178
+    legend: {
179
+      formatter: (name) => (name.length > 6 ? `${name.slice(0, 5)}…` : name),
180
+      itemGap: 4
181
+    },
182
+    tooltipFormatter: (p) => {
183
+      const row = items.find((it) => (it.categoryName || it.categoryCode || '未分类') === p.name)
184
+      const ratio = row?.ratio != null ? `${Number(row.ratio).toFixed(1)}%` : `${p.percent}%`
185
+      return `${p.name}<br/>${p.value} 条 (${ratio})`
186
+    }
187
+  })
198 188
 }
199 189
 
200 190
 /** 模型调用分析 — 分类条数柱图 */

+ 31 - 20
ruoyi-screen/src/views/livestockResources/index.vue

@@ -14,7 +14,11 @@
14 14
           >
15 15
             <div class="content_title"><div>{{ item.label }}</div></div>
16 16
             <div class="content_num">
17
-              <strong class="content_num__value">{{ item.value }}</strong>
17
+              <ScreenScrollNumber
18
+                class="content_num__value"
19
+                :value="item.rawValue"
20
+                :format="item.format"
21
+              />
18 22
               <span v-if="item.unit" class="content_num__unit">{{ item.unit }}</span>
19 23
             </div>
20 24
           </div>
@@ -138,6 +142,7 @@
138 142
 <script setup>
139 143
 import { computed, onMounted, ref } from 'vue'
140 144
 import ScreenChart from '@/components/ScreenChart.vue'
145
+import ScreenScrollNumber from '@/components/ScreenScrollNumber.vue'
141 146
 import { getLivestockResourceDashboard } from '@/api/livestockResources'
142 147
 import {
143 148
   buildActivityTrendOption,
@@ -176,18 +181,19 @@ const wordCloudOption = computed(() => buildWordCloudOption(topKeywords.value))
176 181
 const overviewItems = computed(() => {
177 182
   const o = overview.value
178 183
   return [
179
-    { key: 'totalUsers', label: '累计使用用户', unit: '人', value: display(o?.totalUsers) },
180
-    { key: 'todayActiveUsers', label: '今日活跃用户', unit: '人', value: display(o?.todayActiveUsers) },
181
-    { key: 'todayNewUsers', label: '日新增用户', unit: '人', value: display(o?.todayNewUsers) },
182
-    { key: 'totalSessions', label: '累计会话数', unit: '次', value: display(o?.totalSessions) },
183
-    { key: 'todaySessions', label: '今日会话数', unit: '次', value: display(o?.todaySessions) },
184
-    { key: 'totalQuestionCount', label: '累计提问量', unit: '次', value: display(o?.totalQuestionCount) },
185
-    { key: 'todayQuestionCount', label: '今日提问量', unit: '次', value: display(o?.todayQuestionCount) },
184
+    { key: 'totalUsers', label: '累计使用用户', unit: '人', rawValue: o?.totalUsers, format: 'int' },
185
+    { key: 'todayActiveUsers', label: '今日活跃用户', unit: '人', rawValue: o?.todayActiveUsers, format: 'int' },
186
+    { key: 'todayNewUsers', label: '日新增用户', unit: '人', rawValue: o?.todayNewUsers, format: 'int' },
187
+    { key: 'totalSessions', label: '累计会话数', unit: '次', rawValue: o?.totalSessions, format: 'int' },
188
+    { key: 'todaySessions', label: '今日会话数', unit: '次', rawValue: o?.todaySessions, format: 'int' },
189
+    { key: 'totalQuestionCount', label: '累计提问量', unit: '次', rawValue: o?.totalQuestionCount, format: 'int' },
190
+    { key: 'todayQuestionCount', label: '今日提问量', unit: '次', rawValue: o?.todayQuestionCount, format: 'int' },
186 191
     {
187 192
       key: 'avgResponseSeconds',
188 193
       label: '平均响应时长',
189 194
       unit: '秒',
190
-      value: displaySeconds(o?.avgResponseSeconds)
195
+      rawValue: o?.avgResponseSeconds,
196
+      format: 'area'
191 197
     }
192 198
   ]
193 199
 })
@@ -220,17 +226,6 @@ function display(val) {
220 226
   return formatNum(val)
221 227
 }
222 228
 
223
-function displaySeconds(val) {
224
-  if (val === null || val === undefined || val === '') {
225
-    return '0'
226
-  }
227
-  const n = Number(val)
228
-  if (Number.isNaN(n)) {
229
-    return '0'
230
-  }
231
-  return n.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 1 })
232
-}
233
-
234 229
 function formatRatio(val) {
235 230
   const n = Number(val)
236 231
   if (Number.isNaN(n)) {
@@ -481,6 +476,22 @@ onMounted(() => {
481 476
   color: transparent;
482 477
 }
483 478
 
479
+.content_num :deep(.screen-scroll-number) {
480
+  flex: 1;
481
+  min-width: 0;
482
+  justify-content: flex-end;
483
+  font-size: 20px;
484
+  font-weight: 600;
485
+}
486
+
487
+.content_num :deep(.screen-scroll-number__strip),
488
+.content_num :deep(.screen-scroll-number__sep) {
489
+  background: linear-gradient(to bottom, #98e9aa, #ecd27b);
490
+  -webkit-background-clip: text;
491
+  background-clip: text;
492
+  color: transparent;
493
+}
494
+
484 495
 .content_num__unit {
485 496
   flex-shrink: 0;
486 497
   font-size: 11px;

+ 31 - 64
ruoyi-screen/src/views/tradeSales/chartOptions.js

@@ -1,5 +1,7 @@
1 1
 /** 交易销售大屏 ECharts 配置(对齐 doc/交易销售 设计稿) */
2 2
 
3
+import { buildMallPie2DOption } from '@/utils/mallPie2d'
4
+
3 5
 const AXIS_LABEL = { color: '#9fb0c3', fontSize: 10 }
4 6
 const AXIS_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.35)' } }
5 7
 const SPLIT_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.12)' } }
@@ -286,7 +288,6 @@ export function buildQualityGradePieOption(qualityGrade) {
286 288
   }
287 289
 }
288 290
 
289
-const MALL_PIE_COLOR = ['#5ef0c8', '#ecd27b', '#6eb5ff', '#0f8f72', '#c9a227', '#8b7cf6']
290 291
 const MALL_BAR_COLOR = ['#5ef0c8', '#6eb5ff', '#ecd27b', '#0f8f72', '#c9a227']
291 292
 const WORD_COLORS = ['#5ef0c8', '#ecd27b', '#6eb5ff', '#98e9aa', '#0f8f72', '#c9a227']
292 293
 const MALL_UNAVAILABLE_TEXT = '商城统计未接入'
@@ -298,7 +299,7 @@ function resolveMallEmptyText(mallStatsAvailable, defaultText = '暂无数据')
298 299
   return defaultText
299 300
 }
300 301
 
301
-/** 农资品类销售占比 — 饼图 */
302
+/** 农资品类销售占比 — 2D 环形饼图 */
302 303
 export function buildCategorySalesPieOption(categorySales, mallStatsAvailable = true) {
303 304
   if (mallStatsAvailable === false) {
304 305
     return emptyOption(MALL_UNAVAILABLE_TEXT)
@@ -308,42 +309,26 @@ export function buildCategorySalesPieOption(categorySales, mallStatsAvailable =
308 309
   }
309 310
   const data = categorySales.items
310 311
     .filter((item) => (item.qty ?? 0) > 0)
311
-    .map((item, i) => ({
312
+    .map((item) => ({
312 313
       name: item.categoryName || '未分类',
313
-      value: item.qty ?? 0,
314
-      itemStyle: { color: MALL_PIE_COLOR[i % MALL_PIE_COLOR.length] }
314
+      value: item.qty ?? 0
315 315
     }))
316 316
   if (!data.length) {
317 317
     return emptyOption('暂无品类数据')
318 318
   }
319
-  return {
320
-    tooltip: {
321
-      trigger: 'item',
322
-      formatter: (p) => {
323
-        const row = categorySales.items.find((it) => (it.categoryName || '未分类') === p.name)
324
-        const ratio = row?.ratio != null ? `${Number(row.ratio).toFixed(1)}%` : `${p.percent}%`
325
-        return `${p.name}: ${p.value} (${ratio})`
326
-      }
319
+  const totalQty = categorySales.totalQty ?? data.reduce((sum, d) => sum + d.value, 0)
320
+  return buildMallPie2DOption(data, {
321
+    summaryTitle: String(totalQty),
322
+    summarySubtext: '总销量(件)',
323
+    legend: {
324
+      formatter: (name) => (name.length > 6 ? `${name.slice(0, 5)}…` : name)
327 325
     },
328
-    series: [
329
-      {
330
-        type: 'pie',
331
-        radius: ['0%', '62%'],
332
-        center: ['50%', '52%'],
333
-        label: {
334
-          color: '#e8eef5',
335
-          fontSize: 9,
336
-          formatter: (p) => {
337
-            const row = categorySales.items.find((it) => (it.categoryName || '未分类') === p.name)
338
-            const ratio = row?.ratio != null ? Number(row.ratio).toFixed(1) : p.percent
339
-            return `${p.name}\n${ratio}%`
340
-          }
341
-        },
342
-        labelLine: { length: 6, length2: 4 },
343
-        data
344
-      }
345
-    ]
346
-  }
326
+    tooltipFormatter: (p) => {
327
+      const row = categorySales.items.find((it) => (it.categoryName || '未分类') === p.name)
328
+      const ratio = row?.ratio != null ? `${Number(row.ratio).toFixed(1)}%` : `${p.percent}%`
329
+      return `${p.name}<br/>${p.value} 件 (${ratio})`
330
+    }
331
+  })
347 332
 }
348 333
 
349 334
 /** 商城订单趋势 — 曲线 */
@@ -389,7 +374,7 @@ export function buildMallOrderTrendOption(mallOrderTrend, mallStatsAvailable = t
389 374
   }
390 375
 }
391 376
 
392
-/** 店铺入驻 — 饼图(按月占比) */
377
+/** 店铺入驻 — 2D 环形饼图(按月占比) */
393 378
 export function buildShopEntryPieOption(shopEntry, mallStatsAvailable = true) {
394 379
   if (mallStatsAvailable === false) {
395 380
     return emptyOption(MALL_UNAVAILABLE_TEXT)
@@ -399,42 +384,24 @@ export function buildShopEntryPieOption(shopEntry, mallStatsAvailable = true) {
399 384
   }
400 385
   const data = shopEntry.items
401 386
     .filter((item) => (item.shopCount ?? 0) > 0)
402
-    .map((item, i) => ({
387
+    .map((item) => ({
403 388
       name: `${item.month}月`,
404
-      value: item.shopCount ?? 0,
405
-      itemStyle: { color: MALL_PIE_COLOR[i % MALL_PIE_COLOR.length] }
389
+      value: item.shopCount ?? 0
406 390
     }))
407 391
   if (!data.length) {
408 392
     return emptyOption('暂无入驻数据')
409 393
   }
410
-  return {
411
-    tooltip: {
412
-      trigger: 'item',
413
-      formatter: (p) => {
414
-        const row = shopEntry.items.find((it) => `${it.month}月` === p.name)
415
-        const ratio = row?.ratio != null ? `${Number(row.ratio).toFixed(1)}%` : `${p.percent}%`
416
-        return `${p.name}: ${p.value}家 (${ratio})`
417
-      }
418
-    },
419
-    series: [
420
-      {
421
-        type: 'pie',
422
-        radius: ['0%', '62%'],
423
-        center: ['50%', '52%'],
424
-        label: {
425
-          color: '#e8eef5',
426
-          fontSize: 9,
427
-          formatter: (p) => {
428
-            const row = shopEntry.items.find((it) => `${it.month}月` === p.name)
429
-            const ratio = row?.ratio != null ? Number(row.ratio).toFixed(1) : p.percent
430
-            return `${p.name}\n${ratio}%`
431
-          }
432
-        },
433
-        labelLine: { length: 6, length2: 4 },
434
-        data
435
-      }
436
-    ]
437
-  }
394
+  const yearTotal = shopEntry.yearTotal ?? data.reduce((sum, d) => sum + d.value, 0)
395
+  return buildMallPie2DOption(data, {
396
+    summaryTitle: String(yearTotal),
397
+    summarySubtext: '年入驻(家)',
398
+    legend: { itemGap: 4 },
399
+    tooltipFormatter: (p) => {
400
+      const row = shopEntry.items.find((it) => `${it.month}月` === p.name)
401
+      const ratio = row?.ratio != null ? `${Number(row.ratio).toFixed(1)}%` : `${p.percent}%`
402
+      return `${p.name}<br/>${p.value} 家 (${ratio})`
403
+    }
404
+  })
438 405
 }
439 406
 
440 407
 /** 消费区域排名 Top5 — 柱图(万元) */

+ 36 - 36
ruoyi-screen/src/views/tradeSales/index.vue

@@ -38,7 +38,13 @@
38 38
           >
39 39
             <div class="content_title"><div>{{ item.label }}</div></div>
40 40
             <div class="content_num">
41
-              <strong class="content_num__value">{{ item.value }}</strong>
41
+              <strong v-if="item.placeholder" class="content_num__value">—</strong>
42
+              <ScreenScrollNumber
43
+                v-else
44
+                class="content_num__value"
45
+                :value="item.rawValue"
46
+                :format="item.format"
47
+              />
42 48
               <span v-if="item.unit" class="content_num__unit">{{ item.unit }}</span>
43 49
             </div>
44 50
           </div>
@@ -164,6 +170,7 @@
164 170
 <script setup>
165 171
 import { computed, onMounted, onUnmounted, ref } from 'vue'
166 172
 import ScreenChart from '@/components/ScreenChart.vue'
173
+import ScreenScrollNumber from '@/components/ScreenScrollNumber.vue'
167 174
 import { getTradeSalesDashboard } from '@/api/tradeSales'
168 175
 import {
169 176
   buildCategorySalesPieOption,
@@ -246,42 +253,48 @@ const tradeOverviewItems = computed(() => {
246 253
       key: 'orderCount',
247 254
       label: '牦牛交易订单数',
248 255
       unit: '单',
249
-      value: display(o?.orderCount),
256
+      rawValue: o?.orderCount,
257
+      format: 'int',
250 258
       placeholder: false
251 259
     },
252 260
     {
253 261
       key: 'tradeHeads',
254 262
       label: '牦牛交易量',
255 263
       unit: '头',
256
-      value: display(o?.tradeHeads),
264
+      rawValue: o?.tradeHeads,
265
+      format: 'int',
257 266
       placeholder: false
258 267
     },
259 268
     {
260 269
       key: 'tradeAmount',
261 270
       label: '牦牛交易总额',
262 271
       unit: '元',
263
-      value: displayAmount(o?.tradeAmount),
272
+      rawValue: o?.tradeAmount,
273
+      format: 'money',
264 274
       placeholder: false
265 275
     },
266 276
     {
267 277
       key: 'supplierCount',
268 278
       label: '入驻商铺数',
269 279
       unit: '家',
270
-      value: display(o?.supplierCount),
280
+      rawValue: o?.supplierCount,
281
+      format: 'int',
271 282
       placeholder: false
272 283
     },
273 284
     {
274 285
       key: 'agriOrderCount',
275 286
       label: '农资订单量',
276 287
       unit: '单',
277
-      value: displayAgriCount(o?.agriOrderCount),
288
+      rawValue: o?.agriOrderCount,
289
+      format: 'int',
278 290
       placeholder: o?.agriOrderCount == null
279 291
     },
280 292
     {
281 293
       key: 'agriSalesAmount',
282 294
       label: '农资销售额',
283 295
       unit: '元',
284
-      value: displayAgriAmount(o?.agriSalesAmount),
296
+      rawValue: o?.agriSalesAmount,
297
+      format: 'money',
285 298
       placeholder: o?.agriSalesAmount == null
286 299
     }
287 300
   ]
@@ -298,35 +311,6 @@ function formatNum(val) {
298 311
   return n.toLocaleString('zh-CN')
299 312
 }
300 313
 
301
-function display(val) {
302
-  return formatNum(val)
303
-}
304
-
305
-function displayAmount(val) {
306
-  if (val === null || val === undefined || val === '') {
307
-    return '0'
308
-  }
309
-  const n = Number(val)
310
-  if (Number.isNaN(n)) {
311
-    return '0'
312
-  }
313
-  return n.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 })
314
-}
315
-
316
-function displayAgriCount(val) {
317
-  if (val === null || val === undefined) {
318
-    return '—'
319
-  }
320
-  return display(val)
321
-}
322
-
323
-function displayAgriAmount(val) {
324
-  if (val === null || val === undefined) {
325
-    return '—'
326
-  }
327
-  return displayAmount(val)
328
-}
329
-
330 314
 function formatPrice(val) {
331 315
   if (val === null || val === undefined || val === '') {
332 316
     return '—'
@@ -710,6 +694,22 @@ onUnmounted(() => {
710 694
   color: transparent;
711 695
 }
712 696
 
697
+.content_num :deep(.screen-scroll-number) {
698
+  flex: 1;
699
+  min-width: 0;
700
+  justify-content: flex-end;
701
+  font-size: 24px;
702
+  font-weight: 600;
703
+}
704
+
705
+.content_num :deep(.screen-scroll-number__strip),
706
+.content_num :deep(.screen-scroll-number__sep) {
707
+  background: linear-gradient(to bottom, #98e9aa, #ecd27b);
708
+  -webkit-background-clip: text;
709
+  background-clip: text;
710
+  color: transparent;
711
+}
712
+
713 713
 .content_num__unit {
714 714
   flex-shrink: 0;
715 715
   font-size: 12px;

+ 9 - 0
ruoyi-ui-app/api/livestockResource.js

@@ -8,3 +8,12 @@ export function listLivestockResource(params) {
8 8
     params
9 9
   })
10 10
 }
11
+
12
+/** 畜牧资源详情(id + sourceType:1 医疗 / 2 科技) */
13
+export function getLivestockResourceDetail(id, sourceType) {
14
+  return request({
15
+    url: `/app/livestockResource/${id}`,
16
+    method: 'GET',
17
+    params: { type: sourceType }
18
+  })
19
+}

+ 51 - 0
ruoyi-ui-app/locale/bo.js

@@ -256,6 +256,57 @@ export default {
256 256
       t4: 'བརྙན་ཟྭོའི་སློབ་ཚན།'
257 257
     }
258 258
   },
259
+  livestockResourceDetailPage: {
260
+    navTitle: 'ཐོན་ཁུངས་ཞིབ་ཕྲ།',
261
+    titleFallback: 'ཕྱུགས་རྫས་ཐོན་ཁུངས།',
262
+    loading: 'འཇུག་བཞིན་པ…',
263
+    loadFail: 'ཞིབ་ཕྲ་འཇུག་མ་ཐུབ།',
264
+    notFound: 'ཐོན་ཁུངས་འདི་ཕྱིར་འདོན་ཟིན་པའམ་མེད།',
265
+    invalidParams: 'ཞབས་ཞུའི་གྲངས་འགོད་ནོར་འདུག',
266
+    noIntro: 'མདོར་བརྗོད་མེད།',
267
+    noVideo: 'བརྙན་ཟྭོ་མེད།',
268
+    videoTitle: 'བརྙན་ཟྭོའི་སློབ་ཚན།',
269
+    sectionInfo: 'ཞིབ་ཕྲའི་གནས་ཚུལ།',
270
+    feeUnit: 'ཡུань/སྐར་མ།',
271
+    affiliatedUnit: 'གཞུང་འབྲེལ་ཚན་པ།',
272
+    completionUnit: 'ལེགས་གྲུབ་ཚན་པ།',
273
+    detailAddress: 'ཞིབ་ཕྲའི་ས་གནས།',
274
+    contactPhone: 'འབྲེལ་གཏུག་ཁ་པར།',
275
+    personInCharge: 'ཁག་འགན་པ།',
276
+    teamSize: 'ཚོགས་པའི་ཆེ་ཆུང་།',
277
+    teamMembers: 'ཚོགས་མི།',
278
+    establishDate: 'གསར་འཛུགས་དུས་ཚོད།',
279
+    serviceArea: 'ཞབས་ཞུའི་ཁུལ།',
280
+    consultModes: 'ནད་གཞིས་ཐབས་ལམ།',
281
+    feeStandard: 'གཏན་འབབས་གི་ཚད་གཞི།',
282
+    serviceHours: 'ཞབས་ཞུའི་དུས་ཚོད།',
283
+    serviceWeekdays: 'ཞབས་ཞུའི་གཟའ་ཉིན།',
284
+    orgLevel: 'ལས་ཁུངས་རིམ་པ།',
285
+    equipmentModel: 'ཡོ་བྱད་དཔེ་གཞི།',
286
+    achievementSummary: 'མཇུག་འབྲས་མདོར་བསྡུས།',
287
+    keyTechPrinciple: 'གཙོ་བོའི་ལག་རྩལ་གཞི་རྩ།',
288
+    techAdvantage: 'ལག་རྩལ་ཀུན་ཁྱབ།',
289
+    researchDirection: 'ཞིབ་འཇུག་ཁ་ཕྱོགས།',
290
+    instrumentModel: 'ཡོ་བྱད་དཔེ་གཞི།',
291
+    storageLocation: 'གསོག་འཇོག་ས་གནས།',
292
+    reservationProcess: 'ཁ་ཆད་རིམ་པ།',
293
+    borrowFee: 'གཡར་སྤྲོད་རིན།',
294
+    courseTopic: 'སློབ་ཚན་གཙོ་བོ།',
295
+    consultMode1: 'གནས་ཚད་ནད་གཞིས།',
296
+    consultMode2: 'ཁྱིམ་ནད་གཞིས།',
297
+    consultMode3: 'དྲ་རྒྱུན་ནད་གཞིས།',
298
+    orgLevel1: 'ནad་ཁang',
299
+    orgLevel2: 'སbyong་ཁang',
300
+    weekday: {
301
+      1: 'གཟའ་ཟླ་བ།',
302
+      2: 'གཟའ་མིག་དmar',
303
+      3: 'གཟའ lhag pa',
304
+      4: 'གཟའ phur bu',
305
+      5: 'གཟའ pa sangs',
306
+      6: 'གཟའ spen pa',
307
+      7: 'གཟའ nyi ma'
308
+    }
309
+  },
259 310
   agriClassroomPage: {
260 311
     navTitle: 'ཞིང་ལག་སློབ་ཁང་།',
261 312
     searchPlaceholder: 'ཁྱེད་ཀྱིས་འཚོལ་དགོས་པའི་ཞིང་ལག་སློབ་ཚན་འཚོལ།',

+ 51 - 0
ruoyi-ui-app/locale/zh.js

@@ -249,6 +249,57 @@ export default {
249 249
       t4: '视频课程'
250 250
     }
251 251
   },
252
+  livestockResourceDetailPage: {
253
+    navTitle: '资源详情',
254
+    titleFallback: '畜牧资源',
255
+    loading: '加载中…',
256
+    loadFail: '详情加载失败',
257
+    notFound: '该资源已下线或不存在',
258
+    invalidParams: '参数无效,无法打开详情',
259
+    noIntro: '暂无简介',
260
+    noVideo: '暂无视频',
261
+    videoTitle: '视频课程',
262
+    sectionInfo: '详细信息',
263
+    feeUnit: '元/小时',
264
+    affiliatedUnit: '隶属单位',
265
+    completionUnit: '完成单位',
266
+    detailAddress: '详细地址',
267
+    contactPhone: '联系电话',
268
+    personInCharge: '负责人',
269
+    teamSize: '团队规模',
270
+    teamMembers: '团队成员',
271
+    establishDate: '成立时间',
272
+    serviceArea: '服务区域',
273
+    consultModes: '接诊方式',
274
+    feeStandard: '收费标准',
275
+    serviceHours: '服务时段',
276
+    serviceWeekdays: '服务周日',
277
+    orgLevel: '机构级别',
278
+    equipmentModel: '设备型号',
279
+    achievementSummary: '成果摘要',
280
+    keyTechPrinciple: '关键技术原理',
281
+    techAdvantage: '技术优势',
282
+    researchDirection: '研究方向',
283
+    instrumentModel: '仪器型号',
284
+    storageLocation: '存放位置',
285
+    reservationProcess: '预约流程',
286
+    borrowFee: '借用费用',
287
+    courseTopic: '课程主题',
288
+    consultMode1: '定点接诊',
289
+    consultMode2: '上门接诊',
290
+    consultMode3: '在线接诊',
291
+    orgLevel1: '诊所',
292
+    orgLevel2: '医院',
293
+    weekday: {
294
+      1: '周一',
295
+      2: '周二',
296
+      3: '周三',
297
+      4: '周四',
298
+      5: '周五',
299
+      6: '周六',
300
+      7: '周日'
301
+    }
302
+  },
252 303
   agriClassroomPage: {
253 304
     navTitle: '农技课堂',
254 305
     searchPlaceholder: '搜索您要找的农技课程',

+ 414 - 0
ruoyi-ui-app/package-a/livestock-resource-detail/index.vue

@@ -0,0 +1,414 @@
1
+<template>
2
+  <view :class="pageRootClass" class="tab-page lrd-page">
3
+    <view v-if="loading" class="lrd-state">
4
+      <text class="text-body lrd-state__txt">{{ $t('livestockResourceDetailPage.loading') }}</text>
5
+    </view>
6
+    <view v-else-if="loadError" class="lrd-state">
7
+      <text class="text-body lrd-state__txt">{{ loadError }}</text>
8
+    </view>
9
+    <scroll-view v-else scroll-y class="lrd-scroll" enable-back-to-top>
10
+      <view class="lrd-inner">
11
+        <text class="lrd-title">{{ displayTitle }}</text>
12
+
13
+        <view class="lrd-meta">
14
+          <text v-if="typeLabel" class="lrd-meta__tag text-body">{{ typeLabel }}</text>
15
+          <text v-if="publishTime" class="lrd-meta__date text-body">{{ publishTime }}</text>
16
+        </view>
17
+
18
+        <view v-if="showCoverSection" class="lrd-section">
19
+          <text class="lrd-section__label text-title">{{ $t('newsDetailPage.coverTitle') }}</text>
20
+          <view v-if="hasCover" class="lrd-cover-wrap" role="button" @click="onPreviewCover">
21
+            <image class="lrd-cover" :src="coverSrc" mode="aspectFit" />
22
+          </view>
23
+          <text v-else class="text-body lrd-muted">{{ $t('newsDetailPage.noCover') }}</text>
24
+        </view>
25
+
26
+        <view class="lrd-section">
27
+          <text class="lrd-section__label text-title">{{ $t('newsDetailPage.introTitle') }}</text>
28
+          <text class="text-body lrd-intro">{{ introText }}</text>
29
+        </view>
30
+
31
+        <view v-if="showVideoSection" class="lrd-section">
32
+          <text class="lrd-section__label text-title">{{ $t('livestockResourceDetailPage.videoTitle') }}</text>
33
+          <view v-if="videoSrc" class="lrd-video-wrap">
34
+            <video
35
+              class="lrd-video"
36
+              :src="videoSrc"
37
+              :poster="hasCover ? coverSrc : ''"
38
+              controls
39
+              object-fit="contain"
40
+              :enable-progress-gesture="true"
41
+              :show-center-play-btn="true"
42
+            />
43
+          </view>
44
+          <text v-else class="text-body lrd-muted">{{ $t('livestockResourceDetailPage.noVideo') }}</text>
45
+        </view>
46
+
47
+        <view v-if="detailRows.length" class="lrd-section">
48
+          <text class="lrd-section__label text-title">{{ $t('livestockResourceDetailPage.sectionInfo') }}</text>
49
+          <view v-for="(row, idx) in detailRows" :key="idx" class="lrd-kv">
50
+            <text class="lrd-kv__label text-body">{{ row.label }}</text>
51
+            <text class="lrd-kv__value text-body">{{ row.value }}</text>
52
+          </view>
53
+        </view>
54
+      </view>
55
+    </scroll-view>
56
+  </view>
57
+</template>
58
+
59
+<script>
60
+import tabPage from '@/mixins/tabPage'
61
+import { getLivestockResourceDetail } from '@/api/livestockResource'
62
+import { resolveResourceUrl } from '@/utils/resourceUrl'
63
+import {
64
+  buildLivestockDetailRows,
65
+  formatPublishTime,
66
+  resolveDetailCover,
67
+  resolveDetailVideo
68
+} from '@/utils/livestockResourceDetail'
69
+
70
+const TAB_TYPE_CODES = ['004002', '004006', '004007', '004008']
71
+const TAB_I18N_KEYS = ['t1', 't2', 't3', 't4']
72
+
73
+export default {
74
+  mixins: [tabPage],
75
+  data() {
76
+    return {
77
+      navTitleKey: 'livestockResourceDetailPage.navTitle',
78
+      resourceId: '',
79
+      sourceType: 0,
80
+      typeCode: '',
81
+      detail: null,
82
+      loading: false,
83
+      loadError: '',
84
+      coverRaw: '',
85
+      coverSrc: '',
86
+      videoSrc: ''
87
+    }
88
+  },
89
+  computed: {
90
+    displayTitle() {
91
+      const name = this.detail && (this.detail.resourceName || this.detail.title)
92
+      return name || this.$t('livestockResourceDetailPage.titleFallback')
93
+    },
94
+    typeLabel() {
95
+      const code = (this.detail && this.detail.resourceType) || this.typeCode
96
+      const idx = TAB_TYPE_CODES.indexOf(code)
97
+      if (idx >= 0) {
98
+        return this.$t(`livestockResourcesPage.tabs.${TAB_I18N_KEYS[idx]}`)
99
+      }
100
+      return code || ''
101
+    },
102
+    publishTime() {
103
+      return formatPublishTime(this.detail && this.detail.publishTime)
104
+    },
105
+    introText() {
106
+      return (this.detail && this.detail.introduction) || this.$t('livestockResourceDetailPage.noIntro')
107
+    },
108
+    isVideoCourse() {
109
+      const rt = (this.detail && this.detail.resourceType) || this.typeCode
110
+      return rt === '004008'
111
+    },
112
+    hasCover() {
113
+      return !!(this.coverRaw || '').trim()
114
+    },
115
+    /** 视频课程:封面作 poster,有视频时不单独占一大块封面区 */
116
+    showCoverSection() {
117
+      if (!this.hasCover) {
118
+        return !this.isVideoCourse
119
+      }
120
+      if (this.isVideoCourse && this.videoSrc) {
121
+        return false
122
+      }
123
+      return true
124
+    },
125
+    showVideoSection() {
126
+      return this.isVideoCourse
127
+    },
128
+    detailRows() {
129
+      return buildLivestockDetailRows(this.detail, this.sourceType, (key) => this.$t(key))
130
+    }
131
+  },
132
+  onLoad(query) {
133
+    const q = query || {}
134
+    this.resourceId = this.decodeQuery(q, 'id')
135
+    this.typeCode = this.decodeQuery(q, 'typeCode')
136
+    const st = parseInt(this.decodeQuery(q, 'sourceType'), 10)
137
+    this.sourceType = st === 1 || st === 2 ? st : 0
138
+    this.loadDetail()
139
+  },
140
+  onShow() {
141
+    const title = this.displayTitle
142
+    const p = uni.setNavigationBarTitle({
143
+      title: title === this.$t('livestockResourceDetailPage.titleFallback') ? this.$t(this.navTitleKey) : title
144
+    })
145
+    if (p && typeof p.catch === 'function') {
146
+      p.catch(() => {})
147
+    }
148
+  },
149
+  methods: {
150
+    decodeQuery(q, key) {
151
+      const raw = q && q[key]
152
+      if (raw == null || raw === '') {
153
+        return ''
154
+      }
155
+      try {
156
+        return decodeURIComponent(String(raw))
157
+      } catch (e) {
158
+        return String(raw)
159
+      }
160
+    },
161
+    loadDetail() {
162
+      if (!this.resourceId || !this.sourceType) {
163
+        this.loadError = this.$t('livestockResourceDetailPage.invalidParams')
164
+        return Promise.resolve()
165
+      }
166
+      this.loading = true
167
+      this.loadError = ''
168
+      return getLivestockResourceDetail(this.resourceId, this.sourceType)
169
+        .then((res) => {
170
+          this.detail = res.data || null
171
+          if (!this.detail) {
172
+            this.loadError = this.$t('livestockResourceDetailPage.notFound')
173
+            return
174
+          }
175
+          const cover = resolveDetailCover(this.detail, this.sourceType)
176
+          this.coverRaw = cover || ''
177
+          this.coverSrc = cover ? resolveResourceUrl(cover) : ''
178
+          const video = resolveDetailVideo(this.detail)
179
+          this.videoSrc = video ? resolveResourceUrl(video) : ''
180
+          if (this.detail.resourceType) {
181
+            this.typeCode = this.detail.resourceType
182
+          }
183
+          uni.setNavigationBarTitle({ title: this.displayTitle })
184
+        })
185
+        .catch((e) => {
186
+          this.detail = null
187
+          this.loadError = (e && e.message) || this.$t('livestockResourceDetailPage.loadFail')
188
+        })
189
+        .finally(() => {
190
+          this.loading = false
191
+        })
192
+    },
193
+    onPreviewCover() {
194
+      if (!this.coverSrc) return
195
+      uni.previewImage({ urls: [this.coverSrc], current: 0 })
196
+    }
197
+  }
198
+}
199
+</script>
200
+
201
+<style lang="scss" scoped>
202
+@import '@/styles/morandi.scss';
203
+@import '@/styles/tab-page.scss';
204
+
205
+.lrd-page {
206
+  display: flex;
207
+  flex-direction: column;
208
+  min-width: 0;
209
+  min-height: 100%;
210
+  box-sizing: border-box;
211
+  background: $morandi-bg-page;
212
+}
213
+
214
+.lrd-scroll {
215
+  flex: 1;
216
+  min-height: 0;
217
+  min-width: 0;
218
+}
219
+
220
+.lrd-inner {
221
+  display: flex;
222
+  flex-direction: column;
223
+  align-items: stretch;
224
+  gap: 28rpx;
225
+  min-width: 0;
226
+  padding: 40rpx 32rpx 48rpx;
227
+  box-sizing: border-box;
228
+}
229
+
230
+.lrd-state {
231
+  flex: 1;
232
+  display: flex;
233
+  align-items: center;
234
+  justify-content: center;
235
+  padding: 48rpx 24rpx;
236
+}
237
+
238
+.lrd-state__txt {
239
+  color: $morandi-text-muted;
240
+  text-align: center;
241
+}
242
+
243
+.lrd-title {
244
+  display: block;
245
+  width: 100%;
246
+  text-align: center;
247
+  font-size: 38rpx;
248
+  font-weight: 700;
249
+  line-height: 1.45;
250
+  color: #111827;
251
+  word-break: break-word;
252
+  overflow-wrap: anywhere;
253
+}
254
+
255
+.lrd-meta {
256
+  display: flex;
257
+  flex-direction: row;
258
+  flex-wrap: wrap;
259
+  align-items: center;
260
+  justify-content: center;
261
+  gap: 12rpx 20rpx;
262
+  width: 100%;
263
+}
264
+
265
+.lrd-meta__tag {
266
+  padding: 6rpx 16rpx;
267
+  border-radius: 999rpx;
268
+  font-size: 22rpx;
269
+  line-height: 1.4;
270
+  color: #15803d;
271
+  background: rgba(34, 197, 94, 0.12);
272
+  border: 1rpx solid rgba(34, 197, 94, 0.25);
273
+}
274
+
275
+.lrd-meta__date {
276
+  font-size: 24rpx;
277
+  line-height: 1.5;
278
+  color: $morandi-text-muted;
279
+}
280
+
281
+.lrd-section {
282
+  display: flex;
283
+  flex-direction: column;
284
+  gap: 16rpx;
285
+  min-width: 0;
286
+  padding: 24rpx;
287
+  box-sizing: border-box;
288
+  border-radius: 16rpx;
289
+  background: $morandi-bg-card;
290
+  border: 1rpx solid $morandi-border;
291
+}
292
+
293
+.lrd-section__label {
294
+  font-size: 30rpx;
295
+  font-weight: 600;
296
+  color: $morandi-text;
297
+}
298
+
299
+.lrd-muted {
300
+  font-size: 26rpx;
301
+  line-height: 1.5;
302
+  color: $morandi-text-muted;
303
+}
304
+
305
+.lrd-intro {
306
+  font-size: 28rpx;
307
+  line-height: 1.65;
308
+  color: $morandi-text-secondary;
309
+  word-break: break-word;
310
+  overflow-wrap: anywhere;
311
+  white-space: pre-wrap;
312
+}
313
+
314
+.lrd-cover-wrap {
315
+  width: 100%;
316
+  max-width: 480rpx;
317
+  height: 280rpx;
318
+  margin: 0 auto;
319
+  border-radius: 12rpx;
320
+  overflow: hidden;
321
+  background: $morandi-bg-card-inner;
322
+  border: 1rpx solid $morandi-border-soft;
323
+  display: flex;
324
+  align-items: center;
325
+  justify-content: center;
326
+}
327
+
328
+.lrd-cover {
329
+  display: block;
330
+  width: 100%;
331
+  height: 100%;
332
+}
333
+
334
+.lrd-video-wrap {
335
+  width: 100%;
336
+  min-width: 0;
337
+  border-radius: 12rpx;
338
+  overflow: hidden;
339
+  background: #1a1a1a;
340
+  border: 1rpx solid $morandi-border-soft;
341
+}
342
+
343
+.lrd-video {
344
+  display: block;
345
+  width: 100%;
346
+  height: 360rpx;
347
+  background: #1a1a1a;
348
+}
349
+
350
+.lrd-kv {
351
+  display: flex;
352
+  flex-direction: row;
353
+  align-items: flex-start;
354
+  gap: 16rpx;
355
+  min-width: 0;
356
+  padding: 14rpx 0;
357
+  border-top: 1rpx solid $morandi-border-soft;
358
+
359
+  &:first-of-type {
360
+    border-top: none;
361
+    padding-top: 0;
362
+  }
363
+
364
+  &:last-of-type {
365
+    padding-bottom: 0;
366
+  }
367
+}
368
+
369
+.lrd-kv__label {
370
+  flex-shrink: 0;
371
+  width: 168rpx;
372
+  font-size: 24rpx;
373
+  line-height: 1.55;
374
+  color: $morandi-text-soft;
375
+}
376
+
377
+.lrd-kv__value {
378
+  flex: 1;
379
+  min-width: 0;
380
+  font-size: 26rpx;
381
+  line-height: 1.55;
382
+  color: $morandi-text;
383
+  word-break: break-word;
384
+  overflow-wrap: anywhere;
385
+}
386
+
387
+.lrd-page.lang-bo {
388
+  .lrd-title {
389
+    font-size: 34rpx;
390
+    line-height: 1.75;
391
+    letter-spacing: 2rpx;
392
+    font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
393
+  }
394
+
395
+  .lrd-meta__tag,
396
+  .lrd-meta__date,
397
+  .lrd-intro,
398
+  .lrd-muted,
399
+  .lrd-kv__label,
400
+  .lrd-kv__value {
401
+    font-size: 22rpx;
402
+    line-height: 1.75;
403
+    letter-spacing: 2rpx;
404
+    font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
405
+  }
406
+
407
+  .lrd-section__label {
408
+    font-size: 28rpx;
409
+    line-height: 1.75;
410
+    letter-spacing: 2rpx;
411
+    font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
412
+  }
413
+}
414
+</style>

+ 19 - 33
ruoyi-ui-app/package-a/livestock-resources/index.vue

@@ -48,7 +48,7 @@
48 48
           <template #default="{ item }">
49 49
             <view class="lr-row-cell">
50 50
               <view class="lr-row" :style="{ height: rowBodyPx + 'px' }">
51
-                <view class="lr-row__left" role="button" @click="openNewsDetail(item)">
51
+                <view class="lr-row__left" role="button" @click="openResourceDetail(item)">
52 52
                   <text class="lr-row__title">{{ item.title }}</text>
53 53
                   <text class="text-body lr-row__summary">{{ item.introduction || $t('livestockResourcesPage.noIntro') }}</text>
54 54
                   <view class="lr-row__meta">
@@ -85,7 +85,6 @@ import ULazyLoad from 'uview-plus/components/u-lazy-load/u-lazy-load.vue'
85 85
 import UVirtualList from 'uview-plus/components/u-virtual-list/u-virtual-list.vue'
86 86
 import tabPage from '@/mixins/tabPage'
87 87
 import { resolveResourceUrl } from '@/utils/resourceUrl'
88
-import { putNewsDetailPayload } from '@/utils/newsDetailCache'
89 88
 import { listLivestockResource } from '@/api/livestockResource'
90 89
 
91 90
 /** Tab 与资源类型编码(§1.2) */
@@ -100,7 +99,7 @@ const ROW_GAP_RPX = 20
100 99
 /** lr-body 上下 padding(16 + 24 rpx) */
101 100
 const LR_BODY_PAD_RPX = 40
102 101
 const COVER = '/static/ai/hero.png'
103
-const NEWS_DETAIL_PATH = '/package-a/news-detail/index'
102
+const LIVESTOCK_DETAIL_PATH = '/package-a/livestock-resource-detail/index'
104 103
 
105 104
 export default {
106 105
   components: {
@@ -191,12 +190,16 @@ export default {
191 190
     if (this._searchTimer) clearTimeout(this._searchTimer)
192 191
   },
193 192
   methods: {
194
-    mapResourceRow(row, index) {
193
+    mapResourceRow(row) {
194
+      const resourceId = row.id
195
+      const sourceType = row.sourceType
195 196
       const type = row.type || ''
196 197
       const publishTime = row.publishTime || ''
197 198
       const title = row.title || ''
198 199
       return {
199
-        id: `${type}-${publishTime}-${index}-${title}`,
200
+        id: `${sourceType}-${resourceId}`,
201
+        resourceId,
202
+        sourceType,
200 203
         title,
201 204
         introduction: row.introduction || '',
202 205
         type,
@@ -231,12 +234,11 @@ export default {
231 234
       if (titleKw) {
232 235
         params.title = titleKw
233 236
       }
234
-      const baseIndex = reset ? 0 : this.articles.length
235 237
       return listLivestockResource(params)
236 238
         .then((res) => {
237 239
           const rows = res.rows || []
238 240
           this.listTotal = res.total != null ? Number(res.total) : 0
239
-          const mapped = rows.map((row, i) => this.mapResourceRow(row, baseIndex + i))
241
+          const mapped = rows.map((row) => this.mapResourceRow(row))
240 242
           this.articles = reset ? mapped : this.articles.concat(mapped)
241 243
           this.$nextTick(() => {
242 244
             this.calcLayoutHeights()
@@ -360,33 +362,17 @@ export default {
360 362
       if (!url) return
361 363
       uni.previewImage({ urls: [url], current: 0 })
362 364
     },
363
-    openNewsDetail(item) {
364
-      const payload = {
365
-        title: item.title || '',
366
-        introduction: item.introduction || '',
367
-        type: item.type || '',
368
-        typeLabel: this.tabTitle(item.type),
369
-        coverFileUrl: item.coverFileUrl || '',
370
-        contentFileUrl: item.contentFileUrl || '',
371
-        publishTime: item.publishTime || '',
372
-        listKind: 'livestock'
373
-      }
374
-      const cacheKey = putNewsDetailPayload(payload)
375
-      let url = `${NEWS_DETAIL_PATH}?kind=livestock`
376
-      if (cacheKey) {
377
-        url += `&cacheKey=${encodeURIComponent(cacheKey)}`
378
-      } else {
379
-        url += [
380
-          `&title=${encodeURIComponent(payload.title)}`,
381
-          `&date=${encodeURIComponent(payload.publishTime)}`,
382
-          `&type=${encodeURIComponent(payload.type)}`,
383
-          `&typeLabel=${encodeURIComponent(payload.typeLabel)}`,
384
-          `&introduction=${encodeURIComponent(payload.introduction)}`,
385
-          `&coverFileUrl=${encodeURIComponent(payload.coverFileUrl)}`,
386
-          `&contentFileUrl=${encodeURIComponent(payload.contentFileUrl)}`
387
-        ].join('')
365
+    openResourceDetail(item) {
366
+      if (!item || item.resourceId == null || !item.sourceType) {
367
+        uni.showToast({ title: this.$t('livestockResourceDetailPage.invalidParams'), icon: 'none' })
368
+        return
388 369
       }
389
-      uni.navigateTo({ url })
370
+      const q = [
371
+        `id=${encodeURIComponent(String(item.resourceId))}`,
372
+        `sourceType=${encodeURIComponent(String(item.sourceType))}`,
373
+        `typeCode=${encodeURIComponent(item.type || '')}`
374
+      ].join('&')
375
+      uni.navigateTo({ url: `${LIVESTOCK_DETAIL_PATH}?${q}` })
390 376
     },
391 377
     onTabsChange(_item, index) {
392 378
       this.tabCurrentIndex = typeof index === 'number' ? index : this.tabCurrentIndex

+ 6 - 0
ruoyi-ui-app/pages.json

@@ -113,6 +113,12 @@
113 113
             "disableScroll": true
114 114
           }
115 115
         },
116
+        {
117
+          "path": "livestock-resource-detail/index",
118
+          "style": {
119
+            "navigationBarTitleText": "资源详情"
120
+          }
121
+        },
116 122
         {
117 123
           "path": "agri-classroom/index",
118 124
           "style": {

+ 1 - 1
ruoyi-ui-app/pages/mine/index.vue

@@ -82,7 +82,7 @@ export default {
82 82
     return {
83 83
       navTitleKey: 'nav.mine',
84 84
       menuRows: [
85
-        { id: 'editProfile', icon: 'edit-pen', titleKey: 'minePage.editProfile' },
85
+        // { id: 'editProfile', icon: 'edit-pen', titleKey: 'minePage.editProfile' },
86 86
         // { id: 'orders', icon: 'order', titleKey: 'minePage.myOrders' },
87 87
         { id: 'booking', icon: 'calendar', titleKey: 'minePage.myBooking' },
88 88
         { id: 'enroll', icon: 'file-text', titleKey: 'minePage.myEnrollment' },

+ 165 - 0
ruoyi-ui-app/utils/livestockResourceDetail.js

@@ -0,0 +1,165 @@
1
+/** 畜牧资源详情展示辅助(对接 GET /app/livestockResource/{id}?type=1|2) */
2
+
3
+const CONSULT_MODE_KEYS = {
4
+  1: 'consultMode1',
5
+  2: 'consultMode2',
6
+  3: 'consultMode3'
7
+}
8
+
9
+const ORG_LEVEL_KEYS = {
10
+  1: 'orgLevel1',
11
+  2: 'orgLevel2'
12
+}
13
+
14
+function isEmpty(val) {
15
+  if (val == null) return true
16
+  if (typeof val === 'string') return val.trim() === ''
17
+  if (Array.isArray(val)) return !val.length
18
+  return false
19
+}
20
+
21
+function pickText(val) {
22
+  if (val == null) return ''
23
+  return String(val).trim()
24
+}
25
+
26
+export function formatPublishTime(val) {
27
+  const s = pickText(val)
28
+  if (!s) return ''
29
+  return s.length >= 10 ? s.slice(0, 10) : s
30
+}
31
+
32
+export function formatWeekdays(raw, list, t) {
33
+  let days = []
34
+  if (Array.isArray(list) && list.length) {
35
+    days = list.map((n) => parseInt(n, 10)).filter((n) => n >= 1 && n <= 7)
36
+  } else if (!isEmpty(raw)) {
37
+    days = String(raw)
38
+      .split(',')
39
+      .map((s) => parseInt(s.trim(), 10))
40
+      .filter((n) => n >= 1 && n <= 7)
41
+  }
42
+  if (!days.length) return ''
43
+  return days.map((n) => t(`livestockResourceDetailPage.weekday.${n}`)).join('、')
44
+}
45
+
46
+export function formatConsultModes(raw, list, t) {
47
+  let modes = []
48
+  if (Array.isArray(list) && list.length) {
49
+    modes = list.map((n) => parseInt(n, 10))
50
+  } else if (!isEmpty(raw)) {
51
+    modes = String(raw)
52
+      .split(',')
53
+      .map((s) => parseInt(s.trim(), 10))
54
+      .filter((n) => n >= 1 && n <= 3)
55
+  }
56
+  if (!modes.length) return ''
57
+  return modes
58
+    .map((n) => {
59
+      const key = CONSULT_MODE_KEYS[n]
60
+      return key ? t(`livestockResourceDetailPage.${key}`) : String(n)
61
+    })
62
+    .join('、')
63
+}
64
+
65
+function formatMoney(val) {
66
+  if (val == null || val === '') return ''
67
+  const n = Number(val)
68
+  if (Number.isNaN(n)) return pickText(val)
69
+  return n.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 })
70
+}
71
+
72
+function formatServiceHours(start, end) {
73
+  const s = pickText(start)
74
+  const e = pickText(end)
75
+  if (s && e) return `${s} - ${e}`
76
+  return s || e || ''
77
+}
78
+
79
+function pushRow(rows, label, value) {
80
+  const v = pickText(value)
81
+  if (!v) return
82
+  rows.push({ label, value: v })
83
+}
84
+
85
+/**
86
+ * @param {object} d 详情 data
87
+ * @param {number} sourceType 1 医疗 / 2 科技
88
+ * @param {(key:string)=>string} t i18n
89
+ */
90
+export function buildLivestockDetailRows(d, sourceType, t) {
91
+  if (!d) return []
92
+  const rows = []
93
+  const rt = pickText(d.resourceType)
94
+  const prefix = 'livestockResourceDetailPage'
95
+
96
+  if (sourceType === 1) {
97
+    pushRow(rows, t(`${prefix}.affiliatedUnit`), d.affiliatedUnit)
98
+    pushRow(rows, t(`${prefix}.detailAddress`), d.detailAddress)
99
+    pushRow(rows, t(`${prefix}.contactPhone`), d.contactPhone)
100
+    pushRow(rows, t(`${prefix}.personInCharge`), d.personInCharge)
101
+    pushRow(rows, t(`${prefix}.teamSize`), d.teamSize != null ? String(d.teamSize) : '')
102
+    pushRow(rows, t(`${prefix}.teamMembers`), d.teamMembers)
103
+    if (d.establishDate) {
104
+      pushRow(rows, t(`${prefix}.establishDate`), formatPublishTime(d.establishDate))
105
+    }
106
+    pushRow(rows, t(`${prefix}.serviceArea`), d.serviceArea)
107
+    pushRow(rows, t(`${prefix}.consultModes`), formatConsultModes(d.consultModes, d.consultModesList, t))
108
+    if (d.feeStandard != null && d.feeStandard !== '') {
109
+      pushRow(rows, t(`${prefix}.feeStandard`), `${formatMoney(d.feeStandard)} ${t(`${prefix}.feeUnit`)}`)
110
+    }
111
+    pushRow(rows, t(`${prefix}.serviceHours`), formatServiceHours(d.serviceStartTime, d.serviceEndTime))
112
+    pushRow(rows, t(`${prefix}.serviceWeekdays`), formatWeekdays(d.serviceWeekdays, d.serviceWeekdaysList, t))
113
+    if (d.orgLevel != null && ORG_LEVEL_KEYS[d.orgLevel]) {
114
+      pushRow(rows, t(`${prefix}.orgLevel`), t(`${prefix}.${ORG_LEVEL_KEYS[d.orgLevel]}`))
115
+    }
116
+    pushRow(rows, t(`${prefix}.equipmentModel`), d.equipmentModel)
117
+    return rows
118
+  }
119
+
120
+  pushRow(rows, t(`${prefix}.affiliatedUnit`), d.affiliatedUnit)
121
+  pushRow(rows, t(`${prefix}.completionUnit`), d.completionUnit)
122
+  pushRow(rows, t(`${prefix}.contactPhone`), d.contactPhone)
123
+  pushRow(rows, t(`${prefix}.personInCharge`), d.personInCharge)
124
+  pushRow(rows, t(`${prefix}.detailAddress`), d.detailAddress)
125
+  pushRow(rows, t(`${prefix}.serviceArea`), d.serviceArea)
126
+
127
+  if (rt === '004006') {
128
+    pushRow(rows, t(`${prefix}.achievementSummary`), d.achievementSummary)
129
+    pushRow(rows, t(`${prefix}.keyTechPrinciple`), d.keyTechPrinciple)
130
+    pushRow(rows, t(`${prefix}.techAdvantage`), d.techAdvantage)
131
+    pushRow(rows, t(`${prefix}.researchDirection`), d.researchDirection)
132
+  }
133
+
134
+  if (rt === '004007') {
135
+    pushRow(rows, t(`${prefix}.instrumentModel`), d.instrumentModel)
136
+    pushRow(rows, t(`${prefix}.storageLocation`), d.storageLocation)
137
+    pushRow(rows, t(`${prefix}.reservationProcess`), d.reservationProcess)
138
+    if (d.borrowFee != null && d.borrowFee !== '') {
139
+      pushRow(rows, t(`${prefix}.borrowFee`), `${formatMoney(d.borrowFee)} ${t(`${prefix}.feeUnit`)}`)
140
+    }
141
+    if (d.feeStandard != null && d.feeStandard !== '') {
142
+      pushRow(rows, t(`${prefix}.feeStandard`), `${formatMoney(d.feeStandard)} ${t(`${prefix}.feeUnit`)}`)
143
+    }
144
+    pushRow(rows, t(`${prefix}.serviceHours`), formatServiceHours(d.serviceStartTime, d.serviceEndTime))
145
+    pushRow(rows, t(`${prefix}.serviceWeekdays`), formatWeekdays(d.serviceWeekdays, d.serviceWeekdaysList, t))
146
+  }
147
+
148
+  if (rt === '004008') {
149
+    pushRow(rows, t(`${prefix}.courseTopic`), d.courseTopic)
150
+  }
151
+
152
+  return rows
153
+}
154
+
155
+export function resolveDetailCover(d, sourceType) {
156
+  if (!d) return ''
157
+  if (sourceType === 1) {
158
+    return pickText(d.photoFileUrl)
159
+  }
160
+  return pickText(d.coverFileUrl) || pickText(d.photoFileUrl)
161
+}
162
+
163
+export function resolveDetailVideo(d) {
164
+  return pickText(d && d.videoFileUrl)
165
+}

+ 1 - 0
ruoyi-ui/src/lang/bo/breedingStd.js

@@ -43,6 +43,7 @@ export default {
43 43
     tipCover: "jpg/png པར་ཆེ་ཤོས་10MB(འདེམས་ཆ)",
44 44
     del: "སུབ་པ།",
45 45
     uploadingFile: "ཡིག་ཆ་སྤྲོད་བཞིན་པ།…",
46
+    syncKbLoading: "ཤེས་མཛོད་དུ་མཉམ་འགྲོས་བཞིན་པ།…",
46 47
     uploadingImg: "པར་རིས་སྤྲོད་བཞིན་པ།…",
47 48
     errBodyFmt: "ཡིག་ཆའི་རྣམ་གཞག་མི་འགྲིག",
48 49
     errComma: "མིང་ལ་ཉིས་སྡེར་མི་ཆོག",

+ 1 - 0
ruoyi-ui/src/lang/bo/techService.js

@@ -16,6 +16,7 @@ export default {
16 16
     colCreate: "གསར་བཟོའི་དུས་ཚོད།",
17 17
     pickFile: "ཡིག་ཆ་འདེམ",
18 18
     uploadingFile: "ཡིག་ཆ་འབེབ་བཞིན...",
19
+    syncKbLoading: "ཤེས་བྱིས་མཛོད་མཉམ་སྒྲིག་བཞིན...",
19 20
     errPhotoFmt: "jpg/jpeg/png རྐྱང་།",
20 21
     errPhotoMb: "10MB ལས་མི་འགྲོ།",
21 22
     errVideoFmt: "mp4 རྐྱང་།",

+ 1 - 0
ruoyi-ui/src/lang/zh/breedingStd.js

@@ -43,6 +43,7 @@ export default {
43 43
     tipCover: "支持 jpg、jpeg、png;单张不超过 10MB(选填)",
44 44
     del: "删除",
45 45
     uploadingFile: "正在上传文件,请稍候...",
46
+    syncKbLoading: "正在同步知识库,请稍候...",
46 47
     uploadingImg: "正在上传图片,请稍候...",
47 48
     errBodyFmt: "文件格式不正确,请上传规定格式的正文附件",
48 49
     errComma: "文件名不能包含英文逗号",

+ 1 - 0
ruoyi-ui/src/lang/zh/techService.js

@@ -16,6 +16,7 @@ export default {
16 16
     colCreate: "创建时间",
17 17
     pickFile: "选取文件",
18 18
     uploadingFile: "正在上传文件,请稍候...",
19
+    syncKbLoading: "正在同步知识库,请稍候...",
19 20
     errPhotoFmt: "仅支持 jpg、jpeg、png 格式",
20 21
     errPhotoMb: "图片大小不能超过 10 MB",
21 22
     errVideoFmt: "仅支持 mp4 格式",

+ 16 - 0
ruoyi-ui/src/mixins/breedingStdLocaleMixin.js

@@ -30,6 +30,22 @@ export default {
30 30
         return this.$t("breedingStd.status.kbSynced")
31 31
       }
32 32
       return this.$t("breedingStd.status.dash")
33
+    },
34
+    /** 确认后执行同步知识库,带全局 loading */
35
+    runSyncKbAfterConfirm(confirmMessage, syncFn) {
36
+      let syncLoading = false
37
+      return this.$modal
38
+        .confirm(confirmMessage)
39
+        .then(() => {
40
+          syncLoading = true
41
+          this.$modal.loading(this.$t("breedingStd.common.syncKbLoading"))
42
+          return syncFn()
43
+        })
44
+        .finally(() => {
45
+          if (syncLoading) {
46
+            this.$modal.closeLoading()
47
+          }
48
+        })
33 49
     }
34 50
   }
35 51
 }

+ 16 - 0
ruoyi-ui/src/mixins/techServiceLocaleMixin.js

@@ -60,6 +60,22 @@ export default {
60 60
       }
61 61
       return this.$t("techService.status.dash")
62 62
     },
63
+    /** 确认后执行同步知识库,带全局 loading */
64
+    runSyncKbAfterConfirm(confirmMessage, syncFn) {
65
+      let syncLoading = false
66
+      return this.$modal
67
+        .confirm(confirmMessage)
68
+        .then(() => {
69
+          syncLoading = true
70
+          this.$modal.loading(this.tsCommon("syncKbLoading"))
71
+          return syncFn()
72
+        })
73
+        .finally(() => {
74
+          if (syncLoading) {
75
+            this.$modal.closeLoading()
76
+          }
77
+        })
78
+    },
63 79
     knowledgeTopicText(topic) {
64 80
       if (topic >= 1 && topic <= 5) {
65 81
         return this.$t(`techService.knowledge.topic${topic}`)

+ 1 - 3
ruoyi-ui/src/views/breedingStandards/breedingManage/index.vue

@@ -535,9 +535,7 @@ export default {
535 535
       })
536 536
     },
537 537
     handleSyncKb(row) {
538
-      this.$modal
539
-        .confirm(this.stdT("confirmSync"))
540
-        .then(() => syncKbBreedingStandard(row.id))
538
+      this.runSyncKbAfterConfirm(this.stdT("confirmSync"), () => syncKbBreedingStandard(row.id))
541 539
         .then(() => {
542 540
           this.$modal.msgSuccess(this.$t("breedingStd.common.msgSyncOk"))
543 541
           this.getList()

+ 1 - 3
ruoyi-ui/src/views/breedingStandards/diseaseTreatment/drugIncompatibility/index.vue

@@ -370,9 +370,7 @@ export default {
370 370
       })
371 371
     },
372 372
     handleSyncKb(row) {
373
-      this.$modal
374
-        .confirm(this.stdT("confirmSync"))
375
-        .then(() => syncKbDrugIncompatibilityStandard(row.id))
373
+      this.runSyncKbAfterConfirm(this.stdT("confirmSync"), () => syncKbDrugIncompatibilityStandard(row.id))
376 374
         .then(() => {
377 375
           this.$modal.msgSuccess(this.stdT("msgSyncOk"))
378 376
           this.getList()

+ 1 - 3
ruoyi-ui/src/views/breedingStandards/diseaseTreatment/epidemicTreatment/index.vue

@@ -529,9 +529,7 @@ export default {
529 529
       })
530 530
     },
531 531
     handleSyncKb(row) {
532
-      this.$modal
533
-        .confirm(this.stdT("confirmSync"))
534
-        .then(() => syncKbEpidemicTreatmentStandard(row.id))
532
+      this.runSyncKbAfterConfirm(this.stdT("confirmSync"), () => syncKbEpidemicTreatmentStandard(row.id))
535 533
         .then(() => {
536 534
           this.$modal.msgSuccess(this.$t("breedingStd.common.msgSyncOk"))
537 535
           this.getList()

+ 1 - 3
ruoyi-ui/src/views/breedingStandards/diseaseTreatment/withdrawalPeriod/index.vue

@@ -385,9 +385,7 @@ export default {
385 385
       })
386 386
     },
387 387
     handleSyncKb(row) {
388
-      this.$modal
389
-        .confirm(this.stdT("confirmSync"))
390
-        .then(() => syncKbWithdrawalPeriodStandard(row.id))
388
+      this.runSyncKbAfterConfirm(this.stdT("confirmSync"), () => syncKbWithdrawalPeriodStandard(row.id))
391 389
         .then(() => {
392 390
           this.$modal.msgSuccess(this.stdT("msgSyncOk"))
393 391
           this.getList()

+ 1 - 3
ruoyi-ui/src/views/breedingStandards/equipmentOperation/index.vue

@@ -532,9 +532,7 @@ export default {
532 532
       })
533 533
     },
534 534
     handleSyncKb(row) {
535
-      this.$modal
536
-        .confirm(this.stdT("confirmSync"))
537
-        .then(() => syncKbEquipmentWorkStandard(row.id))
535
+      this.runSyncKbAfterConfirm(this.stdT("confirmSync"), () => syncKbEquipmentWorkStandard(row.id))
538 536
         .then(() => {
539 537
           this.$modal.msgSuccess(this.$t("breedingStd.common.msgSyncOk"))
540 538
           this.getList()

+ 1 - 3
ruoyi-ui/src/views/breedingStandards/feeding/index.vue

@@ -530,9 +530,7 @@ export default {
530 530
       })
531 531
     },
532 532
     handleSyncKb(row) {
533
-      this.$modal
534
-        .confirm(this.stdT("confirmSync"))
535
-        .then(() => syncKbFeedingStandard(row.id))
533
+      this.runSyncKbAfterConfirm(this.stdT("confirmSync"), () => syncKbFeedingStandard(row.id))
536 534
         .then(() => {
537 535
           this.$modal.msgSuccess(this.$t("breedingStd.common.msgSyncOk"))
538 536
           this.getList()

+ 1 - 3
ruoyi-ui/src/views/breedingStandards/growthOutbound/index.vue

@@ -530,9 +530,7 @@ export default {
530 530
       })
531 531
     },
532 532
     handleSyncKb(row) {
533
-      this.$modal
534
-        .confirm(this.stdT("confirmSync"))
535
-        .then(() => syncKbGrowthSlaughterStandard(row.id))
533
+      this.runSyncKbAfterConfirm(this.stdT("confirmSync"), () => syncKbGrowthSlaughterStandard(row.id))
536 534
         .then(() => {
537 535
           this.$modal.msgSuccess(this.$t("breedingStd.common.msgSyncOk"))
538 536
           this.getList()

+ 1 - 3
ruoyi-ui/src/views/techService/knowledge/index.vue

@@ -372,9 +372,7 @@ export default {
372 372
       })
373 373
     },
374 374
     handleSyncKb(row) {
375
-      this.$modal
376
-        .confirm(this.tsT("confirmSync"))
377
-        .then(() => syncKbKnowledge(row.id))
375
+      this.runSyncKbAfterConfirm(this.tsT("confirmSync"), () => syncKbKnowledge(row.id))
378 376
         .then(() => {
379 377
           this.$modal.msgSuccess(this.tsCommon("msgSyncOk"))
380 378
           this.getList()

+ 9 - 2
ruoyi-ui/src/views/tool/gen/index.vue

@@ -274,11 +274,18 @@ export default {
274 274
     /** 同步数据库操作 */
275 275
     handleSynchDb(row) {
276 276
       const tableName = row.tableName
277
-      this.$modal.confirm('确认要强制同步"' + tableName + '"表结构吗?').then(function() {
277
+      let syncLoading = false
278
+      this.$modal.confirm('确认要强制同步"' + tableName + '"表结构吗?').then(() => {
279
+        syncLoading = true
280
+        this.$modal.loading("正在同步数据库,请稍候...")
278 281
         return synchDb(tableName)
279 282
       }).then(() => {
280 283
         this.$modal.msgSuccess("同步成功")
281
-      }).catch(() => {})
284
+      }).catch(() => {}).finally(() => {
285
+        if (syncLoading) {
286
+          this.$modal.closeLoading()
287
+        }
288
+      })
282 289
     },
283 290
     /** 打开导入表弹窗 */
284 291
     openImportTable() {