Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
399 changes: 399 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"canvas-confetti": "^1.9.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"recharts": "^3.10.1",
"zustand": "^5.0.0"
},
"devDependencies": {
Expand Down
12 changes: 12 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
tracing-appender = "0.2"

# 本地时区日界计算(history::get_stats 的 daily_7d)。
# 已在依赖树中(传递依赖),显式声明并开启 local-offset。
time = { version = "0.3", features = ["local-offset"] }

thiserror = "2"
async-trait = "0.1"

Expand Down
55 changes: 55 additions & 0 deletions src-tauri/src/domain/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ impl HistoryRepository {
let now_ms = crate::types::now_unix_ms();
let ms_7_days: i64 = 7 * 24 * 60 * 60 * 1000;
let ms_30_days: i64 = 30 * 24 * 60 * 60 * 1000;
let ms_24h: i64 = 24 * 60 * 60 * 1000;

let last_7_days: u64 = conn
.query_row(
Expand All @@ -180,6 +181,41 @@ impl HistoryRepository {
.map(|n| n as u64)
.map_err(|e| AppError::DatabaseError(e.to_string()))?;

let last_24h: u64 = conn
.query_row(
"SELECT COUNT(*) FROM translation_records WHERE created_at >= ?1",
rusqlite::params![now_ms - ms_24h],
|row| row.get::<_, i64>(0),
)
.map(|n| n as u64)
.map_err(|e| AppError::DatabaseError(e.to_string()))?;

// 近 7 天逐日计数(本地时区日界):SQL 只按毫秒区间过滤,
// 日归属在 Rust 侧做 —— SQLite 的日期函数走 UTC,本地时区的
// 「今天」与其不一致,混用会让趋势图整体错位数小时。
let mut daily_7d: Vec<crate::types::DailyCount> = Vec::with_capacity(7);
{
let today_midnight = local_midnight_ms(now_ms);
for i in (0..7).rev() {
let day_start = today_midnight - i as i64 * ms_24h;
let day_end = day_start + ms_24h;
// 次数与字符量一次查询同时取回(悬浮提示需要字符明细)
let (count, chars): (i64, i64) = conn
.query_row(
"SELECT COUNT(*), COALESCE(SUM(LENGTH(source_text)), 0)
FROM translation_records WHERE created_at >= ?1 AND created_at < ?2",
rusqlite::params![day_start, day_end],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
)
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
daily_7d.push(crate::types::DailyCount {
date_ms: day_start,
count: count as u64,
chars: chars as u64,
});
}
}

let mut by_provider: HashMap<String, u64> = HashMap::new();
{
let mut stmt = conn
Expand All @@ -206,6 +242,8 @@ impl HistoryRepository {
by_provider,
last_7_days,
last_30_days,
last_24h,
daily_7d,
})
}

Expand Down Expand Up @@ -260,6 +298,23 @@ impl HistoryRepository {

// ---- 内部查询辅助函数 ----

/// 计算给定 Unix 毫秒时刻所在「本地时区当天 0 点」的 Unix 毫秒。
///
/// 用 `time` crate 的本地偏移(已在依赖树中,Windows 走 WinAPI /
/// macOS 走 localtime_r,比手搓 libc 声明可靠)。DST 切换日一天可能
/// 是 23/25 小时,7 天窗口的日边界因此整体平移 ≤1h,对趋势图无感知。
fn local_midnight_ms(now_ms: i64) -> i64 {
use time::OffsetDateTime;
let now = OffsetDateTime::from_unix_timestamp(now_ms.div_euclid(1000))
.unwrap_or(OffsetDateTime::UNIX_EPOCH);
// to_offset(UtcOffset) 失败(拿不到本地时区)时按 UTC 兜底:
// 误差只是时区偏移,趋势图仍然成立
let local =
now.to_offset(time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC));
let day_start = local.replace_time(time::Time::MIDNIGHT);
day_start.unix_timestamp() * 1000
}

fn query_all(
conn: &Connection,
limit: i64,
Expand Down
6 changes: 4 additions & 2 deletions src-tauri/src/system/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,8 +425,10 @@ fn open_settings_at(app: &AppHandle, tab: Option<&str>) {
match WebviewWindowBuilder::new(app, "settings", WebviewUrl::App(url.into()))
.title("QuickTranslate 设置")
.additional_browser_args(crate::system::BROWSER_ARGS)
.inner_size(600.0, 480.0)
.min_inner_size(500.0, 400.0)
// 仪表盘(趋势图 + 指标卡)需要纵向空间:480 高的首屏只露得出
// 健康横幅。640×560 让首屏能完整呈现仪表盘 + 第一组设置项。
.inner_size(640.0, 560.0)
.min_inner_size(520.0, 440.0)
.resizable(true)
.center()
.build()
Expand Down
15 changes: 15 additions & 0 deletions src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ pub struct StatsResult {
pub by_provider: HashMap<String, u64>,
pub last_7_days: u64,
pub last_30_days: u64,
/// 近 24 小时翻译次数(仪表盘「最近 24h」卡片)
pub last_24h: u64,
/// 近 7 天逐日翻译次数,按时间升序;末位是今天(部分)。
/// 每日边界按本地时区计算 —— 仪表盘趋势图的横轴。
pub daily_7d: Vec<DailyCount>,
}

/// 单日翻译计数(daily_7d 的元素)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DailyCount {
/// 当日 0 点的 Unix 毫秒(本地时区)
pub date_ms: i64,
pub count: u64,
/// 当日翻译的源字符总量(悬浮提示的详细数据)
pub chars: u64,
}

/// 浮窗定位信息(Rust → 前端 event)
Expand Down
6 changes: 3 additions & 3 deletions src/components/CopyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ export function CopyButton({ text, className = "", label = "复制" }: CopyButto
onClick={handleCopy}
title={copied ? "已复制" : `复制${label}`}
className={[
"inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs",
"inline-flex items-center gap-1 px-2 py-1 rounded-md text-[12.5px] font-medium",
"transition-all duration-150",
"hover:bg-[var(--hover-bg)] active:bg-[var(--active-bg)]",
"active:scale-95",
copied
? "text-green-500 dark:text-green-400"
: "text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]",
? "text-green-600 dark:text-green-400"
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)]",
className,
].join(" ")}
>
Expand Down
16 changes: 16 additions & 0 deletions src/hooks/useTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,18 @@ export function useTheme() {

// 监听系统主题变化(仅 "system" 模式时生效)
const systemMqRef = useRef<MediaQueryList | null>(null);
const systemPollRef = useRef<ReturnType<typeof setInterval> | null>(null);

useEffect(() => {
// 清理旧的系统主题监听器
if (systemMqRef.current) {
systemMqRef.current.onchange = null;
systemMqRef.current = null;
}
if (systemPollRef.current) {
clearInterval(systemPollRef.current);
systemPollRef.current = null;
}

applyTheme(theme);

Expand All @@ -48,11 +53,22 @@ export function useTheme() {
mq.onchange = (e) => {
document.documentElement.classList.toggle("dark", e.matches);
};
// WebView2 的 matchMedia change 事件在部分运行时上不可靠
// (prefers-color-scheme 动态刷新是已知薄弱点,实机 2026-10-20
// 验证「跟随系统」下切系统主题浮窗纹丝不动)。轮询重读兜底:
// applyTheme("system") 幂等 —— 明暗没变时 toggle 不产生 DOM
// mutation,每 2s 的开销只是一次 matchMedia 查询。
systemPollRef.current = setInterval(() => applyTheme("system"), 2000);
}

return () => {
if (systemMqRef.current) {
systemMqRef.current.onchange = null;
systemMqRef.current = null;
}
if (systemPollRef.current) {
clearInterval(systemPollRef.current);
systemPollRef.current = null;
}
};
}, [theme]);
Expand Down
3 changes: 3 additions & 0 deletions src/lib/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export interface StatsResult {
by_provider: Record<string, number>;
last_7_days: number;
last_30_days: number;
last_24h: number;
/** 近 7 天逐日计数,升序,末位是今天(部分)。date_ms 为本地时区当日 0 点 */
daily_7d: Array<{ date_ms: number; count: number; chars: number }>;
}

export interface AppConfig {
Expand Down
Loading
Loading