diff --git a/src/App.vue b/src/App.vue
index bf4a0bf..1bead5a 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,7 +1,6 @@
-
-
\ No newline at end of file
diff --git a/src/components/Home.vue b/src/components/Home.vue
index dc85c24..16ab0f4 100644
--- a/src/components/Home.vue
+++ b/src/components/Home.vue
@@ -20,10 +20,9 @@
{{ row.breed }}
-
- {{ formatPrice(row.price) }}
-
+
+
{{ formatPrice(row.price) }}
@@ -67,6 +66,11 @@
+
+
+
+
正在请求 {{ loadingProvince }} 数据...
+
@@ -77,6 +81,8 @@
@@ -163,6 +169,9 @@
gap: 20px;
padding: 10px;
box-sizing: border-box;
+ position: relative; /* 允许作为背景层容器 */
+ min-height: 100vh; /* 充满视口高度,保证背景覆盖 */
+ background-color: #011819; /* 底层纯色背景,位于背景图之上、内容之下 */
}
/* 全国牛存栏量显示样式 */
@@ -206,7 +215,7 @@
}
.province-price-ranking-chart {
width: 100%;
- height: 500px;
+ height: 300px;
}
.price-ranking-panel {
display: flex;
@@ -304,11 +313,10 @@
color: #eaf7ff;
font-variant-numeric: tabular-nums;
}
-.price-value.on-bar {
- min-width: unset;
- width: 100%;
- text-align: center;
- color: #ffffff;
+.price-value.after-bar {
+ min-width: 86px;
+ text-align: right;
+ color: #eaf7ff;
font-weight: 600;
}
.dashboard-center {
@@ -457,54 +465,104 @@ export default {
const showNoDataToast = ref(false)
const noDataMessage = ref('')
let toastTimer = null
+ const isJumpLoading = ref(false)
+ const loadingProvince = ref('')
+ // 省份点击请求控制器(用于防抖与取消上一次未完成的请求)
+ let provinceClickController = null
+
+ // provinces 接口参数映射:仅处理指定示例,其余保持不变
+ const toApiProvinceParam = (name) => {
+ const map = {
+ '内蒙古自治区': '内蒙古',
+ '四川省': '四川',
+ '新疆维吾尔自治区': '新疆',
+ '西藏自治区': '西藏',
+ '宁夏回族自治区': '宁夏',
+ '广西自治区': '广西',
+ '河北省': '河北',
+ '山东省': '山东',
+ '黑龙江省': '黑龙江',
+ '吉林省': '吉林',
+ '云南省': '云南',
+ '甘肃省': '甘肃',
+ '青海省': '青海',
+ '贵州省': '贵州',
+ '安徽省': '安徽',
+ }
+ return map[name] ?? name
+ }
// 处理省份点击事件
const handleProvinceClick = async (provinceName) => {
+ // 若正在跳转/请求中,直接忽略后续点击,避免重复触发
+ if (isJumpLoading.value) return
+
+ // 跳转前加载动画
+ loadingProvince.value = provinceName
+ isJumpLoading.value = true
+
+ // 取消上一次未完成的请求(防抖与竞态控制)
+ if (provinceClickController) {
+ try { provinceClickController.abort() } catch {}
+ }
+ provinceClickController = new AbortController()
+
+ // 外部接口调用,超时控制(从2秒提高到8秒)
+ let url = ''
+ const timeoutId = setTimeout(() => {
+ try { provinceClickController.abort() } catch {}
+ }, 8000)
try {
- console.log('正在请求省份数据:', provinceName);
- const res = await axios.get('/api/cattle-data', {
- params: { province: provinceName }
- })
-
- console.log('省份数据返回:', res.data);
- const raw = res.data;
- // 兼容直接返回数组或 { data: [] } 的结构
- const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : []);
-
+ const apiParam = toApiProvinceParam(provinceName)
+ url = `/api/cattle-data/provinces?province=${encodeURIComponent(apiParam)}`
+ const res = await fetch(url, { signal: provinceClickController.signal })
+ clearTimeout(timeoutId)
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
+ const raw = await res.json()
+ const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : [])
+
if (list && list.length > 0) {
- console.log('数据存在,准备跳转');
+ // 接口成功且有数据,自动跳转到价格行情页
+ isJumpLoading.value = false
emit('navigate-to-warning', provinceName)
} else {
- console.warn('该地区无数据');
- noDataMessage.value = `该地区暂无数据: ${provinceName}`
- showNoDataToast.value = true
- if (toastTimer) clearTimeout(toastTimer)
- toastTimer = setTimeout(() => {
- showNoDataToast.value = false
- }, 3000)
+ throw new Error('empty payload')
}
} catch (error) {
- console.error('获取省份数据出错:', error)
- noDataMessage.value = `获取数据失败: ${provinceName}`
+ clearTimeout(timeoutId)
+ // 区分超时与其他错误
+ if (error?.name === 'AbortError') {
+ console.warn('[外部接口] 超时已中止请求 (8s):', url)
+ noDataMessage.value = '请求超时'
+ } else {
+ console.error('[外部接口] 调用失败:', error)
+ noDataMessage.value = '接口调用失败'
+ }
+ isJumpLoading.value = false
showNoDataToast.value = true
if (toastTimer) clearTimeout(toastTimer)
toastTimer = setTimeout(() => {
showNoDataToast.value = false
- }, 3000)
+ }, 2000)
+ } finally {
+ provinceClickController = null
}
}
return {
handleProvinceClick,
showNoDataToast,
- noDataMessage
+ noDataMessage,
+ isJumpLoading,
+ loadingProvince
}
},
data() {
return {
// 右侧品种单价排行榜下拉选项与选中值
- rightBreedOptions: ['安格斯','牦牛','黄牛','利木赞牛','鲁西牛','奶牛','肉牛','水牛','西门塔尔牛','夏洛莱牛','杂交牛','牛'],
- rightSelectedBreed: '安格斯',
+ rightBreedOptions: [],
+ rightSelectedBreed: '',
+ breedsLoading: false,
speciesPriceRows: [], // 品种单价排行(接口)
// 存栏率视图模式:percent 或 count
livestockViewMode: 'percent',
@@ -987,7 +1045,7 @@ export default {
? [...this.nationalProvinceAverageRows]
: (Array.isArray(this.nationalPriceTableRows) ? [...this.nationalPriceTableRows] : [])
base.sort((a, b) => a.price - b.price)
- const yCategories = base.map(r => r.province)
+ const xCategories = base.map(r => r.province)
const prices = base.map(r => r.price)
return {
backgroundColor: 'transparent',
@@ -1003,36 +1061,39 @@ export default {
return `${p.axisValue}
${p.seriesName}: ${p.value} 元/斤`
}
},
- grid: { left: '0%', right: '10%', bottom: '5%', top: '5%', containLabel: true },
+ grid: { left: '4%', right: '0%', bottom: '15%', top: '10%', containLabel: true },
xAxis: {
+ type: 'category',
+ data: xCategories,
+ axisLine: { lineStyle: { color: '#00ffff' } },
+ axisLabel: { color: '#ffffff', fontSize: 9, interval: 0 },
+ },
+ yAxis: {
type: 'value',
name: '单价(元/斤)',
nameTextStyle: { color: '#00ffff', fontSize: 11 },
axisLine: { lineStyle: { color: '#00ffff' } },
axisLabel: { color: '#ffffff', fontSize: 10 },
+ min: 12,
+ max: 15,
+ interval: 1,
splitLine: { lineStyle: { color: 'rgba(0,255,255,0.2)' } }
},
- yAxis: {
- type: 'category',
- data: yCategories,
- axisLine: { lineStyle: { color: '#00ffff' } },
- axisLabel: { color: '#ffffff', fontSize: 11 }
- },
series: [{
name: '省份单价',
type: 'bar',
data: prices,
barWidth: '42%',
barCategoryGap: '28%',
- itemStyle: { color: '#4e73df' },
- label: { show: true, position: 'right', color: '#cfefff', fontSize: 10, formatter: '{c} 元/斤' }
+ itemStyle: { color: '#00E1E1' },
+ label: { show: true, position: 'top', color: '#cfefff', fontSize: 10, formatter: '{c} ' }
}],
dataZoom: [
{
type: 'inside',
- yAxisIndex: 0,
+ xAxisIndex: 0,
startValue: 0,
- endValue: yCategories.length - 1,
+ endValue: xCategories.length - 1,
zoomLock: true,
filterMode: 'empty'
}
@@ -1062,6 +1123,16 @@ export default {
immediate: true
}
},
+ mounted() {
+ // 页面挂载后拉取品种列表以填充右侧下拉框
+ console.log('[Home] mounted: fetchBreeds 即将调用')
+ this.fetchBreeds()
+ },
+ created() {
+ // 保险起见,在 created 阶段也触发一次,避免某些场景 mounted 未执行
+ console.log('[Home] created: fetchBreeds 即将调用')
+ this.fetchBreeds()
+ },
methods: {
// 格式化价格显示(千分位)
formatPrice(value) {
@@ -1136,6 +1207,36 @@ export default {
console.warn('获取全国省份平均单价失败:', e)
}
},
+
+ // 拉取品种列表,使用 breedName 作为下拉项
+ async fetchBreeds() {
+ const url = '/api/cattle-data/breeds'
+ this.breedsLoading = true
+ try {
+ console.log('[Home] 调用接口:', url)
+ const res = await fetch(url)
+ const raw = await res.json()
+ console.log('[Home] breeds 接口返回原始数据:', raw)
+ const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : [])
+ const names = list
+ .map(it => it?.breedName ?? it?.name ?? it?.breed)
+ .filter(Boolean)
+ const uniq = Array.from(new Set(names))
+ console.log('[Home] 解析后的品种列表:', uniq)
+ if (uniq.length > 0) {
+ const prev = this.rightSelectedBreed
+ this.rightBreedOptions = uniq
+ if (!prev || !uniq.includes(prev)) {
+ this.rightSelectedBreed = uniq[0]
+ }
+ }
+ } catch (e) {
+ console.warn('[Home] 获取品种列表失败:', e)
+ } finally {
+ this.breedsLoading = false
+ console.log('[Home] breedsLoading 结束,状态:', this.breedsLoading)
+ }
+ },
// 构建环形图(占比)
buildLivestockPieOption() {
const baseline = this.livestockBaseline
@@ -1477,6 +1578,38 @@ export default {
margin-top: 0; /* 移除负边距 */
}
+/* 跳转加载动画样式 */
+.jump-loading-overlay {
+ position: absolute;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.35);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ z-index: 10;
+ pointer-events: none;
+}
+
+.jump-spinner {
+ width: 36px;
+ height: 36px;
+ border: 3px solid rgba(0,212,255,0.35);
+ border-top-color: #00d4ff;
+ border-radius: 50%;
+ animation: jumpSpin 1s linear infinite;
+ margin-bottom: 10px;
+}
+
+.jump-loading-text {
+ color: #eaf7ff;
+ font-size: 14px;
+}
+
+@keyframes jumpSpin {
+ to { transform: rotate(360deg); }
+}
+
.lowest-price-panel h3 {
diff --git a/src/components/Map3D.vue b/src/components/Map3D.vue
index 8bafc6b..50eb33d 100644
--- a/src/components/Map3D.vue
+++ b/src/components/Map3D.vue
@@ -1106,9 +1106,6 @@ export default {
}
});
- // 创建点击反馈提示
- this.showClickFeedback({ clientX: pointer.clientX, clientY: pointer.clientY }, provinceName);
-
// 触发省份点击事件
emit('province-click', provinceName);
@@ -1222,25 +1219,7 @@ export default {
this.isMobileDevice = isMobile;
}
- // 显示点击反馈提示
- showClickFeedback(event, provinceName) {
- const feedback = document.createElement('div');
- feedback.className = 'province-click-feedback';
- feedback.textContent = `正在跳转到 ${provinceName} 预警监测`;
-
- // 设置位置
- feedback.style.left = `${event.clientX - 100}px`;
- feedback.style.top = `${event.clientY - 50}px`;
-
- document.body.appendChild(feedback);
-
- // 0.6秒后移除
- setTimeout(() => {
- if (feedback.parentNode) {
- feedback.parentNode.removeChild(feedback);
- }
- }, 600);
- }
+
getDataRenderMap() {}
@@ -1939,55 +1918,6 @@ export default {
}
}
-/* 点击反馈提示 - 响应式优化 */
-.province-click-feedback {
- position: absolute;
- background: linear-gradient(135deg, rgba(0, 255, 136, 0.9), rgba(0, 204, 102, 0.8));
- color: white;
- padding: 8px 16px;
- border-radius: 20px;
- font-size: 14px;
- font-weight: bold;
- pointer-events: none;
- z-index: 10001;
- animation: clickFeedbackShow 0.6s ease-out;
- box-shadow: 0 4px 15px rgba(0, 255, 136, 0.4);
- border: 1px solid rgba(0, 255, 136, 0.6);
- white-space: nowrap;
-}
-
-/* 移动端优化 */
-@media screen and (max-width: 768px) {
- .province-click-feedback {
- font-size: 12px;
- padding: 6px 12px;
- border-radius: 16px;
- }
-}
-
-/* 触摸设备优化 */
-@media (hover: none) and (pointer: coarse) {
- .province-click-feedback {
- font-size: 13px;
- padding: 8px 14px;
- animation-duration: 0.8s;
- }
-}
-
-@keyframes clickFeedbackShow {
- 0% {
- opacity: 0;
- transform: scale(0.5) translateY(20px);
- }
- 50% {
- opacity: 1;
- transform: scale(1.1) translateY(-10px);
- }
- 100% {
- opacity: 0;
- transform: scale(1) translateY(-30px);
- }
-}
.cattle-source-top5 {
position: absolute;
bottom:80px; /* 定位到底部 */
diff --git a/src/components/Price.vue b/src/components/Price.vue
index 08fe533..7daef9b 100644
--- a/src/components/Price.vue
+++ b/src/components/Price.vue
@@ -1,12 +1,12 @@
@@ -188,79 +383,94 @@ export default {