nx-kv 0.23.0 → 0.24.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.
Files changed (28) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +1 -1
  3. package/src/web/frontend/modules/md/style.css +17 -0
  4. package/src/web/frontend/page/dir/task.jsx +6 -1
  5. package/src/web/frontend/page/group/index.jsx +15 -0
  6. package/src/web/frontend/page/progress/index.jsx +171 -33
  7. package/src/web/frontend/panels.jsx +12 -4
  8. package/src/web/public/assets/{Introduction-BFPD7dEs.js → Introduction-DGcd3YBk.js} +1 -1
  9. package/src/web/public/assets/{cloud-CcKgVB1k.js → cloud-E33bm4CD.js} +1 -1
  10. package/src/web/public/assets/{dag-Cn4d2qQ8.js → dag-ClRadqiu.js} +1 -1
  11. package/src/web/public/assets/{dir-TPbJjoUI.js → dir-Dj-3QRkO.js} +1 -1
  12. package/src/web/public/assets/group-Ckwoc5tW.js +1 -0
  13. package/src/web/public/assets/{index-CfxLBdFE.css → index--c-rj9Y8.css} +1 -1
  14. package/src/web/public/assets/{index-Dlx5BdmV.js → index-DMp4lRKl.js} +2 -2
  15. package/src/web/public/assets/{md-history-ji4xpx2J.js → md-history-CwtrJcZo.js} +1 -1
  16. package/src/web/public/assets/{member-picker-BH-XaSGJ.js → member-picker-BV1a-LiO.js} +1 -1
  17. package/src/web/public/assets/{overview-3P0pC6De.js → overview-CQW1MViJ.js} +1 -1
  18. package/src/web/public/assets/progress-CDiNmHVI.js +3 -0
  19. package/src/web/public/assets/{repos-BvJ_kmUv.js → repos-BYFj2sjE.js} +1 -1
  20. package/src/web/public/assets/{route-crumb-BGrg1YqO.js → route-crumb-DSgy8FTp.js} +1 -1
  21. package/src/web/public/assets/{settings-DCgK_btM.js → settings-BoAQ5YXH.js} +1 -1
  22. package/src/web/public/assets/{task-5ohM-kDI.js → task-Br1K-bzV.js} +2 -2
  23. package/src/web/public/assets/{template-slots-jmHCtXit.js → template-slots-CMB5ZTZE.js} +1 -1
  24. package/src/web/public/assets/{templates-DuxGMEgE.js → templates-DMVIPsdx.js} +1 -1
  25. package/src/web/public/assets/{upload-Cc8eUwzX.js → upload-CzfyPGrk.js} +1 -1
  26. package/src/web/public/index.html +2 -2
  27. package/src/web/public/assets/group-BVbsnISZ.js +0 -1
  28. package/src/web/public/assets/progress-CbJJg5bl.js +0 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  本文件记录对外可见的变更。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。
4
4
 
