dsh-todo-time-log 0.6.0 → 0.6.2
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 +192 -535
- package/lib/client.js +924 -104
- package/lib/index.js +133 -9
- package/lib/store.js +447 -0
- package/lib/transfer.js +573 -0
- package/package.json +18 -6
package/lib/client.js
CHANGED
|
@@ -260,6 +260,42 @@
|
|
|
260
260
|
"notice.serverError": "服务器返回了错误。",
|
|
261
261
|
"toast.undo": "撤销",
|
|
262
262
|
"toast.dismiss": "关闭提示",
|
|
263
|
+
"transfer.open": "数据",
|
|
264
|
+
"transfer.title": "导入 / 导出",
|
|
265
|
+
"transfer.close": "关闭",
|
|
266
|
+
"transfer.exportTitle": "导出",
|
|
267
|
+
"transfer.exportHint": "导出全部待办。JSON 无损、可回导;CSV 适合用 Excel 统计耗时。",
|
|
268
|
+
"transfer.exportJson": "导出 JSON",
|
|
269
|
+
"transfer.exportCsv": "导出 CSV",
|
|
270
|
+
"transfer.exported": "已导出 {count} 条待办({format})",
|
|
271
|
+
"transfer.importTitle": "导入",
|
|
272
|
+
"transfer.importHint": "支持本插件 JSON(v1–v5)、待办数组或 CSV;导入前会自动备份当前数据。",
|
|
273
|
+
"transfer.chooseFile": "选择文件",
|
|
274
|
+
"transfer.noFile": "未选择文件",
|
|
275
|
+
"transfer.reading": "读取中…",
|
|
276
|
+
"transfer.readError": "读取文件失败:{message}",
|
|
277
|
+
"transfer.emptyFile": "文件内容为空。",
|
|
278
|
+
"transfer.noPreview": "选择文件后先显示预览,确认后才写入。",
|
|
279
|
+
"transfer.previewTitle": "导入预览",
|
|
280
|
+
"transfer.previewAdded": "新增 {count}",
|
|
281
|
+
"transfer.previewUpdated": "更新 {count}",
|
|
282
|
+
"transfer.previewSkipped": "跳过 {count}",
|
|
283
|
+
"transfer.previewRemoved": "移除 {count}",
|
|
284
|
+
"transfer.previewTotal": "合计 {count}",
|
|
285
|
+
"transfer.previewRows": "共读取 {count} 行",
|
|
286
|
+
"transfer.previewInvalid": "非法 {count} 行",
|
|
287
|
+
"transfer.rowError": "第 {row} 行:{message}",
|
|
288
|
+
"transfer.modeLabel": "冲突处理",
|
|
289
|
+
"transfer.modeMerge": "合并(推荐)",
|
|
290
|
+
"transfer.modeMergeHint": "同 ID 以更新时间较新的为准;新 ID 追加为待办。",
|
|
291
|
+
"transfer.modeAdd": "仅新增",
|
|
292
|
+
"transfer.modeAddHint": "已存在的 ID 直接跳过,不修改现有待办。",
|
|
293
|
+
"transfer.modeReplace": "覆盖全部",
|
|
294
|
+
"transfer.modeReplaceHint": "先自动备份,再用文件内容替换全部待办。",
|
|
295
|
+
"transfer.confirm": "开始导入",
|
|
296
|
+
"transfer.importing": "导入中…",
|
|
297
|
+
"transfer.imported": "导入完成",
|
|
298
|
+
"transfer.undone": "已撤销导入",
|
|
263
299
|
"error.panel": "待办时间日志面板出现异常",
|
|
264
300
|
"error.panelHint": "请关闭后重新打开面板;如果问题持续存在,请检查插件日志。",
|
|
265
301
|
"error.detail": "详情:{message}"
|
|
@@ -515,6 +551,42 @@
|
|
|
515
551
|
"notice.serverError": "The server returned an error.",
|
|
516
552
|
"toast.undo": "Undo",
|
|
517
553
|
"toast.dismiss": "Dismiss message",
|
|
554
|
+
"transfer.open": "Data",
|
|
555
|
+
"transfer.title": "Import / Export",
|
|
556
|
+
"transfer.close": "Close",
|
|
557
|
+
"transfer.exportTitle": "Export",
|
|
558
|
+
"transfer.exportHint": "Exports every todo. JSON is lossless and re-importable; CSV suits Excel time analysis.",
|
|
559
|
+
"transfer.exportJson": "Export JSON",
|
|
560
|
+
"transfer.exportCsv": "Export CSV",
|
|
561
|
+
"transfer.exported": "Exported {count} todo(s) ({format})",
|
|
562
|
+
"transfer.importTitle": "Import",
|
|
563
|
+
"transfer.importHint": "Accepts this plugin JSON (v1–v5), a todo array or CSV; the current data is backed up first.",
|
|
564
|
+
"transfer.chooseFile": "Choose file",
|
|
565
|
+
"transfer.noFile": "No file selected",
|
|
566
|
+
"transfer.reading": "Reading…",
|
|
567
|
+
"transfer.readError": "Could not read the file: {message}",
|
|
568
|
+
"transfer.emptyFile": "The file is empty.",
|
|
569
|
+
"transfer.noPreview": "Pick a file to preview; nothing is written until you confirm.",
|
|
570
|
+
"transfer.previewTitle": "Import preview",
|
|
571
|
+
"transfer.previewAdded": "Add {count}",
|
|
572
|
+
"transfer.previewUpdated": "Update {count}",
|
|
573
|
+
"transfer.previewSkipped": "Skip {count}",
|
|
574
|
+
"transfer.previewRemoved": "Remove {count}",
|
|
575
|
+
"transfer.previewTotal": "Total {count}",
|
|
576
|
+
"transfer.previewRows": "{count} row(s) read",
|
|
577
|
+
"transfer.previewInvalid": "{count} invalid row(s)",
|
|
578
|
+
"transfer.rowError": "Row {row}: {message}",
|
|
579
|
+
"transfer.modeLabel": "Conflict handling",
|
|
580
|
+
"transfer.modeMerge": "Merge (recommended)",
|
|
581
|
+
"transfer.modeMergeHint": "Same id keeps the newer update; new ids are appended.",
|
|
582
|
+
"transfer.modeAdd": "Add only",
|
|
583
|
+
"transfer.modeAddHint": "Existing ids are skipped; current todos stay untouched.",
|
|
584
|
+
"transfer.modeReplace": "Replace all",
|
|
585
|
+
"transfer.modeReplaceHint": "Backs up first, then replaces every todo with the file contents.",
|
|
586
|
+
"transfer.confirm": "Start import",
|
|
587
|
+
"transfer.importing": "Importing…",
|
|
588
|
+
"transfer.imported": "Import finished",
|
|
589
|
+
"transfer.undone": "Import undone",
|
|
518
590
|
"error.panel": "The todo time log panel crashed",
|
|
519
591
|
"error.panelHint": "Close and reopen the panel; check the plugin logs if it keeps happening.",
|
|
520
592
|
"error.detail": "Details: {message}"
|
|
@@ -577,6 +649,11 @@
|
|
|
577
649
|
{ value: "comfortable", labelKey: "board.densityComfortable" },
|
|
578
650
|
{ value: "compact", labelKey: "board.densityCompact" }
|
|
579
651
|
];
|
|
652
|
+
var TRANSFER_MODES = [
|
|
653
|
+
{ value: "merge", labelKey: "transfer.modeMerge", hintKey: "transfer.modeMergeHint" },
|
|
654
|
+
{ value: "add", labelKey: "transfer.modeAdd", hintKey: "transfer.modeAddHint" },
|
|
655
|
+
{ value: "replace", labelKey: "transfer.modeReplace", hintKey: "transfer.modeReplaceHint" }
|
|
656
|
+
];
|
|
580
657
|
var BOARD_COMPLETED_LIMIT = 10;
|
|
581
658
|
var CALENDAR_BASES = [
|
|
582
659
|
{ value: "createdAt", labelKey: "calendar.basisCreated" },
|
|
@@ -592,6 +669,7 @@
|
|
|
592
669
|
"calendar.weekdaySun"
|
|
593
670
|
];
|
|
594
671
|
var TABLE_COLUMNS = [
|
|
672
|
+
{ key: "check", labelKey: "row.complete", sortBy: null, srOnly: true },
|
|
595
673
|
{ key: "title", labelKey: "table.title", sortBy: "title" },
|
|
596
674
|
{ key: "project", labelKey: "table.project", sortBy: "project" },
|
|
597
675
|
{ key: "status", labelKey: "table.status", sortBy: null },
|
|
@@ -625,8 +703,13 @@
|
|
|
625
703
|
overflow: hidden;
|
|
626
704
|
color: var(--dsw-alias-label-primary);
|
|
627
705
|
background: var(--dsw-alias-bg-base);
|
|
706
|
+
/* 与宿主 UI 同一套字体栈(真机继承宿主 body 字体):显式声明后,静态预览与真机一致,
|
|
707
|
+
也不会被祖先样式改掉。 */
|
|
708
|
+
font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
|
|
709
|
+
"Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif);
|
|
628
710
|
font-size: 14px;
|
|
629
711
|
line-height: 1.6;
|
|
712
|
+
-webkit-font-smoothing: antialiased;
|
|
630
713
|
}
|
|
631
714
|
.ttl-scroll {
|
|
632
715
|
flex: 1;
|
|
@@ -814,8 +897,9 @@
|
|
|
814
897
|
/* 数据区:视图/筛选控制与列表同属一张卡片,成为唯一的数据容器。 */
|
|
815
898
|
.ttl-data {
|
|
816
899
|
border: 0.5px solid var(--dsw-alias-border-l2);
|
|
817
|
-
border-radius: var(--dsw-radius-
|
|
900
|
+
border-radius: var(--dsw-radius-lg);
|
|
818
901
|
background: var(--dsw-alias-bg-base);
|
|
902
|
+
box-shadow: var(--dsw-shadow-lv1);
|
|
819
903
|
overflow: hidden;
|
|
820
904
|
}
|
|
821
905
|
.ttl-data > .ttl-toolbar {
|
|
@@ -1072,8 +1156,8 @@
|
|
|
1072
1156
|
display: inline-flex;
|
|
1073
1157
|
align-items: center;
|
|
1074
1158
|
flex: none;
|
|
1075
|
-
height:
|
|
1076
|
-
padding: 0
|
|
1159
|
+
height: 18px;
|
|
1160
|
+
padding: 0 7px;
|
|
1077
1161
|
border: 0;
|
|
1078
1162
|
border-radius: 9999px;
|
|
1079
1163
|
background: color-mix(in srgb, var(--dsw-alias-label-secondary) 9%, transparent);
|
|
@@ -1087,19 +1171,36 @@
|
|
|
1087
1171
|
background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent);
|
|
1088
1172
|
color: var(--dsw-alias-state-success-primary);
|
|
1089
1173
|
}
|
|
1090
|
-
|
|
1091
|
-
.ttl-badge
|
|
1092
|
-
|
|
1093
|
-
|
|
1174
|
+
/* 状态:胶囊 + 状态点(颜色沿用胶囊文字色)。 */
|
|
1175
|
+
.ttl-status-badge::before {
|
|
1176
|
+
content: '';
|
|
1177
|
+
flex: none;
|
|
1178
|
+
width: 5px;
|
|
1179
|
+
height: 5px;
|
|
1180
|
+
margin-right: 4px;
|
|
1181
|
+
border-radius: 50%;
|
|
1182
|
+
background: currentColor;
|
|
1183
|
+
opacity: 0.9;
|
|
1094
1184
|
}
|
|
1095
|
-
.ttl-badge
|
|
1096
|
-
|
|
1097
|
-
|
|
1185
|
+
.ttl-badge-row { display: inline-flex; flex-wrap: nowrap; align-items: center; gap: 4px; }
|
|
1186
|
+
/* 类型:小号圆角矩形标签(与状态胶囊区分层级)。 */
|
|
1187
|
+
.ttl-badge.ttl-type-badge {
|
|
1188
|
+
border-radius: 5px;
|
|
1189
|
+
padding: 0 6px;
|
|
1190
|
+
font-size: 10.5px;
|
|
1191
|
+
letter-spacing: 0.02em;
|
|
1098
1192
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1193
|
+
/* 优先级:箭头 + 文字,不再用第二个彩色胶囊,避免「标签汤」。 */
|
|
1194
|
+
.ttl-badge.ttl-priority-badge {
|
|
1195
|
+
gap: 3px;
|
|
1196
|
+
padding: 0;
|
|
1197
|
+
background: none;
|
|
1198
|
+
font-weight: 500;
|
|
1102
1199
|
}
|
|
1200
|
+
.ttl-priority-arrow { font-size: 11px; line-height: 1; }
|
|
1201
|
+
.ttl-badge.ttl-priority-badge.ttl-priority-high { background: none; color: var(--dsw-alias-state-error-primary); }
|
|
1202
|
+
.ttl-badge.ttl-priority-badge.ttl-priority-medium { background: none; color: var(--dsw-alias-state-warn-label); }
|
|
1203
|
+
.ttl-badge.ttl-priority-badge.ttl-priority-low { background: none; color: var(--dsw-alias-label-tertiary); }
|
|
1103
1204
|
.ttl-priority-select select { min-width: 64px; }
|
|
1104
1205
|
.ttl-type-select select { min-width: 76px; }
|
|
1105
1206
|
.ttl-badge.ttl-type-bug {
|
|
@@ -1176,7 +1277,7 @@
|
|
|
1176
1277
|
gap: 2px;
|
|
1177
1278
|
}
|
|
1178
1279
|
.ttl-link {
|
|
1179
|
-
padding: 2px
|
|
1280
|
+
padding: 2px 3px;
|
|
1180
1281
|
border: 0;
|
|
1181
1282
|
border-radius: 6px;
|
|
1182
1283
|
background: none;
|
|
@@ -1246,10 +1347,11 @@
|
|
|
1246
1347
|
}
|
|
1247
1348
|
.ttl-skeleton-row {
|
|
1248
1349
|
display: grid;
|
|
1249
|
-
|
|
1350
|
+
/* 与 .ttl-table 的列宽同口径(标题吃剩余,状态列固定),骨架屏不跳列。 */
|
|
1351
|
+
grid-template-columns: 20px minmax(0, 1fr) 72px 132px 100px 100px 92px;
|
|
1250
1352
|
align-items: center;
|
|
1251
|
-
gap:
|
|
1252
|
-
height:
|
|
1353
|
+
gap: 12px;
|
|
1354
|
+
height: 42px;
|
|
1253
1355
|
padding: 0 12px;
|
|
1254
1356
|
border-bottom: 0.5px solid var(--dsw-alias-border-l1);
|
|
1255
1357
|
}
|
|
@@ -1300,14 +1402,34 @@
|
|
|
1300
1402
|
.ttl-board-edit .ttl-project-picker { flex: 1 1 100%; }
|
|
1301
1403
|
.ttl-calendar-entry-edit .ttl-project-picker { flex: 1 1 100%; }
|
|
1302
1404
|
.ttl-gantt-row-edit .ttl-project-picker { flex: 0 1 220px; }.ttl-badge-project {
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1405
|
+
display: inline-flex;
|
|
1406
|
+
align-items: center;
|
|
1407
|
+
gap: 5px;
|
|
1408
|
+
max-width: 100%;
|
|
1306
1409
|
padding: 0;
|
|
1307
1410
|
background: none;
|
|
1308
|
-
color: var(--dsw-alias-label-
|
|
1411
|
+
color: var(--dsw-alias-label-secondary);
|
|
1309
1412
|
font-weight: 400;
|
|
1310
1413
|
}
|
|
1414
|
+
.ttl-project-dot {
|
|
1415
|
+
flex: none;
|
|
1416
|
+
width: 6px;
|
|
1417
|
+
height: 6px;
|
|
1418
|
+
border-radius: 50%;
|
|
1419
|
+
background: var(--dsw-alias-label-tertiary);
|
|
1420
|
+
}
|
|
1421
|
+
.ttl-badge-project[data-tone='business'] .ttl-project-dot { background: var(--dsw-alias-state-business-primary); }
|
|
1422
|
+
.ttl-badge-project[data-tone='done'] .ttl-project-dot { background: var(--dsw-alias-state-done-primary); }
|
|
1423
|
+
.ttl-badge-project[data-tone='success'] .ttl-project-dot { background: var(--dsw-alias-state-success-primary); }
|
|
1424
|
+
.ttl-badge-project[data-tone='warn'] .ttl-project-dot { background: var(--dsw-alias-state-warn-primary); }
|
|
1425
|
+
.ttl-badge-project[data-tone='error'] .ttl-project-dot { background: var(--dsw-alias-state-error-primary); }
|
|
1426
|
+
.ttl-badge-project[data-tone='idle'] .ttl-project-dot { background: var(--dsw-alias-label-caption); }
|
|
1427
|
+
.ttl-project-name {
|
|
1428
|
+
min-width: 0;
|
|
1429
|
+
overflow: hidden;
|
|
1430
|
+
text-overflow: ellipsis;
|
|
1431
|
+
white-space: nowrap;
|
|
1432
|
+
}
|
|
1311
1433
|
.ttl-badge-project-none { color: var(--dsw-alias-label-caption); }
|
|
1312
1434
|
|
|
1313
1435
|
.ttl-list-group {
|
|
@@ -1616,22 +1738,33 @@
|
|
|
1616
1738
|
}
|
|
1617
1739
|
.ttl-table {
|
|
1618
1740
|
width: 100%;
|
|
1619
|
-
|
|
1741
|
+
/* 固定列宽:列宽只由 col 决定,不随标题长短跳动(否则长标题会挤扁状态列,徽标被迫换行)。 */
|
|
1742
|
+
table-layout: fixed;
|
|
1743
|
+
min-width: 860px;
|
|
1620
1744
|
border-collapse: collapse;
|
|
1621
1745
|
font-size: 13px;
|
|
1622
1746
|
}
|
|
1747
|
+
/* 列宽口径:标题吃掉剩余宽度,其余列固定,保证「状态」列永远放得下一行徽标。 */
|
|
1748
|
+
.ttl-col-check { width: 40px; }
|
|
1749
|
+
.ttl-col-project { width: 104px; }
|
|
1750
|
+
.ttl-col-status { width: 160px; }
|
|
1751
|
+
.ttl-col-createdAt { width: 126px; }
|
|
1752
|
+
.ttl-col-completedAt { width: 126px; }
|
|
1753
|
+
.ttl-col-actions { width: 118px; }
|
|
1623
1754
|
.ttl-th {
|
|
1624
|
-
height:
|
|
1755
|
+
height: 34px;
|
|
1625
1756
|
padding: 0 12px;
|
|
1626
|
-
border-bottom: 0.5px solid var(--dsw-alias-border-
|
|
1757
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
|
1627
1758
|
background: var(--dsw-alias-bg-module-platform);
|
|
1628
|
-
color: var(--dsw-alias-label-
|
|
1629
|
-
font-size:
|
|
1759
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1760
|
+
font-size: 11px;
|
|
1630
1761
|
font-weight: 500;
|
|
1631
|
-
line-height:
|
|
1762
|
+
line-height: 16px;
|
|
1763
|
+
letter-spacing: 0.02em;
|
|
1632
1764
|
text-align: left;
|
|
1633
1765
|
white-space: nowrap;
|
|
1634
1766
|
}
|
|
1767
|
+
.ttl-th-check { width: 40px; padding: 0 4px 0 12px; }
|
|
1635
1768
|
.ttl-th-right { text-align: right; }
|
|
1636
1769
|
.ttl-th-sortable { padding: 0; }
|
|
1637
1770
|
.ttl-th-button {
|
|
@@ -1639,14 +1772,15 @@
|
|
|
1639
1772
|
align-items: center;
|
|
1640
1773
|
gap: 4px;
|
|
1641
1774
|
width: 100%;
|
|
1642
|
-
height:
|
|
1775
|
+
height: 34px;
|
|
1643
1776
|
padding: 0 12px;
|
|
1644
1777
|
border: 0;
|
|
1645
1778
|
background: none;
|
|
1646
1779
|
color: inherit;
|
|
1647
1780
|
font: inherit;
|
|
1648
|
-
font-size:
|
|
1649
|
-
|
|
1781
|
+
font-size: 11px;
|
|
1782
|
+
letter-spacing: 0.02em;
|
|
1783
|
+
line-height: 16px;
|
|
1650
1784
|
text-align: left;
|
|
1651
1785
|
white-space: nowrap;
|
|
1652
1786
|
cursor: pointer;
|
|
@@ -1655,7 +1789,10 @@
|
|
|
1655
1789
|
.ttl-th-button:hover { color: var(--dsw-alias-label-primary); }
|
|
1656
1790
|
.ttl-th-button-active { color: var(--dsw-alias-label-primary); }
|
|
1657
1791
|
.ttl-th-arrow { color: var(--dsw-alias-label-tertiary); font-size: 9px; line-height: 1; }
|
|
1658
|
-
.ttl-table-row {
|
|
1792
|
+
.ttl-table-row {
|
|
1793
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l1);
|
|
1794
|
+
transition: background 0.12s ease-out;
|
|
1795
|
+
}
|
|
1659
1796
|
.ttl-table-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
1660
1797
|
.ttl-table tbody tr:last-child { border-bottom: 0; }
|
|
1661
1798
|
.ttl-table-group-row .ttl-td {
|
|
@@ -1671,7 +1808,8 @@
|
|
|
1671
1808
|
vertical-align: middle;
|
|
1672
1809
|
line-height: 20px;
|
|
1673
1810
|
}
|
|
1674
|
-
.ttl-td-
|
|
1811
|
+
.ttl-td-check { width: 40px; padding: 11px 4px 11px 12px; }
|
|
1812
|
+
.ttl-td-title { max-width: none; }
|
|
1675
1813
|
.ttl-td-time {
|
|
1676
1814
|
color: var(--dsw-alias-label-tertiary);
|
|
1677
1815
|
font-size: 12px;
|
|
@@ -1681,7 +1819,17 @@
|
|
|
1681
1819
|
font-variant-numeric: tabular-nums;
|
|
1682
1820
|
}
|
|
1683
1821
|
.ttl-td-actions { text-align: right; white-space: nowrap; }
|
|
1822
|
+
/* 操作区默认压低,行 hover / 键盘聚焦时再提亮,避免整张表被按钮占满注意力。 */
|
|
1823
|
+
.ttl-td-actions .ttl-link { color: var(--dsw-alias-label-tertiary); }
|
|
1824
|
+
.ttl-td-actions .ttl-link:hover { color: var(--dsw-alias-label-primary); }
|
|
1825
|
+
.ttl-table-row:hover .ttl-td-actions .ttl-link,
|
|
1826
|
+
.ttl-table-row:focus-within .ttl-td-actions .ttl-link { color: var(--dsw-alias-label-secondary); }
|
|
1827
|
+
.ttl-td-actions .ttl-link.ttl-link-danger { color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 72%, transparent); }
|
|
1828
|
+
.ttl-table-row:hover .ttl-td-actions .ttl-link.ttl-link-danger,
|
|
1829
|
+
.ttl-td-actions .ttl-link.ttl-link-danger:hover { color: var(--dsw-alias-state-error-primary); }
|
|
1684
1830
|
.ttl-table-title-button {
|
|
1831
|
+
display: -webkit-box;
|
|
1832
|
+
width: 100%;
|
|
1685
1833
|
max-width: 100%;
|
|
1686
1834
|
padding: 0;
|
|
1687
1835
|
border: 0;
|
|
@@ -1692,10 +1840,15 @@
|
|
|
1692
1840
|
font-weight: 500;
|
|
1693
1841
|
line-height: 20px;
|
|
1694
1842
|
text-align: left;
|
|
1843
|
+
overflow: hidden;
|
|
1695
1844
|
overflow-wrap: anywhere;
|
|
1845
|
+
/* 最多两行:长标题截断为省略号,完整文案在 title 与「详情」里可见,行高保持稳定。 */
|
|
1846
|
+
-webkit-line-clamp: 2;
|
|
1847
|
+
-webkit-box-orient: vertical;
|
|
1696
1848
|
cursor: pointer;
|
|
1697
1849
|
}
|
|
1698
1850
|
.ttl-table-title-button:hover { color: var(--dsw-alias-state-business-primary); }
|
|
1851
|
+
.ttl-table-row[data-status='completed'] .ttl-table-title-button { color: var(--dsw-alias-label-secondary); }
|
|
1699
1852
|
.ttl-table-detail-row .ttl-td { padding: 0 12px 10px; }
|
|
1700
1853
|
|
|
1701
1854
|
.ttl-board {
|
|
@@ -1786,7 +1939,7 @@
|
|
|
1786
1939
|
.ttl-board-move-select { height: 24px; max-width: 150px; padding: 0 4px; border: 0.5px solid var(--dsw-alias-border-l3); border-radius: 6px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12px; cursor: pointer; }
|
|
1787
1940
|
.ttl-board-move-select:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary); outline-offset: 1px; }
|
|
1788
1941
|
.ttl-board-compact .ttl-board-card { gap: 2px; padding: 6px 28px 6px 32px; }
|
|
1789
|
-
.ttl-board-compact .ttl-board-check { top: 6px; left: 6px; width:
|
|
1942
|
+
.ttl-board-compact .ttl-board-check { top: 6px; left: 6px; width: 18px; height: 18px; }
|
|
1790
1943
|
.ttl-board-compact .ttl-board-more { top: 4px; right: 2px; }
|
|
1791
1944
|
.ttl-board-compact .ttl-board-card-title { -webkit-line-clamp: 1; }
|
|
1792
1945
|
.ttl-board-compact .ttl-board-card-files { display: none; }
|
|
@@ -1805,8 +1958,13 @@
|
|
|
1805
1958
|
gap: 4px;
|
|
1806
1959
|
padding: 9px 32px 9px 36px;
|
|
1807
1960
|
border: 0.5px solid var(--dsw-alias-border-l2);
|
|
1808
|
-
border-radius:
|
|
1961
|
+
border-radius: 12px;
|
|
1809
1962
|
background: var(--dsw-alias-bg-base);
|
|
1963
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
1964
|
+
}
|
|
1965
|
+
.ttl-board-card:hover {
|
|
1966
|
+
border-color: var(--dsw-alias-border-l3);
|
|
1967
|
+
box-shadow: var(--dsw-shadow-lv1);
|
|
1810
1968
|
}
|
|
1811
1969
|
.ttl-board-card-editing { padding: 9px 10px; }
|
|
1812
1970
|
.ttl-board-card-main { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
@@ -1835,30 +1993,30 @@
|
|
|
1835
1993
|
}
|
|
1836
1994
|
.ttl-board-project { display: inline-flex; align-items: center; gap: 4px; min-width: 0; max-width: 100%; }
|
|
1837
1995
|
.ttl-board-project-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
top: 8px;
|
|
1841
|
-
left: 8px;
|
|
1996
|
+
/* 勾选圈:表格行首与看板卡片共用(看板只额外负责定位)。 */
|
|
1997
|
+
.ttl-check {
|
|
1842
1998
|
display: inline-flex;
|
|
1843
1999
|
align-items: center;
|
|
1844
2000
|
justify-content: center;
|
|
1845
|
-
width:
|
|
1846
|
-
height:
|
|
2001
|
+
width: 20px;
|
|
2002
|
+
height: 20px;
|
|
1847
2003
|
padding: 0;
|
|
1848
|
-
border: 1.5px solid var(--dsw-alias-border-
|
|
2004
|
+
border: 1.5px solid var(--dsw-alias-border-l4);
|
|
1849
2005
|
border-radius: 50%;
|
|
1850
2006
|
background: none;
|
|
1851
2007
|
color: transparent;
|
|
1852
2008
|
cursor: pointer;
|
|
2009
|
+
transition: border-color 0.15s, background 0.15s, color 0.15s;
|
|
1853
2010
|
}
|
|
1854
|
-
.ttl-
|
|
1855
|
-
.ttl-
|
|
2011
|
+
.ttl-check:hover { border-color: var(--dsw-alias-state-success-primary); }
|
|
2012
|
+
.ttl-check[data-checked='true'] {
|
|
1856
2013
|
border-color: var(--dsw-alias-state-success-primary);
|
|
1857
2014
|
background: var(--dsw-alias-state-success-primary);
|
|
1858
2015
|
color: var(--dsw-alias-label-primary-foreground);
|
|
1859
2016
|
}
|
|
1860
|
-
.ttl-
|
|
1861
|
-
.ttl-
|
|
2017
|
+
.ttl-check:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary); outline-offset: 2px; }
|
|
2018
|
+
.ttl-check:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
2019
|
+
.ttl-board-check { position: absolute; top: 8px; left: 8px; }
|
|
1862
2020
|
.ttl-board-more {
|
|
1863
2021
|
position: absolute;
|
|
1864
2022
|
top: 6px;
|
|
@@ -2119,10 +2277,167 @@
|
|
|
2119
2277
|
.ttl-file-chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2120
2278
|
.ttl-file-chip-icon { font-size: 10px; line-height: 1; }
|
|
2121
2279
|
.ttl-file-more { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 20px; }
|
|
2122
|
-
.ttl-title-cell { display: flex; flex-direction: column; align-items:
|
|
2280
|
+
.ttl-title-cell { display: flex; flex-direction: column; align-items: stretch; gap: 3px; min-width: 0; }
|
|
2123
2281
|
.ttl-board-card-files { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
|
|
2124
2282
|
.ttl-calendar-entry-files { display: flex; flex: 1 1 100%; flex-wrap: wrap; gap: 4px; }
|
|
2125
2283
|
.ttl-detail-files { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
2284
|
+
|
|
2285
|
+
/* 导入 / 导出:轻量浮层对话框,内容超高时只滚动对话框主体。 */
|
|
2286
|
+
.ttl-download-anchor {
|
|
2287
|
+
position: absolute;
|
|
2288
|
+
width: 1px;
|
|
2289
|
+
height: 1px;
|
|
2290
|
+
overflow: hidden;
|
|
2291
|
+
clip: rect(0 0 0 0);
|
|
2292
|
+
white-space: nowrap;
|
|
2293
|
+
}
|
|
2294
|
+
.ttl-overlay {
|
|
2295
|
+
position: absolute;
|
|
2296
|
+
inset: 0;
|
|
2297
|
+
z-index: 30;
|
|
2298
|
+
display: flex;
|
|
2299
|
+
align-items: center;
|
|
2300
|
+
justify-content: center;
|
|
2301
|
+
padding: 24px;
|
|
2302
|
+
background: var(--dsw-alias-bg-mask-1);
|
|
2303
|
+
}
|
|
2304
|
+
.ttl-dialog {
|
|
2305
|
+
display: flex;
|
|
2306
|
+
flex-direction: column;
|
|
2307
|
+
width: min(560px, 100%);
|
|
2308
|
+
max-height: 100%;
|
|
2309
|
+
border: 0.5px solid var(--dsw-alias-border-l2);
|
|
2310
|
+
border-radius: var(--dsw-radius-lg);
|
|
2311
|
+
background: var(--dsw-alias-bg-base);
|
|
2312
|
+
box-shadow: var(--dsw-shadow-lv3);
|
|
2313
|
+
overflow: hidden;
|
|
2314
|
+
}
|
|
2315
|
+
.ttl-dialog-head {
|
|
2316
|
+
display: flex;
|
|
2317
|
+
flex: none;
|
|
2318
|
+
align-items: center;
|
|
2319
|
+
justify-content: space-between;
|
|
2320
|
+
gap: 12px;
|
|
2321
|
+
padding: 14px 16px;
|
|
2322
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l1);
|
|
2323
|
+
}
|
|
2324
|
+
.ttl-dialog-title {
|
|
2325
|
+
margin: 0;
|
|
2326
|
+
font-size: 15px;
|
|
2327
|
+
font-weight: 600;
|
|
2328
|
+
line-height: 22px;
|
|
2329
|
+
color: var(--dsw-alias-label-primary);
|
|
2330
|
+
}
|
|
2331
|
+
.ttl-dialog-close {
|
|
2332
|
+
display: inline-flex;
|
|
2333
|
+
align-items: center;
|
|
2334
|
+
justify-content: center;
|
|
2335
|
+
width: 26px;
|
|
2336
|
+
height: 26px;
|
|
2337
|
+
padding: 0;
|
|
2338
|
+
border: 0;
|
|
2339
|
+
border-radius: var(--dsw-radius-sm);
|
|
2340
|
+
background: none;
|
|
2341
|
+
color: var(--dsw-alias-label-tertiary);
|
|
2342
|
+
font: inherit;
|
|
2343
|
+
font-size: 18px;
|
|
2344
|
+
line-height: 1;
|
|
2345
|
+
cursor: pointer;
|
|
2346
|
+
}
|
|
2347
|
+
.ttl-dialog-close:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
2348
|
+
.ttl-dialog-body {
|
|
2349
|
+
display: flex;
|
|
2350
|
+
flex-direction: column;
|
|
2351
|
+
gap: 18px;
|
|
2352
|
+
padding: 16px;
|
|
2353
|
+
overflow-y: auto;
|
|
2354
|
+
}
|
|
2355
|
+
.ttl-dialog-footer {
|
|
2356
|
+
display: flex;
|
|
2357
|
+
flex: none;
|
|
2358
|
+
align-items: center;
|
|
2359
|
+
justify-content: flex-end;
|
|
2360
|
+
gap: 8px;
|
|
2361
|
+
padding: 12px 16px;
|
|
2362
|
+
border-top: 0.5px solid var(--dsw-alias-border-l1);
|
|
2363
|
+
background: var(--dsw-alias-bg-base);
|
|
2364
|
+
}
|
|
2365
|
+
.ttl-transfer-section { display: flex; flex-direction: column; gap: 8px; }
|
|
2366
|
+
.ttl-transfer-heading {
|
|
2367
|
+
margin: 0;
|
|
2368
|
+
font-size: 13px;
|
|
2369
|
+
font-weight: 600;
|
|
2370
|
+
line-height: 20px;
|
|
2371
|
+
color: var(--dsw-alias-label-primary);
|
|
2372
|
+
}
|
|
2373
|
+
.ttl-transfer-hint { margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
|
|
2374
|
+
.ttl-dialog-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
2375
|
+
.ttl-dialog-actions-end { justify-content: flex-end; padding-top: 4px; }
|
|
2376
|
+
.ttl-transfer-file {
|
|
2377
|
+
max-width: 100%;
|
|
2378
|
+
overflow: hidden;
|
|
2379
|
+
text-overflow: ellipsis;
|
|
2380
|
+
white-space: nowrap;
|
|
2381
|
+
font-size: 12px;
|
|
2382
|
+
line-height: 18px;
|
|
2383
|
+
color: var(--dsw-alias-label-secondary);
|
|
2384
|
+
}
|
|
2385
|
+
.ttl-file-input { display: none; }
|
|
2386
|
+
.ttl-transfer-error {
|
|
2387
|
+
margin: 0;
|
|
2388
|
+
padding: 8px 10px;
|
|
2389
|
+
border-radius: var(--dsw-radius-sm);
|
|
2390
|
+
background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);
|
|
2391
|
+
color: var(--dsw-alias-state-error-primary);
|
|
2392
|
+
font-size: 12px;
|
|
2393
|
+
line-height: 18px;
|
|
2394
|
+
}
|
|
2395
|
+
.ttl-transfer-preview {
|
|
2396
|
+
display: flex;
|
|
2397
|
+
flex-direction: column;
|
|
2398
|
+
gap: 10px;
|
|
2399
|
+
padding: 12px;
|
|
2400
|
+
border: 0.5px solid var(--dsw-alias-border-l2);
|
|
2401
|
+
border-radius: var(--dsw-radius-md);
|
|
2402
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
2403
|
+
}
|
|
2404
|
+
.ttl-transfer-preview-title { font-size: 12px; font-weight: 600; line-height: 18px; color: var(--dsw-alias-label-primary); }
|
|
2405
|
+
.ttl-transfer-counts { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
2406
|
+
.ttl-transfer-count {
|
|
2407
|
+
padding: 2px 8px;
|
|
2408
|
+
border-radius: 999px;
|
|
2409
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
2410
|
+
color: var(--dsw-alias-label-secondary);
|
|
2411
|
+
font-size: 12px;
|
|
2412
|
+
line-height: 18px;
|
|
2413
|
+
font-variant-numeric: tabular-nums;
|
|
2414
|
+
}
|
|
2415
|
+
.ttl-transfer-rows { margin: 0; font-size: 11px; line-height: 16px; color: var(--dsw-alias-label-tertiary); }
|
|
2416
|
+
.ttl-transfer-invalid {
|
|
2417
|
+
margin: 0;
|
|
2418
|
+
padding-left: 16px;
|
|
2419
|
+
color: var(--dsw-alias-state-error-primary);
|
|
2420
|
+
font-size: 11px;
|
|
2421
|
+
line-height: 17px;
|
|
2422
|
+
}
|
|
2423
|
+
.ttl-transfer-mode { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 12px; color: var(--dsw-alias-label-tertiary); }
|
|
2424
|
+
.ttl-transfer-mode select {
|
|
2425
|
+
height: 28px;
|
|
2426
|
+
padding: 0 6px;
|
|
2427
|
+
border: 0.5px solid var(--dsw-alias-border-l3);
|
|
2428
|
+
border-radius: var(--dsw-radius-sm);
|
|
2429
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
2430
|
+
color: var(--dsw-alias-label-secondary);
|
|
2431
|
+
font: inherit;
|
|
2432
|
+
font-size: 12px;
|
|
2433
|
+
cursor: pointer;
|
|
2434
|
+
}
|
|
2435
|
+
.ttl-transfer-mode-hint { margin: 0; font-size: 11px; line-height: 16px; color: var(--dsw-alias-label-tertiary); }
|
|
2436
|
+
.ttl-transfer-empty { margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
|
|
2437
|
+
.ttl-overlay .ttl-button { height: 28px; font-size: 12px; }
|
|
2438
|
+
@media (max-width: 560px) {
|
|
2439
|
+
.ttl-overlay { align-items: flex-start; padding: 12px; }
|
|
2440
|
+
}
|
|
2126
2441
|
@media (prefers-reduced-motion: reduce) {
|
|
2127
2442
|
.ttl-input,
|
|
2128
2443
|
.ttl-button,
|
|
@@ -2495,6 +2810,16 @@
|
|
|
2495
2810
|
const name = normalizeProject(project);
|
|
2496
2811
|
return name === "" ? t("project.none") : name;
|
|
2497
2812
|
}
|
|
2813
|
+
var PROJECT_TONES = ["business", "done", "success", "warn", "error"];
|
|
2814
|
+
function projectTone(project) {
|
|
2815
|
+
const name = normalizeProject(project);
|
|
2816
|
+
if (name === "") return "idle";
|
|
2817
|
+
let hash = 0;
|
|
2818
|
+
for (let index = 0; index < name.length; index += 1) {
|
|
2819
|
+
hash = hash * 31 + name.charCodeAt(index) | 0;
|
|
2820
|
+
}
|
|
2821
|
+
return PROJECT_TONES[Math.abs(hash) % PROJECT_TONES.length];
|
|
2822
|
+
}
|
|
2498
2823
|
function compareProjectNames(left, right) {
|
|
2499
2824
|
if (left === right) return 0;
|
|
2500
2825
|
if (left === "") return 1;
|
|
@@ -2824,6 +3149,7 @@
|
|
|
2824
3149
|
(index) => h(
|
|
2825
3150
|
"div",
|
|
2826
3151
|
{ key: index, className: "ttl-skeleton-row" },
|
|
3152
|
+
bar(true),
|
|
2827
3153
|
bar(),
|
|
2828
3154
|
bar(),
|
|
2829
3155
|
bar(true),
|
|
@@ -2886,7 +3212,54 @@
|
|
|
2886
3212
|
function ProjectBadge(props) {
|
|
2887
3213
|
const project = normalizeProject(props && props.project);
|
|
2888
3214
|
const className = project === "" ? "ttl-badge ttl-badge-project ttl-badge-project-none" : "ttl-badge ttl-badge-project";
|
|
2889
|
-
return h(
|
|
3215
|
+
return h(
|
|
3216
|
+
"span",
|
|
3217
|
+
{ className, title: projectLabel(project), "data-tone": projectTone(project) },
|
|
3218
|
+
h("span", { className: "ttl-project-dot", "aria-hidden": "true" }),
|
|
3219
|
+
h("span", { className: "ttl-project-name" }, projectLabel(project))
|
|
3220
|
+
);
|
|
3221
|
+
}
|
|
3222
|
+
function CheckIcon(props) {
|
|
3223
|
+
const checked = Boolean(props && props.checked);
|
|
3224
|
+
return h(
|
|
3225
|
+
"svg",
|
|
3226
|
+
{
|
|
3227
|
+
width: 12,
|
|
3228
|
+
height: 12,
|
|
3229
|
+
viewBox: "0 0 16 16",
|
|
3230
|
+
fill: "none",
|
|
3231
|
+
stroke: "currentColor",
|
|
3232
|
+
strokeWidth: 2,
|
|
3233
|
+
strokeLinecap: "round",
|
|
3234
|
+
strokeLinejoin: "round",
|
|
3235
|
+
"aria-hidden": "true",
|
|
3236
|
+
focusable: "false"
|
|
3237
|
+
},
|
|
3238
|
+
checked ? h("path", { d: "M3.5 8.5l3 3 6-6.5" }) : null
|
|
3239
|
+
);
|
|
3240
|
+
}
|
|
3241
|
+
function CheckButton(props) {
|
|
3242
|
+
const checked = Boolean(props && props.checked);
|
|
3243
|
+
const disabled = Boolean(props && props.disabled);
|
|
3244
|
+
const label = props && typeof props.label === "string" ? props.label : "";
|
|
3245
|
+
const hover = props && typeof props.title === "string" && props.title !== "" ? props.title : void 0;
|
|
3246
|
+
const className = props && typeof props.className === "string" && props.className !== "" ? props.className : "";
|
|
3247
|
+
const onClick = props && typeof props.onClick === "function" ? props.onClick : null;
|
|
3248
|
+
return h(
|
|
3249
|
+
"button",
|
|
3250
|
+
{
|
|
3251
|
+
type: "button",
|
|
3252
|
+
className: className === "" ? "ttl-check" : `ttl-check ${className}`,
|
|
3253
|
+
role: "checkbox",
|
|
3254
|
+
"aria-checked": checked ? "true" : "false",
|
|
3255
|
+
"aria-label": label,
|
|
3256
|
+
title: hover,
|
|
3257
|
+
"data-checked": checked ? "true" : "false",
|
|
3258
|
+
disabled,
|
|
3259
|
+
onClick
|
|
3260
|
+
},
|
|
3261
|
+
h(CheckIcon, { checked })
|
|
3262
|
+
);
|
|
2890
3263
|
}
|
|
2891
3264
|
function GanttView(props) {
|
|
2892
3265
|
const todos = props && Array.isArray(props.todos) ? props.todos : [];
|
|
@@ -3555,22 +3928,6 @@
|
|
|
3555
3928
|
)
|
|
3556
3929
|
);
|
|
3557
3930
|
};
|
|
3558
|
-
const boardCheckIcon = (completed) => h(
|
|
3559
|
-
"svg",
|
|
3560
|
-
{
|
|
3561
|
-
width: 12,
|
|
3562
|
-
height: 12,
|
|
3563
|
-
viewBox: "0 0 16 16",
|
|
3564
|
-
fill: "none",
|
|
3565
|
-
stroke: "currentColor",
|
|
3566
|
-
strokeWidth: 2,
|
|
3567
|
-
strokeLinecap: "round",
|
|
3568
|
-
strokeLinejoin: "round",
|
|
3569
|
-
"aria-hidden": "true",
|
|
3570
|
-
focusable: "false"
|
|
3571
|
-
},
|
|
3572
|
-
completed ? h("path", { d: "M3.5 8.5l3 3 6-6.5" }) : null
|
|
3573
|
-
);
|
|
3574
3931
|
const renderBoardCard = (todo) => {
|
|
3575
3932
|
const isEditing = editingId === todo.id;
|
|
3576
3933
|
const isConfirming = confirmId === todo.id;
|
|
@@ -3609,23 +3966,16 @@
|
|
|
3609
3966
|
prioritySelectControl(editingPriority, setEditingPriority, busyId === todo.id),
|
|
3610
3967
|
typeSelectControl(editingType, setEditingType, busyId === todo.id)
|
|
3611
3968
|
) : [
|
|
3612
|
-
h(
|
|
3613
|
-
"
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
disabled: isBusy,
|
|
3623
|
-
onClick: () => {
|
|
3624
|
-
void handleToggle(todo);
|
|
3625
|
-
}
|
|
3626
|
-
},
|
|
3627
|
-
boardCheckIcon(completed)
|
|
3628
|
-
),
|
|
3969
|
+
h(CheckButton, {
|
|
3970
|
+
key: "check",
|
|
3971
|
+
checked: completed,
|
|
3972
|
+
disabled: isBusy,
|
|
3973
|
+
label: t("board.checkAria", { title }),
|
|
3974
|
+
className: "ttl-board-check",
|
|
3975
|
+
onClick: () => {
|
|
3976
|
+
void handleToggle(todo);
|
|
3977
|
+
}
|
|
3978
|
+
}),
|
|
3629
3979
|
h(
|
|
3630
3980
|
"div",
|
|
3631
3981
|
{ key: "main", className: "ttl-board-card-main" },
|
|
@@ -3639,8 +3989,8 @@
|
|
|
3639
3989
|
"span",
|
|
3640
3990
|
{ className: "ttl-board-project", title: projectLabel(project) },
|
|
3641
3991
|
h("span", {
|
|
3642
|
-
className: "ttl-
|
|
3643
|
-
"data-
|
|
3992
|
+
className: "ttl-project-dot",
|
|
3993
|
+
"data-tone": projectTone(project),
|
|
3644
3994
|
"aria-hidden": "true"
|
|
3645
3995
|
}),
|
|
3646
3996
|
h("span", { className: "ttl-board-project-name" }, projectLabel(project))
|
|
@@ -4089,7 +4439,7 @@
|
|
|
4089
4439
|
|
|
4090
4440
|
// src/client/views/table.js
|
|
4091
4441
|
function createTableView(deps) {
|
|
4092
|
-
const { confirmId, detailIdOf, direction, editingId, emptyActionKey, expandedIds, filter, handleEmptyAction, handleHeaderSort, listStatus, priorityBadge, projectInput, renderActionButtons, renderDetailContent, renderFileChips, setConfirmId, sortBy, statusBadge, titleInput, toggleExpanded, typeBadge, visibleTodos } = deps;
|
|
4442
|
+
const { confirmId, detailIdOf, direction, editingId, emptyActionKey, expandedIds, filter, handleEmptyAction, handleHeaderSort, listStatus, priorityBadge, projectInput, renderActionButtons, renderDetailContent, renderFileChips, renderRowCheck, setConfirmId, sortBy, statusBadge, titleInput, toggleExpanded, typeBadge, visibleTodos } = deps;
|
|
4093
4443
|
const pushTableRow = (rows, todo) => {
|
|
4094
4444
|
const completed = todo.status === "completed";
|
|
4095
4445
|
const isEditing = editingId === todo.id;
|
|
@@ -4131,6 +4481,7 @@
|
|
|
4131
4481
|
}
|
|
4132
4482
|
}
|
|
4133
4483
|
},
|
|
4484
|
+
h("td", { className: "ttl-td ttl-td-check" }, renderRowCheck(todo)),
|
|
4134
4485
|
titleCell,
|
|
4135
4486
|
h(
|
|
4136
4487
|
"td",
|
|
@@ -4144,7 +4495,7 @@
|
|
|
4144
4495
|
),
|
|
4145
4496
|
h("td", { className: "ttl-td ttl-td-time" }, formatTime(todo.createdAt)),
|
|
4146
4497
|
h("td", { className: "ttl-td ttl-td-time" }, completed ? formatTime(todo.completedAt) : EMPTY_TIME),
|
|
4147
|
-
h("td", { className: "ttl-td ttl-td-actions" }, renderActionButtons(todo, { showDetail: true }))
|
|
4498
|
+
h("td", { className: "ttl-td ttl-td-actions" }, renderActionButtons(todo, { showDetail: true, showToggle: false }))
|
|
4148
4499
|
)
|
|
4149
4500
|
);
|
|
4150
4501
|
if (expanded) {
|
|
@@ -4207,6 +4558,11 @@
|
|
|
4207
4558
|
"aria-label": t("table.aria"),
|
|
4208
4559
|
"aria-busy": listStatus === "loading" ? "true" : "false"
|
|
4209
4560
|
},
|
|
4561
|
+
h(
|
|
4562
|
+
"colgroup",
|
|
4563
|
+
null,
|
|
4564
|
+
TABLE_COLUMNS.map((column) => h("col", { key: column.key, className: `ttl-col ttl-col-${column.key}` }))
|
|
4565
|
+
),
|
|
4210
4566
|
h(
|
|
4211
4567
|
"thead",
|
|
4212
4568
|
null,
|
|
@@ -4221,10 +4577,10 @@
|
|
|
4221
4577
|
{
|
|
4222
4578
|
key: column.key,
|
|
4223
4579
|
scope: "col",
|
|
4224
|
-
className: (column.sortBy === null ? "ttl-th" : "ttl-th ttl-th-sortable") + (column.align === "right" ? " ttl-th-right" : "")
|
|
4580
|
+
className: (column.sortBy === null ? "ttl-th" : "ttl-th ttl-th-sortable") + (column.align === "right" ? " ttl-th-right" : "") + ` ttl-th-${column.key}`,
|
|
4225
4581
|
"aria-sort": column.sortBy === null ? void 0 : ariaSort
|
|
4226
4582
|
},
|
|
4227
|
-
column.sortBy === null ? h("span", null, t(column.labelKey)) : h(
|
|
4583
|
+
column.srOnly === true ? h("span", { className: "ttl-sr-only" }, t(column.labelKey)) : column.sortBy === null ? h("span", null, t(column.labelKey)) : h(
|
|
4228
4584
|
"button",
|
|
4229
4585
|
{
|
|
4230
4586
|
type: "button",
|
|
@@ -4247,6 +4603,245 @@
|
|
|
4247
4603
|
return renderTableView;
|
|
4248
4604
|
}
|
|
4249
4605
|
|
|
4606
|
+
// src/client/transfer.js
|
|
4607
|
+
function makeDownload(content, filename, mime) {
|
|
4608
|
+
const text = typeof content === "string" ? content : "";
|
|
4609
|
+
const name = typeof filename === "string" && filename !== "" ? filename : "todos.json";
|
|
4610
|
+
const type = typeof mime === "string" && mime !== "" ? mime : "application/octet-stream";
|
|
4611
|
+
try {
|
|
4612
|
+
if (typeof Blob !== "undefined" && typeof URL !== "undefined" && typeof URL.createObjectURL === "function") {
|
|
4613
|
+
return { url: URL.createObjectURL(new Blob([text], { type })), name };
|
|
4614
|
+
}
|
|
4615
|
+
} catch {
|
|
4616
|
+
}
|
|
4617
|
+
return { url: `data:${type},${encodeURIComponent(text)}`, name };
|
|
4618
|
+
}
|
|
4619
|
+
function readFileText(file) {
|
|
4620
|
+
if (file && typeof file.text === "function") {
|
|
4621
|
+
return file.text().then((text) => String(text ?? ""));
|
|
4622
|
+
}
|
|
4623
|
+
return new Promise((resolve, reject) => {
|
|
4624
|
+
const reader = new FileReader();
|
|
4625
|
+
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
4626
|
+
reader.onerror = () => reject(new Error("读取文件失败"));
|
|
4627
|
+
reader.readAsText(file, "utf-8");
|
|
4628
|
+
});
|
|
4629
|
+
}
|
|
4630
|
+
function countChip(labelKey, count) {
|
|
4631
|
+
const value = Number(count);
|
|
4632
|
+
return h(
|
|
4633
|
+
"span",
|
|
4634
|
+
{ className: "ttl-transfer-count", key: labelKey },
|
|
4635
|
+
t(labelKey, { count: Number.isFinite(value) && value > 0 ? Math.floor(value) : 0 })
|
|
4636
|
+
);
|
|
4637
|
+
}
|
|
4638
|
+
function TransferDialog(props) {
|
|
4639
|
+
const {
|
|
4640
|
+
open,
|
|
4641
|
+
busy,
|
|
4642
|
+
applying,
|
|
4643
|
+
fileName,
|
|
4644
|
+
mode,
|
|
4645
|
+
preview,
|
|
4646
|
+
error,
|
|
4647
|
+
fileInputRef,
|
|
4648
|
+
onClose,
|
|
4649
|
+
onExport,
|
|
4650
|
+
onPickFile,
|
|
4651
|
+
onFileChange,
|
|
4652
|
+
onModeChange,
|
|
4653
|
+
onConfirm
|
|
4654
|
+
} = props;
|
|
4655
|
+
if (open !== true) return null;
|
|
4656
|
+
const record = isRecord(preview) ? preview : null;
|
|
4657
|
+
const summary = record !== null && isRecord(record.summary) ? record.summary : null;
|
|
4658
|
+
const invalid = record !== null && Array.isArray(record.invalid) ? record.invalid.slice(0, 5) : [];
|
|
4659
|
+
const invalidCount = record !== null && typeof record.invalidCount === "number" ? record.invalidCount : 0;
|
|
4660
|
+
const rowCount = record !== null && typeof record.rowCount === "number" ? record.rowCount : 0;
|
|
4661
|
+
const activeMode = TRANSFER_MODES.find((option) => option.value === mode) ?? TRANSFER_MODES[0];
|
|
4662
|
+
const canConfirm = summary !== null && busy !== true && applying !== true;
|
|
4663
|
+
const backdrop = (event) => {
|
|
4664
|
+
if (event && event.target === event.currentTarget) onClose();
|
|
4665
|
+
};
|
|
4666
|
+
return h(
|
|
4667
|
+
"div",
|
|
4668
|
+
{ className: "ttl-overlay", role: "presentation", onMouseDown: backdrop },
|
|
4669
|
+
h(
|
|
4670
|
+
"div",
|
|
4671
|
+
{
|
|
4672
|
+
className: "ttl-dialog",
|
|
4673
|
+
role: "dialog",
|
|
4674
|
+
"aria-modal": "true",
|
|
4675
|
+
"aria-labelledby": "ttl-transfer-title",
|
|
4676
|
+
onKeyDown: (event) => {
|
|
4677
|
+
if (event && event.key === "Escape") {
|
|
4678
|
+
event.stopPropagation();
|
|
4679
|
+
onClose();
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4682
|
+
},
|
|
4683
|
+
h(
|
|
4684
|
+
"div",
|
|
4685
|
+
{ className: "ttl-dialog-head" },
|
|
4686
|
+
h("h2", { id: "ttl-transfer-title", className: "ttl-dialog-title" }, t("transfer.title")),
|
|
4687
|
+
h(
|
|
4688
|
+
"button",
|
|
4689
|
+
{
|
|
4690
|
+
type: "button",
|
|
4691
|
+
className: "ttl-dialog-close",
|
|
4692
|
+
onClick: onClose,
|
|
4693
|
+
disabled: busy === true,
|
|
4694
|
+
"aria-label": t("transfer.close"),
|
|
4695
|
+
autoFocus: true
|
|
4696
|
+
},
|
|
4697
|
+
"×"
|
|
4698
|
+
)
|
|
4699
|
+
),
|
|
4700
|
+
h(
|
|
4701
|
+
"div",
|
|
4702
|
+
{ className: "ttl-dialog-body" },
|
|
4703
|
+
// 导出
|
|
4704
|
+
h(
|
|
4705
|
+
"section",
|
|
4706
|
+
{ className: "ttl-transfer-section" },
|
|
4707
|
+
h("h3", { className: "ttl-transfer-heading" }, t("transfer.exportTitle")),
|
|
4708
|
+
h("p", { className: "ttl-transfer-hint" }, t("transfer.exportHint")),
|
|
4709
|
+
h(
|
|
4710
|
+
"div",
|
|
4711
|
+
{ className: "ttl-dialog-actions" },
|
|
4712
|
+
h(
|
|
4713
|
+
"button",
|
|
4714
|
+
{
|
|
4715
|
+
type: "button",
|
|
4716
|
+
className: "ttl-button ttl-button-outline",
|
|
4717
|
+
disabled: busy === true,
|
|
4718
|
+
onClick: () => onExport("json")
|
|
4719
|
+
},
|
|
4720
|
+
t("transfer.exportJson")
|
|
4721
|
+
),
|
|
4722
|
+
h(
|
|
4723
|
+
"button",
|
|
4724
|
+
{
|
|
4725
|
+
type: "button",
|
|
4726
|
+
className: "ttl-button ttl-button-outline",
|
|
4727
|
+
disabled: busy === true,
|
|
4728
|
+
onClick: () => onExport("csv")
|
|
4729
|
+
},
|
|
4730
|
+
t("transfer.exportCsv")
|
|
4731
|
+
)
|
|
4732
|
+
)
|
|
4733
|
+
),
|
|
4734
|
+
// 导入
|
|
4735
|
+
h(
|
|
4736
|
+
"section",
|
|
4737
|
+
{ className: "ttl-transfer-section" },
|
|
4738
|
+
h("h3", { className: "ttl-transfer-heading" }, t("transfer.importTitle")),
|
|
4739
|
+
h("p", { className: "ttl-transfer-hint" }, t("transfer.importHint")),
|
|
4740
|
+
h(
|
|
4741
|
+
"div",
|
|
4742
|
+
{ className: "ttl-dialog-actions" },
|
|
4743
|
+
h(
|
|
4744
|
+
"button",
|
|
4745
|
+
{
|
|
4746
|
+
type: "button",
|
|
4747
|
+
className: "ttl-button ttl-button-outline",
|
|
4748
|
+
disabled: busy === true,
|
|
4749
|
+
onClick: onPickFile
|
|
4750
|
+
},
|
|
4751
|
+
t("transfer.chooseFile")
|
|
4752
|
+
),
|
|
4753
|
+
h(
|
|
4754
|
+
"span",
|
|
4755
|
+
{ className: "ttl-transfer-file", title: typeof fileName === "string" ? fileName : "" },
|
|
4756
|
+
typeof fileName === "string" && fileName !== "" ? fileName : t("transfer.noFile")
|
|
4757
|
+
)
|
|
4758
|
+
),
|
|
4759
|
+
h("input", {
|
|
4760
|
+
ref: fileInputRef,
|
|
4761
|
+
className: "ttl-file-input",
|
|
4762
|
+
type: "file",
|
|
4763
|
+
accept: ".json,.csv,application/json,text/csv",
|
|
4764
|
+
tabIndex: -1,
|
|
4765
|
+
"aria-label": t("transfer.chooseFile"),
|
|
4766
|
+
onChange: onFileChange
|
|
4767
|
+
}),
|
|
4768
|
+
typeof error === "string" && error !== "" ? h("p", { className: "ttl-transfer-error", role: "alert" }, error) : null,
|
|
4769
|
+
summary !== null ? h(
|
|
4770
|
+
"div",
|
|
4771
|
+
{ className: "ttl-transfer-preview" },
|
|
4772
|
+
h("div", { className: "ttl-transfer-preview-title" }, t("transfer.previewTitle")),
|
|
4773
|
+
h(
|
|
4774
|
+
"div",
|
|
4775
|
+
{ className: "ttl-transfer-counts" },
|
|
4776
|
+
countChip("transfer.previewAdded", summary.added),
|
|
4777
|
+
countChip("transfer.previewUpdated", summary.updated),
|
|
4778
|
+
countChip("transfer.previewSkipped", summary.skipped),
|
|
4779
|
+
Number(summary.removed) > 0 ? countChip("transfer.previewRemoved", summary.removed) : null,
|
|
4780
|
+
countChip("transfer.previewTotal", summary.total)
|
|
4781
|
+
),
|
|
4782
|
+
rowCount > 0 || invalidCount > 0 ? h(
|
|
4783
|
+
"p",
|
|
4784
|
+
{ className: "ttl-transfer-rows" },
|
|
4785
|
+
t("transfer.previewRows", { count: rowCount }),
|
|
4786
|
+
invalidCount > 0 ? ` · ${t("transfer.previewInvalid", { count: invalidCount })}` : ""
|
|
4787
|
+
) : null,
|
|
4788
|
+
invalid.length > 0 ? h(
|
|
4789
|
+
"ul",
|
|
4790
|
+
{ className: "ttl-transfer-invalid" },
|
|
4791
|
+
invalid.map((item, index) => h(
|
|
4792
|
+
"li",
|
|
4793
|
+
{ key: `invalid-${index}` },
|
|
4794
|
+
t("transfer.rowError", {
|
|
4795
|
+
row: isRecord(item) ? Number(item.row) || 0 : 0,
|
|
4796
|
+
message: isRecord(item) && typeof item.message === "string" ? item.message : ""
|
|
4797
|
+
})
|
|
4798
|
+
))
|
|
4799
|
+
) : null,
|
|
4800
|
+
h(
|
|
4801
|
+
"div",
|
|
4802
|
+
{ className: "ttl-transfer-mode" },
|
|
4803
|
+
h("span", { className: "ttl-transfer-mode-label" }, t("transfer.modeLabel")),
|
|
4804
|
+
h(
|
|
4805
|
+
"select",
|
|
4806
|
+
{
|
|
4807
|
+
value: typeof mode === "string" ? mode : "merge",
|
|
4808
|
+
disabled: busy === true,
|
|
4809
|
+
"aria-label": t("transfer.modeLabel"),
|
|
4810
|
+
onChange: (event) => onModeChange(event.target.value)
|
|
4811
|
+
},
|
|
4812
|
+
TRANSFER_MODES.map((option) => h(
|
|
4813
|
+
"option",
|
|
4814
|
+
{ key: option.value, value: option.value },
|
|
4815
|
+
t(option.labelKey)
|
|
4816
|
+
))
|
|
4817
|
+
)
|
|
4818
|
+
),
|
|
4819
|
+
h("p", { className: "ttl-transfer-mode-hint" }, t(activeMode.hintKey))
|
|
4820
|
+
) : h(
|
|
4821
|
+
"p",
|
|
4822
|
+
{ className: "ttl-transfer-empty" },
|
|
4823
|
+
busy === true ? t("transfer.reading") : t("transfer.noPreview")
|
|
4824
|
+
)
|
|
4825
|
+
)
|
|
4826
|
+
),
|
|
4827
|
+
h(
|
|
4828
|
+
"div",
|
|
4829
|
+
{ className: "ttl-dialog-footer" },
|
|
4830
|
+
h(
|
|
4831
|
+
"button",
|
|
4832
|
+
{
|
|
4833
|
+
type: "button",
|
|
4834
|
+
className: "ttl-button ttl-button-primary",
|
|
4835
|
+
disabled: canConfirm !== true,
|
|
4836
|
+
onClick: onConfirm
|
|
4837
|
+
},
|
|
4838
|
+
applying === true ? t("transfer.importing") : t("transfer.confirm")
|
|
4839
|
+
)
|
|
4840
|
+
)
|
|
4841
|
+
)
|
|
4842
|
+
);
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4250
4845
|
// src/client/panel.js
|
|
4251
4846
|
function TodoPanel() {
|
|
4252
4847
|
const [todos, setTodos] = React.useState([]);
|
|
@@ -4265,6 +4860,15 @@
|
|
|
4265
4860
|
const [editTitle, setEditTitle] = React.useState("");
|
|
4266
4861
|
const [confirmId, setConfirmId] = React.useState(null);
|
|
4267
4862
|
const [notice, setNotice] = React.useState(null);
|
|
4863
|
+
const [transferOpen, setTransferOpen] = React.useState(false);
|
|
4864
|
+
const [transferBusy, setTransferBusy] = React.useState(false);
|
|
4865
|
+
const [transferApplying, setTransferApplying] = React.useState(false);
|
|
4866
|
+
const [transferError, setTransferError] = React.useState("");
|
|
4867
|
+
const [transferFileName, setTransferFileName] = React.useState("");
|
|
4868
|
+
const [transferContent, setTransferContent] = React.useState("");
|
|
4869
|
+
const [transferMode, setTransferMode] = React.useState("merge");
|
|
4870
|
+
const [transferPreview, setTransferPreview] = React.useState(null);
|
|
4871
|
+
const [download, setDownload] = React.useState({ url: "", name: "" });
|
|
4268
4872
|
const [expandedIds, setExpandedIds] = React.useState({});
|
|
4269
4873
|
const [view, setView] = React.useState("table");
|
|
4270
4874
|
const [range, setRange] = React.useState("all");
|
|
@@ -4325,6 +4929,8 @@
|
|
|
4325
4929
|
const projectSeqRef = React.useRef(0);
|
|
4326
4930
|
const suggestKindRef = React.useRef("file");
|
|
4327
4931
|
const createBlurTimerRef = React.useRef(null);
|
|
4932
|
+
const transferInputRef = React.useRef(null);
|
|
4933
|
+
const downloadRef = React.useRef(null);
|
|
4328
4934
|
React.useEffect(() => {
|
|
4329
4935
|
mountedRef.current = true;
|
|
4330
4936
|
return () => {
|
|
@@ -4350,6 +4956,21 @@
|
|
|
4350
4956
|
}, delay);
|
|
4351
4957
|
return () => clearTimeout(timer);
|
|
4352
4958
|
}, [notice]);
|
|
4959
|
+
React.useEffect(() => {
|
|
4960
|
+
if (download.url === "" || downloadRef.current === null) return void 0;
|
|
4961
|
+
downloadRef.current.click();
|
|
4962
|
+
const url = download.url;
|
|
4963
|
+
const timer = setTimeout(() => {
|
|
4964
|
+
try {
|
|
4965
|
+
if (typeof URL !== "undefined" && typeof URL.revokeObjectURL === "function") {
|
|
4966
|
+
URL.revokeObjectURL(url);
|
|
4967
|
+
}
|
|
4968
|
+
} catch {
|
|
4969
|
+
}
|
|
4970
|
+
if (mountedRef.current) setDownload({ url: "", name: "" });
|
|
4971
|
+
}, 2e3);
|
|
4972
|
+
return () => clearTimeout(timer);
|
|
4973
|
+
}, [download]);
|
|
4353
4974
|
const loadList = React.useCallback(
|
|
4354
4975
|
async (nextFilter, nextSortBy, nextDirection, nextProject, options) => {
|
|
4355
4976
|
const quiet = Boolean(options && options.quiet);
|
|
@@ -5037,7 +5658,9 @@
|
|
|
5037
5658
|
const detailIdOf = (todo) => `ttl-detail-${String(todo.id).replace(/[^A-Za-z0-9_-]/g, "-")}`;
|
|
5038
5659
|
const statusBadge = (todo) => h(
|
|
5039
5660
|
"span",
|
|
5040
|
-
{
|
|
5661
|
+
{
|
|
5662
|
+
className: todo.status === "completed" ? "ttl-badge ttl-status-badge ttl-badge-completed" : "ttl-badge ttl-status-badge"
|
|
5663
|
+
},
|
|
5041
5664
|
t(todo.status === "completed" ? "row.completed" : "row.pending")
|
|
5042
5665
|
);
|
|
5043
5666
|
const priorityBadge = (todo) => {
|
|
@@ -5049,6 +5672,11 @@
|
|
|
5049
5672
|
className: `ttl-badge ttl-priority-badge ttl-priority-${priority}`,
|
|
5050
5673
|
"data-priority": priority
|
|
5051
5674
|
},
|
|
5675
|
+
h(
|
|
5676
|
+
"span",
|
|
5677
|
+
{ className: "ttl-priority-arrow", "aria-hidden": "true" },
|
|
5678
|
+
priority === "high" ? "↑" : priority === "medium" ? "↗" : "↓"
|
|
5679
|
+
),
|
|
5052
5680
|
t(`priority.${priority}`)
|
|
5053
5681
|
);
|
|
5054
5682
|
};
|
|
@@ -5171,22 +5799,24 @@
|
|
|
5171
5799
|
return buttons;
|
|
5172
5800
|
}
|
|
5173
5801
|
if (opts.showDetail === true) buttons.push(detailToggleButton(todo));
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5802
|
+
if (opts.showToggle !== false) {
|
|
5803
|
+
buttons.push(
|
|
5804
|
+
h(
|
|
5805
|
+
"button",
|
|
5806
|
+
{
|
|
5807
|
+
key: "toggle",
|
|
5808
|
+
type: "button",
|
|
5809
|
+
className: "ttl-link",
|
|
5810
|
+
disabled: isBusy,
|
|
5811
|
+
"aria-label": t("row.toggleAria", { action: toggleLabel, title: todo.title }),
|
|
5812
|
+
onClick: () => {
|
|
5813
|
+
void handleToggle(todo);
|
|
5814
|
+
}
|
|
5815
|
+
},
|
|
5816
|
+
toggleLabel
|
|
5817
|
+
)
|
|
5818
|
+
);
|
|
5819
|
+
}
|
|
5190
5820
|
buttons.push(
|
|
5191
5821
|
h(
|
|
5192
5822
|
"button",
|
|
@@ -5245,6 +5875,19 @@
|
|
|
5245
5875
|
}
|
|
5246
5876
|
return buttons;
|
|
5247
5877
|
};
|
|
5878
|
+
const renderRowCheck = (todo) => {
|
|
5879
|
+
const completed = todo.status === "completed";
|
|
5880
|
+
return h(CheckButton, {
|
|
5881
|
+
checked: completed,
|
|
5882
|
+
disabled: isBusy,
|
|
5883
|
+
label: t("board.checkAria", { title: todo.title === "" ? t("list.empty") : todo.title }),
|
|
5884
|
+
title: t(completed ? "row.uncomplete" : "row.complete"),
|
|
5885
|
+
className: "ttl-row-check",
|
|
5886
|
+
onClick: () => {
|
|
5887
|
+
void handleToggle(todo);
|
|
5888
|
+
}
|
|
5889
|
+
});
|
|
5890
|
+
};
|
|
5248
5891
|
const copyFilePath = (path) => {
|
|
5249
5892
|
try {
|
|
5250
5893
|
if (typeof navigator === "undefined" || navigator === null) return;
|
|
@@ -5405,6 +6048,128 @@
|
|
|
5405
6048
|
if (mountedRef.current) setBusyId(null);
|
|
5406
6049
|
}
|
|
5407
6050
|
};
|
|
6051
|
+
const openTransfer = () => {
|
|
6052
|
+
if (transferBusy) return;
|
|
6053
|
+
setTransferError("");
|
|
6054
|
+
setTransferPreview(null);
|
|
6055
|
+
setTransferFileName("");
|
|
6056
|
+
setTransferContent("");
|
|
6057
|
+
setTransferMode("merge");
|
|
6058
|
+
setTransferOpen(true);
|
|
6059
|
+
};
|
|
6060
|
+
const closeTransfer = () => {
|
|
6061
|
+
if (transferBusy) return;
|
|
6062
|
+
setTransferOpen(false);
|
|
6063
|
+
};
|
|
6064
|
+
const runImportPreview = async (content, mode) => {
|
|
6065
|
+
setTransferBusy(true);
|
|
6066
|
+
setTransferError("");
|
|
6067
|
+
try {
|
|
6068
|
+
const value = await request("/import", { content, mode, dryRun: true });
|
|
6069
|
+
if (!mountedRef.current) return;
|
|
6070
|
+
setTransferPreview(value);
|
|
6071
|
+
} catch (error) {
|
|
6072
|
+
if (!mountedRef.current) return;
|
|
6073
|
+
setTransferPreview(null);
|
|
6074
|
+
setTransferError(messageOf(error));
|
|
6075
|
+
} finally {
|
|
6076
|
+
if (mountedRef.current) setTransferBusy(false);
|
|
6077
|
+
}
|
|
6078
|
+
};
|
|
6079
|
+
const handleTransferModeChange = (nextMode) => {
|
|
6080
|
+
if (transferBusy || transferContent === "") return;
|
|
6081
|
+
setTransferMode(nextMode);
|
|
6082
|
+
void runImportPreview(transferContent, nextMode);
|
|
6083
|
+
};
|
|
6084
|
+
const handleExport = async (format) => {
|
|
6085
|
+
if (transferBusy) return;
|
|
6086
|
+
setTransferBusy(true);
|
|
6087
|
+
setTransferError("");
|
|
6088
|
+
try {
|
|
6089
|
+
const value = await request("/export", { format });
|
|
6090
|
+
if (!mountedRef.current) return;
|
|
6091
|
+
setDownload(makeDownload(value.content, value.filename, value.mime));
|
|
6092
|
+
showNotice("success", t("transfer.exported", {
|
|
6093
|
+
count: Number(value.count) || 0,
|
|
6094
|
+
format: String(value.format || format).toUpperCase()
|
|
6095
|
+
}));
|
|
6096
|
+
setTransferOpen(false);
|
|
6097
|
+
} catch (error) {
|
|
6098
|
+
if (!mountedRef.current) return;
|
|
6099
|
+
setTransferError(messageOf(error));
|
|
6100
|
+
} finally {
|
|
6101
|
+
if (mountedRef.current) setTransferBusy(false);
|
|
6102
|
+
}
|
|
6103
|
+
};
|
|
6104
|
+
const handlePickFile = () => {
|
|
6105
|
+
const input = transferInputRef.current;
|
|
6106
|
+
if (input && typeof input.click === "function") input.click();
|
|
6107
|
+
};
|
|
6108
|
+
const handleImportFileChange = async (event) => {
|
|
6109
|
+
const target = event && event.target ? event.target : null;
|
|
6110
|
+
const files = target && target.files ? target.files : null;
|
|
6111
|
+
const file = files && files.length > 0 ? files[0] : null;
|
|
6112
|
+
if (target) target.value = "";
|
|
6113
|
+
if (file === null) return;
|
|
6114
|
+
let content = "";
|
|
6115
|
+
try {
|
|
6116
|
+
content = await readFileText(file);
|
|
6117
|
+
} catch (error) {
|
|
6118
|
+
if (mountedRef.current) setTransferError(t("transfer.readError", { message: messageOf(error) }));
|
|
6119
|
+
return;
|
|
6120
|
+
}
|
|
6121
|
+
if (!mountedRef.current) return;
|
|
6122
|
+
if (content.trim() === "") {
|
|
6123
|
+
setTransferPreview(null);
|
|
6124
|
+
setTransferError(t("transfer.emptyFile"));
|
|
6125
|
+
return;
|
|
6126
|
+
}
|
|
6127
|
+
setTransferFileName(typeof file.name === "string" ? file.name : "");
|
|
6128
|
+
setTransferContent(content);
|
|
6129
|
+
void runImportPreview(content, transferMode);
|
|
6130
|
+
};
|
|
6131
|
+
const handleRestoreBackup = async (backupFile) => {
|
|
6132
|
+
if (typeof backupFile !== "string" || backupFile === "") return;
|
|
6133
|
+
try {
|
|
6134
|
+
const value = await request("/restore", { file: backupFile });
|
|
6135
|
+
if (!mountedRef.current) return;
|
|
6136
|
+
showNotice("success", notifyText(value, "transfer.undone"));
|
|
6137
|
+
void fetchProjectSuggestions(workspaceRef.current, true);
|
|
6138
|
+
await reload(true);
|
|
6139
|
+
} catch (error) {
|
|
6140
|
+
showNotice("error", messageOf(error));
|
|
6141
|
+
}
|
|
6142
|
+
};
|
|
6143
|
+
const handleImportConfirm = async () => {
|
|
6144
|
+
if (transferBusy || transferContent === "") return;
|
|
6145
|
+
setTransferBusy(true);
|
|
6146
|
+
setTransferApplying(true);
|
|
6147
|
+
setTransferError("");
|
|
6148
|
+
try {
|
|
6149
|
+
const value = await request("/import", { content: transferContent, mode: transferMode });
|
|
6150
|
+
if (!mountedRef.current) return;
|
|
6151
|
+
setTransferOpen(false);
|
|
6152
|
+
setTransferPreview(null);
|
|
6153
|
+
setTransferContent("");
|
|
6154
|
+
setTransferFileName("");
|
|
6155
|
+
const backup = value && typeof value.backup === "string" ? value.backup : "";
|
|
6156
|
+
showNotice("success", notifyText(value, "transfer.imported"), backup === "" ? void 0 : {
|
|
6157
|
+
onClick: () => {
|
|
6158
|
+
void handleRestoreBackup(backup);
|
|
6159
|
+
}
|
|
6160
|
+
});
|
|
6161
|
+
void fetchProjectSuggestions(workspaceRef.current, true);
|
|
6162
|
+
await reload(true);
|
|
6163
|
+
} catch (error) {
|
|
6164
|
+
if (!mountedRef.current) return;
|
|
6165
|
+
setTransferError(messageOf(error));
|
|
6166
|
+
} finally {
|
|
6167
|
+
if (mountedRef.current) {
|
|
6168
|
+
setTransferBusy(false);
|
|
6169
|
+
setTransferApplying(false);
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
};
|
|
5408
6173
|
const shiftCalendarMonth = (delta) => {
|
|
5409
6174
|
const total = calendarYear * 12 + calendarMonth + delta;
|
|
5410
6175
|
setCalendarYear(Math.floor(total / 12));
|
|
@@ -5464,6 +6229,7 @@
|
|
|
5464
6229
|
renderActionButtons,
|
|
5465
6230
|
renderDetailContent,
|
|
5466
6231
|
renderFileChips,
|
|
6232
|
+
renderRowCheck,
|
|
5467
6233
|
setBoardActionsId,
|
|
5468
6234
|
setBoardLaneCollapsed,
|
|
5469
6235
|
setBoardShowAllCompleted,
|
|
@@ -5753,6 +6519,15 @@
|
|
|
5753
6519
|
"div",
|
|
5754
6520
|
{ className: "ttl-root", "data-dsh-todo-time-log": "panel", onKeyDown: handlePanelKeyDown },
|
|
5755
6521
|
h(StyleSheet),
|
|
6522
|
+
// 下载锚点:makeDownload() 生成 blob URL 后由 effect 通过 ref 点击。
|
|
6523
|
+
h("a", {
|
|
6524
|
+
ref: downloadRef,
|
|
6525
|
+
className: "ttl-download-anchor",
|
|
6526
|
+
href: download.url === "" ? void 0 : download.url,
|
|
6527
|
+
download: download.name === "" ? void 0 : download.name,
|
|
6528
|
+
"aria-hidden": "true",
|
|
6529
|
+
tabIndex: -1
|
|
6530
|
+
}),
|
|
5756
6531
|
h(
|
|
5757
6532
|
"div",
|
|
5758
6533
|
{ className: "ttl-scroll" },
|
|
@@ -5772,6 +6547,35 @@
|
|
|
5772
6547
|
h(
|
|
5773
6548
|
"div",
|
|
5774
6549
|
{ className: "ttl-heading-actions" },
|
|
6550
|
+
h(
|
|
6551
|
+
"button",
|
|
6552
|
+
{
|
|
6553
|
+
type: "button",
|
|
6554
|
+
className: "ttl-ghost-button",
|
|
6555
|
+
"aria-haspopup": "dialog",
|
|
6556
|
+
"aria-expanded": transferOpen ? "true" : "false",
|
|
6557
|
+
title: t("transfer.open"),
|
|
6558
|
+
onClick: openTransfer
|
|
6559
|
+
},
|
|
6560
|
+
h(
|
|
6561
|
+
"svg",
|
|
6562
|
+
{
|
|
6563
|
+
className: "ttl-ghost-button-icon",
|
|
6564
|
+
width: 12,
|
|
6565
|
+
height: 12,
|
|
6566
|
+
viewBox: "0 0 16 16",
|
|
6567
|
+
fill: "none",
|
|
6568
|
+
stroke: "currentColor",
|
|
6569
|
+
strokeWidth: 1.4,
|
|
6570
|
+
strokeLinecap: "round",
|
|
6571
|
+
strokeLinejoin: "round",
|
|
6572
|
+
"aria-hidden": "true",
|
|
6573
|
+
focusable: "false"
|
|
6574
|
+
},
|
|
6575
|
+
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
|
+
),
|
|
6577
|
+
t("transfer.open")
|
|
6578
|
+
),
|
|
5775
6579
|
h(
|
|
5776
6580
|
"button",
|
|
5777
6581
|
{
|
|
@@ -5954,7 +6758,23 @@
|
|
|
5954
6758
|
"×"
|
|
5955
6759
|
)
|
|
5956
6760
|
)
|
|
5957
|
-
)
|
|
6761
|
+
),
|
|
6762
|
+
h(TransferDialog, {
|
|
6763
|
+
open: transferOpen,
|
|
6764
|
+
busy: transferBusy,
|
|
6765
|
+
applying: transferApplying,
|
|
6766
|
+
fileName: transferFileName,
|
|
6767
|
+
mode: transferMode,
|
|
6768
|
+
preview: transferPreview,
|
|
6769
|
+
error: transferError,
|
|
6770
|
+
fileInputRef: transferInputRef,
|
|
6771
|
+
onClose: closeTransfer,
|
|
6772
|
+
onExport: handleExport,
|
|
6773
|
+
onPickFile: handlePickFile,
|
|
6774
|
+
onFileChange: handleImportFileChange,
|
|
6775
|
+
onModeChange: handleTransferModeChange,
|
|
6776
|
+
onConfirm: handleImportConfirm
|
|
6777
|
+
})
|
|
5958
6778
|
);
|
|
5959
6779
|
}
|
|
5960
6780
|
function createPanel() {
|