Files
datav---Cattle-Industry/src/components/Price.vue

552 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script>
import { ref, computed, onMounted } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent } from 'echarts/components'
import VChart from 'vue-echarts'
use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, GridComponent, LegendComponent])
export default {
name: 'Price',
components: { VChart },
props: {
selectedProvince: {
type: String,
default: ''
}
},
setup(props) {
const provinceNameMap = {
BJ: '北京市', TJ: '天津市', HE: '河北省', SX: '山西省', NM: '内蒙古自治区',
LN: '辽宁省', JL: '吉林省', HL: '黑龙江省', SH: '上海市', JS: '江苏省',
ZJ: '浙江省', AH: '安徽省', FJ: '福建省', JX: '江西省', SD: '山东省',
HA: '河南省', HB: '湖北省', HN: '湖南省', GD: '广东省', GX: '广西壮族自治区',
HI: '海南省', CQ: '重庆市', SC: '四川省', GZ: '贵州省', YN: '云南省',
XZ: '西藏自治区', SN: '陕西省', GS: '甘肃省', QH: '青海省', NX: '宁夏回族自治区', XJ: '新疆维吾尔自治区'
}
const provinceName = computed(() => provinceNameMap[props.selectedProvince] || props.selectedProvince || '未知地区')
// 省份基础价(元/斤),用于生成示例数据
const basePriceMap = {
NX: 6.8, BJ: 7.2, TJ: 6.9, HI: 7.0, CQ: 6.7, HE: 6.6, SD: 6.9, HB: 6.8
}
// 牛品种列表(示例)
const breedListMap = {
default: ['西门塔尔牛', '利木赞牛', '夏洛来牛', '本地黄牛'],
NX: ['西门塔尔牛', '本地黄牛', '海福特牛'],
BJ: ['利木赞牛', '安格斯牛', '夏洛来牛'],
HI: ['本地黄牛', '安格斯牛'],
CQ: ['本地黄牛', '西门塔尔牛']
}
const unit = ref('元/个')
const days = ref([])
const priceSeries = ref([])
// 省内各地区牛数据列表
const regionRows = ref([])
const selectedRow = ref(null)
const todayPrice = computed(() => (priceSeries.value[priceSeries.value.length - 1] ?? 0))
const max7d = computed(() => (priceSeries.value.length ? Math.max(...priceSeries.value) : 0))
const min7d = computed(() => (priceSeries.value.length ? Math.min(...priceSeries.value) : 0))
const avg7d = computed(() => {
if (!priceSeries.value.length) return 0
const sum = priceSeries.value.reduce((acc, v) => acc + v, 0)
return +(sum / priceSeries.value.length).toFixed(2)
})
// 当日均价(以省内各地区列表的当日价格求平均;无列表则回退为趋势当日值)
const avgToday = computed(() => {
if (selectedRow.value && selectedRow.value.price) {
return +(selectedRow.value.price).toFixed(2)
}
if (regionRows.value.length) {
const sum = regionRows.value.reduce((acc, r) => acc + (r.price || 0), 0)
return +(sum / regionRows.value.length).toFixed(2)
}
return todayPrice.value
})
const breeds = computed(() => breedListMap[props.selectedProvince] || breedListMap.default)
// 日期范围与重量类别筛选
const range = ref(7) // 7/30/60
const weight = ref('normal') // normal/400/500
const setRange = (v) => { range.value = v; genData(selectedRow.value?.id) }
const setWeight = (w) => { weight.value = w; genData(selectedRow.value?.id) }
const isUnknownRegion = (name) => {
const n = (name ?? '').toString()
return n === '未知地区' || n.startsWith('未知地区 ')
}
const chartOption = computed(() => ({
title: { text: `${provinceName.value} 牛价趋势`, left: 'center', textStyle: { color: '#00d4ff' } },
tooltip: { trigger: 'axis' },
grid: { left: 40, right: 20, top: 60, bottom: 40 },
xAxis: { type: 'category', data: days.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: priceSeries.value,
lineStyle: { color: '#00d4ff', width: 2 },
itemStyle: { color: '#00ffcc' },
areaStyle: { color: 'rgba(0,212,255,0.15)' }
}]
}))
// 生成省内地区牛数据列表示例固定为10行
const genRegionRows = () => {
const today = new Date()
const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
const regionTypes = [
'省会城区','市辖区','回族自治县','张北县','市郊区',
'开发区','交易市场','牧区','新区','工业园区'
]
const regions = regionTypes.map(t => `${provinceName.value} ${t}`)
const base = basePriceMap[props.selectedProvince] ?? 6.8
regionRows.value = regions.slice(0, 10).map((r, idx) => {
const d = new Date(today)
const fluct = (Math.sin(idx) * 0.08) + (Math.random() * 0.12 - 0.06)
const breed = (breeds.value[idx % breeds.value.length])
const price = +((base + fluct)).toFixed(2)
const delta = +((Math.random() * 0.6 - 0.3)).toFixed(2)
return {
date: fmt(d),
breed,
region: r,
price,
delta,
up: delta > 0,
id: `${props.selectedProvince}-${idx}`
}
})
}
const genData = (regionId = null) => {
// 根据筛选生成最近 N 天日期与价格数据(可按地区与重量微调)
const baseBase = basePriceMap[props.selectedProvince] ?? 6.8
const regionOffset = regionId ? (parseInt(regionId.split('-')[1]) || 0) * 0.03 : 0
const weightOffset = weight.value === '400' ? 0.15 : (weight.value === '500' ? 0.25 : 0)
const base = baseBase + regionOffset + weightOffset
const today = new Date()
const d = []
const p = []
const count = range.value
for (let i = count - 1; i >= 0; i--) {
const dt = new Date(today)
dt.setDate(today.getDate() - i)
d.push(`${dt.getMonth() + 1}-${String(dt.getDate()).padStart(2, '0')}`)
// 价格在基础价附近小幅波动
const fluct = (Math.sin(i) * 0.08) + (Math.random() * 0.12 - 0.06)
p.push(+((base + fluct)).toFixed(2))
}
days.value = d
priceSeries.value = p
}
const handleRowClick = (row) => {
selectedRow.value = row
genData(row?.id)
}
const yesterdayDelta = computed(() => {
if (!priceSeries.value.length || priceSeries.value.length < 2) return 0
const n = priceSeries.value.length
return +(priceSeries.value[n - 1] - priceSeries.value[n - 2]).toFixed(2)
})
const trendText = computed(() => {
const d = yesterdayDelta.value
if (Math.abs(d) < 0.01) return '平稳'
return d > 0 ? '上涨' : '下跌'
})
const trendSymbol = computed(() => {
const d = yesterdayDelta.value
if (Math.abs(d) < 0.01) return '-'
return d > 0 ? '↑' : '↓'
})
onMounted(() => {
genRegionRows()
genData()
})
return { unit, provinceName, breeds, todayPrice, max7d, min7d, avg7d, avgToday, days, priceSeries, chartOption, regionRows, selectedRow, handleRowClick, isUnknownRegion, range, weight, setRange, setWeight, yesterdayDelta, trendText, trendSymbol }
}
}
</script>
<template>
<div class="price-page">
<div class="price-header">
<h2><span :class="{ highlight: isUnknownRegion(provinceName) }">{{ provinceName }}</span> 牛价行情</h2>
<!-- <div class="unit">单位<span>{{ unit }}</span></div> -->
</div>
<div class="price-content">
<section class="left-info">
<div class="card region-card">
<div class="panel-header">
<div class="diamond-icon"></div>
<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 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>
</section>
<section class="right-stats">
<div class="card detail-card">
<div class="panel-header">
<div class="diamond-icon"></div>
<h3>
<span :class="{ highlight: isUnknownRegion(selectedRow ? selectedRow.region : provinceName) }">
{{ (selectedRow ? selectedRow.region : provinceName) }}
</span>
价格详情
</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>
</section>
</div>
</div>
</template>
<style scoped>
.price-page {
width: 100%;
height: calc(100vh - 80px);
padding: 16px 24px;
display: flex;
flex-direction: column;
gap: 16px;
background: #011819; /* 与预警监测页背景一致 */
position: relative;
}
.price-page::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background:
rgba(0, 212, 255, 0.05),
rgba(0, 212, 255, 0.03),
rgba(0, 212, 255, 0.02);
pointer-events: none;
}
.price-page > * { position: relative; z-index: 1; }
.price-header {
display: flex;
justify-content: center;
align-items: center;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(0,212,255,0.25);
padding: 10px 16px;
border-radius: 8px;
}
.price-header h2 {
font-size: 18px;
color: #eaf7ff;
}
.unit span {
color: #00ffcc;
}
.price-content {
display: grid;
grid-template-columns: 1.4fr 1.6fr; /* 加宽左侧列表区域 */
gap: 16px;
}
.card {
background: rgba(7, 59, 68, 0.15); /* 参考预警监测 .panel 背景 */
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 12px;
padding: 16px 18px;
backdrop-filter: blur(10px);
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: #00d4ff;
opacity: 0.6;
}
.card-title {
color: #bfe9ff;
font-size: 14px;
margin-bottom: 8px;
}
/* 统一标题为预警监测页风格 */
.panel-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
padding-bottom: 6px;
}
.panel-header h3 {
color: #ffffff;
font-size: 16px;
font-weight: 700;
margin: 0;
}
.highlight { color: #00d4ff; }
.diamond-icon {
width: 12px;
height: 12px;
background: #00d4ff;
transform: rotate(45deg);
box-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
.breed-list {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.breed-item {
background: rgba(0,212,255,0.12);
border: 1px solid rgba(0,212,255,0.35);
color: #eaf7ff;
font-size: 12px;
padding: 6px 10px;
border-radius: 16px;
}
.price-today .value {
color: #00ffcc;
font-size: 26px;
font-weight: bold;
margin-right: 8px;
}
.price-today .unit {
color: #cfefff;
}
.right-stats {
display: flex;
flex-direction: column;
gap: 16px;
}
.detail-card {
background: rgba(255,255,255,0.06);
border: 1px solid rgba(0,212,255,0.25);
border-radius: 8px;
padding: 10px 12px;
}
.detail-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.today-price-line {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 12px;
}
.today-price-line .labels { color: #cfefff; font-size: 14px; }
.today-price-line .price-display { display: flex; align-items: baseline; gap: 6px; }
.today-price-line .currency { color: #ff7a28; font-size: 22px; font-weight: 700; }
.today-price-line .value { color: #ff7a28; font-size: 32px; font-weight: 800; }
.today-price-line .unit { color: #ff7a28; font-size: 14px; font-weight: 600; }
.today-price-line .symbol { color: #cfefff; margin-left: 6px; }
.filters { display: flex; justify-content: space-between; align-items: center; }
.filter-btn {
background: rgba(255,255,255,0.06);
border: 1px solid rgba(0,212,255,0.35);
color: #eaf7ff;
font-size: 13px;
padding: 6px 12px;
border-radius: 20px;
cursor: pointer;
transition: all 0.2s ease;
margin-right: 8px;
}
.filter-btn.active { background: rgba(0,212,255,0.2); border-color: #00d4ff; color: #ffffff; }
.filter-btn:hover { background: rgba(0,212,255,0.12); }
.weight-group, .range-group { display: flex; align-items: center; }
.region-table {
display: flex;
flex-direction: column;
gap: 6px;
}
.table-header, .table-row {
display: grid;
grid-template-columns: 1fr 1.6fr 1.2fr 1fr 1fr; /* 时间略窄,地区更宽,价格与涨跌等宽 */
gap: 10px;
align-items: center;
}
.table-header {
padding: 8px 0;
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
color: #ffffff;
font-weight: 600;
}
.table-body {
display: flex;
flex-direction: column;
}
.table-row {
padding: 18px 0; /* 增加上下内边距,提高行高 */
min-height: 60px; /* 提高最小高度,列表更舒展 */
border-bottom: 1px solid rgba(255,255,255,0.08);
color: #eaf7ff;
cursor: pointer;
transition: background-color 0.2s ease, box-shadow 0.2s ease, border-left-color 0.2s ease;
}
.table-row:nth-child(even) { background: rgba(0, 212, 255, 0.05); }
.table-row:nth-child(odd) { background: rgba(255, 255, 255, 0.02); }
.table-row:hover {
background: rgba(0, 212, 255, 0.12);
border-left: 2px solid #00d4ff;
}
.table-row.active {
background: rgba(0, 212, 255, 0.22);
border-left: 3px solid #00d4ff;
box-shadow: inset 0 0 12px rgba(0, 212, 255, 0.35), 0 0 8px rgba(0, 212, 255, 0.25);
color: #ffffff;
}
.table-row.active .td {
font-weight: 600;
}
/* 涨跌颜色(左侧列表也复用) */
.change-up { color: #00e676; font-weight: 600; }
.change-down { color: #ff5252; font-weight: 600; }
.th { font-size: 14px; }
.td { font-size: 14px; }
/* 仅将左侧“省内各地区牛数据列表”模块文字居中 */
.region-card .card-title { text-align: center; }
.region-card .region-table { text-align: center; }
.region-card .th, .region-card .td { text-align: center; }
.td .main { color: #eaf7ff; }
.td .sub { color: #9ec7d9; font-size: 12px; margin-top: 2px; }
.change-zero { color: #b9cdd8; }
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.stat-item {
background: rgba(255,255,255,0.06);
border: 1px solid rgba(0,212,255,0.25);
border-radius: 8px;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.stat-item .label { color: #cfefff; font-size: 13px; }
.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; }
@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); }
}
@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; }
.stats-grid { grid-template-columns: 1fr; }
.th, .td { font-size: 13px; }
}
</style>