dsh-todo-time-log 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/lib/client.js +410 -174
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
|
|
84
84
|
<p align="center"><img src="https://github.com/FantasyStarry/dsh-todo-time-log/raw/main/docs/images/demo-complete.png" alt="完成与撤销操作示范" width="100%"></p>
|
|
85
85
|
|
|
86
|
-
1.
|
|
86
|
+
1. 点行首的勾选圈(看板里是卡片上的勾选框),完成时刻自动写入。
|
|
87
87
|
2. 底部浮出 toast,「撤销」可以一键退回未完成状态。
|
|
88
88
|
3. 表格的「完成时间」列立即出现时间;页面高度不变,视线不被打断。
|
|
89
89
|
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
|
|
92
92
|
<p align="center"><img src="https://github.com/FantasyStarry/dsh-todo-time-log/raw/main/docs/images/demo-quickadd.png" alt="对话式快速录入操作示范" width="100%"></p>
|
|
93
93
|
|
|
94
|
-
1.
|
|
94
|
+
1. 点新建框右侧的「批量」进入批量录入,先确认工作区 —— `@` 文件补全按工作区隔离。
|
|
95
95
|
2. 一句话可以写多条,支持 `#分类`、`@文件`、`!优先级`、`^类型`。
|
|
96
96
|
3. 发送后逐条回执;不满意可以整批撤销。
|
|
97
97
|
|
|
@@ -110,8 +110,9 @@
|
|
|
110
110
|
|
|
111
111
|
| 能力 | 说明 |
|
|
112
112
|
| --- | --- |
|
|
113
|
-
| 🕒 创建 / 完成时间 | 全自动记录,本地时间 `YYYY-MM-DD HH:mm:ss`,未完成显示
|
|
114
|
-
| 🗂 四视图 | 表格 / 看板 / 甘特图 /
|
|
113
|
+
| 🕒 创建 / 完成时间 | 全自动记录,本地时间 `YYYY-MM-DD HH:mm:ss`,未完成显示 `-`;表格的完成时间列直接给出「用时 / 进行中」两行,重复完成、重复取消都会给出明确提示 |
|
|
114
|
+
| 🗂 四视图 | 表格 / 看板 / 甘特图 / 日历,同一份数据四种视角;切换视图保留筛选与排序,排序统一走表头 |
|
|
115
|
+
| 🧭 行内操作 | 勾选圈完成、点标题展开详情、行尾「⋯」编辑 / 删除;删除二次确认后仍可用 toast 撤销 |
|
|
115
116
|
| 📊 甘特图 | 纯前端计算(不新增接口):条形 = 创建 → 完成,未完成延伸到「现在」,支持全部 / 近 7 天 / 近 30 天 |
|
|
116
117
|
| 🏷 分类 · 优先级 · 类型 | 项目分类(可从「已用分类 / 工作区 / 文件夹」建议里选)、高 / 中 / 低优先级、缺陷 / 新功能 / 改进 / 疑问 / 杂项 / 其他 |
|
|
117
118
|
| 📎 `@工作区文件` | 把待办关联到代码文件;只给文件、不给分类时,自动取文件所在目录作为分类 |
|
|
@@ -119,7 +120,8 @@
|
|
|
119
120
|
| 🤖 智能体工具 | 智能体可 `list / get / create / complete / uncomplete / update / delete / health`,与 UI 共用同一实现 |
|
|
120
121
|
| 📦 导入 / 导出 | 页头「数据」:JSON 无损备份、CSV 给 Excel 统计;导入先预览再写入,支持合并 / 仅新增 / 覆盖全部,自动备份并可直接撤销 |
|
|
121
122
|
| 🔒 本地数据 | `<DSH_PROFILE_DIR 或 ~/.dsh>/todo-time-log/todos.json`;临时文件 + rename 原子替换、写入串行化、跨进程文件锁 |
|
|
122
|
-
|
|
|
123
|
+
| ✨ 动效 | 列表入场错位、勾选回弹、详情 / 菜单展开、对话框淡入、甘特条「画」出来、筛选计数弹一下;只用 transform / opacity / clip-path,时长 120–200ms |
|
|
124
|
+
| 🌗 主题与无障碍 | 全部颜色走 `--dsw-alias-*` token,明暗主题自适应;`aria-live`、`aria-sort`、键盘操作;`prefers-reduced-motion` 下全部动画与过渡关闭 |
|
|
123
125
|
| ⌨️ 面板快捷键 | `/` 聚焦搜索、`n` 聚焦新建、`f` 循环切换筛选 |
|
|
124
126
|
|
|
125
127
|
---
|
|
@@ -175,7 +177,7 @@ plugin_manager action=install_bundle target=<本地仓库绝对路径>
|
|
|
175
177
|
| 入口 | 怎么用 | 适合 |
|
|
176
178
|
| --- | --- | --- |
|
|
177
179
|
| 面板表单 | 顶部输入框 + 分类 / 优先级 / 类型选择器,Enter 创建 | 手动逐条记录 |
|
|
178
|
-
| 对话式录入 |
|
|
180
|
+
| 对话式录入 | 新建框右侧「批量」,一句话写多条,逐条回执 + 撤销 | 一次记一堆 |
|
|
179
181
|
| `/todo` 命令 | Harness 主输入框:`/todo 写周报 #工作 lib/store.js` | 不离开对话流 |
|
|
180
182
|
| 智能体工具 | 让模型调用 `todo_time_log`(`create` / `complete` / `list` …) | 让 AI 代记、代查 |
|
|
181
183
|
|
package/lib/client.js
CHANGED
|
@@ -16,16 +16,12 @@
|
|
|
16
16
|
"create.placeholder": "输入待办;可写 #分类 @文件,Enter 创建",
|
|
17
17
|
"create.action": "添加",
|
|
18
18
|
"create.actionBusy": "添加中…",
|
|
19
|
+
"create.batch": "批量",
|
|
20
|
+
"create.batchTitle": "批量录入:一句话记多条",
|
|
19
21
|
"filter.label": "筛选",
|
|
20
22
|
"filter.all": "全部",
|
|
21
23
|
"filter.pending": "未完成",
|
|
22
24
|
"filter.completed": "已完成",
|
|
23
|
-
"sort.label": "排序",
|
|
24
|
-
"sort.createdAt": "创建时间",
|
|
25
|
-
"sort.completedAt": "完成时间",
|
|
26
|
-
"direction.asc": "升序",
|
|
27
|
-
"direction.desc": "降序",
|
|
28
|
-
"direction.toggleAria": "切换排序方向(当前:{direction})",
|
|
29
25
|
"list.loading": "加载中…",
|
|
30
26
|
"list.refreshing": "更新中…",
|
|
31
27
|
"list.error": "加载失败",
|
|
@@ -84,9 +80,6 @@
|
|
|
84
80
|
"projects.groupFolder": "文件夹",
|
|
85
81
|
"projects.countOption": "{name}({count})",
|
|
86
82
|
"group.header": "{project} · {count} 条",
|
|
87
|
-
"sort.updatedAt": "更新时间",
|
|
88
|
-
"sort.project": "分类",
|
|
89
|
-
"sort.priority": "优先级",
|
|
90
83
|
"priority.label": "优先级",
|
|
91
84
|
"priority.none": "未设置",
|
|
92
85
|
"priority.high": "高",
|
|
@@ -126,6 +119,10 @@
|
|
|
126
119
|
"table.status": "状态",
|
|
127
120
|
"table.createdAt": "创建时间",
|
|
128
121
|
"table.completedAt": "完成时间",
|
|
122
|
+
"table.spent": "用时 {duration}",
|
|
123
|
+
"table.elapsed": "进行中 {duration}",
|
|
124
|
+
"table.more": "更多操作",
|
|
125
|
+
"table.moreAria": "打开待办操作:{title}",
|
|
129
126
|
"table.actions": "操作",
|
|
130
127
|
"table.detail": "详情",
|
|
131
128
|
"table.sortAria": "按 {column} 排序",
|
|
@@ -246,6 +243,7 @@
|
|
|
246
243
|
"notice.created": "已创建待办。",
|
|
247
244
|
"notice.updated": "已更新待办。",
|
|
248
245
|
"notice.deleted": "已删除待办。",
|
|
246
|
+
"notice.restored": "已恢复待办。",
|
|
249
247
|
"notice.completed": "已标记为完成。",
|
|
250
248
|
"notice.uncompleted": "已取消完成。",
|
|
251
249
|
"notice.quickAddCreated": "已添加 {count} 条待办",
|
|
@@ -307,16 +305,12 @@
|
|
|
307
305
|
"create.placeholder": "Type a todo; #project @file supported, Enter to create",
|
|
308
306
|
"create.action": "Add",
|
|
309
307
|
"create.actionBusy": "Adding…",
|
|
308
|
+
"create.batch": "Batch",
|
|
309
|
+
"create.batchTitle": "Batch entry: add several todos in one message",
|
|
310
310
|
"filter.label": "Filter",
|
|
311
311
|
"filter.all": "All",
|
|
312
312
|
"filter.pending": "Pending",
|
|
313
313
|
"filter.completed": "Completed",
|
|
314
|
-
"sort.label": "Sort",
|
|
315
|
-
"sort.createdAt": "Created",
|
|
316
|
-
"sort.completedAt": "Completed",
|
|
317
|
-
"direction.asc": "Ascending",
|
|
318
|
-
"direction.desc": "Descending",
|
|
319
|
-
"direction.toggleAria": "Switch sort direction (currently {direction})",
|
|
320
314
|
"list.loading": "Loading…",
|
|
321
315
|
"list.refreshing": "Refreshing…",
|
|
322
316
|
"list.error": "Failed to load",
|
|
@@ -375,9 +369,6 @@
|
|
|
375
369
|
"projects.groupFolder": "Folders",
|
|
376
370
|
"projects.countOption": "{name} ({count})",
|
|
377
371
|
"group.header": "{project} · {count} item(s)",
|
|
378
|
-
"sort.updatedAt": "Updated",
|
|
379
|
-
"sort.project": "Project",
|
|
380
|
-
"sort.priority": "Priority",
|
|
381
372
|
"priority.label": "Priority",
|
|
382
373
|
"priority.none": "None",
|
|
383
374
|
"priority.high": "High",
|
|
@@ -417,6 +408,10 @@
|
|
|
417
408
|
"table.status": "Status",
|
|
418
409
|
"table.createdAt": "Created",
|
|
419
410
|
"table.completedAt": "Completed",
|
|
411
|
+
"table.spent": "{duration} spent",
|
|
412
|
+
"table.elapsed": "{duration} so far",
|
|
413
|
+
"table.more": "More actions",
|
|
414
|
+
"table.moreAria": "Open actions for {title}",
|
|
420
415
|
"table.actions": "Actions",
|
|
421
416
|
"table.detail": "Details",
|
|
422
417
|
"table.sortAria": "Sort by {column}",
|
|
@@ -537,6 +532,7 @@
|
|
|
537
532
|
"notice.created": "Todo created.",
|
|
538
533
|
"notice.updated": "Todo updated.",
|
|
539
534
|
"notice.deleted": "Todo deleted.",
|
|
535
|
+
"notice.restored": "Todo restored.",
|
|
540
536
|
"notice.completed": "Marked as completed.",
|
|
541
537
|
"notice.uncompleted": "Marked as pending.",
|
|
542
538
|
"notice.quickAddCreated": "Added {count} todo(s)",
|
|
@@ -612,13 +608,6 @@
|
|
|
612
608
|
{ value: "pending", labelKey: "filter.pending" },
|
|
613
609
|
{ value: "completed", labelKey: "filter.completed" }
|
|
614
610
|
];
|
|
615
|
-
var SORTS = [
|
|
616
|
-
{ value: "createdAt", labelKey: "sort.createdAt" },
|
|
617
|
-
{ value: "completedAt", labelKey: "sort.completedAt" },
|
|
618
|
-
{ value: "updatedAt", labelKey: "sort.updatedAt" },
|
|
619
|
-
{ value: "project", labelKey: "sort.project" },
|
|
620
|
-
{ value: "priority", labelKey: "sort.priority" }
|
|
621
|
-
];
|
|
622
611
|
var PRIORITIES = [
|
|
623
612
|
{ value: "", labelKey: "priority.none" },
|
|
624
613
|
{ value: "high", labelKey: "priority.high" },
|
|
@@ -710,6 +699,11 @@
|
|
|
710
699
|
font-size: 14px;
|
|
711
700
|
line-height: 1.6;
|
|
712
701
|
-webkit-font-smoothing: antialiased;
|
|
702
|
+
/* 动效令牌:短、克制、只服务状态变化(入场 / 浮层 / 勾选)。 */
|
|
703
|
+
--ttl-ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
704
|
+
--ttl-ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
|
|
705
|
+
--ttl-dur-fast: 120ms;
|
|
706
|
+
--ttl-dur-base: 180ms;
|
|
713
707
|
}
|
|
714
708
|
.ttl-scroll {
|
|
715
709
|
flex: 1;
|
|
@@ -790,6 +784,7 @@
|
|
|
790
784
|
background: var(--dsw-alias-bg-layer-1);
|
|
791
785
|
box-shadow: var(--dsw-shadow-lv1);
|
|
792
786
|
transition: border-color 0.15s, box-shadow 0.15s;
|
|
787
|
+
animation: ttl-fade-in 160ms var(--ttl-ease-out);
|
|
793
788
|
}
|
|
794
789
|
.ttl-composer:hover { border-color: var(--dsw-alias-border-l3); }
|
|
795
790
|
.ttl-composer:focus-within {
|
|
@@ -820,6 +815,8 @@
|
|
|
820
815
|
color: var(--dsw-alias-label-primary);
|
|
821
816
|
}
|
|
822
817
|
.ttl-composer .ttl-button-primary { height: 36px; padding: 0 18px; }
|
|
818
|
+
/* 「批量」是捕获区的模式切换,不是第二个主操作:保持 ghost 层级。 */
|
|
819
|
+
.ttl-composer-batch { height: 32px; padding: 0 8px; }
|
|
823
820
|
.ttl-create-suggest {
|
|
824
821
|
position: absolute;
|
|
825
822
|
top: calc(100% + 6px);
|
|
@@ -860,8 +857,9 @@
|
|
|
860
857
|
font-size: 13px;
|
|
861
858
|
line-height: 1;
|
|
862
859
|
cursor: pointer;
|
|
863
|
-
transition: background 0.15s, color 0.15s;
|
|
860
|
+
transition: background 0.15s, color 0.15s, border-color 0.15s, box-shadow 0.15s, transform var(--ttl-dur-fast) var(--ttl-ease-out);
|
|
864
861
|
}
|
|
862
|
+
.ttl-button:active:not(:disabled) { transform: scale(0.98); }
|
|
865
863
|
.ttl-button-primary {
|
|
866
864
|
height: 34px;
|
|
867
865
|
padding: 0 16px;
|
|
@@ -886,7 +884,6 @@
|
|
|
886
884
|
.ttl-ghost-button:focus-visible,
|
|
887
885
|
.ttl-seg-item:focus-visible,
|
|
888
886
|
.ttl-link:focus-visible,
|
|
889
|
-
.ttl-sort-dir:focus-visible,
|
|
890
887
|
.ttl-toast-action:focus-visible,
|
|
891
888
|
.ttl-toast-close:focus-visible,
|
|
892
889
|
.ttl-select select:focus-visible {
|
|
@@ -914,7 +911,14 @@
|
|
|
914
911
|
border-radius: 0;
|
|
915
912
|
}
|
|
916
913
|
.ttl-data .ttl-list > .ttl-board { padding: 12px 12px 16px; }
|
|
914
|
+
/* 工具栏固定两行:第一行视图 + 搜索;第二行视图专属控件 + 全局筛选。 */
|
|
917
915
|
.ttl-toolbar {
|
|
916
|
+
display: flex;
|
|
917
|
+
flex-direction: column;
|
|
918
|
+
align-items: stretch;
|
|
919
|
+
gap: 8px;
|
|
920
|
+
}
|
|
921
|
+
.ttl-toolbar-row {
|
|
918
922
|
display: flex;
|
|
919
923
|
flex-wrap: wrap;
|
|
920
924
|
align-items: center;
|
|
@@ -949,6 +953,9 @@
|
|
|
949
953
|
line-height: 1;
|
|
950
954
|
white-space: nowrap;
|
|
951
955
|
cursor: pointer;
|
|
956
|
+
transition: background-color var(--ttl-dur-fast) var(--ttl-ease-out),
|
|
957
|
+
color var(--ttl-dur-fast) var(--ttl-ease-out),
|
|
958
|
+
box-shadow var(--ttl-dur-fast) var(--ttl-ease-out);
|
|
952
959
|
}
|
|
953
960
|
.ttl-seg-item:hover { color: var(--dsw-alias-label-primary); }
|
|
954
961
|
.ttl-seg-item[aria-pressed='true'] {
|
|
@@ -962,6 +969,7 @@
|
|
|
962
969
|
font-size: 11px;
|
|
963
970
|
font-weight: 500;
|
|
964
971
|
font-variant-numeric: tabular-nums;
|
|
972
|
+
animation: ttl-count-pop 200ms var(--ttl-ease-out);
|
|
965
973
|
}
|
|
966
974
|
.ttl-seg-item[aria-pressed='true'] .ttl-seg-count { color: var(--dsw-alias-label-secondary); }
|
|
967
975
|
.ttl-select {
|
|
@@ -982,26 +990,6 @@
|
|
|
982
990
|
font-size: 12px;
|
|
983
991
|
cursor: pointer;
|
|
984
992
|
}
|
|
985
|
-
.ttl-sort-dir {
|
|
986
|
-
display: inline-flex;
|
|
987
|
-
align-items: center;
|
|
988
|
-
justify-content: center;
|
|
989
|
-
width: 28px;
|
|
990
|
-
height: 28px;
|
|
991
|
-
padding: 0;
|
|
992
|
-
border: 0.5px solid var(--dsw-alias-border-l3);
|
|
993
|
-
border-radius: var(--dsw-radius-sm);
|
|
994
|
-
background: none;
|
|
995
|
-
color: var(--dsw-alias-label-secondary);
|
|
996
|
-
font: inherit;
|
|
997
|
-
font-size: 13px;
|
|
998
|
-
line-height: 1;
|
|
999
|
-
cursor: pointer;
|
|
1000
|
-
}
|
|
1001
|
-
.ttl-sort-dir:hover {
|
|
1002
|
-
background: var(--dsw-alias-interactive-bg-hover);
|
|
1003
|
-
color: var(--dsw-alias-label-primary);
|
|
1004
|
-
}
|
|
1005
993
|
.ttl-refreshing {
|
|
1006
994
|
min-width: 52px;
|
|
1007
995
|
font-size: 12px;
|
|
@@ -1246,6 +1234,7 @@
|
|
|
1246
1234
|
color: var(--dsw-alias-label-tertiary);
|
|
1247
1235
|
font-size: 12px;
|
|
1248
1236
|
line-height: 20px;
|
|
1237
|
+
animation: ttl-fade-down 160ms var(--ttl-ease-out);
|
|
1249
1238
|
}
|
|
1250
1239
|
.ttl-detail-list {
|
|
1251
1240
|
display: grid;
|
|
@@ -1294,6 +1283,74 @@
|
|
|
1294
1283
|
}
|
|
1295
1284
|
.ttl-link:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1296
1285
|
.ttl-link-danger { color: var(--dsw-alias-state-error-primary); }
|
|
1286
|
+
/* 表格「完成时间」列双行:时刻 + 用时 / 进行中。 */
|
|
1287
|
+
.ttl-td-time .ttl-time-primary { display: block; line-height: 18px; }
|
|
1288
|
+
.ttl-time-duration {
|
|
1289
|
+
display: block;
|
|
1290
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1291
|
+
font-size: 11px;
|
|
1292
|
+
line-height: 16px;
|
|
1293
|
+
font-variant-numeric: tabular-nums;
|
|
1294
|
+
/* 完成后这一行才出现,淡入让「用时」自然接手。 */
|
|
1295
|
+
animation: ttl-fade-in 160ms var(--ttl-ease-out);
|
|
1296
|
+
}
|
|
1297
|
+
/* 表格行尾「⋯」与行内菜单:与看板卡片同一套动作模型。 */
|
|
1298
|
+
.ttl-row-more {
|
|
1299
|
+
width: 22px;
|
|
1300
|
+
height: 22px;
|
|
1301
|
+
padding: 0;
|
|
1302
|
+
border: 0;
|
|
1303
|
+
border-radius: 6px;
|
|
1304
|
+
background: none;
|
|
1305
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1306
|
+
font: inherit;
|
|
1307
|
+
font-size: 15px;
|
|
1308
|
+
line-height: 1;
|
|
1309
|
+
cursor: pointer;
|
|
1310
|
+
}
|
|
1311
|
+
.ttl-row-more:hover {
|
|
1312
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
1313
|
+
color: var(--dsw-alias-label-primary);
|
|
1314
|
+
}
|
|
1315
|
+
.ttl-row-more:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1316
|
+
/* 表格行尾:⋯ 在上,展开后的动作在下,整列右对齐且不撑破操作列。 */
|
|
1317
|
+
.ttl-table-more {
|
|
1318
|
+
display: flex;
|
|
1319
|
+
flex-direction: column;
|
|
1320
|
+
align-items: flex-end;
|
|
1321
|
+
gap: 2px;
|
|
1322
|
+
}
|
|
1323
|
+
.ttl-row-menu {
|
|
1324
|
+
display: flex;
|
|
1325
|
+
flex-wrap: wrap;
|
|
1326
|
+
justify-content: flex-end;
|
|
1327
|
+
gap: 0;
|
|
1328
|
+
animation: ttl-fade-down var(--ttl-dur-fast) var(--ttl-ease-out);
|
|
1329
|
+
}
|
|
1330
|
+
.ttl-row-menu-item {
|
|
1331
|
+
padding: 1px 2px;
|
|
1332
|
+
border: 0;
|
|
1333
|
+
border-radius: 6px;
|
|
1334
|
+
background: none;
|
|
1335
|
+
color: var(--dsw-alias-label-secondary);
|
|
1336
|
+
font: inherit;
|
|
1337
|
+
font-size: 12px;
|
|
1338
|
+
line-height: 18px;
|
|
1339
|
+
white-space: nowrap;
|
|
1340
|
+
cursor: pointer;
|
|
1341
|
+
}
|
|
1342
|
+
.ttl-row-menu-item:hover {
|
|
1343
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
1344
|
+
color: var(--dsw-alias-label-primary);
|
|
1345
|
+
}
|
|
1346
|
+
.ttl-row-menu-item:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1347
|
+
.ttl-row-menu-item-danger { color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 78%, transparent); }
|
|
1348
|
+
.ttl-row-menu-item-danger:hover { color: var(--dsw-alias-state-error-primary); }
|
|
1349
|
+
.ttl-row-edit-actions { display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; }
|
|
1350
|
+
/* 行内编辑时状态列改为两个紧凑选择器(可见标签隐藏,aria-label 仍在)。 */
|
|
1351
|
+
.ttl-edit-status { display: flex; align-items: center; gap: 4px; }
|
|
1352
|
+
.ttl-edit-status .ttl-select > span { display: none; }
|
|
1353
|
+
.ttl-edit-status .ttl-select select { max-width: 78px; font-size: 12px; }
|
|
1297
1354
|
.ttl-edit-input {
|
|
1298
1355
|
flex: 1;
|
|
1299
1356
|
height: 30px;
|
|
@@ -1310,6 +1367,7 @@
|
|
|
1310
1367
|
color: var(--dsw-alias-label-tertiary);
|
|
1311
1368
|
font-size: 13px;
|
|
1312
1369
|
line-height: 20px;
|
|
1370
|
+
animation: ttl-fade-in var(--ttl-dur-base) var(--ttl-ease-out);
|
|
1313
1371
|
}
|
|
1314
1372
|
|
|
1315
1373
|
.ttl-placeholder-title {
|
|
@@ -1607,6 +1665,8 @@
|
|
|
1607
1665
|
height: 16px;
|
|
1608
1666
|
border-radius: 8px;
|
|
1609
1667
|
overflow: hidden;
|
|
1668
|
+
/* 时间条从左向右「画」出来,呼应创建 → 完成的时间方向。 */
|
|
1669
|
+
animation: ttl-bar-draw 420ms var(--ttl-ease-out) both;
|
|
1610
1670
|
}
|
|
1611
1671
|
.ttl-gantt-bar-completed { background: var(--dsw-alias-state-success-secondary); }
|
|
1612
1672
|
.ttl-gantt-bar-ongoing {
|
|
@@ -1625,12 +1685,14 @@
|
|
|
1625
1685
|
font-size: 10px;
|
|
1626
1686
|
line-height: 1;
|
|
1627
1687
|
white-space: nowrap;
|
|
1628
|
-
}
|
|
1688
|
+
}
|
|
1689
|
+
.ttl-chat {
|
|
1629
1690
|
margin-bottom: 16px;
|
|
1630
1691
|
border: 0.5px solid var(--dsw-alias-border-l2);
|
|
1631
1692
|
border-radius: var(--dsw-radius-md);
|
|
1632
1693
|
background: var(--dsw-alias-bg-layer-1);
|
|
1633
1694
|
overflow: hidden;
|
|
1695
|
+
animation: ttl-fade-in 160ms var(--ttl-ease-out);
|
|
1634
1696
|
}
|
|
1635
1697
|
|
|
1636
1698
|
.ttl-chat-headrow {
|
|
@@ -1792,6 +1854,8 @@
|
|
|
1792
1854
|
.ttl-table-row {
|
|
1793
1855
|
border-bottom: 0.5px solid var(--dsw-alias-border-l1);
|
|
1794
1856
|
transition: background 0.12s ease-out;
|
|
1857
|
+
/* 新行 / 切视图时淡入;animation-delay 由行索引注入,形成轻错位。 */
|
|
1858
|
+
animation: ttl-fade-in var(--ttl-dur-base) var(--ttl-ease-out) both;
|
|
1795
1859
|
}
|
|
1796
1860
|
.ttl-table-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
1797
1861
|
.ttl-table tbody tr:last-child { border-bottom: 0; }
|
|
@@ -1960,11 +2024,13 @@
|
|
|
1960
2024
|
border: 0.5px solid var(--dsw-alias-border-l2);
|
|
1961
2025
|
border-radius: 12px;
|
|
1962
2026
|
background: var(--dsw-alias-bg-base);
|
|
1963
|
-
transition: border-color 0.15s, box-shadow 0.15s;
|
|
2027
|
+
transition: border-color 0.15s, box-shadow 0.15s, transform 0.16s var(--ttl-ease-out);
|
|
2028
|
+
animation: ttl-rise-in 200ms var(--ttl-ease-out) backwards;
|
|
1964
2029
|
}
|
|
1965
2030
|
.ttl-board-card:hover {
|
|
1966
2031
|
border-color: var(--dsw-alias-border-l3);
|
|
1967
2032
|
box-shadow: var(--dsw-shadow-lv1);
|
|
2033
|
+
transform: translateY(-1px);
|
|
1968
2034
|
}
|
|
1969
2035
|
.ttl-board-card-editing { padding: 9px 10px; }
|
|
1970
2036
|
.ttl-board-card-main { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
@@ -2016,6 +2082,13 @@
|
|
|
2016
2082
|
}
|
|
2017
2083
|
.ttl-check:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary); outline-offset: 2px; }
|
|
2018
2084
|
.ttl-check:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
2085
|
+
/* 勾选:对勾从 0.55 弹到 1;只在状态切换时过渡,首屏不会逐个弹。 */
|
|
2086
|
+
.ttl-check svg {
|
|
2087
|
+
transform: scale(0.55);
|
|
2088
|
+
transform-origin: 50% 50%;
|
|
2089
|
+
transition: transform 200ms var(--ttl-ease-spring);
|
|
2090
|
+
}
|
|
2091
|
+
.ttl-check[data-checked='true'] svg { transform: scale(1); }
|
|
2019
2092
|
.ttl-board-check { position: absolute; top: 8px; left: 8px; }
|
|
2020
2093
|
.ttl-board-more {
|
|
2021
2094
|
position: absolute;
|
|
@@ -2180,6 +2253,7 @@
|
|
|
2180
2253
|
padding: 6px 8px;
|
|
2181
2254
|
border-radius: 8px;
|
|
2182
2255
|
background: var(--dsw-alias-bg-module-platform);
|
|
2256
|
+
animation: ttl-fade-in var(--ttl-dur-base) var(--ttl-ease-out) both;
|
|
2183
2257
|
}
|
|
2184
2258
|
.ttl-calendar-entry-main {
|
|
2185
2259
|
display: flex;
|
|
@@ -2300,6 +2374,7 @@
|
|
|
2300
2374
|
justify-content: center;
|
|
2301
2375
|
padding: 24px;
|
|
2302
2376
|
background: var(--dsw-alias-bg-mask-1);
|
|
2377
|
+
animation: ttl-fade-in 140ms ease-out;
|
|
2303
2378
|
}
|
|
2304
2379
|
.ttl-dialog {
|
|
2305
2380
|
display: flex;
|
|
@@ -2311,6 +2386,7 @@
|
|
|
2311
2386
|
background: var(--dsw-alias-bg-base);
|
|
2312
2387
|
box-shadow: var(--dsw-shadow-lv3);
|
|
2313
2388
|
overflow: hidden;
|
|
2389
|
+
animation: ttl-dialog-in 200ms var(--ttl-ease-out) both;
|
|
2314
2390
|
}
|
|
2315
2391
|
.ttl-dialog-head {
|
|
2316
2392
|
display: flex;
|
|
@@ -2438,15 +2514,39 @@
|
|
|
2438
2514
|
@media (max-width: 560px) {
|
|
2439
2515
|
.ttl-overlay { align-items: flex-start; padding: 12px; }
|
|
2440
2516
|
}
|
|
2517
|
+
/* ===== 动效关键帧:短、克制,只服务状态变化 ===== */
|
|
2518
|
+
@keyframes ttl-fade-in {
|
|
2519
|
+
from { opacity: 0; }
|
|
2520
|
+
to { opacity: 1; }
|
|
2521
|
+
}
|
|
2522
|
+
@keyframes ttl-rise-in {
|
|
2523
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
2524
|
+
to { opacity: 1; transform: none; }
|
|
2525
|
+
}
|
|
2526
|
+
@keyframes ttl-fade-down {
|
|
2527
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
2528
|
+
to { opacity: 1; transform: none; }
|
|
2529
|
+
}
|
|
2530
|
+
@keyframes ttl-dialog-in {
|
|
2531
|
+
from { opacity: 0; transform: translateY(10px) scale(0.985); }
|
|
2532
|
+
to { opacity: 1; transform: none; }
|
|
2533
|
+
}
|
|
2534
|
+
@keyframes ttl-count-pop {
|
|
2535
|
+
from { opacity: 0.4; transform: translateY(-2px) scale(0.92); }
|
|
2536
|
+
to { opacity: 1; transform: none; }
|
|
2537
|
+
}
|
|
2538
|
+
@keyframes ttl-bar-draw {
|
|
2539
|
+
from { clip-path: inset(0 100% 0 0); }
|
|
2540
|
+
to { clip-path: inset(0 0 0 0); }
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2441
2543
|
@media (prefers-reduced-motion: reduce) {
|
|
2442
|
-
.ttl-
|
|
2443
|
-
.ttl-
|
|
2444
|
-
.ttl-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
.ttl-skeleton-bar { animation: none; }
|
|
2449
|
-
.ttl-toast { animation: none; }
|
|
2544
|
+
.ttl-root *,
|
|
2545
|
+
.ttl-root *::before,
|
|
2546
|
+
.ttl-root *::after {
|
|
2547
|
+
animation: none !important;
|
|
2548
|
+
transition: none !important;
|
|
2549
|
+
}
|
|
2450
2550
|
}
|
|
2451
2551
|
@media (max-width: 720px) {
|
|
2452
2552
|
.ttl-detail-list { grid-template-columns: auto minmax(0, 1fr); }
|
|
@@ -3928,7 +4028,7 @@
|
|
|
3928
4028
|
)
|
|
3929
4029
|
);
|
|
3930
4030
|
};
|
|
3931
|
-
const renderBoardCard = (todo) => {
|
|
4031
|
+
const renderBoardCard = (todo, index) => {
|
|
3932
4032
|
const isEditing = editingId === todo.id;
|
|
3933
4033
|
const isConfirming = confirmId === todo.id;
|
|
3934
4034
|
const actionsOpen = boardActionsId === todo.id;
|
|
@@ -3948,6 +4048,8 @@
|
|
|
3948
4048
|
key: todo.id,
|
|
3949
4049
|
className: isEditing ? "ttl-board-card ttl-board-card-editing" : "ttl-board-card",
|
|
3950
4050
|
"data-status": todo.status,
|
|
4051
|
+
// 入场错位:与表格同一节奏(前 7 张,12ms 一步)。
|
|
4052
|
+
style: { animationDelay: `${Math.min(index || 0, 6) * 12}ms` },
|
|
3951
4053
|
role: "listitem",
|
|
3952
4054
|
onKeyDown: (event) => {
|
|
3953
4055
|
if (!event || event.key !== "Escape") return;
|
|
@@ -4185,7 +4287,7 @@
|
|
|
4185
4287
|
"+"
|
|
4186
4288
|
) : null
|
|
4187
4289
|
),
|
|
4188
|
-
collapsed ? null : h("div", { className: "ttl-board-list", role: "list" }, lane.items.map(renderBoardCard))
|
|
4290
|
+
collapsed ? null : h("div", { className: "ttl-board-list", role: "list" }, lane.items.map((todo, index) => renderBoardCard(todo, index)))
|
|
4189
4291
|
);
|
|
4190
4292
|
};
|
|
4191
4293
|
const renderBoardColumn = (column) => {
|
|
@@ -4235,7 +4337,7 @@
|
|
|
4235
4337
|
tabIndex: 0,
|
|
4236
4338
|
"aria-label": t("board.columnBodyAria", { name: column.label, count: total })
|
|
4237
4339
|
},
|
|
4238
|
-
lanes ? lanes.length === 0 ? h("p", { className: "ttl-board-column-empty" }, t(emptyColumnKey(column))) : h("div", { className: "ttl-board-lanes" }, lanes.map((lane) => renderBoardLane(column, lane))) : visibleItems.length === 0 ? h("p", { className: "ttl-board-column-empty" }, t(emptyColumnKey(column))) : h("div", { className: "ttl-board-list", role: "list" }, visibleItems.map(renderBoardCard)),
|
|
4340
|
+
lanes ? lanes.length === 0 ? h("p", { className: "ttl-board-column-empty" }, t(emptyColumnKey(column))) : h("div", { className: "ttl-board-lanes" }, lanes.map((lane) => renderBoardLane(column, lane))) : visibleItems.length === 0 ? h("p", { className: "ttl-board-column-empty" }, t(emptyColumnKey(column))) : h("div", { className: "ttl-board-list", role: "list" }, visibleItems.map((todo, index) => renderBoardCard(todo, index))),
|
|
4239
4341
|
!lanes && hiddenCount > 0 ? h(
|
|
4240
4342
|
"button",
|
|
4241
4343
|
{
|
|
@@ -4439,12 +4541,16 @@
|
|
|
4439
4541
|
|
|
4440
4542
|
// src/client/views/table.js
|
|
4441
4543
|
function createTableView(deps) {
|
|
4442
|
-
const { confirmId, detailIdOf, direction, editingId, emptyActionKey, expandedIds, filter, handleEmptyAction, handleHeaderSort, listStatus, priorityBadge,
|
|
4443
|
-
const pushTableRow = (rows, todo) => {
|
|
4544
|
+
const { busyId, cancelEdit, confirmId, detailIdOf, direction, editingId, editingPriority, editingType, emptyActionKey, expandedIds, filter, handleDelete, handleEmptyAction, handleHeaderSort, listStatus, nowMs, priorityBadge, prioritySelectControl, projectInput, renderDetailContent, renderFileChips, renderRowCheck, saveEdit, setConfirmId, setEditingPriority, setEditingType, setTableActionsId, sortBy, startEdit, statusBadge, tableActionsId, titleInput, toggleExpanded, typeBadge, typeSelectControl, visibleTodos } = deps;
|
|
4545
|
+
const pushTableRow = (rows, todo, index) => {
|
|
4444
4546
|
const completed = todo.status === "completed";
|
|
4445
4547
|
const isEditing = editingId === todo.id;
|
|
4446
4548
|
const expanded = expandedIds[todo.id] === true;
|
|
4549
|
+
const actionsOpen = tableActionsId === todo.id;
|
|
4550
|
+
const isConfirming = confirmId === todo.id;
|
|
4447
4551
|
const detailId = detailIdOf(todo);
|
|
4552
|
+
const menuId = `ttl-row-menu-${String(todo.id).replace(/[^A-Za-z0-9_-]/g, "-")}`;
|
|
4553
|
+
const duration = formatDurationShort(todo.createdAt, completed ? todo.completedAt : nowMs);
|
|
4448
4554
|
const titleCell = isEditing ? h("td", { className: "ttl-td ttl-td-title" }, titleInput(todo)) : h(
|
|
4449
4555
|
"td",
|
|
4450
4556
|
{ className: "ttl-td ttl-td-title" },
|
|
@@ -4473,11 +4579,15 @@
|
|
|
4473
4579
|
key: todo.id,
|
|
4474
4580
|
className: "ttl-table-row",
|
|
4475
4581
|
"data-status": todo.status,
|
|
4582
|
+
// 入场错位:只给前 7 行加延迟(12ms 一步),长列表不用等。
|
|
4583
|
+
style: { animationDelay: `${Math.min(index || 0, 6) * 12}ms` },
|
|
4476
4584
|
onKeyDown: (event) => {
|
|
4477
|
-
if (event.key
|
|
4585
|
+
if (!event || event.key !== "Escape") return;
|
|
4586
|
+
if (isConfirming || actionsOpen) {
|
|
4478
4587
|
event.preventDefault();
|
|
4479
4588
|
event.stopPropagation();
|
|
4480
4589
|
setConfirmId(null);
|
|
4590
|
+
setTableActionsId(null);
|
|
4481
4591
|
}
|
|
4482
4592
|
}
|
|
4483
4593
|
},
|
|
@@ -4491,11 +4601,140 @@
|
|
|
4491
4601
|
h(
|
|
4492
4602
|
"td",
|
|
4493
4603
|
{ className: "ttl-td ttl-td-status" },
|
|
4494
|
-
|
|
4604
|
+
isEditing ? h(
|
|
4605
|
+
"span",
|
|
4606
|
+
{ className: "ttl-edit-status" },
|
|
4607
|
+
prioritySelectControl(editingPriority, setEditingPriority, busyId === todo.id),
|
|
4608
|
+
typeSelectControl(editingType, setEditingType, busyId === todo.id)
|
|
4609
|
+
) : h("span", { className: "ttl-badge-row" }, statusBadge(todo), typeBadge(todo), priorityBadge(todo))
|
|
4495
4610
|
),
|
|
4496
4611
|
h("td", { className: "ttl-td ttl-td-time" }, formatTime(todo.createdAt)),
|
|
4497
|
-
|
|
4498
|
-
h(
|
|
4612
|
+
// 完成时间列双行:第一行时刻,第二行「用时 / 进行中」——时间账本的核心指标。
|
|
4613
|
+
h(
|
|
4614
|
+
"td",
|
|
4615
|
+
{ className: "ttl-td ttl-td-time" },
|
|
4616
|
+
h("span", { className: "ttl-time-primary" }, completed ? formatTime(todo.completedAt) : EMPTY_TIME),
|
|
4617
|
+
duration === EMPTY_TIME ? null : h(
|
|
4618
|
+
"span",
|
|
4619
|
+
{ className: "ttl-time-duration", "data-status": todo.status },
|
|
4620
|
+
t(completed ? "table.spent" : "table.elapsed", { duration })
|
|
4621
|
+
)
|
|
4622
|
+
),
|
|
4623
|
+
h(
|
|
4624
|
+
"td",
|
|
4625
|
+
{ className: "ttl-td ttl-td-actions" },
|
|
4626
|
+
isEditing ? h(
|
|
4627
|
+
"span",
|
|
4628
|
+
{ className: "ttl-row-edit-actions" },
|
|
4629
|
+
h(
|
|
4630
|
+
"button",
|
|
4631
|
+
{
|
|
4632
|
+
type: "button",
|
|
4633
|
+
className: "ttl-link",
|
|
4634
|
+
disabled: busyId === todo.id,
|
|
4635
|
+
onClick: () => {
|
|
4636
|
+
void saveEdit(todo);
|
|
4637
|
+
}
|
|
4638
|
+
},
|
|
4639
|
+
t("row.save")
|
|
4640
|
+
),
|
|
4641
|
+
h(
|
|
4642
|
+
"button",
|
|
4643
|
+
{ type: "button", className: "ttl-link", disabled: busyId === todo.id, onClick: cancelEdit },
|
|
4644
|
+
t("row.cancel")
|
|
4645
|
+
)
|
|
4646
|
+
) : h(
|
|
4647
|
+
"div",
|
|
4648
|
+
{ className: "ttl-table-more" },
|
|
4649
|
+
h(
|
|
4650
|
+
"button",
|
|
4651
|
+
{
|
|
4652
|
+
type: "button",
|
|
4653
|
+
className: "ttl-row-more",
|
|
4654
|
+
"aria-expanded": actionsOpen ? "true" : "false",
|
|
4655
|
+
"aria-controls": actionsOpen ? menuId : void 0,
|
|
4656
|
+
"aria-label": t("table.moreAria", { title: todo.title === "" ? t("list.empty") : todo.title }),
|
|
4657
|
+
title: t("table.more"),
|
|
4658
|
+
disabled: busyId === todo.id,
|
|
4659
|
+
onClick: () => {
|
|
4660
|
+
setConfirmId(null);
|
|
4661
|
+
setTableActionsId(actionsOpen ? null : todo.id);
|
|
4662
|
+
}
|
|
4663
|
+
},
|
|
4664
|
+
"⋯"
|
|
4665
|
+
),
|
|
4666
|
+
actionsOpen ? h(
|
|
4667
|
+
"div",
|
|
4668
|
+
{ id: menuId, className: "ttl-row-menu", role: "group", "aria-label": t("table.more") },
|
|
4669
|
+
isConfirming ? [
|
|
4670
|
+
h(
|
|
4671
|
+
"button",
|
|
4672
|
+
{
|
|
4673
|
+
key: "confirm",
|
|
4674
|
+
type: "button",
|
|
4675
|
+
className: "ttl-row-menu-item ttl-row-menu-item-danger",
|
|
4676
|
+
disabled: busyId === todo.id,
|
|
4677
|
+
onClick: () => {
|
|
4678
|
+
void handleDelete(todo);
|
|
4679
|
+
}
|
|
4680
|
+
},
|
|
4681
|
+
t("row.confirmDelete")
|
|
4682
|
+
),
|
|
4683
|
+
h(
|
|
4684
|
+
"button",
|
|
4685
|
+
{
|
|
4686
|
+
key: "cancel",
|
|
4687
|
+
type: "button",
|
|
4688
|
+
className: "ttl-row-menu-item",
|
|
4689
|
+
onClick: () => setConfirmId(null)
|
|
4690
|
+
},
|
|
4691
|
+
t("row.cancel")
|
|
4692
|
+
)
|
|
4693
|
+
] : [
|
|
4694
|
+
h(
|
|
4695
|
+
"button",
|
|
4696
|
+
{
|
|
4697
|
+
key: "detail",
|
|
4698
|
+
type: "button",
|
|
4699
|
+
className: "ttl-row-menu-item",
|
|
4700
|
+
"aria-expanded": expanded ? "true" : "false",
|
|
4701
|
+
"aria-controls": detailId,
|
|
4702
|
+
onClick: () => {
|
|
4703
|
+
setTableActionsId(null);
|
|
4704
|
+
toggleExpanded(todo);
|
|
4705
|
+
}
|
|
4706
|
+
},
|
|
4707
|
+
t("table.detail")
|
|
4708
|
+
),
|
|
4709
|
+
h(
|
|
4710
|
+
"button",
|
|
4711
|
+
{
|
|
4712
|
+
key: "edit",
|
|
4713
|
+
type: "button",
|
|
4714
|
+
className: "ttl-row-menu-item",
|
|
4715
|
+
disabled: busyId === todo.id,
|
|
4716
|
+
onClick: () => {
|
|
4717
|
+
setTableActionsId(null);
|
|
4718
|
+
startEdit(todo);
|
|
4719
|
+
}
|
|
4720
|
+
},
|
|
4721
|
+
t("row.edit")
|
|
4722
|
+
),
|
|
4723
|
+
h(
|
|
4724
|
+
"button",
|
|
4725
|
+
{
|
|
4726
|
+
key: "delete",
|
|
4727
|
+
type: "button",
|
|
4728
|
+
className: "ttl-row-menu-item ttl-row-menu-item-danger",
|
|
4729
|
+
disabled: busyId === todo.id,
|
|
4730
|
+
onClick: () => setConfirmId(todo.id)
|
|
4731
|
+
},
|
|
4732
|
+
t("row.delete")
|
|
4733
|
+
)
|
|
4734
|
+
]
|
|
4735
|
+
) : null
|
|
4736
|
+
)
|
|
4737
|
+
)
|
|
4499
4738
|
)
|
|
4500
4739
|
);
|
|
4501
4740
|
if (expanded) {
|
|
@@ -4543,10 +4782,10 @@
|
|
|
4543
4782
|
)
|
|
4544
4783
|
)
|
|
4545
4784
|
);
|
|
4546
|
-
for (const todo of group.items) pushTableRow(rows, todo);
|
|
4785
|
+
for (const [index, todo] of group.items.entries()) pushTableRow(rows, todo, index);
|
|
4547
4786
|
}
|
|
4548
4787
|
} else {
|
|
4549
|
-
for (const todo of visibleTodos) pushTableRow(rows, todo);
|
|
4788
|
+
for (const [index, todo] of visibleTodos.entries()) pushTableRow(rows, todo, index);
|
|
4550
4789
|
}
|
|
4551
4790
|
return h(
|
|
4552
4791
|
"div",
|
|
@@ -4640,6 +4879,7 @@
|
|
|
4640
4879
|
open,
|
|
4641
4880
|
busy,
|
|
4642
4881
|
applying,
|
|
4882
|
+
dirty,
|
|
4643
4883
|
fileName,
|
|
4644
4884
|
mode,
|
|
4645
4885
|
preview,
|
|
@@ -4660,7 +4900,9 @@
|
|
|
4660
4900
|
const rowCount = record !== null && typeof record.rowCount === "number" ? record.rowCount : 0;
|
|
4661
4901
|
const activeMode = TRANSFER_MODES.find((option) => option.value === mode) ?? TRANSFER_MODES[0];
|
|
4662
4902
|
const canConfirm = summary !== null && busy !== true && applying !== true;
|
|
4903
|
+
const canDismissBackdrop = dirty !== true && busy !== true && applying !== true;
|
|
4663
4904
|
const backdrop = (event) => {
|
|
4905
|
+
if (!canDismissBackdrop) return;
|
|
4664
4906
|
if (event && event.target === event.currentTarget) onClose();
|
|
4665
4907
|
};
|
|
4666
4908
|
return h(
|
|
@@ -4676,7 +4918,7 @@
|
|
|
4676
4918
|
onKeyDown: (event) => {
|
|
4677
4919
|
if (event && event.key === "Escape") {
|
|
4678
4920
|
event.stopPropagation();
|
|
4679
|
-
onClose();
|
|
4921
|
+
if (canDismissBackdrop) onClose();
|
|
4680
4922
|
}
|
|
4681
4923
|
}
|
|
4682
4924
|
},
|
|
@@ -4887,6 +5129,7 @@
|
|
|
4887
5129
|
const [messages, setMessages] = React.useState(() => [{ id: 0, role: "assistant", welcome: true }]);
|
|
4888
5130
|
const [boardGroupBy, setBoardGroupBy] = React.useState("status");
|
|
4889
5131
|
const [boardActionsId, setBoardActionsId] = React.useState(null);
|
|
5132
|
+
const [tableActionsId, setTableActionsId] = React.useState(null);
|
|
4890
5133
|
const [boardShowAllCompleted, setBoardShowAllCompleted] = React.useState(false);
|
|
4891
5134
|
const [search, setSearch] = React.useState("");
|
|
4892
5135
|
const [boardLaneCollapsed, setBoardLaneCollapsed] = React.useState({});
|
|
@@ -5244,6 +5487,30 @@
|
|
|
5244
5487
|
cancelEdit();
|
|
5245
5488
|
}
|
|
5246
5489
|
};
|
|
5490
|
+
const restoreDeleted = async (todo) => {
|
|
5491
|
+
if (todo === null || todo === void 0) return;
|
|
5492
|
+
try {
|
|
5493
|
+
const content = JSON.stringify([{
|
|
5494
|
+
id: todo.id,
|
|
5495
|
+
title: todo.title,
|
|
5496
|
+
project: todo.project,
|
|
5497
|
+
files: normalizeFiles(todo.files),
|
|
5498
|
+
status: todo.status,
|
|
5499
|
+
priority: todo.priority,
|
|
5500
|
+
type: todo.type,
|
|
5501
|
+
createdAt: todo.createdAt,
|
|
5502
|
+
completedAt: todo.completedAt,
|
|
5503
|
+
updatedAt: todo.updatedAt
|
|
5504
|
+
}]);
|
|
5505
|
+
await request("/import", { content, mode: "merge" });
|
|
5506
|
+
if (!mountedRef.current) return;
|
|
5507
|
+
showNotice("success", t("notice.restored"));
|
|
5508
|
+
void fetchProjectSuggestions(workspaceRef.current, true);
|
|
5509
|
+
await reload(true);
|
|
5510
|
+
} catch (error) {
|
|
5511
|
+
showNotice("error", messageOf(error));
|
|
5512
|
+
}
|
|
5513
|
+
};
|
|
5247
5514
|
const handleDelete = async (todo) => {
|
|
5248
5515
|
if (todo === null || todo === void 0 || busyId !== null) return;
|
|
5249
5516
|
setBusyId(todo.id);
|
|
@@ -5251,7 +5518,12 @@
|
|
|
5251
5518
|
await request("/delete", { id: todo.id });
|
|
5252
5519
|
if (!mountedRef.current) return;
|
|
5253
5520
|
setConfirmId(null);
|
|
5254
|
-
|
|
5521
|
+
setTableActionsId(null);
|
|
5522
|
+
showNotice("success", t("notice.deleted"), {
|
|
5523
|
+
onClick: () => {
|
|
5524
|
+
void restoreDeleted(todo);
|
|
5525
|
+
}
|
|
5526
|
+
});
|
|
5255
5527
|
void fetchProjectSuggestions(workspaceRef.current, true);
|
|
5256
5528
|
await reload(true);
|
|
5257
5529
|
} catch (error) {
|
|
@@ -6197,6 +6469,7 @@
|
|
|
6197
6469
|
calendarMonth,
|
|
6198
6470
|
calendarSelectedDay,
|
|
6199
6471
|
calendarYear,
|
|
6472
|
+
cancelEdit,
|
|
6200
6473
|
confirmId,
|
|
6201
6474
|
density,
|
|
6202
6475
|
detailIdOf,
|
|
@@ -6230,6 +6503,7 @@
|
|
|
6230
6503
|
renderDetailContent,
|
|
6231
6504
|
renderFileChips,
|
|
6232
6505
|
renderRowCheck,
|
|
6506
|
+
saveEdit,
|
|
6233
6507
|
setBoardActionsId,
|
|
6234
6508
|
setBoardLaneCollapsed,
|
|
6235
6509
|
setBoardShowAllCompleted,
|
|
@@ -6237,9 +6511,11 @@
|
|
|
6237
6511
|
setConfirmId,
|
|
6238
6512
|
setEditingPriority,
|
|
6239
6513
|
setEditingType,
|
|
6514
|
+
setTableActionsId,
|
|
6240
6515
|
sortBy,
|
|
6241
6516
|
startEdit,
|
|
6242
6517
|
statusBadge,
|
|
6518
|
+
tableActionsId,
|
|
6243
6519
|
titleInput,
|
|
6244
6520
|
toggleExpanded,
|
|
6245
6521
|
toggleGanttExpanded,
|
|
@@ -6299,38 +6575,10 @@
|
|
|
6299
6575
|
onClick: () => setFilter(entry.value)
|
|
6300
6576
|
},
|
|
6301
6577
|
t(entry.labelKey),
|
|
6302
|
-
h("span", { className: "ttl-seg-count" }, String(filterCounts[entry.value] || 0))
|
|
6578
|
+
h("span", { key: filterCounts[entry.value] || 0, className: "ttl-seg-count" }, String(filterCounts[entry.value] || 0))
|
|
6303
6579
|
)
|
|
6304
6580
|
)
|
|
6305
6581
|
);
|
|
6306
|
-
const sortSelect = h(
|
|
6307
|
-
"label",
|
|
6308
|
-
{ className: "ttl-select" },
|
|
6309
|
-
h("span", null, t("sort.label")),
|
|
6310
|
-
h(
|
|
6311
|
-
"select",
|
|
6312
|
-
{
|
|
6313
|
-
value: sortBy,
|
|
6314
|
-
"aria-label": t("sort.label"),
|
|
6315
|
-
onChange: (event) => setSortBy(event.target.value)
|
|
6316
|
-
},
|
|
6317
|
-
SORTS.map(
|
|
6318
|
-
(entry) => h("option", { key: entry.value, value: entry.value }, t(entry.labelKey))
|
|
6319
|
-
)
|
|
6320
|
-
)
|
|
6321
|
-
);
|
|
6322
|
-
const directionLabel = t(direction === "asc" ? "direction.asc" : "direction.desc");
|
|
6323
|
-
const directionToggle = h(
|
|
6324
|
-
"button",
|
|
6325
|
-
{
|
|
6326
|
-
type: "button",
|
|
6327
|
-
className: "ttl-sort-dir",
|
|
6328
|
-
"aria-label": t("direction.toggleAria", { direction: directionLabel }),
|
|
6329
|
-
title: t("direction.toggleAria", { direction: directionLabel }),
|
|
6330
|
-
onClick: () => setDirection(direction === "asc" ? "desc" : "asc")
|
|
6331
|
-
},
|
|
6332
|
-
h("span", { "aria-hidden": "true" }, direction === "asc" ? "↑" : "↓")
|
|
6333
|
-
);
|
|
6334
6582
|
const viewToggle = h(
|
|
6335
6583
|
"div",
|
|
6336
6584
|
{ className: "ttl-seg", role: "group", "aria-label": t("view.aria") },
|
|
@@ -6575,41 +6823,38 @@
|
|
|
6575
6823
|
h("path", { d: "M8 2v7m0 0l3-3m-3 3L5 6M3 12v1.5a.5.5 0 0 0 .5.5h9a.5.5 0 0 0 .5-.5V12" })
|
|
6576
6824
|
),
|
|
6577
6825
|
t("transfer.open")
|
|
6578
|
-
),
|
|
6579
|
-
h(
|
|
6580
|
-
"button",
|
|
6581
|
-
{
|
|
6582
|
-
type: "button",
|
|
6583
|
-
className: "ttl-ghost-button",
|
|
6584
|
-
"aria-expanded": chatOpen ? "true" : "false",
|
|
6585
|
-
"aria-controls": chatOpen ? "ttl-chat-panel" : void 0,
|
|
6586
|
-
"aria-label": chatOpen ? t("chat.collapse") : t("chat.expand"),
|
|
6587
|
-
title: chatOpen ? t("chat.collapse") : t("chat.expand"),
|
|
6588
|
-
onClick: () => setChatOpen((value) => !value)
|
|
6589
|
-
},
|
|
6590
|
-
h(
|
|
6591
|
-
"svg",
|
|
6592
|
-
{
|
|
6593
|
-
className: "ttl-ghost-button-icon",
|
|
6594
|
-
width: 12,
|
|
6595
|
-
height: 12,
|
|
6596
|
-
viewBox: "0 0 16 16",
|
|
6597
|
-
fill: "none",
|
|
6598
|
-
stroke: "currentColor",
|
|
6599
|
-
strokeWidth: 1.4,
|
|
6600
|
-
strokeLinecap: "round",
|
|
6601
|
-
strokeLinejoin: "round",
|
|
6602
|
-
"aria-hidden": "true",
|
|
6603
|
-
focusable: "false"
|
|
6604
|
-
},
|
|
6605
|
-
h("path", { d: chatOpen ? "M4 10l4-4 4 4" : "M4 6l4 4-4 4" })
|
|
6606
|
-
),
|
|
6607
|
-
t("chat.title")
|
|
6608
6826
|
)
|
|
6609
6827
|
)
|
|
6610
6828
|
),
|
|
6611
|
-
//
|
|
6612
|
-
h(
|
|
6829
|
+
// 捕获区:单条与批量共用同一位置;批量展开时不在列表上方再叠一层。
|
|
6830
|
+
chatOpen ? h(ChatPanel, {
|
|
6831
|
+
id: "ttl-chat-panel",
|
|
6832
|
+
open: true,
|
|
6833
|
+
busy: chatBusy,
|
|
6834
|
+
messages,
|
|
6835
|
+
input: chatInput,
|
|
6836
|
+
suggestions: fileSuggestions,
|
|
6837
|
+
suggestionsKind: suggestKind,
|
|
6838
|
+
workspaces,
|
|
6839
|
+
workspace,
|
|
6840
|
+
onWorkspaceChange: handleWorkspaceChange,
|
|
6841
|
+
suggestionsOpen: suggestOpen && suggestTarget === "chat",
|
|
6842
|
+
suggestionsLoading: suggestLoading,
|
|
6843
|
+
suggestionsError: suggestError,
|
|
6844
|
+
suggestionsActive: suggestActive,
|
|
6845
|
+
onToggle: () => setChatOpen(false),
|
|
6846
|
+
onChange: (value, caret) => handleChatInputChange(value, caret),
|
|
6847
|
+
onSend: () => {
|
|
6848
|
+
void handleChatSend();
|
|
6849
|
+
},
|
|
6850
|
+
onUndo: (message) => {
|
|
6851
|
+
void handleChatUndo(message);
|
|
6852
|
+
},
|
|
6853
|
+
onSuggestMove: moveFileSuggest,
|
|
6854
|
+
onSuggestClose: closeFileSuggest,
|
|
6855
|
+
onSuggestSelect: selectSuggestion,
|
|
6856
|
+
onCopyFile: copyFilePath
|
|
6857
|
+
}) : h(
|
|
6613
6858
|
"form",
|
|
6614
6859
|
{ className: "ttl-composer", onSubmit: handleCreate, "aria-label": t("create.aria") },
|
|
6615
6860
|
h("input", {
|
|
@@ -6656,6 +6901,18 @@
|
|
|
6656
6901
|
}),
|
|
6657
6902
|
prioritySelectControl(newPriority, setNewPriority, creating),
|
|
6658
6903
|
typeSelectControl(newType, setNewType, creating),
|
|
6904
|
+
h(
|
|
6905
|
+
"button",
|
|
6906
|
+
{
|
|
6907
|
+
type: "button",
|
|
6908
|
+
className: "ttl-ghost-button ttl-composer-batch",
|
|
6909
|
+
title: t("create.batchTitle"),
|
|
6910
|
+
"aria-expanded": "false",
|
|
6911
|
+
"aria-controls": "ttl-chat-panel",
|
|
6912
|
+
onClick: () => setChatOpen(true)
|
|
6913
|
+
},
|
|
6914
|
+
t("create.batch")
|
|
6915
|
+
),
|
|
6659
6916
|
h(
|
|
6660
6917
|
"button",
|
|
6661
6918
|
{
|
|
@@ -6666,35 +6923,6 @@
|
|
|
6666
6923
|
creating ? t("create.actionBusy") : t("create.action")
|
|
6667
6924
|
)
|
|
6668
6925
|
),
|
|
6669
|
-
// 对话录入:按需展开的次要录入方式,默认收起,不与新建抢注意力。
|
|
6670
|
-
chatOpen ? h(ChatPanel, {
|
|
6671
|
-
id: "ttl-chat-panel",
|
|
6672
|
-
open: true,
|
|
6673
|
-
busy: chatBusy,
|
|
6674
|
-
messages,
|
|
6675
|
-
input: chatInput,
|
|
6676
|
-
suggestions: fileSuggestions,
|
|
6677
|
-
suggestionsKind: suggestKind,
|
|
6678
|
-
workspaces,
|
|
6679
|
-
workspace,
|
|
6680
|
-
onWorkspaceChange: handleWorkspaceChange,
|
|
6681
|
-
suggestionsOpen: suggestOpen && suggestTarget === "chat",
|
|
6682
|
-
suggestionsLoading: suggestLoading,
|
|
6683
|
-
suggestionsError: suggestError,
|
|
6684
|
-
suggestionsActive: suggestActive,
|
|
6685
|
-
onToggle: () => setChatOpen((value) => !value),
|
|
6686
|
-
onChange: (value, caret) => handleChatInputChange(value, caret),
|
|
6687
|
-
onSend: () => {
|
|
6688
|
-
void handleChatSend();
|
|
6689
|
-
},
|
|
6690
|
-
onUndo: (message) => {
|
|
6691
|
-
void handleChatUndo(message);
|
|
6692
|
-
},
|
|
6693
|
-
onSuggestMove: moveFileSuggest,
|
|
6694
|
-
onSuggestClose: closeFileSuggest,
|
|
6695
|
-
onSuggestSelect: selectSuggestion,
|
|
6696
|
-
onCopyFile: copyFilePath
|
|
6697
|
-
}) : null,
|
|
6698
6926
|
// 数据区:视图切换与列表同属一张卡片。
|
|
6699
6927
|
h(
|
|
6700
6928
|
"div",
|
|
@@ -6702,26 +6930,32 @@
|
|
|
6702
6930
|
h(
|
|
6703
6931
|
"div",
|
|
6704
6932
|
{ className: "ttl-toolbar" },
|
|
6933
|
+
// 第一行固定:视图切换 + 搜索。切视图时控件位置不变。
|
|
6705
6934
|
h(
|
|
6706
6935
|
"div",
|
|
6707
|
-
{ className: "ttl-
|
|
6708
|
-
viewToggle,
|
|
6709
|
-
|
|
6936
|
+
{ className: "ttl-toolbar-row" },
|
|
6937
|
+
h("div", { className: "ttl-group" }, viewToggle),
|
|
6938
|
+
h(
|
|
6939
|
+
"div",
|
|
6940
|
+
{ className: "ttl-group" },
|
|
6941
|
+
searchInput,
|
|
6942
|
+
h(
|
|
6943
|
+
"span",
|
|
6944
|
+
{ className: "ttl-refreshing", "aria-live": "polite" },
|
|
6945
|
+
refreshing && listStatus !== "loading" ? t("list.refreshing") : ""
|
|
6946
|
+
)
|
|
6947
|
+
)
|
|
6710
6948
|
),
|
|
6949
|
+
// 第二行:左侧是当前视图专属控件,右侧始终是分类 / 类型筛选。
|
|
6711
6950
|
h(
|
|
6712
6951
|
"div",
|
|
6713
|
-
{ className: "ttl-
|
|
6714
|
-
searchInput,
|
|
6715
|
-
projectFilterSelect,
|
|
6716
|
-
typeFilterSelect,
|
|
6717
|
-
view === "table" ? sortSelect : null,
|
|
6718
|
-
view === "table" ? directionToggle : null,
|
|
6719
|
-
view === "calendar" ? calendarNav : null,
|
|
6952
|
+
{ className: "ttl-toolbar-row" },
|
|
6720
6953
|
h(
|
|
6721
|
-
"
|
|
6722
|
-
{ className: "ttl-
|
|
6723
|
-
|
|
6724
|
-
)
|
|
6954
|
+
"div",
|
|
6955
|
+
{ className: "ttl-group" },
|
|
6956
|
+
view === "table" ? filterTabs : view === "board" ? h(React.Fragment, null, boardGroupSelect, densitySelect, filterTabs) : view === "gantt" ? rangeSelect : h(React.Fragment, null, calendarBasisSelect, calendarNav)
|
|
6957
|
+
),
|
|
6958
|
+
h("div", { className: "ttl-group" }, projectFilterSelect, typeFilterSelect)
|
|
6725
6959
|
)
|
|
6726
6960
|
),
|
|
6727
6961
|
h("div", { className: "ttl-list" }, listBody)
|
|
@@ -6763,6 +6997,8 @@
|
|
|
6763
6997
|
open: transferOpen,
|
|
6764
6998
|
busy: transferBusy,
|
|
6765
6999
|
applying: transferApplying,
|
|
7000
|
+
// 有文件 / 预览 / 改动过策略时,遮罩与 Esc 不再直接关闭(防误丢导入内容)。
|
|
7001
|
+
dirty: transferFileName !== "" || transferContent !== "" || transferPreview !== null,
|
|
6766
7002
|
fileName: transferFileName,
|
|
6767
7003
|
mode: transferMode,
|
|
6768
7004
|
preview: transferPreview,
|