feat: 新增Rust项目基础框架和核心功能模块
- 添加Cargo.toml配置文件,定义项目元信息和依赖项 - 实现配置管理模块(ConfigManager),支持JSON配置读写 - 添加爬虫模块(SpiderManager),支持网页内容抓取和解析 - 实现数据库模块(DatabaseManager),使用SQLite存储评论数据 - 添加LLM分析模块(LLMAnalyzer),支持调用AI接口进行情绪分析 - 实现UI界面模块,包含指标显示和波形图绘制功能 - 添加项目文档和截图资源
This commit is contained in:
125
rust/src/ui.rs
Normal file
125
rust/src/ui.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use egui::{Color32, RichText, Stroke, Vec2};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct AppState {
|
||||
pub running: Arc<AtomicBool>,
|
||||
pub current_score: Arc<AtomicI32>,
|
||||
pub status_text: Arc<parking_lot::Mutex<String>>,
|
||||
pub fetch_count: Arc<AtomicI32>,
|
||||
pub analysis_count: Arc<AtomicI32>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
running: Arc::new(AtomicBool::new(false)),
|
||||
current_score: Arc::new(AtomicI32::new(50)),
|
||||
status_text: Arc::new(parking_lot::Mutex::new("就绪".to_string())),
|
||||
fetch_count: Arc::new(AtomicI32::new(0)),
|
||||
analysis_count: Arc::new(AtomicI32::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_score_color(score: i32) -> Color32 {
|
||||
match score {
|
||||
0..=30 => Color32::from_rgb(0, 100, 200),
|
||||
31..=50 => Color32::from_rgb(100, 200, 100),
|
||||
51..=70 => Color32::from_rgb(255, 200, 0),
|
||||
71..=100 => Color32::from_rgb(255, 50, 50),
|
||||
_ => Color32::GRAY,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_score_label(score: i32, cold: i32, warm: i32) -> &'static str {
|
||||
if score < cold {
|
||||
"看跌"
|
||||
} else if score > warm {
|
||||
"看涨"
|
||||
} else {
|
||||
"中性"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_indicator(ui: &mut egui::Ui, score: i32, cold: i32, warm: i32) {
|
||||
let color = get_score_color(score);
|
||||
let label = get_score_label(score, cold, warm);
|
||||
let size = Vec2::new(120.0, 120.0);
|
||||
|
||||
let (rect, _response) = ui.allocate_exact_size(size, egui::Sense::hover());
|
||||
|
||||
let painter = ui.painter();
|
||||
|
||||
painter.circle_filled(rect.center(), 55.0, Color32::from_rgba_unmultiplied(30, 30, 30, 200));
|
||||
painter.circle_stroke(rect.center(), 55.0, Stroke::new(3.0, color));
|
||||
|
||||
let inner_radius = 40.0;
|
||||
let angle = (score as f32 / 100.0) * std::f32::consts::TAU - std::f32::consts::FRAC_PI_2;
|
||||
let indicator_pos = rect.center() + Vec2::new(
|
||||
angle.cos() * inner_radius,
|
||||
angle.sin() * inner_radius,
|
||||
);
|
||||
painter.circle_filled(indicator_pos, 8.0, color);
|
||||
|
||||
let text = RichText::new(format!("{}", score))
|
||||
.heading()
|
||||
.size(36.0)
|
||||
.color(color);
|
||||
painter.text(
|
||||
rect.center() - Vec2::new(0.0, 10.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
text,
|
||||
);
|
||||
|
||||
let label_text = RichText::new(label).size(16.0).color(Color32::WHITE);
|
||||
painter.text(
|
||||
rect.center() + Vec2::new(0.0, 25.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
label_text,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn draw_waveform(ui: &mut egui::Ui, data: &[(String, f64)], width: f32, height: f32) {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
let painter = ui.painter();
|
||||
|
||||
painter.rect_filled(rect, 0.0, Color32::from_rgba_unmultiplied(20, 20, 30, 180));
|
||||
painter.rect_stroke(rect, 0.0, Stroke::new(1.0, Color32::GRAY));
|
||||
|
||||
if data.is_empty() {
|
||||
let text = RichText::new("暂无数据").small().color(Color32::GRAY);
|
||||
painter.text(rect.center(), egui::Align2::CENTER_CENTER, text);
|
||||
return;
|
||||
}
|
||||
|
||||
let values: Vec<f64> = data.iter().map(|(_, v)| *v).collect();
|
||||
let min_val = values.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||
let max_val = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||
let range = if (max_val - min_val).abs() < 0.01 { 1.0 } else { max_val - min_val };
|
||||
|
||||
let padding = 10.0;
|
||||
let draw_width = rect.width() - padding * 2.0;
|
||||
let draw_height = rect.height() - padding * 2.0;
|
||||
|
||||
let step_x = if data.len() > 1 { draw_width / (data.len() - 1) as f32 } else { 0.0 };
|
||||
|
||||
for i in 0..data.len().saturating_sub(1) {
|
||||
let x1 = rect.min.x + padding + (i as f32) * step_x;
|
||||
let x2 = rect.min.x + padding + ((i + 1) as f32) * step_x;
|
||||
|
||||
let y1 = rect.max.y - padding - ((values[i] - min_val) / range * draw_height as f64) as f32;
|
||||
let y2 = rect.max.y - padding - ((values[i + 1] - min_val) / range * draw_height as f64) as f32;
|
||||
|
||||
painter.line_segment(
|
||||
[egui::pos2(x1, y1), egui::pos2(x2, y2)],
|
||||
Stroke::new(2.0, Color32::from_rgb(0, 150, 255)),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user