@@ -5,183 +5,21 @@
import streamlit as st
import numpy as np
import pandas as pd
import plotly . graph_objects as go
import plotly . express as px
from plotly . subplots import make_subplots
# ─── Page Config ────────────────────────────────────────────────────────────
st . set_page_config (
page_title = " 农业智能决策系统 " ,
page_title = " 种植决策助手 " ,
page_icon = " 🌾 " ,
layout = " wide " ,
initial_sidebar_state = " expanded " ,
)
# ─── Custom CSS ─ ─────────────────────────────────────────────────────────────
# ─── Minimal CSS ─────────────────────────────────────────────────────────────
st . markdown ( """
<style>
:root {
--bg-dark: #0a1628;
--bg-card: #0f2040;
--accent-green: #4ade80;
--accent-gold: #f59e0b;
--accent-blue: #38bdf8;
--text-primary: #e2e8f0;
--text-muted: #64748b;
--border: rgba(74, 222, 128, 0.2);
}
html, body, [class*= " css " ] {
font-family: " PingFang SC " , " Microsoft YaHei " , " Noto Serif SC " , serif;
background-color: var(--bg-dark);
color: var(--text-primary);
}
.stApp {
background: linear-gradient(135deg, #0a1628 0 % , #0d1f3c 50 % , #091520 100 % );
}
/* Sidebar */
[data-testid= " stSidebar " ] {
background: linear-gradient(180deg, #0f2040 0 % , #0a1628 100 % );
border-right: 1px solid var(--border);
}
[data-testid= " stSidebar " ] .stSlider label,
[data-testid= " stSidebar " ] .stNumberInput label,
[data-testid= " stSidebar " ] .stSelectbox label {
color: var(--accent-green) !important;
font-size: 0.82rem;
font-family: " JetBrains Mono " , " Fira Code " , " Source Code Pro " , monospace;
letter-spacing: 0.05em;
}
/* Metric cards */
.metric-card {
background: linear-gradient(135deg, #0f2040, #132b55);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px 24px;
text-align: center;
position: relative;
overflow: hidden;
}
.metric-card::before {
content: ' ' ;
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, var(--accent-green), var(--accent-blue));
}
.metric-value {
font-family: " JetBrains Mono " , " Fira Code " , " Source Code Pro " , monospace;
font-size: 2.2rem;
font-weight: 700;
color: var(--accent-green);
line-height: 1;
}
.metric-unit {
font-size: 0.85rem;
color: var(--text-muted);
margin-top: 4px;
}
.metric-label {
font-size: 0.9rem;
color: var(--text-primary);
margin-top: 8px;
}
/* Section headers */
.section-header {
font-size: 0.75rem;
font-family: " JetBrains Mono " , " Fira Code " , " Source Code Pro " , monospace;
letter-spacing: 0.15em;
color: var(--accent-gold);
text-transform: uppercase;
border-bottom: 1px solid rgba(245,158,11,0.3);
padding-bottom: 6px;
margin-bottom: 16px;
margin-top: 24px;
}
/* Crop badge */
.crop-badge {
display: inline-block;
background: linear-gradient(135deg, rgba(74,222,128,0.15), rgba(56,189,248,0.15));
border: 1px solid var(--accent-green);
border-radius: 6px;
padding: 4px 12px;
font-family: " JetBrains Mono " , " Fira Code " , " Source Code Pro " , monospace;
font-size: 0.78rem;
color: var(--accent-green);
margin: 3px;
}
/* Recommendation card */
.rec-card {
background: linear-gradient(135deg, rgba(74,222,128,0.08), rgba(56,189,248,0.05));
border: 1px solid rgba(74,222,128,0.3);
border-radius: 12px;
padding: 18px 22px;
margin: 10px 0;
}
.rec-rank {
font-family: " JetBrains Mono " , " Fira Code " , " Source Code Pro " , monospace;
font-size: 1.5rem;
color: var(--accent-gold);
font-weight: 700;
}
.rec-crop {
font-size: 1.1rem;
color: var(--text-primary);
font-weight: 600;
}
.rec-score {
font-family: " JetBrains Mono " , " Fira Code " , " Source Code Pro " , monospace;
font-size: 0.85rem;
color: var(--accent-blue);
}
/* Hero title */
.hero-title {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, var(--accent-green), var(--accent-blue));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1.2;
}
.hero-sub {
font-family: " JetBrains Mono " , " Fira Code " , " Source Code Pro " , monospace;
font-size: 0.8rem;
color: var(--text-muted);
letter-spacing: 0.1em;
margin-top: 4px;
}
/* Alert boxes */
.alert-good {
background: rgba(74,222,128,0.1);
border-left: 3px solid var(--accent-green);
border-radius: 0 8px 8px 0;
padding: 12px 16px;
margin: 8px 0;
font-size: 0.9rem;
}
.alert-warn {
background: rgba(245,158,11,0.1);
border-left: 3px solid var(--accent-gold);
border-radius: 0 8px 8px 0;
padding: 12px 16px;
margin: 8px 0;
font-size: 0.9rem;
}
/* Override streamlit slider colors */
.stSlider [data-baseweb= " slider " ] [data-testid= " stTickBarMin " ],
.stSlider [data-baseweb= " slider " ] [data-testid= " stTickBarMax " ] {
color: var(--text-muted);
font-family: " PingFang SC " , " Microsoft YaHei " , " Noto Sans SC " , sans- serif;
}
</style>
""" , unsafe_allow_html = True )
@@ -194,42 +32,42 @@ CROPS = {
" optimal " : { " ph " : ( 6.0 , 7.0 ) , " N " : ( 80 , 120 ) , " P " : ( 30 , 60 ) , " K " : ( 40 , 80 ) ,
" rainfall " : ( 150 , 250 ) , " temp " : ( 22 , 30 ) } ,
" base_yield " : 7500 , # kg/ha
" color " : " #4ade80 "
" color " : " #4a7c59 "
} ,
" 小麦 " : {
" emoji " : " 🌿 " ,
" optimal " : { " ph " : ( 6.0 , 7.5 ) , " N " : ( 60 , 100 ) , " P " : ( 20 , 50 ) , " K " : ( 30 , 60 ) ,
" rainfall " : ( 60 , 120 ) , " temp " : ( 15 , 22 ) } ,
" base_yield " : 6000 ,
" color " : " #f59e0b "
" color " : " #c69c5d "
} ,
" 玉米 " : {
" emoji " : " 🌽 " ,
" optimal " : { " ph " : ( 5.8 , 7.0 ) , " N " : ( 100 , 150 ) , " P " : ( 40 , 70 ) , " K " : ( 60 , 100 ) ,
" rainfall " : ( 100 , 180 ) , " temp " : ( 20 , 28 ) } ,
" base_yield " : 8500 ,
" color " : " #fbbf24 "
" color " : " #e8a93f "
} ,
" 大豆 " : {
" emoji " : " 🫘 " ,
" optimal " : { " ph " : ( 6.0 , 7.0 ) , " N " : ( 20 , 50 ) , " P " : ( 30 , 60 ) , " K " : ( 40 , 80 ) ,
" rainfall " : ( 80 , 150 ) , " temp " : ( 18 , 26 ) } ,
" base_yield " : 3500 ,
" color " : " #a78bfa "
" color " : " #8b7cb3 "
} ,
" 油菜 " : {
" emoji " : " 🌻 " ,
" optimal " : { " ph " : ( 6.0 , 7.5 ) , " N " : ( 80 , 130 ) , " P " : ( 30 , 60 ) , " K " : ( 50 , 90 ) ,
" rainfall " : ( 80 , 130 ) , " temp " : ( 15 , 20 ) } ,
" base_yield " : 3000 ,
" color " : " #f 9731 6 "
" color " : " #d 978 36 "
} ,
" 棉花 " : {
" emoji " : " ☁️ " ,
" optimal " : { " ph " : ( 6.0 , 8.0 ) , " N " : ( 60 , 100 ) , " P " : ( 20 , 45 ) , " K " : ( 40 , 70 ) ,
" rainfall " : ( 70 , 120 ) , " temp " : ( 25 , 32 ) } ,
" base_yield " : 4500 ,
" color " : " #e2e8f0 "
" color " : " #5a6b7c "
} ,
}
@@ -294,11 +132,11 @@ def rank_crops(ph, N, P, K, rainfall, temp, pesticide, area):
# ─── Sidebar Inputs ──────────────────────────────────────────────────────────
with st . sidebar :
st . markdown ( ' <div class= " hero-title " >🌾 农业决策</div> ' , unsafe_allow_html = True )
st . markdown ( ' <div class= " hero-sub " >SMART FARMING SYSTEM v2.0</div> ' , unsafe_allow_html = True )
st . markdown ( " --- " )
st . header ( " 🌾 种植决策助手 " )
st . caption ( " 根据土壤和气候,推荐适宜作物 " )
st . divider ( )
st . markdown ( ' <div class= " section-header " >🧪 土壤参数</div> ' , unsafe_allow_html = True )
st . subheader ( " 🧪 土壤参数 " )
col1 , col2 = st . columns ( 2 )
with col1 :
ph = st . slider ( " pH 值 " , 4.0 , 9.0 , 6.5 , 0.1 )
@@ -307,18 +145,18 @@ with st.sidebar:
P = st . slider ( " 磷 P (mg/kg) " , 0 , 100 , 45 , 5 )
K = st . slider ( " 钾 K (mg/kg) " , 0 , 150 , 60 , 5 )
st . markdown ( ' <div class= " section-header " >🌦 气象数据</div> ' , unsafe_allow_html = True )
st . subheader ( " 🌦 气象数据 " )
col3 , col4 = st . columns ( 2 )
with col3 :
rainfall = st . slider ( " 降雨量 (mm/月) " , 0 , 400 , 120 , 10 )
with col4 :
temp = st . slider ( " 温度 (°C) " , 0 , 45 , 22 , 1 )
st . markdown ( ' <div class= " section-header " >🌱 种植参数</div> ' , unsafe_allow_html = True )
st . subheader ( " 🌱 种植参数 " )
area = st . number_input ( " 种植面积 (公顷) " , 0.1 , 10000.0 , 100.0 , 10.0 )
pesticide = st . slider ( " 农药用量 (kg/ha) " , 0 , 200 , 50 , 5 )
st . markdown ( ' <div class= " section-header " >🎯 目标作物</div> ' , unsafe_allow_html = True )
st . subheader ( " 🎯 目标作物 " )
selected_crop = st . selectbox (
" 选择分析作物 " ,
list ( CROPS . keys ( ) ) ,
@@ -333,54 +171,22 @@ best_crop = rankings[0]
# ─── Main Layout ─────────────────────────────────────────────────────────────
st . markdown ( f """
<div style= " display:flex; align-items:baseline; gap:16px; margin-bottom:4px; " >
<div class= " hero-title " >农业智能决策系统</div>
</div>
<div class= " hero-sub " >YIELD = f(SOIL · WEATHER · PESTICIDE) | 基于多因子 Cobb-Douglas 产量模型</div>
""" , unsafe_allow_html = True )
st . markdown ( " <br> " , unsafe_allow_html = True )
st . title ( " 🌾 种植决策助手 " )
st . caption ( " 输入土壤与气象条件,获得作物产量预测与种植建议 " )
# KPI row
overall = np . mean ( list ( factors . values ( ) ) )
k1 , k2 , k3 , k4 = st . columns ( 4 )
with k1 :
st . markdown ( f """
<div class= " metric-card " >
<div class= " metric-value " > { yph : , .0f} </div>
<div class= " metric-unit " >kg / 公顷</div>
<div class= " metric-label " > { CROPS [ selected_crop ] [ ' emoji ' ] } { selected_crop } 单产</div>
</div> """ , unsafe_allow_html = True )
with k2 :
st . markdown ( f """
<div class= " metric-card " >
<div class= " metric-value " > { ytotal / 1000 : ,.1f } </div>
<div class= " metric-unit " >吨 / 总产量</div>
<div class= " metric-label " >📦 { area : .0f } 公顷总产</div>
</div> """ , unsafe_allow_html = True )
with k3 :
overall = np . mean ( list ( factors . values ( ) ) )
st . markdown ( f """
<div class= " metric-card " >
<div class= " metric-value " > { overall * 100 : .1f } %</div>
<div class= " metric-unit " >综合适宜度</div>
<div class= " metric-label " >🎯 环境匹配指数</div>
</div> """ , unsafe_allow_html = True )
with k4 :
st . markdown ( f """
<div class= " metric-card " >
<div class= " metric-value " > { best_crop [ ' emoji ' ] } </div>
<div class= " metric-unit " > { best_crop [ ' crop ' ] } ( { best_crop [ ' score ' ] * 100 : .0f } %)</div>
<div class= " metric-label " >🏆 最优推荐作物</div>
</div> """ , unsafe_allow_html = True )
st . markdown ( " <br> " , unsafe_allow_html = True )
k1. metric ( f " { CROPS [ selected_crop ] [ ' emoji ' ] } { selected_crop } 单产 " , f " { yph : ,.0f } kg/ha " )
k2 . metric ( f " 📦 { area : .0f } 公顷总产 " , f " { ytotal / 1000 : ,.1f } 吨 " )
k3 . metric ( " 🎯 环境匹配指数 " , f " { overall * 100 : .1f } % " )
k4 . metric ( " 🏆 最优推荐作物 " , f " { best_crop [ ' emoji ' ] } { best_crop [ ' crop ' ] } " , f " 匹配度 { best_crop [ ' score ' ] * 100 : .0f } % " )
# ─── Charts Row ──────────────────────────────────────────────────────────────
col_left , col_right = st . columns ( [ 3 , 2 ] )
with col_left :
st . markdown ( ' <div class= " section-header " >📊 影响因子雷达图</div> ' , unsafe_allow_html = True )
st . subheader ( " 📊 影响因子雷达图 " )
factor_names = list ( factors . keys ( ) )
factor_vals = [ round ( v * 100 , 1 ) for v in factors . values ( ) ]
@@ -392,14 +198,14 @@ with col_left:
r = factor_vals_closed ,
theta = factor_names_closed ,
fill = ' toself ' ,
fillcolor = ' rgba(74,222,128, 0.15) ' ,
line = dict ( color = ' #4ade80 ' , width = 2 ) ,
fillcolor = ' rgba(74, 124, 89, 0.15) ' ,
line = dict ( color = ' #4a7c59 ' , width = 2 ) ,
name = selected_crop ,
) )
fig_radar . add_trace ( go . Scatterpolar (
r = [ 100 ] * len ( factor_names_closed ) ,
theta = factor_names_closed ,
line = dict ( color = ' rgba(255,255,255 ,0.1) ' , width = 1 , dash = ' dot ' ) ,
line = dict ( color = ' rgba(0,0,0 ,0.15 ) ' , width = 1 , dash = ' dot ' ) ,
mode = ' lines ' ,
name = ' 理想值 ' ,
) )
@@ -407,14 +213,14 @@ with col_left:
polar = dict (
bgcolor = ' rgba(0,0,0,0) ' ,
radialaxis = dict ( range = [ 0 , 100 ] , showticklabels = True ,
tickfont = dict ( color = ' #64748b ' , size = 9 ) ,
gridcolor = ' rgba(255,255,255 ,0.06 ) ' ) ,
angularaxis = dict ( tickfont = dict ( color = ' #e2e8f0 ' , size = 11 ) ,
gridcolor = ' rgba(255,255,255 ,0.08 ) ' ) ,
tickfont = dict ( color = ' #5a5a5a ' , size = 9 ) ,
gridcolor = ' rgba(0,0,0 ,0.08 ) ' ) ,
angularaxis = dict ( tickfont = dict ( color = ' #2c2c2c ' , size = 11 ) ,
gridcolor = ' rgba(0,0,0 ,0.1 ) ' ) ,
) ,
paper_bgcolor = ' rgba(0,0,0,0) ' ,
plot_bgcolor = ' rgba(0,0,0,0) ' ,
font = dict ( color = ' #e2e8f0 ' ) ,
font = dict ( color = ' #2c2c2c ' ) ,
legend = dict ( orientation = ' h ' , y = - 0.12 , font = dict ( size = 10 ) ) ,
margin = dict ( t = 20 , b = 40 , l = 40 , r = 40 ) ,
height = 320 ,
@@ -422,30 +228,17 @@ with col_left:
st . plotly_chart ( fig_radar , use_container_width = True )
with col_right :
st . markdown ( ' <div class= " section-header " >🏅 作物推荐排行</div> ' , unsafe_allow_html = True )
st . subheader ( " 🏅 作物推荐排行 " )
for i , r in enumerate ( rankings [ : 4 ] ) :
rank_icons = [ " 🥇 " , " 🥈 " , " 🥉 " , " 4️ ⃣ " ]
bar_ wid th = int ( r [ ' score ' ] * 100 )
bar_color = r [ ' color ' ]
st . markdown ( f """
<div class= " rec-card " style= " margin-bottom:8px; " >
<div style= " display:flex; justify-content:space-between; align-items:center; " >
<div>
<span class= " rec-rank " > { rank_icons [ i ] } </span>
<span class= " rec-crop " style= " margin-left:8px; " > { r [ ' emoji ' ] } { r [ ' crop ' ] } </span>
</div>
<span class= " rec-score " > { r [ ' score ' ] * 100 : .1f } %</span>
</div>
<div style= " background:rgba(255,255,255,0.06); border-radius:4px; height:4px; margin-top:8px; overflow:hidden; " >
<div style= " width: { bar_width } %; height:100%; background: { bar_color } ; border-radius:4px; " ></div>
</div>
<div style= " font-size:0.78rem; color:#64748b; margin-top:4px; font-family: ' JetBrains Mono ' ,monospace; " >
{ r [ ' yield_ha ' ] : ,.0f } kg/ha · 总产 { r [ ' total_yield ' ] / 1000 : ,.1f } 吨
</div>
</div> """ , unsafe_allow_html = True )
with st . container ( border = True ) :
c1 , c2 = st . columns ( [ 3 , 1 ] )
c1 . markdown ( f " ** { rank_icons [ i ] } { r [ ' emoji ' ] } { r [ ' crop ' ] } ** " )
c2 . markdown ( f " <div style=' text-align:right; color:#4a7c59; font-weight:600; ' > { r [ ' score ' ] * 100 : .1f } %</div> " , unsafe_allow_html = True )
st . progress ( int ( r [ ' score ' ] * 100 ) , text = f " { r [ ' yield_ha ' ] : ,.0f } kg/ha · 总产 { r [ ' total_yield ' ] / 1000 : ,.1f } 吨 " )
# ─── Sensitivity Analysis ─────────────────────────────────────────────────────
st . markdown ( ' <div class= " section-header " >📈 产量敏感性分析</div> ' , unsafe_allow_html = True )
st . subheader ( " 📈 产量敏感性分析 " )
sa_col1 , sa_col2 = st . columns ( 2 )
@@ -456,18 +249,18 @@ with sa_col1:
fig_N = go . Figure ( )
fig_N . add_trace ( go . Scatter (
x = N_range , y = y_N ,
mode = ' lines ' , line = dict ( color = ' #4ade80 ' , width = 2.5 ) ,
fill = ' tozeroy ' , fillcolor = ' rgba(74,222,128, 0.08) ' ,
mode = ' lines ' , line = dict ( color = ' #4a7c59 ' , width = 2.5 ) ,
fill = ' tozeroy ' , fillcolor = ' rgba(74, 124, 89, 0.08) ' ,
name = ' 产量 '
) )
fig_N . add_vline ( x = N , line = dict ( color = ' #f59e0b ' , width = 1.5 , dash = ' dot ' ) ,
annotation_text = f " 当前 { N } " , annotation_font_color = ' #f59e0b ' )
fig_N . add_vline ( x = N , line = dict ( color = ' #d4a574 ' , width = 1.5 , dash = ' dot ' ) ,
annotation_text = f " 当前 { N } " , annotation_font_color = ' #7c5e42 ' )
fig_N . update_layout (
title = dict ( text = " 氮肥用量 vs 产量 " , font = dict ( color = ' #94a3b8 ' , size = 12 ) ) ,
xaxis = dict ( title = " 氮 N (mg/kg) " , color = ' #64748b ' , gridcolor = ' rgba(255,255,255 ,0.05 ) ' ) ,
yaxis = dict ( title = " 产量 (kg/ha) " , color = ' #64748b ' , gridcolor = ' rgba(255,255,255 ,0.05 ) ' ) ,
title = dict ( text = " 氮肥用量 vs 产量 " , font = dict ( color = ' #5a5a5a ' , size = 12 ) ) ,
xaxis = dict ( title = " 氮 N (mg/kg) " , color = ' #5a5a5a ' , gridcolor = ' rgba(0,0,0 ,0.06 ) ' ) ,
yaxis = dict ( title = " 产量 (kg/ha) " , color = ' #5a5a5a ' , gridcolor = ' rgba(0,0,0 ,0.06 ) ' ) ,
paper_bgcolor = ' rgba(0,0,0,0) ' , plot_bgcolor = ' rgba(0,0,0,0) ' ,
font = dict ( color = ' #e2e8f0 ' , size = 10 ) ,
font = dict ( color = ' #2c2c2c ' , size = 10 ) ,
margin = dict ( t = 36 , b = 36 , l = 50 , r = 20 ) , height = 220 ,
showlegend = False ,
)
@@ -480,25 +273,25 @@ with sa_col2:
fig_R = go . Figure ( )
fig_R . add_trace ( go . Scatter (
x = rain_range , y = y_rain ,
mode = ' lines ' , line = dict ( color = ' #38bdf8 ' , width = 2.5 ) ,
fill = ' tozeroy ' , fillcolor = ' rgba(56,189,24 8,0.08) ' ,
mode = ' lines ' , line = dict ( color = ' #5a8f9e ' , width = 2.5 ) ,
fill = ' tozeroy ' , fillcolor = ' rgba(90, 143, 15 8, 0.08) ' ,
name = ' 产量 '
) )
fig_R . add_vline ( x = rainfall , line = dict ( color = ' #f59e0b ' , width = 1.5 , dash = ' dot ' ) ,
annotation_text = f " 当前 { rainfall } mm " , annotation_font_color = ' #f59e0b ' )
fig_R . add_vline ( x = rainfall , line = dict ( color = ' #d4a574 ' , width = 1.5 , dash = ' dot ' ) ,
annotation_text = f " 当前 { rainfall } mm " , annotation_font_color = ' #7c5e42 ' )
fig_R . update_layout (
title = dict ( text = " 月降雨量 vs 产量 " , font = dict ( color = ' #94a3b8 ' , size = 12 ) ) ,
xaxis = dict ( title = " 降雨量 (mm/月) " , color = ' #64748b ' , gridcolor = ' rgba(255,255,255 ,0.05 ) ' ) ,
yaxis = dict ( title = " 产量 (kg/ha) " , color = ' #64748b ' , gridcolor = ' rgba(255,255,255 ,0.05 ) ' ) ,
title = dict ( text = " 月降雨量 vs 产量 " , font = dict ( color = ' #5a5a5a ' , size = 12 ) ) ,
xaxis = dict ( title = " 降雨量 (mm/月) " , color = ' #5a5a5a ' , gridcolor = ' rgba(0,0,0 ,0.06 ) ' ) ,
yaxis = dict ( title = " 产量 (kg/ha) " , color = ' #5a5a5a ' , gridcolor = ' rgba(0,0,0 ,0.06 ) ' ) ,
paper_bgcolor = ' rgba(0,0,0,0) ' , plot_bgcolor = ' rgba(0,0,0,0) ' ,
font = dict ( color = ' #e2e8f0 ' , size = 10 ) ,
font = dict ( color = ' #2c2c2c ' , size = 10 ) ,
margin = dict ( t = 36 , b = 36 , l = 50 , r = 20 ) , height = 220 ,
showlegend = False ,
)
st . plotly_chart ( fig_R , use_container_width = True )
# ─── All Crops Comparison Bar Chart ───────────────────────────────────────────
st . markdown ( ' <div class= " section-header " >🌐 全作物产量对比</div> ' , unsafe_allow_html = True )
st . subheader ( " 🌐 全作物产量对比 " )
crop_names = [ f " { r [ ' emoji ' ] } { r [ ' crop ' ] } " for r in rankings ]
crop_yields = [ r [ ' yield_ha ' ] for r in rankings ]
@@ -508,78 +301,58 @@ fig_bar = go.Figure()
fig_bar . add_trace ( go . Bar (
x = crop_names , y = crop_yields ,
marker = dict ( color = crop_colors , opacity = 0.85 ,
line = dict ( color = ' rgba(255,255,255 ,0.2 ) ' , width = 1 ) ) ,
line = dict ( color = ' rgba(0,0,0 ,0.08 ) ' , width = 1 ) ) ,
text = [ f " { y : ,.0f } " for y in crop_yields ] ,
textposition = ' outside ' ,
textfont = dict ( color = ' #94a3b8 ' , size = 10 , family = ' JetBrains Mono ' ),
textfont = dict ( color = ' #5a5a5a ' , size = 10 ) ,
) )
fig_bar . update_layout (
xaxis = dict ( color = ' #64748b ' , gridcolor = ' rgba(255,255,255 ,0.04) ' ) ,
yaxis = dict ( title = " 预期产量 (kg/ha) " , color = ' #64748b ' , gridcolor = ' rgba(255,255,255 ,0.05 ) ' ) ,
xaxis = dict ( color = ' #5a5a5a ' , gridcolor = ' rgba(0,0,0 ,0.04) ' ) ,
yaxis = dict ( title = " 预期产量 (kg/ha) " , color = ' #5a5a5a ' , gridcolor = ' rgba(0,0,0 ,0.06 ) ' ) ,
paper_bgcolor = ' rgba(0,0,0,0) ' , plot_bgcolor = ' rgba(0,0,0,0) ' ,
font = dict ( color = ' #e2e8f0 ' , size = 11 ) ,
margin = dict ( t = 20 , b = 30 , l = 60 , r = 20 ) , height = 24 0 ,
font = dict ( color = ' #2c2c2c ' , size = 11 ) ,
margin = dict ( t = 20 , b = 30 , l = 60 , r = 20 ) , height = 26 0 ,
showlegend = False ,
)
st . plotly_chart ( fig_bar , use_container_width = True )
# ─── Advisory Panel ───────────────────────────────────────────────────────────
st . markdown ( ' <div class= " section-header " >💡 智能建议</div> ' , unsafe_allow_html = True )
st . subheader ( " 💡 种植建议 " )
adv1 , adv2 = st . columns ( 2 )
with adv1 :
crop_opt = CROPS [ selected_crop ] [ " optimal " ]
advisories = [ ]
if not ( crop_opt [ " ph " ] [ 0 ] < = ph < = crop_opt [ " ph " ] [ 1 ] ) :
advisories . append ( ( " warn " , f " pH { ph } 偏离 { selected_crop } 适宜范围 { crop_opt [ ' ph ' ] } ,建议 { ' 施石灰 ' if ph < crop_opt [ ' ph ' ] [ 0 ] else ' 施硫磺 ' } 调节 " ) )
st . warning ( f " pH { ph } 偏离 { selected_crop } 适宜范围 { crop_opt [ ' ph ' ] } ,建议 { ' 施石灰 ' if ph < crop_opt [ ' ph ' ] [ 0 ] else ' 施硫磺 ' } 调节 " )
else :
advisories . append ( ( " good " , f " 土壤 pH { ph } 处于 { selected_crop } 适宜范围内 ✓ " ) )
st . success ( f " 土壤 pH { ph } 处于 { selected_crop } 适宜范围内 " )
if N < crop_opt [ " N " ] [ 0 ] :
advisories . append ( ( " warn " , f " 氮肥不足( { N } vs 建议 { crop_opt [ ' N ' ] [ 0 ] } - { crop_opt [ ' N ' ] [ 1 ] } mg/kg) , 建议追施尿素 " ) )
st . warning ( f " 氮肥不足( { N } vs 建议 { crop_opt [ ' N ' ] [ 0 ] } - { crop_opt [ ' N ' ] [ 1 ] } mg/kg) , 建议追施尿素 " )
elif N > crop_opt [ " N " ] [ 1 ] :
advisories . append ( ( " warn " , f " 氮肥过量( { N } mg/kg) , 可能造成徒长, 建议减施 " ) )
st . warning ( f " 氮肥过量( { N } mg/kg) , 可能造成徒长, 建议减施 " )
else :
advisories . append ( ( " good " , f " 氮肥水平 { N } mg/kg 适宜 ✓ " ) )
st . success ( f " 氮肥水平 { N } mg/kg 适宜 " )
if rainfall < crop_opt [ " rainfall " ] [ 0 ] :
advisories . append ( ( " warn " , f " 降雨量不足,建议增加灌溉(缺水 { crop_opt [ ' rainfall ' ] [ 0 ] - rainfall } mm) " ) )
st . warning ( f " 降雨量不足,建议增加灌溉(缺水 { crop_opt [ ' rainfall ' ] [ 0 ] - rainfall } mm) " )
elif rainfall > crop_opt [ " rainfall " ] [ 1 ] :
advisories . append ( ( " warn " , f " 降雨量偏多,注意防涝排水 " ) )
st . warning ( f " 降雨量偏多,注意防涝排水 " )
else :
advisories . append ( ( " good " , f " 降雨量 { rainfall } mm 适合 { selected_crop } 生长 ✓ " ) )
for typ , msg in advisories :
css_class = " alert-good " if typ == " good " else " alert-warn "
st . markdown ( f ' <div class= " { css_class } " > { msg } </div> ' , unsafe_allow_html = True )
st . success ( f " 降雨量 { rainfall } mm 适合 { selected_crop } 生长 " )
with adv2 :
st . markdown ( f """
<div class= " rec-card " >
<div style= " font-size:0.82rem; color:#64748b; font-family: ' JetBrains Mono ' ,monospace; margin-bottom:12px; " >
当前环境参数下适宜种植:
</div>
""" , unsafe_allow_html = True )
badges = " " . join ( [
f ' <span class= " crop-badge " > { r [ " emoji " ] } { r [ " crop " ] } { r [ " score " ] * 100 : .0f } %</span> '
for r in rankings if r [ ' score ' ] > 0.6
] )
st . markdown ( f ' { badges } </div> ' , unsafe_allow_html = True )
suitable = [ r for r in rankings if r [ ' score ' ] > 0.6 ]
if suitable :
st . info ( " 当前环境参数下适宜种植: " + " 、 " . join ( [ f " { r [ ' emoji ' ] } { r [ ' crop ' ] } ( { r [ ' score ' ] * 100 : .0f } %) " for r in suitable ] ) )
else :
st . info ( " 当前环境参数下暂无特别适宜的作物 " )
st . markdown ( f """
<div style= " margin-top:16px; font-size:0.85rem; color:#94a3b8; line-height:1.7; " >
<b style= " color:#4ade80; " >最优方案:</b> { best_crop [ ' emoji ' ] } { best_crop [ ' crop ' ] } <br>
预期单产:<span style= " font-family: ' JetBrains Mono ' ,monospace; color:#38bdf8; " > { best_crop [ ' yield_ha ' ] : ,.0 f } kg/ha</span><br>
{ area : .0f } 公顷总产:<span style= " font-family: ' JetBrains Mono ' ,monospace; color:#38bdf8; " > { best_crop [ ' total_yield ' ] / 1000 : ,.1f } 吨</span>
</div>
""" , unsafe_allow_html = True )
with st . container ( border = True ) :
st . markdown ( f " **最优方案: { best_crop [ ' emoji ' ] } { best_crop [ ' crop ' ] } ** " )
st . markdown ( f " - 预期单产:** { best_crop [ ' yield_ha ' ] : ,.0f } kg/ha** " )
st . markdown ( f " - { area : .0f } 公顷总产:** { best_crop [ ' total_ yield' ] / 1000 : ,.1 f } 吨** " )
# ─── Footer ───────────────────────────────────────────────────────────────────
st . markdow n( " <br> " , unsafe_allow_html = True )
st . markdown ( """
<div style= " text-align:center; font-family: ' JetBrains Mono ' ,monospace; font-size:0.72rem;
color:#334155; padding:16px; border-top:1px solid rgba(74,222,128,0.1); " >
YIELD = f(Soil, Weather, Pesticide) | Cobb-Douglas Multi-Factor Model | 农业智能决策系统
</div>
""" , unsafe_allow_html = True )
st . divider ( )
st . captio n( " 种植决策助手 · 基于 Cobb-Douglas 多因子产量模型 · 仅供参考 " )