修改价格行情模块
This commit is contained in:
137
src/App.vue
137
src/App.vue
@@ -1,7 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import Home from './components/Home.vue'
|
import Home from './components/Home.vue'
|
||||||
import Alert from './components/Alert.vue'
|
|
||||||
import Price from './components/Price.vue'
|
import Price from './components/Price.vue'
|
||||||
|
|
||||||
// 当前激活的页面
|
// 当前激活的页面
|
||||||
@@ -13,7 +12,6 @@ const selectedProvince = ref('')
|
|||||||
// 页面组件映射
|
// 页面组件映射
|
||||||
const pageComponents = {
|
const pageComponents = {
|
||||||
home: Home,
|
home: Home,
|
||||||
alert: Alert,
|
|
||||||
price: Price
|
price: Price
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,9 +90,6 @@ const navigateToWarningMonitor = (provinceName) => {
|
|||||||
<div class="nav-item" :class="{ active: currentPage === 'price' }" @click="switchPage('price', 'NM')">
|
<div class="nav-item" :class="{ active: currentPage === 'price' }" @click="switchPage('price', 'NM')">
|
||||||
<span>价格行情</span>
|
<span>价格行情</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" :class="{ active: currentPage === 'alert' }" @click="switchPage('alert')">
|
|
||||||
<span>预警监测</span>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="title-container">
|
<div class="title-container">
|
||||||
@@ -141,6 +136,7 @@ body {
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
min-height: 100vh; /* 使用 min-height 而不是固定高度 */
|
min-height: 100vh; /* 使用 min-height 而不是固定高度 */
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
background-color: #011819; /* 在应用尚未渲染时也显示纯色背景 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard {
|
.dashboard {
|
||||||
@@ -161,7 +157,7 @@ body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(12, 20, 38, 0.45); /* 降低遮罩不透明度以提升整体亮度 */
|
background: #011819; /* 纯色覆盖层:在背景图之上、内容之下 */
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +493,7 @@ body {
|
|||||||
.dashboard-right {
|
.dashboard-right {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 80px; /* 紧贴顶部标题栏 */
|
top: 80px; /* 紧贴顶部标题栏 */
|
||||||
width: 580px; /* 增加侧边栏宽度 */
|
width: 680px; /* 加宽侧边栏 */
|
||||||
height: calc(100vh - 80px); /* 全屏高度减去顶部标题栏高度 */
|
height: calc(100vh - 80px); /* 全屏高度减去顶部标题栏高度 */
|
||||||
max-height: calc(100vh - 80px); /* 限制最大高度 */
|
max-height: calc(100vh - 80px); /* 限制最大高度 */
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -612,30 +608,23 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-list {
|
.alert-list {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-item {
|
.alert-item {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-item.warning {
|
.alert-item.warning {
|
||||||
border-left: 4px solid #ff6b35;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-item.normal {
|
.alert-item.normal {
|
||||||
border-left: 4px solid #00ff88;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-text {
|
.alert-text {
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-placeholder {
|
.chart-placeholder {
|
||||||
@@ -702,170 +691,112 @@ body {
|
|||||||
|
|
||||||
/* 传感器状态样式 */
|
/* 传感器状态样式 */
|
||||||
.sensor-list {
|
.sensor-list {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sensor-item {
|
.sensor-item {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sensor-name {
|
.sensor-name {
|
||||||
color: #ffffff;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sensor-status.online {
|
.sensor-status.online {
|
||||||
color: #ffffff;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sensor-status.offline {
|
.sensor-status.offline {
|
||||||
color: #84acf0;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* 设备列表样式 */
|
/* 设备列表样式 */
|
||||||
.device-list {
|
.device-list {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-item {
|
.device-item {
|
||||||
padding: 12px;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-name {
|
.device-name {
|
||||||
color: #ffffff;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-status.online {
|
.device-status.online {
|
||||||
color: #ffffff;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-status.offline {
|
.device-status.offline {
|
||||||
color: #84acf0;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 预警规则样式 */
|
/* 预警规则样式 */
|
||||||
.rule-list {
|
.rule-list {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-item {
|
.rule-item {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-name {
|
.rule-name {
|
||||||
color: #ffffff;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-threshold {
|
.rule-threshold {
|
||||||
color: #00d4ff;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 预警详情样式 */
|
/* 预警详情样式 */
|
||||||
.alert-details {
|
.alert-details {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
max-height: 500px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-detail-item {
|
.alert-detail-item {
|
||||||
padding: 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border-left: 4px solid;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-detail-item.high {
|
.alert-detail-item.high {
|
||||||
border-left-color: #ffffff;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-detail-item.medium {
|
.alert-detail-item.medium {
|
||||||
border-left-color: #84acf0;
|
|
||||||
background: rgba(132, 172, 240, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-detail-item.low {
|
.alert-detail-item.low {
|
||||||
border-left-color: #336699;
|
|
||||||
background: rgba(51, 102, 153, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-time {
|
.alert-time {
|
||||||
color: #888;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-title {
|
.alert-title {
|
||||||
color: #ffffff;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-desc {
|
.alert-desc {
|
||||||
color: #cccccc;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-level {
|
.alert-level {
|
||||||
display: inline-block;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-detail-item.high .alert-level {
|
.alert-detail-item.high .alert-level {
|
||||||
background: #ffffff;
|
|
||||||
color: #000000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-detail-item.medium .alert-level {
|
.alert-detail-item.medium .alert-level {
|
||||||
background: #84acf0;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-detail-item.low .alert-level {
|
.alert-detail-item.low .alert-level {
|
||||||
background: #336699;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 处理记录样式 */
|
/* 处理记录样式 */
|
||||||
|
|||||||
32
src/assets/global.css
Normal file
32
src/assets/global.css
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/* 全局隐藏滚动条(保持可滚动) */
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
* {
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* IE/Edge 旧版 */
|
||||||
|
* {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WebKit (Chrome/Safari/新 Edge) */
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 明确为根元素也设置一次,避免某些嵌套容器覆盖 */
|
||||||
|
html, body, #app {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
html::-webkit-scrollbar,
|
||||||
|
body::-webkit-scrollbar,
|
||||||
|
#app::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -20,10 +20,9 @@
|
|||||||
<div>{{ row.breed }}</div>
|
<div>{{ row.breed }}</div>
|
||||||
<div class="price-bar-cell">
|
<div class="price-bar-cell">
|
||||||
<div class="price-bar-track">
|
<div class="price-bar-track">
|
||||||
<div class="price-bar" :style="getPriceBarStyle(row.price)">
|
<div class="price-bar" :style="getPriceBarStyle(row.price)"></div>
|
||||||
<span class="price-value on-bar">{{ formatPrice(row.price) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span class="price-value after-bar">{{ formatPrice(row.price) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,6 +66,11 @@
|
|||||||
<!-- 地图容器 -->
|
<!-- 地图容器 -->
|
||||||
<div class="map-container">
|
<div class="map-container">
|
||||||
<Map3D @province-click="handleProvinceClick" />
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -77,6 +81,8 @@
|
|||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h3>品种单价排行榜(元/斤)</h3>
|
<h3>品种单价排行榜(元/斤)</h3>
|
||||||
<select v-model="rightSelectedBreed" class="breed-selector">
|
<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>
|
<option v-for="b in rightBreedOptions" :key="b" :value="b">{{ b }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,6 +169,9 @@
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
position: relative; /* 允许作为背景层容器 */
|
||||||
|
min-height: 100vh; /* 充满视口高度,保证背景覆盖 */
|
||||||
|
background-color: #011819; /* 底层纯色背景,位于背景图之上、内容之下 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 全国牛存栏量显示样式 */
|
/* 全国牛存栏量显示样式 */
|
||||||
@@ -206,7 +215,7 @@
|
|||||||
}
|
}
|
||||||
.province-price-ranking-chart {
|
.province-price-ranking-chart {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 500px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
.price-ranking-panel {
|
.price-ranking-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -304,11 +313,10 @@
|
|||||||
color: #eaf7ff;
|
color: #eaf7ff;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.price-value.on-bar {
|
.price-value.after-bar {
|
||||||
min-width: unset;
|
min-width: 86px;
|
||||||
width: 100%;
|
text-align: right;
|
||||||
text-align: center;
|
color: #eaf7ff;
|
||||||
color: #ffffff;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.dashboard-center {
|
.dashboard-center {
|
||||||
@@ -457,54 +465,104 @@ export default {
|
|||||||
const showNoDataToast = ref(false)
|
const showNoDataToast = ref(false)
|
||||||
const noDataMessage = ref('')
|
const noDataMessage = ref('')
|
||||||
let toastTimer = null
|
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) => {
|
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 {
|
try {
|
||||||
console.log('正在请求省份数据:', provinceName);
|
const apiParam = toApiProvinceParam(provinceName)
|
||||||
const res = await axios.get('/api/cattle-data', {
|
url = `/api/cattle-data/provinces?province=${encodeURIComponent(apiParam)}`
|
||||||
params: { province: provinceName }
|
const res = await fetch(url, { signal: provinceClickController.signal })
|
||||||
})
|
clearTimeout(timeoutId)
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
console.log('省份数据返回:', res.data);
|
const raw = await res.json()
|
||||||
const raw = res.data;
|
const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : [])
|
||||||
// 兼容直接返回数组或 { data: [] } 的结构
|
|
||||||
const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : []);
|
|
||||||
|
|
||||||
if (list && list.length > 0) {
|
if (list && list.length > 0) {
|
||||||
console.log('数据存在,准备跳转');
|
// 接口成功且有数据,自动跳转到价格行情页
|
||||||
|
isJumpLoading.value = false
|
||||||
emit('navigate-to-warning', provinceName)
|
emit('navigate-to-warning', provinceName)
|
||||||
} else {
|
} else {
|
||||||
console.warn('该地区无数据');
|
throw new Error('empty payload')
|
||||||
noDataMessage.value = `该地区暂无数据: ${provinceName}`
|
|
||||||
showNoDataToast.value = true
|
|
||||||
if (toastTimer) clearTimeout(toastTimer)
|
|
||||||
toastTimer = setTimeout(() => {
|
|
||||||
showNoDataToast.value = false
|
|
||||||
}, 3000)
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取省份数据出错:', error)
|
clearTimeout(timeoutId)
|
||||||
noDataMessage.value = `获取数据失败: ${provinceName}`
|
// 区分超时与其他错误
|
||||||
|
if (error?.name === 'AbortError') {
|
||||||
|
console.warn('[外部接口] 超时已中止请求 (8s):', url)
|
||||||
|
noDataMessage.value = '请求超时'
|
||||||
|
} else {
|
||||||
|
console.error('[外部接口] 调用失败:', error)
|
||||||
|
noDataMessage.value = '接口调用失败'
|
||||||
|
}
|
||||||
|
isJumpLoading.value = false
|
||||||
showNoDataToast.value = true
|
showNoDataToast.value = true
|
||||||
if (toastTimer) clearTimeout(toastTimer)
|
if (toastTimer) clearTimeout(toastTimer)
|
||||||
toastTimer = setTimeout(() => {
|
toastTimer = setTimeout(() => {
|
||||||
showNoDataToast.value = false
|
showNoDataToast.value = false
|
||||||
}, 3000)
|
}, 2000)
|
||||||
|
} finally {
|
||||||
|
provinceClickController = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleProvinceClick,
|
handleProvinceClick,
|
||||||
showNoDataToast,
|
showNoDataToast,
|
||||||
noDataMessage
|
noDataMessage,
|
||||||
|
isJumpLoading,
|
||||||
|
loadingProvince
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// 右侧品种单价排行榜下拉选项与选中值
|
// 右侧品种单价排行榜下拉选项与选中值
|
||||||
rightBreedOptions: ['安格斯','牦牛','黄牛','利木赞牛','鲁西牛','奶牛','肉牛','水牛','西门塔尔牛','夏洛莱牛','杂交牛','牛'],
|
rightBreedOptions: [],
|
||||||
rightSelectedBreed: '安格斯',
|
rightSelectedBreed: '',
|
||||||
|
breedsLoading: false,
|
||||||
speciesPriceRows: [], // 品种单价排行(接口)
|
speciesPriceRows: [], // 品种单价排行(接口)
|
||||||
// 存栏率视图模式:percent 或 count
|
// 存栏率视图模式:percent 或 count
|
||||||
livestockViewMode: 'percent',
|
livestockViewMode: 'percent',
|
||||||
@@ -987,7 +1045,7 @@ export default {
|
|||||||
? [...this.nationalProvinceAverageRows]
|
? [...this.nationalProvinceAverageRows]
|
||||||
: (Array.isArray(this.nationalPriceTableRows) ? [...this.nationalPriceTableRows] : [])
|
: (Array.isArray(this.nationalPriceTableRows) ? [...this.nationalPriceTableRows] : [])
|
||||||
base.sort((a, b) => a.price - b.price)
|
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)
|
const prices = base.map(r => r.price)
|
||||||
return {
|
return {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
@@ -1003,36 +1061,39 @@ export default {
|
|||||||
return `${p.axisValue}<br/>${p.seriesName}: ${p.value} 元/斤`
|
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: {
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: xCategories,
|
||||||
|
axisLine: { lineStyle: { color: '#00ffff' } },
|
||||||
|
axisLabel: { color: '#ffffff', fontSize: 9, interval: 0 },
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
name: '单价(元/斤)',
|
name: '单价(元/斤)',
|
||||||
nameTextStyle: { color: '#00ffff', fontSize: 11 },
|
nameTextStyle: { color: '#00ffff', fontSize: 11 },
|
||||||
axisLine: { lineStyle: { color: '#00ffff' } },
|
axisLine: { lineStyle: { color: '#00ffff' } },
|
||||||
axisLabel: { color: '#ffffff', fontSize: 10 },
|
axisLabel: { color: '#ffffff', fontSize: 10 },
|
||||||
|
min: 12,
|
||||||
|
max: 15,
|
||||||
|
interval: 1,
|
||||||
splitLine: { lineStyle: { color: 'rgba(0,255,255,0.2)' } }
|
splitLine: { lineStyle: { color: 'rgba(0,255,255,0.2)' } }
|
||||||
},
|
},
|
||||||
yAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: yCategories,
|
|
||||||
axisLine: { lineStyle: { color: '#00ffff' } },
|
|
||||||
axisLabel: { color: '#ffffff', fontSize: 11 }
|
|
||||||
},
|
|
||||||
series: [{
|
series: [{
|
||||||
name: '省份单价',
|
name: '省份单价',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: prices,
|
data: prices,
|
||||||
barWidth: '42%',
|
barWidth: '42%',
|
||||||
barCategoryGap: '28%',
|
barCategoryGap: '28%',
|
||||||
itemStyle: { color: '#4e73df' },
|
itemStyle: { color: '#00E1E1' },
|
||||||
label: { show: true, position: 'right', color: '#cfefff', fontSize: 10, formatter: '{c} 元/斤' }
|
label: { show: true, position: 'top', color: '#cfefff', fontSize: 10, formatter: '{c} ' }
|
||||||
}],
|
}],
|
||||||
dataZoom: [
|
dataZoom: [
|
||||||
{
|
{
|
||||||
type: 'inside',
|
type: 'inside',
|
||||||
yAxisIndex: 0,
|
xAxisIndex: 0,
|
||||||
startValue: 0,
|
startValue: 0,
|
||||||
endValue: yCategories.length - 1,
|
endValue: xCategories.length - 1,
|
||||||
zoomLock: true,
|
zoomLock: true,
|
||||||
filterMode: 'empty'
|
filterMode: 'empty'
|
||||||
}
|
}
|
||||||
@@ -1062,6 +1123,16 @@ export default {
|
|||||||
immediate: true
|
immediate: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
// 页面挂载后拉取品种列表以填充右侧下拉框
|
||||||
|
console.log('[Home] mounted: fetchBreeds 即将调用')
|
||||||
|
this.fetchBreeds()
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// 保险起见,在 created 阶段也触发一次,避免某些场景 mounted 未执行
|
||||||
|
console.log('[Home] created: fetchBreeds 即将调用')
|
||||||
|
this.fetchBreeds()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 格式化价格显示(千分位)
|
// 格式化价格显示(千分位)
|
||||||
formatPrice(value) {
|
formatPrice(value) {
|
||||||
@@ -1136,6 +1207,36 @@ export default {
|
|||||||
console.warn('获取全国省份平均单价失败:', e)
|
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() {
|
buildLivestockPieOption() {
|
||||||
const baseline = this.livestockBaseline
|
const baseline = this.livestockBaseline
|
||||||
@@ -1477,6 +1578,38 @@ export default {
|
|||||||
margin-top: 0; /* 移除负边距 */
|
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 {
|
.lowest-price-panel h3 {
|
||||||
|
|||||||
@@ -1106,9 +1106,6 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建点击反馈提示
|
|
||||||
this.showClickFeedback({ clientX: pointer.clientX, clientY: pointer.clientY }, provinceName);
|
|
||||||
|
|
||||||
// 触发省份点击事件
|
// 触发省份点击事件
|
||||||
emit('province-click', provinceName);
|
emit('province-click', provinceName);
|
||||||
|
|
||||||
@@ -1222,25 +1219,7 @@ export default {
|
|||||||
this.isMobileDevice = isMobile;
|
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() {}
|
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 {
|
.cattle-source-top5 {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom:80px; /* 定位到底部 */
|
bottom:80px; /* 定位到底部 */
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { use } from 'echarts/core'
|
import { use } from 'echarts/core'
|
||||||
import { CanvasRenderer } from 'echarts/renderers'
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
import { LineChart } from 'echarts/charts'
|
import { LineChart, PieChart, BarChart } from 'echarts/charts'
|
||||||
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent } from 'echarts/components'
|
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent } from 'echarts/components'
|
||||||
import VChart from 'vue-echarts'
|
import VChart from 'vue-echarts'
|
||||||
|
|
||||||
use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, GridComponent, LegendComponent])
|
use([CanvasRenderer, LineChart, PieChart, BarChart, TitleComponent, TooltipComponent, GridComponent, LegendComponent])
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Price',
|
name: 'Price',
|
||||||
@@ -43,7 +43,7 @@ export default {
|
|||||||
CQ: ['本地黄牛', '西门塔尔牛']
|
CQ: ['本地黄牛', '西门塔尔牛']
|
||||||
}
|
}
|
||||||
|
|
||||||
const unit = ref('元/个')
|
const unit = ref('元/斤')
|
||||||
const days = ref([])
|
const days = ref([])
|
||||||
const priceSeries = ref([])
|
const priceSeries = ref([])
|
||||||
// 省内各地区牛数据列表
|
// 省内各地区牛数据列表
|
||||||
@@ -98,6 +98,83 @@ export default {
|
|||||||
}]
|
}]
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// 品种占比统计(环形图)
|
||||||
|
const breedPieOption = computed(() => {
|
||||||
|
// 按左侧列表统计各品种出现次数作为占比;无数据时按照品种列表均匀分配
|
||||||
|
const counts = new Map()
|
||||||
|
if (regionRows.value.length) {
|
||||||
|
regionRows.value.forEach(r => {
|
||||||
|
const k = r.breed || '未知品种'
|
||||||
|
counts.set(k, (counts.get(k) || 0) + 1)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 示例分布:根据索引给出不同权重
|
||||||
|
breeds.value.forEach((b, i) => {
|
||||||
|
counts.set(b, 10 + (i * 5))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const data = Array.from(counts.entries()).map(([name, value]) => ({ name, value }))
|
||||||
|
// 与大屏主体色一致的配色(青蓝系)
|
||||||
|
const brandColors = ['#00d4ff', '#29e3ff', '#00b3f9', '#3bd1ff', '#0099ff', '#61eaff']
|
||||||
|
return {
|
||||||
|
color: brandColors,
|
||||||
|
title: { text: '品种占比统计', left: 'center', top: 6, textStyle: { color: '#eaf7ff', fontSize: 14 } },
|
||||||
|
legend: { right: 20, top: 'middle', orient: 'vertical', textStyle: { color: '#cfefff' }, itemWidth: 12, itemHeight: 12 },
|
||||||
|
tooltip: { trigger: 'item' },
|
||||||
|
series: [{
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['48%', '70%'],
|
||||||
|
center: ['50%', '55%'],
|
||||||
|
avoidLabelOverlap: true,
|
||||||
|
label: { show: false },
|
||||||
|
emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold', formatter: '{b}: {d}%' } },
|
||||||
|
itemStyle: {
|
||||||
|
borderColor: '#011819',
|
||||||
|
borderWidth: 6,
|
||||||
|
borderRadius: 8
|
||||||
|
},
|
||||||
|
data
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 优先通过接口生成省内地区牛数据列表,失败则回退示例数据
|
||||||
|
const fetchProvinceRegionRows = async () => {
|
||||||
|
const url = '/api/cattle-data'
|
||||||
|
try {
|
||||||
|
const res = await fetch(url)
|
||||||
|
const raw = await res.json()
|
||||||
|
const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : [])
|
||||||
|
const today = new Date()
|
||||||
|
const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
|
||||||
|
|
||||||
|
// 仅保留当前省份的数据
|
||||||
|
const filtered = list.filter(item => {
|
||||||
|
const p = item.province ?? item.provinceName ?? ''
|
||||||
|
return p === provinceName.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const rows = filtered.map((item, idx) => ({
|
||||||
|
id: item.id ?? `${props.selectedProvince}-${idx}`,
|
||||||
|
date: fmt(today),
|
||||||
|
region: item.location ?? '',
|
||||||
|
breed: item.type ?? item.breed ?? '',
|
||||||
|
price: Number(item.price),
|
||||||
|
delta: 0,
|
||||||
|
up: false
|
||||||
|
})).filter(r => r.region && r.breed && Number.isFinite(r.price))
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
regionRows.value = rows
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Price] 获取省内地区牛数据失败:', e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 生成省内地区牛数据列表(示例,固定为10行)
|
// 生成省内地区牛数据列表(示例,固定为10行)
|
||||||
const genRegionRows = () => {
|
const genRegionRows = () => {
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
@@ -154,6 +231,119 @@ export default {
|
|||||||
genData(row?.id)
|
genData(row?.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 年度统计数据(示例)
|
||||||
|
const statsYears = ref([ 2023, 2024,2025])
|
||||||
|
const stockYears = ref([2023, 2024, 2025])
|
||||||
|
const stockCounts = ref([]) // 存栏数量(万头)
|
||||||
|
const slaughterCounts = ref([]) // 出栏数量(万头)
|
||||||
|
|
||||||
|
let stockAbortController = null
|
||||||
|
|
||||||
|
// provinces 接口参数映射:与 Home.vue 保持一致,以避免名称差异导致无数据
|
||||||
|
const toApiProvinceParam = (name) => {
|
||||||
|
const map = {
|
||||||
|
'内蒙古自治区': '内蒙古',
|
||||||
|
'四川省': '四川',
|
||||||
|
'新疆维吾尔自治区': '新疆',
|
||||||
|
'西藏自治区': '西藏',
|
||||||
|
'宁夏回族自治区': '宁夏',
|
||||||
|
'广西壮族自治区': '广西',
|
||||||
|
'河北省': '河北',
|
||||||
|
'山东省': '山东',
|
||||||
|
'黑龙江省': '黑龙江',
|
||||||
|
'吉林省': '吉林',
|
||||||
|
'云南省': '云南',
|
||||||
|
'甘肃省': '甘肃',
|
||||||
|
'青海省': '青海',
|
||||||
|
'贵州省': '贵州',
|
||||||
|
'安徽省': '安徽',
|
||||||
|
}
|
||||||
|
return map[name] ?? name
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchStockData = async () => {
|
||||||
|
// 取消上一次未完成的请求
|
||||||
|
if (stockAbortController) {
|
||||||
|
stockAbortController.abort()
|
||||||
|
}
|
||||||
|
stockAbortController = new AbortController()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!provinceName.value) return
|
||||||
|
const apiParam = toApiProvinceParam(provinceName.value)
|
||||||
|
const url = `/api/cattle-data/provinces?province=${encodeURIComponent(apiParam)}`
|
||||||
|
const res = await fetch(url, { signal: stockAbortController.signal })
|
||||||
|
const raw = await res.json()
|
||||||
|
|
||||||
|
let data = raw
|
||||||
|
if (raw.code === 200 && raw.data) {
|
||||||
|
data = raw.data
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = data
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
target = data.find(d => d.province === provinceName.value || d.provinceName === provinceName.value)
|
||||||
|
if (!target && data.length > 0) target = data[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
stockCounts.value = [
|
||||||
|
Number(target.inventory23th) || 0,
|
||||||
|
Number(target.inventory24th) || 0,
|
||||||
|
Number(target.inventory25th) || 0
|
||||||
|
]
|
||||||
|
// 同步设置出栏统计(与存栏相同来源)
|
||||||
|
slaughterCounts.value = [
|
||||||
|
Number(target.slaughter23th) || 0,
|
||||||
|
Number(target.slaughter24th) || 0,
|
||||||
|
Number(target.slaughter25th) || 0
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
stockCounts.value = [0, 0, 0]
|
||||||
|
slaughterCounts.value = [0, 0, 0]
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name === 'AbortError') return
|
||||||
|
console.error('获取存栏数据失败', e)
|
||||||
|
stockCounts.value = [0, 0, 0]
|
||||||
|
slaughterCounts.value = [0, 0, 0]
|
||||||
|
} finally {
|
||||||
|
stockAbortController = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const genStats = () => {
|
||||||
|
// 统一使用接口返回值,不再生成示例数据
|
||||||
|
fetchStockData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const stockBarOption = computed(() => ({
|
||||||
|
title: { text: '存栏统计', left: 'center', textStyle: { color: '#eaf7ff' } },
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
grid: { left: 40, right: 20, top: 50, bottom: 40 },
|
||||||
|
xAxis: { type: 'category', data: stockYears.value, axisLabel: { color: '#cfefff' }, axisLine: { lineStyle: { color: '#2e6ba8' } } },
|
||||||
|
yAxis: { type: 'value', axisLabel: { color: '#cfefff' }, splitLine: { lineStyle: { color: 'rgba(0,212,255,0.2)' } } },
|
||||||
|
series: [{
|
||||||
|
name: '存栏', type: 'bar', data: stockCounts.value,
|
||||||
|
itemStyle: { color: '#00d4ff' },
|
||||||
|
barWidth: '40%'
|
||||||
|
}]
|
||||||
|
}))
|
||||||
|
|
||||||
|
const slaughterLineOption = computed(() => ({
|
||||||
|
title: { text: '出栏统计', left: 'center', textStyle: { color: '#eaf7ff' } },
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
grid: { left: 40, right: 20, top: 50, bottom: 40 },
|
||||||
|
xAxis: { type: 'category', data: statsYears.value, axisLabel: { color: '#cfefff' }, axisLine: { lineStyle: { color: '#2e6ba8' } } },
|
||||||
|
yAxis: { type: 'value', axisLabel: { color: '#cfefff' }, splitLine: { lineStyle: { color: 'rgba(0,212,255,0.2)' } } },
|
||||||
|
series: [{
|
||||||
|
name: '出栏', type: 'line', smooth: true, data: slaughterCounts.value,
|
||||||
|
lineStyle: { color: '#29e3ff', width: 2 },
|
||||||
|
itemStyle: { color: '#61eaff' },
|
||||||
|
areaStyle: { color: 'rgba(0,212,255,0.12)' }
|
||||||
|
}]
|
||||||
|
}))
|
||||||
|
|
||||||
const yesterdayDelta = computed(() => {
|
const yesterdayDelta = computed(() => {
|
||||||
if (!priceSeries.value.length || priceSeries.value.length < 2) return 0
|
if (!priceSeries.value.length || priceSeries.value.length < 2) return 0
|
||||||
const n = priceSeries.value.length
|
const n = priceSeries.value.length
|
||||||
@@ -170,12 +360,17 @@ export default {
|
|||||||
return d > 0 ? '↑' : '↓'
|
return d > 0 ? '↑' : '↓'
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
// 统一在选中省份变化时加载数据,并在初始化时立即执行一次
|
||||||
genRegionRows()
|
watch(() => props.selectedProvince, async (newVal, oldVal) => {
|
||||||
|
if (newVal === oldVal) return
|
||||||
|
selectedRow.value = null
|
||||||
|
const ok = await fetchProvinceRegionRows()
|
||||||
|
if (!ok) genRegionRows()
|
||||||
genData()
|
genData()
|
||||||
})
|
genStats()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
return { unit, provinceName, breeds, todayPrice, max7d, min7d, avg7d, avgToday, days, priceSeries, chartOption, regionRows, selectedRow, handleRowClick, isUnknownRegion, range, weight, setRange, setWeight, yesterdayDelta, trendText, trendSymbol }
|
return { unit, provinceName, breeds, todayPrice, max7d, min7d, avg7d, avgToday, days, priceSeries, chartOption, breedPieOption, stockBarOption, slaughterLineOption, regionRows, selectedRow, handleRowClick, isUnknownRegion, range, weight, setRange, setWeight, yesterdayDelta, trendText, trendSymbol }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -188,79 +383,94 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="price-content">
|
<div class="price-content">
|
||||||
<section class="left-info">
|
<!-- 第一行:左侧牛数据列表 + 右侧价格详情(等高) -->
|
||||||
<div class="card region-card">
|
<div class="card region-card">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div class="diamond-icon"></div>
|
<div class="diamond-icon"></div>
|
||||||
<h3>省内各地区牛数据列表</h3>
|
<h3>省内各地区牛数据列表</h3>
|
||||||
|
</div>
|
||||||
|
<div class="region-table">
|
||||||
|
<div class="table-header">
|
||||||
|
<div class="th">时间</div>
|
||||||
|
<div class="th">地区</div>
|
||||||
|
<div class="th">品类</div>
|
||||||
|
<div class="th">价格({{ unit }})</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="region-table">
|
<div class="table-body">
|
||||||
<div class="table-header">
|
<div class="table-row" v-for="row in regionRows" :key="row.id" @click="handleRowClick(row)" :class="{ active: selectedRow && selectedRow.id === row.id }">
|
||||||
<div class="th">时间</div>
|
<div class="td">{{ row.date }}</div>
|
||||||
<div class="th">地区</div>
|
<div class="td">{{ row.region }}</div>
|
||||||
<div class="th">品类</div>
|
<div class="td">{{ row.breed }}</div>
|
||||||
<div class="th">价格({{ unit }})</div>
|
<div class="td">{{ row.price.toFixed(2) }}{{ unit }}</div>
|
||||||
<div class="th">涨跌</div>
|
|
||||||
</div>
|
|
||||||
<div class="table-body">
|
|
||||||
<div class="table-row" v-for="row in regionRows" :key="row.id" @click="handleRowClick(row)" :class="{ active: selectedRow && selectedRow.id === row.id }">
|
|
||||||
<div class="td">{{ row.date }}</div>
|
|
||||||
<div class="td">{{ row.region }}</div>
|
|
||||||
<div class="td">{{ row.breed }}</div>
|
|
||||||
<div class="td">{{ row.price.toFixed(2) }}{{ unit }}</div>
|
|
||||||
<div class="td">
|
|
||||||
<span :class="row.delta === 0 ? 'change-zero' : (row.up ? 'change-up' : 'change-down')">
|
|
||||||
{{ row.delta.toFixed(2) }}{{ unit }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="right-stats">
|
<div class="card detail-card">
|
||||||
<div class="card detail-card">
|
<div class="panel-header">
|
||||||
<div class="panel-header">
|
<div class="diamond-icon"></div>
|
||||||
<div class="diamond-icon"></div>
|
<h3>
|
||||||
<h3>
|
<span :class="{ highlight: isUnknownRegion(selectedRow ? selectedRow.region : provinceName) }">
|
||||||
<span :class="{ highlight: isUnknownRegion(selectedRow ? selectedRow.region : provinceName) }">
|
{{ (selectedRow ? selectedRow.region : provinceName) }}
|
||||||
{{ (selectedRow ? selectedRow.region : provinceName) }}
|
</span>
|
||||||
</span>
|
价格详情
|
||||||
价格详情
|
</h3>
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="detail-content">
|
|
||||||
<div class="today-price-line">
|
|
||||||
<div class="labels">
|
|
||||||
<span class="label">今日批发均价</span>
|
|
||||||
<span class="compare">相比昨日 <span :class="yesterdayDelta === 0 ? 'change-zero' : (yesterdayDelta > 0 ? 'change-up' : 'change-down')">{{ trendText }} {{ yesterdayDelta.toFixed(2) }}</span> <span class="symbol">{{ trendSymbol }}</span></span>
|
|
||||||
</div>
|
|
||||||
<div class="price-display">
|
|
||||||
<span class="currency">¥</span>
|
|
||||||
<span class="value">{{ avgToday.toFixed(2) }}</span>
|
|
||||||
<span class="unit">{{ unit }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filters">
|
|
||||||
<div class="weight-group">
|
|
||||||
<button :class="['filter-btn', { active: weight === 'normal' }]" @click="setWeight('normal')">通货</button>
|
|
||||||
<button :class="['filter-btn', { active: weight === '400' }]" @click="setWeight('400')">400斤</button>
|
|
||||||
<button :class="['filter-btn', { active: weight === '500' }]" @click="setWeight('500')">500斤</button>
|
|
||||||
</div>
|
|
||||||
<div class="range-group">
|
|
||||||
<button :class="['filter-btn', { active: range === 7 }]" @click="setRange(7)">近7天</button>
|
|
||||||
<button :class="['filter-btn', { active: range === 30 }]" @click="setRange(30)">近30天</button>
|
|
||||||
<button :class="['filter-btn', { active: range === 60 }]" @click="setRange(60)">近60天</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-chart class="trend-chart" :option="chartOption" autoresize />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<div class="detail-content">
|
||||||
|
<div class="today-price-line">
|
||||||
|
<div class="labels">
|
||||||
|
<span class="label">今日批发均价</span>
|
||||||
|
<span class="compare">相比昨日 <span :class="yesterdayDelta === 0 ? 'change-zero' : (yesterdayDelta > 0 ? 'change-up' : 'change-down')">{{ trendText }} {{ yesterdayDelta.toFixed(2) }}</span> <span class="symbol">{{ trendSymbol }}</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="price-display">
|
||||||
|
<span class="currency">¥</span>
|
||||||
|
<span class="value">{{ avgToday.toFixed(2) }}</span>
|
||||||
|
<span class="unit">{{ unit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters">
|
||||||
|
<div class="weight-group">
|
||||||
|
<button :class="['filter-btn', { active: weight === 'normal' }]" @click="setWeight('normal')">通货</button>
|
||||||
|
<button :class="['filter-btn', { active: weight === '400' }]" @click="setWeight('400')">400斤</button>
|
||||||
|
<button :class="['filter-btn', { active: weight === '500' }]" @click="setWeight('500')">500斤</button>
|
||||||
|
</div>
|
||||||
|
<div class="range-group">
|
||||||
|
<button :class="['filter-btn', { active: range === 7 }]" @click="setRange(7)">近7天</button>
|
||||||
|
<button :class="['filter-btn', { active: range === 30 }]" @click="setRange(30)">近30天</button>
|
||||||
|
<button :class="['filter-btn', { active: range === 60 }]" @click="setRange(60)">近60天</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-chart class="trend-chart" :option="chartOption" autoresize />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第二行:左侧品种占比 + 中间存栏 + 右侧出栏(三列等宽) -->
|
||||||
|
<div class="card breed-card">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="diamond-icon"></div>
|
||||||
|
<h3>品种占比统计</h3>
|
||||||
|
</div>
|
||||||
|
<v-chart class="breed-donut" :option="breedPieOption" autoresize />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card stock-card">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="diamond-icon"></div>
|
||||||
|
<h3>存栏统计</h3>
|
||||||
|
</div>
|
||||||
|
<v-chart class="stock-chart" :option="stockBarOption" autoresize />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card slaughter-card">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="diamond-icon"></div>
|
||||||
|
<h3>出栏统计</h3>
|
||||||
|
</div>
|
||||||
|
<v-chart class="slaughter-chart" :option="slaughterLineOption" autoresize />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -276,6 +486,7 @@ export default {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
background: #011819; /* 与预警监测页背景一致 */
|
background: #011819; /* 与预警监测页背景一致 */
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden; /* 页面高度固定,禁用整体滚动 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-page::before {
|
.price-page::before {
|
||||||
@@ -312,8 +523,11 @@ export default {
|
|||||||
|
|
||||||
.price-content {
|
.price-content {
|
||||||
display: grid;
|
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;
|
gap: 16px;
|
||||||
|
flex: 1; /* 占满剩余高度 */
|
||||||
|
min-height: 0; /* 允许内部滚动 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
@@ -324,8 +538,18 @@ export default {
|
|||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
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 {
|
.card::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -396,23 +620,48 @@ export default {
|
|||||||
color: #cfefff;
|
color: #cfefff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-stats {
|
/* 移除旧的 .right-stats, .left-info, .stats-bottom 相关样式 */
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-card {
|
.breed-card {
|
||||||
background: rgba(255,255,255,0.06);
|
background: rgba(255,255,255,0.06);
|
||||||
border: 1px solid rgba(0,212,255,0.25);
|
border: 1px solid rgba(0,212,255,0.25);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 10px 12px;
|
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 {
|
.detail-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.today-price-line {
|
||||||
@@ -452,7 +701,7 @@ export default {
|
|||||||
|
|
||||||
.table-header, .table-row {
|
.table-header, .table-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1.6fr 1.2fr 1fr 1fr; /* 时间略窄,地区更宽,价格与涨跌等宽 */
|
grid-template-columns: 1fr 1.6fr 1.2fr 1fr; /* 移除涨跌列后调整为四列 */
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -469,6 +718,37 @@ export default {
|
|||||||
flex-direction: column;
|
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 {
|
.table-row {
|
||||||
padding: 18px 0; /* 增加上下内边距,提高行高 */
|
padding: 18px 0; /* 增加上下内边距,提高行高 */
|
||||||
min-height: 60px; /* 提高最小高度,列表更舒展 */
|
min-height: 60px; /* 提高最小高度,列表更舒展 */
|
||||||
@@ -535,18 +815,30 @@ export default {
|
|||||||
.stat-item .value { color: #eaffff; font-size: 22px; font-weight: bold; }
|
.stat-item .value { color: #eaffff; font-size: 22px; font-weight: bold; }
|
||||||
.stat-item .unit { color: #9ed7ff; font-size: 12px; }
|
.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) {
|
@media (max-width: 1366px) {
|
||||||
.trend-chart { height: 280px; }
|
/* 小屏幕下可能需要调整 */
|
||||||
.table-header, .table-row { grid-template-columns: 0.9fr 1.3fr 1fr 0.9fr 0.9fr; }
|
.trend-chart { height: auto; }
|
||||||
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
.breed-donut { height: auto; }
|
||||||
|
/* 保持6列布局,但可能需要调整字体或间距 */
|
||||||
|
/* 如果太挤,可以改为第一行 3+3,第二行 2+2+2 仍然适用,或者改为 2列布局 */
|
||||||
|
/* 这里暂时保持用户要求的布局 */
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.price-content { grid-template-columns: 1fr; }
|
.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; }
|
.stats-grid { grid-template-columns: 1fr; }
|
||||||
.th, .td { font-size: 13px; }
|
.th, .td { font-size: 13px; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import './assets/global.css'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
createApp(App).mount('#app')
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user