版本:v1.1(UI 重构后)
目的:定义「用眼休息助手」产品的功能边界、性能基线、数据契约与界面规范,确保开发、测试、验收有据可依。
为长时间使用 Windows 电脑的用户提供低侵入式用眼休息管理。依据「20-20-20」准则(每 20 分钟看 20 英尺外的物体 20 秒)默认定时提醒,并提供使用统计、应用使用记录等可视化反馈。
- 定时提醒(强/弱模式)
- 当日统计(工作时长、休息时长、休息次数、跳过次数)
- 应用使用时长记录(前台进程名 + 时长,不记录窗口标题,不上传)
- 应用使用历史窗口(今日 / 近 7 天 / 近 30 天 TopN)
- 仪表盘历史(周:近 4 周热图 + 当日详情常驻 / 月:4 个季度 × 3 月热图 + 当月详情常驻)
- 多显示器覆盖
- 系统托盘常驻
- 主题切换(浅/深/跟随系统)
- 开机自启
- 空闲检测自动暂停
- 设置项可视化配置
- 屏幕锁定、Bios 中断、BlockInput
- 联网、云同步、账号体系
- 多语言(首版仅中文)
- 鼠标/键盘宏、健康评估
| ID | 名称 | 描述 | 验收标准 |
|---|---|---|---|
| F-01 | 倒计时触发 | 达到设置间隔(默认 1200 秒)时触发提醒事件 | 期间无系统级干扰、计时误差 ±1 秒 |
| F-02 | 强提醒(全屏) | 每块显示器独立一个无边框全屏窗(Per-Monitor DPI 友好),ESC/按钮关闭算 1 次跳过 | 多显示器全覆盖、可关闭、不抢焦点破坏键鼠输入以外事件 |
| F-03 | 弱提醒(角落浮窗) | 所有显示器右下角各弹一个圆角浮窗,倒计时归零自动消失(基线 = RestSeconds) |
不阻塞焦点,不抢 Esc 行为 |
| F-04 | 模式切换 | 设置项可在 Fullscreen / Weak / Off 之间切换 | 重启托盘后生效 |
| F-05 | 跳过计数 | 用户主动关闭强提醒视为跳过 | 写入 DailyStats.SkipCount |
| F-06 | 完成计数 | 弱提醒时间走完或强提醒倒计时归零计 1 次休息完成 | 写入 DailyStats.RestCount / RestSeconds |
| F-07 | 工作时长累计 | 触发提醒到下一次开始时间,差值记录到 WorkSeconds |
跨日滚动重置 |
| F-08 | 应用使用记录 | 2 秒采样当前前台进程,聚合落库 | 仅记录 ProcessName,无路径、无标题 |
| F-09 | 统计查询 | 当日统计卡片化展示;应用使用可切今日/近 7 天/近 30 天 TopN | 数据 ≤ 1 秒延迟 |
| F-10 | 托盘 | 双击恢复主窗,右键菜单(暂停/休息/打开主窗/退出);菜单图标 16×16 与文字同色 | 主窗关闭 = 隐藏到托盘,不退出 |
| F-11 | 开机自启 | 通过 HKCU 注册表 Run 项 |
可在设置关闭 |
| F-12 | 主题 | Light / Dark / Auto | 实时切换;切换时刷 DynamicResource,所有控件即时跟随 |
| F-13 | 空闲暂停 | 超过 IdleTimeoutSeconds(默认 120 秒)无键盘鼠标 → 计时暂停 |
输入恢复后继续,不丢时长 |
| F-14 | 仪表盘历史(周) | 4 行 × 7 列热图(近 4 周),格子颜色按工作时间 4 档映射;点击格子下方常驻详情卡显示当日工作/完成/跳过/完成率 | 进入仪表盘自动选中今天;切换周/月视图默认重新选中当天/当月 |
| F-15 | 仪表盘历史(月) | 4 行(季度)× 3 列(月)热图,整年 12 个月 | 同 F-14,月视图默认选中本月 |
| F-16 | 应用使用窗口切换 | TopN 内嵌 inline toggle:今日 / 近 7 天 / 近 30 天;TopN 切换 + 筛选 + 排序 |
切换窗口不抖动 |
| F-17 | TopN 内嵌排序 | Top 10 / Top 20 / Top 50 / 全部 四档 | 切换 TopN 不触发全局抖动 |
| F-18 | 周/月视图热图颜色 | Level 0(无数据)格子内文字用 muted 灰(与周视图一致);Level 1-3(有数据)格子内文字全部用白色 | 文字与底色对比度足够 |
| ID | 指标 | 预算 | 测量 |
|---|---|---|---|
| P-01 | 空闲 RSS | ≤ 150 MB | Process.WorkingSet64 启动 5 分钟 |
| P-02 | 提醒窗口峰值 | ≤ 180 MB | 同上,峰值 |
| P-03 | CPU 占用 | < 1 % | 启动 5 分钟平均 |
| P-04 | 启动 → 托盘可见 | ≤ 1.5 s | Stopwatch |
| P-05 | 发布产物体积 | ≤ 80 MB self-contained | 参见 REPORT §3 |
| P-06 | 多显示器 | 任意矩形拼接均可 | EnumDisplayMonitors |
| P-07 | DPI | Per-Monitor V2,UI 不虚 | manifest |
| P-08 | 滚动条 | 8px 细款,暖灰 thumb 默认 55% opacity → hover 90% | 视觉一致,避免双滚动条 |
| P-09 | 热图交互响应 | 切换周/月、重载数据 ≤ 200ms 完成可见状态更新 | 用户主观流畅 |
| P-10 | TopN 抖动 | TopN 切换时整体内容不能有 ≥ 4px 视觉位移 | 8px 滚动条 + 禁用 ItemsView 重算 BarWidth |
CREATE TABLE IF NOT EXISTS app_usage (
date TEXT NOT NULL, -- YYYY-MM-DD
process_name TEXT NOT NULL,
duration_sec INTEGER NOT NULL,
PRIMARY KEY (date, process_name)
);
CREATE TABLE IF NOT EXISTS daily_stats (
date TEXT PRIMARY KEY, -- YYYY-MM-DD
work_seconds INTEGER NOT NULL DEFAULT 0,
rest_seconds INTEGER NOT NULL DEFAULT 0,
rest_count INTEGER NOT NULL DEFAULT 0,
skip_count INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS rest_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
started_at TEXT NOT NULL, -- ISO 8601
ended_at TEXT,
mode TEXT NOT NULL, // Fullscreen | Weak
completed INTEGER NOT NULL -- 0/1
);public record AppUsageRecord(DateOnly Date, string ProcessName, int DurationSeconds);
public record AppUsageAggregate(string ProcessName, int TotalSeconds, int DaysWithUsage,
DateOnly FirstSeen, DateOnly LastSeen);
public record DailyStats(DateOnly Date, int WorkSeconds, int RestSeconds,
int RestCount, int SkipCount);
public enum ReminderMode { Fullscreen, Weak, Off }
public enum WeakPlacement { Primary, Focused }
public enum ThemeMode { Light, Dark, Auto }
public enum HeatmapViewMode { Week, Month }
public enum AppsHistoryMode { Today, Week, Month }
public sealed class Settings {
public int IntervalSeconds { get; set; } = 1200;
public int RestSeconds { get; set; } = 20;
public ReminderMode Mode { get; set; } = ReminderMode.Fullscreen;
public bool SkipAllowed { get; set; } = true;
public WeakPlacement WeakPlacement { get; set; } = WeakPlacement.Primary;
public int IdleTimeoutSeconds { get; set; } = 120;
public bool StartWithWindows { get; set; } = true;
public ThemeMode Theme { get; set; } = ThemeMode.Auto;
public bool RecordAppUsage { get; set; } = true;
public int AppUsageSampleSeconds { get; set; } = 2;
}public interface IAppUsageService {
Task RecordSampleAsync(string processName, int seconds, CancellationToken ct = default);
Task<IReadOnlyList<AppUsageRecord>> GetDayAsync(DateOnly date, CancellationToken ct = default);
Task<IReadOnlyList<AppUsageRecord>> GetTopAsync(int n, CancellationToken ct = default);
// 历史窗口聚合:跨区间按进程名 GROUP BY 累加,附首末次出现
Task<IReadOnlyList<AppUsageAggregate>> GetRangeAsync(DateOnly from, DateOnly to, CancellationToken ct = default);
}仅列相对 v1.0 新增/扩展。完整契约见
src/ProjectEye.Core/Abstractions/*.cs。
public interface IStatisticsService {
Task<DailyStats> GetTodayAsync(CancellationToken ct = default);
Task AddWorkSecondsAsync(int seconds, CancellationToken ct = default);
Task AddRestAsync(int seconds, bool completed, CancellationToken ct = default);
Task AddSkipAsync(CancellationToken ct = default);
Task<IReadOnlyList<DailyStats>> GetDailyRangeAsync(DateOnly from, DateOnly to, CancellationToken ct = default);
Task<IReadOnlyList<MonthlyBucket>> GetMonthlyRangeAsync(int year, CancellationToken ct = default);
}- 无系统标题栏(
WindowStyle=None+AllowsTransparency=True);自定义外框:圆角 16,hairline stroke - 标题栏 48px:左侧 Logo + 副标题;右侧最小化 + 关闭按钮(按钮图标 Path 必须放在 16×16 Grid 内 Stretch=Uniform 才真正居中)
- 侧栏 232px:4 个 NavItem(左 3px accent 条带 + 图标 + 文字)
- 内容区:
ContentPresenter直接承载当前 View,不放外层 ScrollViewer(每个 View 内部自己决定)
- 深色背景
#131312Alpha 0.9 - 240×240 圆环 + 48px 描边,92pt mono 倒计时
- 文案:"让眼睛休息一下" + "请远眺 6 米外的物体,持续 20 秒(20-20-20 原则)"
- 按钮:"跳过本次"
- 多显示器时每块屏各开一个独立窗口(Per-Monitor DPI 友好),第一个为主、其余设 Owner 绑定
- 8px 圆角卡片,纯
SurfaceElevatedBrush底色,不用毛玻璃也不开 AllowsTransparency - HWND 形状通过
CreateRoundRectRgn+SetWindowRgn切成圆角(绕过 Per-Monitor V2 + AllowsTransparency 的副屏渲染黑洞) - 监听
WM_DPICHANGED自动按新 DPI 重切 region - 显示标题 + 倒计时(mono 字体)+ ✕ 跳过
- 多显示器时每块屏右下角各弹一个
- 页面顶部:eyebrow + 段标题 + 右侧工具栏(VerticalAlignment=Center)
- 工具栏:时间窗口 inline toggle(今日 / 近 7 天 / 近 30 天)+ 搜索框(带 search icon left)+ 刷新按钮
- 主体卡片:
- Row 0 (Auto / Margin=20,16,20,12):左侧 inline TopN toggle + 右侧摘要 chip(时段 + 汇总,靠右 8px 间距)
- Row 1 (Auto / Margin=20,4,20,12):列头(# / 应用 / 使用时长 / 占比 % / 时长占比)+ 自绘 Bar
- Row 2:列表,内嵌 ScrollViewer + ItemsControl
- TopN 切换零抖动:通过样式
InlineToggle.MinWidth=64+ 8px 细 ScrollBar 解决 - TopN sort + percent + 占比:列头 RowIcon + 应用名 + mono 时长 + mono 占比 % + 自绘水平条(按 max 比例)
- 页面顶部:eyebrow + 段标题
- 顶部三卡:工作时间 / 已休息 / 完成次数(mono 大数字 26pt)
- 历史卡:
- Row 0:标题 [周/月 toggle] + 上一段/下一段/回到今天
- Row 1:详情卡常驻(默认今天/本月,值随视图切换自动重选):date + tooltip + 工作/完成/跳过/完成率 4 项
- Row 2:周/月热图本体(互斥 Visibility)
- 周视图:4 行(近 4 周)× 7 列(周一到周日);格子 48×48 圆角 6 mono 小标签 "一/二/三/四/五/六/日"
- 月视图:4 行(Q1-Q4)× 3 列(1-3 月);格子 170×60 圆角 8 左上月 + 右下大 mono 时长
- Row 3:图例(少 / 4 档色块 / 多)
- 热图颜色档:
- Level 0(无数据):底色浅灰,字色 muted(与周视图一致)
- Level 1-3(有数据):底色浅→深蓝,字色全白
- 选中态:StrokeStrongBrush 描边
- 打开主窗(icon: eye)
- 立即休息(icon: coffee)
- · 暂停 30 / 60 / 120 分钟(icon: clock,全部跟菜单文字同色 16×16 Path,hover 与 Foreground 联动)
- 退出(icon: x)
- Light:
Surface=#F7F7F5/Elevated=#FFFFFF/Muted=#F0EFEC/Stroke=#E5E2DC/StrokeStrong=#CFCCC5/Foreground=#1C1B1A/MutedText=#6F6E6B/Accent=#3D6CD9/AccentHover=#2E58B8/AccentSoft=#E5EDFB - Dark:
Surface=#131312/Elevated=#1D1D1B/Muted=#262523/Stroke=#2E2D2A/StrokeStrong=#3C3B38/Foreground=#EEEAE0/MutedText=#9B9890/Accent=#6E94F0/AccentHover=#88A8F5/AccentSoft=#1F2B47 - 热图:Level0=#EAE7E0 / Level1=#A8B9E0 / Level2=#7DA0E6 / Level3=#3D6CD9(深色:#262523 / #3B5388 / #5679BD / #88A8F5)
- 圆角 3 档:6 / 10 / 16,绝不混用
- 间距 5 档:4 / 8 / 12 / 16 / 24
- 字体:
Segoe UI Variable+Microsoft YaHei UI,数字 / 倒计时 / 计时专用Cascadia Mono/Consolas回退 - AppFontFallback:使用
Segoe UI Variable(Win11 自带)优先,回退Segoe UI,再回退中文Microsoft YaHei UI
- 主窗不滚动:内容区直接
ContentPresenter装当前 View,不放外层 ScrollViewer - DashboardView:UserControl 整体包 ScrollViewer(Vertical=Auto),窗口小时整体可滚
- SettingsView:内部 ScrollViewer(仅内容区可滚,导航 + 标题固定)
- AppsView:列表内 ScrollViewer(仅列表区可滚,标题 + TopN 固定)
- AboutView:不滚动
- ScrollBar 样式(
Themes/Styles.xaml):- 宽 8px、MinWidth 8px、Padding 2,0
- Track:透明
- Thumb:
StrokeStrongBrush圆角 4,默认 Opacity 0.55 → hover 0.9(150ms transition) - PageUp/PageDown 区域完全透明(不显示大箭头)
- 单元测试:
ProjectEye.Tests,每个 Service 至少 5 用例 - 覆盖率:业务代码 ≥ 70 %
- 可用
dotnet test --collect:"XPlat Code Coverage"验证
dotnet publish -c Release -r win-x64 -p:PublishSingleFile=true -p:SelfContained=true -p:PublishReadyToRun=true -p:EnableCompressionInSingleFile=false -p:IncludeNativeLibrariesForSelfExtract=false- 输出单文件
ProjectEye.exe;关闭 bundle 压缩与 native DLL 自解压,规避 SmartScreen / WDAC 误报
| 决策 | 原因 |
|---|---|
| WPF + .NET 10 | 本机环境已就绪、原生 Windows、低内存 |
| 应用使用只采进程名 | 隐私友好,符合个人工具需求 |
| 全屏可关闭计跳过 | 用户已确认 |
| 弱提醒走角落浮窗而非 Toast | 更及时可见 |
| 单一发布方式,不带安装器 | 简化首版交付 |
| 去 emoji + 自绘矢量 Path 图标 | 跨主题色一致性更好、随主题色风格统一 |
Accent 改 #3D6CD9(去 AI-蓝 + 降饱和) |
暖中性 utility 风格,避免紫罗兰泛化 |
弱提醒用 SetWindowRgn 而非 AllowsTransparency |
保持 Per-Monitor V2 DPI 兼容,不触发副屏黑洞 |
| 主窗不滚动 / 内容页自己决定 | 杜绝双滚动条;保证「标题 + TopN 永远在视口内」 |
| Heatmap 行/列 明确定义(4 周 / 4 季度 × 3 月) | 给出明确语义边界,密度均匀、不出现"孤角" |
| TopN 切换零抖动(固定 MinWidth + 8px 滚动条) | 可用性 + 视觉稳定 |
| ScrollBar 不引第三方库 | 0 包大小、100% 风格一致 |
周/月视图用 bool 属性(IsWeekView / IsMonthView)+ BoolToVisibility,不用 EnumBool 转 Visibility |
避免 WPF 自动转换器在 enum→bool→Visibility 链上静默失败导致双显示 |
进入周/月视图时,VM 在 LoadHeatmapAsync 末尾自动 SelectCell(today) / SelectCell(thisMonth) |
常驻详情卡始终有意义,无需手动点击 |
{ "intervalSeconds": 1200, "restSeconds": 20, "mode": "Fullscreen", // Fullscreen | Weak | Off "skipAllowed": true, "weakPlacement": "Primary", // Primary | Focused(保留枚举,当前实现忽略;弱提醒按所有屏必弹) "idleTimeoutSeconds": 120, "startWithWindows": true, "theme": "Auto", // Light | Dark | Auto "recordAppUsage": true, "appUsageSampleSeconds": 2, "dataFolder": "{exe 所在目录}\\data" }