5
+ ## [0.24.0] - 2026-11-05 进程面板支持项目组
6
+
7
+ ### Added
8
+
9
+ - **进程面板支持按项目组聚合(任务 #56)**:组发布的任务散在成员项目各自的队列里,
10
+ 原来的进程面板锁定单个目录,看不见组的全景。现在:
11
+ - **组页**新增「进程」入口,打开即按组聚合——扫全部队列、收正文
12
+ `frontmatter.board` 指向本组的任务(与组页任务面板同一口径),卡片标题旁标出
13
+ 归属项目;非组任务不混入。
14
+ - **任务页**的工具栏「进程」按任务自身语义分流:任务带工作组语义(`board`)
15
+ 就按组打开,不带就仍按目录打开(现状不变)。
16
+ - **按项目筛选**:面板顶部一组 chips(「全部」+ 各成员项目,带计数),点击过滤
17
+ 进程卡,顶部统计(并发/跑/等/完/平均进度)随筛选范围联动。筛选是会话内状态,
18
+ 切组后自动回落「全部」。
19
+ - 跨队列批量删除按任务**所在队列**分桶逐桶提交(删除 API 按目录寻址),
20
+ 勾选键用 `topicKey:id` 复合键——不同队列的同号任务不会互相顶掉。
21
+
5
22
  ## [0.23.0] - 2026-11-05 定位轨可拖动
6
23
 
7
24
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nx-kv",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "nx-kv — KV 清单管理中枢(远程)+ 本机本地任务队列(localtask):serve 驱动 CLI 与 Web 面板,同源同构,附带可安装 skill 供 agent 提交 / 领取 / 阻塞接收任务",
5
5
  "type": "module",
6
6
  "bin": {
@@ -650,6 +650,23 @@
650
650
  font-weight:600;font-size:11px;font-family:inherit;color:var(--mid,#838A93);
651
651
  }
652
652
  .mdw-prog-all:hover{background:var(--mdw-accent-soft);color:var(--mdw-accent)}
653
+ /* 组模式的项目筛选 chips(任务 #56):「全部」+ 各成员项目,点击过滤进程卡;
654
+ 数量小字用 <b>,选中态用 accent 描边 */
655
+ .mdw-prog-filters{
656
+ flex:none;display:flex;flex-wrap:wrap;gap:6px;padding:8px 12px;
657
+ border-bottom:1px solid var(--line,#E4E2DC);
658
+ }
659
+ .mdw-prog-fchip{
660
+ border:1px solid var(--line,#E4E2DC);background:var(--paper,#fff);color:var(--ink-2,#4B515A);
661
+ border-radius:14px;padding:3px 10px;cursor:pointer;font-size:11px;font-weight:600;font-family:inherit;
662
+ display:inline-flex;align-items:center;gap:4px;
663
+ }
664
+ .mdw-prog-fchip b{font-weight:700;font-size:10px;color:var(--mid,#838A93)}
665
+ .mdw-prog-fchip:hover{border-color:var(--mdw-accent);color:var(--mdw-accent)}
666
+ .mdw-prog-fchip.on{background:var(--mdw-accent-soft);border-color:var(--mdw-accent);color:var(--mdw-accent)}
667
+ .mdw-prog-fchip.on b{color:var(--mdw-accent)}
668
+ /* 组模式卡片标题旁的项目 tag(复用 rp-ptag 视觉,略收间距贴标题) */
669
+ .mdw-tcard-proj{margin-left:6px}
653
670
  /* 勾选后的操作条:已选 n 条 + 清除 + 删除所选 */
654
671
  .mdw-prog-sel{
655
672
  flex:none;display:flex;align-items:center;gap:6px;padding:7px 10px;
@@ -325,13 +325,18 @@ export default function PagePanel({ dirKey, task, beside = false, tabId, model }
325
325
  // 工具栏「进程」:面板化之后进程不再是内嵌右侧栏,而是工作区里独立的一块
326
326
  // (可拖动 / 可浮出 / 可单独关掉)。已开着同一目录的进程面板就聚焦它,不重复开。
327
327
  // 注意 config 要带上 dirKey:进程面板是按**目录**取任务的,缺了会落到全局池。
328
+ // 任务带工作组语义(frontmatter.board,任务 #56)→ 进程按**组**聚合打开:
329
+ // 组发布的任务散在成员项目各自的队列里,单目录视图看不见全景。
328
330
  const openProgress = () => {
329
331
  const m = model || app.model;
330
332
  if (!m) return;
333
+ const boardId = docFm && docFm.board ? String(docFm.board) : '';
331
334
  openPanel(m, {
332
335
  component: 'progress',
333
336
  name: '进程',
334
- config: { dirKey: dirKey || GLOBAL_KEY, task: task != null ? String(task) : undefined },
337
+ config: boardId
338
+ ? { groupKey: boardId, task: task != null ? String(task) : undefined }
339
+ : { dirKey: dirKey || GLOBAL_KEY, task: task != null ? String(task) : undefined },
335
340
  location: 'right',
336
341
  });
337
342
  };
@@ -228,6 +228,21 @@ export default function GroupView({ dirKey, rows, repoData, onNavigate, model })
228
228
  >
229
229
  + 新增任务
230
230
  </button>
231
+ <button
232
+ className="btn small ghost"
233
+ title="打开本组的任务进程面板:组内各项目队列聚合显示,可按项目筛选(任务 #56)"
234
+ onClick={() => {
235
+ if (!model) return;
236
+ openPanel(model, {
237
+ component: 'progress',
238
+ name: '进程',
239
+ config: { groupKey: dirKey },
240
+ location: 'right',
241
+ });
242
+ }}
243
+ >
244
+ 进程
245
+ </button>
231
246
  <button
232
247
  className="btn small ghost"
233
248
  title="挑项目加进这个组(可搜索项目与项目组)"
@@ -2,11 +2,16 @@
2
2
  //
3
3
  // 每张卡片左侧带复选框:多选后批量删除(软删除,CLI localtask task restore 可恢复)。
4
4
  // 点卡片 → 在工作区里开一个 MD 页签(不是切内部 tab)。
5
+ //
6
+ // 组模式(任务 #56):config.groupKey 指向项目组(board)时,进程按**组**聚合——
7
+ // 组发布的任务散在成员项目各自的队列里,单目录视图看不见全景;这里扫全部队列、
8
+ // 收正文 frontmatter.board 指向本组的任务(与组页 groupTasks 同一口径),卡片带
9
+ // 项目 tag,顶部一组筛选 chips 按**项目**过滤,统计随筛选范围走。
5
10
  import { useEffect, useMemo, useState } from 'react';
6
11
  import { api } from '../../api/client.js';
7
12
  import { Icon } from '../../components/icons.jsx';
8
13
  import { GLOBAL_KEY } from '../../modules/board/board-view.js';
9
- import { STAGE_LABELS, taskToProgress } from '../../modules/md/md-parse.js';
14
+ import { parseFrontmatter, STAGE_LABELS, taskToProgress } from '../../modules/md/md-parse.js';
10
15
  import { closeRightSidebar, findOpenPanel } from '../../workspace.js';
11
16
  import { useToast } from '../../components/toast/index.jsx';
12
17
  import { useDialog } from '../../components/popup/index.jsx';
@@ -46,6 +51,7 @@ function stageChips(card) {
46
51
 
47
52
  export default function ProgressPanel({
48
53
  dirKey, task: focusTask, rows = [], refreshLocal, model, onOpenTask, onClosePage,
54
+ group = null,
49
55
  }) {
50
56
  const toast = useToast();
51
57
  const { dialog, node: dialogNode } = useDialog();
@@ -58,7 +64,8 @@ export default function ProgressPanel({
58
64
  const tasks = useMemo(() => (topic?.tasks || []).filter((t) => !t.deletedAt), [topic]);
59
65
 
60
66
  const [now, setNow] = useState(Date.now());
61
- // 多选:存任务 id(不是下标——列表会重排,下标会指错任务)
67
+ // 多选:存卡片键(目录模式 = 任务 id 数字;组模式 = "topicKey:id" 复合键——
68
+ // 不同队列同号任务多的是,拿裸 id 当键会把两个队列的 #1 当成同一张卡)
62
69
  const [picked, setPicked] = useState(() => new Set());
63
70
  const [busy, setBusy] = useState(false);
64
71
  useEffect(() => {
@@ -66,21 +73,84 @@ export default function ProgressPanel({
66
73
  return () => clearInterval(t);
67
74
  }, []);
68
75
 
76
+ // ─── 组模式聚合(任务 #56)──────────────────────────────────────
77
+ // 本组任务 = 全部队列里正文 frontmatter.board 指向本组的任务(与组页同口径)。
78
+ // 每张卡带 topicKey/topicCwd(点开、删除要按任务**所在队列**寻址)与项目名(筛选用)。
79
+ const groupCards = useMemo(() => {
80
+ if (!group) return [];
81
+ const ids = [group.id, group.key, group.name].filter(Boolean);
82
+ const members = group.repos || [];
83
+ const projectNameOf = (projKey, row) => {
84
+ const m = members.find(
85
+ (x) => x && (x.key === projKey || x.name === projKey || (x.path && x.path === projKey)),
86
+ );
87
+ if (m && m.name) return m.name;
88
+ if (projKey) return String(projKey).split('/').pop() || String(projKey);
89
+ return (row && row.name) || '';
90
+ };
91
+ const out = [];
92
+ for (const r of rows || []) {
93
+ if (!r || r.isGlobal || !Array.isArray(r.tasks)) continue;
94
+ for (const t of r.tasks) {
95
+ if (!t || t.deletedAt) continue;
96
+ let fm = {};
97
+ try {
98
+ fm = parseFrontmatter(t.text || '');
99
+ } catch {
100
+ fm = {};
101
+ }
102
+ if (!fm.board || !ids.includes(fm.board)) continue;
103
+ out.push({
104
+ ...taskToProgress(t),
105
+ key: `${r.key}:${t.id}`,
106
+ taskId: t.id,
107
+ topicKey: r.key,
108
+ topicCwd: r.cwd || '',
109
+ project: fm.project || '',
110
+ projectName: projectNameOf(fm.project, r),
111
+ });
112
+ }
113
+ }
114
+ const order = { run: 0, wait: 1, done: 2 };
115
+ return out.sort(
116
+ (a, b) => (order[a.status] ?? 9) - (order[b.status] ?? 9)
117
+ || a.projectName.localeCompare(b.projectName)
118
+ || Number(a.taskId) - Number(b.taskId),
119
+ );
120
+ }, [group, rows]);
121
+
122
+ // 项目筛选(组模式):'' = 全部;值为 projectName。会话内状态,不持久化——
123
+ // 筛选是临时视角;切组后旧筛选不在新组的项目列表里,派生时自动回落「全部」。
124
+ const [projFilter, setProjFilter] = useState('');
125
+
126
+ const projects = useMemo(() => {
127
+ const seen = new Map();
128
+ for (const c of groupCards) {
129
+ if (c.projectName && !seen.has(c.projectName)) seen.set(c.projectName, true);
130
+ }
131
+ return [...seen.keys()];
132
+ }, [groupCards]);
133
+
69
134
  const cards = useMemo(() => {
135
+ if (group) {
136
+ const active = projFilter && projects.includes(projFilter) ? projFilter : '';
137
+ return groupCards.filter((c) => !active || c.projectName === active);
138
+ }
70
139
  const list = tasks.map(taskToProgress);
71
140
  const order = { run: 0, wait: 1, done: 2 };
72
141
  return list.sort((a, b) => (order[a.status] ?? 9) - (order[b.status] ?? 9));
73
- }, [tasks]);
142
+ }, [group, groupCards, projFilter, projects, tasks]);
74
143
 
75
144
  // 选中的任务可能已被别处删掉 → 只留还活着的编号(否则「删除所选」带着死编号)
145
+ const cidOf = (c) => (group ? c.key : c.id); // 组模式 = 复合键,与 picked Set 同口径
76
146
  useEffect(() => {
77
147
  setPicked((prev) => {
78
148
  if (!prev.size) return prev;
79
- const alive = new Set(cards.map((c) => c.id));
149
+ const alive = new Set(cards.map(cidOf));
80
150
  const next = new Set([...prev].filter((id) => alive.has(id)));
81
151
  return next.size === prev.size ? prev : next;
82
152
  });
83
- }, [cards]);
153
+ }, [cards, group]);
84
154
 
85
155
  const live = cards.filter((c) => c.status === 'run' || c.status === 'wait');
86
156
  const running = cards.filter((c) => c.status === 'run').length;
@@ -91,7 +161,7 @@ export default function ProgressPanel({
91
161
  : 0;
92
162
 
93
163
  const allPicked = cards.length > 0 && picked.size === cards.length;
94
- const pickedCards = cards.filter((c) => picked.has(c.id));
164
+ const pickedCards = cards.filter((c) => picked.has(cidOf(c)));
95
165
 
96
166
  const togglePick = (id) => {
97
167
  setPicked((s) => {
@@ -102,17 +172,17 @@ export default function ProgressPanel({
102
172
  });
103
173
  };
104
174
  const toggleAll = () => {
105
- setPicked(allPicked ? new Set() : new Set(cards.map((c) => c.id)));
175
+ setPicked(allPicked ? new Set() : new Set(cards.map(cidOf)));
106
176
  };
107
177
 
108
- // 批量删除(软删除):一次请求逐条删,顺手关掉已打开的页签
178
+ // 批量删除(软删除):目录模式一次请求;组模式按任务**所在队列**分桶逐桶删
179
+ //(删除 API 按目录寻址,跨队列的选中项必须拆开)
109
180
  const delPicked = async () => {
110
181
  if (busy || !pickedCards.length) return;
111
- const ids = pickedCards.map((c) => c.id);
112
- const names = pickedCards.map((c) => `#${c.id} ${c.title}`);
182
+ const names = pickedCards.map((c) => `#${c.taskId ?? c.id} ${c.title}`);
113
183
  const shown = names.length <= 6 ? names.join('\n') : `${names.slice(0, 6).join('\n')}… 共 ${names.length} 条`;
114
184
  const ok = await dialog({
115
- title: `删除选中的 ${ids.length} 条任务`,
185
+ title: `删除选中的 ${pickedCards.length} 条任务`,
116
186
  message: `${shown}\n\n软删除:CLI localtask task restore 可恢复。`,
117
187
  danger: true,
118
188
  okText: '删除',
@@ -120,22 +190,41 @@ export default function ProgressPanel({
120
190
  if (!ok) return;
121
191
  setBusy(true);
122
192
  try {
123
- const res = await api('/api/localtasks/items', {
124
- method: 'DELETE',
125
- body: { ...(isGlobal ? { key: GLOBAL_KEY } : { cwd: topic.cwd }), ids },
126
- });
127
- const n = res?.count ?? ids.length;
128
- const miss = res?.missing?.length ? `(${res.missing.length} 条已不存在)` : '';
129
- // 关掉这些任务的页签(开着的话)
193
+ let n = 0;
194
+ let miss = 0;
195
+ if (group) {
196
+ const byTopic = new Map();
197
+ for (const c of pickedCards) {
198
+ if (!byTopic.has(c.topicKey)) byTopic.set(c.topicKey, { cwd: c.topicCwd, ids: [] });
199
+ byTopic.get(c.topicKey).ids.push(c.taskId);
200
+ }
201
+ for (const { cwd, ids } of byTopic.values()) {
202
+ const res = await api('/api/localtasks/items', { method: 'DELETE', body: { cwd, ids } });
203
+ n += res?.count ?? ids.length;
204
+ miss += res?.missing?.length || 0;
205
+ }
206
+ } else {
207
+ const ids = pickedCards.map((c) => c.id);
208
+ const res = await api('/api/localtasks/items', {
209
+ method: 'DELETE',
210
+ body: { ...(isGlobal ? { key: GLOBAL_KEY } : { cwd: topic.cwd }), ids },
211
+ });
212
+ n = res?.count ?? ids.length;
213
+ miss = res?.missing?.length || 0;
214
+ }
215
+ const missTxt = miss ? `(${miss} 条已不存在)` : '';
216
+ // 关掉这些任务的页签(开着的话)——组模式按各卡所在目录找页签
130
217
  if (model) {
131
- for (const id of ids) {
132
- const page = findOpenPanel(model, 'page', { dirKey, task: String(id) });
218
+ for (const c of pickedCards) {
219
+ const dk = group ? c.topicKey : dirKey;
220
+ const tid = group ? c.taskId : c.id;
221
+ const page = findOpenPanel(model, 'page', { dirKey: dk, task: String(tid) });
133
222
  if (page && onClosePage) onClosePage(page.getId());
134
223
  }
135
224
  }
136
225
  setPicked(new Set());
137
226
  refreshLocal();
138
- toast(`已删除 ${n} 条任务${miss}`);
227
+ toast(`已删除 ${n} 条任务${missTxt}`);
139
228
  } catch (e) {
140
229
  // 失败保留勾选,便于改了再试
141
230
  toast('删除失败: ' + (e.message || e));
@@ -144,8 +233,11 @@ export default function ProgressPanel({
144
233
  }
145
234
  };
146
235
 
147
- const openTask = (id) => {
148
- if (onOpenTask) onOpenTask(id);
236
+ // 点卡打开任务:组模式的任务散在各成员目录,要带**所在队列**的 dirKey 寻址
237
+ const openCard = (c) => {
238
+ if (!onOpenTask) return;
239
+ if (group) onOpenTask(c.taskId, c.topicKey);
240
+ else onOpenTask(c.id);
149
241
  };
150
242
 
151
243
  const focusId = focusTask != null && String(focusTask) !== '' ? String(focusTask) : '';
@@ -154,7 +246,9 @@ export default function ProgressPanel({
154
246
  <div className="mdw-prog-host">
155
247
  {dialogNode}
156
248
  <div className="mdw-prog-h">
157
- <span className="t">{topic?.name || (isGlobal ? '全局任务池' : '任务进程')}</span>
249
+ <span className="t">
250
+ {group ? `${group.name} · 任务进程` : topic?.name || (isGlobal ? '全局任务池' : '任务进程')}
251
+ </span>
158
252
  {cards.length > 0 && (
159
253
  <button
160
254
  type="button"
@@ -177,6 +271,27 @@ export default function ProgressPanel({
177
271
  </button>
178
272
  )}
179
273
  </div>
274
+ {group && projects.length > 1 && (
275
+ <div className="mdw-prog-filters" role="tablist" aria-label="按项目筛选进程卡">
276
+ <button
277
+ type="button"
278
+ className={'mdw-prog-fchip' + (!projFilter || !projects.includes(projFilter) ? ' on' : '')}
279
+ onClick={() => setProjFilter('')}
280
+ >
281
+ 全部 <b>{groupCards.length}</b>
282
+ </button>
283
+ {projects.map((p) => (
284
+ <button
285
+ key={p}
286
+ type="button"
287
+ className={'mdw-prog-fchip' + (projFilter === p ? ' on' : '')}
288
+ onClick={() => setProjFilter(projFilter === p ? '' : p)}
289
+ >
290
+ {p} <b>{groupCards.filter((c) => c.projectName === p).length}</b>
291
+ </button>
292
+ ))}
293
+ </div>
294
+ )}
180
295
  <div className="mdw-prog-sum">
181
296
  <div className="pct-row">
182
297
  <span className="pct">{avg}%</span>
@@ -204,30 +319,49 @@ export default function ProgressPanel({
204
319
  )}
205
320
  <div className="mdw-prog-list">
206
321
  {!cards.length && (
207
- <div className="mdw-prog-empty">此目录还没有 MD 任务<br />写 MD 提交或导入</div>
322
+ <div className="mdw-prog-empty">
323
+ {group ? (
324
+ <>
325
+ 本组还没有进行中的任务<br />在本组创建的任务会出现在这里
326
+ </>
327
+ ) : (
328
+ <>
329
+ 此目录还没有 MD 任务<br />写 MD 提交或导入
330
+ </>
331
+ )}
332
+ </div>
208
333
  )}
209
334
  {cards.map((c) => {
210
- const on = picked.has(c.id);
211
- const isFocus = focusId !== '' && String(c.id) === focusId;
335
+ const cid = cidOf(c);
336
+ const on = picked.has(cid);
337
+ const isFocus = focusId !== '' && String(c.taskId ?? c.id) === focusId;
212
338
  return (
213
- <div className={`mdw-tcard-row${on ? ' on' : ''}`} key={c.id}>
339
+ <div className={`mdw-tcard-row${on ? ' on' : ''}`} key={cid}>
214
340
  <input
215
341
  type="checkbox"
216
342
  className="mdw-tcard-pick"
217
343
  checked={on}
218
- onChange={() => togglePick(c.id)}
219
- aria-label={`选择任务 #${c.id} ${c.title}`}
344
+ onChange={() => togglePick(cid)}
345
+ aria-label={`选择任务 #${c.taskId ?? c.id} ${c.title}`}
220
346
  title="选择此任务(可多选后批量删除)"
221
347
  />
222
348
  <button
223
349
  type="button"
224
350
  className={`mdw-tcard ${c.status}${isFocus ? ' focus' : ''}`}
225
- onClick={() => openTask(c.id)}
351
+ onClick={() => openCard(c)}
226
352
  >
227
353
  <div className="mdw-tcard-h">
228
354
  <span className={`st ${c.status}`} />
229
355
  <div className="titles">
230
- <div className="n">{c.title}<span className="kind">md</span></div>
356
+ <div className="n">
357
+ {c.title}
358
+ {group && c.projectName && (
359
+ <span className="rp-ptag mdw-tcard-proj" title={`归属项目:${c.projectName}`}>
360
+ {c.projectName}
361
+ </span>
362
+ )}
363
+ <span className="kind">md</span>
364
+ </div>
231
365
  <div className="sub">{c.detail}</div>
232
366
  </div>
233
367
  <span
@@ -244,7 +378,11 @@ export default function ProgressPanel({
244
378
  );
245
379
  })}
246
380
  </div>
247
- <div className="mdw-prog-note">同一目录下任务<strong>并发</strong>。点卡打开对应 MD;勾选后可批量删除。</div>
381
+ <div className="mdw-prog-note">
382
+ {group
383
+ ? '组发布的任务散在各成员项目队列,这里聚合显示;按项目筛选,勾选后可跨队列批量删除。'
384
+ : <>同一目录下任务<strong>并发</strong>。点卡打开对应 MD;勾选后可批量删除。</>}
385
+ </div>
248
386
  </div>
249
387
  );
250
388
  }
@@ -49,17 +49,25 @@ function PagePanelView({ dirKey, task, beside, tabId, model }) {
49
49
  return wrap(<PagePanel dirKey={dirKey} task={task} beside={!!beside} tabId={tabId} model={model} />);
50
50
  }
51
51
 
52
- /** 并发进程面板:独立成块,可拖到任意一边;task = 当前主区任务,用于高亮 */
53
- function ProgressPanelView({ dirKey, task }) {
52
+ /** 并发进程面板:独立成块,可拖到任意一边;task = 当前主区任务,用于高亮。
53
+ * groupKey(任务 #56)= 项目组(board)id:进程按**组**聚合组内各项目队列的任务,
54
+ * 卡片带项目 tag 并可按项目筛选;不传 = 目录/全局模式(现状)。 */
55
+ function ProgressPanelView({ dirKey, task, groupKey }) {
54
56
  const app = useApp() || {};
57
+ const board = groupKey
58
+ ? ((app.repoData && app.repoData.boards) || []).find(
59
+ (b) => b && (b.id === groupKey || b.key === groupKey),
60
+ ) || null
61
+ : null;
55
62
  return wrap(
56
63
  <ProgressPanel
57
64
  dirKey={dirKey}
58
65
  task={task}
66
+ group={board}
59
67
  rows={app.rows}
60
68
  refreshLocal={app.refreshLocal}
61
69
  model={app.model}
62
- onOpenTask={(id) => app.openPage && app.openPage({ dirKey, task: String(id), name: `#${id}` })}
70
+ onOpenTask={(id, dk) => app.openPage && app.openPage({ dirKey: dk || dirKey, task: String(id), name: `#${id}` })}
63
71
  onClosePage={(tabId) => app.closePanel && app.closePanel(tabId)}
64
72
  />
65
73
  );
@@ -98,7 +106,7 @@ export const PANELS = {
98
106
  model={ctx && ctx.model}
99
107
  />
100
108
  ) },
101
- progress: { title: '进程', render: (cfg) => <ProgressPanelView dirKey={cfg && cfg.dirKey} task={cfg && cfg.task} /> },
109
+ progress: { title: '进程', render: (cfg) => <ProgressPanelView dirKey={cfg && cfg.dirKey} task={cfg && cfg.task} groupKey={cfg && cfg.groupKey} /> },
102
110
  group: { title: '项目组', render: (cfg, ctx) => <GroupPanel boardId={cfg && cfg.boardId} model={ctx && ctx.model} /> },
103
111
  repos: { title: '仓库管理', render: () => wrap(<ReposView />) },
104
112
  cloud: { title: '云端总览', render: () => wrap(<TodoView />) },
@@ -1 +1 @@
1
- import{d as e,f as t,h as n,m as r,o as i,u as a}from"./attach-v8BxRJ7p.js";import{C as o,D as s,W as c,b as l,j as u,p as d,q as f,y as p}from"./index-Dlx5BdmV.js";import{a as m,i as h,n as g,o as _,r as v,t as y}from"./md-history-ji4xpx2J.js";var b=r({default:()=>w,useWikiNav:()=>C}),x=n(t(),1),S=e();function C(e){let t=c()||{},n=a(),r=e||t.model,i=(0,x.useRef)(null),o=(0,x.useCallback)(async()=>{if(!i.current)try{i.current=(await f(`/api/docs/index`)).index||[]}catch{i.current=[]}return i.current},[]),l=(0,x.useCallback)(async(e,t,n={})=>{let i=(t||await o()).find(t=>t.docId===e);u(r,{component:`doc`,name:i?i.name:e,config:{docId:e,...n.repoKey?{repoKey:n.repoKey}:{},...n.boardKey?{boardKey:n.boardKey,boardName:n.boardName||``}:{}},location:`center`})},[r,o]),d=(0,x.useCallback)(async e=>{let t=await o(),r=s(e,t);if(r&&r.docId)return l(r.docId,t);if(r&&r.candidates)return n(`「${e}」同名文档多份:${r.candidates.join(`、`)}`);n(`未找到「${e}」——可用 nx-kv docs add <rel> 创建`)},[o,l,n]);return{nav:d,onClick:(0,x.useCallback)(e=>{let t=e.target.closest(`[data-wl]`);t&&(e.preventDefault(),d(t.getAttribute(`data-wl`)))},[d]),ensureIndex:o,openByDocId:l}}function w({docId:e,repoKey:t,boardKey:n,boardName:r,model:s}){let c=a(),{dialog:u,node:b}=i(),w=(0,x.useRef)(new Map),E=(0,x.useRef)(null),D=(0,x.useRef)(null),{onClick:O,openByDocId:k}=C(s),A=t?`repo`:n?`board`:``,j=t||n||``,M=(0,x.useMemo)(()=>t?{repoKey:t}:n?{boardKey:n,boardName:r||``}:{},[t,n,r]),[N,P]=(0,x.useState)(null),[F,I]=(0,x.useState)(``),[L,R]=(0,x.useState)(!1),[z,B]=(0,x.useState)(`split`),[V,H]=(0,x.useState)([]),[U,W]=(0,x.useState)(null),G=(0,x.useCallback)(async()=>{I(``);try{let t=await f(`/api/docs/item?ref=`+encodeURIComponent(e||``));P(t),R(!1)}catch(e){I(String(e.message||e)),P(null)}},[e]);(0,x.useEffect)(()=>{P(null),G()},[G]);let K=(0,x.useCallback)(()=>{if(!j){H([]);return}if(A===`repo`){f(`/api/repos/docs?id=`+encodeURIComponent(j)).then(t=>{let n=[],r=(e,t)=>{if(!e)return;let r=e.rel?`rel:`+e.rel:e.path?`path:`+e.path:``;r&&n.push({docId:r,label:e.rel||e.path||(t?`主篇`:`补充`),note:e.note||``,main:t})};r(t.main||{rel:`repo/${j}.md`},!0);for(let e of t.links||[])r(e,!1);e&&!n.some(t=>t.docId===e)&&n.push({docId:e,label:`当前`,note:``,main:!0}),H(n)}).catch(()=>H(e?[{docId:e,label:`当前`,note:``,main:!0}]:[]));return}f(`/api/boards`).then(t=>{let n=(t&&t.boards||(Array.isArray(t)?t:[])).find(e=>e.id===j||e.key===j),r=[];e&&r.push({docId:e,label:`当前`,note:``,main:!0});for(let e of n&&n.docLinks||[]){let t=e.rel?`rel:${e.rel}`:e.path?`path:${e.path}`:``;t&&!r.some(e=>e.docId===t)&&r.push({docId:t,label:e.rel||e.path||`补充`,note:e.note||``,main:!1})}H(r)}).catch(()=>H(e?[{docId:e,label:`当前`,note:``,main:!0}]:[]))},[j,A,e]);(0,x.useEffect)(()=>{K()},[K]);let[q,J]=(0,x.useState)(null);(0,x.useEffect)(()=>{f(`/api/docs/index`).then(e=>{let t=new Set;for(let n of e.index||[])t.add(n.name),n.rel&&t.add(String(n.rel).toLowerCase()),n.path&&t.add(String(n.path).replace(/\\/g,`/`).toLowerCase());J(t)}).catch(()=>{})},[]);let Y=!!(N&&N.writable),X=(0,x.useMemo)(()=>N?p(N.body,[],Y?q:null):``,[N,q,Y]),te=(0,x.useMemo)(()=>N?d(N.body):[],[N]);(0,x.useEffect)(()=>{let e=D.current;if(!e)return;let t=()=>e.querySelector(`.mdw-preview, .mdw-lp`)||null;W(t());let n=typeof MutationObserver<`u`?new MutationObserver(()=>W(t())):null;return n&&n.observe(e,{childList:!0,subtree:!0}),()=>{n&&n.disconnect()}},[N,z,X]);let Z=e=>e().catch(e=>c(String(e.message||e))),ne=(0,x.useCallback)(e=>k(e,null,M),[k,M]),re=()=>{j&&Z(async()=>{let t=await u({title:`补充介绍文档`,input:!0,placeholder:`文档名(如 architecture / api-notes)`});if(t===null)return;let n=l(String(t).trim()||`note`).replace(/\.md$/i,``),i=`${String(e||``).startsWith(`rel:`)?T(e):A===`repo`?`repo/${j}`:`board/${r||j}`}/reference/${n}.md`;await f(`/api/docs/item`,{method:`POST`,body:{rel:i,body:`# ${String(t).trim()||n}\n\n(待补)\n`}});try{A===`repo`?await f(`/api/repos/docs`,{method:`POST`,body:{id:j,rel:i}}):await f(`/api/docs/link`,{method:`POST`,body:{board:j,doc:i}})}catch{}c(`已补充:${i}`),K(),k(`rel:`+i,null,M)})},Q=()=>Z(async()=>{if(!N||!Y)return;let t=await f(`/api/docs/item`,{method:`PATCH`,body:{ref:e,body:N.body}});P(e=>({...e,body:N.body,summary:t.summary})),R(!1),y(w.current,e),c(t.summary?`已保存,摘要已回写`:`已保存`)}),ie=(t,n=`op`)=>{N&&(g(w.current,e,{body:N.body},n),R(!0),P(e=>({...e,body:t,title:o(t,e.title)})))},ae=(t,n=`typing`)=>{if(!N)return;g(w.current,e,{body:N.body},n),R(!0);let r=/^#\s+/m.test(N.body)?N.body.replace(/^#\s+.+$/m,`# ${t||`未命名文档`}`):`# ${t||`未命名文档`}\n\n${N.body}`;P(e=>({...e,body:r,title:t}))},oe=()=>{if(!N)return;let t=h(w.current,e,{body:N.body});t&&(P(e=>({...e,body:t.body,title:o(t.body,e.title)})),R(!0),requestAnimationFrame(()=>E.current?.caretFromDiff?.(N.body,t.body)))},se=()=>{if(!N)return;let t=v(w.current,e,{body:N.body});t&&(P(e=>({...e,body:t.body,title:o(t.body,e.title)})),R(!0),requestAnimationFrame(()=>E.current?.caretFromDiff?.(N.body,t.body)))},ce=()=>Z(async()=>{let t=String(e||``).startsWith(`rel:`)?e.slice(4):String(e||``).startsWith(`path:`)?``:String(e||``);if(!t)return;let n=t.replace(/\.md$/i,``).split(`/`).pop()||`未命名文档`;await f(`/api/docs/item`,{method:`POST`,body:{rel:t,body:`# ${n}\n\n(待补)\n`}}),c(`已创建:${t}`),await G()});if(N&&N.dir)return(0,S.jsxs)(`div`,{className:`mdw docp`,children:[(0,S.jsxs)(`div`,{className:`docp-head`,children:[(0,S.jsx)(`span`,{className:`docp-path mono`,children:N.path}),(0,S.jsx)(`span`,{className:`docp-tag ext`,children:`外部目录 · 只读`})]}),(0,S.jsxs)(`div`,{className:`docp-files`,children:[N.files.map(e=>(0,S.jsx)(`button`,{className:`docp-file`,onClick:()=>k(`path:`+e,null),children:e},e)),!N.files.length&&(0,S.jsx)(`div`,{className:`muted pad`,children:`(目录里还没有 .md 文档)`})]})]});if(F)return(0,S.jsx)(`div`,{className:`mdw docp`,children:(0,S.jsxs)(`div`,{className:`pad stack`,children:[(0,S.jsx)(`div`,{children:(0,S.jsx)(`strong`,{children:`文档打开失败`})}),(0,S.jsx)(`div`,{className:`hint`,children:F})]})});if(!N)return(0,S.jsx)(`div`,{className:`mdw`,children:(0,S.jsx)(`div`,{className:`pad muted`,children:`载入中…`})});if(N.missing){let t=String(e||``).startsWith(`rel:`)||!String(e||``).startsWith(`path:`);return(0,S.jsxs)(`div`,{className:`mdw docp`,children:[(0,S.jsx)(`div`,{className:`docp-head`,children:(0,S.jsx)(`span`,{className:`docp-path mono`,children:N.rel||N.path||e})}),(0,S.jsxs)(`div`,{className:`pad stack`,children:[(0,S.jsx)(`div`,{children:(0,S.jsx)(`strong`,{children:`文档还没有落盘`})}),(0,S.jsx)(`div`,{className:`hint`,children:`描述内容的权威是磁盘上的 .md 文件——对应的字符串字段会在读取时播种,也可以直接创建骨架。`}),t&&(0,S.jsx)(`div`,{children:(0,S.jsx)(`button`,{className:`btn sm`,onClick:ce,children:`创建骨架`})})]})]})}let $=Y?z:`preview`;return(0,S.jsxs)(`div`,{className:`mdw docp`,"data-mode":$,onClick:O,ref:D,children:[(0,S.jsxs)(`div`,{className:`mdw-main`,children:[(0,S.jsxs)(`div`,{className:`docp-head`,children:[(0,S.jsx)(`span`,{className:`docp-path mono`,title:N.rel||N.path,children:N.rel||N.path}),L&&(0,S.jsx)(`span`,{className:`docp-dirty`,children:`未保存`}),!Y&&(0,S.jsx)(`span`,{className:`docp-tag ext`,children:`外部绑定 · 只读`}),(0,S.jsx)(`span`,{className:`grow`}),Y&&(0,S.jsxs)(S.Fragment,{children:[(0,S.jsx)(_,{mode:$,onChange:B}),(0,S.jsx)(`button`,{className:`btn sm docp-save`,onClick:Q,children:`保存`})]})]}),N.summary&&Y&&(0,S.jsxs)(`div`,{className:`docp-summary`,children:[`摘要(回写 desc):`,N.summary]}),(0,S.jsx)(m,{mode:$,title:N.title,body:N.body,previewHtml:X,onTitle:Y?ae:null,onBody:Y?ie:null,attachments:[],docKey:e,editorApiRef:E,onSave:Y?Q:null,onUndo:Y?oe:null,onRedo:Y?se:null}),(0,S.jsx)(`div`,{className:`docp-hint`,children:`双链 [[]] 点击跳转 · Ctrl+S 保存 · 文档即磁盘文件(~/.nx-kv/docs/)`})]}),(0,S.jsx)(ee,{headings:te,scrollRoot:U,links:V,activeDocId:e,onOpenDoc:j?ne:null,onAddRef:j?re:null}),b]})}function T(e){let t=String(e||``).startsWith(`rel:`)?String(e).slice(4):``;return t?/\/index\.md$/i.test(t)?t.replace(/\/index\.md$/i,``):/\.md$/i.test(t)?t.replace(/\.md$/i,``):t.replace(/\/+$/,``):``}function E(e,t){return e?e.doc&&e.doc.path?`path:`+e.doc.path:e.doc&&e.doc.rel?`rel:`+e.doc.rel:e.docPath?`path:`+e.docPath:e.docRel?`rel:`+e.docRel:t===`repo`&&e.key?`rel:repo/`+e.key+`.md`:``:``}function D(e){return e?e.rel?`rel:`+e.rel:e.path?`path:`+e.path:``:``}function O({kind:e,owner:t,model:n}){let{openByDocId:r}=C(n),i=e===`board`?`组介绍`:`项目介绍`,a=e===`board`?t&&(t.id||t.key):t&&t.key,o=t&&t.name,s=(0,x.useMemo)(()=>E(t,e),[t,e]),[c,u]=(0,x.useState)([]),[d,p]=(0,x.useState)(0),m=(0,x.useCallback)(()=>p(e=>e+1),[]);(0,x.useEffect)(()=>{let n=!0;if(!a){u([]);return}if(e===`repo`)f(`/api/repos/docs?id=`+encodeURIComponent(a)).then(e=>{if(!n)return;let t=[];if(e.main){let n=D(e.main);n&&t.push({docId:n,label:e.main.rel||e.main.path||`主篇`,note:e.main.note||``,tag:e.main.tag||``,main:!0,ref:e.main.ref||``})}for(let n of e.links||[]){let e=D(n);e&&t.push({docId:e,label:n.rel||n.path||`补充`,note:n.note||``,tag:n.tag||``,main:!1,ref:n.ref||``})}u(t)}).catch(()=>{n&&u([])});else{let e=()=>{let e=[];s&&e.push({docId:s,label:`主篇`,note:``,tag:``,main:!0,ref:``});for(let n of t?.docLinks||[]){let t=D(n);t&&e.push({docId:t,label:n.rel||n.path||`补充`,note:n.note||``,tag:n.tag||``,main:!1,ref:n.ref||``})}return e};f(`/api/boards/`+encodeURIComponent(String(a))).then(t=>{if(!n)return;let r=Array.isArray(t&&t.docLinks)?t.docLinks:null;if(!r){u(e());return}let i=[];s&&i.push({docId:s,label:`主篇`,note:``,tag:``,main:!0,ref:``});for(let e of r){let t=D(e);t&&i.push({docId:t,label:e.rel||e.path||`补充`,note:e.note||``,tag:e.tag||``,main:!1,ref:e.ref||``})}u(i)}).catch(()=>{n&&u(e())})}return()=>{n=!1}},[e,a,t,s,d]);let h=(0,x.useMemo)(()=>e===`repo`&&a?{repoKey:a}:e===`board`&&a?{boardKey:a,boardName:o||``}:{},[e,a,o]);return{kind:e,label:i,ownerKey:a,docRef:s,links:c,openMain:(0,x.useCallback)(()=>{s&&r(s,null,h)},[s,r,h]),openDoc:(0,x.useCallback)(e=>r(e,null,h),[r,h]),addRef:(0,x.useCallback)(async(t,n)=>{if(!a||!s.startsWith(`rel:`))return n&&n(`当前简介无法补充(需要自有树路径)`),null;let i=await t({title:`补充介绍文档`,input:!0,tagInput:!0,placeholder:`文档名(如 architecture / api-notes)`,tagPlaceholder:`标签(可选,如 qa)`});if(i===null)return null;let c=typeof i==`string`?i:i&&i.value||``,u=typeof i==`object`&&i&&i.tag||``,d=l(String(c).trim()||`note`).replace(/\.md$/i,``),p=`${T(s)||(e===`repo`?`repo/${a}`:`board/${o||a}`)}/reference/${d}.md`;await f(`/api/docs/item`,{method:`POST`,body:{rel:p,body:`# ${String(c).trim()||d}\n\n(待补)\n`}});try{e===`repo`?await f(`/api/repos/docs`,{method:`POST`,body:{id:a,rel:p,tag:u||void 0}}):await f(`/api/docs/link`,{method:`POST`,body:{board:a,doc:p,tag:u||void 0}})}catch{}return n&&n(`已补充:${p}${u?` [${u}]`:``}`),m(),r(`rel:`+p,null,h),p},[a,s,e,o,r,h,m]),setLinkTag:(0,x.useCallback)(async(t,n,r)=>{if(!a)throw Error(`缺少归属`);let i=String(t||``).startsWith(`path:`)?{path:String(t).slice(5)}:{rel:String(t||``).startsWith(`rel:`)?String(t).slice(4):String(t||``)},o=r&&r.note||``;e===`repo`?await f(`/api/repos/docs`,{method:`POST`,body:{id:a,...i,note:o||void 0,tag:n||void 0}}):await f(`/api/docs/link`,{method:`POST`,body:{board:a,doc:i.path||i.rel,note:o||void 0,tag:n||void 0}}),m()},[a,e,m]),reload:m,canEdit:!!s}}function k(e){return e<=1?18:e===2?12:8}function ee({headings:e=[],scrollRoot:t=null,links:n=[],activeDocId:r=``,onOpenDoc:i=null,onAddRef:a=null}){let[o,s]=(0,x.useState)(!1),[c,l]=(0,x.useState)(``),u=(0,x.useMemo)(()=>e||[],[e]),d=(0,x.useMemo)(()=>n||[],[n]);(0,x.useEffect)(()=>{let e=t;if(!e||!u.length)return;let n=()=>u.map(t=>{let n=e.querySelector(`#${CSS.escape(t.id)}`);if(!n)return null;let r=n.getBoundingClientRect(),i=e.getBoundingClientRect();return{id:t.id,y:r.top-i.top}}).filter(Boolean),r=()=>{let e=n();if(!e.length)return;let t=e[0].id;for(let n of e)n.y<=28&&(t=n.id);l(t)};return r(),e.addEventListener(`scroll`,r,{passive:!0}),()=>e.removeEventListener(`scroll`,r)},[t,u]);let f=(0,x.useCallback)(e=>{let n=t;if(!n)return;let r=n.querySelector(`#${CSS.escape(e)}`);if(!r){n.scrollTo({top:0,behavior:`smooth`}),l(e);return}r.scrollIntoView({behavior:`smooth`,block:`start`}),l(e)},[t]);return!u.length&&!d.length&&!a?null:(0,S.jsxs)(`aside`,{className:`doc-rail`+(o?` open`:``),"aria-label":`文档目录`,children:[(0,S.jsxs)(`button`,{type:`button`,className:`doc-rail-menu`,title:o?`收起目录`:`展开目录`,"aria-expanded":o,onClick:()=>s(e=>!e),children:[(0,S.jsx)(`span`,{}),(0,S.jsx)(`span`,{}),(0,S.jsx)(`span`,{})]}),(0,S.jsx)(`div`,{className:`doc-rail-ticks`,role:`navigation`,"aria-label":`章节`,children:u.map(e=>(0,S.jsx)(`button`,{type:`button`,className:`doc-rail-tick`+(c===e.id?` on`:``),style:{"--tw":`${k(e.level)}px`},title:e.text,"aria-current":c===e.id?`true`:void 0,onClick:()=>f(e.id)},e.id))}),o&&(0,S.jsxs)(`div`,{className:`doc-rail-panel`,children:[u.length>0&&(0,S.jsxs)(`div`,{className:`doc-rail-sec`,children:[(0,S.jsx)(`div`,{className:`doc-rail-sec-lab`,children:`本章`}),u.map(e=>(0,S.jsx)(`button`,{type:`button`,className:`doc-rail-row`+(c===e.id?` on`:``),"data-lv":e.level,onClick:()=>f(e.id),children:e.text},e.id))]}),(d.length>0||a)&&(0,S.jsxs)(`div`,{className:`doc-rail-sec`,children:[(0,S.jsx)(`div`,{className:`doc-rail-sec-lab`,children:`介绍文档`}),d.map(e=>(0,S.jsxs)(`button`,{type:`button`,className:`doc-rail-row`+(e.docId===r?` on`:``),title:e.note||e.label,disabled:!i,onClick:()=>i&&i(e.docId),children:[e.main?`● `:`○ `,e.label]},e.docId)),a&&(0,S.jsx)(`button`,{type:`button`,className:`doc-rail-add`,onClick:a,children:`+ 补充文档`})]})]})]})}function A({kind:e,owner:t,model:n}){let r=a(),{dialog:o,node:s}=i(),{label:c,docRef:l,links:u,openMain:d,openDoc:f,addRef:p,setLinkTag:m,canEdit:h}=O({kind:e,owner:t,model:n});if(!t||!l)return null;let g=l.startsWith(`rel:`)?l.slice(4):l,_=u.filter(e=>!e.main),v=()=>p(o,r).catch(e=>r(String(e.message||e))),y=e=>{(async()=>{let t=await o({title:`标签 · ${e.label}`,input:!0,placeholder:`标签(如 qa;留空 = 清掉)`,value:e.tag||``,okText:`保存标签`});if(t===null)return;let n=typeof t==`string`?t:t&&t.tag||``;await m(e.docId,n,e),r(n?`标签已设为 [${n}]`:`标签已清除`)})().catch(e=>r(String(e.message||e)))};return(0,S.jsxs)(`div`,{className:`nx-intro`,children:[(0,S.jsxs)(`div`,{className:`nx-intro-head`,children:[(0,S.jsx)(`button`,{type:`button`,className:`nx-intro-link-main`,title:`打开${c}(新页签阅读 / 编辑)`,onClick:d,children:c}),(0,S.jsx)(`span`,{className:`nx-intro-path mono`,children:g}),(0,S.jsx)(`span`,{className:`nx-intro-spacer`}),h&&l.startsWith(`rel:`)&&(0,S.jsx)(`button`,{type:`button`,className:`nx-intro-btn`,title:`补充一篇介绍文档(reference)`,onClick:v,children:`+ ref`})]}),_.length>0&&(0,S.jsxs)(`div`,{className:`nx-intro-links`,children:[(0,S.jsx)(`span`,{className:`nx-intro-links-lab`,children:`补充`}),_.map(e=>(0,S.jsxs)(`span`,{className:`nx-intro-link-item`,children:[(0,S.jsxs)(`button`,{type:`button`,className:`nx-intro-link`+(e.docId.startsWith(`path:`)?` ext`:``),title:e.note?`${e.label} — ${e.note}`:e.label,onClick:()=>f(e.docId),children:[(0,S.jsx)(`span`,{className:`nx-intro-link-label`,children:e.label}),e.tag?(0,S.jsx)(`span`,{className:`nx-intro-link-tag`,title:`标签:${e.tag}`,children:e.tag}):null,e.note?(0,S.jsx)(`span`,{className:`nx-intro-link-note`,children:e.note}):null,e.docId.startsWith(`path:`)?(0,S.jsx)(`span`,{className:`nx-intro-link-tag`,children:`外部`}):null]}),h&&!e.ref&&(0,S.jsx)(`button`,{type:`button`,className:`nx-intro-tag-btn`,title:e.tag?`改标签(当前:${e.tag})`:`加标签`,"aria-label":`标签:${e.label}`,onClick:()=>y(e),children:`#`})]},e.docId))]}),s]})}export{b as n,C as r,A as t};
1
+ import{d as e,f as t,h as n,m as r,o as i,u as a}from"./attach-v8BxRJ7p.js";import{C as o,D as s,W as c,b as l,j as u,p as d,q as f,y as p}from"./index-DMp4lRKl.js";import{a as m,i as h,n as g,o as _,r as v,t as y}from"./md-history-CwtrJcZo.js";var b=r({default:()=>w,useWikiNav:()=>C}),x=n(t(),1),S=e();function C(e){let t=c()||{},n=a(),r=e||t.model,i=(0,x.useRef)(null),o=(0,x.useCallback)(async()=>{if(!i.current)try{i.current=(await f(`/api/docs/index`)).index||[]}catch{i.current=[]}return i.current},[]),l=(0,x.useCallback)(async(e,t,n={})=>{let i=(t||await o()).find(t=>t.docId===e);u(r,{component:`doc`,name:i?i.name:e,config:{docId:e,...n.repoKey?{repoKey:n.repoKey}:{},...n.boardKey?{boardKey:n.boardKey,boardName:n.boardName||``}:{}},location:`center`})},[r,o]),d=(0,x.useCallback)(async e=>{let t=await o(),r=s(e,t);if(r&&r.docId)return l(r.docId,t);if(r&&r.candidates)return n(`「${e}」同名文档多份:${r.candidates.join(`、`)}`);n(`未找到「${e}」——可用 nx-kv docs add <rel> 创建`)},[o,l,n]);return{nav:d,onClick:(0,x.useCallback)(e=>{let t=e.target.closest(`[data-wl]`);t&&(e.preventDefault(),d(t.getAttribute(`data-wl`)))},[d]),ensureIndex:o,openByDocId:l}}function w({docId:e,repoKey:t,boardKey:n,boardName:r,model:s}){let c=a(),{dialog:u,node:b}=i(),w=(0,x.useRef)(new Map),E=(0,x.useRef)(null),D=(0,x.useRef)(null),{onClick:O,openByDocId:k}=C(s),A=t?`repo`:n?`board`:``,j=t||n||``,M=(0,x.useMemo)(()=>t?{repoKey:t}:n?{boardKey:n,boardName:r||``}:{},[t,n,r]),[N,P]=(0,x.useState)(null),[F,I]=(0,x.useState)(``),[L,R]=(0,x.useState)(!1),[z,B]=(0,x.useState)(`split`),[V,H]=(0,x.useState)([]),[U,W]=(0,x.useState)(null),G=(0,x.useCallback)(async()=>{I(``);try{let t=await f(`/api/docs/item?ref=`+encodeURIComponent(e||``));P(t),R(!1)}catch(e){I(String(e.message||e)),P(null)}},[e]);(0,x.useEffect)(()=>{P(null),G()},[G]);let K=(0,x.useCallback)(()=>{if(!j){H([]);return}if(A===`repo`){f(`/api/repos/docs?id=`+encodeURIComponent(j)).then(t=>{let n=[],r=(e,t)=>{if(!e)return;let r=e.rel?`rel:`+e.rel:e.path?`path:`+e.path:``;r&&n.push({docId:r,label:e.rel||e.path||(t?`主篇`:`补充`),note:e.note||``,main:t})};r(t.main||{rel:`repo/${j}.md`},!0);for(let e of t.links||[])r(e,!1);e&&!n.some(t=>t.docId===e)&&n.push({docId:e,label:`当前`,note:``,main:!0}),H(n)}).catch(()=>H(e?[{docId:e,label:`当前`,note:``,main:!0}]:[]));return}f(`/api/boards`).then(t=>{let n=(t&&t.boards||(Array.isArray(t)?t:[])).find(e=>e.id===j||e.key===j),r=[];e&&r.push({docId:e,label:`当前`,note:``,main:!0});for(let e of n&&n.docLinks||[]){let t=e.rel?`rel:${e.rel}`:e.path?`path:${e.path}`:``;t&&!r.some(e=>e.docId===t)&&r.push({docId:t,label:e.rel||e.path||`补充`,note:e.note||``,main:!1})}H(r)}).catch(()=>H(e?[{docId:e,label:`当前`,note:``,main:!0}]:[]))},[j,A,e]);(0,x.useEffect)(()=>{K()},[K]);let[q,J]=(0,x.useState)(null);(0,x.useEffect)(()=>{f(`/api/docs/index`).then(e=>{let t=new Set;for(let n of e.index||[])t.add(n.name),n.rel&&t.add(String(n.rel).toLowerCase()),n.path&&t.add(String(n.path).replace(/\\/g,`/`).toLowerCase());J(t)}).catch(()=>{})},[]);let Y=!!(N&&N.writable),X=(0,x.useMemo)(()=>N?p(N.body,[],Y?q:null):``,[N,q,Y]),te=(0,x.useMemo)(()=>N?d(N.body):[],[N]);(0,x.useEffect)(()=>{let e=D.current;if(!e)return;let t=()=>e.querySelector(`.mdw-preview, .mdw-lp`)||null;W(t());let n=typeof MutationObserver<`u`?new MutationObserver(()=>W(t())):null;return n&&n.observe(e,{childList:!0,subtree:!0}),()=>{n&&n.disconnect()}},[N,z,X]);let Z=e=>e().catch(e=>c(String(e.message||e))),ne=(0,x.useCallback)(e=>k(e,null,M),[k,M]),re=()=>{j&&Z(async()=>{let t=await u({title:`补充介绍文档`,input:!0,placeholder:`文档名(如 architecture / api-notes)`});if(t===null)return;let n=l(String(t).trim()||`note`).replace(/\.md$/i,``),i=`${String(e||``).startsWith(`rel:`)?T(e):A===`repo`?`repo/${j}`:`board/${r||j}`}/reference/${n}.md`;await f(`/api/docs/item`,{method:`POST`,body:{rel:i,body:`# ${String(t).trim()||n}\n\n(待补)\n`}});try{A===`repo`?await f(`/api/repos/docs`,{method:`POST`,body:{id:j,rel:i}}):await f(`/api/docs/link`,{method:`POST`,body:{board:j,doc:i}})}catch{}c(`已补充:${i}`),K(),k(`rel:`+i,null,M)})},Q=()=>Z(async()=>{if(!N||!Y)return;let t=await f(`/api/docs/item`,{method:`PATCH`,body:{ref:e,body:N.body}});P(e=>({...e,body:N.body,summary:t.summary})),R(!1),y(w.current,e),c(t.summary?`已保存,摘要已回写`:`已保存`)}),ie=(t,n=`op`)=>{N&&(g(w.current,e,{body:N.body},n),R(!0),P(e=>({...e,body:t,title:o(t,e.title)})))},ae=(t,n=`typing`)=>{if(!N)return;g(w.current,e,{body:N.body},n),R(!0);let r=/^#\s+/m.test(N.body)?N.body.replace(/^#\s+.+$/m,`# ${t||`未命名文档`}`):`# ${t||`未命名文档`}\n\n${N.body}`;P(e=>({...e,body:r,title:t}))},oe=()=>{if(!N)return;let t=h(w.current,e,{body:N.body});t&&(P(e=>({...e,body:t.body,title:o(t.body,e.title)})),R(!0),requestAnimationFrame(()=>E.current?.caretFromDiff?.(N.body,t.body)))},se=()=>{if(!N)return;let t=v(w.current,e,{body:N.body});t&&(P(e=>({...e,body:t.body,title:o(t.body,e.title)})),R(!0),requestAnimationFrame(()=>E.current?.caretFromDiff?.(N.body,t.body)))},ce=()=>Z(async()=>{let t=String(e||``).startsWith(`rel:`)?e.slice(4):String(e||``).startsWith(`path:`)?``:String(e||``);if(!t)return;let n=t.replace(/\.md$/i,``).split(`/`).pop()||`未命名文档`;await f(`/api/docs/item`,{method:`POST`,body:{rel:t,body:`# ${n}\n\n(待补)\n`}}),c(`已创建:${t}`),await G()});if(N&&N.dir)return(0,S.jsxs)(`div`,{className:`mdw docp`,children:[(0,S.jsxs)(`div`,{className:`docp-head`,children:[(0,S.jsx)(`span`,{className:`docp-path mono`,children:N.path}),(0,S.jsx)(`span`,{className:`docp-tag ext`,children:`外部目录 · 只读`})]}),(0,S.jsxs)(`div`,{className:`docp-files`,children:[N.files.map(e=>(0,S.jsx)(`button`,{className:`docp-file`,onClick:()=>k(`path:`+e,null),children:e},e)),!N.files.length&&(0,S.jsx)(`div`,{className:`muted pad`,children:`(目录里还没有 .md 文档)`})]})]});if(F)return(0,S.jsx)(`div`,{className:`mdw docp`,children:(0,S.jsxs)(`div`,{className:`pad stack`,children:[(0,S.jsx)(`div`,{children:(0,S.jsx)(`strong`,{children:`文档打开失败`})}),(0,S.jsx)(`div`,{className:`hint`,children:F})]})});if(!N)return(0,S.jsx)(`div`,{className:`mdw`,children:(0,S.jsx)(`div`,{className:`pad muted`,children:`载入中…`})});if(N.missing){let t=String(e||``).startsWith(`rel:`)||!String(e||``).startsWith(`path:`);return(0,S.jsxs)(`div`,{className:`mdw docp`,children:[(0,S.jsx)(`div`,{className:`docp-head`,children:(0,S.jsx)(`span`,{className:`docp-path mono`,children:N.rel||N.path||e})}),(0,S.jsxs)(`div`,{className:`pad stack`,children:[(0,S.jsx)(`div`,{children:(0,S.jsx)(`strong`,{children:`文档还没有落盘`})}),(0,S.jsx)(`div`,{className:`hint`,children:`描述内容的权威是磁盘上的 .md 文件——对应的字符串字段会在读取时播种,也可以直接创建骨架。`}),t&&(0,S.jsx)(`div`,{children:(0,S.jsx)(`button`,{className:`btn sm`,onClick:ce,children:`创建骨架`})})]})]})}let $=Y?z:`preview`;return(0,S.jsxs)(`div`,{className:`mdw docp`,"data-mode":$,onClick:O,ref:D,children:[(0,S.jsxs)(`div`,{className:`mdw-main`,children:[(0,S.jsxs)(`div`,{className:`docp-head`,children:[(0,S.jsx)(`span`,{className:`docp-path mono`,title:N.rel||N.path,children:N.rel||N.path}),L&&(0,S.jsx)(`span`,{className:`docp-dirty`,children:`未保存`}),!Y&&(0,S.jsx)(`span`,{className:`docp-tag ext`,children:`外部绑定 · 只读`}),(0,S.jsx)(`span`,{className:`grow`}),Y&&(0,S.jsxs)(S.Fragment,{children:[(0,S.jsx)(_,{mode:$,onChange:B}),(0,S.jsx)(`button`,{className:`btn sm docp-save`,onClick:Q,children:`保存`})]})]}),N.summary&&Y&&(0,S.jsxs)(`div`,{className:`docp-summary`,children:[`摘要(回写 desc):`,N.summary]}),(0,S.jsx)(m,{mode:$,title:N.title,body:N.body,previewHtml:X,onTitle:Y?ae:null,onBody:Y?ie:null,attachments:[],docKey:e,editorApiRef:E,onSave:Y?Q:null,onUndo:Y?oe:null,onRedo:Y?se:null}),(0,S.jsx)(`div`,{className:`docp-hint`,children:`双链 [[]] 点击跳转 · Ctrl+S 保存 · 文档即磁盘文件(~/.nx-kv/docs/)`})]}),(0,S.jsx)(ee,{headings:te,scrollRoot:U,links:V,activeDocId:e,onOpenDoc:j?ne:null,onAddRef:j?re:null}),b]})}function T(e){let t=String(e||``).startsWith(`rel:`)?String(e).slice(4):``;return t?/\/index\.md$/i.test(t)?t.replace(/\/index\.md$/i,``):/\.md$/i.test(t)?t.replace(/\.md$/i,``):t.replace(/\/+$/,``):``}function E(e,t){return e?e.doc&&e.doc.path?`path:`+e.doc.path:e.doc&&e.doc.rel?`rel:`+e.doc.rel:e.docPath?`path:`+e.docPath:e.docRel?`rel:`+e.docRel:t===`repo`&&e.key?`rel:repo/`+e.key+`.md`:``:``}function D(e){return e?e.rel?`rel:`+e.rel:e.path?`path:`+e.path:``:``}function O({kind:e,owner:t,model:n}){let{openByDocId:r}=C(n),i=e===`board`?`组介绍`:`项目介绍`,a=e===`board`?t&&(t.id||t.key):t&&t.key,o=t&&t.name,s=(0,x.useMemo)(()=>E(t,e),[t,e]),[c,u]=(0,x.useState)([]),[d,p]=(0,x.useState)(0),m=(0,x.useCallback)(()=>p(e=>e+1),[]);(0,x.useEffect)(()=>{let n=!0;if(!a){u([]);return}if(e===`repo`)f(`/api/repos/docs?id=`+encodeURIComponent(a)).then(e=>{if(!n)return;let t=[];if(e.main){let n=D(e.main);n&&t.push({docId:n,label:e.main.rel||e.main.path||`主篇`,note:e.main.note||``,tag:e.main.tag||``,main:!0,ref:e.main.ref||``})}for(let n of e.links||[]){let e=D(n);e&&t.push({docId:e,label:n.rel||n.path||`补充`,note:n.note||``,tag:n.tag||``,main:!1,ref:n.ref||``})}u(t)}).catch(()=>{n&&u([])});else{let e=()=>{let e=[];s&&e.push({docId:s,label:`主篇`,note:``,tag:``,main:!0,ref:``});for(let n of t?.docLinks||[]){let t=D(n);t&&e.push({docId:t,label:n.rel||n.path||`补充`,note:n.note||``,tag:n.tag||``,main:!1,ref:n.ref||``})}return e};f(`/api/boards/`+encodeURIComponent(String(a))).then(t=>{if(!n)return;let r=Array.isArray(t&&t.docLinks)?t.docLinks:null;if(!r){u(e());return}let i=[];s&&i.push({docId:s,label:`主篇`,note:``,tag:``,main:!0,ref:``});for(let e of r){let t=D(e);t&&i.push({docId:t,label:e.rel||e.path||`补充`,note:e.note||``,tag:e.tag||``,main:!1,ref:e.ref||``})}u(i)}).catch(()=>{n&&u(e())})}return()=>{n=!1}},[e,a,t,s,d]);let h=(0,x.useMemo)(()=>e===`repo`&&a?{repoKey:a}:e===`board`&&a?{boardKey:a,boardName:o||``}:{},[e,a,o]);return{kind:e,label:i,ownerKey:a,docRef:s,links:c,openMain:(0,x.useCallback)(()=>{s&&r(s,null,h)},[s,r,h]),openDoc:(0,x.useCallback)(e=>r(e,null,h),[r,h]),addRef:(0,x.useCallback)(async(t,n)=>{if(!a||!s.startsWith(`rel:`))return n&&n(`当前简介无法补充(需要自有树路径)`),null;let i=await t({title:`补充介绍文档`,input:!0,tagInput:!0,placeholder:`文档名(如 architecture / api-notes)`,tagPlaceholder:`标签(可选,如 qa)`});if(i===null)return null;let c=typeof i==`string`?i:i&&i.value||``,u=typeof i==`object`&&i&&i.tag||``,d=l(String(c).trim()||`note`).replace(/\.md$/i,``),p=`${T(s)||(e===`repo`?`repo/${a}`:`board/${o||a}`)}/reference/${d}.md`;await f(`/api/docs/item`,{method:`POST`,body:{rel:p,body:`# ${String(c).trim()||d}\n\n(待补)\n`}});try{e===`repo`?await f(`/api/repos/docs`,{method:`POST`,body:{id:a,rel:p,tag:u||void 0}}):await f(`/api/docs/link`,{method:`POST`,body:{board:a,doc:p,tag:u||void 0}})}catch{}return n&&n(`已补充:${p}${u?` [${u}]`:``}`),m(),r(`rel:`+p,null,h),p},[a,s,e,o,r,h,m]),setLinkTag:(0,x.useCallback)(async(t,n,r)=>{if(!a)throw Error(`缺少归属`);let i=String(t||``).startsWith(`path:`)?{path:String(t).slice(5)}:{rel:String(t||``).startsWith(`rel:`)?String(t).slice(4):String(t||``)},o=r&&r.note||``;e===`repo`?await f(`/api/repos/docs`,{method:`POST`,body:{id:a,...i,note:o||void 0,tag:n||void 0}}):await f(`/api/docs/link`,{method:`POST`,body:{board:a,doc:i.path||i.rel,note:o||void 0,tag:n||void 0}}),m()},[a,e,m]),reload:m,canEdit:!!s}}function k(e){return e<=1?18:e===2?12:8}function ee({headings:e=[],scrollRoot:t=null,links:n=[],activeDocId:r=``,onOpenDoc:i=null,onAddRef:a=null}){let[o,s]=(0,x.useState)(!1),[c,l]=(0,x.useState)(``),u=(0,x.useMemo)(()=>e||[],[e]),d=(0,x.useMemo)(()=>n||[],[n]);(0,x.useEffect)(()=>{let e=t;if(!e||!u.length)return;let n=()=>u.map(t=>{let n=e.querySelector(`#${CSS.escape(t.id)}`);if(!n)return null;let r=n.getBoundingClientRect(),i=e.getBoundingClientRect();return{id:t.id,y:r.top-i.top}}).filter(Boolean),r=()=>{let e=n();if(!e.length)return;let t=e[0].id;for(let n of e)n.y<=28&&(t=n.id);l(t)};return r(),e.addEventListener(`scroll`,r,{passive:!0}),()=>e.removeEventListener(`scroll`,r)},[t,u]);let f=(0,x.useCallback)(e=>{let n=t;if(!n)return;let r=n.querySelector(`#${CSS.escape(e)}`);if(!r){n.scrollTo({top:0,behavior:`smooth`}),l(e);return}r.scrollIntoView({behavior:`smooth`,block:`start`}),l(e)},[t]);return!u.length&&!d.length&&!a?null:(0,S.jsxs)(`aside`,{className:`doc-rail`+(o?` open`:``),"aria-label":`文档目录`,children:[(0,S.jsxs)(`button`,{type:`button`,className:`doc-rail-menu`,title:o?`收起目录`:`展开目录`,"aria-expanded":o,onClick:()=>s(e=>!e),children:[(0,S.jsx)(`span`,{}),(0,S.jsx)(`span`,{}),(0,S.jsx)(`span`,{})]}),(0,S.jsx)(`div`,{className:`doc-rail-ticks`,role:`navigation`,"aria-label":`章节`,children:u.map(e=>(0,S.jsx)(`button`,{type:`button`,className:`doc-rail-tick`+(c===e.id?` on`:``),style:{"--tw":`${k(e.level)}px`},title:e.text,"aria-current":c===e.id?`true`:void 0,onClick:()=>f(e.id)},e.id))}),o&&(0,S.jsxs)(`div`,{className:`doc-rail-panel`,children:[u.length>0&&(0,S.jsxs)(`div`,{className:`doc-rail-sec`,children:[(0,S.jsx)(`div`,{className:`doc-rail-sec-lab`,children:`本章`}),u.map(e=>(0,S.jsx)(`button`,{type:`button`,className:`doc-rail-row`+(c===e.id?` on`:``),"data-lv":e.level,onClick:()=>f(e.id),children:e.text},e.id))]}),(d.length>0||a)&&(0,S.jsxs)(`div`,{className:`doc-rail-sec`,children:[(0,S.jsx)(`div`,{className:`doc-rail-sec-lab`,children:`介绍文档`}),d.map(e=>(0,S.jsxs)(`button`,{type:`button`,className:`doc-rail-row`+(e.docId===r?` on`:``),title:e.note||e.label,disabled:!i,onClick:()=>i&&i(e.docId),children:[e.main?`● `:`○ `,e.label]},e.docId)),a&&(0,S.jsx)(`button`,{type:`button`,className:`doc-rail-add`,onClick:a,children:`+ 补充文档`})]})]})]})}function A({kind:e,owner:t,model:n}){let r=a(),{dialog:o,node:s}=i(),{label:c,docRef:l,links:u,openMain:d,openDoc:f,addRef:p,setLinkTag:m,canEdit:h}=O({kind:e,owner:t,model:n});if(!t||!l)return null;let g=l.startsWith(`rel:`)?l.slice(4):l,_=u.filter(e=>!e.main),v=()=>p(o,r).catch(e=>r(String(e.message||e))),y=e=>{(async()=>{let t=await o({title:`标签 · ${e.label}`,input:!0,placeholder:`标签(如 qa;留空 = 清掉)`,value:e.tag||``,okText:`保存标签`});if(t===null)return;let n=typeof t==`string`?t:t&&t.tag||``;await m(e.docId,n,e),r(n?`标签已设为 [${n}]`:`标签已清除`)})().catch(e=>r(String(e.message||e)))};return(0,S.jsxs)(`div`,{className:`nx-intro`,children:[(0,S.jsxs)(`div`,{className:`nx-intro-head`,children:[(0,S.jsx)(`button`,{type:`button`,className:`nx-intro-link-main`,title:`打开${c}(新页签阅读 / 编辑)`,onClick:d,children:c}),(0,S.jsx)(`span`,{className:`nx-intro-path mono`,children:g}),(0,S.jsx)(`span`,{className:`nx-intro-spacer`}),h&&l.startsWith(`rel:`)&&(0,S.jsx)(`button`,{type:`button`,className:`nx-intro-btn`,title:`补充一篇介绍文档(reference)`,onClick:v,children:`+ ref`})]}),_.length>0&&(0,S.jsxs)(`div`,{className:`nx-intro-links`,children:[(0,S.jsx)(`span`,{className:`nx-intro-links-lab`,children:`补充`}),_.map(e=>(0,S.jsxs)(`span`,{className:`nx-intro-link-item`,children:[(0,S.jsxs)(`button`,{type:`button`,className:`nx-intro-link`+(e.docId.startsWith(`path:`)?` ext`:``),title:e.note?`${e.label} — ${e.note}`:e.label,onClick:()=>f(e.docId),children:[(0,S.jsx)(`span`,{className:`nx-intro-link-label`,children:e.label}),e.tag?(0,S.jsx)(`span`,{className:`nx-intro-link-tag`,title:`标签:${e.tag}`,children:e.tag}):null,e.note?(0,S.jsx)(`span`,{className:`nx-intro-link-note`,children:e.note}):null,e.docId.startsWith(`path:`)?(0,S.jsx)(`span`,{className:`nx-intro-link-tag`,children:`外部`}):null]}),h&&!e.ref&&(0,S.jsx)(`button`,{type:`button`,className:`nx-intro-tag-btn`,title:e.tag?`改标签(当前:${e.tag})`:`加标签`,"aria-label":`标签:${e.label}`,onClick:()=>y(e),children:`#`})]},e.docId))]}),s]})}export{b as n,C as r,A as t};