From 11c92a77c3628250e2943824e2bd761057857461 Mon Sep 17 00:00:00 2001 From: 5771067 <5771067@qq.com> Date: Tue, 27 Jan 2026 03:50:04 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E7=8E=B0=E5=9C=A8=20Time=20&=20Sales=20?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E7=9A=84=E6=97=B6=E9=97=B4=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E4=BC=9A=E6=AD=A3=E7=A1=AE=E4=BD=BF=E7=94=A8=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E6=97=B6=E5=8C=BA=E9=85=8D=E7=BD=AE=EF=BC=88Settings=20?= =?UTF-8?q?=E2=86=92=20Timezone=EF=BC=89=E3=80=82=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=8C=85=E6=8B=AC=EF=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TimeFormat 新增 format_timestamp() 方法支持时区 TimeAndSales 添加 timezone 字段,在 tick 时自动更新 时间在渲染时动态格式化,而不是在数据插入时 --- data/src/panel/timeandsales.rs | 67 ++++++++++++++++++++++ src/main.rs | 3 +- src/modal/pane/settings.rs | 22 ++++++- src/screen/dashboard.rs | 4 +- src/screen/dashboard/pane.rs | 7 ++- src/screen/dashboard/panel/timeandsales.rs | 14 ++++- 6 files changed, 110 insertions(+), 7 deletions(-) diff --git a/data/src/panel/timeandsales.rs b/data/src/panel/timeandsales.rs index aa137c9d..26862ca9 100644 --- a/data/src/panel/timeandsales.rs +++ b/data/src/panel/timeandsales.rs @@ -7,6 +7,70 @@ use crate::util::ok_or_default; const TRADE_RETENTION_MS: u64 = 120_000; +/// Time display format for trades in the Time & Sales panel +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +pub enum TimeFormat { + /// mm:ss.fff (e.g., 45:23.123) + #[default] + MinSecMs, + /// hh:mm:ss (e.g., 14:45:23) + HourMinSec, + /// hh:mm:ss.fff (e.g., 14:45:23.123) + HourMinSecMs, +} + +impl TimeFormat { + pub const ALL: [TimeFormat; 3] = [ + TimeFormat::MinSecMs, + TimeFormat::HourMinSec, + TimeFormat::HourMinSecMs, + ]; + + /// Returns the chrono format string for this time format + pub fn format_str(&self) -> &'static str { + match self { + TimeFormat::MinSecMs => "%M:%S%.3f", + TimeFormat::HourMinSec => "%H:%M:%S", + TimeFormat::HourMinSecMs => "%H:%M:%S%.3f", + } + } + + /// Formats a timestamp in milliseconds according to the time format and timezone + pub fn format_timestamp(&self, ts_ms: u64, timezone: crate::UserTimezone) -> String { + use chrono::DateTime; + + let Some(datetime) = DateTime::from_timestamp( + ts_ms as i64 / 1000, + (ts_ms % 1000) as u32 * 1_000_000, + ) else { + return String::new(); + }; + + let format_str = self.format_str(); + + match timezone { + crate::UserTimezone::Local => datetime + .with_timezone(&chrono::Local) + .format(format_str) + .to_string(), + crate::UserTimezone::Utc => datetime + .with_timezone(&chrono::Utc) + .format(format_str) + .to_string(), + } + } +} + +impl std::fmt::Display for TimeFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TimeFormat::MinSecMs => write!(f, "mm:ss.fff"), + TimeFormat::HourMinSec => write!(f, "hh:mm:ss"), + TimeFormat::HourMinSecMs => write!(f, "hh:mm:ss.fff"), + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] pub struct Config { pub trade_size_filter: f32, @@ -14,6 +78,8 @@ pub struct Config { pub trade_retention: Duration, #[serde(deserialize_with = "ok_or_default", default)] pub stacked_bar: Option, + #[serde(deserialize_with = "ok_or_default", default)] + pub time_format: TimeFormat, } impl Default for Config { @@ -22,6 +88,7 @@ impl Default for Config { trade_size_filter: 0.0, trade_retention: Duration::from_millis(TRADE_RETENTION_MS), stacked_bar: StackedBar::Compact(StackedBarRatio::default()).into(), + time_format: TimeFormat::default(), } } } diff --git a/src/main.rs b/src/main.rs index 6286c462..6426d1eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -195,10 +195,11 @@ impl Flowsurface { } Message::Tick(now) => { let main_window_id = self.main_window.id; + let timezone = self.timezone; return self .active_dashboard_mut() - .tick(now, main_window_id) + .tick(now, timezone, main_window_id) .map(move |msg| Message::Dashboard { layout_id: None, event: msg, diff --git a/src/modal/pane/settings.rs b/src/modal/pane/settings.rs index d9418d2f..a338257a 100644 --- a/src/modal/pane/settings.rs +++ b/src/modal/pane/settings.rs @@ -14,7 +14,7 @@ use data::chart::{ }; use data::layout::pane::VisualConfig; use data::panel::ladder; -use data::panel::timeandsales::{StackedBar, StackedBarRatio}; +use data::panel::timeandsales::{StackedBar, StackedBarRatio, TimeFormat}; use data::util::format_with_commas; use iced::widget::{checkbox, space}; @@ -428,9 +428,29 @@ pub fn timesales_cfg_view<'a>( .into() }; + let time_format_column = { + let time_format_picker = pick_list( + TimeFormat::ALL, + Some(cfg.time_format), + move |new_format| { + Message::VisualConfigChanged( + pane, + VisualConfig::TimeAndSales(timeandsales::Config { + time_format: new_format, + ..cfg + }), + false, + ) + }, + ); + + column![text("Time format").size(14), time_format_picker].spacing(8) + }; + let content = split_column![ trade_size_column, history_column, + time_format_column, stacked_bar, row![space::horizontal(), sync_all_button(pane, VisualConfig::TimeAndSales(cfg))], ; spacing = 12, align_x = Alignment::Start diff --git a/src/screen/dashboard.rs b/src/screen/dashboard.rs index b41a6abc..58f6b6f4 100644 --- a/src/screen/dashboard.rs +++ b/src/screen/dashboard.rs @@ -1002,12 +1002,12 @@ impl Dashboard { }); } - pub fn tick(&mut self, now: Instant, main_window: window::Id) -> Task { + pub fn tick(&mut self, now: Instant, timezone: UserTimezone, main_window: window::Id) -> Task { let mut tasks = vec![]; let layout_id = self.layout_id; self.iter_all_panes_mut(main_window) - .for_each(|(_window_id, _pane, state)| match state.tick(now) { + .for_each(|(_window_id, _pane, state)| match state.tick(now, timezone) { Some(pane::Action::Chart(action)) => match action { chart::Action::ErrorOccurred(err) => { state.status = pane::Status::Ready; diff --git a/src/screen/dashboard/pane.rs b/src/screen/dashboard/pane.rs index 5e5ebc16..1b7d702e 100644 --- a/src/screen/dashboard/pane.rs +++ b/src/screen/dashboard/pane.rs @@ -1519,10 +1519,15 @@ impl State { self.content.last_tick() } - pub fn tick(&mut self, now: Instant) -> Option { + pub fn tick(&mut self, now: Instant, timezone: UserTimezone) -> Option { let invalidate_interval: Option = self.update_interval(); let last_tick: Option = self.last_tick(); + // Update timezone for TimeAndSales panel + if let Content::TimeAndSales(Some(panel)) = &mut self.content { + panel.set_timezone(timezone); + } + if let Some(streams) = self.streams.waiting_to_resolve() && !streams.is_empty() { diff --git a/src/screen/dashboard/panel/timeandsales.rs b/src/screen/dashboard/panel/timeandsales.rs index aced62d6..2055992e 100644 --- a/src/screen/dashboard/panel/timeandsales.rs +++ b/src/screen/dashboard/panel/timeandsales.rs @@ -4,6 +4,7 @@ use data::config::theme::{darken, lighten}; pub use data::panel::timeandsales::Config; use data::panel::timeandsales::{HistAgg, StackedBar, StackedBarRatio, TradeDisplay, TradeEntry}; use exchange::{TickerInfo, Trade, volume_size_unit}; +use data::UserTimezone; use iced::widget::canvas::{self, Text}; use iced::{Alignment, Event, Point, Rectangle, Renderer, Size, Theme, mouse}; @@ -80,6 +81,7 @@ pub struct TimeAndSales { cache: canvas::Cache, last_tick: Instant, scroll_offset: f32, + timezone: UserTimezone, } impl TimeAndSales { @@ -95,6 +97,14 @@ impl TimeAndSales { cache: canvas::Cache::default(), last_tick: Instant::now(), scroll_offset: 0.0, + timezone: UserTimezone::default(), + } + } + + pub fn set_timezone(&mut self, timezone: UserTimezone) { + if self.timezone != timezone { + self.timezone = timezone; + self.cache.clear(); } } @@ -118,7 +128,7 @@ impl TimeAndSales { (trade_time_ms % 1000) as u32 * 1_000_000, ) { let trade_display = TradeDisplay { - time_str: trade_time.format("%M:%S.%3f").to_string(), + time_str: trade_time.format(self.config.time_format.format_str()).to_string(), price: trade.price, qty: trade.qty, is_sell: trade.is_sell, @@ -520,7 +530,7 @@ impl canvas::Program for TimeAndSales { ); let trade_time = create_text( - trade.time_str.clone(), + self.config.time_format.format_timestamp(entry.ts_ms, self.timezone), Point { x: row_width * 0.1, y: y_position, From 1ed118ed6ab9b2182cd2a02c2f1928ee5a8c18e4 Mon Sep 17 00:00:00 2001 From: 5771067 <5771067@qq.com> Date: Tue, 27 Jan 2026 04:04:01 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E8=83=8C?= =?UTF-8?q?=E6=99=AF=E5=8A=9F=E8=83=BD=E6=96=87=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/screen/dashboard/pane.rs | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/screen/dashboard/pane.rs b/src/screen/dashboard/pane.rs index 1b7d702e..343f6a89 100644 --- a/src/screen/dashboard/pane.rs +++ b/src/screen/dashboard/pane.rs @@ -1373,8 +1373,40 @@ impl State { where F: FnOnce() -> Element<'a, Message>, { + // Create watermark layer showing panel type + let watermark: Element<'a, Message> = { + let label = match self.content.kind() { + ContentKind::HeatmapChart => "Heatmap", + ContentKind::CandlestickChart => "Candlestick", + ContentKind::FootprintChart => "Footprint", + ContentKind::ComparisonChart => "Comparison", + ContentKind::TimeAndSales => "Time & Sales", + ContentKind::Ladder => "DOM/Ladder", + ContentKind::Starter => "", + }; + + if label.is_empty() { + iced::widget::Space::new().into() + } else { + center( + text(label) + .size(42) + .color(iced::Color { a: 0.06, ..iced::Color::WHITE }) + ) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + }; + + // Stack watermark behind base, then add toast manager + let base_with_watermark: Element<'a, Message> = iced::widget::stack![watermark, base] + .width(Length::Fill) + .height(Length::Fill) + .into(); + let base = - widget::toast::Manager::new(base, &self.notifications, Alignment::End, move |msg| { + widget::toast::Manager::new(base_with_watermark, &self.notifications, Alignment::End, move |msg| { Message::PaneEvent(pane, Event::DeleteNotification(msg)) }) .into();