feat: 新增Rust项目基础框架和核心功能模块

- 添加Cargo.toml配置文件,定义项目元信息和依赖项
- 实现配置管理模块(ConfigManager),支持JSON配置读写
- 添加爬虫模块(SpiderManager),支持网页内容抓取和解析
- 实现数据库模块(DatabaseManager),使用SQLite存储评论数据
- 添加LLM分析模块(LLMAnalyzer),支持调用AI接口进行情绪分析
- 实现UI界面模块,包含指标显示和波形图绘制功能
- 添加项目文档和截图资源
This commit is contained in:
2026-02-27 17:03:32 +08:00
parent 9096a38ad2
commit b35e235682
41 changed files with 6592 additions and 0 deletions

125
rust/src/ui.rs Normal file
View 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)),
);
}
}