dsh-task-ledger 0.0.47 → 0.0.49

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 CHANGED
@@ -184,7 +184,25 @@ window.__ModuleLoader__.load({
184
184
  if (identity === null || identity === "") return false;
185
185
  return task.owner === identity && (task.status === "doing" || task.status === "blocked");
186
186
  }
187
- /** 状态档命中(停滞由 host 标记,前端不判阈值)——只作用于「其他待办」。 */
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 标记,前端不判阈值)——只作用于第二区。 */
188
206
  function statusHit(task, status) {
189
207
  if (status === "all") return true;
190
208
  if (status === "active") return task.status === "doing" || task.status === "blocked";
@@ -252,7 +270,7 @@ window.__ModuleLoader__.load({
252
270
  kind: "task",
253
271
  key: `${task.workspace.id}:${task.id}`,
254
272
  task,
255
- mine: false
273
+ mine: isMine(task, board)
256
274
  });
257
275
  byWorkspace.set(task.workspace.id, entry);
258
276
  }
@@ -295,123 +313,6 @@ window.__ModuleLoader__.load({
295
313
  return opts?.seconds === true ? `${base}:${pad2(d.getSeconds())}` : base;
296
314
  }
297
315
  //#endregion
298
- //#region src/client/task-row.ts
299
- /**
300
- * 任务行(宿主**无表格件** ⇒ 自绘),**两种形态**(用户 2026-10-17 定稿:两区视觉必须不同,
301
- * 且**互不重用**):
302
- *
303
- * A. `mine`(我的待办)= 整行一张卡,样式照抄 dispatch-table 任务配置卡:
304
- * 左缘 6×36 状态条 + 标题行 + 第二行工作区/标签;右侧灰字时长;悬停出
305
- * 「继续 / 结束 / 释放」;最右常驻「查看」放大镜钮。
306
- *
307
- * B. `others`(其他待办)= 连体长表里的一行(用户 2026-10-18 定稿):**无卡片、无外框**,
308
- * 行 = 一条淡分隔线格:上下居中的状态圆点 + 任务(标题行 + 标签行;标签行最前是执行人
309
- * `@名字`,未认领则不摆)+ 状态列(阻塞/停滞 pill,待认领/进行中文字;「待认领」只出现一次)
310
- * + 最右「@创建人 创建于 年-月-日 时:分:秒」(悬停掉时分秒并让位给操作钮:没人在做 ⇒ 认领)
311
- * + 查看。圆点颜色:灰=待认领 / 浅蓝=进行中(比「我的」蓝灰一档)/ 黄=阻塞 / 红=停滞;
312
- * 停滞 pill 悬停给**配置出来的**阈值解释(staleDays 由 host 下发)。
313
- *
314
- * 按钮点击行为(含查看)归「任务按钮与会话交接」任务,本文件只做样子与提示。
315
- */
316
- /** 行标题:在办 = 任务名(缺则任务号);已结案 = 任务包名(缺则「完成了什么」首句)。 */
317
- function titleOf(row) {
318
- if (row.kind === "task") return row.task.title ?? row.task.id;
319
- const title = row.record.workpackage ?? row.record.summary;
320
- return title !== void 0 && title !== "" ? title : row.record.src ?? "—";
321
- }
322
- /** 状态语义(异常优先:stale 盖过 blocked)。 */
323
- function toneOf(row) {
324
- if (row.kind !== "task") return "closed";
325
- if (row.task.stale === true) return "stale";
326
- if (row.task.status === "blocked") return "blocked";
327
- if (row.task.status === "open") return "open";
328
- return "doing";
329
- }
330
- function TaskRowView(props) {
331
- const { t, row, variant, staleDays } = props;
332
- const tone = toneOf(row);
333
- const isTask = row.kind === "task";
334
- const tags = (row.kind === "task" ? row.task.tags : row.record.tags) ?? [];
335
- const created = row.kind === "task" ? row.task.created ?? null : row.record.created ?? null;
336
- const createdDay = created !== null && created !== void 0 && Date.parse(created) >= 0 ? formatDateTime(created).slice(0, 10) : null;
337
- const staleTip = staleDays === null || staleDays === void 0 ? void 0 : t("statStaleTitle", { n: staleDays });
338
- const viewButton = (0, react.createElement)("button", {
339
- type: "button",
340
- className: "dtl-viewbtn",
341
- "data-tip": t("viewDetail"),
342
- title: t("viewDetail")
343
- }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutlineRegular, { size: 14 }));
344
- const blockedPill = tone === "blocked" ? (0, react.createElement)("span", { className: "dtl-pill dtl-pill--blocked" }, t("statusBlocked")) : null;
345
- const stalePill = tone === "stale" ? (0, react.createElement)("span", {
346
- className: "dtl-pill dtl-pill--stale",
347
- title: staleTip
348
- }, t("statusStale")) : null;
349
- const titleLine = (0, react.createElement)("div", { className: "dtl-taskrow__l1" }, (0, react.createElement)("span", {
350
- className: "dtl-taskrow__title dtl-ellipsis",
351
- title: titleOf(row)
352
- }, titleOf(row)), createdDay === null ? null : (0, react.createElement)("span", { className: "dtl-taskrow__stamp" }, `[${t("fieldCreatedStamp", { day: createdDay })}]`), blockedPill, stalePill);
353
- const orowTitleLine = (0, react.createElement)("div", { className: "dtl-orow__title" }, (0, react.createElement)("span", {
354
- className: "dtl-orow__name dtl-ellipsis",
355
- title: titleOf(row)
356
- }, titleOf(row)));
357
- if (variant === "others") {
358
- const creator = isTask ? row.task.creator : row.record.owner;
359
- const owner = isTask ? row.task.owner : null;
360
- const hasOwner = !(owner === null || owner === void 0 || owner === "");
361
- const who = creator === null || creator === void 0 || creator === "" ? "—" : `@${creator}`;
362
- const createdOk = created !== null && created !== void 0 && Date.parse(created) >= 0;
363
- const ownerChip = hasOwner ? (0, react.createElement)("span", {
364
- className: "dtl-orow__owner",
365
- title: t("fieldOwner")
366
- }, `@${String(owner)}`) : null;
367
- const tagLine = hasOwner || tags.length > 0 ? (0, react.createElement)("div", { className: "dtl-orow__tags" }, ownerChip, tags.map((tag) => (0, react.createElement)("span", {
368
- key: tag,
369
- className: "dtl-minitag",
370
- title: `#${tag}`
371
- }, `#${tag}`))) : null;
372
- 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", {
373
- who,
374
- when: createdOk ? formatDateTime(String(created), { seconds: true }) : t("timeUnknown")
375
- })), (0, react.createElement)("span", { className: "dtl-orow__created-short" }, t("createdAt", {
376
- who,
377
- when: createdOk ? formatDateTime(String(created)) : t("timeUnknown")
378
- }))), (0, react.createElement)("span", { className: "dtl-orow__actions" }, hasOwner ? null : (0, react.createElement)("button", {
379
- type: "button",
380
- className: "dtl-minibtn",
381
- "data-tip": t("tipClaim")
382
- }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutlineRegular, { size: 14 }), t("actionClaim")), viewButton)));
383
- }
384
- const parts = [];
385
- if (isTask) {
386
- const startRaw = row.task.started ?? row.task.created;
387
- if (startRaw !== void 0 && startRaw !== null && startRaw !== "" && Date.parse(startRaw) >= 0) parts.push(`${formatDateTime(startRaw).slice(0, 10)} ${t("fieldStarted")}`);
388
- if (row.task.ageDays !== null && row.task.ageDays !== void 0) parts.push(row.task.ageDays === 0 ? t("ageToday") : t("ageDaysText", { n: row.task.ageDays }));
389
- } else parts.push(`${t("fieldFinished")} ${formatDateTime(row.record.time ?? row.record.created ?? "", { fallback: t("timeUnknown") })}`);
390
- 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: {
391
- flex: "1 1 auto",
392
- minWidth: 0
393
- } }, titleLine, (0, react.createElement)("div", { className: "dtl-taskrow__l2" }, (0, react.createElement)("span", {
394
- className: "dtl-wstag dtl-ellipsis",
395
- title: row.kind === "task" ? row.task.workspace.title : ""
396
- }, row.kind === "task" ? row.task.workspace.title : ""), tags.map((tag) => (0, react.createElement)("span", {
397
- key: tag,
398
- className: "dtl-minitag",
399
- title: `#${tag}`
400
- }, `#${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", {
401
- type: "button",
402
- className: "dtl-minibtn",
403
- "data-tip": t("tipResume")
404
- }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { size: 14 }), t("actionResume")) : null, isTask ? (0, react.createElement)("button", {
405
- type: "button",
406
- className: "dtl-minibtn",
407
- "data-tip": t("tipFinish")
408
- }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, { size: 14 }), t("actionFinish")) : null, isTask ? (0, react.createElement)("button", {
409
- type: "button",
410
- className: "dtl-minibtn",
411
- "data-tip": t("tipRelease")
412
- }, t("actionRelease")) : null, viewButton)));
413
- }
414
- //#endregion
415
316
  //#region src/client/tokens.ts
416
317
  /**
417
318
  * 语义 token 层:`--dtl-*`(dtl = dsh-task-ledger)。
@@ -736,28 +637,29 @@ body[data-ds-dark-theme]{
736
637
  .dtl-orow__row{display:flex;align-items:center;gap:12px;}
737
638
  .dtl-orow__title{display:flex;align-items:baseline;gap:6px;min-width:0;}
738
639
  .dtl-orow__name{font-size:var(--dtl-font-md);font-weight:500;line-height:var(--dtl-line-md);min-width:0;}
739
- /* 第二行:执行人 + #标签(与标题行间距拉开,用户 2026-10-18) */
640
+ /* 第二行:状态标签 + #标签(与标题行间距拉开,用户 2026-10-18;状态位与执行人**互换**后,
641
+ 状态搬到这里、且永远有 ⇒ 这一行不再有「空一排」的情况) */
740
642
  .dtl-orow__tags{display:flex;align-items:center;gap:var(--dtl-space-2);margin-top:7px;min-width:0;}
741
- /* 执行人:别人在做 ⇒ @名字 摆标签行最前(「待认领」只在状态列出现一次,不再重复) */
742
- .dtl-orow__owner{flex:none;padding:0 6px;border-radius:999px;background:var(--dtl-open-bg-soft);
743
- color:var(--dtl-fg-2);font-size:10px;line-height:16px;white-space:nowrap;}
744
- /* 列:任务(弹性)/ 状态 / 创建信息 / 操作 / 查看 */
643
+ /* 列:任务(弹性)/ 执行人 / 右侧槽(创建信息 ⇄ 操作钮) */
745
644
  .dtl-tcell{min-width:0;}
746
645
  .dtl-tcell--title{flex:1 1 auto;}
747
- .dtl-tcell--status{flex:none;width:76px;}
748
- .dtl-tcell--created{flex:none;display:flex;align-items:center;justify-content:flex-end;
646
+ /* 执行人列(原状态列,用户 2026-10-18 互换):标签最宽 20ch ⇒ 列宽按它给,标签再自己收到 100%
647
+ (名字过长时由标签自己的省略号兜住,不许溢出压到右边的槽) */
648
+ .dtl-tcell--status{flex:none;width:110px;}
649
+ .dtl-tcell--status .dtl-tag{max-width:100%;}
650
+ /* 悬停:**创建信息整条让位给操作钮**(用户 2026-10-18:不要「掉掉时分秒」那种半吊子,
651
+ 要整条换掉)。两个都**常驻布局**(visibility 切换、不用 display),格子宽度 = 两者较宽的
652
+ 那个 ⇒ hover 时行宽不变、**页面不跳**(跳了执行人的悬停提示就跟着飘) */
653
+ .dtl-orow__slot{display:grid;align-items:center;justify-items:end;flex:none;
749
654
  font-size:var(--dtl-font-xs);color:var(--dtl-fg-3);white-space:nowrap;font-variant-numeric:tabular-nums;}
750
- /* 悬停:时分秒让位给操作钮(与「我的待办」的 时间⇄按钮 同款互换) */
751
- .dtl-orow__created-short{display:none;}
752
- .dtl-orow:hover .dtl-orow__created-long{display:none;}
753
- .dtl-orow:hover .dtl-orow__created-short{display:inline;}
754
- .dtl-orow__actions{display:none;flex:none;gap:6px;}
755
- .dtl-orow:hover .dtl-orow__actions{display:inline-flex;}
756
- /* 状态列文字(待认领灰 / 进行中浅蓝;阻塞停滞仍是 pill) */
757
- .dtl-statustext{font-size:var(--dtl-font-xs);white-space:nowrap;}
758
- .dtl-statustext--doing{color:var(--dtl-business-dim);}
759
- .dtl-statustext--open{color:var(--dtl-fg-3);}
760
-
655
+ .dtl-orow__slot>*{grid-area:1/1;}
656
+ .dtl-orow__created{visibility:visible;}
657
+ .dtl-orow__actions{visibility:hidden;display:flex;align-items:center;gap:6px;}
658
+ .dtl-orow:hover .dtl-orow__created{visibility:hidden;}
659
+ .dtl-orow:hover .dtl-orow__actions{visibility:visible;}
660
+ /* 「查看」钮在表格行里也要常驻布局(display:none 会不算宽度 ⇒ hover 一出就又把行撑开) */
661
+ .dtl-orow .dtl-viewbtn{display:inline-flex;visibility:hidden;}
662
+ .dtl-orow:hover .dtl-viewbtn{visibility:visible;}
761
663
  /* 行首状态圆点:灰=待认领 / 浅蓝=进行中(比「我的」蓝灰一档)/ 黄=阻塞 / 红=停滞;
762
664
  6px(用户 2026-10-18:8px 太大) */
763
665
  .dtl-dot{display:inline-block;width:6px;height:6px;flex:none;border-radius:50%;}
@@ -810,15 +712,27 @@ body[data-ds-dark-theme]{
810
712
  .dtl-taskrow__row:hover .dtl-taskrow__when{display:none;}
811
713
  .dtl-taskrow__row:hover .dtl-taskrow__actions{display:inline-flex;}
812
714
 
813
- /* 异常 pill:阻塞=黄 / 停滞=红(进行中无标记,只有色条) */
814
- .dtl-pill{flex:none;padding:1px 8px;border-radius:999px;font-size:10px;line-height:16px;font-weight:600;white-space:nowrap;}
815
- .dtl-pill--blocked{background:var(--dtl-warning);color:var(--dtl-on-signal);}
816
- .dtl-pill--stale{background:var(--dtl-danger);color:var(--dtl-on-signal);}
817
-
818
- /* 标签迷你 chip(用户 2026-10-17:标签再小点儿)——10px、无边框、灰底 */
819
- .dtl-minitag{flex:none;padding:0 5px;border-radius:999px;background:var(--dtl-surface-2);
820
- color:var(--dtl-fg-3);font-size:10px;line-height:16px;white-space:nowrap;max-width:120px;
821
- overflow:hidden;text-overflow:ellipsis;}
715
+ /* 标签(用户 2026-10-18):状态 / 执行人 / #标签 **共用一枚件**,只换色调;
716
+ 最大宽度 20ch(≈10 个中文或 20 个字符),超出省略号、悬停跑马灯把全文滚出来。 */
717
+ .dtl-tag{display:inline-block;flex:none;max-width:20ch;padding:0 6px;border-radius:999px;
718
+ background:var(--dtl-chip-bg);color:var(--dtl-fg-3);font-size:10px;line-height:16px;
719
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;}
720
+ /* 状态四色 = **实心色块**(用户 2026-10-18:彩色字看不清 ⇒ 改色块 + 反色字):
721
+ 待认领灰 / 进行中蓝 / 阻塞黄 / 停滞红(与行首圆点同色系)。
722
+ 红黄蓝都是中调 ⇒ 白字两个主题都可读;灰块用「字色底 + 面色字」⇒ 明暗自动反过来 */
723
+ .dtl-tag--open{background:var(--dtl-fg-3);color:var(--dtl-surface-1);font-weight:600;}
724
+ .dtl-tag--doing{background:var(--dtl-business);color:var(--dtl-on-signal);font-weight:600;}
725
+ .dtl-tag--blocked{background:var(--dtl-warning);color:var(--dtl-on-signal);font-weight:600;}
726
+ .dtl-tag--stale{background:var(--dtl-danger);color:var(--dtl-on-signal);font-weight:600;}
727
+ /* 执行人:浅蓝底(与状态色区分:状态是「事怎么样」,人是「谁在做」) */
728
+ .dtl-tag--person{background:var(--dtl-open-bg-soft);color:var(--dtl-fg-2);}
729
+ /* 内层撑满真实宽度(inline-block ⇒ transform 才生效),外层负责裁切与省略号 */
730
+ .dtl-tag__text{display:inline-block;}
731
+ /* 跑马灯只挂在**真溢出**的标签上(短的挂上去会白飘一段) */
732
+ .dtl-tag--long:hover{text-overflow:clip;}
733
+ .dtl-tag--long:hover .dtl-tag__text{animation:dtl-marquee 6s linear infinite;}
734
+ @keyframes dtl-marquee{0%,12%{transform:translateX(0);}
735
+ 88%,100%{transform:translateX(calc(-100% + 20ch - 12px));}}
822
736
 
823
737
  /* 分区标题里的计数:与日期徽同款中括号小字(用户 2026-10-17);0 条不渲染 */
824
738
  .dtl-count{font-size:var(--dtl-font-xs);color:var(--dtl-fg-3);font-variant-numeric:tabular-nums;}
@@ -1011,6 +925,27 @@ body[data-ds-dark-theme]{
1011
925
  color: "var(--dtl-fg-2)"
1012
926
  } }, 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));
1013
927
  }
928
+ /** 标签上限宽度(字符数):1 个中文按 2 计 ⇒ 10 个中文 = 20 个字符(与 CSS 的 20ch 对齐)。 */
929
+ const TAG_MAX_WIDTH = 20;
930
+ /** 显示宽度:全角(中 / 日 / 韩 / 全角符号)一个顶两个英文字符。 */
931
+ function displayWidth(text) {
932
+ let width = 0;
933
+ for (const char of text) width += (char.codePointAt(0) ?? 0) > 11904 ? 2 : 1;
934
+ return width;
935
+ }
936
+ /**
937
+ * 标签(**唯一**一枚件,用户 2026-10-18):状态 / 执行人 / #标签 都走它,只换 tone。
938
+ * 超宽 ⇒ 省略号;悬停 ⇒ 跑马灯滚出全文(溢出时不挂 title,免得原生提示盖住滚动)。
939
+ */
940
+ function Tag(props) {
941
+ ensureUiBase();
942
+ const { text, tone = "neutral", title } = props;
943
+ const long = displayWidth(text) > TAG_MAX_WIDTH;
944
+ return (0, react.createElement)("span", {
945
+ className: `dtl-tag dtl-tag--${tone}${long ? " dtl-tag--long" : ""}`,
946
+ title: long ? void 0 : title ?? text
947
+ }, (0, react.createElement)("span", { className: "dtl-tag__text" }, text));
948
+ }
1014
949
  /** 空态:`guide` = 真空引导卡(虚线),`filtered` = 筛选空小字。 */
1015
950
  function EmptyState(props) {
1016
951
  ensureUiBase();
@@ -1138,6 +1073,154 @@ body[data-ds-dark-theme]{
1138
1073
  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));
1139
1074
  }
1140
1075
  //#endregion
1076
+ //#region src/client/task-row.ts
1077
+ /**
1078
+ * 任务行(宿主**无表格件** ⇒ 自绘),**两种形态**(用户 2026-10-17 定稿:两区视觉必须不同,
1079
+ * 且**互不重用**):
1080
+ *
1081
+ * A. `mine`(我的待办)= 整行一张卡,样式照抄 dispatch-table 任务配置卡:
1082
+ * 左缘 6×36 状态条 + 标题行 + 第二行工作区/标签;右侧灰字时长;悬停出
1083
+ * 「继续 / 结束 / 释放」;最右常驻「查看」放大镜钮。
1084
+ *
1085
+ * B. `others`(全部待办)= 连体长表里的一行(用户 2026-10-18 定稿):**无卡片、无外框**,
1086
+ * 行 = 一条淡分隔线格:上下居中的状态圆点 + 任务(标题行 + 标签行;**标签行最前是状态标签**)
1087
+ * + **执行人列**(原状态位,有执行人才摆 `@名字` 标签)+ 最右「@创建人 创建于 年-月-日 时:分:秒」
1088
+ * (悬停掉时分秒并让位给操作钮)+ 查看。
1089
+ * 状态一律**标签型、四色**(用户 2026-10-18:原来是一排素字 / pill,现在与人员**位置互换**):
1090
+ * 灰=待认领 / 蓝=进行中 / 黄=阻塞 / 红=停滞;停滞标签悬停给**配置出来的**阈值解释
1091
+ * (staleDays 由 host 下发)。圆点颜色同色系。
1092
+ *
1093
+ * 按钮(含查看)**不各判各的**:权限一律问 `resolveActions()`(rows.ts),本文件只按返回值画。
1094
+ *
1095
+ * 按钮点击行为(含查看)归「任务按钮与会话交接」任务,本文件只做样子与提示。
1096
+ */
1097
+ /** 行标题:在办 = 任务名(缺则任务号);已结案 = 任务包名(缺则「完成了什么」首句)。 */
1098
+ function titleOf(row) {
1099
+ if (row.kind === "task") return row.task.title ?? row.task.id;
1100
+ const title = row.record.workpackage ?? row.record.summary;
1101
+ return title !== void 0 && title !== "" ? title : row.record.src ?? "—";
1102
+ }
1103
+ /** 状态语义(异常优先:stale 盖过 blocked)。 */
1104
+ function toneOf(row) {
1105
+ if (row.kind !== "task") return "closed";
1106
+ if (row.task.stale === true) return "stale";
1107
+ if (row.task.status === "blocked") return "blocked";
1108
+ if (row.task.status === "open") return "open";
1109
+ return "doing";
1110
+ }
1111
+ /**
1112
+ * ⚠️ 调试态(用户 2026-10-18):第二区标签行把**四种状态全部写死**摆出来、不走 tone 判断,
1113
+ * 好一次看全四枚色块长什么样(第一个「进行中」,其后阻塞 / 停滞 / 待认领)。
1114
+ * 定稿后:删掉这个常量,标签行换回按 `tone` 取一枚。
1115
+ */
1116
+ const DEBUG_TONES = [
1117
+ "doing",
1118
+ "blocked",
1119
+ "stale",
1120
+ "open"
1121
+ ];
1122
+ /** 状态文案(已结案没有在办状态 ⇒ 不给标签)。 */
1123
+ function statusLabel(tone, t) {
1124
+ if (tone === "stale") return t("statusStale");
1125
+ if (tone === "blocked") return t("statusBlocked");
1126
+ if (tone === "open") return t("statusOpen");
1127
+ if (tone === "doing") return t("statusActive");
1128
+ return null;
1129
+ }
1130
+ function TaskRowView(props) {
1131
+ const { t, row, variant, staleDays, board } = props;
1132
+ const tone = toneOf(row);
1133
+ const isTask = row.kind === "task";
1134
+ const tags = (row.kind === "task" ? row.task.tags : row.record.tags) ?? [];
1135
+ const created = row.kind === "task" ? row.task.created ?? null : row.record.created ?? null;
1136
+ const createdDay = created !== null && created !== void 0 && Date.parse(created) >= 0 ? formatDateTime(created).slice(0, 10) : null;
1137
+ const staleTip = staleDays === null || staleDays === void 0 ? void 0 : t("statStaleTitle", { n: staleDays });
1138
+ const actions = isTask ? resolveActions(row.task, board) : CLOSED_ACTIONS;
1139
+ const viewButton = (0, react.createElement)("button", {
1140
+ type: "button",
1141
+ className: "dtl-viewbtn",
1142
+ "data-tip": t("viewDetail"),
1143
+ title: t("viewDetail")
1144
+ }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutlineRegular, { size: 14 }));
1145
+ const claimButton = (0, react.createElement)("button", {
1146
+ type: "button",
1147
+ className: "dtl-minibtn",
1148
+ "data-tip": t("tipClaim")
1149
+ }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutlineRegular, { size: 14 }), t("actionClaim"));
1150
+ const resumeButton = (0, react.createElement)("button", {
1151
+ type: "button",
1152
+ className: "dtl-minibtn",
1153
+ "data-tip": t("tipResume")
1154
+ }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { size: 14 }), t("actionResume"));
1155
+ const finishButton = (0, react.createElement)("button", {
1156
+ type: "button",
1157
+ className: "dtl-minibtn",
1158
+ "data-tip": t("tipFinish")
1159
+ }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, { size: 14 }), t("actionFinish"));
1160
+ const releaseButton = (0, react.createElement)("button", {
1161
+ type: "button",
1162
+ className: "dtl-minibtn",
1163
+ "data-tip": t("tipRelease")
1164
+ }, t("actionRelease"));
1165
+ const alarmTag = tone === "blocked" || tone === "stale" ? (0, react.createElement)(Tag, {
1166
+ text: statusLabel(tone, t) ?? "",
1167
+ tone,
1168
+ title: tone === "stale" ? staleTip : void 0
1169
+ }) : null;
1170
+ const titleLine = (0, react.createElement)("div", { className: "dtl-taskrow__l1" }, (0, react.createElement)("span", {
1171
+ className: "dtl-taskrow__title dtl-ellipsis",
1172
+ title: titleOf(row)
1173
+ }, titleOf(row)), createdDay === null ? null : (0, react.createElement)("span", { className: "dtl-taskrow__stamp" }, `[${t("fieldCreatedStamp", { day: createdDay })}]`), alarmTag);
1174
+ const orowTitleLine = (0, react.createElement)("div", { className: "dtl-orow__title" }, (0, react.createElement)("span", {
1175
+ className: "dtl-orow__name dtl-ellipsis",
1176
+ title: titleOf(row)
1177
+ }, titleOf(row)));
1178
+ if (variant === "others") {
1179
+ const creator = isTask ? row.task.creator : row.record.owner;
1180
+ const owner = isTask ? row.task.owner : null;
1181
+ const hasOwner = !(owner === null || owner === void 0 || owner === "");
1182
+ const who = creator === null || creator === void 0 || creator === "" ? "—" : `@${creator}`;
1183
+ const createdOk = created !== null && created !== void 0 && Date.parse(created) >= 0;
1184
+ const startedOk = isTask && row.task.started !== null && row.task.started !== void 0 && Date.parse(row.task.started) >= 0;
1185
+ const ownerTip = t("startedAt", {
1186
+ who: String(owner),
1187
+ when: startedOk ? formatDateTime(String(row.task.started)) : t("timeUnknown")
1188
+ });
1189
+ const tagLine = (0, react.createElement)("div", { className: "dtl-orow__tags" }, DEBUG_TONES.map((tone) => (0, react.createElement)(Tag, {
1190
+ key: tone,
1191
+ text: statusLabel(tone, t) ?? "",
1192
+ tone
1193
+ })), tags.map((tag) => (0, react.createElement)(Tag, {
1194
+ key: tag,
1195
+ text: `#${tag}`
1196
+ })));
1197
+ 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, {
1198
+ text: `@${String(owner)}`,
1199
+ tone: "person",
1200
+ title: ownerTip
1201
+ }) : null), (0, react.createElement)("span", { className: "dtl-orow__slot" }, (0, react.createElement)("span", { className: "dtl-orow__created" }, t("createdAt", {
1202
+ who,
1203
+ when: createdOk ? formatDateTime(String(created)) : t("timeUnknown")
1204
+ })), (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))));
1205
+ }
1206
+ const parts = [];
1207
+ if (isTask) {
1208
+ const startRaw = row.task.started ?? row.task.created;
1209
+ if (startRaw !== void 0 && startRaw !== null && startRaw !== "" && Date.parse(startRaw) >= 0) parts.push(`${formatDateTime(startRaw).slice(0, 10)} ${t("fieldStarted")}`);
1210
+ if (row.task.ageDays !== null && row.task.ageDays !== void 0) parts.push(row.task.ageDays === 0 ? t("ageToday") : t("ageDaysText", { n: row.task.ageDays }));
1211
+ } else parts.push(`${t("fieldFinished")} ${formatDateTime(row.record.time ?? row.record.created ?? "", { fallback: t("timeUnknown") })}`);
1212
+ 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: {
1213
+ flex: "1 1 auto",
1214
+ minWidth: 0
1215
+ } }, titleLine, (0, react.createElement)("div", { className: "dtl-taskrow__l2" }, (0, react.createElement)("span", {
1216
+ className: "dtl-wstag dtl-ellipsis",
1217
+ title: row.kind === "task" ? row.task.workspace.title : ""
1218
+ }, row.kind === "task" ? row.task.workspace.title : ""), tags.map((tag) => (0, react.createElement)(Tag, {
1219
+ key: tag,
1220
+ text: `#${tag}`
1221
+ })))), (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)));
1222
+ }
1223
+ //#endregion
1141
1224
  //#region src/client/board-home.ts
