dsh-worktodo 0.1.0 → 0.2.0
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/CHANGELOG.md +141 -2
- package/README.en.md +13 -3
- package/README.md +18 -3
- package/lib/client.js +597 -73
- package/lib/client.js.map +4 -4
- package/lib/index.js +107 -1
- package/package.json +15 -25
- package/src/client/index.js +462 -71
- package/src/parse-due.mjs +170 -0
- package/src/records.mjs +131 -16
- package/src/schema.mjs +9 -1
- package/src/service.mjs +58 -3
- package/src/store.mjs +4 -1
- package/src/tools.mjs +137 -4
package/lib/client.js
CHANGED
|
@@ -36,6 +36,139 @@ __export(index_exports, {
|
|
|
36
36
|
});
|
|
37
37
|
module.exports = __toCommonJS(index_exports);
|
|
38
38
|
var import_react = __toESM(require("react"), 1);
|
|
39
|
+
|
|
40
|
+
// src/parse-due.mjs
|
|
41
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
42
|
+
function dayKey(stamp) {
|
|
43
|
+
const d = new Date(stamp);
|
|
44
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
45
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
46
|
+
}
|
|
47
|
+
function weekStartKey(stamp) {
|
|
48
|
+
const d = new Date(stamp);
|
|
49
|
+
const day = (d.getDay() + 6) % 7;
|
|
50
|
+
const monday = new Date(d.getFullYear(), d.getMonth(), d.getDate() - day);
|
|
51
|
+
return dayKey(monday.getTime());
|
|
52
|
+
}
|
|
53
|
+
var CN_NUM = { \u96F6: 0, \u4E00: 1, \u4E24: 2, \u4E8C: 2, \u4E09: 3, \u56DB: 4, \u4E94: 5, \u516D: 6, \u4E03: 7, \u516B: 8, \u4E5D: 9, \u5341: 10 };
|
|
54
|
+
var WEEKDAY = { \u4E00: 1, \u4E8C: 2, \u4E09: 3, \u56DB: 4, \u4E94: 5, \u516D: 6, \u65E5: 7, \u5929: 7 };
|
|
55
|
+
var PERIOD_HOUR = { \u4ECA\u65E9: 9, \u65E9\u4E0A: 9, \u65E9\u6668: 9, \u4E0A\u5348: 9, \u4E2D\u5348: 12, \u4E0B\u5348: 15, \u508D\u665A: 18, \u4ECA\u665A: 20, \u660E\u665A: 20, \u665A\u4E0A: 20, \u665A: 20, \u591C\u91CC: 21 };
|
|
56
|
+
var toCnNumber = (raw) => {
|
|
57
|
+
if (/^\d+$/.test(raw)) return Number(raw);
|
|
58
|
+
if (raw === "\u5341") return 10;
|
|
59
|
+
if (raw.startsWith("\u5341")) return 10 + (CN_NUM[raw[1]] ?? 0);
|
|
60
|
+
if (raw.includes("\u5341")) {
|
|
61
|
+
const [a, b] = raw.split("\u5341");
|
|
62
|
+
return (CN_NUM[a] ?? 1) * 10 + (CN_NUM[b] ?? 0);
|
|
63
|
+
}
|
|
64
|
+
return CN_NUM[raw] ?? null;
|
|
65
|
+
};
|
|
66
|
+
function parseNaturalDue(text, now = Date.now()) {
|
|
67
|
+
const source = typeof text === "string" ? text : "";
|
|
68
|
+
const spans = [];
|
|
69
|
+
let due = null;
|
|
70
|
+
let dueTime = null;
|
|
71
|
+
let hourFromPeriod = null;
|
|
72
|
+
const today = dayKey(now);
|
|
73
|
+
const todayStart = (/* @__PURE__ */ new Date(`${today}T00:00:00`)).getTime();
|
|
74
|
+
let m = /(\d{4})[-/年](\d{1,2})[-/月](\d{1,2})日?/.exec(source);
|
|
75
|
+
if (m) {
|
|
76
|
+
due = dayKey(new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])).getTime());
|
|
77
|
+
spans.push({ start: m.index, end: m.index + m[0].length, kind: "date" });
|
|
78
|
+
} else {
|
|
79
|
+
m = /(\d{1,2})月(\d{1,2})日?/.exec(source);
|
|
80
|
+
if (m) {
|
|
81
|
+
let year = new Date(now).getFullYear();
|
|
82
|
+
let candidate = dayKey(new Date(year, Number(m[1]) - 1, Number(m[2])).getTime());
|
|
83
|
+
if (candidate < today) candidate = dayKey(new Date(year + 1, Number(m[1]) - 1, Number(m[2])).getTime());
|
|
84
|
+
due = candidate;
|
|
85
|
+
spans.push({ start: m.index, end: m.index + m[0].length, kind: "date" });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const weekRe = /(下下|下个|下|本|这)?(?:周|星期|礼拜)([一二三四五六日天1-7])/g;
|
|
89
|
+
for (let w = weekRe.exec(source); w; w = weekRe.exec(source)) {
|
|
90
|
+
const offsetWeeks = w[1] === "\u4E0B" || w[1] === "\u4E0B\u4E2A" ? 1 : w[1] === "\u4E0B\u4E0B" ? 2 : 0;
|
|
91
|
+
const target = WEEKDAY[w[2]] ?? Number(w[2]);
|
|
92
|
+
const thisWeekStart = (/* @__PURE__ */ new Date(`${weekStartKey(now)}T00:00:00`)).getTime();
|
|
93
|
+
let stamp = thisWeekStart + (target - 1) * DAY_MS + offsetWeeks * 7 * DAY_MS;
|
|
94
|
+
if (offsetWeeks === 0 && stamp < todayStart) stamp += 7 * DAY_MS;
|
|
95
|
+
const candidate = dayKey(stamp);
|
|
96
|
+
if (due === null || spans.some((s) => s.kind === "date" && s.weekday)) {
|
|
97
|
+
spans.filter((s) => s.weekday).forEach((s) => {
|
|
98
|
+
s.dropped = true;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
due = candidate;
|
|
102
|
+
spans.push({ start: w.index, end: w.index + w[0].length, kind: "date", weekday: true });
|
|
103
|
+
}
|
|
104
|
+
if (due === null) {
|
|
105
|
+
const relWords = { \u4ECA\u5929: 0, \u4ECA\u65E5: 0, \u660E\u5929: 1, \u660E\u65E5: 1, \u540E\u5929: 2, \u5927\u540E\u5929: 3, \u6628\u5929: -1, \u6628: -1 };
|
|
106
|
+
for (const [word, offset] of Object.entries(relWords)) {
|
|
107
|
+
const at = source.indexOf(word);
|
|
108
|
+
if (at !== -1) {
|
|
109
|
+
due = dayKey(now + offset * DAY_MS);
|
|
110
|
+
spans.push({ start: at, end: at + word.length, kind: "date" });
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (due === null) {
|
|
116
|
+
const plus = /\+(\d{1,3})\s*d(?:ay)?s?/.exec(source);
|
|
117
|
+
const cn = /([0-9零一两二三四五六七八九十]+)\s*天\s*(?:后|以后|之后)/.exec(source);
|
|
118
|
+
if (plus) {
|
|
119
|
+
due = dayKey(now + Number(plus[1]) * DAY_MS);
|
|
120
|
+
spans.push({ start: plus.index, end: plus.index + plus[0].length, kind: "date" });
|
|
121
|
+
} else if (cn) {
|
|
122
|
+
const n = toCnNumber(cn[1]);
|
|
123
|
+
if (n !== null) {
|
|
124
|
+
due = dayKey(now + n * DAY_MS);
|
|
125
|
+
spans.push({ start: cn.index, end: cn.index + cn[0].length, kind: "date" });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const periodRe = /(今晚|明晚|明早|早上|早晨|上午|中午|下午|傍晚|晚上|夜里)/;
|
|
130
|
+
const period = periodRe.exec(source);
|
|
131
|
+
if (period) {
|
|
132
|
+
hourFromPeriod = PERIOD_HOUR[period[1]] ?? null;
|
|
133
|
+
if (due === null) {
|
|
134
|
+
if (period[1] === "\u4ECA\u665A" || period[1] === "\u660E\u665A") due = dayKey(now + (period[1] === "\u660E\u665A" ? 1 : 0) * DAY_MS);
|
|
135
|
+
if (period[1] === "\u660E\u65E9") due = dayKey(now + DAY_MS);
|
|
136
|
+
}
|
|
137
|
+
spans.push({ start: period.index, end: period.index + period[1].length, kind: "time" });
|
|
138
|
+
}
|
|
139
|
+
const hm = /(\d{1,2})[::](\d{2})/.exec(source);
|
|
140
|
+
const cnHour = /([0-9零一两二三四五六七八九十]+)\s*[点时](半|[0-9零一两二三四五六七八九十]{1,3}分)?/.exec(source);
|
|
141
|
+
if (hm) {
|
|
142
|
+
const h = Number(hm[1]);
|
|
143
|
+
if (h <= 23 && Number(hm[2]) <= 59) dueTime = `${String(h).padStart(2, "0")}:${hm[2]}`;
|
|
144
|
+
spans.push({ start: hm.index, end: hm.index + hm[0].length, kind: "time" });
|
|
145
|
+
} else if (cnHour) {
|
|
146
|
+
let h = toCnNumber(cnHour[1]);
|
|
147
|
+
if (h !== null && h <= 23) {
|
|
148
|
+
if (h <= 12 && hourFromPeriod !== null && hourFromPeriod >= 12) h = h === 12 ? 12 : h + 12;
|
|
149
|
+
const minute = cnHour[2] === "\u534A" ? 30 : cnHour[2] ? toCnNumber(cnHour[2].replace("\u5206", "")) ?? 0 : 0;
|
|
150
|
+
if (minute <= 59) dueTime = `${String(h).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
|
151
|
+
spans.push({ start: cnHour.index, end: cnHour.index + cnHour[0].length, kind: "time" });
|
|
152
|
+
}
|
|
153
|
+
} else if (hourFromPeriod !== null) {
|
|
154
|
+
dueTime = `${String(hourFromPeriod).padStart(2, "0")}:00`;
|
|
155
|
+
}
|
|
156
|
+
if (dueTime && !due) due = today;
|
|
157
|
+
const kept = spans.filter((s) => !s.dropped).sort((a, b) => a.start - b.start);
|
|
158
|
+
let title = source;
|
|
159
|
+
for (const span of [...kept].reverse()) {
|
|
160
|
+
title = title.slice(0, span.start) + " " + title.slice(span.end);
|
|
161
|
+
}
|
|
162
|
+
title = title.replace(/\s+/g, " ").trim();
|
|
163
|
+
return {
|
|
164
|
+
title: title || source.trim(),
|
|
165
|
+
due,
|
|
166
|
+
dueTime,
|
|
167
|
+
matched: kept.map((s) => source.slice(s.start, s.end))
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/client/index.js
|
|
39
172
|
var PKG_NAME = true ? "dsh-worktodo" : "dsh-worktodo";
|
|
40
173
|
var DERIVED_NAME = PKG_NAME.replace(/^dsh-/, "");
|
|
41
174
|
var SHORT_NAME = DERIVED_NAME === "worktodo" ? DERIVED_NAME : "worktodo";
|
|
@@ -71,6 +204,26 @@ var zh = {
|
|
|
71
204
|
"groupWeek": "\u672C\u5468",
|
|
72
205
|
"groupLater": "\u4EE5\u540E",
|
|
73
206
|
"groupCompleted": "\u5DF2\u5B8C\u6210",
|
|
207
|
+
"viewDue": "\u6309\u622A\u6B62",
|
|
208
|
+
"viewProject": "\u6309\u9879\u76EE",
|
|
209
|
+
"noProject": "\u672A\u5F52\u7C7B",
|
|
210
|
+
"sortLabel": "\u6392\u5E8F",
|
|
211
|
+
"sortManual": "\u624B\u52A8",
|
|
212
|
+
"sortPriority": "\u4F18\u5148\u7EA7",
|
|
213
|
+
"sortCreated": "\u521B\u5EFA\u65F6\u95F4",
|
|
214
|
+
"dragHint": "\u62D6\u52A8\u8C03\u6574\u987A\u5E8F\uFF08\u4F1A\u81EA\u52A8\u5207\u5230\u624B\u52A8\u6392\u5E8F\uFF09",
|
|
215
|
+
"versionLabel": "\u7248\u672C",
|
|
216
|
+
"versionHint": "\u5F53\u524D\u5B89\u88C5\u7684 dsh-worktodo \u7248\u672C",
|
|
217
|
+
"updateAvailable": "\u6709\u65B0\u7248\u672C",
|
|
218
|
+
"updateHint": "\u66F4\u65B0\u547D\u4EE4\uFF08\u91CD\u542F DSH \u751F\u6548\uFF09",
|
|
219
|
+
"addOpen": "\u65B0\u589E\u5F85\u529E",
|
|
220
|
+
"addCancel": "\u53D6\u6D88",
|
|
221
|
+
"prioNone": "\u65E0",
|
|
222
|
+
"dateUnset": "\u65E5\u671F \xB7 \u672A\u8BBE",
|
|
223
|
+
"timeUnset": "\u65F6\u95F4 \xB7 \u672A\u8BBE",
|
|
224
|
+
"todayWord": "\u4ECA\u5929",
|
|
225
|
+
"fromParsed": "\u8BC6\u522B",
|
|
226
|
+
"dueTimeLabel": "\u65F6\u95F4",
|
|
74
227
|
"expand": "\u5C55\u5F00",
|
|
75
228
|
"collapse": "\u6536\u8D77",
|
|
76
229
|
"empty": "\u8FD8\u6CA1\u6709\u5F85\u529E",
|
|
@@ -129,6 +282,26 @@ var en = {
|
|
|
129
282
|
"groupWeek": "This week",
|
|
130
283
|
"groupLater": "Later",
|
|
131
284
|
"groupCompleted": "Completed",
|
|
285
|
+
"viewDue": "By due",
|
|
286
|
+
"viewProject": "By project",
|
|
287
|
+
"noProject": "Uncategorized",
|
|
288
|
+
"sortLabel": "Sort",
|
|
289
|
+
"sortManual": "Manual",
|
|
290
|
+
"sortPriority": "Priority",
|
|
291
|
+
"sortCreated": "Created",
|
|
292
|
+
"dragHint": "Drag to reorder (switches to manual sort)",
|
|
293
|
+
"versionLabel": "Version",
|
|
294
|
+
"versionHint": "Installed dsh-worktodo version",
|
|
295
|
+
"updateAvailable": "Update available",
|
|
296
|
+
"updateHint": "Update command (restart DSH to apply)",
|
|
297
|
+
"addOpen": "New todo",
|
|
298
|
+
"addCancel": "Cancel",
|
|
299
|
+
"prioNone": "None",
|
|
300
|
+
"dateUnset": "Date \xB7 unset",
|
|
301
|
+
"timeUnset": "Time \xB7 unset",
|
|
302
|
+
"todayWord": "Today",
|
|
303
|
+
"fromParsed": "parsed",
|
|
304
|
+
"dueTimeLabel": "Time",
|
|
132
305
|
"expand": "Expand",
|
|
133
306
|
"collapse": "Collapse",
|
|
134
307
|
"empty": "No to-dos yet",
|
|
@@ -159,6 +332,11 @@ var en = {
|
|
|
159
332
|
"titleEmpty": "Type something first",
|
|
160
333
|
"untitled": "(untitled)"
|
|
161
334
|
};
|
|
335
|
+
function dayKey2(stamp) {
|
|
336
|
+
const d = new Date(stamp);
|
|
337
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
338
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
339
|
+
}
|
|
162
340
|
var C = {
|
|
163
341
|
label: "var(--dsw-alias-label-primary)",
|
|
164
342
|
label2: "var(--dsw-alias-label-secondary)",
|
|
@@ -226,6 +404,32 @@ var STYLE_TEXT = `
|
|
|
226
404
|
flex-direction: column; align-items: stretch; gap: 6px;
|
|
227
405
|
border: 1px solid ${C.border1}; background: ${C.layer1};
|
|
228
406
|
}
|
|
407
|
+
.wt-seg {
|
|
408
|
+
display: inline-flex; align-items: center; border-radius: 8px; overflow: hidden;
|
|
409
|
+
border: 1px solid ${C.border}; background: ${C.bg2};
|
|
410
|
+
}
|
|
411
|
+
.wt-seg-btn {
|
|
412
|
+
appearance: none; border: 0; background: transparent; color: ${C.label2};
|
|
413
|
+
font: inherit; font-size: 11.5px; line-height: 1; padding: 5px 9px; cursor: pointer;
|
|
414
|
+
}
|
|
415
|
+
.wt-seg-btn.is-on { background: ${C.hover}; color: ${C.label}; font-weight: 600; }
|
|
416
|
+
.wt-seg-btn:hover { color: ${C.label}; }
|
|
417
|
+
.wt-row[draggable="true"] { cursor: grab; }
|
|
418
|
+
.wt-row[draggable="true"]:active { cursor: grabbing; }
|
|
419
|
+
.wt-plus {
|
|
420
|
+
width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
421
|
+
padding: 12px 14px; margin-bottom: 10px; cursor: pointer; font: inherit; font-size: 13.5px; font-weight: 600;
|
|
422
|
+
color: ${C.label2}; background: ${C.layer1};
|
|
423
|
+
border: 1px dashed ${C.border}; border-radius: 12px;
|
|
424
|
+
}
|
|
425
|
+
.wt-plus:hover { color: ${C.label}; border-color: ${C.brand}; background: ${tint(C.brand, 8)}; }
|
|
426
|
+
.wt-plus-mark { font-size: 24px; line-height: 1; font-weight: 400; color: ${C.brand}; }
|
|
427
|
+
.wt-flag-btn {
|
|
428
|
+
appearance: none; display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
|
|
429
|
+
font: inherit; font-size: 11.5px; padding: 5px 9px; border-radius: 9px;
|
|
430
|
+
background: transparent; border: 1px solid ${C.border}; color: ${C.label2};
|
|
431
|
+
}
|
|
432
|
+
.wt-flag-btn.is-on { border-color: currentColor; background: color-mix(in srgb, currentColor 12%, transparent); color: inherit; }
|
|
229
433
|
.wt-group-head {
|
|
230
434
|
display: flex; align-items: center; gap: 7px; padding: 3px 8px; border-radius: 8px;
|
|
231
435
|
cursor: pointer; user-select: none; color: ${C.label2};
|
|
@@ -362,12 +566,13 @@ async function postJson(path, body) {
|
|
|
362
566
|
return parsed.data ?? {};
|
|
363
567
|
}
|
|
364
568
|
var lastList = null;
|
|
365
|
-
|
|
569
|
+
var lastSort = "manual";
|
|
570
|
+
function useList(sort) {
|
|
366
571
|
const [state, setState] = import_react.default.useState(() => lastList ? { status: "ready", data: lastList, error: null } : { status: "loading", data: null, error: null });
|
|
367
572
|
const [tick, setTick] = import_react.default.useState(0);
|
|
368
573
|
import_react.default.useEffect(() => {
|
|
369
574
|
let alive = true;
|
|
370
|
-
fetch(`${ROUTE_LIST}?filter=all`, { cache: "no-store", credentials: "same-origin" }).then(async (response) => {
|
|
575
|
+
fetch(`${ROUTE_LIST}?filter=all&sort=${encodeURIComponent(sort || "manual")}`, { cache: "no-store", credentials: "same-origin" }).then(async (response) => {
|
|
371
576
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
372
577
|
const body = await response.json().catch(() => null);
|
|
373
578
|
if (!body?.ok) throw new Error(body?.error ?? body?.reason ?? "\u672A\u77E5\u9519\u8BEF");
|
|
@@ -385,79 +590,257 @@ function useList() {
|
|
|
385
590
|
return () => {
|
|
386
591
|
alive = false;
|
|
387
592
|
};
|
|
388
|
-
}, [tick]);
|
|
593
|
+
}, [tick, sort]);
|
|
389
594
|
const refresh = import_react.default.useCallback(() => setTick((n) => n + 1), []);
|
|
390
595
|
return { ...state, refresh };
|
|
391
596
|
}
|
|
597
|
+
function IconFlag({ filled, dashed }) {
|
|
598
|
+
return import_react.default.createElement("svg", { width: 13, height: 13, viewBox: "0 0 16 16", "aria-hidden": "true" }, [
|
|
599
|
+
import_react.default.createElement("path", {
|
|
600
|
+
key: "pole",
|
|
601
|
+
d: "M3.6 1.6v13",
|
|
602
|
+
stroke: "currentColor",
|
|
603
|
+
strokeWidth: 1.6,
|
|
604
|
+
strokeLinecap: "round",
|
|
605
|
+
fill: "none",
|
|
606
|
+
strokeDasharray: dashed ? "1.5 1.5" : void 0
|
|
607
|
+
}),
|
|
608
|
+
import_react.default.createElement("path", {
|
|
609
|
+
key: "cloth",
|
|
610
|
+
d: "M5.2 2.4h7.4L11 5.2l1.6 2.8H5.2z",
|
|
611
|
+
fill: filled ? "currentColor" : "none",
|
|
612
|
+
stroke: "currentColor",
|
|
613
|
+
strokeWidth: 1.3,
|
|
614
|
+
strokeLinejoin: "round",
|
|
615
|
+
strokeDasharray: dashed ? "1.5 1.5" : void 0
|
|
616
|
+
})
|
|
617
|
+
]);
|
|
618
|
+
}
|
|
619
|
+
var IconCal = () => import_react.default.createElement("svg", { width: 13, height: 13, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.4, "aria-hidden": "true" }, [
|
|
620
|
+
import_react.default.createElement("rect", { key: "r", x: 2.2, y: 3.4, width: 11.6, height: 10.4, rx: 2 }),
|
|
621
|
+
import_react.default.createElement("path", { key: "p", d: "M2.2 6.6h11.6M5.4 2.2v2.4M10.6 2.2v2.4" })
|
|
622
|
+
]);
|
|
623
|
+
var IconClock = () => import_react.default.createElement("svg", { width: 13, height: 13, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.4, "aria-hidden": "true" }, [
|
|
624
|
+
import_react.default.createElement("circle", { key: "c", cx: 8, cy: 8, r: 5.8 }),
|
|
625
|
+
import_react.default.createElement("path", { key: "p", d: "M8 4.9V8l2.2 1.4" })
|
|
626
|
+
]);
|
|
392
627
|
function QuickAdd({ t, onAdd, busy }) {
|
|
393
628
|
const [title, setTitle] = import_react.default.useState("");
|
|
394
629
|
const [due, setDue] = import_react.default.useState("");
|
|
395
|
-
const [
|
|
630
|
+
const [dueTime, setDueTime] = import_react.default.useState("");
|
|
631
|
+
const [priority, setPriority] = import_react.default.useState("none");
|
|
632
|
+
const [parsed, setParsed] = import_react.default.useState(null);
|
|
633
|
+
const [manual, setManual] = import_react.default.useState(false);
|
|
634
|
+
const [flagOpen, setFlagOpen] = import_react.default.useState(false);
|
|
635
|
+
const dateRef = import_react.default.useRef(null);
|
|
636
|
+
const timeRef = import_react.default.useRef(null);
|
|
637
|
+
const openPicker = (ref) => {
|
|
638
|
+
const el = ref.current;
|
|
639
|
+
if (!el) return;
|
|
640
|
+
if (typeof el.showPicker === "function") {
|
|
641
|
+
try {
|
|
642
|
+
el.showPicker();
|
|
643
|
+
return;
|
|
644
|
+
} catch {
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
el.focus();
|
|
648
|
+
try {
|
|
649
|
+
el.click();
|
|
650
|
+
} catch {
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
import_react.default.useEffect(() => {
|
|
654
|
+
if (manual) return void 0;
|
|
655
|
+
const text = title;
|
|
656
|
+
if (!text || !text.trim()) {
|
|
657
|
+
setParsed(null);
|
|
658
|
+
return void 0;
|
|
659
|
+
}
|
|
660
|
+
const timer = setTimeout(() => {
|
|
661
|
+
const result = parseNaturalDue(text, Date.now());
|
|
662
|
+
if (result.matched.length) {
|
|
663
|
+
setParsed(result);
|
|
664
|
+
setDue(result.due ?? "");
|
|
665
|
+
setDueTime(result.dueTime ?? "");
|
|
666
|
+
} else {
|
|
667
|
+
setParsed(null);
|
|
668
|
+
}
|
|
669
|
+
}, 300);
|
|
670
|
+
return () => clearTimeout(timer);
|
|
671
|
+
}, [title, manual]);
|
|
396
672
|
const submit = () => {
|
|
397
673
|
if (busy) return;
|
|
398
|
-
|
|
399
|
-
|
|
674
|
+
let payloadTitle = title;
|
|
675
|
+
if (!manual) {
|
|
676
|
+
const result = parseNaturalDue(title, Date.now());
|
|
677
|
+
if (result.matched.length) payloadTitle = result.title;
|
|
678
|
+
}
|
|
679
|
+
Promise.resolve(onAdd({
|
|
680
|
+
title: payloadTitle,
|
|
681
|
+
due: due || null,
|
|
682
|
+
dueTime: dueTime || null,
|
|
683
|
+
priority
|
|
684
|
+
})).then((ok) => {
|
|
685
|
+
if (ok !== false) {
|
|
686
|
+
setTitle("");
|
|
687
|
+
setParsed(null);
|
|
688
|
+
setManual(false);
|
|
689
|
+
setFlagOpen(false);
|
|
690
|
+
}
|
|
400
691
|
}).catch(() => {
|
|
401
692
|
});
|
|
402
693
|
};
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
694
|
+
const today = dayKey2(Date.now());
|
|
695
|
+
const dateLabel = due ? due === today ? t("todayWord") : `${due.slice(5).replace("-", "-")}` : t("dateUnset");
|
|
696
|
+
const timeLabel = dueTime || t("timeUnset");
|
|
697
|
+
const prioLabel = priority === "high" ? t("prioHigh") : priority === "normal" ? t("prioNormal") : priority === "low" ? t("prioLow") : t("prioNone");
|
|
698
|
+
const prioColor = priority === "high" ? C.error : priority === "normal" ? C.warn : C.label3;
|
|
699
|
+
const flags = [
|
|
700
|
+
{ value: "none", label: t("prioNone"), color: C.label3, dashed: true },
|
|
701
|
+
{ value: "high", label: t("prioHigh"), color: C.error },
|
|
702
|
+
{ value: "normal", label: t("prioNormal"), color: C.warn },
|
|
703
|
+
{ value: "low", label: t("prioLow"), color: C.label3 }
|
|
704
|
+
];
|
|
705
|
+
const setDate = (value) => {
|
|
706
|
+
setManual(true);
|
|
707
|
+
setDue(value);
|
|
708
|
+
if (!value) setDueTime("");
|
|
709
|
+
setFlagOpen(false);
|
|
710
|
+
};
|
|
711
|
+
const setTime = (value) => {
|
|
712
|
+
setManual(true);
|
|
713
|
+
if (value && !due) setDue(dayKey2(Date.now()));
|
|
714
|
+
setDueTime(value);
|
|
715
|
+
};
|
|
716
|
+
return import_react.default.createElement("div", { key: "qa", style: { marginBottom: 12 } }, [
|
|
717
|
+
// ---- 第一行:内容 + 添加 ----
|
|
718
|
+
import_react.default.createElement("div", { key: "l1", style: { display: "flex", alignItems: "center", gap: 8 } }, [
|
|
424
719
|
import_react.default.createElement("input", {
|
|
425
|
-
key: "
|
|
720
|
+
key: "ti",
|
|
426
721
|
className: "wt-input",
|
|
427
|
-
|
|
428
|
-
value:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
722
|
+
style: { flex: "1 1 auto", fontSize: 13, padding: "7px 10px" },
|
|
723
|
+
value: title,
|
|
724
|
+
placeholder: t("addPlaceholder"),
|
|
725
|
+
"aria-label": t("addPlaceholder"),
|
|
726
|
+
onChange: (event) => {
|
|
727
|
+
const next = event.target.value;
|
|
728
|
+
setTitle(next);
|
|
729
|
+
if (!next.trim()) {
|
|
730
|
+
setManual(false);
|
|
731
|
+
setParsed(null);
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
onKeyDown: (event) => {
|
|
735
|
+
if (event.key === "Enter") submit();
|
|
736
|
+
}
|
|
737
|
+
}),
|
|
738
|
+
import_react.default.createElement("button", {
|
|
739
|
+
key: "add",
|
|
740
|
+
type: "button",
|
|
741
|
+
className: "wt-btn wt-btn-primary",
|
|
742
|
+
disabled: Boolean(busy),
|
|
743
|
+
onClick: submit
|
|
744
|
+
}, t("add"))
|
|
432
745
|
]),
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
746
|
+
// ---- 第二行(更小):日期 / 时间 / 优先级 ----
|
|
747
|
+
import_react.default.createElement("div", { key: "l2", style: { display: "flex", alignItems: "center", gap: 10, marginTop: 8, fontSize: 11.5, flexWrap: "wrap" } }, [
|
|
748
|
+
// 日期:label 里套一个透明的原生 date 输入,点它即弹系统日历
|
|
749
|
+
import_react.default.createElement("label", {
|
|
750
|
+
key: "due",
|
|
751
|
+
className: `wt-chip${parsed && parsed.due ? " is-parsed" : ""}`,
|
|
752
|
+
title: t("dueLabel"),
|
|
753
|
+
onClick: (event) => {
|
|
754
|
+
event.preventDefault();
|
|
755
|
+
openPicker(dateRef);
|
|
756
|
+
},
|
|
757
|
+
style: { position: "relative", cursor: "pointer", color: due ? C.label : C.label3, borderColor: parsed && parsed.due ? C.brand : void 0 }
|
|
445
758
|
}, [
|
|
446
|
-
import_react.default.createElement(
|
|
447
|
-
import_react.default.createElement("
|
|
448
|
-
import_react.default.createElement("
|
|
759
|
+
import_react.default.createElement(IconCal, { key: "i" }),
|
|
760
|
+
import_react.default.createElement("span", { key: "v" }, due ? `${today === due ? t("todayWord") : due}${parsed && parsed.due ? ` \xB7 ${t("fromParsed")}` : ""}` : t("dateUnset")),
|
|
761
|
+
import_react.default.createElement("input", {
|
|
762
|
+
key: "in",
|
|
763
|
+
ref: dateRef,
|
|
764
|
+
type: "date",
|
|
765
|
+
value: due,
|
|
766
|
+
"aria-label": t("dueLabel"),
|
|
767
|
+
style: { position: "absolute", inset: 0, opacity: 0, border: 0, padding: 0, width: "100%", height: "100%", cursor: "pointer" },
|
|
768
|
+
onChange: (event) => setDate(event.target.value)
|
|
769
|
+
})
|
|
770
|
+
]),
|
|
771
|
+
// 时间:同样用原生 time 输入;只选时间时自动补今天
|
|
772
|
+
import_react.default.createElement("label", {
|
|
773
|
+
key: "time",
|
|
774
|
+
className: `wt-chip${parsed && parsed.dueTime ? " is-parsed" : ""}`,
|
|
775
|
+
title: t("dueTimeLabel"),
|
|
776
|
+
onClick: (event) => {
|
|
777
|
+
event.preventDefault();
|
|
778
|
+
openPicker(timeRef);
|
|
779
|
+
},
|
|
780
|
+
style: { position: "relative", cursor: "pointer", color: dueTime ? C.label : C.label3, borderColor: parsed && parsed.dueTime ? C.brand : void 0 }
|
|
781
|
+
}, [
|
|
782
|
+
import_react.default.createElement(IconClock, { key: "i" }),
|
|
783
|
+
import_react.default.createElement("span", { key: "v" }, `${timeLabel}${parsed && parsed.dueTime ? ` \xB7 ${t("fromParsed")}` : ""}`),
|
|
784
|
+
import_react.default.createElement("input", {
|
|
785
|
+
key: "in",
|
|
786
|
+
ref: timeRef,
|
|
787
|
+
type: "time",
|
|
788
|
+
value: dueTime,
|
|
789
|
+
lang: "zh-CN",
|
|
790
|
+
"aria-label": t("dueTimeLabel"),
|
|
791
|
+
style: { position: "absolute", inset: 0, opacity: 0, border: 0, padding: 0, width: "100%", height: "100%", cursor: "pointer" },
|
|
792
|
+
onChange: (event) => setTime(event.target.value)
|
|
793
|
+
})
|
|
794
|
+
]),
|
|
795
|
+
// 优先级:点开四个小旗
|
|
796
|
+
import_react.default.createElement("span", { key: "prio", style: { position: "relative" } }, [
|
|
797
|
+
import_react.default.createElement("button", {
|
|
798
|
+
key: "btn",
|
|
799
|
+
type: "button",
|
|
800
|
+
className: "wt-chip",
|
|
801
|
+
style: { color: prioColor, cursor: "pointer", font: "inherit", fontSize: 11.5 },
|
|
802
|
+
title: t("priorityLabel"),
|
|
803
|
+
"aria-expanded": flagOpen,
|
|
804
|
+
onClick: () => setFlagOpen((open) => !open)
|
|
805
|
+
}, [
|
|
806
|
+
import_react.default.createElement(IconFlag, { key: "f", dashed: priority === "none", filled: priority !== "none" }),
|
|
807
|
+
import_react.default.createElement("span", { key: "v" }, `${t("priorityLabel")} \xB7 ${prioLabel}`)
|
|
808
|
+
]),
|
|
809
|
+
flagOpen ? import_react.default.createElement("div", {
|
|
810
|
+
key: "pop",
|
|
811
|
+
style: {
|
|
812
|
+
position: "absolute",
|
|
813
|
+
top: "100%",
|
|
814
|
+
left: 0,
|
|
815
|
+
zIndex: 5,
|
|
816
|
+
marginTop: 4,
|
|
817
|
+
padding: 6,
|
|
818
|
+
display: "flex",
|
|
819
|
+
gap: 4,
|
|
820
|
+
background: C.layer1,
|
|
821
|
+
border: `1px solid ${C.border}`,
|
|
822
|
+
borderRadius: 10,
|
|
823
|
+
boxShadow: "0 8px 24px rgba(0,0,0,.12)"
|
|
824
|
+
}
|
|
825
|
+
}, flags.map((flag) => import_react.default.createElement("button", {
|
|
826
|
+
key: flag.value,
|
|
827
|
+
type: "button",
|
|
828
|
+
className: "wt-chip",
|
|
829
|
+
style: { color: flag.color, font: "inherit", fontSize: 11.5, cursor: "pointer", borderColor: priority === flag.value ? flag.color : void 0 },
|
|
830
|
+
title: flag.label,
|
|
831
|
+
onClick: () => {
|
|
832
|
+
setPriority(flag.value);
|
|
833
|
+
setFlagOpen(false);
|
|
834
|
+
}
|
|
835
|
+
}, [
|
|
836
|
+
import_react.default.createElement(IconFlag, { key: "f", filled: priority === flag.value, dashed: flag.value === "none" }),
|
|
837
|
+
import_react.default.createElement("span", { key: "v" }, flag.label)
|
|
838
|
+
]))) : null
|
|
449
839
|
])
|
|
450
|
-
])
|
|
451
|
-
import_react.default.createElement("button", {
|
|
452
|
-
key: "add",
|
|
453
|
-
type: "button",
|
|
454
|
-
className: "wt-btn wt-btn-primary",
|
|
455
|
-
disabled: Boolean(busy),
|
|
456
|
-
onClick: submit
|
|
457
|
-
}, t("add"))
|
|
840
|
+
])
|
|
458
841
|
]);
|
|
459
842
|
}
|
|
460
|
-
function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
843
|
+
function TodoRow({ todo, today, t, onToggle, onRemove, onSave, drag }) {
|
|
461
844
|
const [editing, setEditing] = import_react.default.useState(false);
|
|
462
845
|
const [draft, setDraft] = import_react.default.useState(null);
|
|
463
846
|
const [notesOpen, setNotesOpen] = import_react.default.useState(false);
|
|
@@ -477,6 +860,7 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
|
477
860
|
due,
|
|
478
861
|
priority: row.priority === "high" || row.priority === "low" ? row.priority : "normal",
|
|
479
862
|
project: typeof row.project === "string" ? row.project : "",
|
|
863
|
+
dueTime: typeof row.dueTime === "string" ? row.dueTime : "",
|
|
480
864
|
notes: typeof row.notes === "string" ? row.notes : ""
|
|
481
865
|
});
|
|
482
866
|
setNotesOpen(Boolean(row.notes));
|
|
@@ -492,7 +876,12 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
|
492
876
|
const title = String(draft.title ?? "").trim();
|
|
493
877
|
if (title !== (row.title ?? "")) patch.title = title;
|
|
494
878
|
const nextDue = draft.due || "";
|
|
495
|
-
if (nextDue !== due)
|
|
879
|
+
if (nextDue !== due) {
|
|
880
|
+
patch.due = nextDue;
|
|
881
|
+
if (!nextDue) patch.dueTime = "none";
|
|
882
|
+
}
|
|
883
|
+
const nextDueTime = String(draft.dueTime ?? "").trim();
|
|
884
|
+
if (nextDue && nextDueTime !== (row.dueTime ?? "")) patch.dueTime = nextDueTime === "" ? "none" : nextDueTime;
|
|
496
885
|
const priority = draft.priority || "normal";
|
|
497
886
|
if (priority !== (row.priority ?? "normal")) patch.priority = priority;
|
|
498
887
|
const project = String(draft.project ?? "").trim();
|
|
@@ -551,6 +940,15 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
|
551
940
|
"aria-label": t("dueLabel"),
|
|
552
941
|
onChange: (event) => setDraft((prev) => ({ ...prev, due: event.target.value }))
|
|
553
942
|
})),
|
|
943
|
+
field("tm", t("dueTimeLabel"), import_react.default.createElement("input", {
|
|
944
|
+
className: "wt-input",
|
|
945
|
+
style: inputStyle,
|
|
946
|
+
type: "time",
|
|
947
|
+
value: draft.dueTime,
|
|
948
|
+
disabled: !draft.due,
|
|
949
|
+
"aria-label": t("dueTimeLabel"),
|
|
950
|
+
onChange: (event) => setDraft((prev) => ({ ...prev, dueTime: event.target.value }))
|
|
951
|
+
})),
|
|
554
952
|
field("prio", t("priorityLabel"), import_react.default.createElement("select", {
|
|
555
953
|
className: "wt-input",
|
|
556
954
|
style: inputStyle,
|
|
@@ -627,7 +1025,39 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
|
627
1025
|
title: row.source === "agent" ? t("sourceAgent") : t("sourceSession")
|
|
628
1026
|
}, row.source === "agent" ? t("sourceAgent") : t("sourceSession")));
|
|
629
1027
|
}
|
|
630
|
-
|
|
1028
|
+
const dragAttrs = drag ? {
|
|
1029
|
+
draggable: true,
|
|
1030
|
+
onDragStart: (event) => {
|
|
1031
|
+
try {
|
|
1032
|
+
event.dataTransfer?.setData("text/plain", row.id ?? "");
|
|
1033
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
|
1034
|
+
} catch {
|
|
1035
|
+
}
|
|
1036
|
+
drag.onStart?.(row.id);
|
|
1037
|
+
},
|
|
1038
|
+
onDragOver: (event) => {
|
|
1039
|
+
event.preventDefault();
|
|
1040
|
+
drag.onOver?.(row.id);
|
|
1041
|
+
},
|
|
1042
|
+
onDrop: (event) => {
|
|
1043
|
+
event.preventDefault();
|
|
1044
|
+
drag.onDrop?.(row.id);
|
|
1045
|
+
},
|
|
1046
|
+
onDragEnd: () => drag.onEnd?.(),
|
|
1047
|
+
title: drag.hint,
|
|
1048
|
+
style: drag.dragging ? { opacity: 0.45 } : void 0
|
|
1049
|
+
} : {};
|
|
1050
|
+
return import_react.default.createElement("div", { className: "wt-row", ...dragAttrs, style: row.priority === "high" ? { borderLeft: `2px solid ${C.error}`, paddingLeft: 8 } : void 0 }, [
|
|
1051
|
+
/* 优先级呈现(设计稿 v5):高=红色左缘 + 实心红旗;中=琥珀;低=灰;无=什么都不显示 */
|
|
1052
|
+
row.priority && row.priority !== "none" ? import_react.default.createElement("span", {
|
|
1053
|
+
key: "prio-flag",
|
|
1054
|
+
title: row.priority === "high" ? t("prioHigh") : row.priority === "normal" ? t("prioNormal") : t("prioLow"),
|
|
1055
|
+
style: {
|
|
1056
|
+
display: "inline-flex",
|
|
1057
|
+
flex: "0 0 auto",
|
|
1058
|
+
color: row.priority === "high" ? C.error : row.priority === "normal" ? C.warn : C.label3
|
|
1059
|
+
}
|
|
1060
|
+
}, import_react.default.createElement(IconFlag, { filled: row.priority === "high" })) : null,
|
|
631
1061
|
import_react.default.createElement("button", {
|
|
632
1062
|
key: "check",
|
|
633
1063
|
type: "button",
|
|
@@ -674,7 +1104,7 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
|
674
1104
|
}, confirming ? t("confirmDelete") : import_react.default.createElement(IconTrash, { key: "i" }))
|
|
675
1105
|
]);
|
|
676
1106
|
}
|
|
677
|
-
function Group({ label, items, closed, t, today, onToggleOpen, onToggleTodo, onRemove, onSave }) {
|
|
1107
|
+
function Group({ label, items, meta, closed, t, today, onToggleOpen, onToggleTodo, onRemove, onSave, drag }) {
|
|
678
1108
|
const list = asArray(items);
|
|
679
1109
|
if (!list.length) return null;
|
|
680
1110
|
return import_react.default.createElement("div", { style: { marginTop: 6 } }, [
|
|
@@ -688,7 +1118,7 @@ function Group({ label, items, closed, t, today, onToggleOpen, onToggleTodo, onR
|
|
|
688
1118
|
}, [
|
|
689
1119
|
import_react.default.createElement(IconChevron, { key: "c", open: !closed }),
|
|
690
1120
|
import_react.default.createElement("span", { key: "l", style: { fontWeight: 600, fontSize: 12 } }, label),
|
|
691
|
-
import_react.default.createElement("span", { key: "n", style: { fontSize: 11, color: C.label3 } }, String(list.length)),
|
|
1121
|
+
import_react.default.createElement("span", { key: "n", style: { fontSize: 11, color: C.label3 } }, meta || String(list.length)),
|
|
692
1122
|
import_react.default.createElement("span", { key: "sp", style: { flex: 1 } }),
|
|
693
1123
|
import_react.default.createElement("span", { key: "h", style: { fontSize: 11, color: C.label3 } }, closed ? t("expand") : t("collapse"))
|
|
694
1124
|
]),
|
|
@@ -699,7 +1129,15 @@ function Group({ label, items, closed, t, today, onToggleOpen, onToggleTodo, onR
|
|
|
699
1129
|
t,
|
|
700
1130
|
onToggle: onToggleTodo,
|
|
701
1131
|
onRemove,
|
|
702
|
-
onSave
|
|
1132
|
+
onSave,
|
|
1133
|
+
drag: drag ? {
|
|
1134
|
+
dragging: drag.dragId === todo?.id,
|
|
1135
|
+
hint: drag.hint,
|
|
1136
|
+
onStart: drag.onStart,
|
|
1137
|
+
onOver: drag.onOver,
|
|
1138
|
+
onDrop: drag.onDrop,
|
|
1139
|
+
onEnd: drag.onEnd
|
|
1140
|
+
} : void 0
|
|
703
1141
|
})))
|
|
704
1142
|
]);
|
|
705
1143
|
}
|
|
@@ -747,7 +1185,13 @@ var GROUPS = [
|
|
|
747
1185
|
];
|
|
748
1186
|
function WorkTodoPanel(props) {
|
|
749
1187
|
const t = typeof props?.t === "function" ? props.t : (key) => zh[key] ?? key;
|
|
750
|
-
const
|
|
1188
|
+
const [sort, setSortState] = import_react.default.useState(lastSort);
|
|
1189
|
+
const setSort = import_react.default.useCallback((next) => {
|
|
1190
|
+
lastSort = next;
|
|
1191
|
+
setSortState(next);
|
|
1192
|
+
}, []);
|
|
1193
|
+
const [dragId, setDragId] = import_react.default.useState(null);
|
|
1194
|
+
const { status, data, error, refresh } = useList(sort);
|
|
751
1195
|
const [barError, setBarError] = import_react.default.useState(null);
|
|
752
1196
|
const [dismissedReadError, setDismissedReadError] = import_react.default.useState(null);
|
|
753
1197
|
const readError = error && error !== dismissedReadError ? error : null;
|
|
@@ -755,6 +1199,7 @@ function WorkTodoPanel(props) {
|
|
|
755
1199
|
const [notice, setNotice] = import_react.default.useState(null);
|
|
756
1200
|
const [pending, setPending] = import_react.default.useState(0);
|
|
757
1201
|
const [closed, setClosed] = import_react.default.useState({ completed: true });
|
|
1202
|
+
const [view, setView] = import_react.default.useState("due");
|
|
758
1203
|
const runWrite = import_react.default.useCallback(async (job) => {
|
|
759
1204
|
setPending((n) => n + 1);
|
|
760
1205
|
try {
|
|
@@ -784,6 +1229,30 @@ function WorkTodoPanel(props) {
|
|
|
784
1229
|
const handleToggle = import_react.default.useCallback((ref) => runWrite(() => postJson("/status", { ref, toggle: true })), [runWrite]);
|
|
785
1230
|
const handleRemove = import_react.default.useCallback((ref) => runWrite(() => postJson("/remove", { ref })), [runWrite]);
|
|
786
1231
|
const handleSave = import_react.default.useCallback((ref, patch) => runWrite(() => postJson("/update", { ref, ...patch })), [runWrite]);
|
|
1232
|
+
const handleDropOn = import_react.default.useCallback((targetId) => {
|
|
1233
|
+
const dragging = dragId;
|
|
1234
|
+
setDragId(null);
|
|
1235
|
+
if (!dragging || !targetId || dragging === targetId) return Promise.resolve(false);
|
|
1236
|
+
const ids = asArray(data?.todos).map((todo) => todo?.id).filter(Boolean);
|
|
1237
|
+
const from = ids.indexOf(dragging);
|
|
1238
|
+
if (from === -1) return Promise.resolve(false);
|
|
1239
|
+
ids.splice(from, 1);
|
|
1240
|
+
const to = ids.indexOf(targetId);
|
|
1241
|
+
ids.splice(to === -1 ? ids.length : to, 0, dragging);
|
|
1242
|
+
return runWrite(() => postJson("/reorder", { ids })).then((result) => result.ok);
|
|
1243
|
+
}, [dragId, data, runWrite]);
|
|
1244
|
+
const dragHandlers = import_react.default.useMemo(() => ({
|
|
1245
|
+
dragId,
|
|
1246
|
+
hint: t("dragHint"),
|
|
1247
|
+
onStart: (id) => {
|
|
1248
|
+
setDragId(id);
|
|
1249
|
+
if (sort !== "manual") setSort("manual");
|
|
1250
|
+
},
|
|
1251
|
+
onOver: () => {
|
|
1252
|
+
},
|
|
1253
|
+
onDrop: (id) => handleDropOn(id),
|
|
1254
|
+
onEnd: () => setDragId(null)
|
|
1255
|
+
}), [dragId, sort, t, setSort, handleDropOn]);
|
|
787
1256
|
const handleAdopt = import_react.default.useCallback((sessionTodos) => runWrite(() => postJson("/adopt", { todos: sessionTodos })).then((result) => {
|
|
788
1257
|
if (result.ok) {
|
|
789
1258
|
const created = Array.isArray(result.data?.created) ? result.data.created.length : 0;
|
|
@@ -832,9 +1301,18 @@ function WorkTodoPanel(props) {
|
|
|
832
1301
|
const stats = data?.stats ?? {};
|
|
833
1302
|
const today = typeof data?.today === "string" && data.today ? data.today : localToday();
|
|
834
1303
|
const store = data?.store ?? null;
|
|
1304
|
+
const version = typeof data?.version === "string" ? data.version : "";
|
|
1305
|
+
const update = data?.update ?? null;
|
|
835
1306
|
const num = (value) => Number.isFinite(Number(value)) ? Number(value) : 0;
|
|
836
1307
|
const overdueCount = num(stats.overdue);
|
|
837
|
-
const
|
|
1308
|
+
const projectGroups = asArray(data?.projectGroups);
|
|
1309
|
+
const viewGroups = view === "project" ? projectGroups.map((bucket) => ({
|
|
1310
|
+
key: `p:${bucket?.key ?? ""}`,
|
|
1311
|
+
label: bucket?.project ? String(bucket.project) : t("noProject"),
|
|
1312
|
+
meta: `${num(bucket?.open)}/${num(bucket?.total)}${num(bucket?.overdue) ? ` \xB7 ${t("statOverdue")} ${num(bucket.overdue)}` : ""}`,
|
|
1313
|
+
items: bucket?.todos
|
|
1314
|
+
})) : GROUPS.map((group) => ({ key: group.key, label: t(group.label), items: groups[group.key] }));
|
|
1315
|
+
const totalVisible = viewGroups.reduce((sum, group) => sum + asArray(group.items).length, 0);
|
|
838
1316
|
const chips = [
|
|
839
1317
|
{ key: "open", label: t("statOpen"), value: num(stats.open) },
|
|
840
1318
|
{ key: "overdue", label: t("statOverdue"), value: overdueCount, alert: overdueCount > 0 },
|
|
@@ -858,6 +1336,31 @@ function WorkTodoPanel(props) {
|
|
|
858
1336
|
import_react.default.createElement("span", { key: "l", style: { color: C.label3 } }, chip.label),
|
|
859
1337
|
import_react.default.createElement("b", { key: "v", style: { fontWeight: 650 } }, String(chip.value))
|
|
860
1338
|
])),
|
|
1339
|
+
import_react.default.createElement("span", { key: "sort", className: "wt-seg", title: t("sortLabel") }, [
|
|
1340
|
+
...["manual", "priority", "created"].map((mode) => import_react.default.createElement("button", {
|
|
1341
|
+
key: mode,
|
|
1342
|
+
type: "button",
|
|
1343
|
+
className: `wt-seg-btn${sort === mode ? " is-on" : ""}`,
|
|
1344
|
+
title: mode === "manual" ? t("dragHint") : `${t("sortLabel")}\uFF1A${t(`sort${mode === "manual" ? "Manual" : mode === "priority" ? "Priority" : "Created"}`)}`,
|
|
1345
|
+
onClick: () => setSort(mode)
|
|
1346
|
+
}, t(`sort${mode === "manual" ? "Manual" : mode === "priority" ? "Priority" : "Created"}`)))
|
|
1347
|
+
]),
|
|
1348
|
+
import_react.default.createElement("span", { key: "view", className: "wt-seg" }, [
|
|
1349
|
+
import_react.default.createElement("button", {
|
|
1350
|
+
key: "due",
|
|
1351
|
+
type: "button",
|
|
1352
|
+
className: `wt-seg-btn${view === "due" ? " is-on" : ""}`,
|
|
1353
|
+
title: t("viewDue"),
|
|
1354
|
+
onClick: () => setView("due")
|
|
1355
|
+
}, t("viewDue")),
|
|
1356
|
+
import_react.default.createElement("button", {
|
|
1357
|
+
key: "proj",
|
|
1358
|
+
type: "button",
|
|
1359
|
+
className: `wt-seg-btn${view === "project" ? " is-on" : ""}`,
|
|
1360
|
+
title: t("viewProject"),
|
|
1361
|
+
onClick: () => setView("project")
|
|
1362
|
+
}, t("viewProject"))
|
|
1363
|
+
]),
|
|
861
1364
|
import_react.default.createElement("span", { key: "sp", style: { flex: 1 } }),
|
|
862
1365
|
import_react.default.createElement("button", {
|
|
863
1366
|
key: "rf",
|
|
@@ -938,17 +1441,19 @@ function WorkTodoPanel(props) {
|
|
|
938
1441
|
}) : null,
|
|
939
1442
|
import_react.default.createElement(QuickAdd, { key: "quick", t, busy: pending > 0, onAdd: handleAdd }),
|
|
940
1443
|
/* 分组列表 */
|
|
941
|
-
totalVisible ? import_react.default.createElement("div", { key: "groups" },
|
|
1444
|
+
totalVisible ? import_react.default.createElement("div", { key: "groups" }, viewGroups.map((group) => import_react.default.createElement(Group, {
|
|
942
1445
|
key: group.key,
|
|
943
|
-
label:
|
|
944
|
-
|
|
1446
|
+
label: group.label,
|
|
1447
|
+
meta: group.meta,
|
|
1448
|
+
items: group.items,
|
|
945
1449
|
closed: Boolean(closed[group.key]),
|
|
946
1450
|
t,
|
|
947
1451
|
today,
|
|
948
1452
|
onToggleOpen: () => setClosed((prev) => ({ ...prev, [group.key]: !prev[group.key] })),
|
|
949
1453
|
onToggleTodo: handleToggle,
|
|
950
1454
|
onRemove: handleRemove,
|
|
951
|
-
onSave: handleSave
|
|
1455
|
+
onSave: handleSave,
|
|
1456
|
+
drag: dragHandlers
|
|
952
1457
|
}))) : import_react.default.createElement("div", {
|
|
953
1458
|
key: "empty",
|
|
954
1459
|
style: { padding: "22px 12px", textAlign: "center", color: C.label3 }
|
|
@@ -960,11 +1465,30 @@ function WorkTodoPanel(props) {
|
|
|
960
1465
|
import_react.default.createElement("div", {
|
|
961
1466
|
key: "foot",
|
|
962
1467
|
style: { marginTop: 14, paddingTop: 8, borderTop: `1px solid ${C.border1}`, fontSize: 10.5, color: C.label3, lineHeight: 1.7 }
|
|
963
|
-
},
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1468
|
+
}, [
|
|
1469
|
+
/* 角落信息:装了哪一版 + 有没有新版(一眼可见,不占主区) */
|
|
1470
|
+
import_react.default.createElement("div", {
|
|
1471
|
+
key: "ver",
|
|
1472
|
+
style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap", marginBottom: 2 }
|
|
1473
|
+
}, [
|
|
1474
|
+
import_react.default.createElement("span", {
|
|
1475
|
+
key: "v",
|
|
1476
|
+
title: t("versionHint"),
|
|
1477
|
+
style: { color: C.label3 }
|
|
1478
|
+
}, `${t("versionLabel")} ${version ? `v${version}` : "\u2014"}`),
|
|
1479
|
+
update?.hasUpdate ? import_react.default.createElement("span", {
|
|
1480
|
+
key: "u",
|
|
1481
|
+
className: "wt-chip",
|
|
1482
|
+
style: { fontSize: 10, color: C.ok, borderColor: tint(C.ok, 50), background: tint(C.ok, 12) },
|
|
1483
|
+
title: `${t("updateHint")}\uFF1Adsh plugin --profile desktop add dsh-worktodo@${update.latest}`
|
|
1484
|
+
}, `${t("updateAvailable")} v${update.latest}`) : null
|
|
1485
|
+
]),
|
|
1486
|
+
store && store.location ? import_react.default.createElement("div", {
|
|
1487
|
+
key: "store",
|
|
1488
|
+
title: `${t("storeAt")}\uFF1A${store.location}`,
|
|
1489
|
+
style: { wordBreak: "break-all" }
|
|
1490
|
+
}, `${t("storeAt")} \xB7 ${store.kind === "domain" ? t("storeDomain") : t("storeFile")} \xB7 ${store.location}`) : null
|
|
1491
|
+
])
|
|
968
1492
|
]);
|
|
969
1493
|
}
|
|
970
1494
|
var inject = ["slots", "locale"];
|