Skip to content

Latest commit

 

History

History
263 lines (234 loc) · 15.5 KB

File metadata and controls

263 lines (234 loc) · 15.5 KB

ProjectEye — 产品规范说明 (SPEC)

版本:v1.1(UI 重构后)
目的:定义「用眼休息助手」产品的功能边界、性能基线、数据契约与界面规范,确保开发、测试、验收有据可依。


1. 产品定位

为长时间使用 Windows 电脑的用户提供低侵入式用眼休息管理。依据「20-20-20」准则(每 20 分钟看 20 英尺外的物体 20 秒)默认定时提醒,并提供使用统计、应用使用记录等可视化反馈。

2. 范围

2.1 In Scope

  • 定时提醒(强/弱模式)
  • 当日统计(工作时长、休息时长、休息次数、跳过次数)
  • 应用使用时长记录(前台进程名 + 时长,不记录窗口标题,不上传)
  • 应用使用历史窗口(今日 / 近 7 天 / 近 30 天 TopN)
  • 仪表盘历史(周:近 4 周热图 + 当日详情常驻 / 月:4 个季度 × 3 月热图 + 当月详情常驻)
  • 多显示器覆盖
  • 系统托盘常驻
  • 主题切换(浅/深/跟随系统)
  • 开机自启
  • 空闲检测自动暂停
  • 设置项可视化配置

2.2 Out of Scope

  • 屏幕锁定、Bios 中断、BlockInput
  • 联网、云同步、账号体系
  • 多语言(首版仅中文)
  • 鼠标/键盘宏、健康评估

3. 功能性需求 (Functional Requirements)

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(有数据)格子内文字全部用白色 文字与底色对比度足够

4. 非功能性需求 (Performance / UX)

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

5. 数据契约

5.1 Settings(JSON)

{
  "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"
}

5.2 SQLite Schema

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
);

5.3 C# Models(ProjectEye.Core)

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;
}

5.4 IAppUsageService 新增

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);
}

6. 接口契约(ProjectEye.Core)—— 与 v1.0 兼容,未变更的方法略

仅列相对 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);
}

7. 用户界面规范

7.1 主窗(默认 1100×780,最小 880×640)

  • 无系统标题栏WindowStyle=None + AllowsTransparency=True);自定义外框:圆角 16,hairline stroke
  • 标题栏 48px:左侧 Logo + 副标题;右侧最小化 + 关闭按钮(按钮图标 Path 必须放在 16×16 Grid 内 Stretch=Uniform 才真正居中)
  • 侧栏 232px:4 个 NavItem(左 3px accent 条带 + 图标 + 文字)
  • 内容区ContentPresenter 直接承载当前 View,不放外层 ScrollViewer(每个 View 内部自己决定)

7.2 全屏提醒 (每屏独立)

  • 深色背景 #131312 Alpha 0.9
  • 240×240 圆环 + 48px 描边,92pt mono 倒计时
  • 文案:"让眼睛休息一下" + "请远眺 6 米外的物体,持续 20 秒(20-20-20 原则)"
  • 按钮:"跳过本次"
  • 多显示器时每块屏各开一个独立窗口(Per-Monitor DPI 友好),第一个为主、其余设 Owner 绑定

7.3 弱提醒(右下角 320×116)

  • 8px 圆角卡片,纯 SurfaceElevatedBrush 底色,不用毛玻璃也不开 AllowsTransparency
  • HWND 形状通过 CreateRoundRectRgn + SetWindowRgn 切成圆角(绕过 Per-Monitor V2 + AllowsTransparency 的副屏渲染黑洞)
  • 监听 WM_DPICHANGED 自动按新 DPI 重切 region
  • 显示标题 + 倒计时(mono 字体)+ ✕ 跳过
  • 多显示器时每块屏右下角各弹一个

7.4 应用使用视图

  • 页面顶部: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 比例)

7.5 仪表盘视图

  • 页面顶部: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 描边

7.6 托盘菜单

  • 打开主窗(icon: eye)
  • 立即休息(icon: coffee)
  • · 暂停 30 / 60 / 120 分钟(icon: clock,全部跟菜单文字同色 16×16 Path,hover 与 Foreground 联动)
  • 退出(icon: x)

7.7 主题(升级自 v1.0)

  • LightSurface=#F7F7F5 / Elevated=#FFFFFF / Muted=#F0EFEC / Stroke=#E5E2DC / StrokeStrong=#CFCCC5 / Foreground=#1C1B1A / MutedText=#6F6E6B / Accent=#3D6CD9 / AccentHover=#2E58B8 / AccentSoft=#E5EDFB
  • DarkSurface=#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

7.8 滚动策略(v1.1 新增)

  • 主窗不滚动:内容区直接 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 区域完全透明(不显示大箭头)

8. 测试要求

  • 单元测试:ProjectEye.Tests,每个 Service 至少 5 用例
  • 覆盖率:业务代码 ≥ 70 %
  • 可用 dotnet test --collect:"XPlat Code Coverage" 验证

9. 发布

  • 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 误报

10. 风险与决策记录

决策 原因
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) 常驻详情卡始终有意义,无需手动点击