feat(V0.2): 打卡 / 日历 / 清单模块
- 数据: Habit/CheckIn/CheckList/CheckListItem/CheckListTemplate + 迁移 + 重新生成 client
- 后端: server/checkin/period.js 周期计算(日/周/月/年, 周一起始, Asia/Shanghai)
/api/checkin/* 打卡项CRUD/打卡/撤销/补打/某日详情/月历
/api/checklists/* 清单与检查项CRUD/勾选/全选/复制/模板(系统预置出差·回老家)
- 前端: 底部新增「打卡」Tab -> /checkin, 页内三视图
环形进度 RingProgress + 完成特效(粒子/光晕/触感, 可关闭)
打卡视图: 待办列表/乐观更新/撤销/新建编辑
日历视图: 月历网格 + 今天·昨天·明天 + 日期标记/完成率
清单视图: 场景清单/勾选进度条/模板生成/另存模板/归档
- 文档: docs/V0.2/需求文档.md 补充实现约定(附录B)与接口清单
This commit is contained in:
@@ -45,3 +45,5 @@ cache/apizero/
|
|||||||
.cache/
|
.cache/
|
||||||
# 米家凭证缓存 (含 token, 绝不能提交)
|
# 米家凭证缓存 (含 token, 绝不能提交)
|
||||||
server/mihome/.cache/
|
server/mihome/.cache/
|
||||||
|
# 工作区记忆/会话数据 (本地私有, 不入库)
|
||||||
|
.workbuddy/
|
||||||
|
|||||||
+47
-8
@@ -1,6 +1,6 @@
|
|||||||
# iBoard V0.2 需求文档 — 打卡 / 日历 / 清单
|
# iBoard V0.2 需求文档 — 打卡 / 日历 / 清单
|
||||||
|
|
||||||
> 版本:V0.2 · 分支:`dev-0.2` · 状态:待评审 · 日期:2026-10-22
|
> 版本:V0.2 · 分支:`dev-0.2` · 日期:2026-10-22
|
||||||
> 目标项目:iboard(Tauri 2 + React 18 + antd-mobile + Express + Prisma/MariaDB,移动端优先)
|
> 目标项目:iboard(Tauri 2 + React 18 + antd-mobile + Express + Prisma/MariaDB,移动端优先)
|
||||||
> 基线版本:V0.1(`dev-0.1`)
|
> 基线版本:V0.1(`dev-0.1`)
|
||||||
|
|
||||||
@@ -8,16 +8,16 @@
|
|||||||
|
|
||||||
## 1. 总体描述
|
## 1. 总体描述
|
||||||
|
|
||||||
在 iBoard 移动端新增一个「打卡」模块,用**一个页面、三个视图**解决两类日常需求:
|
在 iBoard 移动端新增一个「打卡」模块一个「清单」模块,满足两类日常需求:
|
||||||
|
### 打卡
|
||||||
1. **周期性打卡**(习惯养成):用户定义"每天 n 次 / 每周 n 次 / 每月 n 次 / 每年 n 次"的目标,
|
1. **周期性打卡**(习惯养成):用户定义"每天 n 次 / 每周 n 次 / 每月 n 次 / 每年 n 次"的目标,
|
||||||
每次打卡后卡片上的**环形进度**按 `已完成次数 / 目标次数` 增长,达标时环形闭合并播放完成特效;
|
每次打卡后卡片上的**环形进度**按 `已完成次数 / 目标次数` 增长,达标时环形闭合并播放完成特效;
|
||||||
2. **日历回看**:以日历为载体,快速查看**今天**(要做什么)、**昨天**(做得怎样)、
|
2. **日历回看**:以日历为载体,快速查看**今天**(要做什么)、**昨天**(做得怎样)、
|
||||||
**明天**(计划做什么),日期上以圆点/实心标记完成情况;
|
**明天**(计划做什么),日期上以圆点/实心标记完成情况;
|
||||||
3. **检查清单**(场景化一次性事项):面向**出差、回老家**等临时场景,逐条勾选确认,
|
|
||||||
避免遗漏;清单可复用为模板,下次一键生成。
|
|
||||||
|
|
||||||
一句话:**用环形进度做周期性打卡,用日历做时间维度的回看与计划,用清单覆盖临时性的检查场景。**
|
### 清单
|
||||||
|
1. **清单**(场景化一次性事项):面向**出差、回老家**等临时场景,逐条勾选确认,
|
||||||
|
避免遗漏;清单可复用为模板,下次一键生成。
|
||||||
|
|
||||||
### 1.1 本期范围
|
### 1.1 本期范围
|
||||||
|
|
||||||
@@ -117,8 +117,8 @@
|
|||||||
|
|
||||||
| 编号 | 需求描述 | 验收要点 | 优先级 |
|
| 编号 | 需求描述 | 验收要点 | 优先级 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| F-SY-01 | 底部 TabBar 新增**「打卡」页**(现有 4 个 Tab → 5 个),作为本模块唯一入口 | 图标风格与现有 TabIcon 一致 | P0 |
|
| F-SY-01 | 底部 TabBar 新增**「打卡」页** 和 **「清单」页** | 图标风格与现有 TabIcon 一致 | P0 |
|
||||||
| F-SY-02 | 「打卡」页内以子 Tab(Segmented / SwipeTabs)切换 **打卡 / 日历 / 清单** 三个视图 | 复用现有 `SwipeTabs` 组件 | P0 |
|
| F-SY-02 | 「打卡」页内集成 打卡/日历 两个功能,日历放在上半截,默认显示一行为周视图,下拉显示为月视图。 | 日历中选择时间与下部打卡任务联动 | P0 |
|
||||||
| F-SY-03 | 所有接口复用现有鉴权(`authRequired`,JWT),数据按 `userId` 隔离 | 未登录跳转 `/login` | P0 |
|
| F-SY-03 | 所有接口复用现有鉴权(`authRequired`,JWT),数据按 `userId` 隔离 | 未登录跳转 `/login` | P0 |
|
||||||
| F-SY-04 | 后端新增 REST 路由(建议 `/api/checkin/*`、`/api/checklist/*`),挂载到 `server/index.js` | 与现有路由风格一致 | P0 |
|
| F-SY-04 | 后端新增 REST 路由(建议 `/api/checkin/*`、`/api/checklist/*`),挂载到 `server/index.js` | 与现有路由风格一致 | P0 |
|
||||||
| F-SY-05 | 时间处理统一 **Asia/Shanghai**,前端与后端对"日历日"的判定一致 | 跨零点打卡归属当天 | P0 |
|
| F-SY-05 | 时间处理统一 **Asia/Shanghai**,前端与后端对"日历日"的判定一致 | 跨零点打卡归属当天 | P0 |
|
||||||
@@ -293,3 +293,42 @@ model CheckListTemplate {
|
|||||||
```
|
```
|
||||||
|
|
||||||
> 说明:周期进度由 `CheckIn` 按周期窗口现场聚合(不冗余计数字段),避免并发与修改 n 后的一致性问题。
|
> 说明:周期进度由 `CheckIn` 按周期窗口现场聚合(不冗余计数字段),避免并发与修改 n 后的一致性问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录 B:实现约定(开发期补充,用于消除口径歧义)
|
||||||
|
|
||||||
|
| # | 约定 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| B-1 | **周期窗口** | 日=自然日;周=周一 00:00 ~ 周日 23:59;月=自然月;年=自然年;时区 Asia/Shanghai |
|
||||||
|
| B-2 | **环形进度** | 周期完成度 = 周期内累计打卡次数 / n(每周 n 次:打 1 次=1/n 环;每天 1 次:打 1 次直接闭合) |
|
||||||
|
| B-3 | **日历日期标记** | 用"当日完成度":当日次数 / **日均期望次数**(每天 n 次→n;每周 n 次→ceil(n/7);每月→ceil(n/当月天数);每年→ceil(n/365))。全完成=实心绿点,部分=黄点,超额=紫点,无记录=不显示 |
|
||||||
|
| B-4 | **streak** | 从当前周期往回数连续达标的周期数;**当前周期未结束且未达标不算断签**(跳过继续往前数) |
|
||||||
|
| B-5 | **补打限制** | 只能补打最近 7 天(`BACKFILL_DAYS`),未来日期禁止打卡 |
|
||||||
|
| B-6 | **撤销语义** | 撤销删除本周期内"最近一次"打卡(按 date desc、seq desc 取第一条) |
|
||||||
|
| B-7 | **删除打卡项** | 默认归档(`archived=true`,历史保留);`?hard=1` 才物理删除并清记录 |
|
||||||
|
| B-8 | **接口前缀** | 打卡 `/api/checkin/*`、清单 `/api/checklists/*`,全部 `authRequired`,数据按 `userId` 隔离 |
|
||||||
|
| B-9 | **前端入口** | 底部新增第 5 个 Tab「打卡」→ `/checkin`,页内三视图:打卡 / 日历 / 清单 |
|
||||||
|
| B-10 | **特效开关** | 完成特效 + 触感反馈受 `localStorage iboard_celebrate` 控制(默认开),系统开启"减弱动效"时自动降级 |
|
||||||
|
|
||||||
|
### 接口清单
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| GET | `/api/checkin/habits?date=` | 打卡项列表 + 当前周期进度 + streak |
|
||||||
|
| POST | `/api/checkin/habits` | 新建打卡项 |
|
||||||
|
| PUT | `/api/checkin/habits/:id` | 编辑(改 n / 周期 / 归档) |
|
||||||
|
| DELETE | `/api/checkin/habits/:id?hard=1` | 归档(默认)/ 物理删除 |
|
||||||
|
| POST | `/api/checkin/habits/:id/check` | 打卡一次(可带 `date` 补打、`note`) |
|
||||||
|
| DELETE | `/api/checkin/habits/:id/check?date=` | 撤销本周期最近一次 |
|
||||||
|
| GET | `/api/checkin/day?date=` | 某日详情(今天可打卡 / 昨天可补打 / 明天只读) |
|
||||||
|
| GET | `/api/checkin/calendar?month=YYYY-MM` | 月历:每日状态 + 当月完成率 |
|
||||||
|
| GET | `/api/checklists?archived=0\|1` | 清单列表(含进度) |
|
||||||
|
| POST | `/api/checklists` | 新建清单(可带 `templateId` 从模板生成) |
|
||||||
|
| GET/PUT/DELETE | `/api/checklists/:id` | 详情 / 编辑 / 删除 |
|
||||||
|
| POST | `/api/checklists/:id/duplicate` | 复制为新清单(勾选重置) |
|
||||||
|
| POST | `/api/checklists/:id/items` | 新增检查项 |
|
||||||
|
| PUT/DELETE | `/api/checklists/items/:id` | 改标题 / 勾选(`done`) / 删除 |
|
||||||
|
| POST | `/api/checklists/:id/check-all` | 一键全选 / 全不选 |
|
||||||
|
| GET/POST | `/api/checklists/templates` | 模板列表(含系统预置)/ 另存为模板 |
|
||||||
|
| DELETE | `/api/checklists/templates/:id` | 删除自定义模板 |
|
||||||
|
|||||||
@@ -107,3 +107,28 @@ export type MihomeEnvSample = Prisma.MihomeEnvSampleModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type MihomeEnvHour = Prisma.MihomeEnvHourModel
|
export type MihomeEnvHour = Prisma.MihomeEnvHourModel
|
||||||
|
/**
|
||||||
|
* Model Habit
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Habit = Prisma.HabitModel
|
||||||
|
/**
|
||||||
|
* Model CheckIn
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type CheckIn = Prisma.CheckInModel
|
||||||
|
/**
|
||||||
|
* Model CheckList
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type CheckList = Prisma.CheckListModel
|
||||||
|
/**
|
||||||
|
* Model CheckListItem
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type CheckListItem = Prisma.CheckListItemModel
|
||||||
|
/**
|
||||||
|
* Model CheckListTemplate
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type CheckListTemplate = Prisma.CheckListTemplateModel
|
||||||
|
|||||||
@@ -131,3 +131,28 @@ export type MihomeEnvSample = Prisma.MihomeEnvSampleModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type MihomeEnvHour = Prisma.MihomeEnvHourModel
|
export type MihomeEnvHour = Prisma.MihomeEnvHourModel
|
||||||
|
/**
|
||||||
|
* Model Habit
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Habit = Prisma.HabitModel
|
||||||
|
/**
|
||||||
|
* Model CheckIn
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type CheckIn = Prisma.CheckInModel
|
||||||
|
/**
|
||||||
|
* Model CheckList
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type CheckList = Prisma.CheckListModel
|
||||||
|
/**
|
||||||
|
* Model CheckListItem
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type CheckListItem = Prisma.CheckListItemModel
|
||||||
|
/**
|
||||||
|
* Model CheckListTemplate
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type CheckListTemplate = Prisma.CheckListTemplateModel
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -401,7 +401,12 @@ export const ModelName = {
|
|||||||
MihomeHome: 'MihomeHome',
|
MihomeHome: 'MihomeHome',
|
||||||
MihomeDevice: 'MihomeDevice',
|
MihomeDevice: 'MihomeDevice',
|
||||||
MihomeEnvSample: 'MihomeEnvSample',
|
MihomeEnvSample: 'MihomeEnvSample',
|
||||||
MihomeEnvHour: 'MihomeEnvHour'
|
MihomeEnvHour: 'MihomeEnvHour',
|
||||||
|
Habit: 'Habit',
|
||||||
|
CheckIn: 'CheckIn',
|
||||||
|
CheckList: 'CheckList',
|
||||||
|
CheckListItem: 'CheckListItem',
|
||||||
|
CheckListTemplate: 'CheckListTemplate'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
@@ -417,7 +422,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
omit: GlobalOmitOptions
|
omit: GlobalOmitOptions
|
||||||
}
|
}
|
||||||
meta: {
|
meta: {
|
||||||
modelProps: "user" | "article" | "strategy" | "indicator" | "indicatorPoint" | "news" | "alert" | "serverNode" | "serverNodeHistory" | "serviceCheck" | "econ_SowInventory" | "scraperCache" | "oilPriceHistory" | "mihomeAccount" | "mihomeHome" | "mihomeDevice" | "mihomeEnvSample" | "mihomeEnvHour"
|
modelProps: "user" | "article" | "strategy" | "indicator" | "indicatorPoint" | "news" | "alert" | "serverNode" | "serverNodeHistory" | "serviceCheck" | "econ_SowInventory" | "scraperCache" | "oilPriceHistory" | "mihomeAccount" | "mihomeHome" | "mihomeDevice" | "mihomeEnvSample" | "mihomeEnvHour" | "habit" | "checkIn" | "checkList" | "checkListItem" | "checkListTemplate"
|
||||||
txIsolationLevel: TransactionIsolationLevel
|
txIsolationLevel: TransactionIsolationLevel
|
||||||
}
|
}
|
||||||
model: {
|
model: {
|
||||||
@@ -1609,6 +1614,336 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Habit: {
|
||||||
|
payload: Prisma.$HabitPayload<ExtArgs>
|
||||||
|
fields: Prisma.HabitFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.HabitFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$HabitPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.HabitFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$HabitPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.HabitFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$HabitPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.HabitFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$HabitPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.HabitFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$HabitPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.HabitCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$HabitPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.HabitCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.HabitDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$HabitPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.HabitUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$HabitPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.HabitDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.HabitUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.HabitUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$HabitPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.HabitAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateHabit>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.HabitGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.HabitGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.HabitCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.HabitCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CheckIn: {
|
||||||
|
payload: Prisma.$CheckInPayload<ExtArgs>
|
||||||
|
fields: Prisma.CheckInFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.CheckInFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckInPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.CheckInFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckInPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.CheckInFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckInPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.CheckInFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckInPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.CheckInFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckInPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.CheckInCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckInPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.CheckInCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.CheckInDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckInPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.CheckInUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckInPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.CheckInDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.CheckInUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.CheckInUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckInPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.CheckInAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateCheckIn>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.CheckInGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.CheckInGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.CheckInCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.CheckInCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CheckList: {
|
||||||
|
payload: Prisma.$CheckListPayload<ExtArgs>
|
||||||
|
fields: Prisma.CheckListFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.CheckListFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.CheckListFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.CheckListFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.CheckListFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.CheckListFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.CheckListCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.CheckListCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.CheckListDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.CheckListUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.CheckListDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.CheckListUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.CheckListUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.CheckListAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateCheckList>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.CheckListGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.CheckListGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.CheckListCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.CheckListCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CheckListItem: {
|
||||||
|
payload: Prisma.$CheckListItemPayload<ExtArgs>
|
||||||
|
fields: Prisma.CheckListItemFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.CheckListItemFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListItemPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.CheckListItemFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListItemPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.CheckListItemFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListItemPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.CheckListItemFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListItemPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.CheckListItemFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListItemPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.CheckListItemCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListItemPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.CheckListItemCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.CheckListItemDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListItemPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.CheckListItemUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListItemPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.CheckListItemDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.CheckListItemUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.CheckListItemUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListItemPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.CheckListItemAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateCheckListItem>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.CheckListItemGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.CheckListItemGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.CheckListItemCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.CheckListItemCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CheckListTemplate: {
|
||||||
|
payload: Prisma.$CheckListTemplatePayload<ExtArgs>
|
||||||
|
fields: Prisma.CheckListTemplateFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.CheckListTemplateFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListTemplatePayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.CheckListTemplateFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListTemplatePayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.CheckListTemplateFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListTemplatePayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.CheckListTemplateFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListTemplatePayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.CheckListTemplateFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListTemplatePayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.CheckListTemplateCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListTemplatePayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.CheckListTemplateCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.CheckListTemplateDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListTemplatePayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.CheckListTemplateUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListTemplatePayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.CheckListTemplateDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.CheckListTemplateUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.CheckListTemplateUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CheckListTemplatePayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.CheckListTemplateAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateCheckListTemplate>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.CheckListTemplateGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.CheckListTemplateGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.CheckListTemplateCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.CheckListTemplateCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} & {
|
} & {
|
||||||
other: {
|
other: {
|
||||||
@@ -1924,6 +2259,81 @@ export const MihomeEnvHourScalarFieldEnum = {
|
|||||||
export type MihomeEnvHourScalarFieldEnum = (typeof MihomeEnvHourScalarFieldEnum)[keyof typeof MihomeEnvHourScalarFieldEnum]
|
export type MihomeEnvHourScalarFieldEnum = (typeof MihomeEnvHourScalarFieldEnum)[keyof typeof MihomeEnvHourScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const HabitScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
userId: 'userId',
|
||||||
|
name: 'name',
|
||||||
|
period: 'period',
|
||||||
|
target: 'target',
|
||||||
|
color: 'color',
|
||||||
|
icon: 'icon',
|
||||||
|
note: 'note',
|
||||||
|
archived: 'archived',
|
||||||
|
sortOrder: 'sortOrder',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type HabitScalarFieldEnum = (typeof HabitScalarFieldEnum)[keyof typeof HabitScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckInScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
habitId: 'habitId',
|
||||||
|
userId: 'userId',
|
||||||
|
date: 'date',
|
||||||
|
seq: 'seq',
|
||||||
|
note: 'note',
|
||||||
|
source: 'source',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckInScalarFieldEnum = (typeof CheckInScalarFieldEnum)[keyof typeof CheckInScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckListScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
userId: 'userId',
|
||||||
|
title: 'title',
|
||||||
|
scene: 'scene',
|
||||||
|
note: 'note',
|
||||||
|
archived: 'archived',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckListScalarFieldEnum = (typeof CheckListScalarFieldEnum)[keyof typeof CheckListScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckListItemScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
listId: 'listId',
|
||||||
|
title: 'title',
|
||||||
|
note: 'note',
|
||||||
|
done: 'done',
|
||||||
|
doneAt: 'doneAt',
|
||||||
|
sortOrder: 'sortOrder',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckListItemScalarFieldEnum = (typeof CheckListItemScalarFieldEnum)[keyof typeof CheckListItemScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckListTemplateScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
userId: 'userId',
|
||||||
|
name: 'name',
|
||||||
|
scene: 'scene',
|
||||||
|
items: 'items',
|
||||||
|
builtin: 'builtin',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckListTemplateScalarFieldEnum = (typeof CheckListTemplateScalarFieldEnum)[keyof typeof CheckListTemplateScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const SortOrder = {
|
export const SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
@@ -2128,6 +2538,51 @@ export const MihomeEnvSampleOrderByRelevanceFieldEnum = {
|
|||||||
export type MihomeEnvSampleOrderByRelevanceFieldEnum = (typeof MihomeEnvSampleOrderByRelevanceFieldEnum)[keyof typeof MihomeEnvSampleOrderByRelevanceFieldEnum]
|
export type MihomeEnvSampleOrderByRelevanceFieldEnum = (typeof MihomeEnvSampleOrderByRelevanceFieldEnum)[keyof typeof MihomeEnvSampleOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const HabitOrderByRelevanceFieldEnum = {
|
||||||
|
name: 'name',
|
||||||
|
period: 'period',
|
||||||
|
color: 'color',
|
||||||
|
icon: 'icon',
|
||||||
|
note: 'note'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type HabitOrderByRelevanceFieldEnum = (typeof HabitOrderByRelevanceFieldEnum)[keyof typeof HabitOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckInOrderByRelevanceFieldEnum = {
|
||||||
|
date: 'date',
|
||||||
|
note: 'note',
|
||||||
|
source: 'source'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckInOrderByRelevanceFieldEnum = (typeof CheckInOrderByRelevanceFieldEnum)[keyof typeof CheckInOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckListOrderByRelevanceFieldEnum = {
|
||||||
|
title: 'title',
|
||||||
|
scene: 'scene',
|
||||||
|
note: 'note'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckListOrderByRelevanceFieldEnum = (typeof CheckListOrderByRelevanceFieldEnum)[keyof typeof CheckListOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckListItemOrderByRelevanceFieldEnum = {
|
||||||
|
title: 'title',
|
||||||
|
note: 'note'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckListItemOrderByRelevanceFieldEnum = (typeof CheckListItemOrderByRelevanceFieldEnum)[keyof typeof CheckListItemOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckListTemplateOrderByRelevanceFieldEnum = {
|
||||||
|
name: 'name',
|
||||||
|
scene: 'scene'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckListTemplateOrderByRelevanceFieldEnum = (typeof CheckListTemplateOrderByRelevanceFieldEnum)[keyof typeof CheckListTemplateOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Field references
|
* Field references
|
||||||
@@ -2302,6 +2757,11 @@ export type GlobalOmitConfig = {
|
|||||||
mihomeDevice?: Prisma.MihomeDeviceOmit
|
mihomeDevice?: Prisma.MihomeDeviceOmit
|
||||||
mihomeEnvSample?: Prisma.MihomeEnvSampleOmit
|
mihomeEnvSample?: Prisma.MihomeEnvSampleOmit
|
||||||
mihomeEnvHour?: Prisma.MihomeEnvHourOmit
|
mihomeEnvHour?: Prisma.MihomeEnvHourOmit
|
||||||
|
habit?: Prisma.HabitOmit
|
||||||
|
checkIn?: Prisma.CheckInOmit
|
||||||
|
checkList?: Prisma.CheckListOmit
|
||||||
|
checkListItem?: Prisma.CheckListItemOmit
|
||||||
|
checkListTemplate?: Prisma.CheckListTemplateOmit
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Types for Logging */
|
/* Types for Logging */
|
||||||
|
|||||||
@@ -68,7 +68,12 @@ export const ModelName = {
|
|||||||
MihomeHome: 'MihomeHome',
|
MihomeHome: 'MihomeHome',
|
||||||
MihomeDevice: 'MihomeDevice',
|
MihomeDevice: 'MihomeDevice',
|
||||||
MihomeEnvSample: 'MihomeEnvSample',
|
MihomeEnvSample: 'MihomeEnvSample',
|
||||||
MihomeEnvHour: 'MihomeEnvHour'
|
MihomeEnvHour: 'MihomeEnvHour',
|
||||||
|
Habit: 'Habit',
|
||||||
|
CheckIn: 'CheckIn',
|
||||||
|
CheckList: 'CheckList',
|
||||||
|
CheckListItem: 'CheckListItem',
|
||||||
|
CheckListTemplate: 'CheckListTemplate'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
@@ -363,6 +368,81 @@ export const MihomeEnvHourScalarFieldEnum = {
|
|||||||
export type MihomeEnvHourScalarFieldEnum = (typeof MihomeEnvHourScalarFieldEnum)[keyof typeof MihomeEnvHourScalarFieldEnum]
|
export type MihomeEnvHourScalarFieldEnum = (typeof MihomeEnvHourScalarFieldEnum)[keyof typeof MihomeEnvHourScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const HabitScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
userId: 'userId',
|
||||||
|
name: 'name',
|
||||||
|
period: 'period',
|
||||||
|
target: 'target',
|
||||||
|
color: 'color',
|
||||||
|
icon: 'icon',
|
||||||
|
note: 'note',
|
||||||
|
archived: 'archived',
|
||||||
|
sortOrder: 'sortOrder',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type HabitScalarFieldEnum = (typeof HabitScalarFieldEnum)[keyof typeof HabitScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckInScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
habitId: 'habitId',
|
||||||
|
userId: 'userId',
|
||||||
|
date: 'date',
|
||||||
|
seq: 'seq',
|
||||||
|
note: 'note',
|
||||||
|
source: 'source',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckInScalarFieldEnum = (typeof CheckInScalarFieldEnum)[keyof typeof CheckInScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckListScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
userId: 'userId',
|
||||||
|
title: 'title',
|
||||||
|
scene: 'scene',
|
||||||
|
note: 'note',
|
||||||
|
archived: 'archived',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckListScalarFieldEnum = (typeof CheckListScalarFieldEnum)[keyof typeof CheckListScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckListItemScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
listId: 'listId',
|
||||||
|
title: 'title',
|
||||||
|
note: 'note',
|
||||||
|
done: 'done',
|
||||||
|
doneAt: 'doneAt',
|
||||||
|
sortOrder: 'sortOrder',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckListItemScalarFieldEnum = (typeof CheckListItemScalarFieldEnum)[keyof typeof CheckListItemScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckListTemplateScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
userId: 'userId',
|
||||||
|
name: 'name',
|
||||||
|
scene: 'scene',
|
||||||
|
items: 'items',
|
||||||
|
builtin: 'builtin',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckListTemplateScalarFieldEnum = (typeof CheckListTemplateScalarFieldEnum)[keyof typeof CheckListTemplateScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const SortOrder = {
|
export const SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
@@ -566,3 +646,48 @@ export const MihomeEnvSampleOrderByRelevanceFieldEnum = {
|
|||||||
|
|
||||||
export type MihomeEnvSampleOrderByRelevanceFieldEnum = (typeof MihomeEnvSampleOrderByRelevanceFieldEnum)[keyof typeof MihomeEnvSampleOrderByRelevanceFieldEnum]
|
export type MihomeEnvSampleOrderByRelevanceFieldEnum = (typeof MihomeEnvSampleOrderByRelevanceFieldEnum)[keyof typeof MihomeEnvSampleOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const HabitOrderByRelevanceFieldEnum = {
|
||||||
|
name: 'name',
|
||||||
|
period: 'period',
|
||||||
|
color: 'color',
|
||||||
|
icon: 'icon',
|
||||||
|
note: 'note'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type HabitOrderByRelevanceFieldEnum = (typeof HabitOrderByRelevanceFieldEnum)[keyof typeof HabitOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckInOrderByRelevanceFieldEnum = {
|
||||||
|
date: 'date',
|
||||||
|
note: 'note',
|
||||||
|
source: 'source'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckInOrderByRelevanceFieldEnum = (typeof CheckInOrderByRelevanceFieldEnum)[keyof typeof CheckInOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckListOrderByRelevanceFieldEnum = {
|
||||||
|
title: 'title',
|
||||||
|
scene: 'scene',
|
||||||
|
note: 'note'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckListOrderByRelevanceFieldEnum = (typeof CheckListOrderByRelevanceFieldEnum)[keyof typeof CheckListOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckListItemOrderByRelevanceFieldEnum = {
|
||||||
|
title: 'title',
|
||||||
|
note: 'note'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckListItemOrderByRelevanceFieldEnum = (typeof CheckListItemOrderByRelevanceFieldEnum)[keyof typeof CheckListItemOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CheckListTemplateOrderByRelevanceFieldEnum = {
|
||||||
|
name: 'name',
|
||||||
|
scene: 'scene'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CheckListTemplateOrderByRelevanceFieldEnum = (typeof CheckListTemplateOrderByRelevanceFieldEnum)[keyof typeof CheckListTemplateOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|||||||
@@ -26,4 +26,9 @@ export type * from './models/MihomeHome.ts'
|
|||||||
export type * from './models/MihomeDevice.ts'
|
export type * from './models/MihomeDevice.ts'
|
||||||
export type * from './models/MihomeEnvSample.ts'
|
export type * from './models/MihomeEnvSample.ts'
|
||||||
export type * from './models/MihomeEnvHour.ts'
|
export type * from './models/MihomeEnvHour.ts'
|
||||||
|
export type * from './models/Habit.ts'
|
||||||
|
export type * from './models/CheckIn.ts'
|
||||||
|
export type * from './models/CheckList.ts'
|
||||||
|
export type * from './models/CheckListItem.ts'
|
||||||
|
export type * from './models/CheckListTemplate.ts'
|
||||||
export type * from './commonInputTypes.ts'
|
export type * from './commonInputTypes.ts'
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -224,6 +224,10 @@ export type UserWhereInput = {
|
|||||||
password?: Prisma.StringFilter<"User"> | string
|
password?: Prisma.StringFilter<"User"> | string
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
|
habits?: Prisma.HabitListRelationFilter
|
||||||
|
checkIns?: Prisma.CheckInListRelationFilter
|
||||||
|
checkLists?: Prisma.CheckListListRelationFilter
|
||||||
|
templates?: Prisma.CheckListTemplateListRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserOrderByWithRelationInput = {
|
export type UserOrderByWithRelationInput = {
|
||||||
@@ -233,6 +237,10 @@ export type UserOrderByWithRelationInput = {
|
|||||||
password?: Prisma.SortOrder
|
password?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
|
habits?: Prisma.HabitOrderByRelationAggregateInput
|
||||||
|
checkIns?: Prisma.CheckInOrderByRelationAggregateInput
|
||||||
|
checkLists?: Prisma.CheckListOrderByRelationAggregateInput
|
||||||
|
templates?: Prisma.CheckListTemplateOrderByRelationAggregateInput
|
||||||
_relevance?: Prisma.UserOrderByRelevanceInput
|
_relevance?: Prisma.UserOrderByRelevanceInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,6 +254,10 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
password?: Prisma.StringFilter<"User"> | string
|
password?: Prisma.StringFilter<"User"> | string
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
|
habits?: Prisma.HabitListRelationFilter
|
||||||
|
checkIns?: Prisma.CheckInListRelationFilter
|
||||||
|
checkLists?: Prisma.CheckListListRelationFilter
|
||||||
|
templates?: Prisma.CheckListTemplateListRelationFilter
|
||||||
}, "id" | "email">
|
}, "id" | "email">
|
||||||
|
|
||||||
export type UserOrderByWithAggregationInput = {
|
export type UserOrderByWithAggregationInput = {
|
||||||
@@ -280,6 +292,10 @@ export type UserCreateInput = {
|
|||||||
password: string
|
password: string
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
habits?: Prisma.HabitCreateNestedManyWithoutUserInput
|
||||||
|
checkIns?: Prisma.CheckInCreateNestedManyWithoutUserInput
|
||||||
|
checkLists?: Prisma.CheckListCreateNestedManyWithoutUserInput
|
||||||
|
templates?: Prisma.CheckListTemplateCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedCreateInput = {
|
export type UserUncheckedCreateInput = {
|
||||||
@@ -289,6 +305,10 @@ export type UserUncheckedCreateInput = {
|
|||||||
password: string
|
password: string
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
habits?: Prisma.HabitUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
checkIns?: Prisma.CheckInUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
checkLists?: Prisma.CheckListUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
templates?: Prisma.CheckListTemplateUncheckedCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUpdateInput = {
|
export type UserUpdateInput = {
|
||||||
@@ -297,6 +317,10 @@ export type UserUpdateInput = {
|
|||||||
password?: Prisma.StringFieldUpdateOperationsInput | string
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
habits?: Prisma.HabitUpdateManyWithoutUserNestedInput
|
||||||
|
checkIns?: Prisma.CheckInUpdateManyWithoutUserNestedInput
|
||||||
|
checkLists?: Prisma.CheckListUpdateManyWithoutUserNestedInput
|
||||||
|
templates?: Prisma.CheckListTemplateUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedUpdateInput = {
|
export type UserUncheckedUpdateInput = {
|
||||||
@@ -306,6 +330,10 @@ export type UserUncheckedUpdateInput = {
|
|||||||
password?: Prisma.StringFieldUpdateOperationsInput | string
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
habits?: Prisma.HabitUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
checkIns?: Prisma.CheckInUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
checkLists?: Prisma.CheckListUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
templates?: Prisma.CheckListTemplateUncheckedUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateManyInput = {
|
export type UserCreateManyInput = {
|
||||||
@@ -375,6 +403,16 @@ export type UserSumOrderByAggregateInput = {
|
|||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserScalarRelationFilter = {
|
||||||
|
is?: Prisma.UserWhereInput
|
||||||
|
isNot?: Prisma.UserWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserNullableScalarRelationFilter = {
|
||||||
|
is?: Prisma.UserWhereInput | null
|
||||||
|
isNot?: Prisma.UserWhereInput | null
|
||||||
|
}
|
||||||
|
|
||||||
export type StringFieldUpdateOperationsInput = {
|
export type StringFieldUpdateOperationsInput = {
|
||||||
set?: string
|
set?: string
|
||||||
}
|
}
|
||||||
@@ -391,6 +429,368 @@ export type IntFieldUpdateOperationsInput = {
|
|||||||
divide?: number
|
divide?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserCreateNestedOneWithoutHabitsInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutHabitsInput, Prisma.UserUncheckedCreateWithoutHabitsInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutHabitsInput
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateOneRequiredWithoutHabitsNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutHabitsInput, Prisma.UserUncheckedCreateWithoutHabitsInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutHabitsInput
|
||||||
|
upsert?: Prisma.UserUpsertWithoutHabitsInput
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutHabitsInput, Prisma.UserUpdateWithoutHabitsInput>, Prisma.UserUncheckedUpdateWithoutHabitsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateNestedOneWithoutCheckInsInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutCheckInsInput, Prisma.UserUncheckedCreateWithoutCheckInsInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutCheckInsInput
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateOneRequiredWithoutCheckInsNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutCheckInsInput, Prisma.UserUncheckedCreateWithoutCheckInsInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutCheckInsInput
|
||||||
|
upsert?: Prisma.UserUpsertWithoutCheckInsInput
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutCheckInsInput, Prisma.UserUpdateWithoutCheckInsInput>, Prisma.UserUncheckedUpdateWithoutCheckInsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateNestedOneWithoutCheckListsInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutCheckListsInput, Prisma.UserUncheckedCreateWithoutCheckListsInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutCheckListsInput
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateOneRequiredWithoutCheckListsNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutCheckListsInput, Prisma.UserUncheckedCreateWithoutCheckListsInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutCheckListsInput
|
||||||
|
upsert?: Prisma.UserUpsertWithoutCheckListsInput
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutCheckListsInput, Prisma.UserUpdateWithoutCheckListsInput>, Prisma.UserUncheckedUpdateWithoutCheckListsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateNestedOneWithoutTemplatesInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutTemplatesInput, Prisma.UserUncheckedCreateWithoutTemplatesInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutTemplatesInput
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateOneWithoutTemplatesNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutTemplatesInput, Prisma.UserUncheckedCreateWithoutTemplatesInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutTemplatesInput
|
||||||
|
upsert?: Prisma.UserUpsertWithoutTemplatesInput
|
||||||
|
disconnect?: Prisma.UserWhereInput | boolean
|
||||||
|
delete?: Prisma.UserWhereInput | boolean
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutTemplatesInput, Prisma.UserUpdateWithoutTemplatesInput>, Prisma.UserUncheckedUpdateWithoutTemplatesInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateWithoutHabitsInput = {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
checkIns?: Prisma.CheckInCreateNestedManyWithoutUserInput
|
||||||
|
checkLists?: Prisma.CheckListCreateNestedManyWithoutUserInput
|
||||||
|
templates?: Prisma.CheckListTemplateCreateNestedManyWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedCreateWithoutHabitsInput = {
|
||||||
|
id?: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
checkIns?: Prisma.CheckInUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
checkLists?: Prisma.CheckListUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
templates?: Prisma.CheckListTemplateUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateOrConnectWithoutHabitsInput = {
|
||||||
|
where: Prisma.UserWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutHabitsInput, Prisma.UserUncheckedCreateWithoutHabitsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpsertWithoutHabitsInput = {
|
||||||
|
update: Prisma.XOR<Prisma.UserUpdateWithoutHabitsInput, Prisma.UserUncheckedUpdateWithoutHabitsInput>
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutHabitsInput, Prisma.UserUncheckedCreateWithoutHabitsInput>
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateToOneWithWhereWithoutHabitsInput = {
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.UserUpdateWithoutHabitsInput, Prisma.UserUncheckedUpdateWithoutHabitsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateWithoutHabitsInput = {
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
checkIns?: Prisma.CheckInUpdateManyWithoutUserNestedInput
|
||||||
|
checkLists?: Prisma.CheckListUpdateManyWithoutUserNestedInput
|
||||||
|
templates?: Prisma.CheckListTemplateUpdateManyWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedUpdateWithoutHabitsInput = {
|
||||||
|
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
checkIns?: Prisma.CheckInUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
checkLists?: Prisma.CheckListUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
templates?: Prisma.CheckListTemplateUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateWithoutCheckInsInput = {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
habits?: Prisma.HabitCreateNestedManyWithoutUserInput
|
||||||
|
checkLists?: Prisma.CheckListCreateNestedManyWithoutUserInput
|
||||||
|
templates?: Prisma.CheckListTemplateCreateNestedManyWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedCreateWithoutCheckInsInput = {
|
||||||
|
id?: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
habits?: Prisma.HabitUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
checkLists?: Prisma.CheckListUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
templates?: Prisma.CheckListTemplateUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateOrConnectWithoutCheckInsInput = {
|
||||||
|
where: Prisma.UserWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutCheckInsInput, Prisma.UserUncheckedCreateWithoutCheckInsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpsertWithoutCheckInsInput = {
|
||||||
|
update: Prisma.XOR<Prisma.UserUpdateWithoutCheckInsInput, Prisma.UserUncheckedUpdateWithoutCheckInsInput>
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutCheckInsInput, Prisma.UserUncheckedCreateWithoutCheckInsInput>
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateToOneWithWhereWithoutCheckInsInput = {
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.UserUpdateWithoutCheckInsInput, Prisma.UserUncheckedUpdateWithoutCheckInsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateWithoutCheckInsInput = {
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
habits?: Prisma.HabitUpdateManyWithoutUserNestedInput
|
||||||
|
checkLists?: Prisma.CheckListUpdateManyWithoutUserNestedInput
|
||||||
|
templates?: Prisma.CheckListTemplateUpdateManyWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedUpdateWithoutCheckInsInput = {
|
||||||
|
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
habits?: Prisma.HabitUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
checkLists?: Prisma.CheckListUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
templates?: Prisma.CheckListTemplateUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateWithoutCheckListsInput = {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
habits?: Prisma.HabitCreateNestedManyWithoutUserInput
|
||||||
|
checkIns?: Prisma.CheckInCreateNestedManyWithoutUserInput
|
||||||
|
templates?: Prisma.CheckListTemplateCreateNestedManyWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedCreateWithoutCheckListsInput = {
|
||||||
|
id?: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
habits?: Prisma.HabitUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
checkIns?: Prisma.CheckInUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
templates?: Prisma.CheckListTemplateUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateOrConnectWithoutCheckListsInput = {
|
||||||
|
where: Prisma.UserWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutCheckListsInput, Prisma.UserUncheckedCreateWithoutCheckListsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpsertWithoutCheckListsInput = {
|
||||||
|
update: Prisma.XOR<Prisma.UserUpdateWithoutCheckListsInput, Prisma.UserUncheckedUpdateWithoutCheckListsInput>
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutCheckListsInput, Prisma.UserUncheckedCreateWithoutCheckListsInput>
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateToOneWithWhereWithoutCheckListsInput = {
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.UserUpdateWithoutCheckListsInput, Prisma.UserUncheckedUpdateWithoutCheckListsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateWithoutCheckListsInput = {
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
habits?: Prisma.HabitUpdateManyWithoutUserNestedInput
|
||||||
|
checkIns?: Prisma.CheckInUpdateManyWithoutUserNestedInput
|
||||||
|
templates?: Prisma.CheckListTemplateUpdateManyWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedUpdateWithoutCheckListsInput = {
|
||||||
|
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
habits?: Prisma.HabitUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
checkIns?: Prisma.CheckInUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
templates?: Prisma.CheckListTemplateUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateWithoutTemplatesInput = {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
habits?: Prisma.HabitCreateNestedManyWithoutUserInput
|
||||||
|
checkIns?: Prisma.CheckInCreateNestedManyWithoutUserInput
|
||||||
|
checkLists?: Prisma.CheckListCreateNestedManyWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedCreateWithoutTemplatesInput = {
|
||||||
|
id?: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
habits?: Prisma.HabitUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
checkIns?: Prisma.CheckInUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
checkLists?: Prisma.CheckListUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateOrConnectWithoutTemplatesInput = {
|
||||||
|
where: Prisma.UserWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutTemplatesInput, Prisma.UserUncheckedCreateWithoutTemplatesInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpsertWithoutTemplatesInput = {
|
||||||
|
update: Prisma.XOR<Prisma.UserUpdateWithoutTemplatesInput, Prisma.UserUncheckedUpdateWithoutTemplatesInput>
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutTemplatesInput, Prisma.UserUncheckedCreateWithoutTemplatesInput>
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateToOneWithWhereWithoutTemplatesInput = {
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.UserUpdateWithoutTemplatesInput, Prisma.UserUncheckedUpdateWithoutTemplatesInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateWithoutTemplatesInput = {
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
habits?: Prisma.HabitUpdateManyWithoutUserNestedInput
|
||||||
|
checkIns?: Prisma.CheckInUpdateManyWithoutUserNestedInput
|
||||||
|
checkLists?: Prisma.CheckListUpdateManyWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedUpdateWithoutTemplatesInput = {
|
||||||
|
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
habits?: Prisma.HabitUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
checkIns?: Prisma.CheckInUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
checkLists?: Prisma.CheckListUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count Type UserCountOutputType
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type UserCountOutputType = {
|
||||||
|
habits: number
|
||||||
|
checkIns: number
|
||||||
|
checkLists: number
|
||||||
|
templates: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
habits?: boolean | UserCountOutputTypeCountHabitsArgs
|
||||||
|
checkIns?: boolean | UserCountOutputTypeCountCheckInsArgs
|
||||||
|
checkLists?: boolean | UserCountOutputTypeCountCheckListsArgs
|
||||||
|
templates?: boolean | UserCountOutputTypeCountTemplatesArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type UserCountOutputTypeDefaultArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the UserCountOutputType
|
||||||
|
*/
|
||||||
|
select?: Prisma.UserCountOutputTypeSelect<ExtArgs> | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type UserCountOutputTypeCountHabitsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.HabitWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type UserCountOutputTypeCountCheckInsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.CheckInWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type UserCountOutputTypeCountCheckListsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.CheckListWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type UserCountOutputTypeCountTemplatesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.CheckListTemplateWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
@@ -400,6 +800,11 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
|
|||||||
password?: boolean
|
password?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
|
habits?: boolean | Prisma.User$habitsArgs<ExtArgs>
|
||||||
|
checkIns?: boolean | Prisma.User$checkInsArgs<ExtArgs>
|
||||||
|
checkLists?: boolean | Prisma.User$checkListsArgs<ExtArgs>
|
||||||
|
templates?: boolean | Prisma.User$templatesArgs<ExtArgs>
|
||||||
|
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["user"]>
|
}, ExtArgs["result"]["user"]>
|
||||||
|
|
||||||
|
|
||||||
@@ -414,10 +819,22 @@ export type UserSelectScalar = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "password" | "createdAt" | "updatedAt", ExtArgs["result"]["user"]>
|
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "password" | "createdAt" | "updatedAt", ExtArgs["result"]["user"]>
|
||||||
|
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
habits?: boolean | Prisma.User$habitsArgs<ExtArgs>
|
||||||
|
checkIns?: boolean | Prisma.User$checkInsArgs<ExtArgs>
|
||||||
|
checkLists?: boolean | Prisma.User$checkListsArgs<ExtArgs>
|
||||||
|
templates?: boolean | Prisma.User$templatesArgs<ExtArgs>
|
||||||
|
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
|
}
|
||||||
|
|
||||||
export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
name: "User"
|
name: "User"
|
||||||
objects: {}
|
objects: {
|
||||||
|
habits: Prisma.$HabitPayload<ExtArgs>[]
|
||||||
|
checkIns: Prisma.$CheckInPayload<ExtArgs>[]
|
||||||
|
checkLists: Prisma.$CheckListPayload<ExtArgs>[]
|
||||||
|
templates: Prisma.$CheckListTemplatePayload<ExtArgs>[]
|
||||||
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
@@ -765,6 +1182,10 @@ readonly fields: UserFieldRefs;
|
|||||||
*/
|
*/
|
||||||
export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
||||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||||
|
habits<T extends Prisma.User$habitsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$habitsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$HabitPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
checkIns<T extends Prisma.User$checkInsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$checkInsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CheckInPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
checkLists<T extends Prisma.User$checkListsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$checkListsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CheckListPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
templates<T extends Prisma.User$templatesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$templatesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CheckListTemplatePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||||
@@ -816,6 +1237,10 @@ export type UserFindUniqueArgs<ExtArgs extends runtime.Types.Extensions.Internal
|
|||||||
* Omit specific fields from the User
|
* Omit specific fields from the User
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.UserOmit<ExtArgs> | null
|
omit?: Prisma.UserOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.UserInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* Filter, which User to fetch.
|
* Filter, which User to fetch.
|
||||||
*/
|
*/
|
||||||
@@ -834,6 +1259,10 @@ export type UserFindUniqueOrThrowArgs<ExtArgs extends runtime.Types.Extensions.I
|
|||||||
* Omit specific fields from the User
|
* Omit specific fields from the User
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.UserOmit<ExtArgs> | null
|
omit?: Prisma.UserOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.UserInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* Filter, which User to fetch.
|
* Filter, which User to fetch.
|
||||||
*/
|
*/
|
||||||
@@ -852,6 +1281,10 @@ export type UserFindFirstArgs<ExtArgs extends runtime.Types.Extensions.InternalA
|
|||||||
* Omit specific fields from the User
|
* Omit specific fields from the User
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.UserOmit<ExtArgs> | null
|
omit?: Prisma.UserOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.UserInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* Filter, which User to fetch.
|
* Filter, which User to fetch.
|
||||||
*/
|
*/
|
||||||
@@ -900,6 +1333,10 @@ export type UserFindFirstOrThrowArgs<ExtArgs extends runtime.Types.Extensions.In
|
|||||||
* Omit specific fields from the User
|
* Omit specific fields from the User
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.UserOmit<ExtArgs> | null
|
omit?: Prisma.UserOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.UserInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* Filter, which User to fetch.
|
* Filter, which User to fetch.
|
||||||
*/
|
*/
|
||||||
@@ -948,6 +1385,10 @@ export type UserFindManyArgs<ExtArgs extends runtime.Types.Extensions.InternalAr
|
|||||||
* Omit specific fields from the User
|
* Omit specific fields from the User
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.UserOmit<ExtArgs> | null
|
omit?: Prisma.UserOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.UserInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* Filter, which Users to fetch.
|
* Filter, which Users to fetch.
|
||||||
*/
|
*/
|
||||||
@@ -996,6 +1437,10 @@ export type UserCreateArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
* Omit specific fields from the User
|
* Omit specific fields from the User
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.UserOmit<ExtArgs> | null
|
omit?: Prisma.UserOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.UserInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* The data needed to create a User.
|
* The data needed to create a User.
|
||||||
*/
|
*/
|
||||||
@@ -1025,6 +1470,10 @@ export type UserUpdateArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
* Omit specific fields from the User
|
* Omit specific fields from the User
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.UserOmit<ExtArgs> | null
|
omit?: Prisma.UserOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.UserInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* The data needed to update a User.
|
* The data needed to update a User.
|
||||||
*/
|
*/
|
||||||
@@ -1065,6 +1514,10 @@ export type UserUpsertArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
* Omit specific fields from the User
|
* Omit specific fields from the User
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.UserOmit<ExtArgs> | null
|
omit?: Prisma.UserOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.UserInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* The filter to search for the User to update in case it exists.
|
* The filter to search for the User to update in case it exists.
|
||||||
*/
|
*/
|
||||||
@@ -1091,6 +1544,10 @@ export type UserDeleteArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
* Omit specific fields from the User
|
* Omit specific fields from the User
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.UserOmit<ExtArgs> | null
|
omit?: Prisma.UserOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.UserInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* Filter which User to delete.
|
* Filter which User to delete.
|
||||||
*/
|
*/
|
||||||
@@ -1111,6 +1568,102 @@ export type UserDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Internal
|
|||||||
limit?: number
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User.habits
|
||||||
|
*/
|
||||||
|
export type User$habitsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the Habit
|
||||||
|
*/
|
||||||
|
select?: Prisma.HabitSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the Habit
|
||||||
|
*/
|
||||||
|
omit?: Prisma.HabitOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.HabitInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.HabitWhereInput
|
||||||
|
orderBy?: Prisma.HabitOrderByWithRelationInput | Prisma.HabitOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.HabitWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.HabitScalarFieldEnum | Prisma.HabitScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User.checkIns
|
||||||
|
*/
|
||||||
|
export type User$checkInsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the CheckIn
|
||||||
|
*/
|
||||||
|
select?: Prisma.CheckInSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the CheckIn
|
||||||
|
*/
|
||||||
|
omit?: Prisma.CheckInOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.CheckInInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.CheckInWhereInput
|
||||||
|
orderBy?: Prisma.CheckInOrderByWithRelationInput | Prisma.CheckInOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.CheckInWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.CheckInScalarFieldEnum | Prisma.CheckInScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User.checkLists
|
||||||
|
*/
|
||||||
|
export type User$checkListsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the CheckList
|
||||||
|
*/
|
||||||
|
select?: Prisma.CheckListSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the CheckList
|
||||||
|
*/
|
||||||
|
omit?: Prisma.CheckListOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.CheckListInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.CheckListWhereInput
|
||||||
|
orderBy?: Prisma.CheckListOrderByWithRelationInput | Prisma.CheckListOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.CheckListWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.CheckListScalarFieldEnum | Prisma.CheckListScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User.templates
|
||||||
|
*/
|
||||||
|
export type User$templatesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the CheckListTemplate
|
||||||
|
*/
|
||||||
|
select?: Prisma.CheckListTemplateSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the CheckListTemplate
|
||||||
|
*/
|
||||||
|
omit?: Prisma.CheckListTemplateOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.CheckListTemplateInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.CheckListTemplateWhereInput
|
||||||
|
orderBy?: Prisma.CheckListTemplateOrderByWithRelationInput | Prisma.CheckListTemplateOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.CheckListTemplateWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.CheckListTemplateScalarFieldEnum | Prisma.CheckListTemplateScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User without action
|
* User without action
|
||||||
*/
|
*/
|
||||||
@@ -1123,4 +1676,8 @@ export type UserDefaultArgs<ExtArgs extends runtime.Types.Extensions.InternalArg
|
|||||||
* Omit specific fields from the User
|
* Omit specific fields from the User
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.UserOmit<ExtArgs> | null
|
omit?: Prisma.UserOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.UserInclude<ExtArgs> | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `Habit` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`userId` INTEGER NOT NULL,
|
||||||
|
`name` VARCHAR(191) NOT NULL,
|
||||||
|
`period` VARCHAR(191) NOT NULL DEFAULT 'day',
|
||||||
|
`target` INTEGER NOT NULL DEFAULT 1,
|
||||||
|
`color` VARCHAR(191) NOT NULL DEFAULT '#1677ff',
|
||||||
|
`icon` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
`note` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
`archived` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`sortOrder` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
INDEX `Habit_userId_archived_sortOrder_idx`(`userId`, `archived`, `sortOrder`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `CheckIn` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`habitId` INTEGER NOT NULL,
|
||||||
|
`userId` INTEGER NOT NULL,
|
||||||
|
`date` VARCHAR(10) NOT NULL,
|
||||||
|
`seq` INTEGER NOT NULL DEFAULT 1,
|
||||||
|
`note` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
`source` VARCHAR(191) NOT NULL DEFAULT 'user',
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
|
||||||
|
UNIQUE INDEX `CheckIn_habitId_date_seq_key`(`habitId`, `date`, `seq`),
|
||||||
|
INDEX `CheckIn_userId_date_idx`(`userId`, `date`),
|
||||||
|
INDEX `CheckIn_habitId_date_idx`(`habitId`, `date`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `CheckList` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`userId` INTEGER NOT NULL,
|
||||||
|
`title` VARCHAR(191) NOT NULL,
|
||||||
|
`scene` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
`note` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
`archived` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
INDEX `CheckList_userId_archived_idx`(`userId`, `archived`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `CheckListItem` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`listId` INTEGER NOT NULL,
|
||||||
|
`title` VARCHAR(191) NOT NULL,
|
||||||
|
`note` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
`done` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`doneAt` DATETIME(3) NULL,
|
||||||
|
`sortOrder` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
INDEX `CheckListItem_listId_sortOrder_idx`(`listId`, `sortOrder`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `CheckListTemplate` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`userId` INTEGER NULL,
|
||||||
|
`name` VARCHAR(191) NOT NULL,
|
||||||
|
`scene` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
`items` JSON NOT NULL,
|
||||||
|
`builtin` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
INDEX `CheckListTemplate_userId_builtin_idx`(`userId`, `builtin`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `Habit` ADD CONSTRAINT `Habit_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `CheckIn` ADD CONSTRAINT `CheckIn_habitId_fkey` FOREIGN KEY (`habitId`) REFERENCES `Habit`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `CheckIn` ADD CONSTRAINT `CheckIn_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `CheckList` ADD CONSTRAINT `CheckList_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `CheckListItem` ADD CONSTRAINT `CheckListItem_listId_fkey` FOREIGN KEY (`listId`) REFERENCES `CheckList`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `CheckListTemplate` ADD CONSTRAINT `CheckListTemplate_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -19,6 +19,12 @@ model User {
|
|||||||
password String
|
password String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// V0.2 打卡 / 清单: 用户删除时级联清理
|
||||||
|
habits Habit[]
|
||||||
|
checkIns CheckIn[]
|
||||||
|
checkLists CheckList[]
|
||||||
|
templates CheckListTemplate[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Article {
|
model Article {
|
||||||
@@ -281,4 +287,91 @@ model MihomeEnvHour {
|
|||||||
@@index([deviceId, hour])
|
@@index([deviceId, hour])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 打卡 / 日历 / 清单 (V0.2) ====================
|
||||||
|
|
||||||
|
// 打卡项: 周期性目标 (每天 / 每周 / 每月 / 每年 n 次)
|
||||||
|
model Habit {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
userId Int
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
name String
|
||||||
|
period String @default("day") // day | week | month | year
|
||||||
|
target Int @default(1) // 周期内目标次数 n (>=1)
|
||||||
|
color String @default("#1677ff")
|
||||||
|
icon String @default("") // emoji 或图标名, 预留
|
||||||
|
note String @default("")
|
||||||
|
archived Boolean @default(false) // 归档: 不出现在今日待办, 历史保留
|
||||||
|
sortOrder Int @default(0)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
records CheckIn[]
|
||||||
|
|
||||||
|
@@index([userId, archived, sortOrder])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打卡记录: 一次打卡 = 一条; 同日多次 => seq 递增 (1..n, 超额继续累加)
|
||||||
|
model CheckIn {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
habitId Int
|
||||||
|
habit Habit @relation(fields: [habitId], references: [id], onDelete: Cascade)
|
||||||
|
userId Int
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
date String @db.VarChar(10) // 本地日历日 "YYYY-MM-DD" (Asia/Shanghai)
|
||||||
|
seq Int @default(1) // 当日第几次
|
||||||
|
note String @default("")
|
||||||
|
source String @default("user") // user | backfill (补打)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([habitId, date, seq])
|
||||||
|
@@index([userId, date])
|
||||||
|
@@index([habitId, date])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查清单: 出差 / 回老家 等场景的一次性清单
|
||||||
|
model CheckList {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
userId Int
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
title String
|
||||||
|
scene String @default("") // 出差 | 回老家 | 自定义
|
||||||
|
note String @default("")
|
||||||
|
archived Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
items CheckListItem[]
|
||||||
|
|
||||||
|
@@index([userId, archived])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查项: 勾选即完成
|
||||||
|
model CheckListItem {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
listId Int
|
||||||
|
list CheckList @relation(fields: [listId], references: [id], onDelete: Cascade)
|
||||||
|
title String
|
||||||
|
note String @default("")
|
||||||
|
done Boolean @default(false)
|
||||||
|
doneAt DateTime?
|
||||||
|
sortOrder Int @default(0)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([listId, sortOrder])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清单模板: builtin = 系统预置 (userId 为空); 否则为用户自定义
|
||||||
|
model CheckListTemplate {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
userId Int?
|
||||||
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
name String
|
||||||
|
scene String @default("")
|
||||||
|
items Json // [{ title, note }]
|
||||||
|
builtin Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([userId, builtin])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
// 打卡周期计算: 全部以 Asia/Shanghai 的"本地日历日"为准
|
||||||
|
// 日期统一用 "YYYY-MM-DD" 字符串 (字典序 = 时间序, 可直接用于范围查询)
|
||||||
|
|
||||||
|
export const TZ = 'Asia/Shanghai'
|
||||||
|
|
||||||
|
const FMT = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: TZ,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 任意时间 -> 本地日历日 "YYYY-MM-DD" */
|
||||||
|
export function toDateStr(d = new Date()) {
|
||||||
|
return FMT.format(d instanceof Date ? d : new Date(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "YYYY-MM-DD" -> UTC 零点的 Date (仅用于日期运算, 不做时区展示) */
|
||||||
|
function toUTC(s) {
|
||||||
|
const [y, m, d] = String(s).split('-').map(Number)
|
||||||
|
return new Date(Date.UTC(y, (m || 1) - 1, d || 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromUTC(dt) {
|
||||||
|
return dt.toISOString().slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addDays(s, n) {
|
||||||
|
const d = toUTC(s)
|
||||||
|
d.setUTCDate(d.getUTCDate() + n)
|
||||||
|
return fromUTC(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addMonths(s, n) {
|
||||||
|
const [y, m] = String(s).split('-').map(Number)
|
||||||
|
const first = new Date(Date.UTC(y, (m || 1) - 1 + n, 1))
|
||||||
|
const day = Number(String(s).slice(8, 10)) || 1
|
||||||
|
const lastDay = new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + 1, 0)).getUTCDate()
|
||||||
|
first.setUTCDate(Math.min(day, lastDay))
|
||||||
|
return fromUTC(first)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 周一为一周第一天 */
|
||||||
|
export function startOfWeek(s) {
|
||||||
|
const dow = (toUTC(s).getUTCDay() + 6) % 7 // 0=周一
|
||||||
|
return addDays(s, -dow)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function endOfWeek(s) {
|
||||||
|
return addDays(startOfWeek(s), 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startOfMonth(s) {
|
||||||
|
return String(s).slice(0, 8) + '01'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function endOfMonth(s) {
|
||||||
|
const [y, m] = String(s).split('-').map(Number)
|
||||||
|
return fromUTC(new Date(Date.UTC(y, m || 1, 0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startOfYear(s) {
|
||||||
|
return String(s).slice(0, 4) + '-01-01'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function endOfYear(s) {
|
||||||
|
return String(s).slice(0, 4) + '-12-31'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PERIODS = ['day', 'week', 'month', 'year']
|
||||||
|
|
||||||
|
export const PERIOD_LABEL = { day: '每天', week: '每周', month: '每月', year: '每年' }
|
||||||
|
|
||||||
|
/** 周期窗口 [start, end] (闭区间, 本地日历日) */
|
||||||
|
export function periodRange(period, s) {
|
||||||
|
switch (period) {
|
||||||
|
case 'week': return { start: startOfWeek(s), end: endOfWeek(s) }
|
||||||
|
case 'month': return { start: startOfMonth(s), end: endOfMonth(s) }
|
||||||
|
case 'year': return { start: startOfYear(s), end: endOfYear(s) }
|
||||||
|
default: return { start: s, end: s }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 周期窗口内的天数 */
|
||||||
|
export function periodDays(period, s) {
|
||||||
|
const { start, end } = periodRange(period, s)
|
||||||
|
return Math.round((toUTC(end) - toUTC(start)) / 86400000) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日均期望次数: 用于日历上"某一天是否完成了当天应做的份额"
|
||||||
|
* - 每天 n 次 -> n
|
||||||
|
* - 每周 n 次 -> ceil(n / 7)
|
||||||
|
* - 每月 n 次 -> ceil(n / 当月天数)
|
||||||
|
* - 每年 n 次 -> ceil(n / 365)
|
||||||
|
* 环形进度仍以"周期完成度"(周期内累计次数 / n) 为准, 两者不冲突
|
||||||
|
*/
|
||||||
|
export function expectedPerDay(period, target, s) {
|
||||||
|
const n = Math.max(1, Number(target) || 1)
|
||||||
|
if (period === 'week') return Math.max(1, Math.ceil(n / 7))
|
||||||
|
if (period === 'month') return Math.max(1, Math.ceil(n / periodDays('month', s)))
|
||||||
|
if (period === 'year') return Math.max(1, Math.ceil(n / 365))
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 该周期在日期 d 之前是否已完整结束 (用于 streak 判断: 未结束的周期不算断签) */
|
||||||
|
export function periodIsClosed(period, d, today) {
|
||||||
|
const { end } = periodRange(period, d)
|
||||||
|
return end < today
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 遍历 [start, end] 闭区间的每一天 */
|
||||||
|
export function eachDay(start, end, fn) {
|
||||||
|
let cur = start
|
||||||
|
let guard = 0
|
||||||
|
while (cur <= end && guard++ < 4000) {
|
||||||
|
fn(cur)
|
||||||
|
cur = addDays(cur, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ import otaRouter from './routes/ota.js'
|
|||||||
import apizeroRouter from './routes/apizero.js'
|
import apizeroRouter from './routes/apizero.js'
|
||||||
import monitorsRouter from './routes/monitors.js'
|
import monitorsRouter from './routes/monitors.js'
|
||||||
import mihomeRouter from './routes/mihome.js'
|
import mihomeRouter from './routes/mihome.js'
|
||||||
|
import checkinRouter from './routes/checkin.js'
|
||||||
|
import checklistsRouter from './routes/checklists.js'
|
||||||
|
|
||||||
const PORT = Number(process.env.PORT) || 3001
|
const PORT = Number(process.env.PORT) || 3001
|
||||||
const DIST_DIR = path.join(process.cwd(), 'dist')
|
const DIST_DIR = path.join(process.cwd(), 'dist')
|
||||||
@@ -36,6 +38,9 @@ app.use('/api', articlesRouter)
|
|||||||
app.use('/api', apizeroRouter)
|
app.use('/api', apizeroRouter)
|
||||||
app.use('/api', monitorsRouter)
|
app.use('/api', monitorsRouter)
|
||||||
app.use('/api/mihome', mihomeRouter)
|
app.use('/api/mihome', mihomeRouter)
|
||||||
|
// V0.2 打卡 / 日历 / 清单
|
||||||
|
app.use('/api/checkin', checkinRouter)
|
||||||
|
app.use('/api/checklists', checklistsRouter)
|
||||||
|
|
||||||
// 静态资源 (Vite build 产物)
|
// 静态资源 (Vite build 产物)
|
||||||
if (HAS_DIST) {
|
if (HAS_DIST) {
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
import { prisma } from '../db.js'
|
||||||
|
import { authRequired } from '../middleware.js'
|
||||||
|
import {
|
||||||
|
toDateStr, addDays, periodRange, periodDays, expectedPerDay,
|
||||||
|
periodIsClosed, eachDay, PERIODS, PERIOD_LABEL,
|
||||||
|
} from '../checkin/period.js'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
router.use(authRequired)
|
||||||
|
|
||||||
|
// 补打卡允许的回溯天数 (F-CK-10)
|
||||||
|
const BACKFILL_DAYS = 7
|
||||||
|
|
||||||
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/
|
||||||
|
const MONTH_RE = /^\d{4}-\d{2}$/
|
||||||
|
|
||||||
|
function normDate(v) {
|
||||||
|
const s = String(v || '')
|
||||||
|
return DATE_RE.test(s) ? s : toDateStr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 统计辅助 ----------
|
||||||
|
|
||||||
|
// 记录 -> { habitId: { date: count } }
|
||||||
|
function groupRecords(records) {
|
||||||
|
const map = new Map()
|
||||||
|
for (const r of records) {
|
||||||
|
if (!map.has(r.habitId)) map.set(r.habitId, new Map())
|
||||||
|
const byDate = map.get(r.habitId)
|
||||||
|
byDate.set(r.date, (byDate.get(r.date) || 0) + 1)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
function countInRange(byDate, start, end) {
|
||||||
|
let n = 0
|
||||||
|
eachDay(start, end, (d) => { n += byDate.get(d) || 0 })
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连续达标的周期数: 当前周期未结束且未达标 -> 跳过(不算断签), 不是中断
|
||||||
|
function computeStreak(habit, byDate, today) {
|
||||||
|
let cursor = today
|
||||||
|
let streak = 0
|
||||||
|
for (let i = 0; i < 400; i++) {
|
||||||
|
const { start } = periodRange(habit.period, cursor)
|
||||||
|
const done = countInRange(byDate, start, periodRange(habit.period, cursor).end)
|
||||||
|
if (done >= habit.target) {
|
||||||
|
streak++
|
||||||
|
} else if (periodIsClosed(habit.period, cursor, today)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const prevEnd = addDays(start, -1)
|
||||||
|
if (prevEnd < '1970-01-01') break
|
||||||
|
cursor = prevEnd
|
||||||
|
}
|
||||||
|
return streak
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadActiveHabits(userId) {
|
||||||
|
return prisma.habit.findMany({
|
||||||
|
where: { userId, archived: false },
|
||||||
|
orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组装单个打卡项的展示态
|
||||||
|
function buildItem(habit, byDate, date, today) {
|
||||||
|
const { start, end } = periodRange(habit.period, date)
|
||||||
|
const done = countInRange(byDate, start, end)
|
||||||
|
const target = Math.max(1, habit.target)
|
||||||
|
const onDate = byDate.get(date) || 0
|
||||||
|
const expPerDay = expectedPerDay(habit.period, target, date)
|
||||||
|
return {
|
||||||
|
id: habit.id,
|
||||||
|
name: habit.name,
|
||||||
|
period: habit.period,
|
||||||
|
periodLabel: PERIOD_LABEL[habit.period] || '每天',
|
||||||
|
target,
|
||||||
|
color: habit.color,
|
||||||
|
icon: habit.icon,
|
||||||
|
note: habit.note,
|
||||||
|
archived: habit.archived,
|
||||||
|
periodStart: start,
|
||||||
|
periodEnd: end,
|
||||||
|
periodDays: periodDays(habit.period, date),
|
||||||
|
done,
|
||||||
|
remaining: Math.max(0, target - done),
|
||||||
|
percent: Math.min(1, target > 0 ? done / target : 1),
|
||||||
|
completed: done >= target,
|
||||||
|
over: Math.max(0, done - target),
|
||||||
|
countOnDate: onDate,
|
||||||
|
expectedPerDay: expPerDay,
|
||||||
|
dayDone: onDate >= expPerDay,
|
||||||
|
streak: computeStreak(habit, byDate, today),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拉取 [start, end] 内的打卡记录并分组
|
||||||
|
async function loadRecords(userId, habits, ranges) {
|
||||||
|
let min = ranges[0]?.start
|
||||||
|
let max = ranges[0]?.end
|
||||||
|
for (const r of ranges) {
|
||||||
|
if (r.start < min) min = r.start
|
||||||
|
if (r.end > max) max = r.end
|
||||||
|
}
|
||||||
|
if (!min || !max) return new Map()
|
||||||
|
const records = await prisma.checkIn.findMany({
|
||||||
|
where: { userId, habitId: { in: habits.map((h) => h.id) }, date: { gte: min, lte: max } },
|
||||||
|
select: { habitId: true, date: true },
|
||||||
|
})
|
||||||
|
return groupRecords(records)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 打卡项 ----------
|
||||||
|
|
||||||
|
// GET /api/checkin/habits?date=YYYY-MM-DD 今日(或指定日)待办 + 周期进度
|
||||||
|
router.get('/habits', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const today = toDateStr()
|
||||||
|
const date = normDate(req.query.date)
|
||||||
|
const habits = await loadActiveHabits(userId)
|
||||||
|
const ranges = habits.map((h) => periodRange(h.period, date))
|
||||||
|
const grouped = await loadRecords(userId, habits, ranges.length ? ranges : [{ start: date, end: date }])
|
||||||
|
const items = habits.map((h) => buildItem(h, grouped.get(h.id) || new Map(), date, today))
|
||||||
|
const doneCount = items.filter((i) => i.completed).length
|
||||||
|
res.json({ today, date, items, summary: { total: items.length, done: doneCount } })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// POST /api/checkin/habits 新建打卡项
|
||||||
|
router.post('/habits', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const { name, period = 'day', target = 1, color = '#1677ff', icon = '', note = '' } = req.body || {}
|
||||||
|
const cleanName = String(name || '').trim()
|
||||||
|
if (!cleanName) return res.status(400).json({ error: '名称不能为空' })
|
||||||
|
if (cleanName.length > 20) return res.status(400).json({ error: '名称不能超过 20 个字' })
|
||||||
|
if (!PERIODS.includes(period)) return res.status(400).json({ error: '周期类型不合法' })
|
||||||
|
const t = Math.floor(Number(target))
|
||||||
|
if (!Number.isFinite(t) || t < 1) return res.status(400).json({ error: '目标次数需为 ≥1 的整数' })
|
||||||
|
const row = await prisma.habit.create({
|
||||||
|
data: { userId, name: cleanName, period, target: t, color: String(color), icon: String(icon), note: String(note || '') },
|
||||||
|
})
|
||||||
|
res.json({ item: row })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// PUT /api/checkin/habits/:id 编辑
|
||||||
|
router.put('/habits/:id', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const id = Number(req.params.id)
|
||||||
|
const exist = await prisma.habit.findFirst({ where: { id, userId } })
|
||||||
|
if (!exist) return res.status(404).json({ error: '打卡项不存在' })
|
||||||
|
const { name, period, target, color, icon, note, archived, sortOrder } = req.body || {}
|
||||||
|
const data = {}
|
||||||
|
if (name !== undefined) {
|
||||||
|
const n = String(name).trim()
|
||||||
|
if (!n) return res.status(400).json({ error: '名称不能为空' })
|
||||||
|
if (n.length > 20) return res.status(400).json({ error: '名称不能超过 20 个字' })
|
||||||
|
data.name = n
|
||||||
|
}
|
||||||
|
if (period !== undefined) {
|
||||||
|
if (!PERIODS.includes(period)) return res.status(400).json({ error: '周期类型不合法' })
|
||||||
|
data.period = period
|
||||||
|
}
|
||||||
|
if (target !== undefined) {
|
||||||
|
const t = Math.floor(Number(target))
|
||||||
|
if (!Number.isFinite(t) || t < 1) return res.status(400).json({ error: '目标次数需为 ≥1 的整数' })
|
||||||
|
data.target = t
|
||||||
|
}
|
||||||
|
if (color !== undefined) data.color = String(color)
|
||||||
|
if (icon !== undefined) data.icon = String(icon)
|
||||||
|
if (note !== undefined) data.note = String(note)
|
||||||
|
if (archived !== undefined) data.archived = !!archived
|
||||||
|
if (sortOrder !== undefined) data.sortOrder = Number(sortOrder) || 0
|
||||||
|
const row = await prisma.habit.update({ where: { id }, data })
|
||||||
|
res.json({ item: row })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// DELETE /api/checkin/habits/:id?hard=1 默认归档(保留历史), hard=1 物理删除
|
||||||
|
router.delete('/habits/:id', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const id = Number(req.params.id)
|
||||||
|
const exist = await prisma.habit.findFirst({ where: { id, userId } })
|
||||||
|
if (!exist) return res.status(404).json({ error: '打卡项不存在' })
|
||||||
|
if (req.query.hard === '1') {
|
||||||
|
await prisma.checkIn.deleteMany({ where: { habitId: id } })
|
||||||
|
await prisma.habit.delete({ where: { id } })
|
||||||
|
return res.json({ ok: true, removed: true })
|
||||||
|
}
|
||||||
|
const row = await prisma.habit.update({ where: { id }, data: { archived: true } })
|
||||||
|
res.json({ ok: true, removed: false, item: row })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------- 打卡 / 撤销 ----------
|
||||||
|
|
||||||
|
// POST /api/checkin/habits/:id/check { date?, note? } 打一次卡
|
||||||
|
router.post('/habits/:id/check', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const id = Number(req.params.id)
|
||||||
|
const habit = await prisma.habit.findFirst({ where: { id, userId } })
|
||||||
|
if (!habit) return res.status(404).json({ error: '打卡项不存在' })
|
||||||
|
const today = toDateStr()
|
||||||
|
const date = normDate((req.body || {}).date)
|
||||||
|
if (date > today) return res.status(400).json({ error: '不能为未来日期打卡' })
|
||||||
|
if (date < addDays(today, -BACKFILL_DAYS)) {
|
||||||
|
return res.status(400).json({ error: `最多只能补打最近 ${BACKFILL_DAYS} 天` })
|
||||||
|
}
|
||||||
|
const dayRecords = await prisma.checkIn.findMany({ where: { habitId: id, date }, select: { seq: true } })
|
||||||
|
const seq = dayRecords.reduce((m, r) => Math.max(m, r.seq), 0) + 1
|
||||||
|
const note = String((req.body || {}).note || '').slice(0, 200)
|
||||||
|
await prisma.checkIn.create({
|
||||||
|
data: { habitId: id, userId, date, seq, note, source: date === today ? 'user' : 'backfill' },
|
||||||
|
})
|
||||||
|
const grouped = await loadRecords(userId, [habit], [periodRange(habit.period, date)])
|
||||||
|
res.json({ item: buildItem(habit, grouped.get(habit.id) || new Map(), date, today), date })
|
||||||
|
} catch (e) {
|
||||||
|
// 唯一约束冲突 = 并发重复提交, 视为已打卡
|
||||||
|
if (e && e.code === 'P2002') return res.status(409).json({ error: '该次打卡已记录' })
|
||||||
|
next(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// DELETE /api/checkin/habits/:id/check?date= 撤销本周期内最近一次打卡
|
||||||
|
router.delete('/habits/:id/check', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const id = Number(req.params.id)
|
||||||
|
const habit = await prisma.habit.findFirst({ where: { id, userId } })
|
||||||
|
if (!habit) return res.status(404).json({ error: '打卡项不存在' })
|
||||||
|
const today = toDateStr()
|
||||||
|
const date = normDate(req.query.date)
|
||||||
|
const { start, end } = periodRange(habit.period, date)
|
||||||
|
const last = await prisma.checkIn.findFirst({
|
||||||
|
where: { habitId: id, date: { gte: start, lte: end } },
|
||||||
|
orderBy: [{ date: 'desc' }, { seq: 'desc' }],
|
||||||
|
})
|
||||||
|
if (!last) return res.status(400).json({ error: '本周期还没有打卡记录' })
|
||||||
|
await prisma.checkIn.delete({ where: { id: last.id } })
|
||||||
|
const grouped = await loadRecords(userId, [habit], [{ start, end }])
|
||||||
|
res.json({ item: buildItem(habit, grouped.get(habit.id) || new Map(), date, today), date })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------- 某日详情 / 月历 ----------
|
||||||
|
|
||||||
|
// GET /api/checkin/day?date=YYYY-MM-DD 当天每个打卡项的明细 (含可打卡/只读标记)
|
||||||
|
router.get('/day', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const today = toDateStr()
|
||||||
|
const date = normDate(req.query.date)
|
||||||
|
const habits = await loadActiveHabits(userId)
|
||||||
|
const ranges = habits.map((h) => periodRange(h.period, date))
|
||||||
|
const grouped = await loadRecords(userId, habits, ranges.length ? ranges : [{ start: date, end: date }])
|
||||||
|
const items = habits.map((h) => {
|
||||||
|
const base = buildItem(h, grouped.get(h.id) || new Map(), date, today)
|
||||||
|
return { ...base, canCheck: date <= today && date >= addDays(today, -BACKFILL_DAYS) }
|
||||||
|
})
|
||||||
|
res.json({ today, date, isToday: date === today, isFuture: date > today, items })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// GET /api/checkin/calendar?month=YYYY-MM 月历: 每天完成情况 + 当月完成率
|
||||||
|
router.get('/calendar', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const today = toDateStr()
|
||||||
|
const month = MONTH_RE.test(String(req.query.month || '')) ? String(req.query.month) : today.slice(0, 7)
|
||||||
|
const start = month + '-01'
|
||||||
|
const end = addDays(monthStartNext(month), -1)
|
||||||
|
const habits = await prisma.habit.findMany({ where: { userId }, orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }] })
|
||||||
|
const active = habits.filter((h) => !h.archived)
|
||||||
|
const records = await prisma.checkIn.findMany({
|
||||||
|
where: { userId, date: { gte: start, lte: end } },
|
||||||
|
select: { habitId: true, date: true },
|
||||||
|
})
|
||||||
|
const grouped = groupRecords(records)
|
||||||
|
const days = []
|
||||||
|
let expTotal = 0
|
||||||
|
let doneTotal = 0
|
||||||
|
eachDay(start, end, (d) => {
|
||||||
|
let expected = 0
|
||||||
|
let actual = 0
|
||||||
|
let over = false
|
||||||
|
const detail = []
|
||||||
|
for (const h of active) {
|
||||||
|
if (toDateStr(h.createdAt) > d) continue // 创建之前的日期不计入期望
|
||||||
|
const exp = expectedPerDay(h.period, Math.max(1, h.target), d)
|
||||||
|
const cnt = (grouped.get(h.id) || new Map()).get(d) || 0
|
||||||
|
expected += exp
|
||||||
|
actual += Math.min(cnt, exp)
|
||||||
|
if (cnt > exp) over = true
|
||||||
|
detail.push({ id: h.id, name: h.name, color: h.color, count: cnt, expected: exp, done: cnt >= exp })
|
||||||
|
}
|
||||||
|
const isFuture = d > today
|
||||||
|
if (!isFuture) { expTotal += expected; doneTotal += actual }
|
||||||
|
let status = 'empty'
|
||||||
|
if (expected > 0 && actual >= expected) status = over ? 'over' : 'full'
|
||||||
|
else if (actual > 0) status = 'partial'
|
||||||
|
days.push({
|
||||||
|
date: d, expected, actual, status, isToday: d === today, isFuture,
|
||||||
|
habits: detail,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
res.json({
|
||||||
|
today, month, days,
|
||||||
|
rate: expTotal > 0 ? Math.min(1, doneTotal / expTotal) : 0,
|
||||||
|
habits: active.map((h) => ({ id: h.id, name: h.name, color: h.color, period: h.period, target: h.target })),
|
||||||
|
})
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
function monthStartNext(month) {
|
||||||
|
const [y, m] = month.split('-').map(Number)
|
||||||
|
const d = new Date(Date.UTC(y, m, 1)) // m 已 +1 (下月)
|
||||||
|
return d.toISOString().slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
import { prisma } from '../db.js'
|
||||||
|
import { authRequired } from '../middleware.js'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
router.use(authRequired)
|
||||||
|
|
||||||
|
// 系统预置模板 (builtin): 首次访问模板接口时幂等写入
|
||||||
|
const BUILTIN_TEMPLATES = [
|
||||||
|
{
|
||||||
|
name: '出差',
|
||||||
|
scene: '出差',
|
||||||
|
items: [
|
||||||
|
{ title: '身份证 / 证件' },
|
||||||
|
{ title: '手机充电器 + 充电宝' },
|
||||||
|
{ title: '笔记本电脑 + 电源' },
|
||||||
|
{ title: '耳机' },
|
||||||
|
{ title: '换洗衣物' },
|
||||||
|
{ title: '洗漱包' },
|
||||||
|
{ title: '常用药' },
|
||||||
|
{ title: '雨伞' },
|
||||||
|
{ title: '车票 / 机票 / 酒店确认' },
|
||||||
|
{ title: '工牌 / 名片' },
|
||||||
|
{ title: '报销单据' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '回老家',
|
||||||
|
scene: '回老家',
|
||||||
|
items: [
|
||||||
|
{ title: '身份证 / 证件' },
|
||||||
|
{ title: '钥匙' },
|
||||||
|
{ title: '手机充电器 + 充电宝' },
|
||||||
|
{ title: '换洗衣物' },
|
||||||
|
{ title: '洗漱包' },
|
||||||
|
{ title: '常用药' },
|
||||||
|
{ title: '给家人的礼物 / 伴手礼' },
|
||||||
|
{ title: '长辈的药品 / 营养品' },
|
||||||
|
{ title: '口罩' },
|
||||||
|
{ title: '现金零钱' },
|
||||||
|
{ title: '车票 / 加油 / 行程确认' },
|
||||||
|
{ title: '家里门窗水电检查' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async function ensureBuiltin() {
|
||||||
|
const count = await prisma.checkListTemplate.count({ where: { builtin: true } })
|
||||||
|
if (count >= BUILTIN_TEMPLATES.length) return
|
||||||
|
for (const t of BUILTIN_TEMPLATES) {
|
||||||
|
const exist = await prisma.checkListTemplate.findFirst({ where: { builtin: true, name: t.name } })
|
||||||
|
if (exist) continue
|
||||||
|
await prisma.checkListTemplate.create({
|
||||||
|
data: { name: t.name, scene: t.scene, items: t.items, builtin: true, userId: null },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LIST_INCLUDE = { items: { orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }] } }
|
||||||
|
|
||||||
|
function withProgress(list) {
|
||||||
|
const items = list.items || []
|
||||||
|
const done = items.filter((i) => i.done).length
|
||||||
|
return {
|
||||||
|
...list,
|
||||||
|
items,
|
||||||
|
total: items.length,
|
||||||
|
done,
|
||||||
|
percent: items.length > 0 ? done / items.length : 0,
|
||||||
|
completed: items.length > 0 && done === items.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 模板 ----------
|
||||||
|
|
||||||
|
// GET /api/checklists/templates
|
||||||
|
router.get('/templates', async (_req, res, next) => {
|
||||||
|
try {
|
||||||
|
await ensureBuiltin()
|
||||||
|
const rows = await prisma.checkListTemplate.findMany({
|
||||||
|
where: { OR: [{ builtin: true }, { userId: _req.user.sub }] },
|
||||||
|
orderBy: [{ builtin: 'desc' }, { id: 'asc' }],
|
||||||
|
})
|
||||||
|
res.json({ items: rows })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// POST /api/checklists/templates 另存为模板 { name, scene, items: [{title, note}] }
|
||||||
|
router.post('/templates', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const { name, scene = '', items = [] } = req.body || {}
|
||||||
|
const cleanName = String(name || '').trim()
|
||||||
|
if (!cleanName) return res.status(400).json({ error: '模板名称不能为空' })
|
||||||
|
const list = (Array.isArray(items) ? items : [])
|
||||||
|
.map((i) => ({ title: String(i?.title || '').trim(), note: String(i?.note || '') }))
|
||||||
|
.filter((i) => i.title)
|
||||||
|
if (!list.length) return res.status(400).json({ error: '模板至少需要一个检查项' })
|
||||||
|
const row = await prisma.checkListTemplate.create({
|
||||||
|
data: { userId, name: cleanName, scene: String(scene), items: list, builtin: false },
|
||||||
|
})
|
||||||
|
res.json({ item: row })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// DELETE /api/checklists/templates/:id
|
||||||
|
router.delete('/templates/:id', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const id = Number(req.params.id)
|
||||||
|
const exist = await prisma.checkListTemplate.findFirst({ where: { id, userId } })
|
||||||
|
if (!exist) return res.status(404).json({ error: '模板不存在或无权删除' })
|
||||||
|
await prisma.checkListTemplate.delete({ where: { id } })
|
||||||
|
res.json({ ok: true })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------- 清单 ----------
|
||||||
|
|
||||||
|
// GET /api/checklists?archived=0|1
|
||||||
|
router.get('/', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const archived = String(req.query.archived || '0') === '1'
|
||||||
|
const rows = await prisma.checkList.findMany({
|
||||||
|
where: { userId, archived },
|
||||||
|
include: LIST_INCLUDE,
|
||||||
|
orderBy: [{ updatedAt: 'desc' }],
|
||||||
|
})
|
||||||
|
res.json({ items: rows.map(withProgress) })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// POST /api/checklists { title, scene?, note?, items?: string[] | [{title,note}], templateId? }
|
||||||
|
router.post('/', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const { title, scene = '', note = '', items = [], templateId } = req.body || {}
|
||||||
|
const cleanTitle = String(title || '').trim()
|
||||||
|
if (!cleanTitle) return res.status(400).json({ error: '清单名称不能为空' })
|
||||||
|
|
||||||
|
let seed = (Array.isArray(items) ? items : [])
|
||||||
|
.map((i) => (typeof i === 'string' ? { title: i } : i))
|
||||||
|
.map((i) => ({ title: String(i?.title || '').trim(), note: String(i?.note || '') }))
|
||||||
|
.filter((i) => i.title)
|
||||||
|
|
||||||
|
if (!seed.length && templateId) {
|
||||||
|
const tpl = await prisma.checkListTemplate.findFirst({
|
||||||
|
where: { id: Number(templateId), OR: [{ builtin: true }, { userId }] },
|
||||||
|
})
|
||||||
|
if (tpl && Array.isArray(tpl.items)) {
|
||||||
|
seed = tpl.items.map((i) => ({ title: String(i?.title || '').trim(), note: String(i?.note || '') })).filter((i) => i.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = await prisma.checkList.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
title: cleanTitle,
|
||||||
|
scene: String(scene),
|
||||||
|
note: String(note),
|
||||||
|
items: { create: seed.map((i, idx) => ({ title: i.title, note: i.note, sortOrder: idx })) },
|
||||||
|
},
|
||||||
|
include: LIST_INCLUDE,
|
||||||
|
})
|
||||||
|
res.json({ item: withProgress(row) })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// GET /api/checklists/:id
|
||||||
|
router.get('/:id', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const id = Number(req.params.id)
|
||||||
|
const row = await prisma.checkList.findFirst({ where: { id, userId }, include: LIST_INCLUDE })
|
||||||
|
if (!row) return res.status(404).json({ error: '清单不存在' })
|
||||||
|
res.json({ item: withProgress(row) })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// PUT /api/checklists/:id { title?, scene?, note?, archived? }
|
||||||
|
router.put('/:id', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const id = Number(req.params.id)
|
||||||
|
const exist = await prisma.checkList.findFirst({ where: { id, userId } })
|
||||||
|
if (!exist) return res.status(404).json({ error: '清单不存在' })
|
||||||
|
const { title, scene, note, archived } = req.body || {}
|
||||||
|
const data = {}
|
||||||
|
if (title !== undefined) {
|
||||||
|
const t = String(title).trim()
|
||||||
|
if (!t) return res.status(400).json({ error: '清单名称不能为空' })
|
||||||
|
data.title = t
|
||||||
|
}
|
||||||
|
if (scene !== undefined) data.scene = String(scene)
|
||||||
|
if (note !== undefined) data.note = String(note)
|
||||||
|
if (archived !== undefined) data.archived = !!archived
|
||||||
|
const row = await prisma.checkList.update({ where: { id }, data, include: LIST_INCLUDE })
|
||||||
|
res.json({ item: withProgress(row) })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// DELETE /api/checklists/:id
|
||||||
|
router.delete('/:id', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const id = Number(req.params.id)
|
||||||
|
const exist = await prisma.checkList.findFirst({ where: { id, userId } })
|
||||||
|
if (!exist) return res.status(404).json({ error: '清单不存在' })
|
||||||
|
await prisma.checkListItem.deleteMany({ where: { listId: id } })
|
||||||
|
await prisma.checkList.delete({ where: { id } })
|
||||||
|
res.json({ ok: true })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// POST /api/checklists/:id/duplicate 复制一份新清单 (勾选状态重置)
|
||||||
|
router.post('/:id/duplicate', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const id = Number(req.params.id)
|
||||||
|
const src = await prisma.checkList.findFirst({ where: { id, userId }, include: LIST_INCLUDE })
|
||||||
|
if (!src) return res.status(404).json({ error: '清单不存在' })
|
||||||
|
const row = await prisma.checkList.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
title: src.title + ' 副本',
|
||||||
|
scene: src.scene,
|
||||||
|
note: src.note,
|
||||||
|
items: {
|
||||||
|
create: src.items.map((i, idx) => ({ title: i.title, note: i.note, done: false, sortOrder: idx })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: LIST_INCLUDE,
|
||||||
|
})
|
||||||
|
res.json({ item: withProgress(row) })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------- 检查项 ----------
|
||||||
|
|
||||||
|
// POST /api/checklists/:id/items { title, note? }
|
||||||
|
router.post('/:id/items', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const listId = Number(req.params.id)
|
||||||
|
const list = await prisma.checkList.findFirst({ where: { id: listId, userId } })
|
||||||
|
if (!list) return res.status(404).json({ error: '清单不存在' })
|
||||||
|
const title = String((req.body || {}).title || '').trim()
|
||||||
|
if (!title) return res.status(400).json({ error: '检查项不能为空' })
|
||||||
|
if (title.length > 40) return res.status(400).json({ error: '检查项不能超过 40 个字' })
|
||||||
|
const max = await prisma.checkListItem.findFirst({ where: { listId }, orderBy: { sortOrder: 'desc' } })
|
||||||
|
const row = await prisma.checkListItem.create({
|
||||||
|
data: { listId, title, note: String((req.body || {}).note || ''), sortOrder: (max?.sortOrder || 0) + 1 },
|
||||||
|
})
|
||||||
|
res.json({ item: row })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// PUT /api/checklists/items/:id { title?, note?, done?, sortOrder? }
|
||||||
|
router.put('/items/:id', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const id = Number(req.params.id)
|
||||||
|
const exist = await prisma.checkListItem.findFirst({ where: { id, list: { userId } } })
|
||||||
|
if (!exist) return res.status(404).json({ error: '检查项不存在' })
|
||||||
|
const { title, note, done, sortOrder } = req.body || {}
|
||||||
|
const data = {}
|
||||||
|
if (title !== undefined) {
|
||||||
|
const t = String(title).trim()
|
||||||
|
if (!t) return res.status(400).json({ error: '检查项不能为空' })
|
||||||
|
data.title = t
|
||||||
|
}
|
||||||
|
if (note !== undefined) data.note = String(note)
|
||||||
|
if (sortOrder !== undefined) data.sortOrder = Number(sortOrder) || 0
|
||||||
|
if (done !== undefined) {
|
||||||
|
data.done = !!done
|
||||||
|
data.doneAt = done ? new Date() : null
|
||||||
|
}
|
||||||
|
const row = await prisma.checkListItem.update({ where: { id }, data })
|
||||||
|
res.json({ item: row })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// DELETE /api/checklists/items/:id
|
||||||
|
router.delete('/items/:id', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const id = Number(req.params.id)
|
||||||
|
const exist = await prisma.checkListItem.findFirst({ where: { id, list: { userId } } })
|
||||||
|
if (!exist) return res.status(404).json({ error: '检查项不存在' })
|
||||||
|
await prisma.checkListItem.delete({ where: { id } })
|
||||||
|
res.json({ ok: true })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// POST /api/checklists/:id/check-all { done: boolean } 一键全选 / 全不选
|
||||||
|
router.post('/:id/check-all', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.sub
|
||||||
|
const listId = Number(req.params.id)
|
||||||
|
const list = await prisma.checkList.findFirst({ where: { id: listId, userId } })
|
||||||
|
if (!list) return res.status(404).json({ error: '清单不存在' })
|
||||||
|
const done = !!(req.body || {}).done
|
||||||
|
await prisma.checkListItem.updateMany({
|
||||||
|
where: { listId },
|
||||||
|
data: { done, doneAt: done ? new Date() : null },
|
||||||
|
})
|
||||||
|
const row = await prisma.checkList.findFirst({ where: { id: listId }, include: LIST_INCLUDE })
|
||||||
|
res.json({ item: withProgress(row) })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -7,6 +7,7 @@ import MacroPage from './pages/MacroPage.jsx'
|
|||||||
import MonitorPage from './pages/MonitorPage.jsx'
|
import MonitorPage from './pages/MonitorPage.jsx'
|
||||||
import ServerDetail from './pages/monitor/ServerDetail.jsx'
|
import ServerDetail from './pages/monitor/ServerDetail.jsx'
|
||||||
import MePage from './pages/MePage.jsx'
|
import MePage from './pages/MePage.jsx'
|
||||||
|
import CheckinPage from './pages/checkin/CheckinPage.jsx'
|
||||||
import { useAuth } from './auth/AuthContext.jsx'
|
import { useAuth } from './auth/AuthContext.jsx'
|
||||||
|
|
||||||
// 已登录访问 /login 自动回首页
|
// 已登录访问 /login 自动回首页
|
||||||
@@ -36,6 +37,8 @@ export default function App() {
|
|||||||
<Route path="/macro/*" element={<MacroPage />} />
|
<Route path="/macro/*" element={<MacroPage />} />
|
||||||
<Route path="/monitor" element={<MonitorPage />} />
|
<Route path="/monitor" element={<MonitorPage />} />
|
||||||
<Route path="/monitor/server/:uuid" element={<ServerDetail />} />
|
<Route path="/monitor/server/:uuid" element={<ServerDetail />} />
|
||||||
|
{/* V0.2 打卡 / 日历 / 清单 */}
|
||||||
|
<Route path="/checkin" element={<CheckinPage />} />
|
||||||
<Route path="/me" element={<MePage />} />
|
<Route path="/me" element={<MePage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { api } from './client.js'
|
||||||
|
|
||||||
|
const q = (params) => {
|
||||||
|
const s = new URLSearchParams()
|
||||||
|
Object.entries(params || {}).forEach(([k, v]) => {
|
||||||
|
if (v !== undefined && v !== null && v !== '') s.set(k, v)
|
||||||
|
})
|
||||||
|
const str = s.toString()
|
||||||
|
return str ? '?' + str : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打卡 / 日历
|
||||||
|
export const checkinApi = {
|
||||||
|
habits: (date) => api('/checkin/habits' + q({ date })),
|
||||||
|
day: (date) => api('/checkin/day' + q({ date })),
|
||||||
|
calendar: (month) => api('/checkin/calendar' + q({ month })),
|
||||||
|
createHabit: (body) => api('/checkin/habits', { method: 'POST', body }),
|
||||||
|
updateHabit: (id, body) => api(`/checkin/habits/${id}`, { method: 'PUT', body }),
|
||||||
|
deleteHabit: (id, hard) => api(`/checkin/habits/${id}${hard ? '?hard=1' : ''}`, { method: 'DELETE' }),
|
||||||
|
check: (id, date, note) => api(`/checkin/habits/${id}/check`, { method: 'POST', body: { date, note } }),
|
||||||
|
uncheck: (id, date) => api(`/checkin/habits/${id}/check` + q({ date }), { method: 'DELETE' }),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查清单
|
||||||
|
export const checklistApi = {
|
||||||
|
lists: (archived) => api('/checklists' + q({ archived: archived ? 1 : 0 })),
|
||||||
|
list: (id) => api(`/checklists/${id}`),
|
||||||
|
create: (body) => api('/checklists', { method: 'POST', body }),
|
||||||
|
update: (id, body) => api(`/checklists/${id}`, { method: 'PUT', body }),
|
||||||
|
remove: (id) => api(`/checklists/${id}`, { method: 'DELETE' }),
|
||||||
|
duplicate: (id) => api(`/checklists/${id}/duplicate`, { method: 'POST', body: {} }),
|
||||||
|
addItem: (id, body) => api(`/checklists/${id}/items`, { method: 'POST', body }),
|
||||||
|
updateItem: (itemId, body) => api(`/checklists/items/${itemId}`, { method: 'PUT', body }),
|
||||||
|
removeItem: (itemId) => api(`/checklists/items/${itemId}`, { method: 'DELETE' }),
|
||||||
|
checkAll: (id, done) => api(`/checklists/${id}/check-all`, { method: 'POST', body: { done } }),
|
||||||
|
templates: () => api('/checklists/templates'),
|
||||||
|
createTemplate: (body) => api('/checklists/templates', { method: 'POST', body }),
|
||||||
|
removeTemplate: (id) => api(`/checklists/templates/${id}`, { method: 'DELETE' }),
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ const TABS = [
|
|||||||
{ key: '/', name: 'home', label: '首页' },
|
{ key: '/', name: 'home', label: '首页' },
|
||||||
{ key: '/macro', name: 'macro', label: '宏观' },
|
{ key: '/macro', name: 'macro', label: '宏观' },
|
||||||
{ key: '/monitor', name: 'monitor', label: '监控' },
|
{ key: '/monitor', name: 'monitor', label: '监控' },
|
||||||
|
{ key: '/checkin', name: 'checkin', label: '打卡' },
|
||||||
{ key: '/me', name: 'me', label: '我' },
|
{ key: '/me', name: 'me', label: '我' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
// 环形进度组件 (F-CK-03/04/05, U-01)
|
||||||
|
// props:
|
||||||
|
// percent: 0~1 完成度
|
||||||
|
// size: 外框尺寸(px)
|
||||||
|
// stroke: 环宽(px)
|
||||||
|
// color: 进度色
|
||||||
|
// completed: 是否达标 (达标时显示对勾而非数字)
|
||||||
|
// done/target: 中心文案
|
||||||
|
// onClick: 可选, 点击整个环触发 (用于"点环即打卡")
|
||||||
|
export default function RingProgress({
|
||||||
|
percent = 0,
|
||||||
|
size = 58,
|
||||||
|
stroke = 6,
|
||||||
|
color = '#1677ff',
|
||||||
|
completed = false,
|
||||||
|
done = 0,
|
||||||
|
target = 1,
|
||||||
|
over = 0,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
disabled = false,
|
||||||
|
}) {
|
||||||
|
const p = Math.max(0, Math.min(1, Number(percent) || 0))
|
||||||
|
const r = (size - stroke) / 2
|
||||||
|
const c = 2 * Math.PI * r
|
||||||
|
const offset = c * (1 - p)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={'ring-wrap' + (onClick ? ' clickable' : '') + (disabled ? ' disabled' : '')}
|
||||||
|
style={{ width: size, height: size }}
|
||||||
|
onClick={disabled ? undefined : onClick}
|
||||||
|
>
|
||||||
|
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="ring-svg">
|
||||||
|
{/* 背景环 */}
|
||||||
|
<circle
|
||||||
|
cx={size / 2} cy={size / 2} r={r}
|
||||||
|
fill="none" stroke="#ececf0" strokeWidth={stroke}
|
||||||
|
/>
|
||||||
|
{/* 进度环: 从 12 点方向顺时针 */}
|
||||||
|
<circle
|
||||||
|
className="ring-bar"
|
||||||
|
cx={size / 2} cy={size / 2} r={r}
|
||||||
|
fill="none" stroke={color} strokeWidth={stroke} strokeLinecap="round"
|
||||||
|
strokeDasharray={c} strokeDashoffset={offset}
|
||||||
|
transform={`rotate(-90 ${size / 2} ${size / 2})`}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div className="ring-center">
|
||||||
|
{children !== undefined ? children : (
|
||||||
|
completed ? (
|
||||||
|
<svg className="ring-check" width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M4 12.5l5 5L20 6.5" stroke={color} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<span className="ring-num">
|
||||||
|
{done}
|
||||||
|
<em>/{target}</em>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{over > 0 && <span className="ring-over">+{over}</span>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成特效 (F-CK-07, U-02/U-03/U-07)
|
||||||
|
// props: fire 计数器(每次 +1 触发一次), color 主色
|
||||||
|
// 尊重"特效开关": localStorage iboard_celebrate === '0' 时不播放
|
||||||
|
export function Celebrate({ fire = 0, color = '#1677ff' }) {
|
||||||
|
const [burst, setBurst] = useState(0)
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
const prev = useRef(fire)
|
||||||
|
const timer = useRef(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fire === prev.current) return
|
||||||
|
prev.current = fire
|
||||||
|
if (fire <= 0) return
|
||||||
|
if (!effectsEnabled()) return
|
||||||
|
setBurst((n) => n + 1)
|
||||||
|
setVisible(true)
|
||||||
|
haptic()
|
||||||
|
clearTimeout(timer.current)
|
||||||
|
timer.current = setTimeout(() => setVisible(false), 900)
|
||||||
|
return () => clearTimeout(timer.current)
|
||||||
|
}, [fire])
|
||||||
|
|
||||||
|
if (!visible) return null
|
||||||
|
const dots = Array.from({ length: 14 }, (_, i) => i)
|
||||||
|
return (
|
||||||
|
<div className="celebrate" key={burst}>
|
||||||
|
<span className="celebrate-ring" style={{ borderColor: color }} />
|
||||||
|
{dots.map((i) => {
|
||||||
|
const angle = (360 / dots.length) * i + (i % 2 ? 8 : -8)
|
||||||
|
const dist = 26 + (i % 4) * 8
|
||||||
|
return (
|
||||||
|
<i
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
background: i % 3 === 0 ? '#ffc53d' : color,
|
||||||
|
'--a': angle + 'deg',
|
||||||
|
'--d': dist + 'px',
|
||||||
|
animationDelay: (i % 3) * 40 + 'ms',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特效全局开关 (默认开)
|
||||||
|
export function effectsEnabled() {
|
||||||
|
try {
|
||||||
|
if (localStorage.getItem('iboard_celebrate') === '0') return false
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
// 系统"减弱动效"偏好时降级
|
||||||
|
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||||
|
try {
|
||||||
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return false
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setEffectsEnabled(v) {
|
||||||
|
try { localStorage.setItem('iboard_celebrate', v ? '1' : '0') } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轻量触感反馈 (Web Vibration API; Tauri Android WebView 支持)
|
||||||
|
export function haptic(ms = 18) {
|
||||||
|
try {
|
||||||
|
if (typeof navigator !== 'undefined' && navigator.vibrate) navigator.vibrate(ms)
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
@@ -22,6 +22,15 @@ const ICONS = {
|
|||||||
<path d="M3 12h4l2-5 4 10 2-5h6" fill={active ? 'currentColor' : 'none'} fillOpacity={active ? 0.12 : 0} />
|
<path d="M3 12h4l2-5 4 10 2-5h6" fill={active ? 'currentColor' : 'none'} fillOpacity={active ? 0.12 : 0} />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
|
// 打卡: 日历 + 对勾
|
||||||
|
checkin: (active) => (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<rect x="3" y="5" width="18" height="16" rx="2" fill={active ? 'currentColor' : 'none'} fillOpacity={active ? 0.12 : 0} />
|
||||||
|
<path d="M3 10h18" />
|
||||||
|
<path d="M8 3v4M16 3v4" />
|
||||||
|
<path d="M9 15.5l2.2 2.2L15.5 13" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
me: (active) => (
|
me: (active) => (
|
||||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round">
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round">
|
||||||
<circle cx="12" cy="8" r="4" fill={active ? 'currentColor' : 'none'} fillOpacity={active ? 0.15 : 0} />
|
<circle cx="12" cy="8" r="4" fill={active ? 'currentColor' : 'none'} fillOpacity={active ? 0.15 : 0} />
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { Button, DotLoading, Empty, Segmented, Toast } from 'antd-mobile'
|
||||||
|
import { Celebrate } from '../../components/RingProgress.jsx'
|
||||||
|
import { checkinApi } from '../../api/checkin.js'
|
||||||
|
import useRefreshOnVisible from '../../hooks/useRefreshOnVisible.js'
|
||||||
|
import {
|
||||||
|
toDateStr, addDays, addMonths, monthGrid, weekdayLabels, dayLabel, fmtMonth,
|
||||||
|
} from './date.js'
|
||||||
|
|
||||||
|
// 日历视图: 月历网格 + 今天/昨天/明天 三视图 + 日期标记 (F-CL-*)
|
||||||
|
export default function CalendarView({ date, onDateChange }) {
|
||||||
|
const today = toDateStr()
|
||||||
|
const [month, setMonth] = useState(() => date.slice(0, 7))
|
||||||
|
const [cal, setCal] = useState(null) // 月历数据
|
||||||
|
const [detail, setDetail] = useState(null) // 选中日详情
|
||||||
|
const [err, setErr] = useState('')
|
||||||
|
const [bursts, setBursts] = useState({})
|
||||||
|
|
||||||
|
const loadCal = useCallback((silent) => {
|
||||||
|
if (!silent) setErr('')
|
||||||
|
checkinApi.calendar(month)
|
||||||
|
.then(setCal)
|
||||||
|
.catch((e) => { if (!silent) setErr(e.message) })
|
||||||
|
}, [month])
|
||||||
|
|
||||||
|
const loadDetail = useCallback((silent) => {
|
||||||
|
checkinApi.day(date).then(setDetail).catch(() => {})
|
||||||
|
}, [date])
|
||||||
|
|
||||||
|
useEffect(() => { loadCal(false) }, [loadCal])
|
||||||
|
useEffect(() => { setDetail(null); loadDetail(false) }, [loadDetail])
|
||||||
|
useRefreshOnVisible(() => { loadCal(true); loadDetail(true) })
|
||||||
|
|
||||||
|
// 切换日期时, 若跨月则同步月份
|
||||||
|
useEffect(() => {
|
||||||
|
const m = date.slice(0, 7)
|
||||||
|
setMonth((cur) => (cur === m ? cur : m))
|
||||||
|
}, [date])
|
||||||
|
|
||||||
|
const days = useMemo(() => monthGrid(month), [month])
|
||||||
|
const byDate = useMemo(() => {
|
||||||
|
const map = new Map()
|
||||||
|
;(cal?.days || []).forEach((d) => map.set(d.date, d))
|
||||||
|
return map
|
||||||
|
}, [cal])
|
||||||
|
|
||||||
|
const quickValue = useMemo(() => {
|
||||||
|
if (date === today) return 0
|
||||||
|
if (date === addDays(today, -1)) return -1
|
||||||
|
if (date === addDays(today, 1)) return 1
|
||||||
|
return null
|
||||||
|
}, [date, today])
|
||||||
|
|
||||||
|
async function checkFromCalendar(item) {
|
||||||
|
if (!item.canCheck) return
|
||||||
|
const snapshot = detail
|
||||||
|
setDetail((d) => ({
|
||||||
|
...d,
|
||||||
|
items: d.items.map((x) => {
|
||||||
|
if (x.id !== item.id) return x
|
||||||
|
const done = x.done + 1
|
||||||
|
const countOnDate = (x.countOnDate || 0) + 1
|
||||||
|
return {
|
||||||
|
...x, done, countOnDate,
|
||||||
|
percent: Math.min(1, done / x.target),
|
||||||
|
completed: done >= x.target,
|
||||||
|
over: Math.max(0, done - x.target),
|
||||||
|
dayDone: countOnDate >= x.expectedPerDay,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
try {
|
||||||
|
const res = await checkinApi.check(item.id, date)
|
||||||
|
const fresh = await checkinApi.day(date)
|
||||||
|
setDetail(fresh)
|
||||||
|
if (res.item.completed) setBursts((m) => ({ ...m, [item.id]: (m[item.id] || 0) + 1 }))
|
||||||
|
loadCal(true)
|
||||||
|
} catch (e) {
|
||||||
|
setDetail(snapshot)
|
||||||
|
Toast.show({ content: e.message || '打卡失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = detail?.items || []
|
||||||
|
const isFuture = !!detail?.isFuture
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="calendar-view">
|
||||||
|
<div className="cal-month-bar">
|
||||||
|
<span className="cal-arrow" onClick={() => setMonth((m) => addMonths(m, -1))}>‹</span>
|
||||||
|
<span className="cal-month">{fmtMonth(month)}</span>
|
||||||
|
<span className="cal-arrow" onClick={() => setMonth((m) => addMonths(m, 1))}>›</span>
|
||||||
|
<span className="cal-today-btn" onClick={() => onDateChange(today)}>回到今天</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="cal-quick">
|
||||||
|
<Segmented
|
||||||
|
options={[
|
||||||
|
{ label: '昨天', value: -1 },
|
||||||
|
{ label: '今天', value: 0 },
|
||||||
|
{ label: '明天', value: 1 },
|
||||||
|
]}
|
||||||
|
value={quickValue}
|
||||||
|
onChange={(v) => onDateChange(addDays(today, Number(v)))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{err && <div className="error-banner">{err}<span className="retry-link" onClick={() => loadCal(false)}>重试</span></div>}
|
||||||
|
|
||||||
|
<div className="cal-grid">
|
||||||
|
<div className="cal-weekday">
|
||||||
|
{weekdayLabels().map((w) => <span key={w}>{w}</span>)}
|
||||||
|
</div>
|
||||||
|
<div className="cal-days">
|
||||||
|
{days.map((d) => {
|
||||||
|
const info = byDate.get(d)
|
||||||
|
const inMonth = d.slice(0, 7) === month
|
||||||
|
const status = info?.status || 'empty'
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={d}
|
||||||
|
className={
|
||||||
|
'cal-day' + (inMonth ? '' : ' out') + (d === date ? ' selected' : '') +
|
||||||
|
(info?.isToday ? ' today' : '') + (info?.isFuture ? ' future' : '')
|
||||||
|
}
|
||||||
|
onClick={() => onDateChange(d)}
|
||||||
|
>
|
||||||
|
<span className="cal-day-num">{Number(d.slice(8, 10))}</span>
|
||||||
|
{inMonth && status !== 'empty' && (
|
||||||
|
<span className={'cal-dot ' + status} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{cal && (
|
||||||
|
<div className="cal-rate">本月完成率 <strong>{Math.round((cal.rate || 0) * 100)}%</strong></div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="cal-detail">
|
||||||
|
<div className="cal-detail-title">
|
||||||
|
{dayLabel(date, today)} · {date}
|
||||||
|
{isFuture && <span className="habit-badge">计划(不可打卡)</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{detail === null && <div className="monitor-loading"><DotLoading /> 加载中...</div>}
|
||||||
|
{detail !== null && items.length === 0 && <Empty description="当天没有打卡项" />}
|
||||||
|
|
||||||
|
{items.map((item) => (
|
||||||
|
<div key={item.id} className={'cal-item' + (item.completed ? ' done' : '')}>
|
||||||
|
<span className="cal-item-dot" style={{ background: item.color }} />
|
||||||
|
<span className="cal-item-name">{item.name}</span>
|
||||||
|
<span className="cal-item-meta">
|
||||||
|
{item.periodLabel} {item.target} 次 · 当日 {item.countOnDate || 0}/{item.expectedPerDay}
|
||||||
|
</span>
|
||||||
|
<span className="cal-item-right">
|
||||||
|
{item.canCheck ? (
|
||||||
|
<Button size="mini" color={item.dayDone ? 'default' : 'primary'} onClick={() => checkFromCalendar(item)}>
|
||||||
|
{item.dayDone ? '已达标' : '打卡'}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<span className="cal-item-plan">{item.dayDone ? '计划' : '待办'}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<Celebrate fire={bursts[item.id] || 0} color={item.color} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { NavBar } from 'antd-mobile'
|
||||||
|
import SwipeTabs from '../../components/SwipeTabs.jsx'
|
||||||
|
import HabitView from './HabitView.jsx'
|
||||||
|
import CalendarView from './CalendarView.jsx'
|
||||||
|
import ListView from './ListView.jsx'
|
||||||
|
import { toDateStr } from './date.js'
|
||||||
|
|
||||||
|
// 打卡页: 一个页面三个视图 —— 打卡(环形进度) / 日历(今天·昨天·明天) / 清单(场景检查)
|
||||||
|
export default function CheckinPage() {
|
||||||
|
const [active, setActive] = useState('habit')
|
||||||
|
const [date, setDate] = useState(() => toDateStr())
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ key: 'habit', label: '打卡', render: () => <HabitView date={date} /> },
|
||||||
|
{ key: 'calendar', label: '日历', render: () => <CalendarView date={date} onDateChange={setDate} /> },
|
||||||
|
{ key: 'list', label: '清单', render: () => <ListView /> },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-with-navbar">
|
||||||
|
<NavBar back={null}>打卡</NavBar>
|
||||||
|
<div className="checkin-content">
|
||||||
|
<SwipeTabs tabs={tabs} active={active} onChange={setActive} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import {
|
||||||
|
Button, Dialog, DotLoading, Empty, Input, Popup, Selector, Stepper, Toast,
|
||||||
|
} from 'antd-mobile'
|
||||||
|
import RingProgress, { Celebrate, effectsEnabled, setEffectsEnabled } from '../../components/RingProgress.jsx'
|
||||||
|
import { checkinApi } from '../../api/checkin.js'
|
||||||
|
import useRefreshOnVisible from '../../hooks/useRefreshOnVisible.js'
|
||||||
|
import { toDateStr, dayLabel } from './date.js'
|
||||||
|
|
||||||
|
const PERIOD_OPTIONS = [
|
||||||
|
{ label: '每天', value: 'day' },
|
||||||
|
{ label: '每周', value: 'week' },
|
||||||
|
{ label: '每月', value: 'month' },
|
||||||
|
{ label: '每年', value: 'year' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const COLORS = ['#1677ff', '#00b578', '#ff8f1f', '#ff3141', '#a855f7', '#14b8a6', '#f43f9e']
|
||||||
|
|
||||||
|
const EMPTY_FORM = { id: null, name: '', period: 'day', target: 1, color: COLORS[0], note: '' }
|
||||||
|
|
||||||
|
// 打卡视图: 今日待办列表 + 环形进度 + 打卡/撤销 + 新建/编辑
|
||||||
|
export default function HabitView({ date, onDateChange }) {
|
||||||
|
const [data, setData] = useState(null) // { today, date, items, summary }
|
||||||
|
const [err, setErr] = useState('')
|
||||||
|
const [bursts, setBursts] = useState({}) // { habitId: 触发计数 }
|
||||||
|
const [form, setForm] = useState(EMPTY_FORM)
|
||||||
|
const [showForm, setShowForm] = useState(false)
|
||||||
|
const [effects, setEffects] = useState(effectsEnabled())
|
||||||
|
|
||||||
|
const load = useCallback((silent) => {
|
||||||
|
if (!silent) setErr('')
|
||||||
|
checkinApi.habits(date)
|
||||||
|
.then((d) => setData(d))
|
||||||
|
.catch((e) => { if (!silent) setErr(e.message) })
|
||||||
|
}, [date])
|
||||||
|
|
||||||
|
useEffect(() => { load(false) }, [load])
|
||||||
|
useRefreshOnVisible(() => load(true))
|
||||||
|
|
||||||
|
const items = useMemo(() => {
|
||||||
|
const list = data?.items || []
|
||||||
|
// 未完成在前, 已完成在后 (F-CK-16 默认排序)
|
||||||
|
return [...list].sort((a, b) => Number(a.completed) - Number(b.completed) || a.id - b.id)
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const fireBurst = (id) => setBursts((m) => ({ ...m, [id]: (m[id] || 0) + 1 }))
|
||||||
|
|
||||||
|
// 乐观更新: 先本地 +1, 成功用服务端结果覆盖, 失败回滚 (F-SY-06/E-02)
|
||||||
|
async function doCheck(item) {
|
||||||
|
const before = data
|
||||||
|
const patch = (it) => {
|
||||||
|
const done = it.done + 1
|
||||||
|
return {
|
||||||
|
...it,
|
||||||
|
done,
|
||||||
|
remaining: Math.max(0, it.target - done),
|
||||||
|
percent: Math.min(1, done / it.target),
|
||||||
|
completed: done >= it.target,
|
||||||
|
over: Math.max(0, done - it.target),
|
||||||
|
countOnDate: (it.countOnDate || 0) + 1,
|
||||||
|
dayDone: (it.countOnDate || 0) + 1 >= it.expectedPerDay,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setData((d) => ({ ...d, items: d.items.map((x) => (x.id === item.id ? patch(x) : x)) }))
|
||||||
|
try {
|
||||||
|
const res = await checkinApi.check(item.id, date)
|
||||||
|
setData((d) => ({ ...d, items: d.items.map((x) => (x.id === item.id ? res.item : x)) }))
|
||||||
|
if (res.item.completed) fireBurst(item.id)
|
||||||
|
} catch (e) {
|
||||||
|
setData(before)
|
||||||
|
Toast.show({ content: e.message || '打卡失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doUncheck(item) {
|
||||||
|
const before = data
|
||||||
|
setData((d) => ({
|
||||||
|
...d,
|
||||||
|
items: d.items.map((x) => {
|
||||||
|
if (x.id !== item.id) return x
|
||||||
|
const done = Math.max(0, x.done - 1)
|
||||||
|
return {
|
||||||
|
...x,
|
||||||
|
done,
|
||||||
|
remaining: Math.max(0, x.target - done),
|
||||||
|
percent: Math.min(1, done / x.target),
|
||||||
|
completed: done >= x.target,
|
||||||
|
over: Math.max(0, done - x.target),
|
||||||
|
countOnDate: Math.max(0, (x.countOnDate || 0) - 1),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
try {
|
||||||
|
const res = await checkinApi.uncheck(item.id, date)
|
||||||
|
setData((d) => ({ ...d, items: d.items.map((x) => (x.id === item.id ? res.item : x)) }))
|
||||||
|
} catch (e) {
|
||||||
|
setData(before)
|
||||||
|
Toast.show({ content: e.message || '撤销失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitForm() {
|
||||||
|
const name = String(form.name || '').trim()
|
||||||
|
if (!name) return Toast.show({ content: '请填写名称' })
|
||||||
|
if (name.length > 20) return Toast.show({ content: '名称不能超过 20 个字' })
|
||||||
|
const payload = { name, period: form.period, target: Number(form.target) || 1, color: form.color, note: form.note }
|
||||||
|
try {
|
||||||
|
if (form.id) await checkinApi.updateHabit(form.id, payload)
|
||||||
|
else await checkinApi.createHabit(payload)
|
||||||
|
setShowForm(false)
|
||||||
|
setForm(EMPTY_FORM)
|
||||||
|
load(true)
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show({ content: e.message || '保存失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeHabit(item) {
|
||||||
|
const ok = await Dialog.confirm({
|
||||||
|
content: `归档「${item.name}」?归档后不再出现在待办,历史记录保留。`,
|
||||||
|
confirmText: '归档',
|
||||||
|
cancelText: '取消',
|
||||||
|
})
|
||||||
|
if (!ok) return
|
||||||
|
try {
|
||||||
|
await checkinApi.deleteHabit(item.id)
|
||||||
|
Toast.show({ content: '已归档' })
|
||||||
|
load(true)
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show({ content: e.message || '操作失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = data?.summary || { total: 0, done: 0 }
|
||||||
|
const isToday = data ? data.date === data.today : true
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="habit-view">
|
||||||
|
<div className="habit-head">
|
||||||
|
<div className="habit-head-left">
|
||||||
|
<div className="habit-date">{dayLabel(date, data?.today || toDateStr())}</div>
|
||||||
|
<div className="habit-summary">
|
||||||
|
已完成 <strong>{summary.done}</strong> / {summary.total}
|
||||||
|
{!isToday && <span className="habit-badge">非今天</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="habit-head-right">
|
||||||
|
<Button size="mini" fill="outline" color="primary" onClick={() => setEffects((v) => {
|
||||||
|
const next = !v
|
||||||
|
setEffectsEnabled(next)
|
||||||
|
return next
|
||||||
|
})}>
|
||||||
|
{effects ? '特效开' : '特效关'}
|
||||||
|
</Button>
|
||||||
|
<Button size="mini" color="primary" onClick={() => { setForm(EMPTY_FORM); setShowForm(true) }}>
|
||||||
|
+ 新建
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{err && (
|
||||||
|
<div className="error-banner">
|
||||||
|
{err}
|
||||||
|
<span className="retry-link" onClick={() => load(false)}>重试</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data === null && !err && <div className="monitor-loading"><DotLoading /> 加载中...</div>}
|
||||||
|
|
||||||
|
{data !== null && !err && items.length === 0 && (
|
||||||
|
<div className="checkin-empty">
|
||||||
|
<Empty description="还没有打卡项" />
|
||||||
|
<Button color="primary" size="small" onClick={() => { setForm(EMPTY_FORM); setShowForm(true) }}>
|
||||||
|
创建第一个打卡项
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="habit-list">
|
||||||
|
{items.map((item) => (
|
||||||
|
<div key={item.id} className={'habit-card' + (item.completed ? ' done' : '')}>
|
||||||
|
<RingProgress
|
||||||
|
percent={item.percent}
|
||||||
|
done={item.done}
|
||||||
|
target={item.target}
|
||||||
|
over={item.over}
|
||||||
|
completed={item.completed}
|
||||||
|
color={item.color}
|
||||||
|
onClick={() => doCheck(item)}
|
||||||
|
/>
|
||||||
|
<div className="habit-main">
|
||||||
|
<div className="habit-name">{item.name}</div>
|
||||||
|
<div className="habit-meta">
|
||||||
|
{item.periodLabel} {item.target} 次
|
||||||
|
{item.streak > 0 && <span className="habit-streak">连续 {item.streak}</span>}
|
||||||
|
</div>
|
||||||
|
{item.note && <div className="habit-note">{item.note}</div>}
|
||||||
|
<div className="habit-range">
|
||||||
|
{item.periodStart === item.periodEnd
|
||||||
|
? '今日'
|
||||||
|
: `${item.periodStart.slice(5)} ~ ${item.periodEnd.slice(5)}`}
|
||||||
|
{item.over > 0 && <span className="habit-over">超额 {item.over}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="habit-actions">
|
||||||
|
<Button
|
||||||
|
size="mini"
|
||||||
|
color={item.completed ? 'default' : 'primary'}
|
||||||
|
onClick={() => doCheck(item)}
|
||||||
|
>
|
||||||
|
{item.completed ? '再打一次' : '打卡'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="habit-foot">
|
||||||
|
{item.done > 0 && (
|
||||||
|
<span className="habit-link" onClick={() => doUncheck(item)}>撤销</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className="habit-link"
|
||||||
|
onClick={() => {
|
||||||
|
setForm({ id: item.id, name: item.name, period: item.period, target: item.target, color: item.color, note: item.note })
|
||||||
|
setShowForm(true)
|
||||||
|
}}
|
||||||
|
>编辑</span>
|
||||||
|
<span className="habit-link danger" onClick={() => removeHabit(item)}>归档</span>
|
||||||
|
</div>
|
||||||
|
<Celebrate fire={bursts[item.id] || 0} color={item.color} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 新建 / 编辑 */}
|
||||||
|
<Popup
|
||||||
|
visible={showForm}
|
||||||
|
onMaskClick={() => setShowForm(false)}
|
||||||
|
bodyStyle={{ borderTopLeftRadius: 12, borderTopRightRadius: 12, paddingBottom: 16 }}
|
||||||
|
>
|
||||||
|
<div className="form-popup">
|
||||||
|
<div className="form-title">{form.id ? '编辑打卡项' : '新建打卡项'}</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label>名称</label>
|
||||||
|
<Input
|
||||||
|
placeholder="例如:喝水 / 健身 / 读书"
|
||||||
|
value={form.name}
|
||||||
|
maxLength={20}
|
||||||
|
onChange={(v) => setForm((f) => ({ ...f, name: v }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label>周期</label>
|
||||||
|
<Selector
|
||||||
|
options={PERIOD_OPTIONS}
|
||||||
|
value={[form.period]}
|
||||||
|
onChange={(v) => setForm((f) => ({ ...f, period: v[0] }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label>目标次数(周期内)</label>
|
||||||
|
<Stepper min={1} max={999} value={form.target} onChange={(v) => setForm((f) => ({ ...f, target: v }))} />
|
||||||
|
</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label>颜色</label>
|
||||||
|
<div className="color-row">
|
||||||
|
{COLORS.map((c) => (
|
||||||
|
<span
|
||||||
|
key={c}
|
||||||
|
className={'color-dot' + (form.color === c ? ' active' : '')}
|
||||||
|
style={{ background: c }}
|
||||||
|
onClick={() => setForm((f) => ({ ...f, color: c }))}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label>备注(可选)</label>
|
||||||
|
<Input
|
||||||
|
placeholder="补充说明"
|
||||||
|
value={form.note}
|
||||||
|
onChange={(v) => setForm((f) => ({ ...f, note: v }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-actions">
|
||||||
|
<Button block color="primary" onClick={submitForm}>保存</Button>
|
||||||
|
<Button block fill="outline" onClick={() => setShowForm(false)} style={{ marginTop: 8 }}>取消</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import {
|
||||||
|
Button, Dialog, DotLoading, Empty, Input, Popup, Segmented, Selector, Toast,
|
||||||
|
} from 'antd-mobile'
|
||||||
|
import { Celebrate } from '../../components/RingProgress.jsx'
|
||||||
|
import { checklistApi } from '../../api/checkin.js'
|
||||||
|
import useRefreshOnVisible from '../../hooks/useRefreshOnVisible.js'
|
||||||
|
|
||||||
|
const SCENES = ['出差', '回老家', '其他']
|
||||||
|
|
||||||
|
// 清单视图: 出差/回老家等场景的一次性检查清单 (F-LS-*)
|
||||||
|
export default function ListView() {
|
||||||
|
const [archived, setArchived] = useState(false)
|
||||||
|
const [lists, setLists] = useState(null)
|
||||||
|
const [err, setErr] = useState('')
|
||||||
|
const [openId, setOpenId] = useState(null)
|
||||||
|
const [newItem, setNewItem] = useState({})
|
||||||
|
const [bursts, setBursts] = useState({})
|
||||||
|
const [templates, setTemplates] = useState([])
|
||||||
|
const [showForm, setShowForm] = useState(false)
|
||||||
|
const [form, setForm] = useState({ title: '', scene: '出差', templateId: '' })
|
||||||
|
|
||||||
|
const load = useCallback((silent) => {
|
||||||
|
if (!silent) setErr('')
|
||||||
|
checklistApi.lists(archived)
|
||||||
|
.then((d) => setLists(d.items || []))
|
||||||
|
.catch((e) => { if (!silent) setErr(e.message) })
|
||||||
|
}, [archived])
|
||||||
|
|
||||||
|
useEffect(() => { setLists(null); load(false) }, [load])
|
||||||
|
useRefreshOnVisible(() => load(true))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checklistApi.templates().then((d) => setTemplates(d.items || [])).catch(() => {})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const withProgress = useCallback((list) => {
|
||||||
|
const items = list.items || []
|
||||||
|
const done = items.filter((i) => i.done).length
|
||||||
|
return {
|
||||||
|
...list,
|
||||||
|
total: items.length,
|
||||||
|
done,
|
||||||
|
percent: items.length > 0 ? done / items.length : 0,
|
||||||
|
completed: items.length > 0 && done === items.length,
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const patchList = (id, updater) =>
|
||||||
|
setLists((prev) => (prev || []).map((l) => (l.id === id ? updater(l) : l)))
|
||||||
|
|
||||||
|
// 勾选: 乐观更新 + 服务端确认 (F-LS-03, F-SY-06)
|
||||||
|
async function toggleItem(listId, item) {
|
||||||
|
const snapshot = lists
|
||||||
|
const next = !item.done
|
||||||
|
patchList(listId, (l) => withProgress({
|
||||||
|
...l,
|
||||||
|
items: l.items.map((i) => (i.id === item.id ? { ...i, done: next } : i)),
|
||||||
|
}))
|
||||||
|
try {
|
||||||
|
await checklistApi.updateItem(item.id, { done: next })
|
||||||
|
const l = (lists || []).find((x) => x.id === listId)
|
||||||
|
const updated = withProgress({
|
||||||
|
...l,
|
||||||
|
items: l.items.map((i) => (i.id === item.id ? { ...i, done: next } : i)),
|
||||||
|
})
|
||||||
|
if (updated.completed) setBursts((m) => ({ ...m, [listId]: (m[listId] || 0) + 1 }))
|
||||||
|
} catch (e) {
|
||||||
|
setLists(snapshot)
|
||||||
|
Toast.show({ content: e.message || '更新失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addItem(listId, title) {
|
||||||
|
const t = String(title || '').trim()
|
||||||
|
if (!t) return
|
||||||
|
try {
|
||||||
|
const { item } = await checklistApi.addItem(listId, { title: t })
|
||||||
|
patchList(listId, (l) => withProgress({ ...l, items: [...l.items, item] }))
|
||||||
|
setNewItem((m) => ({ ...m, [listId]: '' }))
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show({ content: e.message || '添加失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeItem(listId, itemId) {
|
||||||
|
const snapshot = lists
|
||||||
|
patchList(listId, (l) => withProgress({ ...l, items: l.items.filter((i) => i.id !== itemId) }))
|
||||||
|
try {
|
||||||
|
await checklistApi.removeItem(itemId)
|
||||||
|
} catch (e) {
|
||||||
|
setLists(snapshot)
|
||||||
|
Toast.show({ content: e.message || '删除失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAll(listId, done) {
|
||||||
|
const snapshot = lists
|
||||||
|
patchList(listId, (l) => withProgress({ ...l, items: l.items.map((i) => ({ ...i, done })) }))
|
||||||
|
try {
|
||||||
|
const { item } = await checklistApi.checkAll(listId, done)
|
||||||
|
patchList(listId, () => withProgress(item))
|
||||||
|
if (item.completed) setBursts((m) => ({ ...m, [listId]: (m[listId] || 0) + 1 }))
|
||||||
|
} catch (e) {
|
||||||
|
setLists(snapshot)
|
||||||
|
Toast.show({ content: e.message || '操作失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createList() {
|
||||||
|
const title = String(form.title || '').trim()
|
||||||
|
if (!title) return Toast.show({ content: '请填写清单名称' })
|
||||||
|
try {
|
||||||
|
await checklistApi.create({ title, scene: form.scene, templateId: form.templateId || undefined })
|
||||||
|
setShowForm(false)
|
||||||
|
setForm({ title: '', scene: '出差', templateId: '' })
|
||||||
|
load(true)
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show({ content: e.message || '创建失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function duplicate(list) {
|
||||||
|
try {
|
||||||
|
await checklistApi.duplicate(list.id)
|
||||||
|
Toast.show({ content: '已复制为新清单' })
|
||||||
|
load(true)
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show({ content: e.message || '复制失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveAsTemplate(list) {
|
||||||
|
try {
|
||||||
|
await checklistApi.createTemplate({
|
||||||
|
name: list.title,
|
||||||
|
scene: list.scene || '其他',
|
||||||
|
items: (list.items || []).map((i) => ({ title: i.title, note: i.note })),
|
||||||
|
})
|
||||||
|
Toast.show({ content: '已保存为模板' })
|
||||||
|
const d = await checklistApi.templates()
|
||||||
|
setTemplates(d.items || [])
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show({ content: e.message || '保存失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleArchive(list) {
|
||||||
|
try {
|
||||||
|
await checklistApi.update(list.id, { archived: !list.archived })
|
||||||
|
Toast.show({ content: list.archived ? '已恢复到进行中' : '已归档' })
|
||||||
|
load(true)
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show({ content: e.message || '操作失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeList(list) {
|
||||||
|
const ok = await Dialog.confirm({ content: `删除「${list.title}」?`, confirmText: '删除', cancelText: '取消' })
|
||||||
|
if (!ok) return
|
||||||
|
try {
|
||||||
|
await checklistApi.remove(list.id)
|
||||||
|
load(true)
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show({ content: e.message || '删除失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = useMemo(() => lists || [], [lists])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="list-view">
|
||||||
|
<div className="list-view-head">
|
||||||
|
<Segmented
|
||||||
|
options={[{ label: '进行中', value: 'active' }, { label: '已归档', value: 'archived' }]}
|
||||||
|
value={archived ? 'archived' : 'active'}
|
||||||
|
onChange={(v) => setArchived(v === 'archived')}
|
||||||
|
/>
|
||||||
|
<Button size="mini" color="primary" onClick={() => setShowForm(true)}>+ 新建</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{err && <div className="error-banner">{err}<span className="retry-link" onClick={() => load(false)}>重试</span></div>}
|
||||||
|
{lists === null && !err && <div className="monitor-loading"><DotLoading /> 加载中...</div>}
|
||||||
|
{lists !== null && !err && sorted.length === 0 && (
|
||||||
|
<Empty description={archived ? '暂无归档清单' : '还没有检查清单'} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="checklist-wrap">
|
||||||
|
{sorted.map((list) => {
|
||||||
|
const open = openId === list.id
|
||||||
|
return (
|
||||||
|
<div key={list.id} className={'checklist-card' + (list.completed ? ' done' : '')}>
|
||||||
|
<div className="checklist-head" onClick={() => setOpenId(open ? null : list.id)}>
|
||||||
|
<div className="checklist-title-row">
|
||||||
|
<span className="checklist-title">{list.title}</span>
|
||||||
|
{list.scene && <span className="checklist-scene">{list.scene}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="checklist-progress-row">
|
||||||
|
<div className="checklist-bar">
|
||||||
|
<div
|
||||||
|
className="checklist-bar-fill"
|
||||||
|
style={{ width: Math.round((list.percent || 0) * 100) + '%' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="checklist-count">{list.done}/{list.total}</span>
|
||||||
|
<span className={'checklist-arrow' + (open ? ' open' : '')}>›</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<div className="checklist-body">
|
||||||
|
{(list.items || []).map((item) => (
|
||||||
|
<div key={item.id} className={'checklist-item' + (item.done ? ' done' : '')}>
|
||||||
|
<span
|
||||||
|
className="check-box"
|
||||||
|
onClick={(e) => { e.stopPropagation(); toggleItem(list.id, item) }}
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none">
|
||||||
|
<path d="M4 12.5l5 5L20 6.5" stroke="#fff" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span className="checklist-item-title" onClick={() => toggleItem(list.id, item)}>
|
||||||
|
{item.title}
|
||||||
|
{item.note && <em>{item.note}</em>}
|
||||||
|
</span>
|
||||||
|
<span className="checklist-item-del" onClick={() => removeItem(list.id, item.id)}>×</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="checklist-add">
|
||||||
|
<Input
|
||||||
|
placeholder="添加检查项"
|
||||||
|
value={newItem[list.id] || ''}
|
||||||
|
onChange={(v) => setNewItem((m) => ({ ...m, [list.id]: v }))}
|
||||||
|
onEnterPress={() => addItem(list.id, newItem[list.id])}
|
||||||
|
/>
|
||||||
|
<Button size="mini" color="primary" onClick={() => addItem(list.id, newItem[list.id])}>添加</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="checklist-actions">
|
||||||
|
<span className="habit-link" onClick={() => checkAll(list.id, true)}>全选</span>
|
||||||
|
<span className="habit-link" onClick={() => checkAll(list.id, false)}>全不选</span>
|
||||||
|
<span className="habit-link" onClick={() => duplicate(list)}>复制</span>
|
||||||
|
<span className="habit-link" onClick={() => saveAsTemplate(list)}>存为模板</span>
|
||||||
|
<span className="habit-link" onClick={() => toggleArchive(list)}>{list.archived ? '恢复' : '归档'}</span>
|
||||||
|
<span className="habit-link danger" onClick={() => removeList(list)}>删除</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Celebrate fire={bursts[list.id] || 0} color="#00b578" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popup
|
||||||
|
visible={showForm}
|
||||||
|
onMaskClick={() => setShowForm(false)}
|
||||||
|
bodyStyle={{ borderTopLeftRadius: 12, borderTopRightRadius: 12, paddingBottom: 16 }}
|
||||||
|
>
|
||||||
|
<div className="form-popup">
|
||||||
|
<div className="form-title">新建检查清单</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label>名称</label>
|
||||||
|
<Input placeholder="例如:出差 · 杭州" value={form.title} onChange={(v) => setForm((f) => ({ ...f, title: v }))} />
|
||||||
|
</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label>场景</label>
|
||||||
|
<Selector
|
||||||
|
options={SCENES.map((s) => ({ label: s, value: s }))}
|
||||||
|
value={[form.scene]}
|
||||||
|
onChange={(v) => setForm((f) => ({ ...f, scene: v[0] }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label>从模板生成(可选)</label>
|
||||||
|
<Selector
|
||||||
|
options={[{ label: '不使用模板', value: '' }, ...templates.map((t) => ({ label: t.name, value: String(t.id) }))]}
|
||||||
|
value={[form.templateId]}
|
||||||
|
onChange={(v) => setForm((f) => ({ ...f, templateId: v[0] }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-actions">
|
||||||
|
<Button block color="primary" onClick={createList}>创建</Button>
|
||||||
|
<Button block fill="outline" onClick={() => setShowForm(false)} style={{ marginTop: 8 }}>取消</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// 前端日期/周期工具: 与 server/checkin/period.js 保持同一套口径 (Asia/Shanghai)
|
||||||
|
export const TZ = 'Asia/Shanghai'
|
||||||
|
|
||||||
|
const FMT = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 任意时间 -> 本地日历日 "YYYY-MM-DD" */
|
||||||
|
export function toDateStr(d = new Date()) {
|
||||||
|
return FMT.format(d instanceof Date ? d : new Date(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUTC(s) {
|
||||||
|
const [y, m, d] = String(s).split('-').map(Number)
|
||||||
|
return new Date(Date.UTC(y, (m || 1) - 1, d || 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromUTC(dt) {
|
||||||
|
return dt.toISOString().slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addDays(s, n) {
|
||||||
|
const d = toUTC(s)
|
||||||
|
d.setUTCDate(d.getUTCDate() + n)
|
||||||
|
return fromUTC(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addMonths(s, n) {
|
||||||
|
const [y, m] = String(s).split('-').map(Number)
|
||||||
|
const first = new Date(Date.UTC(y, (m || 1) - 1 + n, 1))
|
||||||
|
const day = Number(String(s).slice(8, 10)) || 1
|
||||||
|
const lastDay = new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + 1, 0)).getUTCDate()
|
||||||
|
first.setUTCDate(Math.min(day, lastDay))
|
||||||
|
return fromUTC(first)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 周一为一周第一天 */
|
||||||
|
export function startOfWeek(s) {
|
||||||
|
const dow = (toUTC(s).getUTCDay() + 6) % 7
|
||||||
|
return addDays(s, -dow)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PERIOD_LABEL = { day: '每天', week: '每周', month: '每月', year: '每年' }
|
||||||
|
|
||||||
|
export function periodRange(period, s) {
|
||||||
|
switch (period) {
|
||||||
|
case 'week': {
|
||||||
|
const start = startOfWeek(s)
|
||||||
|
return { start, end: addDays(start, 6) }
|
||||||
|
}
|
||||||
|
case 'month': {
|
||||||
|
const start = String(s).slice(0, 8) + '01'
|
||||||
|
const [y, m] = String(s).split('-').map(Number)
|
||||||
|
return { start, end: fromUTC(new Date(Date.UTC(y, m || 1, 0))) }
|
||||||
|
}
|
||||||
|
case 'year': {
|
||||||
|
const y = String(s).slice(0, 4)
|
||||||
|
return { start: y + '-01-01', end: y + '-12-31' }
|
||||||
|
}
|
||||||
|
default: return { start: s, end: s }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 月历网格: 返回 6*7 = 42 天 (含前后月的补位), 周一起始 */
|
||||||
|
export function monthGrid(monthStr) {
|
||||||
|
const first = monthStr + '-01'
|
||||||
|
const gridStart = startOfWeek(first)
|
||||||
|
const days = []
|
||||||
|
for (let i = 0; i < 42; i++) days.push(addDays(gridStart, i))
|
||||||
|
return days
|
||||||
|
}
|
||||||
|
|
||||||
|
export function weekdayLabels() {
|
||||||
|
return ['一', '二', '三', '四', '五', '六', '日']
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 相对今天的文案: 昨天 / 今天 / 明天 / MM-DD */
|
||||||
|
export function dayLabel(dateStr, today = toDateStr()) {
|
||||||
|
if (dateStr === today) return '今天'
|
||||||
|
if (dateStr === addDays(today, -1)) return '昨天'
|
||||||
|
if (dateStr === addDays(today, 1)) return '明天'
|
||||||
|
return dateStr.slice(5).replace('-', '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fmtMonth(monthStr) {
|
||||||
|
const [y, m] = monthStr.split('-')
|
||||||
|
return y + '年' + Number(m) + '月'
|
||||||
|
}
|
||||||
@@ -500,3 +500,171 @@ input, textarea, select { font: inherit; }
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.oil-day.future .oil-day-status { color: rgba(255,255,255,.85); }
|
.oil-day.future .oil-day-status { color: rgba(255,255,255,.85); }
|
||||||
|
|
||||||
|
/* ================== 打卡 / 日历 / 清单 (V0.2) ================== */
|
||||||
|
.checkin-content { padding: 0 0 16px; flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
|
||||||
|
.checkin-content .adm-tabs-content { --content-padding: 0; }
|
||||||
|
|
||||||
|
/* ----- 环形进度 ----- */
|
||||||
|
.ring-wrap { position: relative; display: inline-flex; flex-shrink: 0; align-items: center; justify-content: center; }
|
||||||
|
.ring-wrap.clickable { cursor: pointer; }
|
||||||
|
.ring-wrap.disabled { opacity: .5; }
|
||||||
|
.ring-svg { display: block; }
|
||||||
|
.ring-bar { transition: stroke-dashoffset 0.45s cubic-bezier(0.4, 0, 0.2, 1); }
|
||||||
|
.ring-center {
|
||||||
|
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 13px; font-weight: 600; color: var(--color-text); font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.ring-num em { font-style: normal; font-size: 10px; color: var(--color-muted); font-weight: 400; }
|
||||||
|
.ring-check { animation: ring-check-in 0.32s ease; }
|
||||||
|
@keyframes ring-check-in { from { transform: scale(0.4); opacity: 0; } to { transform: scale(1); opacity: 1; } }
|
||||||
|
.ring-over {
|
||||||
|
position: absolute; right: -2px; bottom: -2px; font-size: 10px; font-weight: 600;
|
||||||
|
color: #fff; background: #ff8f1f; border-radius: 8px; padding: 0 4px; line-height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----- 完成特效 ----- */
|
||||||
|
.celebrate { position: absolute; inset: 0; pointer-events: none; z-index: 6; }
|
||||||
|
.celebrate i {
|
||||||
|
position: absolute; left: 50%; top: 50%; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px;
|
||||||
|
border-radius: 50%; animation: burst-fly 0.7s ease-out forwards;
|
||||||
|
}
|
||||||
|
@keyframes burst-fly {
|
||||||
|
0% { transform: rotate(var(--a)) translateX(0) scale(1); opacity: 1; }
|
||||||
|
70% { opacity: 1; }
|
||||||
|
100% { transform: rotate(var(--a)) translateX(var(--d)) scale(0.3); opacity: 0; }
|
||||||
|
}
|
||||||
|
.celebrate-ring {
|
||||||
|
position: absolute; left: 50%; top: 50%; width: 64px; height: 64px; margin: -32px 0 0 -32px;
|
||||||
|
border-radius: 50%; border: 2px solid var(--color-primary); animation: ring-pulse 0.7s ease-out forwards;
|
||||||
|
}
|
||||||
|
@keyframes ring-pulse {
|
||||||
|
0% { transform: scale(0.3); opacity: 0.9; }
|
||||||
|
100% { transform: scale(1.7); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----- 打卡视图 ----- */
|
||||||
|
.habit-view { padding: 0 12px 20px; }
|
||||||
|
.habit-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 8px; padding: 10px 2px; }
|
||||||
|
.habit-date { font-size: 18px; font-weight: 700; color: var(--color-text); }
|
||||||
|
.habit-summary { font-size: 12px; color: var(--color-muted); margin-top: 2px; }
|
||||||
|
.habit-summary strong { color: var(--color-primary); font-size: 14px; }
|
||||||
|
.habit-badge { margin-left: 6px; font-size: 11px; color: #ad6800; background: #fff7e6; border-radius: 4px; padding: 1px 5px; }
|
||||||
|
.habit-head-right { display: flex; gap: 6px; flex-shrink: 0; }
|
||||||
|
.checkin-empty { padding: 48px 0; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
||||||
|
|
||||||
|
.habit-list { display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.habit-card {
|
||||||
|
position: relative; background: #fff; border-radius: 10px; padding: 12px;
|
||||||
|
display: grid; grid-template-columns: auto 1fr auto; grid-template-areas: "ring main actions" "foot foot foot";
|
||||||
|
gap: 8px 12px; box-shadow: 0 1px 2px rgba(0,0,0,.04);
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
.habit-card:active { background: #fafafa; }
|
||||||
|
.habit-card.done { background: #f7fbf8; }
|
||||||
|
.habit-card .ring-wrap { grid-area: ring; }
|
||||||
|
.habit-main { grid-area: main; min-width: 0; }
|
||||||
|
.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(--color-border);
|
||||||
|
}
|
||||||
|
.habit-name { font-size: 15px; font-weight: 600; color: var(--color-text); word-break: break-all; }
|
||||||
|
.habit-meta { font-size: 12px; color: var(--color-muted); margin-top: 3px; }
|
||||||
|
.habit-streak { margin-left: 8px; color: #ff8f1f; background: rgba(255,143,31,.12); border-radius: 4px; padding: 1px 6px; }
|
||||||
|
.habit-note { font-size: 11px; color: var(--color-muted); margin-top: 3px; }
|
||||||
|
.habit-range { font-size: 11px; color: var(--color-muted); margin-top: 3px; font-variant-numeric: tabular-nums; }
|
||||||
|
.habit-over { margin-left: 8px; color: #ff8f1f; }
|
||||||
|
.habit-link { font-size: 12px; color: var(--color-primary); cursor: pointer; }
|
||||||
|
.habit-link.danger { color: var(--color-danger); }
|
||||||
|
|
||||||
|
/* ----- 日历视图 ----- */
|
||||||
|
.calendar-view { padding: 0 12px 20px; }
|
||||||
|
.cal-month-bar { display: flex; align-items: center; gap: 10px; padding: 8px 2px; }
|
||||||
|
.cal-month { font-size: 16px; font-weight: 700; color: var(--color-text); }
|
||||||
|
.cal-arrow { font-size: 20px; color: var(--color-muted); cursor: pointer; padding: 0 4px; line-height: 1; }
|
||||||
|
.cal-today-btn { margin-left: auto; font-size: 12px; color: var(--color-primary); cursor: pointer; }
|
||||||
|
.cal-quick { padding: 2px 0 10px; }
|
||||||
|
.cal-grid { background: #fff; border-radius: 10px; padding: 8px 6px 10px; box-shadow: 0 1px 2px rgba(0,0,0,.04); }
|
||||||
|
.cal-weekday, .cal-days { display: grid; grid-template-columns: repeat(7, 1fr); }
|
||||||
|
.cal-weekday span { text-align: center; font-size: 11px; color: var(--color-muted); padding: 4px 0; }
|
||||||
|
.cal-day {
|
||||||
|
position: relative; height: 38px; display: flex; flex-direction: column; align-items: center;
|
||||||
|
justify-content: center; border-radius: 8px; cursor: pointer;
|
||||||
|
}
|
||||||
|
.cal-day-num { font-size: 13px; color: var(--color-text); font-variant-numeric: tabular-nums; }
|
||||||
|
.cal-day.out .cal-day-num { color: #c8c8c8; }
|
||||||
|
.cal-day.future .cal-day-num { color: #b0b0b0; }
|
||||||
|
.cal-day.today .cal-day-num { color: var(--color-primary); font-weight: 700; }
|
||||||
|
.cal-day.selected { background: var(--color-primary); }
|
||||||
|
.cal-day.selected .cal-day-num { color: #fff; font-weight: 600; }
|
||||||
|
.cal-dot { width: 5px; height: 5px; border-radius: 50%; margin-top: 3px; }
|
||||||
|
.cal-dot.partial { background: #ffc53d; }
|
||||||
|
.cal-dot.full { background: #00b578; }
|
||||||
|
.cal-dot.over { background: #a855f7; }
|
||||||
|
.cal-day.selected .cal-dot { background: #fff; }
|
||||||
|
.cal-rate { font-size: 12px; color: var(--color-muted); padding: 8px 2px; }
|
||||||
|
.cal-rate strong { color: var(--color-primary); }
|
||||||
|
.cal-detail { margin-top: 6px; }
|
||||||
|
.cal-detail-title { font-size: 14px; font-weight: 600; color: var(--color-text); padding: 6px 2px 8px; }
|
||||||
|
.cal-item {
|
||||||
|
position: relative; display: flex; align-items: center; gap: 8px; background: #fff;
|
||||||
|
border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; box-shadow: 0 1px 2px rgba(0,0,0,.04);
|
||||||
|
}
|
||||||
|
.cal-item.done { background: #f7fbf8; }
|
||||||
|
.cal-item-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||||
|
.cal-item-name { font-size: 14px; font-weight: 600; color: var(--color-text); }
|
||||||
|
.cal-item-meta { font-size: 11px; color: var(--color-muted); flex: 1; text-align: right; font-variant-numeric: tabular-nums; }
|
||||||
|
.cal-item-right { flex-shrink: 0; }
|
||||||
|
.cal-item-plan { font-size: 12px; color: var(--color-muted); }
|
||||||
|
|
||||||
|
/* ----- 清单视图 ----- */
|
||||||
|
.list-view { padding: 0 12px 20px; }
|
||||||
|
.list-view-head { display: flex; align-items: center; gap: 8px; padding: 8px 0 10px; }
|
||||||
|
.list-view-head .adm-segmented { flex: 1; }
|
||||||
|
.checklist-wrap { display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.checklist-card {
|
||||||
|
position: relative; background: #fff; border-radius: 10px; padding: 12px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,.04);
|
||||||
|
}
|
||||||
|
.checklist-card.done { background: #f7fbf8; }
|
||||||
|
.checklist-head { cursor: pointer; }
|
||||||
|
.checklist-title-row { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.checklist-title { font-size: 15px; font-weight: 600; color: var(--color-text); }
|
||||||
|
.checklist-scene { font-size: 11px; color: var(--color-primary); background: #f0f5ff; border-radius: 4px; padding: 1px 6px; }
|
||||||
|
.checklist-progress-row { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
|
||||||
|
.checklist-bar { flex: 1; height: 6px; border-radius: 3px; background: #f0f0f0; overflow: hidden; }
|
||||||
|
.checklist-bar-fill { height: 100%; border-radius: 3px; background: #00b578; transition: width 0.4s ease; }
|
||||||
|
.checklist-count { font-size: 12px; color: var(--color-muted); font-variant-numeric: tabular-nums; }
|
||||||
|
.checklist-arrow { color: var(--color-muted); transition: transform 0.2s ease; }
|
||||||
|
.checklist-arrow.open { transform: rotate(90deg); }
|
||||||
|
.checklist-body { margin-top: 10px; padding-top: 8px; border-top: 1px dashed var(--color-border); }
|
||||||
|
.checklist-item { display: flex; align-items: center; gap: 8px; padding: 7px 0; }
|
||||||
|
.check-box {
|
||||||
|
width: 20px; height: 20px; border-radius: 50%; border: 1.5px solid #d0d0d0; flex-shrink: 0;
|
||||||
|
display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
|
||||||
|
transition: background 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
.check-box svg { opacity: 0; transition: opacity 0.15s ease; }
|
||||||
|
.checklist-item.done .check-box { background: #00b578; border-color: #00b578; }
|
||||||
|
.checklist-item.done .check-box svg { opacity: 1; }
|
||||||
|
.checklist-item-title { flex: 1; font-size: 14px; color: var(--color-text); cursor: pointer; }
|
||||||
|
.checklist-item-title em { display: block; font-style: normal; font-size: 11px; color: var(--color-muted); margin-top: 2px; }
|
||||||
|
.checklist-item.done .checklist-item-title { color: #a0a0a0; text-decoration: line-through; }
|
||||||
|
.checklist-item-del { color: #ccc; font-size: 18px; line-height: 1; padding: 0 4px; cursor: pointer; }
|
||||||
|
.checklist-add { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
|
||||||
|
.checklist-add .adm-input { flex: 1; border: 1px solid var(--color-border); border-radius: 6px; padding: 6px 8px; }
|
||||||
|
.checklist-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 10px; }
|
||||||
|
|
||||||
|
/* ----- 表单弹窗 (打卡项 / 清单) ----- */
|
||||||
|
.form-popup { padding: 16px 16px 8px; }
|
||||||
|
.form-title { font-size: 16px; font-weight: 700; color: var(--color-text); margin-bottom: 12px; }
|
||||||
|
.form-field { margin-bottom: 14px; }
|
||||||
|
.form-field > label { display: block; font-size: 12px; color: var(--color-muted); margin-bottom: 6px; }
|
||||||
|
.form-field .adm-input { border: 1px solid var(--color-border); border-radius: 8px; padding: 8px 10px; }
|
||||||
|
.form-actions { margin-top: 18px; }
|
||||||
|
.color-row { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.color-dot {
|
||||||
|
width: 26px; height: 26px; border-radius: 50%; cursor: pointer; display: inline-block;
|
||||||
|
border: 2px solid transparent; transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
.color-dot.active { border-color: rgba(0,0,0,.25); transform: scale(1.12); }
|
||||||
|
|||||||
Reference in New Issue
Block a user