dsh-task-ledger 0.0.46 → 0.0.48
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/dist/client.js +318 -189
- package/dist/client.js.map +1 -1
- package/dist/host/repositories/cache.d.ts +27 -0
- package/dist/host/repositories/cache.js +57 -0
- package/dist/host/repositories/ledger.repository.d.ts +2 -2
- package/dist/host/repositories/ledger.repository.js +19 -10
- package/dist/host/routes.d.ts +17 -0
- package/dist/host/routes.js +213 -64
- package/dist/host/services/session.service.d.ts +110 -0
- package/dist/host/services/session.service.js +145 -0
- package/dist/host/skill/registration.d.ts +2 -0
- package/dist/host/skill/registration.js +2 -0
- package/dist/host/workspace.d.ts +6 -0
- package/dist/index.js +30 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -10,6 +10,8 @@ window.__ModuleLoader__.load({
|
|
|
10
10
|
/** host 路由前缀(全仓唯一;与 host/routes.ts 的 API_PREFIX 对应)。 */
|
|
11
11
|
const API_PREFIX = "api/task-ledger";
|
|
12
12
|
const DEFAULT_TIMEOUT_MS = 8e3;
|
|
13
|
+
/** 趋势端点读 history,比快路径慢一个量级(20 万条实测 4s+),超时单给。 */
|
|
14
|
+
const TREND_TIMEOUT_MS = 3e4;
|
|
13
15
|
/**
|
|
14
16
|
* 错误响应榨人话:host 路由(routes.ts)发的是 `{ error: "..." }`,那里的话是**能直接上界面的**
|
|
15
17
|
* (带工作区名、python3 起不来之类的原文)。不带它界面上只剩「HTTP 500」,等于没报错。
|
|
@@ -40,9 +42,24 @@ window.__ModuleLoader__.load({
|
|
|
40
42
|
clearTimeout(timer);
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* 首页快照(快路径:在办全量 + 统计 + 各工作区身份;**不含趋势**)。
|
|
47
|
+
* `fresh` = 让 host 先清 1.5s 读缓存再算(手动刷新 / 首读都要最新,不吃上一屏的缓存)。
|
|
48
|
+
*/
|
|
49
|
+
function fetchBoardSnapshot(opts = {}) {
|
|
50
|
+
return fetchJson(`/board/snapshot${freshSuffix(opts)}`);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 趋势(慢路径:近 8 周 + 四个自然周期,host 侧要读 history)。
|
|
54
|
+
* 超时给得比默认宽:历史量大时读得久(实测 20 万条量级 4s+);
|
|
55
|
+
* 旧宿主没有该端点 ⇒ 404,直接抛给调用方兜底,不阻塞列表。
|
|
56
|
+
*/
|
|
57
|
+
function fetchTrend(opts = {}) {
|
|
58
|
+
return fetchJson(`/board/trend${freshSuffix(opts)}`, {}, TREND_TIMEOUT_MS);
|
|
59
|
+
}
|
|
60
|
+
/** `?fresh=1` 只在真要最新值时带;不带就吃 host 的 1.5s 缓存。 */
|
|
61
|
+
function freshSuffix(opts) {
|
|
62
|
+
return opts.fresh === true ? "?fresh=1" : "";
|
|
46
63
|
}
|
|
47
64
|
//#endregion
|
|
48
65
|
//#region src/client/store.ts
|
|
@@ -66,7 +83,10 @@ window.__ModuleLoader__.load({
|
|
|
66
83
|
keyword: "",
|
|
67
84
|
othersKeyword: ""
|
|
68
85
|
},
|
|
69
|
-
refreshing: false
|
|
86
|
+
refreshing: false,
|
|
87
|
+
trend: null,
|
|
88
|
+
trendStatus: "idle",
|
|
89
|
+
trendError: null
|
|
70
90
|
};
|
|
71
91
|
const listeners = /* @__PURE__ */ new Set();
|
|
72
92
|
/** 首读只触发一次(谁先订阅谁触发),之后一律靠 `refresh()` / `setQuery()`。 */
|
|
@@ -96,17 +116,24 @@ window.__ModuleLoader__.load({
|
|
|
96
116
|
listeners.delete(listener);
|
|
97
117
|
};
|
|
98
118
|
}
|
|
99
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* 重读(2026-10-18 拆端点):**两段**——先快路径 `/board/snapshot`(只读在办,首屏立即可渲染),
|
|
121
|
+
* 再慢路径 `/board/trend`(读 history,后台拉;迟到按 token 丢弃,不盖新快照)。
|
|
122
|
+
* 趋势失败(含旧宿主 404)不阻塞列表:trendStatus 置 error,图表区显示兜底文案。
|
|
123
|
+
* 两段都带 `fresh=1`:本函数只由首读与手动刷新触发,都要最新值(host 缓存清两次无害,清是全局的)。
|
|
124
|
+
*/
|
|
100
125
|
async function refresh() {
|
|
101
126
|
const token = ++runToken;
|
|
102
127
|
set({ refreshing: true });
|
|
103
128
|
try {
|
|
104
|
-
const board = await
|
|
129
|
+
const board = await fetchBoardSnapshot({ fresh: true });
|
|
105
130
|
if (token !== runToken) return;
|
|
106
131
|
set({
|
|
107
132
|
status: "ready",
|
|
108
133
|
board,
|
|
109
|
-
error: null
|
|
134
|
+
error: null,
|
|
135
|
+
trendStatus: "loading",
|
|
136
|
+
trendError: null
|
|
110
137
|
});
|
|
111
138
|
} catch (error) {
|
|
112
139
|
if (token !== runToken) return;
|
|
@@ -114,9 +141,26 @@ window.__ModuleLoader__.load({
|
|
|
114
141
|
status: "error",
|
|
115
142
|
error: error instanceof Error ? error.message : String(error)
|
|
116
143
|
});
|
|
144
|
+
return;
|
|
117
145
|
} finally {
|
|
118
146
|
if (token === runToken) set({ refreshing: false });
|
|
119
147
|
}
|
|
148
|
+
try {
|
|
149
|
+
const trend = await fetchTrend({ fresh: true });
|
|
150
|
+
if (token !== runToken) return;
|
|
151
|
+
set({
|
|
152
|
+
trend,
|
|
153
|
+
trendStatus: "ready",
|
|
154
|
+
trendError: null
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (token !== runToken) return;
|
|
158
|
+
set({
|
|
159
|
+
trend: null,
|
|
160
|
+
trendStatus: "error",
|
|
161
|
+
trendError: error instanceof Error ? error.message : String(error)
|
|
162
|
+
});
|
|
163
|
+
}
|
|
120
164
|
}
|
|
121
165
|
/** 改筛选:全部在浏览器里完成(快照已含全部在办),不发请求。 */
|
|
122
166
|
function setQuery(patch) {
|
|
@@ -140,7 +184,25 @@ window.__ModuleLoader__.load({
|
|
|
140
184
|
if (identity === null || identity === "") return false;
|
|
141
185
|
return task.owner === identity && (task.status === "doing" || task.status === "blocked");
|
|
142
186
|
}
|
|
143
|
-
|
|
187
|
+
function resolveActions(task, board) {
|
|
188
|
+
const mine = isMine(task, board);
|
|
189
|
+
return {
|
|
190
|
+
view: true,
|
|
191
|
+
claim: !mine && task.status === "open",
|
|
192
|
+
resume: mine,
|
|
193
|
+
finish: mine,
|
|
194
|
+
release: mine
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** 已结案行(统计 tab):只有查看,没有在办动作。 */
|
|
198
|
+
const CLOSED_ACTIONS = {
|
|
199
|
+
view: true,
|
|
200
|
+
claim: false,
|
|
201
|
+
resume: false,
|
|
202
|
+
finish: false,
|
|
203
|
+
release: false
|
|
204
|
+
};
|
|
205
|
+
/** 状态档命中(停滞由 host 标记,前端不判阈值)——只作用于第二区。 */
|
|
144
206
|
function statusHit(task, status) {
|
|
145
207
|
if (status === "all") return true;
|
|
146
208
|
if (status === "active") return task.status === "doing" || task.status === "blocked";
|
|
@@ -156,10 +218,11 @@ window.__ModuleLoader__.load({
|
|
|
156
218
|
return String(b.task.created ?? b.task.started ?? "").localeCompare(String(a.task.created ?? a.task.started ?? ""));
|
|
157
219
|
}
|
|
158
220
|
/**
|
|
159
|
-
* 当前筛选下的两区视图(用户 2026-10-
|
|
221
|
+
* 当前筛选下的两区视图(用户 2026-10-18 调整):
|
|
160
222
|
* - 第一区「我的待办」= 我名下 doing/blocked,吃工作区下拉 + 标题搜索,按创建时间倒序;
|
|
161
|
-
* -
|
|
162
|
-
* **标题 / 执行人搜索**(
|
|
223
|
+
* - 第二区「全部待办」= **所有**在办任务按工作区分组(含我的——用户明确不要过滤掉自己),
|
|
224
|
+
* 吃**状态档**(segmented)+ **标题 / 执行人搜索**(othersKeyword);一个整表(外框连着)。
|
|
225
|
+
* ⇒ 我的任务会同时出现在两个区,这是预期(顶部快看 / 底部全量)。
|
|
163
226
|
*/
|
|
164
227
|
function selectView(current) {
|
|
165
228
|
const { board, query } = current;
|
|
@@ -167,7 +230,7 @@ window.__ModuleLoader__.load({
|
|
|
167
230
|
mine: [],
|
|
168
231
|
groups: [],
|
|
169
232
|
total: 0,
|
|
170
|
-
|
|
233
|
+
allCounts: {
|
|
171
234
|
total: 0,
|
|
172
235
|
doing: 0,
|
|
173
236
|
open: 0,
|
|
@@ -175,13 +238,13 @@ window.__ModuleLoader__.load({
|
|
|
175
238
|
stale: 0
|
|
176
239
|
}
|
|
177
240
|
};
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
total:
|
|
181
|
-
doing:
|
|
182
|
-
open:
|
|
183
|
-
blocked:
|
|
184
|
-
stale:
|
|
241
|
+
const allTasks = board.tasks;
|
|
242
|
+
const allCounts = {
|
|
243
|
+
total: allTasks.length,
|
|
244
|
+
doing: allTasks.filter((task) => task.status === "doing").length,
|
|
245
|
+
open: allTasks.filter((task) => task.status === "open").length,
|
|
246
|
+
blocked: allTasks.filter((task) => task.status === "blocked").length,
|
|
247
|
+
stale: allTasks.filter((task) => task.stale === true).length
|
|
185
248
|
};
|
|
186
249
|
const mineRows = board.tasks.filter((task) => isMine(task, board)).filter((task) => query.workspace === "" || task.workspace.id === query.workspace).filter((task) => hitKeyword(query.keyword, [
|
|
187
250
|
task.title,
|
|
@@ -196,7 +259,7 @@ window.__ModuleLoader__.load({
|
|
|
196
259
|
task,
|
|
197
260
|
mine: true
|
|
198
261
|
}));
|
|
199
|
-
const rest = query.scope === "mine" ? [] : board.tasks.filter((task) =>
|
|
262
|
+
const rest = query.scope === "mine" ? [] : board.tasks.filter((task) => statusHit(task, query.status)).filter((task) => hitKeyword(query.othersKeyword, [task.title, task.owner]));
|
|
200
263
|
const byWorkspace = /* @__PURE__ */ new Map();
|
|
201
264
|
for (const task of rest) {
|
|
202
265
|
const entry = byWorkspace.get(task.workspace.id) ?? {
|
|
@@ -207,7 +270,7 @@ window.__ModuleLoader__.load({
|
|
|
207
270
|
kind: "task",
|
|
208
271
|
key: `${task.workspace.id}:${task.id}`,
|
|
209
272
|
task,
|
|
210
|
-
mine:
|
|
273
|
+
mine: isMine(task, board)
|
|
211
274
|
});
|
|
212
275
|
byWorkspace.set(task.workspace.id, entry);
|
|
213
276
|
}
|
|
@@ -224,8 +287,8 @@ window.__ModuleLoader__.load({
|
|
|
224
287
|
return {
|
|
225
288
|
mine: mineRows,
|
|
226
289
|
groups,
|
|
227
|
-
total:
|
|
228
|
-
|
|
290
|
+
total: groups.reduce((sum, group) => sum + group.rows.length, 0),
|
|
291
|
+
allCounts
|
|
229
292
|
};
|
|
230
293
|
}
|
|
231
294
|
//#endregion
|
|
@@ -250,123 +313,6 @@ window.__ModuleLoader__.load({
|
|
|
250
313
|
return opts?.seconds === true ? `${base}:${pad2(d.getSeconds())}` : base;
|
|
251
314
|
}
|
|
252
315
|
//#endregion
|
|
253
|
-
//#region src/client/task-row.ts
|
|
254
|
-
/**
|
|
255
|
-
* 任务行(宿主**无表格件** ⇒ 自绘),**两种形态**(用户 2026-10-17 定稿:两区视觉必须不同,
|
|
256
|
-
* 且**互不重用**):
|
|
257
|
-
*
|
|
258
|
-
* A. `mine`(我的待办)= 整行一张卡,样式照抄 dispatch-table 任务配置卡:
|
|
259
|
-
* 左缘 6×36 状态条 + 标题行 + 第二行工作区/标签;右侧灰字时长;悬停出
|
|
260
|
-
* 「继续 / 结束 / 释放」;最右常驻「查看」放大镜钮。
|
|
261
|
-
*
|
|
262
|
-
* B. `others`(其他待办)= 连体长表里的一行(用户 2026-10-18 定稿):**无卡片、无外框**,
|
|
263
|
-
* 行 = 一条淡分隔线格:上下居中的状态圆点 + 任务(标题行 + 标签行;标签行最前是执行人
|
|
264
|
-
* `@名字`,未认领则不摆)+ 状态列(阻塞/停滞 pill,待认领/进行中文字;「待认领」只出现一次)
|
|
265
|
-
* + 最右「@创建人 创建于 年-月-日 时:分:秒」(悬停掉时分秒并让位给操作钮:没人在做 ⇒ 认领)
|
|
266
|
-
* + 查看。圆点颜色:灰=待认领 / 浅蓝=进行中(比「我的」蓝灰一档)/ 黄=阻塞 / 红=停滞;
|
|
267
|
-
* 停滞 pill 悬停给**配置出来的**阈值解释(staleDays 由 host 下发)。
|
|
268
|
-
*
|
|
269
|
-
* 按钮点击行为(含查看)归「任务按钮与会话交接」任务,本文件只做样子与提示。
|
|
270
|
-
*/
|
|
271
|
-
/** 行标题:在办 = 任务名(缺则任务号);已结案 = 任务包名(缺则「完成了什么」首句)。 */
|
|
272
|
-
function titleOf(row) {
|
|
273
|
-
if (row.kind === "task") return row.task.title ?? row.task.id;
|
|
274
|
-
const title = row.record.workpackage ?? row.record.summary;
|
|
275
|
-
return title !== void 0 && title !== "" ? title : row.record.src ?? "—";
|
|
276
|
-
}
|
|
277
|
-
/** 状态语义(异常优先:stale 盖过 blocked)。 */
|
|
278
|
-
function toneOf(row) {
|
|
279
|
-
if (row.kind !== "task") return "closed";
|
|
280
|
-
if (row.task.stale === true) return "stale";
|
|
281
|
-
if (row.task.status === "blocked") return "blocked";
|
|
282
|
-
if (row.task.status === "open") return "open";
|
|
283
|
-
return "doing";
|
|
284
|
-
}
|
|
285
|
-
function TaskRowView(props) {
|
|
286
|
-
const { t, row, variant, staleDays } = props;
|
|
287
|
-
const tone = toneOf(row);
|
|
288
|
-
const isTask = row.kind === "task";
|
|
289
|
-
const tags = (row.kind === "task" ? row.task.tags : row.record.tags) ?? [];
|
|
290
|
-
const created = row.kind === "task" ? row.task.created ?? null : row.record.created ?? null;
|
|
291
|
-
const createdDay = created !== null && created !== void 0 && Date.parse(created) >= 0 ? formatDateTime(created).slice(0, 10) : null;
|
|
292
|
-
const staleTip = staleDays === null || staleDays === void 0 ? void 0 : t("statStaleTitle", { n: staleDays });
|
|
293
|
-
const viewButton = (0, react.createElement)("button", {
|
|
294
|
-
type: "button",
|
|
295
|
-
className: "dtl-viewbtn",
|
|
296
|
-
"data-tip": t("viewDetail"),
|
|
297
|
-
title: t("viewDetail")
|
|
298
|
-
}, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutlineRegular, { size: 14 }));
|
|
299
|
-
const blockedPill = tone === "blocked" ? (0, react.createElement)("span", { className: "dtl-pill dtl-pill--blocked" }, t("statusBlocked")) : null;
|
|
300
|
-
const stalePill = tone === "stale" ? (0, react.createElement)("span", {
|
|
301
|
-
className: "dtl-pill dtl-pill--stale",
|
|
302
|
-
title: staleTip
|
|
303
|
-
}, t("statusStale")) : null;
|
|
304
|
-
const titleLine = (0, react.createElement)("div", { className: "dtl-taskrow__l1" }, (0, react.createElement)("span", {
|
|
305
|
-
className: "dtl-taskrow__title dtl-ellipsis",
|
|
306
|
-
title: titleOf(row)
|
|
307
|
-
}, titleOf(row)), createdDay === null ? null : (0, react.createElement)("span", { className: "dtl-taskrow__stamp" }, `[${t("fieldCreatedStamp", { day: createdDay })}]`), blockedPill, stalePill);
|
|
308
|
-
const orowTitleLine = (0, react.createElement)("div", { className: "dtl-orow__title" }, (0, react.createElement)("span", {
|
|
309
|
-
className: "dtl-orow__name dtl-ellipsis",
|
|
310
|
-
title: titleOf(row)
|
|
311
|
-
}, titleOf(row)));
|
|
312
|
-
if (variant === "others") {
|
|
313
|
-
const creator = isTask ? row.task.creator : row.record.owner;
|
|
314
|
-
const owner = isTask ? row.task.owner : null;
|
|
315
|
-
const hasOwner = !(owner === null || owner === void 0 || owner === "");
|
|
316
|
-
const who = creator === null || creator === void 0 || creator === "" ? "—" : `@${creator}`;
|
|
317
|
-
const createdOk = created !== null && created !== void 0 && Date.parse(created) >= 0;
|
|
318
|
-
const ownerChip = hasOwner ? (0, react.createElement)("span", {
|
|
319
|
-
className: "dtl-orow__owner",
|
|
320
|
-
title: t("fieldOwner")
|
|
321
|
-
}, `@${String(owner)}`) : null;
|
|
322
|
-
const tagLine = hasOwner || tags.length > 0 ? (0, react.createElement)("div", { className: "dtl-orow__tags" }, ownerChip, tags.map((tag) => (0, react.createElement)("span", {
|
|
323
|
-
key: tag,
|
|
324
|
-
className: "dtl-minitag",
|
|
325
|
-
title: `#${tag}`
|
|
326
|
-
}, `#${tag}`))) : null;
|
|
327
|
-
return (0, react.createElement)("div", { className: "dtl-orow" }, (0, react.createElement)("div", { className: "dtl-orow__row" }, (0, react.createElement)("span", { className: `dtl-dot dtl-dot--${tone}` }), (0, react.createElement)("div", { className: "dtl-tcell dtl-tcell--title" }, orowTitleLine, tagLine), (0, react.createElement)("span", { className: "dtl-tcell dtl-tcell--status" }, tone === "blocked" ? blockedPill : tone === "stale" ? stalePill : null, tone === "doing" || tone === "open" ? (0, react.createElement)("span", { className: `dtl-statustext dtl-statustext--${tone}` }, tone === "open" ? t("statusOpen") : t("statusActive")) : null), (0, react.createElement)("span", { className: "dtl-tcell dtl-tcell--created" }, (0, react.createElement)("span", { className: "dtl-orow__created-long" }, t("createdAt", {
|
|
328
|
-
who,
|
|
329
|
-
when: createdOk ? formatDateTime(String(created), { seconds: true }) : t("timeUnknown")
|
|
330
|
-
})), (0, react.createElement)("span", { className: "dtl-orow__created-short" }, t("createdAt", {
|
|
331
|
-
who,
|
|
332
|
-
when: createdOk ? formatDateTime(String(created)) : t("timeUnknown")
|
|
333
|
-
}))), (0, react.createElement)("span", { className: "dtl-orow__actions" }, hasOwner ? null : (0, react.createElement)("button", {
|
|
334
|
-
type: "button",
|
|
335
|
-
className: "dtl-minibtn",
|
|
336
|
-
"data-tip": t("tipClaim")
|
|
337
|
-
}, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutlineRegular, { size: 14 }), t("actionClaim")), viewButton)));
|
|
338
|
-
}
|
|
339
|
-
const parts = [];
|
|
340
|
-
if (isTask) {
|
|
341
|
-
const startRaw = row.task.started ?? row.task.created;
|
|
342
|
-
if (startRaw !== void 0 && startRaw !== null && startRaw !== "" && Date.parse(startRaw) >= 0) parts.push(`${formatDateTime(startRaw).slice(0, 10)} ${t("fieldStarted")}`);
|
|
343
|
-
if (row.task.ageDays !== null && row.task.ageDays !== void 0) parts.push(row.task.ageDays === 0 ? t("ageToday") : t("ageDaysText", { n: row.task.ageDays }));
|
|
344
|
-
} else parts.push(`${t("fieldFinished")} ${formatDateTime(row.record.time ?? row.record.created ?? "", { fallback: t("timeUnknown") })}`);
|
|
345
|
-
return (0, react.createElement)("div", { className: "dtl-taskrow" }, (0, react.createElement)("div", { className: "dtl-taskrow__row" }, (0, react.createElement)("span", { className: `dtl-rail dtl-rail--${tone}` }), (0, react.createElement)("div", { style: {
|
|
346
|
-
flex: "1 1 auto",
|
|
347
|
-
minWidth: 0
|
|
348
|
-
} }, titleLine, (0, react.createElement)("div", { className: "dtl-taskrow__l2" }, (0, react.createElement)("span", {
|
|
349
|
-
className: "dtl-wstag dtl-ellipsis",
|
|
350
|
-
title: row.kind === "task" ? row.task.workspace.title : ""
|
|
351
|
-
}, row.kind === "task" ? row.task.workspace.title : ""), tags.map((tag) => (0, react.createElement)("span", {
|
|
352
|
-
key: tag,
|
|
353
|
-
className: "dtl-minitag",
|
|
354
|
-
title: `#${tag}`
|
|
355
|
-
}, `#${tag}`)))), (0, react.createElement)("span", { className: "dtl-taskrow__when" }, parts.length === 0 ? t("timeUnknown") : parts.join("丨")), (0, react.createElement)("span", { className: "dtl-taskrow__actions" }, isTask ? (0, react.createElement)("button", {
|
|
356
|
-
type: "button",
|
|
357
|
-
className: "dtl-minibtn",
|
|
358
|
-
"data-tip": t("tipResume")
|
|
359
|
-
}, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { size: 14 }), t("actionResume")) : null, isTask ? (0, react.createElement)("button", {
|
|
360
|
-
type: "button",
|
|
361
|
-
className: "dtl-minibtn",
|
|
362
|
-
"data-tip": t("tipFinish")
|
|
363
|
-
}, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, { size: 14 }), t("actionFinish")) : null, isTask ? (0, react.createElement)("button", {
|
|
364
|
-
type: "button",
|
|
365
|
-
className: "dtl-minibtn",
|
|
366
|
-
"data-tip": t("tipRelease")
|
|
367
|
-
}, t("actionRelease")) : null, viewButton)));
|
|
368
|
-
}
|
|
369
|
-
//#endregion
|
|
370
316
|
//#region src/client/tokens.ts
|
|
371
317
|
/**
|
|
372
318
|
* 语义 token 层:`--dtl-*`(dtl = dsh-task-ledger)。
|
|
@@ -691,15 +637,16 @@ body[data-ds-dark-theme]{
|
|
|
691
637
|
.dtl-orow__row{display:flex;align-items:center;gap:12px;}
|
|
692
638
|
.dtl-orow__title{display:flex;align-items:baseline;gap:6px;min-width:0;}
|
|
693
639
|
.dtl-orow__name{font-size:var(--dtl-font-md);font-weight:500;line-height:var(--dtl-line-md);min-width:0;}
|
|
694
|
-
/*
|
|
640
|
+
/* 第二行:状态标签 + #标签(与标题行间距拉开,用户 2026-10-18;状态位与执行人**互换**后,
|
|
641
|
+
状态搬到这里、且永远有 ⇒ 这一行不再有「空一排」的情况) */
|
|
695
642
|
.dtl-orow__tags{display:flex;align-items:center;gap:var(--dtl-space-2);margin-top:7px;min-width:0;}
|
|
696
|
-
/*
|
|
697
|
-
.dtl-orow__owner{flex:none;padding:0 6px;border-radius:999px;background:var(--dtl-open-bg-soft);
|
|
698
|
-
color:var(--dtl-fg-2);font-size:10px;line-height:16px;white-space:nowrap;}
|
|
699
|
-
/* 列:任务(弹性)/ 状态 / 创建信息 / 操作 / 查看 */
|
|
643
|
+
/* 列:任务(弹性)/ 执行人 / 创建信息 / 操作 / 查看 */
|
|
700
644
|
.dtl-tcell{min-width:0;}
|
|
701
645
|
.dtl-tcell--title{flex:1 1 auto;}
|
|
702
|
-
|
|
646
|
+
/* 执行人列(原状态列,用户 2026-10-18 互换):标签最宽 20ch ⇒ 列宽按它给,标签再自己收到 100%
|
|
647
|
+
(名字过长时由标签自己的省略号兜住,不许溢出压到创建信息列) */
|
|
648
|
+
.dtl-tcell--status{flex:none;width:110px;}
|
|
649
|
+
.dtl-tcell--status .dtl-tag{max-width:100%;}
|
|
703
650
|
.dtl-tcell--created{flex:none;display:flex;align-items:center;justify-content:flex-end;
|
|
704
651
|
font-size:var(--dtl-font-xs);color:var(--dtl-fg-3);white-space:nowrap;font-variant-numeric:tabular-nums;}
|
|
705
652
|
/* 悬停:时分秒让位给操作钮(与「我的待办」的 时间⇄按钮 同款互换) */
|
|
@@ -708,11 +655,6 @@ body[data-ds-dark-theme]{
|
|
|
708
655
|
.dtl-orow:hover .dtl-orow__created-short{display:inline;}
|
|
709
656
|
.dtl-orow__actions{display:none;flex:none;gap:6px;}
|
|
710
657
|
.dtl-orow:hover .dtl-orow__actions{display:inline-flex;}
|
|
711
|
-
/* 状态列文字(待认领灰 / 进行中浅蓝;阻塞停滞仍是 pill) */
|
|
712
|
-
.dtl-statustext{font-size:var(--dtl-font-xs);white-space:nowrap;}
|
|
713
|
-
.dtl-statustext--doing{color:var(--dtl-business-dim);}
|
|
714
|
-
.dtl-statustext--open{color:var(--dtl-fg-3);}
|
|
715
|
-
|
|
716
658
|
/* 行首状态圆点:灰=待认领 / 浅蓝=进行中(比「我的」蓝灰一档)/ 黄=阻塞 / 红=停滞;
|
|
717
659
|
6px(用户 2026-10-18:8px 太大) */
|
|
718
660
|
.dtl-dot{display:inline-block;width:6px;height:6px;flex:none;border-radius:50%;}
|
|
@@ -765,15 +707,25 @@ body[data-ds-dark-theme]{
|
|
|
765
707
|
.dtl-taskrow__row:hover .dtl-taskrow__when{display:none;}
|
|
766
708
|
.dtl-taskrow__row:hover .dtl-taskrow__actions{display:inline-flex;}
|
|
767
709
|
|
|
768
|
-
/*
|
|
769
|
-
|
|
770
|
-
.dtl-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
/*
|
|
774
|
-
.dtl-
|
|
775
|
-
|
|
776
|
-
|
|
710
|
+
/* 标签(用户 2026-10-18):状态 / 执行人 / #标签 **共用一枚件**,只换色调;
|
|
711
|
+
最大宽度 20ch(≈10 个中文或 20 个字符),超出省略号、悬停跑马灯把全文滚出来。 */
|
|
712
|
+
.dtl-tag{display:inline-block;flex:none;max-width:20ch;padding:0 6px;border-radius:999px;
|
|
713
|
+
background:var(--dtl-chip-bg);color:var(--dtl-fg-3);font-size:10px;line-height:16px;
|
|
714
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;}
|
|
715
|
+
/* 状态四色:待认领灰 / 进行中蓝 / 阻塞黄 / 停滞红(与行首圆点同色系) */
|
|
716
|
+
.dtl-tag--open{color:var(--dtl-fg-2);font-weight:600;}
|
|
717
|
+
.dtl-tag--doing{background:var(--dtl-business-soft);color:var(--dtl-business);font-weight:600;}
|
|
718
|
+
.dtl-tag--blocked{background:var(--dtl-warning-soft);color:var(--dtl-warning);font-weight:600;}
|
|
719
|
+
.dtl-tag--stale{background:var(--dtl-danger-soft);color:var(--dtl-danger);font-weight:600;}
|
|
720
|
+
/* 执行人:浅蓝底(与状态色区分:状态是「事怎么样」,人是「谁在做」) */
|
|
721
|
+
.dtl-tag--person{background:var(--dtl-open-bg-soft);color:var(--dtl-fg-2);}
|
|
722
|
+
/* 内层撑满真实宽度(inline-block ⇒ transform 才生效),外层负责裁切与省略号 */
|
|
723
|
+
.dtl-tag__text{display:inline-block;}
|
|
724
|
+
/* 跑马灯只挂在**真溢出**的标签上(短的挂上去会白飘一段) */
|
|
725
|
+
.dtl-tag--long:hover{text-overflow:clip;}
|
|
726
|
+
.dtl-tag--long:hover .dtl-tag__text{animation:dtl-marquee 6s linear infinite;}
|
|
727
|
+
@keyframes dtl-marquee{0%,12%{transform:translateX(0);}
|
|
728
|
+
88%,100%{transform:translateX(calc(-100% + 20ch - 12px));}}
|
|
777
729
|
|
|
778
730
|
/* 分区标题里的计数:与日期徽同款中括号小字(用户 2026-10-17);0 条不渲染 */
|
|
779
731
|
.dtl-count{font-size:var(--dtl-font-xs);color:var(--dtl-fg-3);font-variant-numeric:tabular-nums;}
|
|
@@ -966,6 +918,27 @@ body[data-ds-dark-theme]{
|
|
|
966
918
|
color: "var(--dtl-fg-2)"
|
|
967
919
|
} }, props.label), (0, react.createElement)("span", { className: "dtl-stat__value" }, shown), props.hint === void 0 || props.hint === null || props.hint === "" ? null : (0, react.createElement)("span", { className: "dtl-stat__hint" }, props.hint));
|
|
968
920
|
}
|
|
921
|
+
/** 标签上限宽度(字符数):1 个中文按 2 计 ⇒ 10 个中文 = 20 个字符(与 CSS 的 20ch 对齐)。 */
|
|
922
|
+
const TAG_MAX_WIDTH = 20;
|
|
923
|
+
/** 显示宽度:全角(中 / 日 / 韩 / 全角符号)一个顶两个英文字符。 */
|
|
924
|
+
function displayWidth(text) {
|
|
925
|
+
let width = 0;
|
|
926
|
+
for (const char of text) width += (char.codePointAt(0) ?? 0) > 11904 ? 2 : 1;
|
|
927
|
+
return width;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* 标签(**唯一**一枚件,用户 2026-10-18):状态 / 执行人 / #标签 都走它,只换 tone。
|
|
931
|
+
* 超宽 ⇒ 省略号;悬停 ⇒ 跑马灯滚出全文(溢出时不挂 title,免得原生提示盖住滚动)。
|
|
932
|
+
*/
|
|
933
|
+
function Tag(props) {
|
|
934
|
+
ensureUiBase();
|
|
935
|
+
const { text, tone = "neutral", title } = props;
|
|
936
|
+
const long = displayWidth(text) > TAG_MAX_WIDTH;
|
|
937
|
+
return (0, react.createElement)("span", {
|
|
938
|
+
className: `dtl-tag dtl-tag--${tone}${long ? " dtl-tag--long" : ""}`,
|
|
939
|
+
title: long ? void 0 : title ?? text
|
|
940
|
+
}, (0, react.createElement)("span", { className: "dtl-tag__text" }, text));
|
|
941
|
+
}
|
|
969
942
|
/** 空态:`guide` = 真空引导卡(虚线),`filtered` = 筛选空小字。 */
|
|
970
943
|
function EmptyState(props) {
|
|
971
944
|
ensureUiBase();
|
|
@@ -1093,6 +1066,143 @@ body[data-ds-dark-theme]{
|
|
|
1093
1066
|
return (0, react.createElement)("div", { className: "dtl-gchart-legend" }, (0, react.createElement)("span", null, (0, react.createElement)("i", { style: { background: "var(--dtl-business)" } }), props.doneLabel), (0, react.createElement)("span", null, (0, react.createElement)("i", { style: { background: "var(--dtl-chart-remaining)" } }), props.remainingLabel), (0, react.createElement)("span", null, (0, react.createElement)("i", { style: { background: "var(--dtl-chart-created)" } }), props.createdLabel));
|
|
1094
1067
|
}
|
|
1095
1068
|
//#endregion
|
|
1069
|
+
//#region src/client/task-row.ts
|
|
1070
|
+
/**
|
|
1071
|
+
* 任务行(宿主**无表格件** ⇒ 自绘),**两种形态**(用户 2026-10-17 定稿:两区视觉必须不同,
|
|
1072
|
+
* 且**互不重用**):
|
|
1073
|
+
*
|
|
1074
|
+
* A. `mine`(我的待办)= 整行一张卡,样式照抄 dispatch-table 任务配置卡:
|
|
1075
|
+
* 左缘 6×36 状态条 + 标题行 + 第二行工作区/标签;右侧灰字时长;悬停出
|
|
1076
|
+
* 「继续 / 结束 / 释放」;最右常驻「查看」放大镜钮。
|
|
1077
|
+
*
|
|
1078
|
+
* B. `others`(全部待办)= 连体长表里的一行(用户 2026-10-18 定稿):**无卡片、无外框**,
|
|
1079
|
+
* 行 = 一条淡分隔线格:上下居中的状态圆点 + 任务(标题行 + 标签行;**标签行最前是状态标签**)
|
|
1080
|
+
* + **执行人列**(原状态位,有执行人才摆 `@名字` 标签)+ 最右「@创建人 创建于 年-月-日 时:分:秒」
|
|
1081
|
+
* (悬停掉时分秒并让位给操作钮)+ 查看。
|
|
1082
|
+
* 状态一律**标签型、四色**(用户 2026-10-18:原来是一排素字 / pill,现在与人员**位置互换**):
|
|
1083
|
+
* 灰=待认领 / 蓝=进行中 / 黄=阻塞 / 红=停滞;停滞标签悬停给**配置出来的**阈值解释
|
|
1084
|
+
* (staleDays 由 host 下发)。圆点颜色同色系。
|
|
1085
|
+
*
|
|
1086
|
+
* 按钮(含查看)**不各判各的**:权限一律问 `resolveActions()`(rows.ts),本文件只按返回值画。
|
|
1087
|
+
*
|
|
1088
|
+
* 按钮点击行为(含查看)归「任务按钮与会话交接」任务,本文件只做样子与提示。
|
|
1089
|
+
*/
|
|
1090
|
+
/** 行标题:在办 = 任务名(缺则任务号);已结案 = 任务包名(缺则「完成了什么」首句)。 */
|
|
1091
|
+
function titleOf(row) {
|
|
1092
|
+
if (row.kind === "task") return row.task.title ?? row.task.id;
|
|
1093
|
+
const title = row.record.workpackage ?? row.record.summary;
|
|
1094
|
+
return title !== void 0 && title !== "" ? title : row.record.src ?? "—";
|
|
1095
|
+
}
|
|
1096
|
+
/** 状态语义(异常优先:stale 盖过 blocked)。 */
|
|
1097
|
+
function toneOf(row) {
|
|
1098
|
+
if (row.kind !== "task") return "closed";
|
|
1099
|
+
if (row.task.stale === true) return "stale";
|
|
1100
|
+
if (row.task.status === "blocked") return "blocked";
|
|
1101
|
+
if (row.task.status === "open") return "open";
|
|
1102
|
+
return "doing";
|
|
1103
|
+
}
|
|
1104
|
+
/** 状态文案(已结案没有在办状态 ⇒ 不给标签)。 */
|
|
1105
|
+
function statusLabel(tone, t) {
|
|
1106
|
+
if (tone === "stale") return t("statusStale");
|
|
1107
|
+
if (tone === "blocked") return t("statusBlocked");
|
|
1108
|
+
if (tone === "open") return t("statusOpen");
|
|
1109
|
+
if (tone === "doing") return t("statusActive");
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
function TaskRowView(props) {
|
|
1113
|
+
const { t, row, variant, staleDays, board } = props;
|
|
1114
|
+
const tone = toneOf(row);
|
|
1115
|
+
const isTask = row.kind === "task";
|
|
1116
|
+
const tags = (row.kind === "task" ? row.task.tags : row.record.tags) ?? [];
|
|
1117
|
+
const created = row.kind === "task" ? row.task.created ?? null : row.record.created ?? null;
|
|
1118
|
+
const createdDay = created !== null && created !== void 0 && Date.parse(created) >= 0 ? formatDateTime(created).slice(0, 10) : null;
|
|
1119
|
+
const staleTip = staleDays === null || staleDays === void 0 ? void 0 : t("statStaleTitle", { n: staleDays });
|
|
1120
|
+
const actions = isTask ? resolveActions(row.task, board) : CLOSED_ACTIONS;
|
|
1121
|
+
const viewButton = (0, react.createElement)("button", {
|
|
1122
|
+
type: "button",
|
|
1123
|
+
className: "dtl-viewbtn",
|
|
1124
|
+
"data-tip": t("viewDetail"),
|
|
1125
|
+
title: t("viewDetail")
|
|
1126
|
+
}, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutlineRegular, { size: 14 }));
|
|
1127
|
+
const claimButton = (0, react.createElement)("button", {
|
|
1128
|
+
type: "button",
|
|
1129
|
+
className: "dtl-minibtn",
|
|
1130
|
+
"data-tip": t("tipClaim")
|
|
1131
|
+
}, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutlineRegular, { size: 14 }), t("actionClaim"));
|
|
1132
|
+
const resumeButton = (0, react.createElement)("button", {
|
|
1133
|
+
type: "button",
|
|
1134
|
+
className: "dtl-minibtn",
|
|
1135
|
+
"data-tip": t("tipResume")
|
|
1136
|
+
}, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { size: 14 }), t("actionResume"));
|
|
1137
|
+
const finishButton = (0, react.createElement)("button", {
|
|
1138
|
+
type: "button",
|
|
1139
|
+
className: "dtl-minibtn",
|
|
1140
|
+
"data-tip": t("tipFinish")
|
|
1141
|
+
}, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, { size: 14 }), t("actionFinish"));
|
|
1142
|
+
const releaseButton = (0, react.createElement)("button", {
|
|
1143
|
+
type: "button",
|
|
1144
|
+
className: "dtl-minibtn",
|
|
1145
|
+
"data-tip": t("tipRelease")
|
|
1146
|
+
}, t("actionRelease"));
|
|
1147
|
+
const statusText = statusLabel(tone, t);
|
|
1148
|
+
const statusTag = statusText === null || tone === "closed" ? null : (0, react.createElement)(Tag, {
|
|
1149
|
+
text: statusText,
|
|
1150
|
+
tone,
|
|
1151
|
+
title: tone === "stale" ? staleTip ?? statusText : statusText
|
|
1152
|
+
});
|
|
1153
|
+
const alarmTag = tone === "blocked" || tone === "stale" ? (0, react.createElement)(Tag, {
|
|
1154
|
+
text: statusLabel(tone, t) ?? "",
|
|
1155
|
+
tone,
|
|
1156
|
+
title: tone === "stale" ? staleTip : void 0
|
|
1157
|
+
}) : null;
|
|
1158
|
+
const titleLine = (0, react.createElement)("div", { className: "dtl-taskrow__l1" }, (0, react.createElement)("span", {
|
|
1159
|
+
className: "dtl-taskrow__title dtl-ellipsis",
|
|
1160
|
+
title: titleOf(row)
|
|
1161
|
+
}, titleOf(row)), createdDay === null ? null : (0, react.createElement)("span", { className: "dtl-taskrow__stamp" }, `[${t("fieldCreatedStamp", { day: createdDay })}]`), alarmTag);
|
|
1162
|
+
const orowTitleLine = (0, react.createElement)("div", { className: "dtl-orow__title" }, (0, react.createElement)("span", {
|
|
1163
|
+
className: "dtl-orow__name dtl-ellipsis",
|
|
1164
|
+
title: titleOf(row)
|
|
1165
|
+
}, titleOf(row)));
|
|
1166
|
+
if (variant === "others") {
|
|
1167
|
+
const creator = isTask ? row.task.creator : row.record.owner;
|
|
1168
|
+
const owner = isTask ? row.task.owner : null;
|
|
1169
|
+
const hasOwner = !(owner === null || owner === void 0 || owner === "");
|
|
1170
|
+
const who = creator === null || creator === void 0 || creator === "" ? "—" : `@${creator}`;
|
|
1171
|
+
const createdOk = created !== null && created !== void 0 && Date.parse(created) >= 0;
|
|
1172
|
+
const tagLine = (0, react.createElement)("div", { className: "dtl-orow__tags" }, statusTag, tags.map((tag) => (0, react.createElement)(Tag, {
|
|
1173
|
+
key: tag,
|
|
1174
|
+
text: `#${tag}`
|
|
1175
|
+
})));
|
|
1176
|
+
return (0, react.createElement)("div", { className: "dtl-orow" }, (0, react.createElement)("div", { className: "dtl-orow__row" }, (0, react.createElement)("span", { className: `dtl-dot dtl-dot--${tone}` }), (0, react.createElement)("div", { className: "dtl-tcell dtl-tcell--title" }, orowTitleLine, tagLine), (0, react.createElement)("span", { className: "dtl-tcell dtl-tcell--status" }, hasOwner ? (0, react.createElement)(Tag, {
|
|
1177
|
+
text: `@${String(owner)}`,
|
|
1178
|
+
tone: "person",
|
|
1179
|
+
title: t("fieldOwner")
|
|
1180
|
+
}) : null), (0, react.createElement)("span", { className: "dtl-tcell dtl-tcell--created" }, (0, react.createElement)("span", { className: "dtl-orow__created-long" }, t("createdAt", {
|
|
1181
|
+
who,
|
|
1182
|
+
when: createdOk ? formatDateTime(String(created), { seconds: true }) : t("timeUnknown")
|
|
1183
|
+
})), (0, react.createElement)("span", { className: "dtl-orow__created-short" }, t("createdAt", {
|
|
1184
|
+
who,
|
|
1185
|
+
when: createdOk ? formatDateTime(String(created)) : t("timeUnknown")
|
|
1186
|
+
}))), (0, react.createElement)("span", { className: "dtl-orow__actions" }, actions.claim ? claimButton : null, actions.resume ? resumeButton : null, actions.finish ? finishButton : null, actions.release ? releaseButton : null, actions.view ? viewButton : null)));
|
|
1187
|
+
}
|
|
1188
|
+
const parts = [];
|
|
1189
|
+
if (isTask) {
|
|
1190
|
+
const startRaw = row.task.started ?? row.task.created;
|
|
1191
|
+
if (startRaw !== void 0 && startRaw !== null && startRaw !== "" && Date.parse(startRaw) >= 0) parts.push(`${formatDateTime(startRaw).slice(0, 10)} ${t("fieldStarted")}`);
|
|
1192
|
+
if (row.task.ageDays !== null && row.task.ageDays !== void 0) parts.push(row.task.ageDays === 0 ? t("ageToday") : t("ageDaysText", { n: row.task.ageDays }));
|
|
1193
|
+
} else parts.push(`${t("fieldFinished")} ${formatDateTime(row.record.time ?? row.record.created ?? "", { fallback: t("timeUnknown") })}`);
|
|
1194
|
+
return (0, react.createElement)("div", { className: "dtl-taskrow" }, (0, react.createElement)("div", { className: "dtl-taskrow__row" }, (0, react.createElement)("span", { className: `dtl-rail dtl-rail--${tone}` }), (0, react.createElement)("div", { style: {
|
|
1195
|
+
flex: "1 1 auto",
|
|
1196
|
+
minWidth: 0
|
|
1197
|
+
} }, titleLine, (0, react.createElement)("div", { className: "dtl-taskrow__l2" }, (0, react.createElement)("span", {
|
|
1198
|
+
className: "dtl-wstag dtl-ellipsis",
|
|
1199
|
+
title: row.kind === "task" ? row.task.workspace.title : ""
|
|
1200
|
+
}, row.kind === "task" ? row.task.workspace.title : ""), tags.map((tag) => (0, react.createElement)(Tag, {
|
|
1201
|
+
key: tag,
|
|
1202
|
+
text: `#${tag}`
|
|
1203
|
+
})))), (0, react.createElement)("span", { className: "dtl-taskrow__when" }, parts.length === 0 ? t("timeUnknown") : parts.join("丨")), (0, react.createElement)("span", { className: "dtl-taskrow__actions" }, actions.resume ? resumeButton : null, actions.finish ? finishButton : null, actions.release ? releaseButton : null, actions.view ? viewButton : null)));
|
|
1204
|
+
}
|
|
1205
|
+
//#endregion
|
|
1096
1206
|
//#region src/client/board-home.ts
|
|
1097
1207
|
/**
|
|
1098
1208
|
* 看板首页(`main` 槽整页内容):6 统计卡 + 近 8 周趋势图 + 两区任务列表。
|
|
@@ -1166,6 +1276,11 @@ body[data-ds-dark-theme]{
|
|
|
1166
1276
|
const board = state.board;
|
|
1167
1277
|
const view = (0, react.useMemo)(() => selectView(state), [state]);
|
|
1168
1278
|
const stats = board?.stats;
|
|
1279
|
+
const trend = state.trend;
|
|
1280
|
+
const periods = trend?.trend;
|
|
1281
|
+
const trendPending = state.trendStatus === "idle" || state.trendStatus === "loading";
|
|
1282
|
+
/** 快照 + 趋势两段各自的降级工作区(op 不同,合并展示,别装没看见)。 */
|
|
1283
|
+
const degraded = [...board?.degraded ?? [], ...trend?.degraded ?? []];
|
|
1169
1284
|
const workspaceOptions = (0, react.useMemo)(() => [{
|
|
1170
1285
|
value: "",
|
|
1171
1286
|
label: t("workspaceAll")
|
|
@@ -1181,11 +1296,12 @@ body[data-ds-dark-theme]{
|
|
|
1181
1296
|
hint,
|
|
1182
1297
|
interactive: false
|
|
1183
1298
|
});
|
|
1184
|
-
/** 统计台卡:不可点(首页没有已结案可筛),hover 给具体日期区间。
|
|
1299
|
+
/** 统计台卡:不可点(首页没有已结案可筛),hover 给具体日期区间。
|
|
1300
|
+
* 趋势未到 / 读挂 ⇒ 数字留 '—'(StatCard 对 0 的处理)、小字整条不显示,**不拿 0 充数**。 */
|
|
1185
1301
|
const periodCard = (label, period) => (0, react.createElement)(StatCard, {
|
|
1186
1302
|
label: t(label),
|
|
1187
1303
|
value: period?.done ?? 0,
|
|
1188
|
-
hint: t("statCreatedHint", { n: period
|
|
1304
|
+
hint: period === void 0 ? void 0 : t("statCreatedHint", { n: period.created }),
|
|
1189
1305
|
interactive: false,
|
|
1190
1306
|
title: period === void 0 ? void 0 : t("periodRange", {
|
|
1191
1307
|
from: period.from,
|
|
@@ -1193,7 +1309,7 @@ body[data-ds-dark-theme]{
|
|
|
1193
1309
|
})
|
|
1194
1310
|
});
|
|
1195
1311
|
const [chartScope, setChartScope] = (0, react.useState)("all");
|
|
1196
|
-
const chartWeeks = (0, react.useMemo)(() => (
|
|
1312
|
+
const chartWeeks = (0, react.useMemo)(() => (trend?.closed.weekly[chartScope] ?? []).map((bucket) => {
|
|
1197
1313
|
const end = bucket.end ?? bucket.week ?? "";
|
|
1198
1314
|
return {
|
|
1199
1315
|
label: end.slice(5) || bucket.week?.slice(5) || "",
|
|
@@ -1203,7 +1319,7 @@ body[data-ds-dark-theme]{
|
|
|
1203
1319
|
title: chartScope === "mine" ? `截至 ${end}(${t("chartOnlyMine")})` : `截至 ${end}`
|
|
1204
1320
|
};
|
|
1205
1321
|
}), [
|
|
1206
|
-
|
|
1322
|
+
trend,
|
|
1207
1323
|
chartScope,
|
|
1208
1324
|
t
|
|
1209
1325
|
]);
|
|
@@ -1262,7 +1378,7 @@ body[data-ds-dark-theme]{
|
|
|
1262
1378
|
}),
|
|
1263
1379
|
title: t("statStaleTitle", { n: stats.staleDays }),
|
|
1264
1380
|
interactive: false
|
|
1265
|
-
}), periodCard("statWeekDone",
|
|
1381
|
+
}), periodCard("statWeekDone", periods?.thisWeek), periodCard("statLastWeekDone", periods?.lastWeek), periodCard("statMonthDone", periods?.thisMonth), periodCard("statLastMonthDone", periods?.lastMonth)), (0, react.createElement)("div", {
|
|
1266
1382
|
className: "dtl-card",
|
|
1267
1383
|
style: chartCardStyle
|
|
1268
1384
|
}, (0, react.createElement)("div", { style: {
|
|
@@ -1275,7 +1391,13 @@ body[data-ds-dark-theme]{
|
|
|
1275
1391
|
onChange: (event) => {
|
|
1276
1392
|
setChartScope(event.target.checked ? "mine" : "all");
|
|
1277
1393
|
}
|
|
1278
|
-
}), t("chartOnlyMine"))),
|
|
1394
|
+
}), t("chartOnlyMine"))), trendPending ? (0, react.createElement)("div", { style: {
|
|
1395
|
+
...hintStyle,
|
|
1396
|
+
padding: "var(--dtl-space-4) 0"
|
|
1397
|
+
} }, t("chartLoading")) : state.trendStatus === "error" ? (0, react.createElement)("div", { style: {
|
|
1398
|
+
...hintStyle,
|
|
1399
|
+
padding: "var(--dtl-space-4) 0"
|
|
1400
|
+
} }, `${t("chartUnavailable")}${state.trendError === null ? "" : `:${state.trendError}`}`) : chartEmpty ? (0, react.createElement)("div", { style: {
|
|
1279
1401
|
...hintStyle,
|
|
1280
1402
|
padding: "var(--dtl-space-4) 0"
|
|
1281
1403
|
} }, t("chartEmpty")) : (0, react.createElement)(GroupedBarChart, {
|
|
@@ -1321,7 +1443,7 @@ body[data-ds-dark-theme]{
|
|
|
1321
1443
|
color: "var(--dtl-danger)",
|
|
1322
1444
|
flex: "none",
|
|
1323
1445
|
fontWeight: 600
|
|
1324
|
-
} }, t("degradedTitle")), (0, react.createElement)("span", null,
|
|
1446
|
+
} }, t("degradedTitle")), (0, react.createElement)("span", null, degraded.map((item) => t("degradedItem", {
|
|
1325
1447
|
title: item.workspaceTitle,
|
|
1326
1448
|
message: item.message
|
|
1327
1449
|
})).join(";"))) : null, (0, react.createElement)("div", { style: regionHeadRowStyle }, (0, react.createElement)("span", { style: {
|
|
@@ -1355,7 +1477,8 @@ body[data-ds-dark-theme]{
|
|
|
1355
1477
|
t,
|
|
1356
1478
|
row,
|
|
1357
1479
|
variant: "mine",
|
|
1358
|
-
staleDays: board?.stats.staleDays ?? null
|
|
1480
|
+
staleDays: board?.stats.staleDays ?? null,
|
|
1481
|
+
board
|
|
1359
1482
|
})), (0, react.createElement)("div", null, (0, react.createElement)("div", { style: regionHeadRowStyle }, (0, react.createElement)("span", { style: {
|
|
1360
1483
|
color: "var(--dtl-fg-3)",
|
|
1361
1484
|
display: "inline-flex",
|
|
@@ -1363,7 +1486,7 @@ body[data-ds-dark-theme]{
|
|
|
1363
1486
|
} }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconFlatListOutlineRegular, { size: 16 })), (0, react.createElement)("span", { style: {
|
|
1364
1487
|
...regionHeadTitleStyle,
|
|
1365
1488
|
fontSize: "var(--dtl-font-md)"
|
|
1366
|
-
} }, t("
|
|
1489
|
+
} }, t("regionAll")), (0, react.createElement)("span", { style: { flex: "1 1 auto" } }), (0, react.createElement)(Segmented, {
|
|
1367
1490
|
id: "dtl-others-status",
|
|
1368
1491
|
value: state.query.status,
|
|
1369
1492
|
label: t("statusLabel"),
|
|
@@ -1371,27 +1494,27 @@ body[data-ds-dark-theme]{
|
|
|
1371
1494
|
{
|
|
1372
1495
|
value: "all",
|
|
1373
1496
|
label: t("statusAll"),
|
|
1374
|
-
badge: orZero(view.
|
|
1497
|
+
badge: orZero(view.allCounts.total)
|
|
1375
1498
|
},
|
|
1376
1499
|
{
|
|
1377
1500
|
value: "active",
|
|
1378
1501
|
label: t("statusActive"),
|
|
1379
|
-
badge: orZero(view.
|
|
1502
|
+
badge: orZero(view.allCounts.doing)
|
|
1380
1503
|
},
|
|
1381
1504
|
{
|
|
1382
1505
|
value: "open",
|
|
1383
1506
|
label: t("statusOpen"),
|
|
1384
|
-
badge: orZero(view.
|
|
1507
|
+
badge: orZero(view.allCounts.open)
|
|
1385
1508
|
},
|
|
1386
1509
|
{
|
|
1387
1510
|
value: "blocked",
|
|
1388
1511
|
label: t("statusBlocked"),
|
|
1389
|
-
badge: orZero(view.
|
|
1512
|
+
badge: orZero(view.allCounts.blocked)
|
|
1390
1513
|
},
|
|
1391
1514
|
{
|
|
1392
1515
|
value: "stale",
|
|
1393
1516
|
label: t("statusStale"),
|
|
1394
|
-
badge: orZero(view.
|
|
1517
|
+
badge: orZero(view.allCounts.stale)
|
|
1395
1518
|
}
|
|
1396
1519
|
],
|
|
1397
1520
|
onChange: (next) => {
|
|
@@ -1414,30 +1537,31 @@ body[data-ds-dark-theme]{
|
|
|
1414
1537
|
fontSize: "var(--dtl-font-lg)",
|
|
1415
1538
|
fontWeight: 600,
|
|
1416
1539
|
marginBottom: "var(--dtl-space-1)"
|
|
1417
|
-
} }, t("
|
|
1540
|
+
} }, t("allEmptyTitle")), (0, react.createElement)("div", { style: {
|
|
1418
1541
|
fontSize: "var(--dtl-font-md)",
|
|
1419
1542
|
color: "var(--dtl-fg-2)",
|
|
1420
1543
|
marginBottom: "var(--dtl-space-3)"
|
|
1421
|
-
} }, t("
|
|
1544
|
+
} }, t("allEmptyHint")), (0, react.createElement)(Button, {
|
|
1422
1545
|
variant: "primary",
|
|
1423
1546
|
size: "md",
|
|
1424
1547
|
onClick: onBack
|
|
1425
|
-
}, t("
|
|
1548
|
+
}, t("allEmptyAction"))) : (0, react.createElement)("div", { style: { marginTop: "var(--dtl-space-2)" } }, view.groups.map((group) => (0, react.createElement)(WorkspaceGroupView, {
|
|
1426
1549
|
key: group.workspaceId,
|
|
1427
1550
|
t,
|
|
1428
1551
|
group,
|
|
1429
|
-
staleDays: board?.stats.staleDays ?? null
|
|
1552
|
+
staleDays: board?.stats.staleDays ?? null,
|
|
1553
|
+
board
|
|
1430
1554
|
})))))))));
|
|
1431
1555
|
}
|
|
1432
1556
|
/** 角标:>0 才给数字,0 = undefined(不标)。 */
|
|
1433
1557
|
function orZero(n) {
|
|
1434
1558
|
return n > 0 ? n : void 0;
|
|
1435
1559
|
}
|
|
1436
|
-
/**
|
|
1560
|
+
/** 一个工作区分组(全部待办表格段):段头(点+名+统计)→ 列头行 → 任务行。
|
|
1437
1561
|
* 统计口径(用户 2026-10-18):共 N|待办 N|进行中 N 常显(0 也显);
|
|
1438
1562
|
* 阻塞 / 停滞**只在 >0 时出现**。 */
|
|
1439
1563
|
function WorkspaceGroupView(props) {
|
|
1440
|
-
const { t, group, staleDays } = props;
|
|
1564
|
+
const { t, group, staleDays, board } = props;
|
|
1441
1565
|
const [open, setOpen] = (0, react.useState)(group.rows.length <= GROUP_COLLAPSE_OVER);
|
|
1442
1566
|
const stats = [
|
|
1443
1567
|
t("groupTotal", { n: group.total }),
|
|
@@ -1463,7 +1587,8 @@ body[data-ds-dark-theme]{
|
|
|
1463
1587
|
t,
|
|
1464
1588
|
row,
|
|
1465
1589
|
variant: "others",
|
|
1466
|
-
staleDays
|
|
1590
|
+
staleDays,
|
|
1591
|
+
board
|
|
1467
1592
|
}))) : null);
|
|
1468
1593
|
}
|
|
1469
1594
|
//#endregion
|
|
@@ -1504,6 +1629,8 @@ body[data-ds-dark-theme]{
|
|
|
1504
1629
|
chartCaption: "完成(按结案时间)/ 新增(按创建时间),周一起桶;我的 = owner 是我",
|
|
1505
1630
|
chartBucket: "{week} 当周:完成 {done} 条,新增 {created} 条",
|
|
1506
1631
|
chartEmpty: "近 8 周没有记录",
|
|
1632
|
+
chartLoading: "趋势读取中…(列表已就绪,统计稍后到)",
|
|
1633
|
+
chartUnavailable: "趋势暂时读不到(不影响上面的任务列表)",
|
|
1507
1634
|
chartScopeAll: "所有",
|
|
1508
1635
|
chartScopeMine: "我的",
|
|
1509
1636
|
chartOnlyMine: "只看我的",
|
|
@@ -1525,7 +1652,7 @@ body[data-ds-dark-theme]{
|
|
|
1525
1652
|
workspaceAll: "全部工作区",
|
|
1526
1653
|
searchPlaceholder: "搜索",
|
|
1527
1654
|
regionMine: "我的待办",
|
|
1528
|
-
|
|
1655
|
+
regionAll: "全部待办",
|
|
1529
1656
|
showingCount: "显示 {n} 条",
|
|
1530
1657
|
groupTotal: "共 {n} 条",
|
|
1531
1658
|
groupOpen: "待办 {n} 条",
|
|
@@ -1537,9 +1664,9 @@ body[data-ds-dark-theme]{
|
|
|
1537
1664
|
colOwner: "执行人",
|
|
1538
1665
|
colStatus: "状态",
|
|
1539
1666
|
colTime: "时长",
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1667
|
+
allEmptyTitle: "没有可显示的事项",
|
|
1668
|
+
allEmptyHint: "切换上面的状态过滤试试,或新建一个任务。",
|
|
1669
|
+
allEmptyAction: "新建任务",
|
|
1543
1670
|
expandAria: "展开过程文档",
|
|
1544
1671
|
collapseAria: "收起",
|
|
1545
1672
|
ownerUnclaimed: "待认领",
|
|
@@ -1615,6 +1742,8 @@ body[data-ds-dark-theme]{
|
|
|
1615
1742
|
chartCaption: "done (by close time) / created (by create time), weeks start Monday; mine = owner is me",
|
|
1616
1743
|
chartBucket: "week of {week}: {done} done, {created} created",
|
|
1617
1744
|
chartEmpty: "Nothing in the last 8 weeks",
|
|
1745
|
+
chartLoading: "Loading trend… (list is ready, stats will follow)",
|
|
1746
|
+
chartUnavailable: "Trend is unavailable right now (the task lists above are unaffected)",
|
|
1618
1747
|
chartScopeAll: "All",
|
|
1619
1748
|
chartScopeMine: "Mine",
|
|
1620
1749
|
chartOnlyMine: "Mine only",
|
|
@@ -1636,7 +1765,7 @@ body[data-ds-dark-theme]{
|
|
|
1636
1765
|
workspaceAll: "All workspaces",
|
|
1637
1766
|
searchPlaceholder: "Search",
|
|
1638
1767
|
regionMine: "My todos",
|
|
1639
|
-
|
|
1768
|
+
regionAll: "All tasks",
|
|
1640
1769
|
showingCount: "{n} shown",
|
|
1641
1770
|
groupTotal: "{n} total",
|
|
1642
1771
|
groupOpen: "{n} open",
|
|
@@ -1648,9 +1777,9 @@ body[data-ds-dark-theme]{
|
|
|
1648
1777
|
colOwner: "Owner",
|
|
1649
1778
|
colStatus: "Status",
|
|
1650
1779
|
colTime: "Duration",
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1780
|
+
allEmptyTitle: "Nothing to show",
|
|
1781
|
+
allEmptyHint: "Try another status filter above, or create a task.",
|
|
1782
|
+
allEmptyAction: "New task",
|
|
1654
1783
|
expandAria: "Expand worklog",
|
|
1655
1784
|
collapseAria: "Collapse",
|
|
1656
1785
|
ownerUnclaimed: "unclaimed",
|