docs(V0.2): 新增设计文档 / 数据库设计文档 / 界面设计静态原型
- 设计文档.md: 架构与分层、目录结构、周期口径与边界规则、前端组件与交互、 后端接口清单、关键流程时序、非功能设计、测试验证、风险与演进 - 数据库设计文档.md: ER、5 张表逐字段、索引与外键级联、关键设计决策 (date 用 VARCHAR(10) 的原因 / 不冗余计数 / seq 幂等 / 归档语义 / 模板 JSON)、 容量估算、典型 SQL、迁移与回滚、V0.3 预留 - 界面设计.html: 420x820 静态高保真原型 8 屏 (打卡/日历/清单/新建弹窗/ 完成特效/明天计划视图/清单完成/空态) + 设计规范、组件状态表、动效与适配约定
This commit is contained in:
@@ -0,0 +1,325 @@
|
|||||||
|
# iBoard V0.2 数据库设计文档
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 版本 | V0.2 |
|
||||||
|
| 文档状态 | 已实现(与代码一致) |
|
||||||
|
| 数据库 | MariaDB / MySQL 8(Prisma `provider = "mysql"`) |
|
||||||
|
| 字符集 | `utf8mb4 / utf8mb4_unicode_ci` |
|
||||||
|
| 时区约定 | 应用层统一 `Asia/Shanghai`,库内 `DATETIME` 存 UTC |
|
||||||
|
| 相关文件 | `prisma/schema.prisma`、`prisma/migrations/20261022120000_add_checkin_checklist/migration.sql` |
|
||||||
|
| 关联文档 | `需求文档.md`、`设计文档.md`、`界面设计.html` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
V0.2 新增「打卡 / 日历 / 清单」三个功能域,共引入 **5 张表**:
|
||||||
|
|
||||||
|
| 表 | 作用 | 归属域 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Habit` | 打卡项(每天/每周/每月/每年 n 次的目标定义) | 打卡 |
|
||||||
|
| `CheckIn` | 打卡记录(一次打卡 = 一条) | 打卡 |
|
||||||
|
| `CheckList` | 检查清单(出差/回老家等场景的一次性清单) | 清单 |
|
||||||
|
| `CheckListItem` | 检查项(勾选即完成) | 清单 |
|
||||||
|
| `CheckListTemplate` | 清单模板(系统预置 + 用户自定义) | 清单 |
|
||||||
|
|
||||||
|
日历视图**不新增表**:完全由 `Habit` + `CheckIn` 按月聚合得出(见 §7.3)。
|
||||||
|
|
||||||
|
### 1.1 设计原则
|
||||||
|
|
||||||
|
1. **记录为真,进度不落库。** 环形进度、完成率、streak 一律由 `CheckIn` 现场聚合,不在 `Habit` 上冗余 `doneCount` 字段——避免并发计数不一致、以及用户中途把「每周 3 次」改成「每周 5 次」后需要回填历史的问题。
|
||||||
|
2. **日期以本地日历日字符串存储。** `CheckIn.date` 用 `VARCHAR(10)` 存 `"YYYY-MM-DD"`(Asia/Shanghai 下的日历日),而不是 `DATETIME`:范围查询走索引、字典序等于时间序、天然幂等边界,且彻底规避「UTC 存 + 本地读」导致的跨零点归属错误。
|
||||||
|
3. **软删除优先。** 打卡项删除默认走 `archived` 归档(保留历史统计),只有显式 `?hard=1` 才物理删除。
|
||||||
|
4. **一切按 `userId` 隔离。** 所有业务表都带 `userId`,`Habit`/`CheckIn`/`CheckList`/`CheckListTemplate` 外键 `ON DELETE CASCADE` 到 `User`。
|
||||||
|
5. **迁移只增不改。** 新增需求一律新迁移文件,不修改已执行的迁移(部署走 `prisma migrate deploy`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 概念模型(ER)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┐
|
||||||
|
│ User │ (既有表, 本次仅新增反向关系)
|
||||||
|
└────┬─────┘
|
||||||
|
┌───────────────┼────────────────────┬─────────────────┐
|
||||||
|
│ │ │ │
|
||||||
|
┌────▼────┐ ┌────▼────┐ ┌─────▼─────┐ ┌──────▼──────┐
|
||||||
|
│ Habit │1───*│ CheckIn │ │ CheckList │1─* │CheckListItem│
|
||||||
|
└─────────┘ └─────────┘ └───────────┘ └─────────────┘
|
||||||
|
打卡项 打卡记录 场景清单 检查项
|
||||||
|
(周期+目标n) (habitId,date,seq) (done, doneAt)
|
||||||
|
▲
|
||||||
|
│ 生成(复制条目, 不建外键)
|
||||||
|
┌─────┴──────────┐
|
||||||
|
│CheckListTemplate│
|
||||||
|
│ (builtin/自定义) │
|
||||||
|
└────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
基数说明:
|
||||||
|
|
||||||
|
| 关系 | 基数 | 级联 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| User → Habit | 1 : N | Cascade |
|
||||||
|
| User → CheckIn | 1 : N | Cascade |
|
||||||
|
| Habit → CheckIn | 1 : N | Cascade |
|
||||||
|
| User → CheckList | 1 : N | Cascade |
|
||||||
|
| CheckList → CheckListItem | 1 : N | Cascade |
|
||||||
|
| User → CheckListTemplate | 1 : N(可空,系统预置为空) | Cascade |
|
||||||
|
| CheckListTemplate → CheckList | 无外键,创建时**复制**条目内容 | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 表结构
|
||||||
|
|
||||||
|
### 3.1 `Habit` — 打卡项
|
||||||
|
|
||||||
|
周期目标的**定义**表,不含任何计数。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 空 | 默认 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `id` | INT PK | 否 | AUTO_INCREMENT | 主键 |
|
||||||
|
| `userId` | INT FK | 否 | — | 所属用户 |
|
||||||
|
| `name` | VARCHAR(191) | 否 | — | 名称,接口校验 1~20 字 |
|
||||||
|
| `period` | VARCHAR(191) | 否 | `'day'` | `day` \| `week` \| `month` \| `year` |
|
||||||
|
| `target` | INT | 否 | `1` | 周期内目标次数 n(校验 ≥1) |
|
||||||
|
| `color` | VARCHAR(191) | 否 | `'#1677ff'` | 环形与标记主色(7 色预设之一) |
|
||||||
|
| `icon` | VARCHAR(191) | 否 | `''` | emoji / 图标名,**V0.2 预留不展示** |
|
||||||
|
| `note` | VARCHAR(191) | 否 | `''` | 备注 |
|
||||||
|
| `archived` | BOOLEAN | 否 | `false` | 归档后不出现在待办,历史保留 |
|
||||||
|
| `sortOrder` | INT | 否 | `0` | 排序权重(升序) |
|
||||||
|
| `createdAt` | DATETIME(3) | 否 | now() | 用于「创建之前的日期不计入月历期望」 |
|
||||||
|
| `updatedAt` | DATETIME(3) | 否 | 自动 | — |
|
||||||
|
|
||||||
|
索引:`INDEX(userId, archived, sortOrder)` — 覆盖「取当前用户未归档打卡项」主查询。
|
||||||
|
|
||||||
|
### 3.2 `CheckIn` — 打卡记录
|
||||||
|
|
||||||
|
**一次打卡 = 一条记录**,同日多次用 `seq` 递增。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 空 | 默认 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `id` | INT PK | 否 | AUTO_INCREMENT | 主键 |
|
||||||
|
| `habitId` | INT FK | 否 | — | 所属打卡项 |
|
||||||
|
| `userId` | INT FK | 否 | — | 冗余用户(用于单用户全量区间查询,避免 join) |
|
||||||
|
| `date` | VARCHAR(10) | 否 | — | 本地日历日 `"YYYY-MM-DD"` |
|
||||||
|
| `seq` | INT | 否 | `1` | 当日第几次(1..n,超额继续累加) |
|
||||||
|
| `note` | VARCHAR(191) | 否 | `''` | 单次备注(接口截断 200 字) |
|
||||||
|
| `source` | VARCHAR(191) | 否 | `'user'` | `user` \| `backfill`(补打) |
|
||||||
|
| `createdAt` | DATETIME(3) | 否 | now() | 打卡时刻(UTC) |
|
||||||
|
|
||||||
|
约束与索引:
|
||||||
|
|
||||||
|
| 名称 | 类型 | 字段 | 作用 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `CheckIn_habitId_date_seq_key` | **UNIQUE** | `(habitId, date, seq)` | 并发重复提交的幂等防线(冲突 → P2002 → 409) |
|
||||||
|
| `CheckIn_userId_date_idx` | INDEX | `(userId, date)` | 月历按月区间扫全量记录 |
|
||||||
|
| `CheckIn_habitId_date_idx` | INDEX | `(habitId, date)` | 单打卡项周期区间聚合 |
|
||||||
|
|
||||||
|
### 3.3 `CheckList` — 检查清单
|
||||||
|
|
||||||
|
| 字段 | 类型 | 空 | 默认 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `id` | INT PK | 否 | AUTO_INCREMENT | 主键 |
|
||||||
|
| `userId` | INT FK | 否 | — | 所属用户 |
|
||||||
|
| `title` | VARCHAR(191) | 否 | — | 清单名,如「出差 · 杭州」 |
|
||||||
|
| `scene` | VARCHAR(191) | 否 | `''` | 场景:`出差` \| `回老家` \| `其他` |
|
||||||
|
| `note` | VARCHAR(191) | 否 | `''` | 备注 |
|
||||||
|
| `archived` | BOOLEAN | 否 | `false` | 归档 |
|
||||||
|
| `createdAt` / `updatedAt` | DATETIME(3) | 否 | — | 列表默认按 `updatedAt` 倒序 |
|
||||||
|
|
||||||
|
索引:`INDEX(userId, archived)` — 对应「进行中 / 已归档」两个分段列表。
|
||||||
|
|
||||||
|
### 3.4 `CheckListItem` — 检查项
|
||||||
|
|
||||||
|
| 字段 | 类型 | 空 | 默认 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `id` | INT PK | 否 | AUTO_INCREMENT | 主键 |
|
||||||
|
| `listId` | INT FK | 否 | — | 所属清单 |
|
||||||
|
| `title` | VARCHAR(191) | 否 | — | 条目名(校验 ≤40 字) |
|
||||||
|
| `note` | VARCHAR(191) | 否 | `''` | 条目备注 |
|
||||||
|
| `done` | BOOLEAN | 否 | `false` | 是否已完成 |
|
||||||
|
| `doneAt` | DATETIME(3) | **是** | NULL | 勾选时刻;取消勾选置回 NULL |
|
||||||
|
| `sortOrder` | INT | 否 | `0` | 排序(升序,同值按 id) |
|
||||||
|
| `createdAt` / `updatedAt` | DATETIME(3) | 否 | — | — |
|
||||||
|
|
||||||
|
索引:`INDEX(listId, sortOrder)`。
|
||||||
|
|
||||||
|
### 3.5 `CheckListTemplate` — 清单模板
|
||||||
|
|
||||||
|
| 字段 | 类型 | 空 | 默认 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `id` | INT PK | 否 | AUTO_INCREMENT | 主键 |
|
||||||
|
| `userId` | INT FK | **是** | NULL | NULL = 系统预置(`builtin=true`) |
|
||||||
|
| `name` | VARCHAR(191) | 否 | — | 模板名 |
|
||||||
|
| `scene` | VARCHAR(191) | 否 | `''` | 场景标签 |
|
||||||
|
| `items` | **JSON** | 否 | — | `[{ title, note }]`,创建清单时展开为 `CheckListItem` |
|
||||||
|
| `builtin` | BOOLEAN | 否 | `false` | 系统预置:只读、不可删(接口层限制) |
|
||||||
|
| `createdAt` / `updatedAt` | DATETIME(3) | 否 | — | — |
|
||||||
|
|
||||||
|
索引:`INDEX(userId, builtin)` — 查询条件为 `builtin=true OR userId=me`。
|
||||||
|
|
||||||
|
系统预置数据(首次访问 `GET /api/checklists/templates` 时**幂等写入**):
|
||||||
|
|
||||||
|
| 模板 | 条目数 | 条目 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 出差 | 11 | 身份证/证件、手机充电器+充电宝、笔记本电脑+电源、耳机、换洗衣物、洗漱包、常用药、雨伞、车票/机票/酒店确认、工牌/名片、报销单据 |
|
||||||
|
| 回老家 | 12 | 身份证/证件、钥匙、手机充电器+充电宝、换洗衣物、洗漱包、常用药、给家人的礼物/伴手礼、长辈的药品/营养品、口罩、现金零钱、车票/加油/行程确认、家里门窗水电检查 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 外键与级联
|
||||||
|
|
||||||
|
| 子表.字段 | 父表.字段 | ON DELETE | ON UPDATE |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `Habit.userId` | `User.id` | CASCADE | CASCADE |
|
||||||
|
| `CheckIn.habitId` | `Habit.id` | CASCADE | CASCADE |
|
||||||
|
| `CheckIn.userId` | `User.id` | CASCADE | CASCADE |
|
||||||
|
| `CheckList.userId` | `User.id` | CASCADE | CASCADE |
|
||||||
|
| `CheckListItem.listId` | `CheckList.id` | CASCADE | CASCADE |
|
||||||
|
| `CheckListTemplate.userId` | `User.id` | CASCADE | CASCADE |
|
||||||
|
|
||||||
|
删除链路:删用户 → 级联清 Habit/CheckIn/CheckList/Template;删 Habit → 清其全部 CheckIn;删 CheckList → 清其全部 Item。业务层删打卡项默认只置 `archived=true`,物理删需 `?hard=1`(此时先 `deleteMany` 记录再删主表)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 关键设计决策
|
||||||
|
|
||||||
|
### 5.1 为什么 `date` 用 `VARCHAR(10)` 而不是 `DATETIME`
|
||||||
|
|
||||||
|
| 维度 | VARCHAR(10) 本地日历日 | DATETIME(UTC) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 跨零点归属 | 明确:由应用层按 Asia/Shanghai 计算一次,落库即定 | 读时需要再转时区,易错 |
|
||||||
|
| 范围查询 | `date >= '2026-10-01' AND date <= '2026-10-31'` 直接走索引 | 需函数/边界换算,易失效 |
|
||||||
|
| 幂等边界 | `UNIQUE(habitId, date, seq)` 天然按「日」收敛 | 需按日截取 |
|
||||||
|
| 字典序 | 等于时间序,可安全字符串比较 | — |
|
||||||
|
| 存储 | 10 字节 | 5 字节(略优,但不足以抵消上述成本) |
|
||||||
|
|
||||||
|
代价:跨时区展示需自行换算——本项目全链路 Asia/Shanghai,不成为问题。
|
||||||
|
|
||||||
|
### 5.2 为什么不冗余 `doneCount` / `streak`
|
||||||
|
|
||||||
|
- **并发**:计数需事务 + 行锁,聚合查询天然一致。
|
||||||
|
- **改配置**:用户把「每周 3 次」改成「每周 5 次」,冗余计数立刻失真,需要全量重算;现方案改 `target` 立即生效。
|
||||||
|
- **撤销**:撤销即删一条记录,无需维护计数器。
|
||||||
|
- **成本**:单个用户打卡项数量在 10~30 量级,月历查询一次拉取整月记录(≤ 数百行)在内存聚合,开销可接受。
|
||||||
|
|
||||||
|
### 5.3 `seq` 与幂等
|
||||||
|
|
||||||
|
同日多次打卡用 `seq = max(seq)+1`。唯一约束 `(habitId, date, seq)` 使并发重复提交必然撞 P2002,服务端转 409「该次打卡已记录」,前端幂等收敛,不会出现「点两下加两次」的体验事故。
|
||||||
|
|
||||||
|
### 5.4 归档 vs 物理删除
|
||||||
|
|
||||||
|
| 操作 | 语义 | 数据影响 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `DELETE /api/checkin/habits/:id` | 归档(默认) | `archived=true`,记录全保留 |
|
||||||
|
| `DELETE /api/checkin/habits/:id?hard=1` | 物理删除 | 先删 CheckIn,再删 Habit |
|
||||||
|
| `DELETE /api/checklists/:id` | 物理删除(一次性清单,不留历史) | 先删 Item,再删 List |
|
||||||
|
|
||||||
|
理由:打卡是长期行为数据,历史构成 streak 与完成率;清单是短期事务,用完即归档/删除。
|
||||||
|
|
||||||
|
### 5.5 模板用 JSON 而非明细表
|
||||||
|
|
||||||
|
模板条目结构固定(`title` + `note`)、只在「创建清单」时整体读取并展开、不需要按条目检索或统计 —— 用 `JSON` 列避免额外的 `TemplateItem` 表与 join。展开后落到 `CheckListItem`,之后模板改动不影响已生成清单。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 数据量估算
|
||||||
|
|
||||||
|
单用户规模假设:打卡项 20 个,平均每天打卡 25 次;清单每月 4 份,每份 12 条。
|
||||||
|
|
||||||
|
| 表 | 年增量(单用户) | 行大小(约) | 年占用(约) |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `CheckIn` | 25 × 365 ≈ 9,100 | ~80 B | ~0.7 MB |
|
||||||
|
| `CheckListItem` | 4 × 12 × 12 = 576 | ~90 B | ~0.05 MB |
|
||||||
|
| `Habit` / `CheckList` / `CheckListTemplate` | < 100 | ~200 B | 忽略 |
|
||||||
|
|
||||||
|
结论:**无需分区、无需归档策略**。10 用户 × 5 年 ≈ 45 万行 CheckIn,仍远低于单表瓶颈。若未来接入多租户或做年视图长周期统计,再考虑按月汇总表。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 典型查询
|
||||||
|
|
||||||
|
### 7.1 今日待办 + 周期进度
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 步骤 1: 取未归档打卡项 (走 Habit_userId_archived_sortOrder_idx)
|
||||||
|
SELECT id, name, period, target, color FROM Habit
|
||||||
|
WHERE userId = ? AND archived = 0 ORDER BY sortOrder, id;
|
||||||
|
|
||||||
|
-- 步骤 2: 取覆盖所有周期窗口的最小区间内的记录 (走 CheckIn_userId_date_idx)
|
||||||
|
SELECT habitId, date FROM CheckIn
|
||||||
|
WHERE userId = ? AND date >= ? AND date <= ?; -- 如 week: '2026-10-19' ~ '2026-10-25'
|
||||||
|
-- 步骤 3: 内存按 habitId/date 分组 -> 计算 done/percent/streak
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 打一次卡
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT MAX(seq) FROM CheckIn WHERE habitId = ? AND date = ?; -- -> seq
|
||||||
|
INSERT INTO CheckIn (habitId, userId, date, seq, note, source) VALUES (?, ?, ?, ?, ?, 'user');
|
||||||
|
-- 冲突 -> P2002 -> 409
|
||||||
|
```
|
||||||
|
|
||||||
|
撤销(撤销本周期最近一次):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT id FROM CheckIn WHERE habitId = ? AND date BETWEEN ? AND ?
|
||||||
|
ORDER BY date DESC, seq DESC LIMIT 1;
|
||||||
|
DELETE FROM CheckIn WHERE id = ?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 月历(日历视图数据来源)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT habitId, date FROM CheckIn
|
||||||
|
WHERE userId = ? AND date >= '2026-10-01' AND date <= '2026-10-31';
|
||||||
|
```
|
||||||
|
|
||||||
|
按天聚合后与 `Habit.expectedPerDay` 比较得出日状态:
|
||||||
|
|
||||||
|
| 状态 | 条件 | 展示 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `full` | 全部打卡项当日次数 ≥ 日均期望 | 绿点 |
|
||||||
|
| `over` | 有任一打卡项当日次数 > 日均期望 | 紫点 |
|
||||||
|
| `partial` | 有完成但未全完成 | 黄点 |
|
||||||
|
| `empty` | 无记录或当天无打卡项 | 无点 |
|
||||||
|
|
||||||
|
`Habit.createdAt` 早于该日的才计入期望,避免新建打卡项把历史日期标成「未完成」。
|
||||||
|
|
||||||
|
### 7.4 清单列表(带进度)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT l.*, i.* FROM CheckList l
|
||||||
|
LEFT JOIN CheckListItem i ON i.listId = l.id
|
||||||
|
WHERE l.userId = ? AND l.archived = ?
|
||||||
|
ORDER BY l.updatedAt DESC, i.sortOrder, i.id;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 迁移
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 迁移目录 | `prisma/migrations/20261022120000_add_checkin_checklist/` |
|
||||||
|
| 内容 | 5 张 `CREATE TABLE` + 6 条 `ADD CONSTRAINT`(见 `migration.sql`) |
|
||||||
|
| 生成方式 | 手写 SQL(与 `prisma migrate diff` 产出格式一致),已通过 `prisma migrate deploy` 验证 |
|
||||||
|
| 部署命令 | `npx prisma migrate deploy && npx prisma generate` |
|
||||||
|
| 回滚 | 无自动回滚。应急:`DROP TABLE CheckIn, CheckListItem, CheckList, CheckListTemplate, Habit;`(注意外键顺序)并把 `_prisma_migrations` 中该记录删除 |
|
||||||
|
|
||||||
|
>V0.2 全为新增表与新增字段(`User` 侧仅新增 Prisma 反向关系,不产生 DDL),对既有数据零影响。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. V0.3 预留
|
||||||
|
|
||||||
|
| 方向 | 可能的表变更 |
|
||||||
|
| --- | --- |
|
||||||
|
| 打卡提醒 | `Habit` 增加 `remindAt TIME`、`remindDays` |
|
||||||
|
| 打卡统计快照 | 新增 `HabitDailyStat(userId, date, done, expected)` 汇总表,用于年视图/热力图加速 |
|
||||||
|
| 清单共享协作 | 新增 `CheckListShare(listId, userId, role)` |
|
||||||
|
| 模板市场 | `CheckListTemplate` 增加 `visibility`、`useCount` |
|
||||||
|
|
||||||
|
以上均可通过「新增表 / 新增可空字段」完成,不影响 V0.2 数据结构。
|
||||||
@@ -0,0 +1,889 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>iBoard V0.2 界面设计 · 打卡 / 日历 / 清单</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#f0f2f5; --page:#ffffff; --card:#ffffff;
|
||||||
|
--text:#333333; --muted:#666666; --weak:#999999; --line:#eeeeee;
|
||||||
|
--primary:#1677ff; --success:#00b578; --warn:#ffc53d; --over:#a855f7;
|
||||||
|
--danger:#ff3141; --orange:#ff8f1f; --teal:#14b8a6; --pink:#f43f9e;
|
||||||
|
--nav-h:45px; --tab-h:72px;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{
|
||||||
|
background:var(--bg); color:#1f2328; font-size:14px; line-height:1.6;
|
||||||
|
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Hiragino Sans GB','Microsoft YaHei',Roboto,sans-serif;
|
||||||
|
-webkit-font-smoothing:antialiased;
|
||||||
|
}
|
||||||
|
.wrap{max-width:1400px;margin:0 auto;padding:36px 24px 72px}
|
||||||
|
.doc-head{border-bottom:1px solid #e3e6ea;padding-bottom:20px;margin-bottom:28px}
|
||||||
|
.doc-head h1{font-size:26px;font-weight:700;letter-spacing:.5px}
|
||||||
|
.doc-head p{color:var(--muted);font-size:13px;margin-top:6px}
|
||||||
|
.meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
|
||||||
|
.meta span{font-size:12px;background:#fff;border:1px solid #e3e6ea;border-radius:999px;padding:3px 12px;color:var(--muted)}
|
||||||
|
h2{font-size:19px;font-weight:700;margin:40px 0 6px;display:flex;align-items:center;gap:10px}
|
||||||
|
h2::before{content:"";width:4px;height:17px;background:var(--primary);border-radius:2px}
|
||||||
|
h2 .sub{font-size:12px;font-weight:400;color:var(--weak)}
|
||||||
|
h3{font-size:15px;font-weight:700;margin:22px 0 10px;color:#24292f}
|
||||||
|
p.desc{color:var(--muted);font-size:13px;margin-bottom:16px}
|
||||||
|
|
||||||
|
/* ---------- 设计规范 ---------- */
|
||||||
|
.tokens{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:18px}
|
||||||
|
.panel{background:#fff;border:1px solid #e6e8eb;border-radius:12px;padding:16px 18px}
|
||||||
|
.panel h4{font-size:13px;color:var(--muted);font-weight:600;margin-bottom:12px;letter-spacing:.4px}
|
||||||
|
.swatches{display:flex;flex-wrap:wrap;gap:10px}
|
||||||
|
.sw{width:88px;text-align:center}
|
||||||
|
.sw i{display:block;height:44px;border-radius:8px;border:1px solid rgba(0,0,0,.06)}
|
||||||
|
.sw b{display:block;font-size:11px;font-weight:600;margin-top:5px}
|
||||||
|
.sw em{display:block;font-size:10px;font-style:normal;color:var(--weak)}
|
||||||
|
.kv{display:grid;grid-template-columns:96px 1fr;gap:6px 12px;font-size:12px}
|
||||||
|
.kv span{color:var(--weak)}
|
||||||
|
.kv b{font-weight:600}
|
||||||
|
.type-row{display:flex;align-items:baseline;gap:12px;padding:6px 0;border-bottom:1px dashed var(--line);font-size:12px}
|
||||||
|
.type-row:last-child{border-bottom:none}
|
||||||
|
.type-row em{font-style:normal;color:var(--weak);width:132px;flex-shrink:0}
|
||||||
|
.type-row span{color:#444}
|
||||||
|
|
||||||
|
/* ---------- 手机 ---------- */
|
||||||
|
.stage{display:grid;grid-template-columns:repeat(auto-fit,minmax(420px,1fr));gap:30px 26px;justify-items:center}
|
||||||
|
.shot{width:420px}
|
||||||
|
.phone{
|
||||||
|
width:420px;height:820px;background:var(--page);position:relative;overflow:hidden;
|
||||||
|
border:1px solid #d8dde3;border-radius:22px;box-shadow:0 10px 24px rgba(15,23,42,.08);
|
||||||
|
display:flex;flex-direction:column;
|
||||||
|
}
|
||||||
|
.phone::after{content:"";position:absolute;top:8px;left:50%;transform:translateX(-50%);width:118px;height:20px;background:#111827;border-radius:12px;opacity:.9}
|
||||||
|
.navbar{height:var(--nav-h);margin-top:28px;flex-shrink:0;display:flex;align-items:center;justify-content:center;
|
||||||
|
font-size:17px;font-weight:600;border-bottom:1px solid var(--line);background:#fff}
|
||||||
|
.body{flex:1;min-height:0;overflow:hidden;background:var(--bg);padding:0 0 8px}
|
||||||
|
.tabbar{height:var(--tab-h);flex-shrink:0;background:#fff;border-top:1px solid var(--line);
|
||||||
|
display:flex;align-items:flex-start;padding-top:7px}
|
||||||
|
.tab{flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;color:var(--weak);font-size:11px}
|
||||||
|
.tab.on{color:var(--primary)}
|
||||||
|
.tab svg{width:23px;height:23px}
|
||||||
|
.caption{margin-top:10px;background:#fff;border:1px solid #e6e8eb;border-radius:10px;padding:10px 14px}
|
||||||
|
.caption b{font-size:13px}
|
||||||
|
.caption p{font-size:12px;color:var(--muted);margin-top:3px}
|
||||||
|
.tag{display:inline-block;font-size:11px;color:var(--primary);background:#f0f5ff;border-radius:4px;padding:1px 6px;margin-left:6px}
|
||||||
|
|
||||||
|
/* ---------- 打卡视图 ---------- */
|
||||||
|
.habit-head{display:flex;justify-content:space-between;align-items:flex-end;padding:12px 14px 8px}
|
||||||
|
.habit-date{font-size:18px;font-weight:700}
|
||||||
|
.habit-summary{font-size:12px;color:var(--muted);margin-top:2px}
|
||||||
|
.habit-summary strong{color:var(--primary);font-size:14px}
|
||||||
|
.badge{font-size:11px;color:#ad6800;background:#fff7e6;border-radius:4px;padding:1px 5px;margin-left:6px}
|
||||||
|
.mini-btn{font-size:11px;border:1px solid var(--primary);color:var(--primary);border-radius:999px;padding:3px 10px;background:#fff}
|
||||||
|
.mini-btn.solid{background:var(--primary);color:#fff;border-color:var(--primary)}
|
||||||
|
.mini-btn.grey{border-color:#dcdcdc;color:var(--muted)}
|
||||||
|
.head-right{display:flex;gap:6px}
|
||||||
|
.habit-list{padding:0 12px;display:flex;flex-direction:column;gap:10px}
|
||||||
|
.habit-card{position:relative;background:#fff;border-radius:10px;padding:12px;box-shadow:0 1px 2px rgba(0,0,0,.04);
|
||||||
|
display:grid;grid-template-columns:auto 1fr auto;grid-template-areas:"ring main actions" "foot foot foot";gap:8px 12px}
|
||||||
|
.habit-card.done{background:#f7fbf8}
|
||||||
|
.ring{grid-area:ring;width:58px;height:58px;position:relative;display:flex;align-items:center;justify-content:center}
|
||||||
|
.ring svg{display:block}
|
||||||
|
.ring-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:16px;font-weight:700}
|
||||||
|
.ring-center em{font-style:normal;font-size:10px;color:var(--muted);font-weight:400}
|
||||||
|
.ring-over{position:absolute;top:-4px;right:-10px;font-size:10px;color:#fff;background:var(--orange);border-radius:999px;padding:0 5px}
|
||||||
|
.habit-main{grid-area:main;min-width:0}
|
||||||
|
.habit-name{font-size:15px;font-weight:600}
|
||||||
|
.habit-meta{font-size:12px;color:var(--muted);margin-top:3px}
|
||||||
|
.streak{margin-left:8px;color:var(--orange);background:rgba(255,143,31,.12);border-radius:4px;padding:1px 6px}
|
||||||
|
.habit-range{font-size:11px;color:var(--muted);margin-top:3px}
|
||||||
|
.over-txt{margin-left:8px;color:var(--orange)}
|
||||||
|
.habit-actions{grid-area:actions;display:flex;align-items:center}
|
||||||
|
.habit-foot{grid-area:foot;display:flex;gap:14px;padding-top:8px;border-top:1px dashed var(--line)}
|
||||||
|
.link{font-size:12px;color:var(--primary)}
|
||||||
|
.link.danger{color:var(--danger)}
|
||||||
|
|
||||||
|
/* ---------- 日历视图 ---------- */
|
||||||
|
.cal-month-bar{display:flex;align-items:center;gap:10px;padding:12px 14px 6px}
|
||||||
|
.cal-month{font-size:16px;font-weight:700}
|
||||||
|
.cal-arrow{font-size:20px;color:var(--muted);padding:0 4px;line-height:1}
|
||||||
|
.cal-today{margin-left:auto;font-size:12px;color:var(--primary)}
|
||||||
|
.seg{display:flex;margin:4px 14px 10px;background:#f2f3f5;border-radius:8px;padding:3px}
|
||||||
|
.seg div{flex:1;text-align:center;font-size:13px;padding:5px 0;border-radius:6px;color:var(--muted)}
|
||||||
|
.seg div.on{background:#fff;color:var(--text);font-weight:600;box-shadow:0 1px 2px rgba(0,0,0,.06)}
|
||||||
|
.cal-grid{margin:0 12px;background:#fff;border-radius:10px;padding:8px 6px 10px;box-shadow:0 1px 2px rgba(0,0,0,.04)}
|
||||||
|
.grid7{display:grid;grid-template-columns:repeat(7,1fr)}
|
||||||
|
.wd span{text-align:center;font-size:11px;color:var(--muted);padding:4px 0}
|
||||||
|
.day{display:flex;flex-direction:column;align-items:center;justify-content:flex-start;padding:5px 0;border-radius:8px}
|
||||||
|
.day .num{font-size:13px}
|
||||||
|
.day.out .num{color:#c8c8c8}
|
||||||
|
.day.future .num{color:#b0b0b0}
|
||||||
|
.day.today .num{color:var(--primary);font-weight:700}
|
||||||
|
.day.sel{background:var(--primary)}
|
||||||
|
.day.sel .num{color:#fff;font-weight:600}
|
||||||
|
.dot{width:5px;height:5px;border-radius:50%;margin-top:3px}
|
||||||
|
.dot.partial{background:var(--warn)} .dot.full{background:var(--success)} .dot.over{background:var(--over)}
|
||||||
|
.day.sel .dot{background:#fff}
|
||||||
|
.cal-rate{font-size:12px;color:var(--muted);padding:8px 14px}
|
||||||
|
.cal-rate strong{color:var(--primary)}
|
||||||
|
.cal-detail-title{font-size:14px;font-weight:600;padding:6px 14px 8px}
|
||||||
|
.cal-item{display:flex;align-items:center;gap:8px;background:#fff;border-radius:8px;padding:10px 12px;margin:0 12px 8px;box-shadow:0 1px 2px rgba(0,0,0,.04)}
|
||||||
|
.cal-item.done{background:#f7fbf8}
|
||||||
|
.cdot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
||||||
|
.cname{font-size:14px;font-weight:600}
|
||||||
|
.cmeta{font-size:11px;color:var(--muted);flex:1;text-align:right}
|
||||||
|
.cplan{font-size:12px;color:var(--muted)}
|
||||||
|
|
||||||
|
/* ---------- 清单视图 ---------- */
|
||||||
|
.list-head{display:flex;align-items:center;gap:8px;padding:12px 14px 10px}
|
||||||
|
.list-head .seg{flex:1;margin:0}
|
||||||
|
.checklist-card{background:#fff;border-radius:10px;padding:12px;margin:0 12px 10px;box-shadow:0 1px 2px rgba(0,0,0,.04)}
|
||||||
|
.checklist-card.done{background:#f7fbf8}
|
||||||
|
.cl-title-row{display:flex;align-items:center;gap:8px}
|
||||||
|
.cl-title{font-size:15px;font-weight:600}
|
||||||
|
.cl-scene{font-size:11px;color:var(--primary);background:#f0f5ff;border-radius:4px;padding:1px 6px}
|
||||||
|
.progress-row{display:flex;align-items:center;gap:8px;margin-top:8px}
|
||||||
|
.bar{flex:1;height:6px;border-radius:3px;background:#f0f0f0;overflow:hidden}
|
||||||
|
.bar i{display:block;height:100%;border-radius:3px;background:var(--success)}
|
||||||
|
.count{font-size:12px;color:var(--muted)}
|
||||||
|
.arrow{color:var(--muted);transform:rotate(90deg)}
|
||||||
|
.cl-body{margin-top:10px;padding-top:8px;border-top:1px dashed var(--line)}
|
||||||
|
.cl-item{display:flex;align-items:center;gap:8px;padding:7px 0}
|
||||||
|
.box{width:20px;height:20px;border-radius:50%;border:1.5px solid #d0d0d0;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||||
|
.cl-item.done .box{background:var(--success);border-color:var(--success)}
|
||||||
|
.box svg{opacity:0} .cl-item.done .box svg{opacity:1}
|
||||||
|
.cl-item-title{flex:1;font-size:14px}
|
||||||
|
.cl-item.done .cl-item-title{color:#a0a0a0;text-decoration:line-through}
|
||||||
|
.cl-del{color:#ccc;font-size:18px;line-height:1;padding:0 4px}
|
||||||
|
.cl-add{display:flex;gap:8px;margin-top:8px}
|
||||||
|
.cl-add .inp{flex:1;border:1px solid var(--line);border-radius:6px;padding:6px 8px;font-size:13px;color:var(--weak);background:#fff}
|
||||||
|
.cl-actions{display:flex;flex-wrap:wrap;gap:12px;margin-top:10px}
|
||||||
|
|
||||||
|
/* ---------- 弹窗 ---------- */
|
||||||
|
.mask{position:absolute;inset:28px 0 0 0;background:rgba(0,0,0,.45);z-index:9}
|
||||||
|
.sheet{position:absolute;left:0;right:0;bottom:0;background:#fff;border-radius:12px 12px 0 0;padding:16px 16px 12px;z-index:10;max-height:560px;overflow:hidden}
|
||||||
|
.sheet-title{font-size:16px;font-weight:700;margin-bottom:12px}
|
||||||
|
.field{margin-bottom:13px}
|
||||||
|
.field label{display:block;font-size:12px;color:var(--muted);margin-bottom:6px}
|
||||||
|
.inp{border:1px solid var(--line);border-radius:8px;padding:8px 10px;font-size:14px;background:#fff;color:var(--text)}
|
||||||
|
.inp.ph{color:#b8bcc4}
|
||||||
|
.chips{display:flex;gap:8px}
|
||||||
|
.chip{flex:1;text-align:center;font-size:13px;padding:6px 0;border-radius:8px;background:#f5f6f8;color:var(--muted)}
|
||||||
|
.chip.on{background:var(--primary);color:#fff;font-weight:600}
|
||||||
|
.stepper{display:inline-flex;align-items:center;gap:10px}
|
||||||
|
.stepper span{width:28px;height:28px;border-radius:6px;background:#f5f6f8;display:flex;align-items:center;justify-content:center;color:var(--muted)}
|
||||||
|
.stepper span.act{background:var(--primary);color:#fff}
|
||||||
|
.stepper b{font-size:16px;min-width:24px;text-align:center}
|
||||||
|
.colors{display:flex;gap:10px;flex-wrap:wrap}
|
||||||
|
.cd{width:26px;height:26px;border-radius:50%;border:2px solid transparent}
|
||||||
|
.cd.on{border-color:rgba(0,0,0,.25);transform:scale(1.12)}
|
||||||
|
.btn-primary{display:block;text-align:center;background:var(--primary);color:#fff;border-radius:8px;padding:10px 0;font-size:15px;font-weight:600;margin-top:18px}
|
||||||
|
.btn-ghost{display:block;text-align:center;border:1px solid var(--line);border-radius:8px;padding:9px 0;font-size:15px;color:var(--muted);margin-top:8px;background:#fff}
|
||||||
|
|
||||||
|
/* ---------- 特效 ---------- */
|
||||||
|
.fx{position:absolute;inset:0;pointer-events:none}
|
||||||
|
.fx .halo{position:absolute;left:50%;top:50%;width:88px;height:88px;margin:-44px 0 0 -44px;border-radius:50%;border:2px solid var(--success);opacity:.55}
|
||||||
|
.fx .halo2{width:108px;height:108px;margin:-54px 0 0 -54px;opacity:.25}
|
||||||
|
.fx i{position:absolute;left:50%;top:50%;width:6px;height:6px;border-radius:50%;}
|
||||||
|
|
||||||
|
/* ---------- 空态 ---------- */
|
||||||
|
.empty{text-align:center;padding:70px 20px;color:var(--weak)}
|
||||||
|
.empty .ico{width:64px;height:64px;margin:0 auto 12px;border-radius:50%;background:#f2f3f5;display:flex;align-items:center;justify-content:center;color:#c4c8ce;font-size:30px}
|
||||||
|
.empty p{font-size:13px;margin-bottom:12px}
|
||||||
|
|
||||||
|
/* ---------- 表格 ---------- */
|
||||||
|
table{width:100%;border-collapse:collapse;background:#fff;border:1px solid #e6e8eb;border-radius:10px;overflow:hidden;font-size:13px}
|
||||||
|
th,td{border-bottom:1px solid var(--line);padding:9px 12px;text-align:left;vertical-align:middle}
|
||||||
|
th{background:#fafbfc;font-weight:600;color:#444;white-space:nowrap}
|
||||||
|
tr:last-child td{border-bottom:none}
|
||||||
|
td code{background:#f4f6f8;border-radius:4px;padding:1px 5px;font-size:12px}
|
||||||
|
.legend{display:flex;flex-wrap:wrap;gap:18px;font-size:12px;color:var(--muted);margin:10px 0 0}
|
||||||
|
.legend span{display:flex;align-items:center;gap:6px}
|
||||||
|
.note{font-size:12px;color:var(--weak);margin-top:8px}
|
||||||
|
.flow{background:#fff;border:1px solid #e6e8eb;border-radius:12px;padding:16px 18px;font-size:13px;color:#333;white-space:pre;overflow-x:auto;line-height:1.7}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<div class="doc-head">
|
||||||
|
<h1>iBoard V0.2 · 界面设计</h1>
|
||||||
|
<p>打卡 / 日历 / 清单 —— 移动端静态高保真原型(设计基线 420 × 820,适配 375~430px)</p>
|
||||||
|
<div class="meta">
|
||||||
|
<span>版本 V0.2</span><span>分支 dev-0.2</span><span>antd-mobile 风格</span>
|
||||||
|
<span>底部 5 Tab(新增「打卡」)</span><span>时区 Asia/Shanghai</span><span>文档日期 2026-10-22</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ 1 设计规范 ============ -->
|
||||||
|
<h2>1 设计规范<span class="sub">Design Tokens</span></h2>
|
||||||
|
<p class="desc">沿用项目既有 <code>:root</code> 变量(<code>src/styles/globals.css</code>),本模块不引入新主题。</p>
|
||||||
|
<div class="tokens">
|
||||||
|
<div class="panel">
|
||||||
|
<h4>语义色</h4>
|
||||||
|
<div class="swatches">
|
||||||
|
<div class="sw"><i style="background:#1677ff"></i><b>#1677ff</b><em>主色 / 今日 / 链接</em></div>
|
||||||
|
<div class="sw"><i style="background:#00b578"></i><b>#00b578</b><em>完成 / 进度条</em></div>
|
||||||
|
<div class="sw"><i style="background:#ffc53d"></i><b>#ffc53d</b><em>部分完成 / 粒子</em></div>
|
||||||
|
<div class="sw"><i style="background:#a855f7"></i><b>#a855f7</b><em>超额完成</em></div>
|
||||||
|
<div class="sw"><i style="background:#ff8f1f"></i><b>#ff8f1f</b><em>连续天数 / 超额徽标</em></div>
|
||||||
|
<div class="sw"><i style="background:#ff3141"></i><b>#ff3141</b><em>危险操作</em></div>
|
||||||
|
<div class="sw"><i style="background:#f5f5f5"></i><b>#f5f5f5</b><em>页面底色</em></div>
|
||||||
|
<div class="sw"><i style="background:#f7fbf8"></i><b>#f7fbf8</b><em>已完成卡片底</em></div>
|
||||||
|
</div>
|
||||||
|
<div class="legend" style="margin-top:14px">
|
||||||
|
<span><i class="dot full"></i>日历·全部完成</span>
|
||||||
|
<span><i class="dot partial"></i>日历·部分完成</span>
|
||||||
|
<span><i class="dot over"></i>日历·超额</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<h4>布局与圆角</h4>
|
||||||
|
<div class="kv">
|
||||||
|
<span>视口</span><b>420 × 820(375~430 自适应)</b>
|
||||||
|
<span>NavBar 高</span><b>45px</b>
|
||||||
|
<span>TabBar 高</span><b>72px(含安全区)</b>
|
||||||
|
<span>页面内边距</span><b>左右 12px</b>
|
||||||
|
<span>卡片圆角</span><b>10px</b>
|
||||||
|
<span>卡片间距</span><b>10px 纵向</b>
|
||||||
|
<span>卡片阴影</span><b>0 1px 2px rgba(0,0,0,.04)</b>
|
||||||
|
<span>环形尺寸</span><b>58 × 58,环宽 6px</b>
|
||||||
|
<span>最小点按区</span><b>≥ 40 × 40</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<h4>字号层级</h4>
|
||||||
|
<div class="type-row"><em>18 / 700</em><span style="font-size:18px;font-weight:700">日期标题「10月22日」</span></div>
|
||||||
|
<div class="type-row"><em>17 / 600</em><span style="font-size:17px;font-weight:600">NavBar 标题「打卡」</span></div>
|
||||||
|
<div class="type-row"><em>16 / 700</em><span style="font-size:16px;font-weight:700">环形中心数字 / 弹窗标题</span></div>
|
||||||
|
<div class="type-row"><em>15 / 600</em><span style="font-size:15px;font-weight:600">打卡项名 / 清单标题</span></div>
|
||||||
|
<div class="type-row"><em>13 / 400</em><span style="font-size:13px">正文 / 日历日期数字</span></div>
|
||||||
|
<div class="type-row"><em>12 / 400</em><span style="font-size:12px;color:#666">次要信息 / 操作链接</span></div>
|
||||||
|
<div class="type-row"><em>11 / 400</em><span style="font-size:11px;color:#999">备注 / 周期范围 / 徽标</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ 2 主界面 ============ -->
|
||||||
|
<h2>2 主界面<span class="sub">三个子视图(SwipeTabs 左右滑动切换)</span></h2>
|
||||||
|
|
||||||
|
<div class="stage">
|
||||||
|
|
||||||
|
<!-- ---------- 2.1 打卡视图 ---------- -->
|
||||||
|
<div class="shot">
|
||||||
|
<div class="phone">
|
||||||
|
<div class="navbar">打卡</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="habit-head">
|
||||||
|
<div>
|
||||||
|
<div class="habit-date">10月22日 周四</div>
|
||||||
|
<div class="habit-summary">已完成 <strong>2</strong> / 4</div>
|
||||||
|
</div>
|
||||||
|
<div class="head-right">
|
||||||
|
<div class="mini-btn">特效开</div>
|
||||||
|
<div class="mini-btn solid">+ 新建</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="habit-list">
|
||||||
|
<!-- 未完成:每天 2 次,0/2 -->
|
||||||
|
<div class="habit-card">
|
||||||
|
<div class="ring">
|
||||||
|
<svg width="58" height="58" viewBox="0 0 58 58">
|
||||||
|
<circle cx="29" cy="29" r="26" fill="none" stroke="#ececf0" stroke-width="6"/>
|
||||||
|
<circle cx="29" cy="29" r="26" fill="none" stroke="#1677ff" stroke-width="6" stroke-linecap="round"
|
||||||
|
stroke-dasharray="163.4" stroke-dashoffset="163.4" transform="rotate(-90 29 29)"/>
|
||||||
|
</svg>
|
||||||
|
<div class="ring-center">0<em>/2</em></div>
|
||||||
|
</div>
|
||||||
|
<div class="habit-main">
|
||||||
|
<div class="habit-name">喝水</div>
|
||||||
|
<div class="habit-meta">每天 2 次<span class="streak">连续 12</span></div>
|
||||||
|
<div class="habit-range">今日</div>
|
||||||
|
</div>
|
||||||
|
<div class="habit-actions"><div class="mini-btn solid">打卡</div></div>
|
||||||
|
<div class="habit-foot"><span class="link">编辑</span><span class="link danger">归档</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未完成:每周 3 次,1/3 = 33.3% -->
|
||||||
|
<div class="habit-card">
|
||||||
|
<div class="ring">
|
||||||
|
<svg width="58" height="58" viewBox="0 0 58 58">
|
||||||
|
<circle cx="29" cy="29" r="26" fill="none" stroke="#ececf0" stroke-width="6"/>
|
||||||
|
<circle cx="29" cy="29" r="26" fill="none" stroke="#ff8f1f" stroke-width="6" stroke-linecap="round"
|
||||||
|
stroke-dasharray="163.4" stroke-dashoffset="108.9" transform="rotate(-90 29 29)"/>
|
||||||
|
</svg>
|
||||||
|
<div class="ring-center">1<em>/3</em></div>
|
||||||
|
</div>
|
||||||
|
<div class="habit-main">
|
||||||
|
<div class="habit-name">健身</div>
|
||||||
|
<div class="habit-meta">每周 3 次<span class="streak">连续 5</span></div>
|
||||||
|
<div class="habit-range">10-19 ~ 10-25</div>
|
||||||
|
</div>
|
||||||
|
<div class="habit-actions"><div class="mini-btn solid">打卡</div></div>
|
||||||
|
<div class="habit-foot"><span class="link">撤销</span><span class="link">编辑</span><span class="link danger">归档</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已完成:每月 12 次,12/12 闭合 -->
|
||||||
|
<div class="habit-card done">
|
||||||
|
<div class="ring">
|
||||||
|
<svg width="58" height="58" viewBox="0 0 58 58">
|
||||||
|
<circle cx="29" cy="29" r="26" fill="none" stroke="#ececf0" stroke-width="6"/>
|
||||||
|
<circle cx="29" cy="29" r="26" fill="none" stroke="#00b578" stroke-width="6" stroke-linecap="round"
|
||||||
|
stroke-dasharray="163.4" stroke-dashoffset="0" transform="rotate(-90 29 29)"/>
|
||||||
|
</svg>
|
||||||
|
<div class="ring-center">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M4 12.5l5 5L20 6.5" stroke="#00b578" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="habit-main">
|
||||||
|
<div class="habit-name">读书</div>
|
||||||
|
<div class="habit-meta">每月 12 次<span class="streak">连续 3</span></div>
|
||||||
|
<div class="habit-range">10-01 ~ 10-31</div>
|
||||||
|
</div>
|
||||||
|
<div class="habit-actions"><div class="mini-btn grey">再打一次</div></div>
|
||||||
|
<div class="habit-foot"><span class="link">撤销</span><span class="link">编辑</span><span class="link danger">归档</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已完成 + 超额:每天 1 次,打了 2 次 -->
|
||||||
|
<div class="habit-card done">
|
||||||
|
<div class="ring">
|
||||||
|
<svg width="58" height="58" viewBox="0 0 58 58">
|
||||||
|
<circle cx="29" cy="29" r="26" fill="none" stroke="#ececf0" stroke-width="6"/>
|
||||||
|
<circle cx="29" cy="29" r="26" fill="none" stroke="#a855f7" stroke-width="6" stroke-linecap="round"
|
||||||
|
stroke-dasharray="163.4" stroke-dashoffset="0" transform="rotate(-90 29 29)"/>
|
||||||
|
</svg>
|
||||||
|
<div class="ring-center">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M4 12.5l5 5L20 6.5" stroke="#a855f7" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="ring-over">+1</span>
|
||||||
|
</div>
|
||||||
|
<div class="habit-main">
|
||||||
|
<div class="habit-name">吃药</div>
|
||||||
|
<div class="habit-meta">每天 1 次<span class="streak">连续 30</span></div>
|
||||||
|
<div class="habit-range">今日<span class="over-txt">超额 1</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="habit-actions"><div class="mini-btn grey">再打一次</div></div>
|
||||||
|
<div class="habit-foot"><span class="link">撤销</span><span class="link">编辑</span><span class="link danger">归档</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tabbar">
|
||||||
|
<div class="tab">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 11l8-6 8 6v8a1 1 0 0 1-1 1h-4v-6H9v6H5a1 1 0 0 1-1-1z"/></svg>首页</div>
|
||||||
|
<div class="tab">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||||
|
<div class="tab">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||||
|
<div class="tab on">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||||
|
<div class="tab">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption">
|
||||||
|
<b>2.1 打卡视图 · 今天<span class="tag">默认进入</span></b>
|
||||||
|
<p>头部日期 + 完成数;未完成卡片排在前面。环形中心未达标显示 <code>done/target</code>,达标显示对勾(每天 1 次打完即刻闭合);超额在右上角显示 <code>+n</code>。点环或「打卡」按钮均可打卡,卡片底部提供撤销 / 编辑 / 归档。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ---------- 2.2 日历视图 ---------- -->
|
||||||
|
<div class="shot">
|
||||||
|
<div class="phone">
|
||||||
|
<div class="navbar">打卡</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="seg" style="width:calc(100% - 28px);margin-left:14px">
|
||||||
|
<div>打卡</div><div class="on">日历</div><div>清单</div>
|
||||||
|
</div>
|
||||||
|
<div class="cal-month-bar">
|
||||||
|
<span class="cal-arrow">‹</span><span class="cal-month">2026年10月</span><span class="cal-arrow">›</span>
|
||||||
|
<span class="cal-today">回到今天</span>
|
||||||
|
</div>
|
||||||
|
<div class="seg">
|
||||||
|
<div>昨天</div><div class="on">今天</div><div>明天</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cal-grid">
|
||||||
|
<div class="grid7 wd"><span>一</span><span>二</span><span>三</span><span>四</span><span>五</span><span>六</span><span>日</span></div>
|
||||||
|
<div class="grid7">
|
||||||
|
<div class="day out"><span class="num">28</span></div>
|
||||||
|
<div class="day out"><span class="num">29</span></div>
|
||||||
|
<div class="day out"><span class="num">30</span></div>
|
||||||
|
<div class="day"><span class="num">1</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">2</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">3</span><i class="dot partial"></i></div>
|
||||||
|
<div class="day"><span class="num">4</span><i class="dot full"></i></div>
|
||||||
|
|
||||||
|
<div class="day"><span class="num">5</span></div>
|
||||||
|
<div class="day"><span class="num">6</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">7</span><i class="dot over"></i></div>
|
||||||
|
<div class="day"><span class="num">8</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">9</span><i class="dot partial"></i></div>
|
||||||
|
<div class="day"><span class="num">10</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">11</span><i class="dot full"></i></div>
|
||||||
|
|
||||||
|
<div class="day"><span class="num">12</span></div>
|
||||||
|
<div class="day"><span class="num">13</span></div>
|
||||||
|
<div class="day"><span class="num">14</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">15</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">16</span><i class="dot partial"></i></div>
|
||||||
|
<div class="day"><span class="num">17</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">18</span><i class="dot full"></i></div>
|
||||||
|
|
||||||
|
<div class="day"><span class="num">19</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">20</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">21</span><i class="dot full"></i></div>
|
||||||
|
<div class="day today sel"><span class="num">22</span><i class="dot partial"></i></div>
|
||||||
|
<div class="day future"><span class="num">23</span></div>
|
||||||
|
<div class="day future"><span class="num">24</span></div>
|
||||||
|
<div class="day future"><span class="num">25</span></div>
|
||||||
|
|
||||||
|
<div class="day future"><span class="num">26</span></div>
|
||||||
|
<div class="day future"><span class="num">27</span></div>
|
||||||
|
<div class="day future"><span class="num">28</span></div>
|
||||||
|
<div class="day future"><span class="num">29</span></div>
|
||||||
|
<div class="day future"><span class="num">30</span></div>
|
||||||
|
<div class="day future"><span class="num">31</span></div>
|
||||||
|
<div class="day out"><span class="num">1</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cal-rate">本月完成率 <strong>82%</strong></div>
|
||||||
|
<div class="cal-detail-title">今天 · 2026-10-22</div>
|
||||||
|
|
||||||
|
<div class="cal-item done">
|
||||||
|
<i class="cdot" style="background:#1677ff"></i><span class="cname">喝水</span>
|
||||||
|
<span class="cmeta">每天 2 次 · 当日 2/2</span>
|
||||||
|
<span class="mini-btn grey">已达标</span>
|
||||||
|
</div>
|
||||||
|
<div class="cal-item">
|
||||||
|
<i class="cdot" style="background:#ff8f1f"></i><span class="cname">健身</span>
|
||||||
|
<span class="cmeta">每周 3 次 · 当日 0/1</span>
|
||||||
|
<span class="mini-btn solid">打卡</span>
|
||||||
|
</div>
|
||||||
|
<div class="cal-item done">
|
||||||
|
<i class="cdot" style="background:#00b578"></i><span class="cname">读书</span>
|
||||||
|
<span class="cmeta">每月 12 次 · 当日 1/1</span>
|
||||||
|
<span class="mini-btn grey">已达标</span>
|
||||||
|
</div>
|
||||||
|
<div class="cal-item done">
|
||||||
|
<i class="cdot" style="background:#a855f7"></i><span class="cname">吃药</span>
|
||||||
|
<span class="cmeta">每天 1 次 · 当日 2/1</span>
|
||||||
|
<span class="mini-btn grey">已达标</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tabbar">
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 11l8-6 8 6v8a1 1 0 0 1-1 1h-4v-6H9v6H5a1 1 0 0 1-1-1z"/></svg>首页</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||||
|
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption">
|
||||||
|
<b>2.2 日历视图 · 今天选中</b>
|
||||||
|
<p>周一起始月历 + 「昨天 / 今天 / 明天」快捷分段;日期下方状态点:绿=全部完成、黄=部分完成、紫=超额、无点=未完成或未来。今天用主色数字并选中高亮,未来日期数字弱化。下方为该日明细,可直接打卡;底部展示本月完成率。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ---------- 2.3 清单视图 ---------- -->
|
||||||
|
<div class="shot">
|
||||||
|
<div class="phone">
|
||||||
|
<div class="navbar">打卡</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="seg" style="width:calc(100% - 28px);margin-left:14px">
|
||||||
|
<div>打卡</div><div>日历</div><div class="on">清单</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-head">
|
||||||
|
<div class="seg"><div class="on">进行中</div><div>已归档</div></div>
|
||||||
|
<div class="mini-btn solid">+ 新建</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checklist-card">
|
||||||
|
<div class="cl-title-row"><span class="cl-title">出差 · 杭州</span><span class="cl-scene">出差</span></div>
|
||||||
|
<div class="progress-row">
|
||||||
|
<div class="bar"><i style="width:66.7%"></i></div><span class="count">8/12</span><span class="arrow">›</span>
|
||||||
|
</div>
|
||||||
|
<div class="cl-body">
|
||||||
|
<div class="cl-item done"><span class="box"><svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="cl-item-title">身份证 / 证件</span><span class="cl-del">×</span></div>
|
||||||
|
<div class="cl-item done"><span class="box"><svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="cl-item-title">手机充电器 + 充电宝</span><span class="cl-del">×</span></div>
|
||||||
|
<div class="cl-item done"><span class="box"><svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="cl-item-title">笔记本电脑 + 电源</span><span class="cl-del">×</span></div>
|
||||||
|
<div class="cl-item done"><span class="box"><svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="cl-item-title">耳机</span><span class="cl-del">×</span></div>
|
||||||
|
<div class="cl-item done"><span class="box"><svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="cl-item-title">换洗衣物</span><span class="cl-del">×</span></div>
|
||||||
|
<div class="cl-item done"><span class="box"><svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="cl-item-title">洗漱包</span><span class="cl-del">×</span></div>
|
||||||
|
<div class="cl-item done"><span class="box"><svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="cl-item-title">常用药</span><span class="cl-del">×</span></div>
|
||||||
|
<div class="cl-item done"><span class="box"><svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="cl-item-title">车票 / 机票 / 酒店确认</span><span class="cl-del">×</span></div>
|
||||||
|
<div class="cl-item"><span class="box"><svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="cl-item-title">雨伞</span><span class="cl-del">×</span></div>
|
||||||
|
<div class="cl-item"><span class="box"><svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="cl-item-title">工牌 / 名片</span><span class="cl-del">×</span></div>
|
||||||
|
<div class="cl-item"><span class="box"><svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="cl-item-title">报销单据</span><span class="cl-del">×</span></div>
|
||||||
|
<div class="cl-item"><span class="box"><svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="cl-item-title">现金零钱</span><span class="cl-del">×</span></div>
|
||||||
|
|
||||||
|
<div class="cl-add"><div class="inp ph">添加检查项</div><div class="mini-btn solid">添加</div></div>
|
||||||
|
<div class="cl-actions">
|
||||||
|
<span class="link">全选</span><span class="link">全不选</span><span class="link">复制</span>
|
||||||
|
<span class="link">存为模板</span><span class="link">归档</span><span class="link danger">删除</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checklist-card">
|
||||||
|
<div class="cl-title-row"><span class="cl-title">回老家 · 国庆</span><span class="cl-scene">回老家</span></div>
|
||||||
|
<div class="progress-row">
|
||||||
|
<div class="bar"><i style="width:0%"></i></div><span class="count">0/12</span><span class="arrow" style="transform:none">›</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tabbar">
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 11l8-6 8 6v8a1 1 0 0 1-1 1h-4v-6H9v6H5a1 1 0 0 1-1-1z"/></svg>首页</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||||
|
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption">
|
||||||
|
<b>2.3 清单视图 · 展开态</b>
|
||||||
|
<p>卡片头部常显标题、场景标签、进度条与 <code>8/12</code>;点击展开逐条勾选(勾选后绿色圆底、文字加删除线并弱化)。底部可继续添加检查项,并提供全选 / 全不选 / 复制 / 存为模板 / 归档 / 删除。收起态只保留头部一行。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ---------- 2.4 新建打卡项弹窗 ---------- -->
|
||||||
|
<div class="shot">
|
||||||
|
<div class="phone">
|
||||||
|
<div class="navbar">打卡</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="habit-head">
|
||||||
|
<div>
|
||||||
|
<div class="habit-date">10月22日 周四</div>
|
||||||
|
<div class="habit-summary">已完成 <strong>2</strong> / 4</div>
|
||||||
|
</div>
|
||||||
|
<div class="head-right"><div class="mini-btn">特效开</div><div class="mini-btn solid">+ 新建</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="habit-list">
|
||||||
|
<div class="habit-card">
|
||||||
|
<div class="ring">
|
||||||
|
<svg width="58" height="58" viewBox="0 0 58 58">
|
||||||
|
<circle cx="29" cy="29" r="26" fill="none" stroke="#ececf0" stroke-width="6"/>
|
||||||
|
<circle cx="29" cy="29" r="26" fill="none" stroke="#1677ff" stroke-width="6" stroke-linecap="round" stroke-dasharray="163.4" stroke-dashoffset="163.4" transform="rotate(-90 29 29)"/>
|
||||||
|
</svg><div class="ring-center">0<em>/2</em></div>
|
||||||
|
</div>
|
||||||
|
<div class="habit-main"><div class="habit-name">喝水</div><div class="habit-meta">每天 2 次</div><div class="habit-range">今日</div></div>
|
||||||
|
<div class="habit-actions"><div class="mini-btn solid">打卡</div></div>
|
||||||
|
<div class="habit-foot"><span class="link">编辑</span><span class="link danger">归档</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mask"></div>
|
||||||
|
<div class="sheet">
|
||||||
|
<div class="sheet-title">新建打卡项</div>
|
||||||
|
<div class="field"><label>名称</label><div class="inp ph">例如:喝水 / 健身 / 读书</div></div>
|
||||||
|
<div class="field"><label>周期</label>
|
||||||
|
<div class="chips"><div class="chip on">每天</div><div class="chip">每周</div><div class="chip">每月</div><div class="chip">每年</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="field"><label>目标次数(周期内)</label>
|
||||||
|
<div class="stepper"><span>−</span><b>1</b><span class="act">+</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="field"><label>颜色</label>
|
||||||
|
<div class="colors">
|
||||||
|
<span class="cd on" style="background:#1677ff"></span><span class="cd" style="background:#00b578"></span>
|
||||||
|
<span class="cd" style="background:#ff8f1f"></span><span class="cd" style="background:#ff3141"></span>
|
||||||
|
<span class="cd" style="background:#a855f7"></span><span class="cd" style="background:#14b8a6"></span>
|
||||||
|
<span class="cd" style="background:#f43f9e"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field"><label>备注(可选)</label><div class="inp ph">补充说明</div></div>
|
||||||
|
<div class="btn-primary">保存</div>
|
||||||
|
<div class="btn-ghost">取消</div>
|
||||||
|
</div>
|
||||||
|
<div class="tabbar">
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 11l8-6 8 6v8a1 1 0 0 1-1 1h-4v-6H9v6H5a1 1 0 0 1-1-1z"/></svg>首页</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||||
|
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption">
|
||||||
|
<b>2.4 新建 / 编辑打卡项(底部弹层)</b>
|
||||||
|
<p>名称 ≤20 字;周期四选一(每天 / 每周 / 每月 / 每年);目标次数用 Stepper,最小 1;颜色 7 色预设;备注可选。编辑态标题变为「编辑打卡项」并回填当前值。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ---------- 2.5 完成特效 ---------- -->
|
||||||
|
<div class="shot">
|
||||||
|
<div class="phone">
|
||||||
|
<div class="navbar">打卡</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="habit-head">
|
||||||
|
<div><div class="habit-date">10月22日 周四</div><div class="habit-summary">已完成 <strong>3</strong> / 4</div></div>
|
||||||
|
<div class="head-right"><div class="mini-btn">特效开</div><div class="mini-btn solid">+ 新建</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="habit-list">
|
||||||
|
<div class="habit-card done" style="overflow:visible">
|
||||||
|
<div class="ring">
|
||||||
|
<!-- 特效层:光环 + 14 颗粒子(静态呈现 t=250ms 时的形态) -->
|
||||||
|
<div class="fx">
|
||||||
|
<div class="halo"></div><div class="halo halo2"></div>
|
||||||
|
<i style="background:#ffc53d;transform:translate(-50%,-50%) rotate(0deg) translateY(-34px)"></i>
|
||||||
|
<i style="background:#00b578;transform:translate(-50%,-50%) rotate(26deg) translateY(-40px)"></i>
|
||||||
|
<i style="background:#00b578;transform:translate(-50%,-50%) rotate(52deg) translateY(-34px)"></i>
|
||||||
|
<i style="background:#ffc53d;transform:translate(-50%,-50%) rotate(78deg) translateY(-46px)"></i>
|
||||||
|
<i style="background:#00b578;transform:translate(-50%,-50%) rotate(104deg) translateY(-34px)"></i>
|
||||||
|
<i style="background:#00b578;transform:translate(-50%,-50%) rotate(130deg) translateY(-40px)"></i>
|
||||||
|
<i style="background:#ffc53d;transform:translate(-50%,-50%) rotate(156deg) translateY(-34px)"></i>
|
||||||
|
<i style="background:#00b578;transform:translate(-50%,-50%) rotate(182deg) translateY(-46px)"></i>
|
||||||
|
<i style="background:#00b578;transform:translate(-50%,-50%) rotate(208deg) translateY(-34px)"></i>
|
||||||
|
<i style="background:#ffc53d;transform:translate(-50%,-50%) rotate(234deg) translateY(-40px)"></i>
|
||||||
|
<i style="background:#00b578;transform:translate(-50%,-50%) rotate(260deg) translateY(-34px)"></i>
|
||||||
|
<i style="background:#00b578;transform:translate(-50%,-50%) rotate(286deg) translateY(-46px)"></i>
|
||||||
|
<i style="background:#ffc53d;transform:translate(-50%,-50%) rotate(312deg) translateY(-34px)"></i>
|
||||||
|
<i style="background:#00b578;transform:translate(-50%,-50%) rotate(338deg) translateY(-40px)"></i>
|
||||||
|
</div>
|
||||||
|
<svg width="58" height="58" viewBox="0 0 58 58">
|
||||||
|
<circle cx="29" cy="29" r="26" fill="none" stroke="#ececf0" stroke-width="6"/>
|
||||||
|
<circle cx="29" cy="29" r="26" fill="none" stroke="#00b578" stroke-width="6" stroke-linecap="round" stroke-dasharray="163.4" stroke-dashoffset="0" transform="rotate(-90 29 29)"/>
|
||||||
|
</svg>
|
||||||
|
<div class="ring-center">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#00b578" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="habit-main"><div class="habit-name">吃药</div><div class="habit-meta">每天 1 次<span class="streak">连续 30</span></div><div class="habit-range">今日</div></div>
|
||||||
|
<div class="habit-actions"><div class="mini-btn grey">再打一次</div></div>
|
||||||
|
<div class="habit-foot"><span class="link">撤销</span><span class="link">编辑</span><span class="link danger">归档</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="habit-card">
|
||||||
|
<div class="ring">
|
||||||
|
<svg width="58" height="58" viewBox="0 0 58 58">
|
||||||
|
<circle cx="29" cy="29" r="26" fill="none" stroke="#ececf0" stroke-width="6"/>
|
||||||
|
<circle cx="29" cy="29" r="26" fill="none" stroke="#ff8f1f" stroke-width="6" stroke-linecap="round" stroke-dasharray="163.4" stroke-dashoffset="108.9" transform="rotate(-90 29 29)"/>
|
||||||
|
</svg><div class="ring-center">1<em>/3</em></div>
|
||||||
|
</div>
|
||||||
|
<div class="habit-main"><div class="habit-name">健身</div><div class="habit-meta">每周 3 次</div><div class="habit-range">10-19 ~ 10-25</div></div>
|
||||||
|
<div class="habit-actions"><div class="mini-btn solid">打卡</div></div>
|
||||||
|
<div class="habit-foot"><span class="link">撤销</span><span class="link">编辑</span><span class="link danger">归档</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tabbar">
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 11l8-6 8 6v8a1 1 0 0 1-1 1h-4v-6H9v6H5a1 1 0 0 1-1-1z"/></svg>首页</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||||
|
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption">
|
||||||
|
<b>2.5 达标瞬间 · 完成特效</b>
|
||||||
|
<p>环形闭合 + 对勾缩放入场(0.32s);同时 14 颗粒子沿圆周扩散(26~58px,交替主色与金色)+ 主色光环脉冲(0.7s),总时长 ≤900ms,<code>pointer-events:none</code> 不阻塞后续操作;并触发 18ms 轻震动。可在头部一键关闭(持久化开关),系统「减弱动效」时自动降级。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ---------- 2.6 日历·明天(计划) ---------- -->
|
||||||
|
<div class="shot">
|
||||||
|
<div class="phone">
|
||||||
|
<div class="navbar">打卡</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="seg" style="width:calc(100% - 28px);margin-left:14px"><div>打卡</div><div class="on">日历</div><div>清单</div></div>
|
||||||
|
<div class="cal-month-bar"><span class="cal-arrow">‹</span><span class="cal-month">2026年10月</span><span class="cal-arrow">›</span><span class="cal-today">回到今天</span></div>
|
||||||
|
<div class="seg"><div>昨天</div><div>今天</div><div class="on">明天</div></div>
|
||||||
|
<div class="cal-grid">
|
||||||
|
<div class="grid7 wd"><span>一</span><span>二</span><span>三</span><span>四</span><span>五</span><span>六</span><span>日</span></div>
|
||||||
|
<div class="grid7">
|
||||||
|
<div class="day out"><span class="num">28</span></div><div class="day out"><span class="num">29</span></div><div class="day out"><span class="num">30</span></div>
|
||||||
|
<div class="day"><span class="num">1</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">2</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">3</span><i class="dot partial"></i></div>
|
||||||
|
<div class="day"><span class="num">4</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">5</span></div>
|
||||||
|
<div class="day"><span class="num">6</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">7</span><i class="dot over"></i></div>
|
||||||
|
<div class="day"><span class="num">8</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">9</span><i class="dot partial"></i></div>
|
||||||
|
<div class="day"><span class="num">10</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">11</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">12</span></div><div class="day"><span class="num">13</span></div>
|
||||||
|
<div class="day"><span class="num">14</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">15</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">16</span><i class="dot partial"></i></div>
|
||||||
|
<div class="day"><span class="num">17</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">18</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">19</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">20</span><i class="dot full"></i></div>
|
||||||
|
<div class="day today"><span class="num">21</span><i class="dot full"></i></div>
|
||||||
|
<div class="day"><span class="num">22</span><i class="dot partial"></i></div>
|
||||||
|
<div class="day future sel"><span class="num">23</span></div>
|
||||||
|
<div class="day future"><span class="num">24</span></div>
|
||||||
|
<div class="day future"><span class="num">25</span></div>
|
||||||
|
<div class="day future"><span class="num">26</span></div><div class="day future"><span class="num">27</span></div>
|
||||||
|
<div class="day future"><span class="num">28</span></div><div class="day future"><span class="num">29</span></div>
|
||||||
|
<div class="day future"><span class="num">30</span></div><div class="day future"><span class="num">31</span></div>
|
||||||
|
<div class="day out"><span class="num">1</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cal-rate">本月完成率 <strong>82%</strong></div>
|
||||||
|
<div class="cal-detail-title">明天 · 2026-10-23<span class="badge">计划(不可打卡)</span></div>
|
||||||
|
<div class="cal-item"><i class="cdot" style="background:#1677ff"></i><span class="cname">喝水</span><span class="cmeta">每天 2 次 · 当日 0/2</span><span class="cplan">待办</span></div>
|
||||||
|
<div class="cal-item"><i class="cdot" style="background:#ff8f1f"></i><span class="cname">健身</span><span class="cmeta">每周 3 次 · 当日 0/1</span><span class="cplan">待办</span></div>
|
||||||
|
<div class="cal-item"><i class="cdot" style="background:#00b578"></i><span class="cname">读书</span><span class="cmeta">每月 12 次 · 当日 0/1</span><span class="cplan">待办</span></div>
|
||||||
|
<div class="cal-item"><i class="cdot" style="background:#a855f7"></i><span class="cname">吃药</span><span class="cmeta">每天 1 次 · 当日 0/1</span><span class="cplan">待办</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="tabbar">
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 11l8-6 8 6v8a1 1 0 0 1-1 1h-4v-6H9v6H5a1 1 0 0 1-1-1z"/></svg>首页</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||||
|
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption">
|
||||||
|
<b>2.6 日历 · 明天(计划,只读)</b>
|
||||||
|
<p>未来日期不允许打卡:标题右侧显示「计划(不可打卡)」徽标,条目右侧只显示「待办」文案而不渲染按钮,月历上未来日期数字弱化且不打点。昨天视图则相反——可补打最近 7 天内的漏打卡。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ---------- 2.7 清单完成 + 空态 ---------- -->
|
||||||
|
<div class="shot">
|
||||||
|
<div class="phone">
|
||||||
|
<div class="navbar">打卡</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="seg" style="width:calc(100% - 28px);margin-left:14px"><div>打卡</div><div>日历</div><div class="on">清单</div></div>
|
||||||
|
<div class="list-head">
|
||||||
|
<div class="seg"><div class="on">进行中</div><div>已归档</div></div>
|
||||||
|
<div class="mini-btn solid">+ 新建</div>
|
||||||
|
</div>
|
||||||
|
<div class="checklist-card done" style="position:relative;overflow:visible">
|
||||||
|
<div class="cl-title-row"><span class="cl-title">出差 · 杭州</span><span class="cl-scene">出差</span></div>
|
||||||
|
<div class="progress-row"><div class="bar"><i style="width:100%"></i></div><span class="count">12/12</span><span class="arrow">›</span></div>
|
||||||
|
<div class="fx" style="left:50%;top:34px;right:auto;bottom:auto;width:0;height:0">
|
||||||
|
<div class="halo" style="border-color:#00b578"></div>
|
||||||
|
<i style="background:#ffc53d;transform:translate(-50%,-50%) rotate(10deg) translateY(-46px)"></i>
|
||||||
|
<i style="background:#00b578;transform:translate(-50%,-50%) rotate(46deg) translateY(-56px)"></i>
|
||||||
|
<i style="background:#00b578;transform:translate(-50%,-50%) rotate(92deg) translateY(-46px)"></i>
|
||||||
|
<i style="background:#ffc53d;transform:translate(-50%,-50%) rotate(140deg) translateY(-62px)"></i>
|
||||||
|
<i style="background:#00b578;transform:translate(-50%,-50%) rotate(190deg) translateY(-46px)"></i>
|
||||||
|
<i style="background:#00b578;transform:translate(-50%,-50%) rotate(240deg) translateY(-56px)"></i>
|
||||||
|
<i style="background:#ffc53d;transform:translate(-50%,-50%) rotate(300deg) translateY(-46px)"></i>
|
||||||
|
<i style="background:#00b578;transform:translate(-50%,-50%) rotate(340deg) translateY(-62px)"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="checklist-card">
|
||||||
|
<div class="cl-title-row"><span class="cl-title">回老家 · 国庆</span><span class="cl-scene">回老家</span></div>
|
||||||
|
<div class="progress-row"><div class="bar"><i style="width:0%"></i></div><span class="count">0/12</span><span class="arrow" style="transform:none">›</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tabbar">
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 11l8-6 8 6v8a1 1 0 0 1-1 1h-4v-6H9v6H5a1 1 0 0 1-1-1z"/></svg>首页</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||||
|
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption">
|
||||||
|
<b>2.7 清单全部完成(12/12)</b>
|
||||||
|
<p>进度条 100%、计数变为 <code>12/12</code>、卡片底色转 <code>#f7fbf8</code>,并播放与打卡一致的完成特效。清单完成后用户通常选择「归档」或「复制」复用。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ---------- 2.8 空态与归档 ---------- -->
|
||||||
|
<div class="shot">
|
||||||
|
<div class="phone">
|
||||||
|
<div class="navbar">打卡</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="habit-head">
|
||||||
|
<div><div class="habit-date">10月22日 周四</div><div class="habit-summary">已完成 <strong>0</strong> / 0</div></div>
|
||||||
|
<div class="head-right"><div class="mini-btn">特效关</div><div class="mini-btn solid">+ 新建</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="empty">
|
||||||
|
<div class="ico">○</div>
|
||||||
|
<p>还没有打卡项</p>
|
||||||
|
<div style="display:inline-block" class="mini-btn solid">创建第一个打卡项</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tabbar">
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 11l8-6 8 6v8a1 1 0 0 1-1 1h-4v-6H9v6H5a1 1 0 0 1-1-1z"/></svg>首页</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||||
|
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||||
|
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption">
|
||||||
|
<b>2.8 空态</b>
|
||||||
|
<p>三个视图均具备加载中 / 空态 / 错误(带重试)三态。空态给出明确引导按钮;头部「特效开 / 特效关」为全局动效开关,关闭后仅保留环形与对勾,不播放粒子与震动。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ 3 组件状态 ============ -->
|
||||||
|
<h2>3 组件状态定义<span class="sub">Component States</span></h2>
|
||||||
|
|
||||||
|
<h3>3.1 环形进度(尺寸 58,环宽 6,r=26,周长 163.4)</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>状态</th><th>条件</th><th>环形</th><th>中心</th><th>卡片底</th><th>按钮</th></tr>
|
||||||
|
<tr><td>未开始</td><td><code>done = 0</code></td><td>仅灰底环</td><td><code>0/2</code></td><td>白</td><td>打卡(主色)</td></tr>
|
||||||
|
<tr><td>进行中</td><td><code>0 < done < target</code></td><td>按 <code>done/target</code> 填充(45°…330°)</td><td><code>1/3</code></td><td>白</td><td>打卡(主色)</td></tr>
|
||||||
|
<tr><td>已达标</td><td><code>done = target</code></td><td>闭合整圈</td><td>对勾(主色)</td><td><code>#f7fbf8</code></td><td>再打一次(灰)</td></tr>
|
||||||
|
<tr><td>超额</td><td><code>done > target</code></td><td>闭合 + 右上角 <code>+n</code> 橙标</td><td>对勾</td><td><code>#f7fbf8</code></td><td>再打一次(灰)</td></tr>
|
||||||
|
<tr><td>每日 1 次达标</td><td><code>target = 1</code> 且已打卡</td><td><b>直接整圈闭合</b>,不做渐增生长期</td><td>对勾</td><td><code>#f7fbf8</code></td><td>再打一次</td></tr>
|
||||||
|
</table>
|
||||||
|
<p class="note">环形进度 = 周期完成度(<code>done / n</code>);日历日标记 = 当日份额(<code>当日次数 / 日均期望</code>),两者口径不同,互不影响。</p>
|
||||||
|
|
||||||
|
<h3>3.2 日历日期</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>状态</th><th>判定</th><th>视觉</th></tr>
|
||||||
|
<tr><td>全部完成 <code>full</code></td><td>所有打卡项当日次数 ≥ 日均期望,且无超额</td><td>绿色圆点</td></tr>
|
||||||
|
<tr><td>超额 <code>over</code></td><td>全部达标且存在任一打卡项当日次数 > 期望</td><td>紫色圆点</td></tr>
|
||||||
|
<tr><td>部分完成 <code>partial</code></td><td>有完成但未全部完成</td><td>黄色圆点</td></tr>
|
||||||
|
<tr><td>无 <code>empty</code></td><td>无记录 / 当天无打卡项 / 未来日期</td><td>不显示圆点</td></tr>
|
||||||
|
<tr><td>今天</td><td><code>date = today</code></td><td>数字主色加粗</td></tr>
|
||||||
|
<tr><td>选中</td><td>用户点击</td><td>主色实心底,数字与圆点转白</td></tr>
|
||||||
|
<tr><td>非本月 / 未来</td><td>月份不符 / <code>date > today</code></td><td>数字弱化(#c8c8c8 / #b0b0b0)</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>3.3 清单检查项</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>状态</th><th>勾选框</th><th>文字</th><th>触发</th></tr>
|
||||||
|
<tr><td>未完成</td><td>1.5px 灰边圆圈,对勾透明</td><td>#333</td><td>点击圆圈或文字切换</td></tr>
|
||||||
|
<tr><td>已完成</td><td>绿底绿边,白色对勾显现(0.2s)</td><td>#a0a0a0 + 删除线</td><td>同上,写 <code>doneAt</code></td></tr>
|
||||||
|
<tr><td>整单完成</td><td>—</td><td>进度条 100%、卡片转绿</td><td>最后一项勾选时播放特效</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- ============ 4 交互与动效 ============ -->
|
||||||
|
<h2>4 交互与动效<span class="sub">Motion</span></h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>动效</th><th>时长</th><th>曲线 / 实现</th><th>说明</th></tr>
|
||||||
|
<tr><td>环形增长</td><td>450ms</td><td><code>cubic-bezier(.4,0,.2,1)</code>,<code>stroke-dashoffset</code> 过渡</td><td>12 点方向顺时针增长</td></tr>
|
||||||
|
<tr><td>对勾入场</td><td>320ms</td><td><code>scale(.4) → 1</code> + 透明度</td><td>达标瞬间出现</td></tr>
|
||||||
|
<tr><td>完成粒子</td><td>≤900ms</td><td>14 颗粒子按角度 <code>--a</code>、距离 <code>--d</code> 扩散,交错 40ms 延迟</td><td>不阻塞点击</td></tr>
|
||||||
|
<tr><td>光环脉冲</td><td>700ms</td><td>主色圆环放大淡出</td><td>与粒子同时开始</td></tr>
|
||||||
|
<tr><td>触感反馈</td><td>18ms</td><td><code>navigator.vibrate(18)</code></td><td>WebView 不支持则静默跳过</td></tr>
|
||||||
|
<tr><td>进度条填充</td><td>400ms</td><td><code>width</code> 过渡</td><td>清单勾选后更新</td></tr>
|
||||||
|
<tr><td>视图切换</td><td>—</td><td>SwipeTabs 跟手横滑</td><td>与既有模块一致</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>4.1 打卡时序</h3>
|
||||||
|
<div class="flow">用户点环 / 点「打卡」
|
||||||
|
└─ 前端乐观更新:done+1,重算 percent / completed / over,环形立即增长
|
||||||
|
└─ POST /api/checkin/habits/:id/check
|
||||||
|
├─ 200 → 用服务端 item 覆盖本地;若 completed → 触发特效 + 震动
|
||||||
|
├─ 400 → 回滚 + Toast(未来日期 / 超出 7 天补打窗口)
|
||||||
|
├─ 409 → 视为已打卡,刷新一次(幂等)
|
||||||
|
└─ 网络失败 → 回滚 + Toast「打卡失败」</div>
|
||||||
|
|
||||||
|
<!-- ============ 5 适配与可达性 ============ -->
|
||||||
|
<h2>5 适配与可达性</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>项</th><th>约定</th></tr>
|
||||||
|
<tr><td>视口</td><td>设计稿 420×820;375~430px 内元素不换行、不裁切;底部 TabBar 5 项每项 ≥ 40px 可点宽度</td></tr>
|
||||||
|
<tr><td>安全区</td><td>TabBar 使用 antd-mobile <code>safeArea</code>,页面底部留出 <code>tabbar-h + safe-bottom</code></td></tr>
|
||||||
|
<tr><td>减弱动效</td><td>系统开启「减弱动效」或用户关闭特效开关 → 不播放粒子 / 光环 / 震动,只保留环形与对勾</td></tr>
|
||||||
|
<tr><td>大字号</td><td>卡片采用 grid 自适应列宽,文字超长换行不截断;名称限制 20 字、检查项 40 字</td></tr>
|
||||||
|
<tr><td>深色模式</td><td>V0.2 沿用现有浅色主题,暂不适配</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p class="note" style="margin-top:26px">本文档为静态设计稿,不含脚本逻辑;实现对应 <code>src/pages/checkin/*.jsx</code>、<code>src/components/RingProgress.jsx</code> 与样式段(<code>src/styles/globals.css</code>)。接口与数据口径见《设计文档.md》与《数据库设计文档.md》。</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
# iBoard V0.2 设计文档 — 打卡 / 日历 / 清单
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 版本 | V0.2(对应分支 `dev-0.2`) |
|
||||||
|
| 文档状态 | 已实现(与代码一致) |
|
||||||
|
| 上游文档 | `需求文档.md`(需求编号 F-CK / F-CL / F-LS / F-SY / U / D / N / E 沿用本文) |
|
||||||
|
| 配套文档 | `数据库设计文档.md`、`界面设计.html` |
|
||||||
|
| 技术栈 | Tauri 2 + React 18 + antd-mobile + React Router v6 + Express 4 + Prisma/MariaDB |
|
||||||
|
| 设计基线 | 移动端优先,视口 420×820,375~430px 自适应;时区统一 `Asia/Shanghai` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 总体设计
|
||||||
|
|
||||||
|
### 1.1 目标与范围
|
||||||
|
|
||||||
|
在既有 iBoard(首页 / 宏观 / 监控 / 我)之上新增**第 5 个底部 Tab「打卡」**,页面内以三个子视图承载三类需求:
|
||||||
|
|
||||||
|
| 子视图 | 解决问题 | 核心隐喻 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **打卡** | 周期性习惯的量化坚持 | 环形进度,点环即打卡 |
|
||||||
|
| **日历** | 回看昨天、执行今天、预知明天 | 月历网格 + 日状态点 |
|
||||||
|
| **清单** | 出差/回老家等一次性场景的漏项检查 | 勾选清单 + 进度条 |
|
||||||
|
|
||||||
|
**不纳入 V0.2**:多用户共享清单、提醒推送、数据导出、打卡项分组、年视图热力图(见 §9 演进)。
|
||||||
|
|
||||||
|
### 1.2 架构总览
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────── 前端 (React + antd-mobile, 打包进 Tauri / OTA) ────────────────────────┐
|
||||||
|
│ App.jsx 路由 │
|
||||||
|
│ └ /checkin ──► CheckinPage ── SwipeTabs ──┬── HabitView (打卡) │
|
||||||
|
│ ├── CalendarView (日历) │
|
||||||
|
│ └── ListView (清单) │
|
||||||
|
│ 组件: RingProgress(环形) / Celebrate(特效) / BottomNav / TabIcon │
|
||||||
|
│ 工具: src/pages/checkin/date.js (本地日期, 与后端同口径) apiClient: src/api/checkin.js │
|
||||||
|
└──────────────────────────────────┬─────────────────────────────────────────────────────────────┘
|
||||||
|
│ HTTPS + JWT (Authorization: Bearer)
|
||||||
|
┌──────────────────────────────────▼─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Express (server/index.js) │
|
||||||
|
│ /api/checkin ─► routes/checkin.js ─┐ │
|
||||||
|
│ /api/checklists─► routes/checklists.js ─┤─► middleware.authRequired ─► Prisma Client │
|
||||||
|
│ └─► checkin/period.js (周期口径唯一来源) │
|
||||||
|
└──────────────────────────────────┬─────────────────────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
MariaDB: Habit / CheckIn / CheckList / CheckListItem / CheckListTemplate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 分层与职责
|
||||||
|
|
||||||
|
| 层 | 位置 | 职责 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 页面 | `src/pages/checkin/*.jsx` | 视图编排、本地状态、乐观更新与回滚 |
|
||||||
|
| 组件 | `src/components/RingProgress.jsx` | 纯展示:SVG 环形 + 完成特效 + 特效开关 |
|
||||||
|
| 日期工具 | `src/pages/checkin/date.js` | 与后端同口径的本地日期运算(**改一处需同步另一处**) |
|
||||||
|
| API 封装 | `src/api/checkin.js` | 统一走 `api()`(自带 token / 错误解析) |
|
||||||
|
| 路由 | `server/routes/checkin.js`、`checklists.js` | 参数校验、鉴权、组装展示态 |
|
||||||
|
| 领域逻辑 | `server/checkin/period.js` | 周期窗口、日均期望、streak 判定 |
|
||||||
|
| 持久化 | Prisma Client(`generated/prisma`) | 表操作 |
|
||||||
|
|
||||||
|
### 1.4 目录结构(本次新增/修改)
|
||||||
|
|
||||||
|
```
|
||||||
|
prisma/
|
||||||
|
schema.prisma # +5 个 model,User 加反向关系
|
||||||
|
migrations/20261022120000_add_checkin_checklist/ # 新建表 + 外键
|
||||||
|
server/
|
||||||
|
checkin/period.js # 周期计算(唯一口径)
|
||||||
|
routes/checkin.js # /api/checkin/*
|
||||||
|
routes/checklists.js # /api/checklists/*
|
||||||
|
index.js # 挂载两个路由
|
||||||
|
src/
|
||||||
|
api/checkin.js # checkinApi / checklistApi
|
||||||
|
components/RingProgress.jsx # 环形进度 + Celebrate 特效
|
||||||
|
components/BottomNav.jsx # 新增「打卡」Tab
|
||||||
|
components/TabIcon.jsx # 新增 checkin 图标
|
||||||
|
pages/checkin/CheckinPage.jsx # 容器:三视图切换
|
||||||
|
pages/checkin/HabitView.jsx # 打卡视图
|
||||||
|
pages/checkin/CalendarView.jsx # 日历视图
|
||||||
|
pages/checkin/ListView.jsx # 清单视图
|
||||||
|
pages/checkin/date.js # 前端日期工具
|
||||||
|
App.jsx # 新增 /checkin 路由
|
||||||
|
styles/globals.css # 打卡相关样式段
|
||||||
|
docs/V0.2/ # 需求 / 设计 / 数据库设计 / 界面设计
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 领域模型与核心口径
|
||||||
|
|
||||||
|
### 2.1 实体
|
||||||
|
|
||||||
|
`Habit`(打卡项定义)· `CheckIn`(一次打卡)· `CheckList`(场景清单)· `CheckListItem`(检查项)· `CheckListTemplate`(模板)。字段与索引见《数据库设计文档》。
|
||||||
|
|
||||||
|
### 2.2 周期口径(唯一来源:`server/checkin/period.js`)
|
||||||
|
|
||||||
|
| 概念 | 定义 |
|
||||||
|
| --- | --- |
|
||||||
|
| 本地日历日 | `Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai' })` → `"YYYY-MM-DD"` |
|
||||||
|
| 周起始 | **周一**(`startOfWeek` 把周日映射到第 7 天) |
|
||||||
|
| 周期窗口 | day = [d, d];week = [周一, 周日];month = [1 号, 月末];year = [1/1, 12/31] |
|
||||||
|
| **环形进度** | `percent = 周期内累计次数 / n`,上限 1 —— 回答「这周期做了多少」 |
|
||||||
|
| **日均期望次数** | day→n;week→`ceil(n/7)`;month→`ceil(n/当月天数)`;year→`ceil(n/365)` —— 回答「今天该做多少」 |
|
||||||
|
| 完成 | `done >= target`(超额允许,`over = done - target` 继续累加) |
|
||||||
|
| streak | 从当前周期向前回溯:达标 +1;**当前周期未结束且未达标 → 跳过而非中断**;已结束周期未达标 → 终止 |
|
||||||
|
|
||||||
|
>两种口径并存是有意为之:环形看「周期整体」,日历日标记看「当天份额」。例如「每周 3 次」,周三打 1 次 → 环形 33.3%,当天已达标(1 ≥ ceil(3/7)=1)。
|
||||||
|
|
||||||
|
### 2.3 边界规则
|
||||||
|
|
||||||
|
| 场景 | 规则 | 编号 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 提前打卡(未来日期) | 拒绝,返回 400「不能为未来日期打卡」 | F-CK-11 |
|
||||||
|
| 补打 | 仅限最近 **7 天**,记录 `source='backfill'` | F-CK-10 |
|
||||||
|
| 跨零点 | 以服务端 Asia/Shanghai 日历日为准,前端不自行判断 | E-02 |
|
||||||
|
| 周期中途新建 | 按完整 n 计算,不按剩余天数折算 | E-01 |
|
||||||
|
| 编辑 `target` | 立即生效,历史记录不改动 | F-CK-13 |
|
||||||
|
| 删除打卡项 | 默认归档(`archived=true`),`?hard=1` 物理删并清记录 | E-05 |
|
||||||
|
| 清单删除 | 物理删(一次性场景不留历史) | F-LS-10 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 前端设计
|
||||||
|
|
||||||
|
### 3.1 路由与容器
|
||||||
|
|
||||||
|
```
|
||||||
|
/checkin → CheckinPage
|
||||||
|
├─ NavBar「打卡」
|
||||||
|
└─ SwipeTabs(左右滑动切换,与既有模块交互一致)
|
||||||
|
├─ habit → HabitView (受 date 影响)
|
||||||
|
├─ calendar → CalendarView (持有选中 date,可回写容器)
|
||||||
|
└─ list → ListView (独立)
|
||||||
|
```
|
||||||
|
|
||||||
|
`date` 状态提升到 `CheckinPage`:日历里点某天 → 切回打卡视图时即为该日,实现「日历与打卡联动」。
|
||||||
|
|
||||||
|
### 3.2 组件树与关键状态
|
||||||
|
|
||||||
|
```
|
||||||
|
CheckinPage
|
||||||
|
├── useState: active('habit'|'calendar'|'list'), date('YYYY-MM-DD')
|
||||||
|
HabitView(date)
|
||||||
|
├── data: { today, date, items[], summary:{total,done} }
|
||||||
|
├── bursts: { habitId: n } // 特效触发计数(+1 即播放一次)
|
||||||
|
├── form / showForm // 新建/编辑弹窗
|
||||||
|
└── effects: boolean // 特效开关(持久化 localStorage)
|
||||||
|
CalendarView(date, onDateChange)
|
||||||
|
├── month, cal(月历), detail(当日), bursts
|
||||||
|
└── quickValue: -1|0|1|null // 昨天/今天/明天 分段控件
|
||||||
|
ListView()
|
||||||
|
├── archived, lists, openId(展开的清单), newItem, templates, form/showForm
|
||||||
|
└── bursts
|
||||||
|
```
|
||||||
|
|
||||||
|
统一约定:
|
||||||
|
- 加载中 → `DotLoading`;错误 → `error-banner` + 重试;空 → antd-mobile `Empty` + 引导按钮(三态齐全,U-06)。
|
||||||
|
- 页面重新可见时静默刷新(`useRefreshOnVisible`),与既有模块一致。
|
||||||
|
|
||||||
|
### 3.3 环形进度组件 `RingProgress`
|
||||||
|
|
||||||
|
| 项 | 设计 |
|
||||||
|
| --- | --- |
|
||||||
|
| 实现 | 双 `<circle>`:灰底环 + `stroke-dasharray/dashoffset` 进度环,`rotate(-90)` 从 12 点顺时针 |
|
||||||
|
| 尺寸 | `size=58`,`stroke=6`,`r=26`,周长 `2πr≈163.4` |
|
||||||
|
| 动画 | `transition: stroke-dashoffset .45s cubic-bezier(.4,0,.2,1)`(400ms 级,U-01) |
|
||||||
|
| 中心内容 | 未达标 → `done / target` 数字;**达标 → 对勾 SVG**(`ring-check-in` 缩放入场);超额 → 右上角 `+n` 徽标 |
|
||||||
|
| 交互 | 整个环可点 = 打卡(`clickable`),同时卡片右侧保留显式「打卡」按钮,降低发现成本 |
|
||||||
|
| 无障碍/降级 | `disabled` 态半透明;系统「减弱动效」时特效自动关闭 |
|
||||||
|
|
||||||
|
**完成特效 `Celebrate`**(F-CK-07 / U-02):`fire` 计数器 +1 即触发一次 → 14 个粒子沿圆周角度扩散(`--a/--d` CSS 变量)+ 主色光环脉冲 0.7s,总时长 ≤900ms,`pointer-events:none` 不阻塞后续操作;同时 `navigator.vibrate(18)` 轻震动(U-03)。开关持久化在 `localStorage.iboard_celebrate`,`prefers-reduced-motion` 自动降级(U-07)。
|
||||||
|
|
||||||
|
### 3.4 交互清单
|
||||||
|
|
||||||
|
| 视图 | 主要交互 |
|
||||||
|
| --- | --- |
|
||||||
|
| 打卡 | 点环/点按钮打卡(乐观更新,失败回滚 + Toast)、撤销、编辑、归档、新建弹窗(名称/周期/目标次数/颜色/备注)、特效开关 |
|
||||||
|
| 日历 | 月份左右切换、回到今天、「昨天/今天/明天」分段切换、点日期选中、当日详情内打卡(未来日期只读显示「计划」)、本月完成率 |
|
||||||
|
| 清单 | 进行中/已归档切换、展开/收起、逐条勾选、添加检查项、全选/全不选、复制、存为模板、归档/恢复、删除、从模板新建 |
|
||||||
|
|
||||||
|
排序:打卡项列表**未完成在前**(F-CK-16);清单按 `updatedAt` 倒序。
|
||||||
|
|
||||||
|
### 3.5 视觉规范
|
||||||
|
|
||||||
|
| 类别 | 取值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 主色 | `#1677ff`;成功 `#00b578`;警告/部分 `#ffc53d`;超额 `#a855f7`;危险 `#ff3141` |
|
||||||
|
| 背景 / 卡片 | `#f5f5f5` / `#ffffff`,卡片 `radius 10px` + `0 1px 2px rgba(0,0,0,.04)` |
|
||||||
|
| 文字 | 主 `#333`、次 `#666`、弱 `#999` |
|
||||||
|
| 日历日状态点 | full 绿 / partial 黄 / over 紫 / empty 无 |
|
||||||
|
| 打卡项预设色 | `#1677ff #00b578 #ff8f1f #ff3141 #a855f7 #14b8a6 #f43f9e` |
|
||||||
|
|
||||||
|
完整高保真稿见 `界面设计.html`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 后端设计
|
||||||
|
|
||||||
|
### 4.1 通用约定
|
||||||
|
|
||||||
|
- 两个路由文件首行 `router.use(authRequired)`,**全部接口需登录**;用户维度一律取 `req.user.sub`,不信任 body 中的 `userId`。
|
||||||
|
- 响应统一 `{ item }` / `{ items }` / `{ ok }`;错误统一 `{ error: '中文提示' }` + 语义化状态码。
|
||||||
|
- 展示态(percent / completed / streak / dayDone …)由服务端计算后下发,前端不做二次口径推导,避免两端不一致。
|
||||||
|
|
||||||
|
### 4.2 接口清单
|
||||||
|
|
||||||
|
#### 打卡 `/api/checkin`
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 | 关键响应字段 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| GET | `/habits?date=` | 指定日待办 + 周期进度 | `items[]`(含 `done/target/percent/completed/over/streak/expectedPerDay/periodStart,End`)、`summary` |
|
||||||
|
| POST | `/habits` | 新建打卡项 | `item` |
|
||||||
|
| PUT | `/habits/:id` | 编辑(名称/周期/目标/颜色/备注/归档/排序) | `item` |
|
||||||
|
| DELETE | `/habits/:id?hard=1` | 默认归档;`hard=1` 物理删 | `{ ok, removed }` |
|
||||||
|
| POST | `/habits/:id/check` | 打一次卡(`{ date?, note? }`) | `item`(最新展示态) |
|
||||||
|
| DELETE | `/habits/:id/check?date=` | 撤销本周期最近一次 | `item` |
|
||||||
|
| GET | `/day?date=` | 某日每个打卡项明细 | `items[]`(含 `canCheck`)、`isToday`、`isFuture` |
|
||||||
|
| GET | `/calendar?month=YYYY-MM` | 月历 + 完成率 | `days[]`(`status/expected/actual/isToday/isFuture/habits[]`)、`rate`、`habits[]` |
|
||||||
|
|
||||||
|
#### 清单 `/api/checklists`
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| GET | `/?archived=0|1` | 清单列表(带 `done/total/percent/completed`) |
|
||||||
|
| POST | `/` | 新建(`{ title, scene, note?, items?, templateId? }`) |
|
||||||
|
| GET | `/:id` | 单条详情 |
|
||||||
|
| PUT | `/:id` | 改名称/场景/备注/归档 |
|
||||||
|
| DELETE | `/:id` | 物理删除(连带检查项) |
|
||||||
|
| POST | `/:id/duplicate` | 复制为新清单(勾选状态重置) |
|
||||||
|
| POST | `/:id/items` | 新增检查项 |
|
||||||
|
| PUT | `/items/:id` | 改标题/备注/勾选(`done` 变更时写 `doneAt`) |
|
||||||
|
| DELETE | `/items/:id` | 删除检查项 |
|
||||||
|
| POST | `/:id/check-all` | 一键全选/全不选(`{ done }`) |
|
||||||
|
| GET | `/templates` | 模板列表(含系统预置,首次访问幂等写入) |
|
||||||
|
| POST | `/templates` | 另存为模板 |
|
||||||
|
| DELETE | `/templates/:id` | 删除**自建**模板(预置模板拒绝) |
|
||||||
|
|
||||||
|
### 4.3 关键流程:打卡
|
||||||
|
|
||||||
|
```
|
||||||
|
用户点环
|
||||||
|
│
|
||||||
|
├─ 前端: 本地 +1,重算 percent/completed/over(乐观更新)
|
||||||
|
│
|
||||||
|
└─ POST /api/checkin/habits/:id/check
|
||||||
|
├─ 校验 habit 归属 → 404
|
||||||
|
├─ date > today → 400 不能为未来日期打卡
|
||||||
|
├─ date < today-7 → 400 最多只能补打最近 7 天
|
||||||
|
├─ seq = max(seq)+1 → insert
|
||||||
|
│ └─ P2002 → 409 该次打卡已记录(幂等)
|
||||||
|
└─ 返回最新 item
|
||||||
|
│
|
||||||
|
├─ 成功: 用服务端 item 覆盖本地;若 completed → fire 特效
|
||||||
|
└─ 失败: setData(before) 回滚 + Toast
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 关键流程:月历聚合
|
||||||
|
|
||||||
|
1. 取全部 Habit(含归档)→ 过滤 `archived=false` 得 `active`。
|
||||||
|
2. 一次查询当月 `[01, 月末]` 全部 CheckIn,按 `habitId → date → count` 分组。
|
||||||
|
3. 逐日遍历:仅统计 `Habit.createdAt <= 该日` 的项;累加 `expected += expectedPerDay(...)`、`actual += min(count, expected)`;任一 `count > expected` 记 `over`。
|
||||||
|
4. `status`:全完成且无超额 → `full`;全完成且有超额 → `over`;有完成未全完成 → `partial`;否则 `empty`。
|
||||||
|
5. 完成率 `rate = Σactual / Σexpected`(**只统计非未来日期**)。
|
||||||
|
|
||||||
|
复杂度:`O(天数 × 打卡项数)`,30×20=600 次内存运算,无 N+1 查询。
|
||||||
|
|
||||||
|
### 4.5 校验与错误码
|
||||||
|
|
||||||
|
| 状态码 | 场景 |
|
||||||
|
| --- | --- |
|
||||||
|
| 400 | 名称为空/超长、周期非法、target < 1、未来日期打卡、超出补打窗口、本周期无记录可撤销、模板缺条目 |
|
||||||
|
| 404 | 资源不存在或不属于当前用户 |
|
||||||
|
| 409 | 并发重复打卡(P2002) |
|
||||||
|
| 401 | 未登录 / token 失效(`authRequired`) |
|
||||||
|
|
||||||
|
所有写操作均用 `prisma.xxx.findFirst({ where: { id, userId } })` 或 `where: { id, list: { userId } }` 做归属校验,杜绝越权改他人数据。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 非功能设计
|
||||||
|
|
||||||
|
| 维度 | 设计 |
|
||||||
|
| --- | --- |
|
||||||
|
| 首屏 | 三视图懒切换,只有激活视图发请求;单视图首屏 ≤ 1 次列表请求(N-01) |
|
||||||
|
| 弱网/失败 | 乐观更新 + 失败回滚 + Toast;错误横幅带重试;`useRefreshOnVisible` 静默刷新 |
|
||||||
|
| 幂等 | 唯一约束 `(habitId, date, seq)` + 前端按钮 loading,防连点重复计次 |
|
||||||
|
| 时区 | 全链路 Asia/Shanghai;库内 `DATETIME` 存 UTC,业务日期用 `VARCHAR(10)` 本地日历日 |
|
||||||
|
| 兼容 | 375~430px 自适应;Tauri Android WebView 支持 `navigator.vibrate`,不支持时静默跳过 |
|
||||||
|
| 性能 | 月历/周期统计均为「一次范围查询 + 内存聚合」,避免逐项查询 |
|
||||||
|
| 安全 | 全接口 `authRequired`;输入长度截断(名称 20、检查项 40、备注 200);无 SQL 拼接(Prisma 参数化) |
|
||||||
|
| 可维护 | 周期口径单点(`server/checkin/period.js`),前端 `date.js` 与之对齐,改动需同步两处(已在文档与注释中标注) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 测试与验证
|
||||||
|
|
||||||
|
| 层级 | 内容 | 结果 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 单元 | 周期工具 23 条断言(窗口边界、周一起始、月末、闰年、日均期望、streak 跳过未结束周期) | 全通过 |
|
||||||
|
| 接口冒烟 | 自签 JWT 端到端:每天 1 次闭合、每周 3 次 33.3%、超额 +1、撤销归零、未来日期 400、月历 31 天、模板生成 11 项、全选完成、复制重置 | 全通过,测试数据已清理 |
|
||||||
|
| 构建 | `npm run build` | 通过 |
|
||||||
|
| 迁移 | `prisma migrate deploy` 到目标库 | 成功 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 里程碑(对应需求文档 M1~M5)
|
||||||
|
|
||||||
|
| 里程碑 | 内容 | 状态 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| M1 | 数据模型 + 迁移 + 周期模块 + 全部后端接口 | 完成 |
|
||||||
|
| M2 | 打卡视图:环形进度、特效、撤销/补打、新建编辑 | 完成 |
|
||||||
|
| M3 | 日历视图:月历 + 昨天/今天/明天 + 完成率 | 完成 |
|
||||||
|
| M4 | 清单视图:CRUD、勾选、进度、模板复用 | 完成 |
|
||||||
|
| M5 | 文档(需求/设计/数据库/界面)+ 构建校验 + 提交 | 完成 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 风险与权衡
|
||||||
|
|
||||||
|
| 项 | 权衡 | 缓解 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 周期口径前后端各实现一份 | 前端需离线即时反馈,不能等接口 | 两端算法同源同注释;口径变更必须同步 `period.js` 与 `date.js`(已写入项目记忆) |
|
||||||
|
| 进度实时聚合而非冗余计数 | 换取一致性与配置即时生效,牺牲少量 CPU | 单用户数据量极小(月历 30 天 × 20 项),实测无感;V0.3 可加汇总表 |
|
||||||
|
| 未来日期禁止打卡 | 防止「提前打完一周」刷进度,符合「日历看明天=计划」语义 | 明天视图只读展示「计划」,明确文案 |
|
||||||
|
| 特效可能打扰 | 部分用户不喜动效 | 一键开关 + `prefers-reduced-motion` 自动降级 |
|
||||||
|
| 新增第 5 个 Tab | 底部空间变拥挤 | 图标线性简洁;`TabBar` 原生 safe-area,5 项在 375px 下仍在可点区(≥44px) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 后续演进(V0.3+)
|
||||||
|
|
||||||
|
1. **提醒与通知**:`Habit.remindAt` + Tauri 本地通知,解决「忘打卡」。
|
||||||
|
2. **补打放宽 / 历史编辑**:当前 7 天限制可配置化。
|
||||||
|
3. **统计视图**:连续天数榜单、月/年热力图(届时引入 `HabitDailyStat` 汇总表)。
|
||||||
|
4. **打卡项分组与排序拖拽**:现有 `sortOrder` 已预留,仅需交互。
|
||||||
|
5. **清单协作/分享**:引入 `CheckListShare`。
|
||||||
|
6. **数据导出**:清单完成后导出为图片或文本(便于发群里确认)。
|
||||||
Reference in New Issue
Block a user