2026-04-07 17:27:38 +08:00
|
|
|
use egui::{Color32, FontId, Stroke, Vec2};
|
|
|
|
|
use std::sync::atomic::{AtomicBool, AtomicI32};
|
2026-02-27 17:03:32 +08:00
|
|
|
use std::sync::Arc;
|
2026-04-07 17:27:38 +08:00
|
|
|
use parking_lot::Mutex;
|
2026-02-27 17:03:32 +08:00
|
|
|
|
|
|
|
|
pub struct AppState {
|
|
|
|
|
pub running: Arc<AtomicBool>,
|
|
|
|
|
pub current_score: Arc<AtomicI32>,
|
2026-04-07 17:27:38 +08:00
|
|
|
pub status_text: Arc<Mutex<String>>,
|
2026-02-27 17:03:32 +08:00
|
|
|
pub fetch_count: Arc<AtomicI32>,
|
|
|
|
|
pub analysis_count: Arc<AtomicI32>,
|
2026-04-07 17:27:38 +08:00
|
|
|
pub no_content_count: Arc<AtomicI32>,
|
2026-02-27 17:03:32 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AppState {
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
running: Arc::new(AtomicBool::new(false)),
|
|
|
|
|
current_score: Arc::new(AtomicI32::new(50)),
|
2026-04-07 17:27:38 +08:00
|
|
|
status_text: Arc::new(Mutex::new("就绪".to_string())),
|
2026-02-27 17:03:32 +08:00
|
|
|
fetch_count: Arc::new(AtomicI32::new(0)),
|
|
|
|
|
analysis_count: Arc::new(AtomicI32::new(0)),
|
2026-04-07 17:27:38 +08:00
|
|
|
no_content_count: Arc::new(AtomicI32::new(0)),
|
2026-02-27 17:03:32 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for AppState {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self::new()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_score_color(score: i32) -> Color32 {
|
|
|
|
|
match score {
|
2026-04-07 17:27:38 +08:00
|
|
|
0..=30 => Color32::from_rgb(21, 101, 192), // 深蓝
|
|
|
|
|
31..=38 => Color32::from_rgb(25, 118, 210), // 蓝色
|
|
|
|
|
39..=44 => Color32::from_rgb(66, 165, 245), // 浅蓝
|
|
|
|
|
45..=55 => Color32::from_rgb(102, 187, 106), // 绿色
|
|
|
|
|
56..=64 => Color32::from_rgb(255, 167, 38), // 橙色
|
|
|
|
|
65..=69 => Color32::from_rgb(251, 140, 0), // 深橙
|
|
|
|
|
70..=100 => Color32::from_rgb(229, 57, 53), // 红色
|
2026-02-27 17:03:32 +08:00
|
|
|
_ => Color32::GRAY,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_score_label(score: i32, cold: i32, warm: i32) -> &'static str {
|
|
|
|
|
if score < cold {
|
|
|
|
|
"看跌"
|
|
|
|
|
} else if score > warm {
|
|
|
|
|
"看涨"
|
|
|
|
|
} else {
|
|
|
|
|
"中性"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 17:27:38 +08:00
|
|
|
pub fn get_score_description(score: i32) -> &'static str {
|
|
|
|
|
match score {
|
|
|
|
|
0..=29 => "极度悲观",
|
|
|
|
|
30..=38 => "悲观",
|
|
|
|
|
39..=44 => "偏悲观",
|
|
|
|
|
45..=55 => "中立",
|
|
|
|
|
56..=64 => "偏乐观",
|
|
|
|
|
65..=69 => "乐观",
|
|
|
|
|
70..=100 => "极度乐观",
|
|
|
|
|
_ => "无法判断",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 17:03:32 +08:00
|
|
|
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);
|
2026-04-07 17:27:38 +08:00
|
|
|
let description = get_score_description(score);
|
|
|
|
|
let size = Vec2::new(140.0, 160.0);
|
2026-02-27 17:03:32 +08:00
|
|
|
|
|
|
|
|
let (rect, _response) = ui.allocate_exact_size(size, egui::Sense::hover());
|
|
|
|
|
|
|
|
|
|
let painter = ui.painter();
|
2026-04-07 17:27:38 +08:00
|
|
|
let center = rect.center();
|
2026-02-27 17:03:32 +08:00
|
|
|
|
2026-04-07 17:27:38 +08:00
|
|
|
// 外圈背景 - 白色主题
|
|
|
|
|
painter.circle_filled(center, 65.0, Color32::from_rgb(240, 240, 245));
|
|
|
|
|
painter.circle_stroke(center, 65.0, Stroke::new(3.0, Color32::from_rgb(200, 200, 210)));
|
2026-02-27 17:03:32 +08:00
|
|
|
|
2026-04-07 17:27:38 +08:00
|
|
|
// 内圈颜色
|
|
|
|
|
painter.circle_filled(center, 58.0, color);
|
|
|
|
|
|
|
|
|
|
// 发光效果
|
|
|
|
|
for i in 1..=3 {
|
|
|
|
|
let alpha = (60 / i) as u8;
|
|
|
|
|
let glow_color = Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), alpha);
|
|
|
|
|
painter.circle_filled(center, 58.0 - (i as f32) * 5.0, glow_color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 中心白色背景
|
|
|
|
|
painter.circle_filled(center, 45.0, Color32::from_rgb(255, 255, 255));
|
2026-02-27 17:03:32 +08:00
|
|
|
|
2026-04-07 17:27:38 +08:00
|
|
|
// 分数文本 - 深色
|
|
|
|
|
let score_text = format!("{}", score);
|
2026-02-27 17:03:32 +08:00
|
|
|
painter.text(
|
2026-04-07 17:27:38 +08:00
|
|
|
center - Vec2::new(0.0, 5.0),
|
2026-02-27 17:03:32 +08:00
|
|
|
egui::Align2::CENTER_CENTER,
|
2026-04-07 17:27:38 +08:00
|
|
|
score_text,
|
|
|
|
|
FontId::proportional(42.0),
|
|
|
|
|
color,
|
2026-02-27 17:03:32 +08:00
|
|
|
);
|
|
|
|
|
|
2026-04-07 17:27:38 +08:00
|
|
|
// 标签文本 - 深色
|
2026-02-27 17:03:32 +08:00
|
|
|
painter.text(
|
2026-04-07 17:27:38 +08:00
|
|
|
center + Vec2::new(0.0, 28.0),
|
2026-02-27 17:03:32 +08:00
|
|
|
egui::Align2::CENTER_CENTER,
|
2026-04-07 17:27:38 +08:00
|
|
|
label,
|
|
|
|
|
FontId::proportional(16.0),
|
|
|
|
|
Color32::from_rgb(50, 50, 55),
|
2026-02-27 17:03:32 +08:00
|
|
|
);
|
2026-04-07 17:27:38 +08:00
|
|
|
|
|
|
|
|
// 描述文本(在指示灯下方)
|
|
|
|
|
ui.add_space(10.0);
|
|
|
|
|
ui.label(egui::RichText::new(description).size(14.0).color(Color32::from_rgb(100, 100, 110)));
|
2026-02-27 17:03:32 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-07 17:27:38 +08:00
|
|
|
pub fn draw_waveform(ui: &mut egui::Ui, data: &[(String, f64)], _width: f32, _height: f32) {
|
2026-02-27 17:03:32 +08:00
|
|
|
let rect = ui.available_rect_before_wrap();
|
|
|
|
|
let painter = ui.painter();
|
|
|
|
|
|
2026-04-07 17:27:38 +08:00
|
|
|
// 白色背景
|
|
|
|
|
painter.rect_filled(rect, 8.0, Color32::from_rgb(255, 255, 255));
|
|
|
|
|
painter.rect_stroke(rect, 8.0, Stroke::new(1.0, Color32::from_rgb(220, 220, 230)));
|
2026-02-27 17:03:32 +08:00
|
|
|
|
|
|
|
|
if data.is_empty() {
|
2026-04-07 17:27:38 +08:00
|
|
|
let text = "暂无数据";
|
|
|
|
|
painter.text(
|
|
|
|
|
rect.center(),
|
|
|
|
|
egui::Align2::CENTER_CENTER,
|
|
|
|
|
text,
|
|
|
|
|
FontId::proportional(14.0),
|
|
|
|
|
Color32::from_rgb(150, 150, 160),
|
|
|
|
|
);
|
2026-02-27 17:03:32 +08:00
|
|
|
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 };
|
|
|
|
|
|
2026-04-07 17:27:38 +08:00
|
|
|
let padding = 15.0;
|
2026-02-27 17:03:32 +08:00
|
|
|
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 };
|
|
|
|
|
|
2026-04-07 17:27:38 +08:00
|
|
|
// 绘制网格线
|
|
|
|
|
for i in 0..=4 {
|
|
|
|
|
let y = rect.max.y - padding - (i as f32) * (draw_height / 4.0);
|
|
|
|
|
painter.line_segment(
|
|
|
|
|
[egui::pos2(rect.min.x + padding, y), egui::pos2(rect.max.x - padding, y)],
|
|
|
|
|
Stroke::new(1.0, Color32::from_rgba_unmultiplied(200, 200, 210, 100)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 绘制数据线
|
2026-02-27 17:03:32 +08:00
|
|
|
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)],
|
2026-04-07 17:27:38 +08:00
|
|
|
Stroke::new(2.0, Color32::from_rgb(0, 123, 255)),
|
2026-02-27 17:03:32 +08:00
|
|
|
);
|
|
|
|
|
}
|
2026-04-07 17:27:38 +08:00
|
|
|
|
|
|
|
|
// 绘制数据点
|
|
|
|
|
for i in 0..data.len() {
|
|
|
|
|
let x = rect.min.x + padding + (i as f32) * step_x;
|
|
|
|
|
let y = rect.max.y - padding - ((values[i] - min_val) / range * draw_height as f64) as f32;
|
|
|
|
|
|
|
|
|
|
painter.circle_filled(egui::pos2(x, y), 3.0, Color32::from_rgb(0, 150, 255));
|
|
|
|
|
}
|
2026-02-27 17:03:32 +08:00
|
|
|
}
|