diff --git a/public/favicon.svg b/public/favicon.svg index 763e9c2..7b5eb65 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,4 +1,4 @@ - + @@ -13,11 +13,11 @@ - + - - + + - \ No newline at end of file + diff --git a/src/components/Home.vue b/src/components/Home.vue index 9dba168..c148944 100644 --- a/src/components/Home.vue +++ b/src/components/Home.vue @@ -11,19 +11,18 @@
序号
省份
+
地区
品种
-
单价(元/斤)
+
单价
+
时间
{{ idx + 1 }}
{{ row.province }}
+
{{ row.location }}
{{ row.breed }}
-
-
-
-
- {{ formatPrice(row.price) }} -
+
{{ formatPrice(row.price) }}
+
{{ formatDate(row.time) }}
@@ -237,6 +236,7 @@ padding: 10px; height: 250px; /* 设置固定高度 */ overflow-y: auto; + position: relative; } .species-price-table-header, .species-price-table-row { @@ -249,8 +249,13 @@ .species-price-table-header { color: #84acf0; border-bottom: 1px solid rgba(132, 172, 240, 0.2); - padding-bottom: 6px; + padding: 10px 0; font-weight: bold; + position: sticky; + top: 0; + z-index: 10; + background: #0C2435; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35); } .species-price-table-row { color: #eaf7ff; @@ -267,24 +272,47 @@ flex: 1; min-height: 0; overflow-y: auto; + position: relative; } .price-table-header, .price-table-row { display: grid; - grid-template-columns: 0.6fr 1fr 1.2fr 2fr; - gap: 12px; + grid-template-columns: 0.6fr 1fr 1.2fr 1.2fr 1.4fr 0.9fr; + gap: 14px; align-items: center; - font-size: 14px; + font-size: 15px; +} + +.price-table-header > div:nth-child(5), +.price-table-row > div:nth-child(5) { + padding-left: 12px; +} + +.price-table-header > div:nth-child(4), +.price-table-row > div:nth-child(4) { + padding-right: 8px; } .price-table-header { color: #84acf0; border-bottom: 1px solid rgba(132, 172, 240, 0.2); - padding-bottom: 6px; + padding: 8px 0; font-weight: bold; + position: sticky; + top: 0; + z-index: 10; + background: #0C2435; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35); } .price-table-row { color: #eaf7ff; } +/* 奇偶行颜色区分 */ +/* .price-table-row:nth-child(odd) { + background: rgba(255, 255, 255, 0.03); +} +.price-table-row:nth-child(even) { + background: rgba(0, 212, 255, 0.06); +} */ /* 单价列条形图样式 */ .price-bar-cell { display: flex; @@ -309,13 +337,13 @@ } .price-value { min-width: 86px; - text-align: right; + text-align: left; color: #eaf7ff; font-variant-numeric: tabular-nums; } .price-value.after-bar { min-width: 86px; - text-align: right; + text-align: left; color: #eaf7ff; font-weight: 600; } @@ -386,6 +414,15 @@ gap: 10px; margin-bottom: 10px; } +.price-ranking-panel .panel-header { + margin-bottom: 0; +} +.price-ranking-panel .price-table { + padding-top: 0; +} +.price-ranking-panel .species-price-table { + padding-top: 0; +} .panel-header h3 { color: #ffffff; @@ -478,7 +515,7 @@ export default { '新疆维吾尔自治区': '新疆', '西藏自治区': '西藏', '宁夏回族自治区': '宁夏', - '广西自治区': '广西', + '广西壮族自治区': '广西', '河北省': '河北', '山东省': '山东', '黑龙江省': '黑龙江', @@ -776,16 +813,16 @@ export default { // 全国牛单价排行榜表格数据 nationalPriceTableRows: [ - { id: 1, province: '河北省', breed: '安格斯牛', price: 14200 }, - { id: 2, province: '山东省', breed: '荷斯坦牛', price: 17000 }, - { id: 3, province: '江苏省', breed: '复洲黄牛', price: 14500 }, - { id: 4, province: '浙江省', breed: '西门塔尔牛', price: 16800 }, - { id: 5, province: '新疆维吾尔自治区', breed: '夏洛莱牛', price: 16500 }, - { id: 6, province: '甘肃省', breed: '水牛', price: 17387 }, - { id: 7, province: '广东省', breed: '安格斯牛', price: 14200 }, - { id: 8, province: '广西壮族自治区', breed: '荷斯坦牛', price: 17000 }, - { id: 9, province: '湖南省', breed: '复洲黄牛', price: 14500 }, - { id: 10, province: '河南省', breed: '西门塔尔牛', price: 16800 } + { id: 1, province: '河北省', location: '石家庄市', breed: '安格斯牛', price: 14200, time: '2025-12-03' }, + { id: 2, province: '山东省', location: '济南市', breed: '荷斯坦牛', price: 17000, time: '2025-12-03' }, + { id: 3, province: '江苏省', location: '南京市', breed: '复洲黄牛', price: 14500, time: '2025-12-03' }, + { id: 4, province: '浙江省', location: '杭州市', breed: '西门塔尔牛', price: 16800, time: '2025-12-03' }, + { id: 5, province: '新疆维吾尔自治区', location: '乌鲁木齐市', breed: '夏洛莱牛', price: 16500, time: '2025-12-03' }, + { id: 6, province: '甘肃省', location: '兰州市', breed: '水牛', price: 17387, time: '2025-12-03' }, + { id: 7, province: '广东省', location: '广州市', breed: '安格斯牛', price: 14200, time: '2025-12-03' }, + { id: 8, province: '广西壮族自治区', location: '南宁市', breed: '荷斯坦牛', price: 17000, time: '2025-12-03' }, + { id: 9, province: '湖南省', location: '长沙市', breed: '复洲黄牛', price: 14500, time: '2025-12-03' }, + { id: 10, province: '河南省', location: '郑州市', breed: '西门塔尔牛', price: 16800, time: '2025-12-03' } ], // 全国省份平均单价数据源(专用于右侧省份排行图表) @@ -846,10 +883,11 @@ export default { ], // 耳标统计数据 - earTagStats: { - completed: 45678, - planned: 52000 - }, + earTagStats: { + completed: 45678, + planned: 52000 + }, + _provinceDailyTimer: null, // 耳标佩戴统计堆叠柱状图配置 earTagChartOption: { @@ -1079,9 +1117,9 @@ export default { nameTextStyle: { color: '#00ffff', fontSize: 11 }, axisLine: { lineStyle: { color: '#00ffff' } }, axisLabel: { color: '#ffffff', fontSize: 10 }, - min: 12, - max: 15, - interval: 1, + min: 5, + max: 25, + interval: 5, splitLine: { lineStyle: { color: 'rgba(0,255,255,0.2)' } } }, series: [{ @@ -1147,6 +1185,19 @@ export default { return String(value || 0) } }, + formatDate(value) { + if (!value) { + const t = new Date() + const m = String(t.getMonth() + 1).padStart(2, '0') + const day = String(t.getDate()).padStart(2, '0') + return `${m}-${day}` + } + const d = new Date(value) + if (Number.isNaN(d.getTime())) return String(value) + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${m}-${day}` + }, // 计算条形宽度样式 getPriceBarStyle(price) { const max = this.priceBarMax || 1 @@ -1165,8 +1216,10 @@ export default { const rows = list.map((item, idx) => ({ id: item.id ?? idx + 1, province: item.province ?? item.provinceName ?? '', + location: item.location ?? '', breed: item.type ?? item.breed ?? '', - price: Number(item.price) + price: Number(item.price), + time: item.time ?? item.priceDate ?? item.date ?? '' })).filter(r => r.province && r.breed && Number.isFinite(r.price)) // 更新数据源(驱动左侧表格与右侧省份排行图表) this.nationalPriceTableRows = rows @@ -1197,19 +1250,29 @@ export default { // 拉取全国省份平均单价排行榜数据(字段:province/provincePrice) async fetchNationalProvinceAverages() { - const url = '/api/cattle-data/provinces' - try { - const res = await fetch(url) - const raw = await res.json() - const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : []) - const rows = list.map((item, idx) => ({ - id: item.id ?? idx + 1, - province: item.province ?? item.provinceName ?? '', - price: Number(item.provincePrice) - })).filter(r => r.province && Number.isFinite(r.price)) - this.nationalProvinceAverageRows = rows - } catch (e) { - console.warn('获取全国省份平均单价失败:', e) + const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` + const base = new Date() + let tryDate = fmt(base) + for (let i = 0; i < 7; i++) { + const url = `/api/cattle-data/province-daily-prices?priceDate=${encodeURIComponent(tryDate)}` + + try { + const res = await fetch(url) + const raw = await res.json() + const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : []) + const rows = list.map((item, idx) => ({ + id: item.id ?? idx + 1, + province: item.province ?? item.provinceName ?? '', + price: Number(item.price ?? item.provincePrice) + })).filter(r => r.province && Number.isFinite(r.price)) + if (rows.length > 0) { + this.nationalProvinceAverageRows = rows + break + } + } catch (e) {} + const d = new Date(tryDate) + d.setDate(d.getDate() - 1) + tryDate = fmt(d) } }, @@ -1563,6 +1626,25 @@ export default { this.fetchNationalPriceRanking() // 拉取全国省份平均单价排行榜数据 this.fetchNationalProvinceAverages() + if (this._provinceDailyTimer) clearTimeout(this._provinceDailyTimer) + const schedule = () => { + const now = new Date() + const next = new Date(now) + next.setHours(12, 0, 0, 0) + if (now.getTime() >= next.getTime()) next.setDate(next.getDate() + 1) + const delay = next.getTime() - now.getTime() + this._provinceDailyTimer = setTimeout(async () => { + await this.fetchNationalProvinceAverages() + schedule() + }, Math.max(1000, delay)) + } + schedule() + }, + beforeUnmount() { + if (this._provinceDailyTimer) { + clearTimeout(this._provinceDailyTimer) + this._provinceDailyTimer = null + } } } diff --git a/src/components/Map3D.vue b/src/components/Map3D.vue index 50eb33d..508262a 100644 --- a/src/components/Map3D.vue +++ b/src/components/Map3D.vue @@ -65,6 +65,29 @@ export default { { province: '四川', count: 811 }, ]); + const fetchCattleSourceTop3 = async () => { + try { + const url = '/api/cattle-data/provinces' + const res = await fetch(url) + const raw = await res.json() + const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : []) + const rows = list.map((item) => { + const province = item.province ?? item.provinceName ?? '' + // 接口字段单位为“万头”,无需再除以10000 + const invRaw = item.inventory25th ?? item.inventory_25 ?? item.inventory2025 ?? item.inventory + const inv = Number(invRaw) + return { province, inv } + }).filter(x => x.province && Number.isFinite(x.inv) && x.inv > 0) + if (rows.length === 0) return + rows.sort((a, b) => b.inv - a.inv) + const top3 = rows.slice(0, 3).map(x => ({ + province: x.province, + count: Math.round(x.inv) + })) + cattleSourceTop5.value = top3 + } catch (e) {} + } + // 重置 const resize = () => { baseEarth.resize(); @@ -713,6 +736,7 @@ export default { }; }; onMounted(async () => { + fetchCattleSourceTop3() console.log('=== Map3D组件已挂载 ==='); // console.log('farmData:', farmData); // 移除farmData引用 // 等待DOM完全渲染 diff --git a/src/utils/lodash/lodash.default.js b/src/utils/lodash/lodash.default.js index c61abf9..08a2926 100644 --- a/src/utils/lodash/lodash.default.js +++ b/src/utils/lodash/lodash.default.js @@ -544,7 +544,7 @@ baseForOwn(LazyWrapper.prototype, function(func, methodName) { var checkIteratee = /^(?:filter|find|map|reject)|While$/.test(methodName), isTaker = /^(?:head|last)$/.test(methodName), lodashFunc = lodash[isTaker ? ('take' + (methodName == 'last' ? 'Right' : '')) : methodName], - retUnwrapped = isTaker || /^find/.test(methodName); + retUnwrapped = isTaker || methodName.startsWith('find'); if (!lodashFunc) { return;