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 {
-
-
-
-
-

省内各地区牛数据列表

+ +
+
+
+

省内各地区牛数据列表

+
+
+
+
时间
+
地区
+
品类
+
价格({{ unit }})
-
-
-
时间
-
地区
-
品类
-
价格({{ unit }})
-
涨跌
-
-
-
-
{{ row.date }}
-
{{ row.region }}
-
{{ row.breed }}
-
{{ row.price.toFixed(2) }}{{ unit }}
-
- - {{ row.delta.toFixed(2) }}{{ unit }} - -
-
+
+
+
{{ row.date }}
+
{{ row.region }}
+
{{ row.breed }}
+
{{ row.price.toFixed(2) }}{{ unit }}
- -
+
-
-
-
-
-

- - {{ (selectedRow ? selectedRow.region : provinceName) }} - - 价格详情 -

-
-
-
-
- 今日批发均价 - 相比昨日 {{ trendText }} {{ yesterdayDelta.toFixed(2) }} {{ trendSymbol }} -
-
- ¥ - {{ avgToday.toFixed(2) }} - {{ unit }} -
-
- -
-
- - - -
-
- - - -
-
- - -
+
+
+
+

+ + {{ (selectedRow ? selectedRow.region : provinceName) }} + + 价格详情 +

-
+
+
+
+ 今日批发均价 + 相比昨日 {{ trendText }} {{ yesterdayDelta.toFixed(2) }} {{ trendSymbol }} +
+
+ ¥ + {{ avgToday.toFixed(2) }} + {{ unit }} +
+
+ +
+
+ + + +
+
+ + + +
+
+ + +
+ + + +
+
+
+

品种占比统计

+
+ +
+ +
+
+
+

存栏统计

+
+ +
+ +
+
+
+

出栏统计

+
+ +
@@ -276,6 +486,7 @@ export default { gap: 16px; background: #011819; /* 与预警监测页背景一致 */ position: relative; + overflow: hidden; /* 页面高度固定,禁用整体滚动 */ } .price-page::before { @@ -312,8 +523,11 @@ export default { .price-content { display: grid; - grid-template-columns: 1.4fr 1.6fr; /* 加宽左侧列表区域 */ + grid-template-columns: repeat(6, 1fr); /* 6列,方便分配 3:3 和 2:2:2 */ + grid-template-rows: 1fr 1fr; /* 两行等高 */ gap: 16px; + flex: 1; /* 占满剩余高度 */ + min-height: 0; /* 允许内部滚动 */ } .card { @@ -324,8 +538,18 @@ export default { backdrop-filter: blur(10px); position: relative; overflow: hidden; + height: 100%; /* 与所在网格行高度一致 */ + display: flex; + flex-direction: column; /* 让内部内容自适应高度 */ } +/* 网格跨度设置 */ +.region-card { grid-column: span 3; } +.detail-card { grid-column: span 3; } +.breed-card { grid-column: span 2; } +.stock-card { grid-column: span 2; } +.slaughter-card { grid-column: span 2; } + .card::before { content: ''; position: absolute; @@ -396,23 +620,48 @@ export default { color: #cfefff; } -.right-stats { - display: flex; - flex-direction: column; - gap: 16px; -} +/* 移除旧的 .right-stats, .left-info, .stats-bottom 相关样式 */ -.detail-card { +.breed-card { background: rgba(255,255,255,0.06); border: 1px solid rgba(0,212,255,0.25); border-radius: 8px; padding: 10px 12px; } +.breed-donut { + width: 100%; + flex: 1; + min-height: 0; + margin: 0 auto; +} + +.detail-card { + background: rgba(255,255,255,0.06); + border: 1px solid rgba(0,212,255,0.25); + border-radius: 8px; + padding: 8px 10px; /* 缩小内边距,降低模块总高度 */ +} + .detail-content { display: flex; flex-direction: column; - gap: 12px; + gap: 8px; /* 收紧纵向间距 */ + flex: 1; + min-height: 0; +} + +.stock-card, .slaughter-card { + background: rgba(255,255,255,0.06); + border: 1px solid rgba(0,212,255,0.25); + border-radius: 8px; + padding: 10px 12px; +} + +.stock-chart, .slaughter-chart { + width: 100%; + flex: 1; + min-height: 0; } .today-price-line { @@ -452,7 +701,7 @@ export default { .table-header, .table-row { display: grid; - grid-template-columns: 1fr 1.6fr 1.2fr 1fr 1fr; /* 时间略窄,地区更宽,价格与涨跌等宽 */ + grid-template-columns: 1fr 1.6fr 1.2fr 1fr; /* 移除涨跌列后调整为四列 */ gap: 10px; align-items: center; } @@ -469,6 +718,37 @@ export default { flex-direction: column; } +/* 缩小左侧“省内各地区牛数据列表”模块高度 - 移除固定高度 */ +.region-card .region-table { + height: auto; + flex: 1; /* 占据剩余空间 */ + min-height: 0; /* 允许压缩 */ + display: flex; + flex-direction: column; +} +.region-card .region-table .table-body { + height: auto; + flex: 1; /* 占据剩余空间 */ + overflow-y: auto; /* 允许滚动 */ + min-height: 0; /* 允许压缩 */ +} + +/* 自定义滚动条样式 */ +.region-card .region-table .table-body::-webkit-scrollbar { + width: 6px; +} +.region-card .region-table .table-body::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 3px; +} +.region-card .region-table .table-body::-webkit-scrollbar-thumb { + background: rgba(0, 212, 255, 0.3); + border-radius: 3px; +} +.region-card .region-table .table-body::-webkit-scrollbar-thumb:hover { + background: rgba(0, 212, 255, 0.5); +} + .table-row { padding: 18px 0; /* 增加上下内边距,提高行高 */ min-height: 60px; /* 提高最小高度,列表更舒展 */ @@ -535,18 +815,30 @@ export default { .stat-item .value { color: #eaffff; font-size: 22px; font-weight: bold; } .stat-item .unit { color: #9ed7ff; font-size: 12px; } -.trend-chart { width: 100%; height: 320px; } +.trend-chart { + width: 100%; + flex: 1; + min-height: 0; +} @media (max-width: 1366px) { - .trend-chart { height: 280px; } - .table-header, .table-row { grid-template-columns: 0.9fr 1.3fr 1fr 0.9fr 0.9fr; } - .stats-grid { grid-template-columns: repeat(2, 1fr); } + /* 小屏幕下可能需要调整 */ + .trend-chart { height: auto; } + .breed-donut { height: auto; } + /* 保持6列布局,但可能需要调整字体或间距 */ + /* 如果太挤,可以改为第一行 3+3,第二行 2+2+2 仍然适用,或者改为 2列布局 */ + /* 这里暂时保持用户要求的布局 */ } @media (max-width: 768px) { .price-content { grid-template-columns: 1fr; } - .table-header, .table-row { grid-template-columns: 1fr 1.2fr 1fr 0.9fr 0.9fr; } + .breed-donut { height: 220px; } + .trend-chart { height: 180px; } + .stats-bottom { grid-template-columns: 1fr; } + .table-header, .table-row { grid-template-columns: 1fr 1.2fr 1fr 0.9fr; } .stats-grid { grid-template-columns: 1fr; } .th, .td { font-size: 13px; } } - \ No newline at end of file + + + diff --git a/src/main.js b/src/main.js index 01433bc..58940f6 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,5 @@ import { createApp } from 'vue' import App from './App.vue' +import './assets/global.css' createApp(App).mount('#app') diff --git a/vite.config.js b/vite.config.js index c54f20f..db7c767 100644 --- a/vite.config.js +++ b/vite.config.js @@ -25,3 +25,4 @@ export default defineConfig({ }, }, }) +