1142
1225
  /**
1143
1226
  * 看板首页(`main` 槽整页内容):6 统计卡 + 近 8 周趋势图 + 两区任务列表。
@@ -1412,7 +1495,8 @@ body[data-ds-dark-theme]{
1412
1495
  t,
1413
1496
  row,
1414
1497
  variant: "mine",
1415
- staleDays: board?.stats.staleDays ?? null
1498
+ staleDays: board?.stats.staleDays ?? null,
1499
+ board
1416
1500
  })), (0, react.createElement)("div", null, (0, react.createElement)("div", { style: regionHeadRowStyle }, (0, react.createElement)("span", { style: {
1417
1501
  color: "var(--dtl-fg-3)",
1418
1502
  display: "inline-flex",
@@ -1483,7 +1567,8 @@ body[data-ds-dark-theme]{
1483
1567
  key: group.workspaceId,
1484
1568
  t,
1485
1569
  group,
1486
- staleDays: board?.stats.staleDays ?? null
1570
+ staleDays: board?.stats.staleDays ?? null,
1571
+ board
1487
1572
  })))))))));
1488
1573
  }
1489
1574
  /** 角标:>0 才给数字,0 = undefined(不标)。 */
@@ -1494,7 +1579,7 @@ body[data-ds-dark-theme]{
1494
1579
  * 统计口径(用户 2026-10-18):共 N|待办 N|进行中 N 常显(0 也显);
1495
1580
  * 阻塞 / 停滞**只在 >0 时出现**。 */
1496
1581
  function WorkspaceGroupView(props) {
1497
- const { t, group, staleDays } = props;
1582
+ const { t, group, staleDays, board } = props;
1498
1583
  const [open, setOpen] = (0, react.useState)(group.rows.length <= GROUP_COLLAPSE_OVER);
1499
1584
  const stats = [
1500
1585
  t("groupTotal", { n: group.total }),
@@ -1520,7 +1605,8 @@ body[data-ds-dark-theme]{
1520
1605
  t,
1521
1606
  row,
1522
1607
  variant: "others",
1523
- staleDays
1608
+ staleDays,
1609
+ board
1524
1610
  }))) : null);
1525
1611
  }
1526
1612
  //#endregion
@@ -1619,6 +1705,7 @@ body[data-ds-dark-theme]{
1619
1705
  actionClaim: "认领",
1620
1706
  tipClaim: "认领这个任务,变成我在做",
1621
1707
  createdAt: "{who} 创建于 {when}",
1708
+ startedAt: "{who} 开始于 {when}",
1622
1709
  viewDetail: "查看详情",
1623
1710
  fieldCreatedStamp: "{day} 创建",
1624
1711
  mineEmpty: "您现在没有正在进行的工作",
@@ -1732,6 +1819,7 @@ body[data-ds-dark-theme]{
1732
1819
  actionClaim: "Claim",
1733
1820
  tipClaim: "claim this task for yourself",
1734
1821
  createdAt: "created by {who} on {when}",
1822
+ startedAt: "{who} started on {when}",
1735
1823
  viewDetail: "View details",
1736
1824
  fieldCreatedStamp: "created {day}",
1737
1825
  mineEmpty: "Nothing in progress right now",