修改价格行情模块
This commit is contained in:
@@ -20,10 +20,9 @@
|
||||
<div>{{ row.breed }}</div>
|
||||
<div class="price-bar-cell">
|
||||
<div class="price-bar-track">
|
||||
<div class="price-bar" :style="getPriceBarStyle(row.price)">
|
||||
<span class="price-value on-bar">{{ formatPrice(row.price) }}</span>
|
||||
</div>
|
||||
<div class="price-bar" :style="getPriceBarStyle(row.price)"></div>
|
||||
</div>
|
||||
<span class="price-value after-bar">{{ formatPrice(row.price) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,6 +66,11 @@
|
||||
<!-- 地图容器 -->
|
||||
<div class="map-container">
|
||||
<Map3D @province-click="handleProvinceClick" />
|
||||
<!-- 跳转加载动画覆盖层 -->
|
||||
<div v-if="isJumpLoading" class="jump-loading-overlay">
|
||||
<div class="jump-spinner"></div>
|
||||
<div class="jump-loading-text">正在请求 {{ loadingProvince }} 数据...</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -77,6 +81,8 @@
|
||||
<div class="panel-header">
|
||||
<h3>品种单价排行榜(元/斤)</h3>
|
||||
<select v-model="rightSelectedBreed" class="breed-selector">
|
||||
<option v-if="breedsLoading" disabled>加载中...</option>
|
||||
<option v-else-if="rightBreedOptions.length === 0" disabled>暂无数据</option>
|
||||
<option v-for="b in rightBreedOptions" :key="b" :value="b">{{ b }}</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -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}<br/>${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 {
|
||||
|
||||
Reference in New Issue
Block a user