dsh-task-ledger 0.0.6 → 0.0.8
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 +744 -472
- package/dist/client.js.map +1 -1
- package/dist/host/board.d.ts +46 -16
- package/dist/host/board.js +76 -26
- package/dist/host/routes.js +3 -3
- package/package.json +1 -1
- package/skill/package.json +1 -1
- package/skill/scripts/progress.py +29 -13
package/dist/client.js
CHANGED
|
@@ -10,6 +10,18 @@ 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
|
+
/**
|
|
14
|
+
* 错误响应榨人话:host 路由(routes.ts)发的是 `{ error: "..." }`,那里的话是**能直接上界面的**
|
|
15
|
+
* (带工作区名、python3 起不来之类的原文)。不带它界面上只剩「HTTP 500」,等于没报错。
|
|
16
|
+
*/
|
|
17
|
+
function httpErrorMessage(status, body) {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(body);
|
|
20
|
+
if (typeof parsed.error === "string" && parsed.error !== "") return `HTTP ${status}:${parsed.error}`;
|
|
21
|
+
} catch {}
|
|
22
|
+
const text = body.trim().slice(0, 200);
|
|
23
|
+
return text === "" ? `HTTP ${String(status)}` : `HTTP ${String(status)}:${text}`;
|
|
24
|
+
}
|
|
13
25
|
/** 带超时的 JSON 取数(相对路径,同源)。 */
|
|
14
26
|
async function fetchJson(path, init = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
15
27
|
const controller = new AbortController();
|
|
@@ -22,7 +34,7 @@ window.__ModuleLoader__.load({
|
|
|
22
34
|
signal: controller.signal,
|
|
23
35
|
cache: "no-store"
|
|
24
36
|
});
|
|
25
|
-
if (!res.ok) throw new Error(
|
|
37
|
+
if (!res.ok) throw new Error(httpErrorMessage(res.status, await res.text().catch(() => "")));
|
|
26
38
|
return await res.json();
|
|
27
39
|
} finally {
|
|
28
40
|
clearTimeout(timer);
|
|
@@ -32,16 +44,6 @@ window.__ModuleLoader__.load({
|
|
|
32
44
|
function fetchBoard() {
|
|
33
45
|
return fetchJson("/board");
|
|
34
46
|
}
|
|
35
|
-
/** 已结案列表(服务端按窗口 / 标签 / 分页过滤)。 */
|
|
36
|
-
function fetchHistory(params) {
|
|
37
|
-
const query = new URLSearchParams();
|
|
38
|
-
if (params.since !== void 0) query.set("since", params.since);
|
|
39
|
-
if (params.until !== void 0) query.set("until", params.until);
|
|
40
|
-
if (params.tag !== void 0 && params.tag !== "") query.set("tag", params.tag);
|
|
41
|
-
if (params.limit !== void 0) query.set("limit", String(params.limit));
|
|
42
|
-
const suffix = query.toString();
|
|
43
|
-
return fetchJson(`/history${suffix === "" ? "" : `?${suffix}`}`).then((body) => body.records ?? []);
|
|
44
|
-
}
|
|
45
47
|
/** 单条在办 + 过程文档正文(展开行用)。 */
|
|
46
48
|
function fetchTaskDetail(workspaceId, taskId) {
|
|
47
49
|
return fetchJson(`/task?${new URLSearchParams({
|
|
@@ -49,83 +51,26 @@ window.__ModuleLoader__.load({
|
|
|
49
51
|
id: taskId
|
|
50
52
|
}).toString()}`);
|
|
51
53
|
}
|
|
52
|
-
/** 筛选项候选(在办全量 + 近 13 个月历史的去重 owner / category / tag)。 */
|
|
53
|
-
function fetchFilters() {
|
|
54
|
-
return fetchJson("/filters");
|
|
55
|
-
}
|
|
56
54
|
//#endregion
|
|
57
|
-
//#region src/client/
|
|
55
|
+
//#region src/client/store.ts
|
|
58
56
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
57
|
+
* 看板数据仓(client 侧**唯一**取数入口):浮标角标与整页共用同一份快照,
|
|
58
|
+
* 谁先挂载谁触发首读;刷新按钮走同一个 `refresh()`,**不轮询**(2026-10-17 拍板:
|
|
59
|
+
* 后端纯按需,刷新时机归前端 = 打开面板首读 + 手动刷新)。
|
|
61
60
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* 消费方式:`useSyncExternalStore(boardSubscribe, boardSnapshot)`。
|
|
62
|
+
* ⚠️ `getSnapshot` 必须返回**稳定引用**(每次新建对象会被判定为「一直在变」⇒ 无限重渲染),
|
|
63
|
+
* 故只在真变了时才换 `state` 对象。
|
|
64
64
|
*/
|
|
65
|
-
function pad2(value) {
|
|
66
|
-
return String(value).padStart(2, "0");
|
|
67
|
-
}
|
|
68
|
-
/** ISO → `YYYY-MM-DD HH:mm`(`seconds: true` 时补 `:ss`);解析失败返回 `fallback`(缺省原串)。 */
|
|
69
|
-
function formatDateTime(iso, opts) {
|
|
70
|
-
const ms = Date.parse(iso);
|
|
71
|
-
if (Number.isNaN(ms)) return opts?.fallback ?? iso;
|
|
72
|
-
const d = new Date(ms);
|
|
73
|
-
const base = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
74
|
-
return opts?.seconds === true ? `${base}:${pad2(d.getSeconds())}` : base;
|
|
75
|
-
}
|
|
76
|
-
/** 周一 00:00 所在日期(与 host `board.ts` 的 `mondayDateString` 同口径:周一为一周之始)。 */
|
|
77
|
-
function mondayOf(now) {
|
|
78
|
-
const date = new Date(now);
|
|
79
|
-
date.setDate(date.getDate() - (date.getDay() + 6) % 7);
|
|
80
|
-
return date;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* 时间范围 → `{ since, until }`(`YYYY-MM-DD`,含边界)。`all` 返回空对象 = 不拼参数。
|
|
84
|
-
* 近一年 = 今天往前推 365 天(与统计卡「近 13 个月文件」不同口径:那是文件窗口,这是创建时间过滤)。
|
|
85
|
-
*/
|
|
86
|
-
function rangeToDates(range, now = /* @__PURE__ */ new Date()) {
|
|
87
|
-
const ymd = (date) => `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
|
|
88
|
-
if (range === "week") return {
|
|
89
|
-
since: ymd(mondayOf(now)),
|
|
90
|
-
until: ymd(now)
|
|
91
|
-
};
|
|
92
|
-
if (range === "month") return {
|
|
93
|
-
since: ymd(new Date(now.getFullYear(), now.getMonth(), 1)),
|
|
94
|
-
until: ymd(now)
|
|
95
|
-
};
|
|
96
|
-
if (range === "year") {
|
|
97
|
-
const start = new Date(now);
|
|
98
|
-
start.setDate(start.getDate() - 365);
|
|
99
|
-
return {
|
|
100
|
-
since: ymd(start),
|
|
101
|
-
until: ymd(now)
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
return {};
|
|
105
|
-
}
|
|
106
|
-
/** 任务行是否落在时间范围内(在办侧在浏览器里筛;无创建时间的行按「不带时间过滤」处理)。 */
|
|
107
|
-
function withinRange(created, since, until) {
|
|
108
|
-
if (since === void 0 && until === void 0) return true;
|
|
109
|
-
if (created === null || created === void 0 || created === "") return false;
|
|
110
|
-
const day = created.slice(0, 10);
|
|
111
|
-
if (since !== void 0 && day < since) return false;
|
|
112
|
-
if (until !== void 0 && day > until) return false;
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
65
|
let state = {
|
|
116
66
|
status: "loading",
|
|
117
67
|
board: null,
|
|
118
68
|
error: null,
|
|
119
|
-
history: [],
|
|
120
|
-
historyStatus: "idle",
|
|
121
|
-
historyError: null,
|
|
122
|
-
filters: null,
|
|
123
69
|
query: {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
keyword: ""
|
|
128
|
-
limit: 500
|
|
70
|
+
scope: "all",
|
|
71
|
+
status: "all",
|
|
72
|
+
workspace: "",
|
|
73
|
+
keyword: ""
|
|
129
74
|
},
|
|
130
75
|
refreshing: false
|
|
131
76
|
};
|
|
@@ -157,7 +102,7 @@ window.__ModuleLoader__.load({
|
|
|
157
102
|
listeners.delete(listener);
|
|
158
103
|
};
|
|
159
104
|
}
|
|
160
|
-
/** 重读:在办快照 +
|
|
105
|
+
/** 重读:在办快照 + 完成类统计与近 8 周趋势(一次 `/board` 全带回)。 */
|
|
161
106
|
async function refresh() {
|
|
162
107
|
const token = ++runToken;
|
|
163
108
|
set({ refreshing: true });
|
|
@@ -169,8 +114,6 @@ window.__ModuleLoader__.load({
|
|
|
169
114
|
board,
|
|
170
115
|
error: null
|
|
171
116
|
});
|
|
172
|
-
loadFilters(token);
|
|
173
|
-
if (state.query.bucket === "closed") await loadHistory(token);
|
|
174
117
|
} catch (error) {
|
|
175
118
|
if (token !== runToken) return;
|
|
176
119
|
set({
|
|
@@ -181,88 +124,59 @@ window.__ModuleLoader__.load({
|
|
|
181
124
|
if (token === runToken) set({ refreshing: false });
|
|
182
125
|
}
|
|
183
126
|
}
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const filters = await fetchFilters();
|
|
187
|
-
if (token === runToken) set({ filters });
|
|
188
|
-
} catch {}
|
|
189
|
-
}
|
|
190
|
-
async function loadHistory(token) {
|
|
191
|
-
const { range, tag, limit } = state.query;
|
|
192
|
-
const dates = rangeToDates(range);
|
|
193
|
-
set({
|
|
194
|
-
historyStatus: "loading",
|
|
195
|
-
historyError: null
|
|
196
|
-
});
|
|
197
|
-
try {
|
|
198
|
-
const records = await fetchHistory({
|
|
199
|
-
...dates,
|
|
200
|
-
tag,
|
|
201
|
-
limit
|
|
202
|
-
});
|
|
203
|
-
if (token !== runToken) return;
|
|
204
|
-
set({
|
|
205
|
-
history: records,
|
|
206
|
-
historyStatus: "ready"
|
|
207
|
-
});
|
|
208
|
-
} catch (error) {
|
|
209
|
-
if (token !== runToken) return;
|
|
210
|
-
set({
|
|
211
|
-
historyStatus: "error",
|
|
212
|
-
historyError: error instanceof Error ? error.message : String(error)
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
/** 改筛选:已结案档要重取历史(服务端过滤);在办档只在浏览器里重筛(数据已全)。 */
|
|
127
|
+
/** 改筛选:全部在浏览器里完成(快照已含全部在办),不发请求。 */
|
|
217
128
|
function setQuery(patch) {
|
|
218
|
-
|
|
129
|
+
set({ query: {
|
|
219
130
|
...state.query,
|
|
220
131
|
...patch
|
|
221
|
-
};
|
|
222
|
-
set({ query });
|
|
223
|
-
if (query.bucket === "closed") loadHistory(++runToken);
|
|
224
|
-
}
|
|
225
|
-
/** 「加载更多」:加大 limit 重取(skill 脚本无 offset,笨办法;500 条一页无感)。 */
|
|
226
|
-
function loadMoreHistory() {
|
|
227
|
-
setQuery({ limit: state.query.limit + 500 });
|
|
132
|
+
} });
|
|
228
133
|
}
|
|
229
134
|
//#endregion
|
|
230
135
|
//#region src/client/rows.ts
|
|
231
|
-
/**
|
|
232
|
-
* 列表行的筛选 / 排序(**唯一**实现,纯函数、不碰网络):页面只渲染,不各写一份过滤。
|
|
233
|
-
*
|
|
234
|
-
* 单独成文件是为了能被冒烟直测(store.ts 引 api.ts,Node 剥类型解析不了无扩展名相对导入)。
|
|
235
|
-
*/
|
|
236
136
|
/** 关键字命中(标题 / 任务号 / 人 / 分类 / 标签;已结案另含「完成了什么」)。 */
|
|
237
137
|
function hitKeyword(keyword, parts) {
|
|
238
138
|
const needle = keyword.trim().toLowerCase();
|
|
239
139
|
if (needle === "") return true;
|
|
240
140
|
return parts.some((part) => part?.toLowerCase().includes(needle) === true);
|
|
241
141
|
}
|
|
142
|
+
/** 「我的」判定:owner = 该工作区的当前 git 身份,且状态是 doing / blocked(与 host 统计同口径)。 */
|
|
143
|
+
function isMine(task, board) {
|
|
144
|
+
if (board === null) return false;
|
|
145
|
+
const identity = board.identities[task.workspace.id] ?? null;
|
|
146
|
+
if (identity === null || identity === "") return false;
|
|
147
|
+
return task.owner === identity && (task.status === "doing" || task.status === "blocked");
|
|
148
|
+
}
|
|
149
|
+
/** 状态档命中(停滞由 host 标记,前端不判阈值)。 */
|
|
150
|
+
function statusHit(task, status) {
|
|
151
|
+
if (status === "all") return true;
|
|
152
|
+
if (status === "active") return task.status === "doing" || task.status === "blocked";
|
|
153
|
+
if (status === "open") return task.status === "open";
|
|
154
|
+
if (status === "blocked") return task.status === "blocked";
|
|
155
|
+
return task.stale === true;
|
|
156
|
+
}
|
|
157
|
+
/** 组内排序:别人的 doing/blocked 在前、待认领在后;各自按创建 / 开始时间倒序。 */
|
|
158
|
+
function inGroupOrder(a, b) {
|
|
159
|
+
if (a.kind !== "task" || b.kind !== "task") return 0;
|
|
160
|
+
const rank = (task) => task.status === "open" ? 1 : 0;
|
|
161
|
+
if (rank(a.task) !== rank(b.task)) return rank(a.task) - rank(b.task);
|
|
162
|
+
return String(b.task.created ?? b.task.started ?? "").localeCompare(String(a.task.created ?? a.task.started ?? ""));
|
|
163
|
+
}
|
|
242
164
|
/**
|
|
243
|
-
*
|
|
244
|
-
* -
|
|
245
|
-
* -
|
|
165
|
+
* 当前筛选下的两区视图:
|
|
166
|
+
* - 第一区「我的待办」= 我名下 doing/blocked(命中状态档 / 工作区 / 关键字);
|
|
167
|
+
* - 第二区「按工作区」= 其余任务(别人的 doing/blocked + 全部待认领),按工作区分组;
|
|
168
|
+
* - 范围=「我的」时只出第一区。
|
|
246
169
|
*/
|
|
247
|
-
function
|
|
248
|
-
const { board,
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
key: `${record.src ?? ""}:${record.time ?? ""}:${index}`,
|
|
258
|
-
record
|
|
259
|
-
}));
|
|
260
|
-
const dates = rangeToDates(query.range);
|
|
261
|
-
return (board?.tasks ?? []).filter((task) => {
|
|
262
|
-
if (query.bucket === "open" && task.status !== "open") return false;
|
|
263
|
-
if (query.bucket === "mine" && !isMine(task, board)) return false;
|
|
264
|
-
if (!withinRange(task.created, dates.since, dates.until)) return false;
|
|
265
|
-
if (query.tag !== "" && !(task.tags ?? []).includes(query.tag)) return false;
|
|
170
|
+
function selectView(current) {
|
|
171
|
+
const { board, query } = current;
|
|
172
|
+
if (board === null) return {
|
|
173
|
+
mine: [],
|
|
174
|
+
groups: [],
|
|
175
|
+
total: 0
|
|
176
|
+
};
|
|
177
|
+
const tasks = board.tasks.filter((task) => {
|
|
178
|
+
if (!statusHit(task, query.status)) return false;
|
|
179
|
+
if (query.workspace !== "" && task.workspace.id !== query.workspace) return false;
|
|
266
180
|
return hitKeyword(query.keyword, [
|
|
267
181
|
task.title,
|
|
268
182
|
task.id,
|
|
@@ -271,30 +185,74 @@ window.__ModuleLoader__.load({
|
|
|
271
185
|
task.category,
|
|
272
186
|
...task.tags ?? []
|
|
273
187
|
]);
|
|
274
|
-
})
|
|
188
|
+
});
|
|
189
|
+
const mineRows = tasks.filter((task) => isMine(task, board)).sort((a, b) => String(b.created ?? b.started ?? "").localeCompare(String(a.created ?? a.started ?? ""))).map((task) => ({
|
|
275
190
|
kind: "task",
|
|
276
191
|
key: `${task.workspace.id}:${task.id}`,
|
|
277
192
|
task,
|
|
278
|
-
mine:
|
|
193
|
+
mine: true
|
|
279
194
|
}));
|
|
195
|
+
const rest = query.scope === "mine" ? [] : tasks.filter((task) => !isMine(task, board));
|
|
196
|
+
const byWorkspace = /* @__PURE__ */ new Map();
|
|
197
|
+
for (const task of rest) {
|
|
198
|
+
const entry = byWorkspace.get(task.workspace.id) ?? {
|
|
199
|
+
title: task.workspace.title,
|
|
200
|
+
rows: []
|
|
201
|
+
};
|
|
202
|
+
entry.rows.push({
|
|
203
|
+
kind: "task",
|
|
204
|
+
key: `${task.workspace.id}:${task.id}`,
|
|
205
|
+
task,
|
|
206
|
+
mine: false
|
|
207
|
+
});
|
|
208
|
+
byWorkspace.set(task.workspace.id, entry);
|
|
209
|
+
}
|
|
210
|
+
const groups = [...byWorkspace.entries()].map(([workspaceId, entry]) => ({
|
|
211
|
+
workspaceId,
|
|
212
|
+
title: entry.title,
|
|
213
|
+
doingCount: entry.rows.filter((row) => row.kind === "task" && row.task.status !== "open").length,
|
|
214
|
+
openCount: entry.rows.filter((row) => row.kind === "task" && row.task.status === "open").length,
|
|
215
|
+
rows: entry.rows.sort(inGroupOrder)
|
|
216
|
+
})).sort((a, b) => a.title.localeCompare(b.title, "zh-Hans-CN"));
|
|
217
|
+
return {
|
|
218
|
+
mine: mineRows,
|
|
219
|
+
groups,
|
|
220
|
+
total: mineRows.length + groups.reduce((sum, group) => sum + group.rows.length, 0)
|
|
221
|
+
};
|
|
280
222
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/client/format.ts
|
|
225
|
+
/**
|
|
226
|
+
* 客户端时间口径**单源**:补零 / 时刻串(本机时区)。
|
|
227
|
+
* 解析失败按调用方给的兜底返回(不编造时间)。
|
|
228
|
+
*
|
|
229
|
+
* 2026-10-17 改版后首页不再做时间范围过滤(时间只作用于统计卡与趋势图,由 host 聚合)
|
|
230
|
+
* ⇒ 原来的 `rangeToDates` / `withinRange` / `RangeKey` 一并删除;统计 tab 要做时间筛选时
|
|
231
|
+
* 按当时口径在此重建,别处不许再写一份。
|
|
232
|
+
*/
|
|
233
|
+
function pad2(value) {
|
|
234
|
+
return String(value).padStart(2, "0");
|
|
235
|
+
}
|
|
236
|
+
/** ISO → `YYYY-MM-DD HH:mm`(`seconds: true` 时补 `:ss`);解析失败返回 `fallback`(缺省原串)。 */
|
|
237
|
+
function formatDateTime(iso, opts) {
|
|
238
|
+
const ms = Date.parse(iso);
|
|
239
|
+
if (Number.isNaN(ms)) return opts?.fallback ?? iso;
|
|
240
|
+
const d = new Date(ms);
|
|
241
|
+
const base = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
242
|
+
return opts?.seconds === true ? `${base}:${pad2(d.getSeconds())}` : base;
|
|
287
243
|
}
|
|
288
244
|
//#endregion
|
|
289
245
|
//#region src/client/tokens.ts
|
|
290
246
|
/**
|
|
291
247
|
* 语义 token 层:`--dtl-*`(dtl = dsh-task-ledger)。
|
|
292
248
|
*
|
|
293
|
-
*
|
|
249
|
+
* 规矩(docs/design/ui-style.md §三,2026-10-17 对齐 dispatch 表后收严):
|
|
294
250
|
* - **只有本文件定义 `--dtl-*`**;其它文件一律 `var(--dtl-*)` 消费,不许直接写 `var(--dsw-*)`;
|
|
295
251
|
* - 每个 token 都带兜底值 ⇒ 宿主升级换名时不炸(最坏退到兜底色,不会白屏);
|
|
296
252
|
* - 明暗差异只写在本文件末尾的 `body[data-ds-dark-theme]` 段:能靠宿主 alias 表达的
|
|
297
253
|
* 一律不写覆盖(alias 自己随主题变),只有「固定中性面 / 半透明底」这类 alias 表达不了的才进该段。
|
|
254
|
+
* - **离散刻度**:圆角 4/8/12/16/20/28、间距 4/8/12/16、控件高 24/28/32、字号 11/12/13/14/16
|
|
255
|
+
* ——业务文件不许出现 5/7/9px、不许自造高度档(要加档位改本文件)。
|
|
298
256
|
*
|
|
299
257
|
* 明暗判据 = `body[data-ds-dark-theme]`(宿主 0.2.0-rc.2 源码核实:启动脚本 toggleAttribute 写入)。
|
|
300
258
|
* 插件**禁用** `prefers-color-scheme`(它跟系统、不跟用户在宿主里的选择)。
|
|
@@ -304,11 +262,12 @@ window.__ModuleLoader__.load({
|
|
|
304
262
|
*/
|
|
305
263
|
const UI_TOKENS_CSS = `
|
|
306
264
|
body{
|
|
307
|
-
/* ── 文字四档
|
|
265
|
+
/* ── 文字四档 + 反色面 ───────────────────────────────────────── */
|
|
308
266
|
--dtl-fg:var(--dsw-alias-label-primary,#1f2328);
|
|
309
267
|
--dtl-fg-2:var(--dsw-alias-label-secondary,#5c6370);
|
|
310
268
|
--dtl-fg-3:var(--dsw-alias-label-tertiary,#767d87);
|
|
311
269
|
--dtl-fg-4:var(--dsw-alias-label-caption,#9aa1ab);
|
|
270
|
+
--dtl-fg-dim:var(--dsw-alias-label-dimmed,#b9bfc7);
|
|
312
271
|
/* 反色面上的字(角标 / 实心钮):暗色下是 bluish-1000 ⇒ 用 -foreground 而非 -inverted */
|
|
313
272
|
--dtl-fg-inverse:var(--dsw-alias-label-primary-foreground,#fff);
|
|
314
273
|
|
|
@@ -316,24 +275,36 @@ body{
|
|
|
316
275
|
--dtl-surface-1:var(--dsw-alias-bg-layer-1,#fff);
|
|
317
276
|
--dtl-surface-2:var(--dsw-alias-bg-layer-2,#fff);
|
|
318
277
|
--dtl-surface-base:var(--dsw-alias-bg-base,#fff);
|
|
278
|
+
/* 「选中亮片」底盘(配 shadow-raised 用) */
|
|
279
|
+
--dtl-surface-raised:var(--dsw-alias-bg-layer-1,#fff);
|
|
319
280
|
/* 固定中性实面(自绘浮标 / 角标这类必须自己变的东西) */
|
|
320
281
|
--dtl-solid:var(--dsw-static-neutral-00,#fff);
|
|
321
282
|
--dtl-on-solid:var(--dsw-alias-label-primary,#1f2328);
|
|
283
|
+
/* 斑马纹 / 次层面(浅色下近白、深色下深一档) */
|
|
284
|
+
--dtl-plate:var(--dsw-static-neutral-50,#fafafa);
|
|
285
|
+
--dtl-plate-hover:var(--dsw-static-neutral-100,#f5f5f5);
|
|
286
|
+
/* 表头底:浅色比斑马纹再深一档(两者不能撞色) */
|
|
287
|
+
--dtl-head-bg:var(--dsw-static-neutral-100,#f5f5f5);
|
|
322
288
|
|
|
323
|
-
/* ──
|
|
289
|
+
/* ── 描边四档(宿主真值 l1 4% / l2 10% / l3 12% / l4 16%)────── */
|
|
290
|
+
--dtl-border-faint:var(--dsw-alias-border-l1,#0000000a);
|
|
324
291
|
--dtl-border:var(--dsw-alias-border-l2,#0000001a);
|
|
325
292
|
--dtl-border-strong:var(--dsw-alias-border-l3,#0000001f);
|
|
293
|
+
--dtl-border-heavy:var(--dsw-alias-border-l4,#00000029);
|
|
326
294
|
|
|
327
295
|
/* ── 语义色 ──────────────────────────────────────────────────── */
|
|
328
|
-
/*
|
|
296
|
+
/* 品牌「面」色:明色近黑、暗色近白(实心钮选中态即此),不是蓝 */
|
|
297
|
+
--dtl-accent:var(--dsw-alias-brand-primary,#0f1115);
|
|
298
|
+
/* 蓝色强调 / 选中 / 焦点一律走这条 */
|
|
329
299
|
--dtl-business:var(--dsw-alias-state-business-primary,#3b5bdb);
|
|
330
300
|
--dtl-success:var(--dsw-alias-state-success-primary,#22c55e);
|
|
331
301
|
/* ⚠️ 宿主是 state-warn(不是 state-warning,后者无此定义) */
|
|
332
302
|
--dtl-warning:var(--dsw-alias-state-warn-primary,#f59e0b);
|
|
333
303
|
--dtl-danger:var(--dsw-alias-state-error-primary,#ec1313);
|
|
334
|
-
|
|
304
|
+
--dtl-link:var(--dsw-alias-link,#3b5bdb);
|
|
305
|
+
/* 语义色实面上的字(角标 / 实心提示):红绿黄都是中调 ⇒ 白字两个主题都可读 */
|
|
335
306
|
--dtl-on-signal:#fff;
|
|
336
|
-
/*
|
|
307
|
+
/* 语义色浅底:color-mix 现算 ⇒ 跟随宿主 alias,主题一换自动成立 */
|
|
337
308
|
--dtl-business-soft:color-mix(in srgb,var(--dtl-business) 8%,transparent);
|
|
338
309
|
--dtl-success-soft:color-mix(in srgb,var(--dtl-success) 8%,transparent);
|
|
339
310
|
--dtl-warning-soft:color-mix(in srgb,var(--dtl-warning) 8%,transparent);
|
|
@@ -341,6 +312,9 @@ body{
|
|
|
341
312
|
|
|
342
313
|
/* ── 交互底 ──────────────────────────────────────────────────── */
|
|
343
314
|
--dtl-hover:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));
|
|
315
|
+
--dtl-active:var(--dsw-alias-interactive-bg-active,#2631481a);
|
|
316
|
+
/* 卡 / 行 hover 高亮:淡蓝(灰高亮在卡底上几乎看不出来) */
|
|
317
|
+
--dtl-card-hover:rgba(37,99,235,.07);
|
|
344
318
|
/* 展开行底:带透明度的蓝,透出卡片底色(灰底跟斑马纹分不出来) */
|
|
345
319
|
--dtl-open-bg:rgba(37,99,235,.10);
|
|
346
320
|
--dtl-open-bg-soft:rgba(37,99,235,.05);
|
|
@@ -348,28 +322,44 @@ body{
|
|
|
348
322
|
--dtl-chip-bg:rgba(15,15,15,.07);
|
|
349
323
|
--dtl-chip-bg-hover:rgba(15,15,15,.14);
|
|
350
324
|
|
|
351
|
-
/* ──
|
|
325
|
+
/* ── 圆角(宿主真值 4/8/12/16/20/28)──────────────────────────── */
|
|
326
|
+
--dtl-radius-xs:var(--dsw-radius-xs,4px);
|
|
352
327
|
--dtl-radius-sm:var(--dsw-radius-sm,8px);
|
|
353
328
|
--dtl-radius-md:var(--dsw-radius-md,12px);
|
|
354
329
|
--dtl-radius-lg:var(--dsw-radius-lg,16px);
|
|
330
|
+
--dtl-radius-xl:var(--dsw-radius-xl,20px);
|
|
331
|
+
--dtl-radius-panel:var(--dsw-radius-panel,28px);
|
|
332
|
+
|
|
333
|
+
/* ── 间距四拍(只许这四档)────────────────────────────────────── */
|
|
355
334
|
--dtl-space-1:4px;
|
|
356
335
|
--dtl-space-2:8px;
|
|
357
336
|
--dtl-space-3:12px;
|
|
358
337
|
--dtl-space-4:16px;
|
|
338
|
+
|
|
339
|
+
/* ── 控件高度:离散三档(调用点不许自定义高度)────────────────── */
|
|
359
340
|
--dtl-control-h-sm:24px;
|
|
360
341
|
--dtl-control-h-md:28px;
|
|
361
342
|
--dtl-control-h-lg:32px;
|
|
343
|
+
|
|
344
|
+
/* ── 字号 / 行高(直绑宿主字号族 11/12/13/14/16)──────────────── */
|
|
362
345
|
--dtl-font-xs:var(--dsw-font-xxxs-11-font-size,11px);
|
|
363
346
|
--dtl-font-sm:var(--dsw-font-xxs-12-font-size,12px);
|
|
364
347
|
--dtl-font-md:var(--dsw-font-xs-13-font-size,13px);
|
|
365
348
|
--dtl-font-lg:var(--dsw-font-s-14-font-size,14px);
|
|
366
349
|
--dtl-font-xl:var(--dsw-font-base-16-font-size,16px);
|
|
350
|
+
--dtl-line-xs:var(--dsw-font-xxxs-11-line-height,14px);
|
|
351
|
+
--dtl-line-sm:var(--dsw-font-xxs-12-line-height,18px);
|
|
352
|
+
--dtl-line-md:var(--dsw-font-xs-13-line-height,20px);
|
|
353
|
+
--dtl-line-lg:var(--dsw-font-s-14-line-height,22px);
|
|
354
|
+
--dtl-line-xl:var(--dsw-font-base-16-line-height,24px);
|
|
367
355
|
--dtl-font-mono:var(--ds-font-family-code,monospace);
|
|
368
356
|
|
|
369
357
|
/* ── 投影 / 动效 / 层级 ──────────────────────────────────────── */
|
|
370
358
|
--dtl-shadow-1:var(--dsw-elevation-soft,0 4px 16px 0 #00000008);
|
|
359
|
+
--dtl-shadow-raised:var(--dsw-elevation-soft,0 4px 16px 0 #00000008);
|
|
371
360
|
--dtl-shadow-2:var(--dsw-shadow-lv3,0 12px 32px 0 #00000014);
|
|
372
361
|
--dtl-dur:var(--ds-transition-duration,.2s);
|
|
362
|
+
--dtl-dur-fast:var(--ds-transition-duration-fast,.12s);
|
|
373
363
|
--dtl-ease:var(--ds-ease-in-out,cubic-bezier(.4,0,.2,1));
|
|
374
364
|
--dtl-z-badge:20;
|
|
375
365
|
}
|
|
@@ -379,9 +369,14 @@ body{
|
|
|
379
369
|
body[data-ds-dark-theme]{
|
|
380
370
|
--dtl-solid:var(--dsw-static-neutral-900,#0f0f0f);
|
|
381
371
|
--dtl-on-solid:var(--dsw-static-neutral-00,#fff);
|
|
372
|
+
--dtl-plate:var(--dsw-static-neutral-850,#212123);
|
|
373
|
+
--dtl-plate-hover:var(--dsw-static-neutral-800,#292929);
|
|
374
|
+
/* 暗色表头反过来:比卡片面亮、比斑马纹浅一档 */
|
|
375
|
+
--dtl-head-bg:var(--dtl-surface-2);
|
|
382
376
|
/* 暗色下蓝要更亮、透明度更高才压得住深底 */
|
|
383
377
|
--dtl-open-bg:rgba(96,165,250,.18);
|
|
384
378
|
--dtl-open-bg-soft:rgba(96,165,250,.08);
|
|
379
|
+
--dtl-card-hover:rgba(96,165,250,.13);
|
|
385
380
|
/* 暗色 chip 底板反过来掺白 */
|
|
386
381
|
--dtl-chip-bg:rgba(255,255,255,.08);
|
|
387
382
|
--dtl-chip-bg-hover:rgba(255,255,255,.16);
|
|
@@ -391,129 +386,214 @@ body[data-ds-dark-theme]{
|
|
|
391
386
|
--dtl-warning-soft:color-mix(in srgb,var(--dtl-warning) 5%,transparent);
|
|
392
387
|
--dtl-danger-soft:color-mix(in srgb,var(--dtl-danger) 5%,transparent);
|
|
393
388
|
}
|
|
389
|
+
`;
|
|
390
|
+
/**
|
|
391
|
+
* 控件皮肤(L2):**一类控件唯一实现**,抄 dispatch 表 controls-css 的规矩——
|
|
392
|
+
* 只消费 var(--dtl-*),一个颜色 / 尺寸 / 圆角 / 字号字面量都不许出现;
|
|
393
|
+
* 高度只取三档,有边框的变体用 1px 真边框 + 内部 padding 补回 ⇒ 有边 / 无边同高。
|
|
394
|
+
*/
|
|
395
|
+
const UI_CONTROLS_CSS = `
|
|
396
|
+
/* ── 分段控件(滑动块)────────────────────────────────────────────
|
|
397
|
+
结构一套 + 两个受控轴:size(sm|md|lg) × variant(default|inset)。
|
|
398
|
+
变体只覆盖两个颜色变量(--seg-track / --seg-thumb),结构 / 尺寸 / 交互零重复。 */
|
|
399
|
+
.dtl-seg{display:inline-flex;align-items:center;gap:2px;padding:2px;border-radius:var(--dtl-radius-md);background:var(--seg-track);}
|
|
400
|
+
.dtl-seg--default{--seg-track:var(--dtl-surface-2);--seg-thumb:var(--dtl-surface-raised);border:1px solid var(--dtl-border);}
|
|
401
|
+
.dtl-seg--inset{--seg-track:var(--dtl-hover);--seg-thumb:var(--dtl-surface-raised);border:0;padding:3px;}
|
|
402
|
+
.dtl-seg__item{appearance:none;display:inline-flex;align-items:center;gap:4px;box-sizing:border-box;
|
|
403
|
+
height:calc(var(--dtl-control-h-sm) - 6px);padding:0 12px;border:0;border-radius:var(--dtl-radius-sm);
|
|
404
|
+
background:transparent;color:var(--dtl-fg-2);font-family:inherit;font-size:var(--dtl-font-sm);line-height:var(--dtl-line-sm);
|
|
405
|
+
font-weight:400;white-space:nowrap;cursor:pointer;
|
|
406
|
+
transition:color var(--dtl-dur-fast) var(--dtl-ease),background-color var(--dtl-dur-fast) var(--dtl-ease),box-shadow var(--dtl-dur-fast) var(--dtl-ease);}
|
|
407
|
+
.dtl-seg--md .dtl-seg__item{height:calc(var(--dtl-control-h-md) - 6px);}
|
|
408
|
+
.dtl-seg--lg .dtl-seg__item{height:calc(var(--dtl-control-h-lg) - 6px);}
|
|
409
|
+
.dtl-seg__item:hover:not([aria-pressed='true']):not(:disabled){color:var(--dtl-fg);}
|
|
410
|
+
.dtl-seg__item[aria-pressed='true']{background:var(--seg-thumb);box-shadow:var(--dtl-shadow-raised);color:var(--dtl-fg);font-weight:600;}
|
|
411
|
+
.dtl-seg__item:focus-visible{outline:2px solid var(--dtl-business);outline-offset:-2px;}
|
|
412
|
+
.dtl-seg__item:disabled{cursor:default;opacity:.4;}
|
|
413
|
+
/* 段内角标(如「待认领 3」):语义色实面 + 反白字,0 不渲染;必须是正圆 */
|
|
414
|
+
.dtl-seg__badge{display:inline-flex;align-items:center;justify-content:center;flex:none;
|
|
415
|
+
width:16px;height:16px;padding:0;overflow:hidden;border-radius:50%;
|
|
416
|
+
background:var(--dtl-danger);color:var(--dtl-on-signal);font-size:10px;line-height:16px;font-weight:500;font-variant-numeric:tabular-nums;}
|
|
417
|
+
|
|
418
|
+
/* ── 按钮 / 图标钮 ───────────────────────────────────────────────
|
|
419
|
+
与 Segmented 同一条高度纪律;variant 只换颜色,结构 / 尺寸 / 交互只此一份。 */
|
|
420
|
+
.dtl-btn{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:var(--dtl-space-1);
|
|
421
|
+
box-sizing:border-box;border:1px solid transparent;border-radius:var(--dtl-radius-md);
|
|
422
|
+
background:transparent;color:var(--dtl-fg);font-family:inherit;font-weight:500;white-space:nowrap;cursor:pointer;
|
|
423
|
+
transition:background-color var(--dtl-dur-fast) var(--dtl-ease),border-color var(--dtl-dur-fast) var(--dtl-ease),color var(--dtl-dur-fast) var(--dtl-ease),opacity var(--dtl-dur-fast) var(--dtl-ease);}
|
|
424
|
+
.dtl-btn--sm{height:var(--dtl-control-h-sm);padding:0 10px;font-size:var(--dtl-font-sm);line-height:var(--dtl-line-sm);}
|
|
425
|
+
.dtl-btn--md{height:var(--dtl-control-h-md);padding:0 12px;font-size:var(--dtl-font-md);line-height:var(--dtl-line-md);}
|
|
426
|
+
.dtl-btn--lg{height:var(--dtl-control-h-lg);padding:0 14px;font-size:var(--dtl-font-md);line-height:var(--dtl-line-md);}
|
|
427
|
+
.dtl-btn--primary{background:var(--dtl-accent);color:var(--dtl-fg-inverse);}
|
|
428
|
+
.dtl-btn--primary:hover:not(:disabled){opacity:.9;}
|
|
429
|
+
.dtl-btn--outline{background:var(--dtl-surface-1);border-color:var(--dtl-border-strong);color:var(--dtl-fg);}
|
|
430
|
+
.dtl-btn--outline:hover:not(:disabled){background:var(--dtl-hover);}
|
|
431
|
+
.dtl-btn--ghost{color:var(--dtl-fg-2);}
|
|
432
|
+
.dtl-btn--ghost:hover:not(:disabled){background:var(--dtl-hover);color:var(--dtl-fg);}
|
|
433
|
+
.dtl-btn:focus-visible{outline:2px solid var(--dtl-business);outline-offset:2px;}
|
|
434
|
+
.dtl-btn:disabled{cursor:default;opacity:.5;}
|
|
435
|
+
.dtl-iconbtn{appearance:none;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;
|
|
436
|
+
border:1px solid transparent;border-radius:var(--dtl-radius-sm);background:transparent;color:var(--dtl-fg-2);
|
|
437
|
+
font-family:inherit;cursor:pointer;flex:none;
|
|
438
|
+
transition:background-color var(--dtl-dur-fast) var(--dtl-ease),border-color var(--dtl-dur-fast) var(--dtl-ease),color var(--dtl-dur-fast) var(--dtl-ease),opacity var(--dtl-dur-fast) var(--dtl-ease);}
|
|
439
|
+
.dtl-iconbtn--sm{width:var(--dtl-control-h-sm);height:var(--dtl-control-h-sm);font-size:var(--dtl-font-sm);}
|
|
440
|
+
.dtl-iconbtn--md{width:var(--dtl-control-h-md);height:var(--dtl-control-h-md);font-size:var(--dtl-font-md);}
|
|
441
|
+
.dtl-iconbtn--lg{width:var(--dtl-control-h-lg);height:var(--dtl-control-h-lg);font-size:var(--dtl-font-md);}
|
|
442
|
+
.dtl-iconbtn--outline{background:var(--dtl-surface-1);border-color:var(--dtl-border);color:var(--dtl-fg-2);}
|
|
443
|
+
.dtl-iconbtn:hover:not(:disabled){background:var(--dtl-hover);color:var(--dtl-fg);}
|
|
444
|
+
.dtl-iconbtn:focus-visible{outline:2px solid var(--dtl-business);outline-offset:1px;}
|
|
445
|
+
.dtl-iconbtn:disabled{cursor:default;opacity:.4;}
|
|
446
|
+
|
|
447
|
+
/* ── 输入 / 搜索框 / 下拉 ──────────────────────────────────────── */
|
|
448
|
+
.dtl-input{box-sizing:border-box;border:1px solid var(--dtl-border);border-radius:var(--dtl-radius-md);
|
|
449
|
+
background:var(--dtl-surface-1);color:var(--dtl-fg);font-family:inherit;outline:none;
|
|
450
|
+
transition:border-color var(--dtl-dur-fast) var(--dtl-ease),box-shadow var(--dtl-dur-fast) var(--dtl-ease);}
|
|
451
|
+
.dtl-input--sm{height:var(--dtl-control-h-sm);padding:0 8px;font-size:var(--dtl-font-sm);line-height:var(--dtl-line-sm);}
|
|
452
|
+
.dtl-input--md{height:var(--dtl-control-h-md);padding:0 10px;font-size:var(--dtl-font-md);line-height:var(--dtl-line-md);}
|
|
453
|
+
.dtl-input--lg{height:var(--dtl-control-h-lg);padding:0 12px;font-size:var(--dtl-font-md);line-height:var(--dtl-line-md);}
|
|
454
|
+
.dtl-input::placeholder{color:var(--dtl-fg-dim);}
|
|
455
|
+
.dtl-input:focus{border-color:var(--dtl-business);box-shadow:0 0 0 1px var(--dtl-business);}
|
|
456
|
+
.dtl-input:disabled{cursor:default;opacity:.5;}
|
|
457
|
+
/* 原生 select: appearance:none + 自绘箭头 ⇒ 与输入框同宗(宿主没有廉价下拉件) */
|
|
458
|
+
.dtl-select{appearance:none;-webkit-appearance:none;box-sizing:border-box;
|
|
459
|
+
border:1px solid var(--dtl-border);border-radius:var(--dtl-radius-md);background:var(--dtl-surface-1);color:var(--dtl-fg);
|
|
460
|
+
font-family:inherit;outline:none;cursor:pointer;
|
|
461
|
+
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23767d87' stroke-width='1.5'><path d='M4 6.5 8 10.5 12 6.5'/></svg>");
|
|
462
|
+
background-repeat:no-repeat;background-position:right 8px center;background-size:14px;
|
|
463
|
+
transition:border-color var(--dtl-dur-fast) var(--dtl-ease),box-shadow var(--dtl-dur-fast) var(--dtl-ease);}
|
|
464
|
+
.dtl-select--sm{height:var(--dtl-control-h-sm);padding:0 26px 0 8px;font-size:var(--dtl-font-sm);line-height:var(--dtl-line-sm);}
|
|
465
|
+
.dtl-select--md{height:var(--dtl-control-h-md);padding:0 26px 0 10px;font-size:var(--dtl-font-md);line-height:var(--dtl-line-md);}
|
|
466
|
+
.dtl-select--lg{height:var(--dtl-control-h-lg);padding:0 26px 0 12px;font-size:var(--dtl-font-md);line-height:var(--dtl-line-md);}
|
|
467
|
+
.dtl-select:focus{border-color:var(--dtl-business);box-shadow:0 0 0 1px var(--dtl-business);}
|
|
468
|
+
.dtl-select:disabled{cursor:default;opacity:.5;}
|
|
469
|
+
|
|
470
|
+
/* ── 卡 / chip / 空态 / 列表行(业务壳,皮肤仍只消费 token)────── */
|
|
471
|
+
.dtl-card{background:var(--dtl-surface-1);border:1px solid var(--dtl-border);border-radius:var(--dtl-radius-md);
|
|
472
|
+
transition:background-color var(--dtl-dur-fast) var(--dtl-ease);}
|
|
473
|
+
.dtl-card--clickable{cursor:pointer;text-align:left;font:inherit;color:inherit;width:100%;}
|
|
474
|
+
.dtl-card--clickable:hover{background:var(--dtl-card-hover);}
|
|
475
|
+
.dtl-card--clickable[data-active='true']{border-color:var(--dtl-business);}
|
|
476
|
+
.dtl-card--clickable:focus-visible{outline:2px solid var(--dtl-business);outline-offset:1px;}
|
|
477
|
+
.dtl-chip{display:inline-flex;align-items:center;gap:4px;height:20px;padding:0 8px;border-radius:var(--dtl-radius-sm);
|
|
478
|
+
background:var(--dtl-chip-bg);color:var(--dtl-fg-2);font-size:var(--dtl-font-xs);line-height:var(--dtl-line-xs);white-space:nowrap;}
|
|
479
|
+
.dtl-row{display:flex;align-items:center;gap:var(--dtl-space-2);width:100%;box-sizing:border-box;
|
|
480
|
+
padding:var(--dtl-space-2) var(--dtl-space-3);border:0;border-bottom:1px solid var(--dtl-border-faint);
|
|
481
|
+
background:transparent;font:inherit;color:inherit;text-align:left;cursor:pointer;
|
|
482
|
+
transition:background-color var(--dtl-dur-fast) var(--dtl-ease);}
|
|
483
|
+
.dtl-row:hover{background:var(--dtl-card-hover);}
|
|
484
|
+
.dtl-row:focus-visible{outline:2px solid var(--dtl-business);outline-offset:-2px;}
|
|
485
|
+
.dtl-row[data-open='true']{background:var(--dtl-open-bg);}
|
|
486
|
+
.dtl-row-body{padding:var(--dtl-space-2) var(--dtl-space-3) var(--dtl-space-3);border-bottom:1px solid var(--dtl-border-faint);
|
|
487
|
+
background:var(--dtl-open-bg-soft);font-size:var(--dtl-font-md);}
|
|
488
|
+
.dtl-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
489
|
+
|
|
490
|
+
@media (prefers-reduced-motion: reduce){
|
|
491
|
+
.dtl-seg__item,.dtl-btn,.dtl-iconbtn,.dtl-input,.dtl-select,.dtl-card,.dtl-row{transition:none;}
|
|
492
|
+
}
|
|
394
493
|
`;
|
|
395
494
|
//#endregion
|
|
396
495
|
//#region src/client/ui.ts
|
|
397
496
|
/**
|
|
398
|
-
* UI 基础层:样式单源 +
|
|
497
|
+
* UI 基础层:样式单源 + 共用元件(**一类控件只有一份实现**)。
|
|
399
498
|
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
* 2.
|
|
499
|
+
* 纪律(用户 2026-10-17「样式与方法一律抽象,不要反复重写」;形态抄本机
|
|
500
|
+
* `dsh-task-dispatch-table` 的 `ui/tokens.ts` + `ui/controls-css.ts` + `ui/*.tsx`):
|
|
501
|
+
* 1. **样式**:颜色 / 圆角 / 字号 / 高度一律 `var(--dtl-*)`(定义只在 tokens.ts),
|
|
502
|
+
* 业务文件不许再写第二份 `var(--dsw-*)`、不许自造 `--dtl-*`、不许自造高度档;
|
|
503
|
+
* 2. **元件**:分段 / 按钮 / 图标钮 / 输入 / 下拉 / 卡 / chip / 空态 / 图表各一份,
|
|
504
|
+
* 页面与浮标共用。官方 primitives 只留「官方做得更好」的(StateDot / MarkdownText / 图标)。
|
|
405
505
|
*/
|
|
406
|
-
/**
|
|
506
|
+
/** 基础层样式(tokens + 控件皮肤;幂等注入)。 */
|
|
407
507
|
const UI_BASE_CSS = `
|
|
408
|
-
/* 统计卡:可点、hover
|
|
409
|
-
.dtl-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
background:var(--dtl-surface-1);color:var(--dtl-fg);
|
|
414
|
-
cursor:pointer;text-align:left;font:inherit;
|
|
415
|
-
transition:background var(--dtl-dur) var(--dtl-ease),border-color var(--dtl-dur) var(--dtl-ease);
|
|
416
|
-
}
|
|
417
|
-
.dtl-card:hover{background:var(--dtl-hover)}
|
|
418
|
-
.dtl-card[aria-pressed="true"]{border-color:var(--dtl-business);background:var(--dtl-business-soft)}
|
|
419
|
-
.dtl-card:focus-visible{outline:2px solid var(--dtl-business);outline-offset:2px}
|
|
508
|
+
/* 统计卡:可点、hover 淡蓝、选中蓝描边 */
|
|
509
|
+
.dtl-stat{display:flex;flex-direction:column;gap:var(--dtl-space-1);
|
|
510
|
+
padding:var(--dtl-space-3) var(--dtl-space-4);text-align:left;}
|
|
511
|
+
.dtl-stat__value{font-size:var(--dtl-font-xl);font-weight:600;line-height:var(--dtl-line-xl);font-variant-numeric:tabular-nums;}
|
|
512
|
+
.dtl-stat__hint{font-size:var(--dtl-font-xs);line-height:var(--dtl-line-xs);color:var(--dtl-fg-3);}
|
|
420
513
|
|
|
421
|
-
/* 列表行:头部可点,hover
|
|
422
|
-
.dtl-row{
|
|
423
|
-
|
|
424
|
-
.dtl-row-head{
|
|
425
|
-
display:flex;align-items:center;gap:var(--dtl-space-2);
|
|
426
|
-
width:100%;padding:var(--dtl-space-3);border:0;background:transparent;
|
|
514
|
+
/* 列表行:头部可点,hover 淡蓝;展开行走 --dtl-open-bg */
|
|
515
|
+
.dtl-row-head{display:flex;align-items:center;gap:var(--dtl-space-2);
|
|
516
|
+
width:100%;padding:var(--dtl-space-2) var(--dtl-space-3);border:0;background:transparent;
|
|
427
517
|
color:inherit;font:inherit;text-align:left;cursor:pointer;
|
|
428
|
-
transition:background var(--dtl-dur) var(--dtl-ease);
|
|
429
|
-
}
|
|
430
|
-
.dtl-row-head:
|
|
431
|
-
.dtl-row-
|
|
432
|
-
.dtl-row
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
/* chip(分类 / 标签):小底板走 --dtl-chip-bg,hover 加深 */
|
|
436
|
-
.dtl-chip{
|
|
437
|
-
display:inline-flex;align-items:center;gap:var(--dtl-space-1);
|
|
438
|
-
padding:0 var(--dtl-space-2);height:20px;border-radius:var(--dtl-radius-sm);
|
|
439
|
-
background:var(--dtl-chip-bg);color:var(--dtl-fg-2);
|
|
440
|
-
font-size:var(--dtl-font-xs);white-space:nowrap;max-width:20ch;overflow:hidden;text-overflow:ellipsis;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/* 原生下拉 / 输入框:宿主没有廉价的选择器,用原生控件 + token 配色(不用自绘菜单) */
|
|
444
|
-
.dtl-select,.dtl-input{
|
|
445
|
-
height:var(--dtl-control-h-md);box-sizing:border-box;
|
|
446
|
-
padding:0 var(--dtl-space-2);border:1px solid var(--dtl-border);border-radius:var(--dtl-radius-sm);
|
|
447
|
-
background:var(--dtl-surface-1);color:var(--dtl-fg);
|
|
448
|
-
font-family:inherit;font-size:var(--dtl-font-md);
|
|
449
|
-
}
|
|
450
|
-
.dtl-input{padding-left:var(--dtl-space-3);min-width:220px}
|
|
451
|
-
.dtl-select:focus-visible,.dtl-input:focus-visible{outline:2px solid var(--dtl-business);outline-offset:1px}
|
|
518
|
+
transition:background-color var(--dtl-dur-fast) var(--dtl-ease);}
|
|
519
|
+
.dtl-row-head:hover{background:var(--dtl-card-hover);}
|
|
520
|
+
.dtl-row-head:focus-visible{outline:2px solid var(--dtl-business);outline-offset:-2px;}
|
|
521
|
+
.dtl-row[data-open="true"]{background:var(--dtl-open-bg);}
|
|
522
|
+
.dtl-row-body{padding:var(--dtl-space-2) var(--dtl-space-3) var(--dtl-space-3);
|
|
523
|
+
background:var(--dtl-open-bg-soft);font-size:var(--dtl-font-md);}
|
|
452
524
|
|
|
453
525
|
/* 空态:真空 = 虚线引导卡;筛选空 = 小字 */
|
|
454
|
-
.dtl-empty-guide{
|
|
455
|
-
padding:var(--dtl-space-4);border:1px dashed var(--dtl-border-strong);border-radius:var(--dtl-radius-md);
|
|
456
|
-
background:transparent;
|
|
457
|
-
}
|
|
526
|
+
.dtl-empty-guide{padding:var(--dtl-space-4);border:1px dashed var(--dtl-border-strong);border-radius:var(--dtl-radius-md);background:transparent;}
|
|
458
527
|
.dtl-empty-filtered{padding:var(--dtl-space-4) 0;color:var(--dtl-fg-3);font-size:var(--dtl-font-md)}
|
|
459
528
|
|
|
460
529
|
/* 浮标角标:0→n 跳变时一次脉冲,不自动消失 */
|
|
461
|
-
.dtl-badge{
|
|
462
|
-
position:absolute;top:-2px;right:-2px;min-width:16px;height:16px;padding:0 4px;
|
|
530
|
+
.dtl-badge{position:absolute;top:-2px;right:-2px;min-width:16px;height:16px;padding:0 4px;
|
|
463
531
|
border-radius:8px;background:var(--dtl-danger);color:var(--dtl-on-signal);
|
|
464
|
-
font-size:10px;line-height:16px;text-align:center;font-weight:600;
|
|
465
|
-
}
|
|
532
|
+
font-size:10px;line-height:16px;text-align:center;font-weight:600;}
|
|
466
533
|
.dtl-badge[data-pulse="true"]{animation:dtl-pulse 900ms var(--dtl-ease) 1}
|
|
467
534
|
@keyframes dtl-pulse{0%{transform:scale(.6);opacity:.4}60%{transform:scale(1.15)}100%{transform:scale(1)}}
|
|
468
535
|
|
|
469
536
|
/* 过程正文:宽度受限、长行折行 */
|
|
470
537
|
.dtl-worklog{max-width:72ch;font-size:var(--dtl-font-md);line-height:1.6}
|
|
538
|
+
|
|
539
|
+
/* 趋势柱状图:div 柱 + token 色,无新依赖 */
|
|
540
|
+
.dtl-chart{display:flex;align-items:flex-end;gap:var(--dtl-space-2);height:120px;}
|
|
541
|
+
.dtl-chart__col{display:flex;flex-direction:column;justify-content:flex-end;align-items:center;gap:var(--dtl-space-1);flex:1 1 0;height:100%;min-width:0;}
|
|
542
|
+
.dtl-chart__bar{width:100%;max-width:28px;min-height:2px;border-radius:var(--dtl-radius-xs) var(--dtl-radius-xs) 0 0;
|
|
543
|
+
background:var(--dtl-business);transition:background-color var(--dtl-dur-fast) var(--dtl-ease);}
|
|
544
|
+
.dtl-chart__col:hover .dtl-chart__bar{background:var(--dtl-accent);}
|
|
545
|
+
.dtl-chart__val{font-size:var(--dtl-font-xs);line-height:var(--dtl-line-xs);color:var(--dtl-fg-2);font-variant-numeric:tabular-nums;}
|
|
546
|
+
.dtl-chart__label{font-size:10px;line-height:var(--dtl-line-xs);color:var(--dtl-fg-3);white-space:nowrap;}
|
|
471
547
|
`;
|
|
472
548
|
const STYLE_ID = "dtl-ui-base";
|
|
473
|
-
/** 幂等注入 token 层 + 基础层(面板与浮标谁先挂载都只注入一次)。 */
|
|
549
|
+
/** 幂等注入 token 层 + 控件皮肤 + 基础层(面板与浮标谁先挂载都只注入一次)。 */
|
|
474
550
|
function ensureUiBase() {
|
|
475
551
|
if (typeof document === "undefined") return;
|
|
476
552
|
if (document.getElementById(STYLE_ID) !== null) return;
|
|
477
553
|
const style = document.createElement("style");
|
|
478
554
|
style.id = STYLE_ID;
|
|
479
|
-
style.textContent = `${UI_TOKENS_CSS}\n${UI_BASE_CSS}`;
|
|
555
|
+
style.textContent = `${UI_TOKENS_CSS}\n${UI_CONTROLS_CSS}\n${UI_BASE_CSS}`;
|
|
480
556
|
document.head.appendChild(style);
|
|
481
557
|
}
|
|
482
|
-
/**
|
|
483
|
-
function
|
|
558
|
+
/** 分段控件(单选):同一份结构 + size 轴;外观差异只由 variant 的两个颜色变量表达。 */
|
|
559
|
+
function Segmented(props) {
|
|
560
|
+
ensureUiBase();
|
|
561
|
+
const size = props.size ?? "md";
|
|
562
|
+
const variant = props.variant ?? "default";
|
|
563
|
+
return (0, react.createElement)("div", {
|
|
564
|
+
role: "group",
|
|
565
|
+
"aria-label": props.label,
|
|
566
|
+
id: props.id,
|
|
567
|
+
className: `dtl-seg dtl-seg--${variant} dtl-seg--${size}`,
|
|
568
|
+
style: props.style
|
|
569
|
+
}, props.items.map((item) => (0, react.createElement)("button", {
|
|
570
|
+
key: item.value,
|
|
571
|
+
type: "button",
|
|
572
|
+
className: "dtl-seg__item",
|
|
573
|
+
"aria-pressed": item.value === props.value,
|
|
574
|
+
onClick: () => {
|
|
575
|
+
props.onChange(item.value);
|
|
576
|
+
}
|
|
577
|
+
}, item.label, item.badge !== void 0 && item.badge > 0 ? (0, react.createElement)("span", { className: "dtl-seg__badge" }, String(item.badge)) : null)));
|
|
578
|
+
}
|
|
579
|
+
function Button(props) {
|
|
580
|
+
ensureUiBase();
|
|
484
581
|
return (0, react.createElement)("button", {
|
|
485
582
|
type: "button",
|
|
486
|
-
className: "dtl-
|
|
487
|
-
|
|
583
|
+
className: `dtl-btn dtl-btn--${props.variant ?? "outline"} dtl-btn--${props.size ?? "md"}`,
|
|
584
|
+
disabled: props.disabled === true,
|
|
488
585
|
title: props.title,
|
|
489
586
|
onClick: props.onClick
|
|
490
|
-
},
|
|
491
|
-
fontSize: "var(--dtl-font-sm)",
|
|
492
|
-
color: "var(--dtl-fg-2)"
|
|
493
|
-
} }, props.label), (0, react.createElement)("span", { style: {
|
|
494
|
-
fontSize: "24px",
|
|
495
|
-
fontWeight: 600,
|
|
496
|
-
lineHeight: 1.2
|
|
497
|
-
} }, props.value));
|
|
498
|
-
}
|
|
499
|
-
/** chip(分类 / 标签 / 工作区名)。 */
|
|
500
|
-
function Chip(props) {
|
|
501
|
-
const style = {};
|
|
502
|
-
if (props.tone === "signal" && props.color !== void 0) {
|
|
503
|
-
style.background = props.color;
|
|
504
|
-
style.color = "var(--dtl-on-signal)";
|
|
505
|
-
}
|
|
506
|
-
return (0, react.createElement)("span", {
|
|
507
|
-
className: "dtl-chip",
|
|
508
|
-
style,
|
|
509
|
-
title: props.text
|
|
510
|
-
}, props.text);
|
|
587
|
+
}, props.icon, props.children);
|
|
511
588
|
}
|
|
512
|
-
/**
|
|
589
|
+
/** 原生下拉(工作区 / 状态共用一份;宿主没有廉价下拉件 ⇒ 原生 + token 皮肤)。 */
|
|
513
590
|
function Select(props) {
|
|
591
|
+
ensureUiBase();
|
|
592
|
+
const size = props.size ?? "md";
|
|
514
593
|
return (0, react.createElement)("select", {
|
|
515
|
-
className:
|
|
594
|
+
className: `dtl-select dtl-select--${size}`,
|
|
516
595
|
"aria-label": props.ariaLabel,
|
|
596
|
+
style: props.width === void 0 ? void 0 : { width: `${props.width}px` },
|
|
517
597
|
value: props.value,
|
|
518
598
|
onChange: (event) => {
|
|
519
599
|
props.onChange(event.target.value);
|
|
@@ -523,10 +603,12 @@ body[data-ds-dark-theme]{
|
|
|
523
603
|
value: option.value
|
|
524
604
|
}, option.label)));
|
|
525
605
|
}
|
|
526
|
-
/**
|
|
606
|
+
/** 搜索框(关键字过滤;占满剩余宽度由调用方的 flex 决定)。 */
|
|
527
607
|
function SearchInput(props) {
|
|
608
|
+
ensureUiBase();
|
|
609
|
+
const size = props.size ?? "md";
|
|
528
610
|
return (0, react.createElement)("input", {
|
|
529
|
-
className:
|
|
611
|
+
className: `dtl-input dtl-input--${size}`,
|
|
530
612
|
type: "search",
|
|
531
613
|
"aria-label": props.ariaLabel,
|
|
532
614
|
placeholder: props.placeholder,
|
|
@@ -537,8 +619,35 @@ body[data-ds-dark-theme]{
|
|
|
537
619
|
}
|
|
538
620
|
});
|
|
539
621
|
}
|
|
622
|
+
/** 统计卡:主数字 + 灰色口径小字;点一下联动下方列表筛选。 */
|
|
623
|
+
function StatCard(props) {
|
|
624
|
+
ensureUiBase();
|
|
625
|
+
return (0, react.createElement)("button", {
|
|
626
|
+
type: "button",
|
|
627
|
+
className: "dtl-card dtl-card--clickable dtl-stat",
|
|
628
|
+
"data-active": props.active,
|
|
629
|
+
title: props.title,
|
|
630
|
+
onClick: props.onClick
|
|
631
|
+
}, (0, react.createElement)("span", { style: {
|
|
632
|
+
fontSize: "var(--dtl-font-sm)",
|
|
633
|
+
color: "var(--dtl-fg-2)"
|
|
634
|
+
} }, props.label), (0, react.createElement)("span", { className: "dtl-stat__value" }, props.value), props.hint === void 0 || props.hint === "" ? null : (0, react.createElement)("span", { className: "dtl-stat__hint" }, props.hint));
|
|
635
|
+
}
|
|
636
|
+
/** chip(分类 / 标签 / 工作区名)。 */
|
|
637
|
+
function Chip(props) {
|
|
638
|
+
ensureUiBase();
|
|
639
|
+
return (0, react.createElement)("span", {
|
|
640
|
+
className: "dtl-chip",
|
|
641
|
+
style: props.color === void 0 ? void 0 : {
|
|
642
|
+
background: props.color,
|
|
643
|
+
color: "var(--dtl-on-signal)"
|
|
644
|
+
},
|
|
645
|
+
title: props.text
|
|
646
|
+
}, props.text);
|
|
647
|
+
}
|
|
540
648
|
/** 空态:`guide` = 真空引导卡(虚线),`filtered` = 筛选空小字。 */
|
|
541
649
|
function EmptyState(props) {
|
|
650
|
+
ensureUiBase();
|
|
542
651
|
if (props.tone === "filtered") return (0, react.createElement)("div", { className: "dtl-empty-filtered" }, props.title);
|
|
543
652
|
return (0, react.createElement)("div", { className: "dtl-empty-guide" }, (0, react.createElement)("div", { style: {
|
|
544
653
|
fontSize: "var(--dtl-font-lg)",
|
|
@@ -549,6 +658,26 @@ body[data-ds-dark-theme]{
|
|
|
549
658
|
color: "var(--dtl-fg-2)"
|
|
550
659
|
} }, props.hint));
|
|
551
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* 柱状图(div 柱,无新依赖):每桶一柱,柱高按最大值归一;
|
|
663
|
+
* 数值常显(10px 灰),具体时刻走 title。零值的桶画 2px 矮桩,避免「这一周不存在」的误读。
|
|
664
|
+
*/
|
|
665
|
+
function BarChart(props) {
|
|
666
|
+
ensureUiBase();
|
|
667
|
+
const max = props.buckets.reduce((peak, bucket) => Math.max(peak, bucket.value), 0);
|
|
668
|
+
return (0, react.createElement)("div", {
|
|
669
|
+
className: "dtl-chart",
|
|
670
|
+
role: "img",
|
|
671
|
+
"aria-label": props.ariaLabel
|
|
672
|
+
}, props.buckets.map((bucket) => (0, react.createElement)("div", {
|
|
673
|
+
key: bucket.label,
|
|
674
|
+
className: "dtl-chart__col",
|
|
675
|
+
title: bucket.title
|
|
676
|
+
}, (0, react.createElement)("span", { className: "dtl-chart__val" }, bucket.value === 0 ? "" : String(bucket.value)), (0, react.createElement)("span", {
|
|
677
|
+
className: "dtl-chart__bar",
|
|
678
|
+
style: { height: `${max === 0 ? 2 : Math.max(2, Math.round(bucket.value / max * 100))}%` }
|
|
679
|
+
}), (0, react.createElement)("span", { className: "dtl-chart__label" }, bucket.label))));
|
|
680
|
+
}
|
|
552
681
|
//#endregion
|
|
553
682
|
//#region src/client/task-row.ts
|
|
554
683
|
/**
|
|
@@ -558,6 +687,9 @@ body[data-ds-dark-theme]{
|
|
|
558
687
|
*
|
|
559
688
|
* 行展开 = 过程文档路径 + 过程正文(在办走 `GET /task` 取正文;已结案只有路径——
|
|
560
689
|
* host 没有按历史行取正文的路由,不编造内容)。
|
|
690
|
+
*
|
|
691
|
+
* `showWorkspace`:第一区「我的待办」是跨工作区平铺 ⇒ 工作区升为一列(定宽,不再是
|
|
692
|
+
* 和标签混在一起的小 chip);第二区分组行由分组标题说明归属,行内不再重复。
|
|
561
693
|
*/
|
|
562
694
|
/** 行的状态点语义(在办三态 + 已结案)。 */
|
|
563
695
|
function dotOf(row) {
|
|
@@ -577,6 +709,36 @@ body[data-ds-dark-theme]{
|
|
|
577
709
|
if (row.kind === "task") return row.task.owner ?? row.task.creator ?? null;
|
|
578
710
|
return row.record.owner ?? null;
|
|
579
711
|
}
|
|
712
|
+
/** 工作区徽标(第一区专用列):色块首字母 + 标题,定宽、超出省略(不猜官方图标名)。 */
|
|
713
|
+
function WorkspaceTag(props) {
|
|
714
|
+
if (props.workspace === void 0) return null;
|
|
715
|
+
const initial = props.workspace.title.trim().slice(0, 1);
|
|
716
|
+
return (0, react.createElement)("span", {
|
|
717
|
+
className: "dtl-ellipsis",
|
|
718
|
+
style: {
|
|
719
|
+
display: "inline-flex",
|
|
720
|
+
alignItems: "center",
|
|
721
|
+
gap: "var(--dtl-space-1)",
|
|
722
|
+
flex: "0 0 140px",
|
|
723
|
+
minWidth: 0,
|
|
724
|
+
color: "var(--dtl-fg-2)",
|
|
725
|
+
fontSize: "var(--dtl-font-xs)"
|
|
726
|
+
},
|
|
727
|
+
title: props.workspace.title
|
|
728
|
+
}, (0, react.createElement)("span", { style: {
|
|
729
|
+
display: "inline-flex",
|
|
730
|
+
alignItems: "center",
|
|
731
|
+
justifyContent: "center",
|
|
732
|
+
flex: "none",
|
|
733
|
+
width: "16px",
|
|
734
|
+
height: "16px",
|
|
735
|
+
borderRadius: "var(--dtl-radius-xs)",
|
|
736
|
+
background: "var(--dtl-business-soft)",
|
|
737
|
+
color: "var(--dtl-business)",
|
|
738
|
+
fontSize: "10px",
|
|
739
|
+
fontWeight: 600
|
|
740
|
+
} }, initial === "" ? "·" : initial), props.workspace.title);
|
|
741
|
+
}
|
|
580
742
|
function TaskRowView(props) {
|
|
581
743
|
const { t, row } = props;
|
|
582
744
|
const [open, setOpen] = (0, react.useState)(false);
|
|
@@ -615,13 +777,11 @@ body[data-ds-dark-theme]{
|
|
|
615
777
|
onClick: () => {
|
|
616
778
|
setOpen(!open);
|
|
617
779
|
}
|
|
618
|
-
}, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: dotOf(row) }), (0, react.createElement)("span", {
|
|
780
|
+
}, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: dotOf(row) }), props.showWorkspace === true ? (0, react.createElement)(WorkspaceTag, { workspace }) : null, (0, react.createElement)("span", {
|
|
781
|
+
className: "dtl-ellipsis",
|
|
619
782
|
style: {
|
|
620
783
|
flex: "1 1 auto",
|
|
621
784
|
minWidth: 0,
|
|
622
|
-
overflow: "hidden",
|
|
623
|
-
textOverflow: "ellipsis",
|
|
624
|
-
whiteSpace: "nowrap",
|
|
625
785
|
fontSize: "var(--dtl-font-md)"
|
|
626
786
|
},
|
|
627
787
|
title: titleOf(row)
|
|
@@ -632,10 +792,11 @@ body[data-ds-dark-theme]{
|
|
|
632
792
|
display: "flex",
|
|
633
793
|
alignItems: "center",
|
|
634
794
|
gap: "var(--dtl-space-2)",
|
|
795
|
+
flex: "none",
|
|
635
796
|
color: "var(--dtl-fg-3)",
|
|
636
797
|
fontSize: "var(--dtl-font-xs)",
|
|
637
798
|
whiteSpace: "nowrap"
|
|
638
|
-
} },
|
|
799
|
+
} }, (0, react.createElement)("span", { title: stamp.value ?? void 0 }, `${stamp.label} ${when}`), (0, react.createElement)("span", null, who === null || who === "" ? t("ownerUnclaimed") : `@${who}`)), (0, react.createElement)(open ? _deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular : _deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { size: 16 })), open ? (0, react.createElement)("div", { className: "dtl-row-body" }, (0, react.createElement)("div", { style: {
|
|
639
800
|
margin: "var(--dtl-space-2) 0",
|
|
640
801
|
fontSize: "var(--dtl-font-sm)",
|
|
641
802
|
color: "var(--dtl-fg-2)"
|
|
@@ -686,11 +847,12 @@ body[data-ds-dark-theme]{
|
|
|
686
847
|
//#endregion
|
|
687
848
|
//#region src/client/board-home.ts
|
|
688
849
|
/**
|
|
689
|
-
* 看板首页(`main` 槽整页内容):
|
|
690
|
-
* 定稿见 docs/design/features/board-home.md
|
|
691
|
-
*
|
|
850
|
+
* 看板首页(`main` 槽整页内容):6 统计卡 + 近 8 周趋势图 + 两区任务列表。
|
|
851
|
+
* 定稿见 docs/design/features/board-home.md §二(2026-10-17 专家组改版);
|
|
852
|
+
* 按钮矩阵归「任务按钮与会话交接」任务(写入路径 A/B 未拍板),本页**不画**任务动作按钮。
|
|
692
853
|
*
|
|
693
|
-
* 刷新纪律(2026-10-17 拍板):挂载首读 +
|
|
854
|
+
* 刷新纪律(2026-10-17 拍板):挂载首读 + 手动刷新按钮,**不轮询**;空态三分
|
|
855
|
+
* (未就绪不渲染 / 真空引导卡 / 筛选空小字)。
|
|
694
856
|
*/
|
|
695
857
|
/** 整页容器:占满中栏、自己滚动。 */
|
|
696
858
|
const pageStyle = {
|
|
@@ -713,12 +875,24 @@ body[data-ds-dark-theme]{
|
|
|
713
875
|
fontSize: "var(--dtl-font-xl)",
|
|
714
876
|
fontWeight: 600
|
|
715
877
|
};
|
|
716
|
-
|
|
878
|
+
/** 统计区:左 2/3 卡网格 + 右 1/3 趋势图(窄屏堆叠)。 */
|
|
879
|
+
const statsAreaStyle = {
|
|
717
880
|
display: "grid",
|
|
718
|
-
gridTemplateColumns: "
|
|
881
|
+
gridTemplateColumns: "minmax(0,2fr) minmax(0,1fr)",
|
|
719
882
|
gap: "var(--dtl-space-3)",
|
|
720
883
|
marginBottom: "var(--dtl-space-4)"
|
|
721
884
|
};
|
|
885
|
+
const cardsGridStyle = {
|
|
886
|
+
display: "grid",
|
|
887
|
+
gridTemplateColumns: "repeat(auto-fit,minmax(148px,1fr))",
|
|
888
|
+
gap: "var(--dtl-space-3)"
|
|
889
|
+
};
|
|
890
|
+
const chartCardStyle = {
|
|
891
|
+
display: "flex",
|
|
892
|
+
flexDirection: "column",
|
|
893
|
+
gap: "var(--dtl-space-2)",
|
|
894
|
+
padding: "var(--dtl-space-3) var(--dtl-space-4)"
|
|
895
|
+
};
|
|
722
896
|
const filterRowStyle = {
|
|
723
897
|
display: "flex",
|
|
724
898
|
alignItems: "center",
|
|
@@ -726,10 +900,43 @@ body[data-ds-dark-theme]{
|
|
|
726
900
|
flexWrap: "wrap",
|
|
727
901
|
marginBottom: "var(--dtl-space-3)"
|
|
728
902
|
};
|
|
903
|
+
const searchWrapStyle = {
|
|
904
|
+
flex: "1 1 220px",
|
|
905
|
+
minWidth: 0
|
|
906
|
+
};
|
|
729
907
|
const hintStyle = {
|
|
730
908
|
fontSize: "var(--dtl-font-sm)",
|
|
731
909
|
color: "var(--dtl-fg-2)"
|
|
732
910
|
};
|
|
911
|
+
const regionHeadStyle = {
|
|
912
|
+
display: "flex",
|
|
913
|
+
alignItems: "baseline",
|
|
914
|
+
gap: "var(--dtl-space-2)",
|
|
915
|
+
margin: "var(--dtl-space-4) 0 var(--dtl-space-2)",
|
|
916
|
+
fontSize: "var(--dtl-font-lg)",
|
|
917
|
+
fontWeight: 600
|
|
918
|
+
};
|
|
919
|
+
const groupHeadStyle = {
|
|
920
|
+
display: "flex",
|
|
921
|
+
alignItems: "center",
|
|
922
|
+
gap: "var(--dtl-space-2)",
|
|
923
|
+
width: "100%",
|
|
924
|
+
padding: "var(--dtl-space-2) var(--dtl-space-3)",
|
|
925
|
+
border: 0,
|
|
926
|
+
borderRadius: "var(--dtl-radius-md)",
|
|
927
|
+
background: "var(--dtl-surface-2)",
|
|
928
|
+
color: "inherit",
|
|
929
|
+
font: "inherit",
|
|
930
|
+
textAlign: "left",
|
|
931
|
+
cursor: "pointer"
|
|
932
|
+
};
|
|
933
|
+
const groupCountStyle = {
|
|
934
|
+
fontSize: "var(--dtl-font-xs)",
|
|
935
|
+
color: "var(--dtl-fg-3)",
|
|
936
|
+
fontWeight: 400
|
|
937
|
+
};
|
|
938
|
+
/** 分组默认折叠阈值(组内太多条时先收起,用户点开)。 */
|
|
939
|
+
const GROUP_COLLAPSE_OVER = 30;
|
|
733
940
|
function BoardHome(props) {
|
|
734
941
|
const { t, onBack } = props;
|
|
735
942
|
ensureUiBase();
|
|
@@ -739,27 +946,27 @@ body[data-ds-dark-theme]{
|
|
|
739
946
|
refresh();
|
|
740
947
|
}, []);
|
|
741
948
|
const board = state.board;
|
|
742
|
-
const
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
value: tag,
|
|
748
|
-
label: `#${tag}`
|
|
749
|
-
}))], [state.filters, t]);
|
|
750
|
-
/** 统计卡 → 筛选联动(点第二下不反选:档位语义是「切到这一档」,不是开关)。 */
|
|
751
|
-
const card = (label, value, bucket, range) => (0, react.createElement)(StatCard, {
|
|
949
|
+
const view = (0, react.useMemo)(() => selectView(state), [state]);
|
|
950
|
+
const stats = board?.stats;
|
|
951
|
+
const days = (value) => value === null || value === void 0 ? t("daysNone") : t("daysValue", { n: value });
|
|
952
|
+
/** 统计卡 → 筛选联动(卡即入口:点一下跳到对应视角)。 */
|
|
953
|
+
const card = (label, value, hint, patch) => (0, react.createElement)(StatCard, {
|
|
752
954
|
label: t(label),
|
|
753
955
|
value,
|
|
754
|
-
|
|
956
|
+
hint,
|
|
957
|
+
active: isActive(state.query, patch),
|
|
755
958
|
onClick: () => {
|
|
756
|
-
setQuery(
|
|
757
|
-
bucket,
|
|
758
|
-
range,
|
|
759
|
-
limit: 500
|
|
760
|
-
});
|
|
959
|
+
setQuery(patch);
|
|
761
960
|
}
|
|
762
961
|
});
|
|
962
|
+
const chartBuckets = (0, react.useMemo)(() => (board?.closed.weekly ?? []).map((bucket) => ({
|
|
963
|
+
label: bucket.week.slice(5),
|
|
964
|
+
value: bucket.count,
|
|
965
|
+
title: t("chartBucket", {
|
|
966
|
+
week: bucket.week,
|
|
967
|
+
n: bucket.count
|
|
968
|
+
})
|
|
969
|
+
})), [board, t]);
|
|
763
970
|
return (0, react.createElement)("div", { style: pageStyle }, (0, react.createElement)("div", { style: {
|
|
764
971
|
display: "flex",
|
|
765
972
|
justifyContent: "center"
|
|
@@ -773,7 +980,7 @@ body[data-ds-dark-theme]{
|
|
|
773
980
|
alignItems: "center",
|
|
774
981
|
gap: "var(--dtl-space-3)",
|
|
775
982
|
minWidth: 0
|
|
776
|
-
} }, (0, react.createElement)(
|
|
983
|
+
} }, (0, react.createElement)(Button, {
|
|
777
984
|
variant: "outline",
|
|
778
985
|
size: "md",
|
|
779
986
|
onClick: onBack
|
|
@@ -781,11 +988,11 @@ body[data-ds-dark-theme]{
|
|
|
781
988
|
display: "flex",
|
|
782
989
|
alignItems: "center",
|
|
783
990
|
gap: "var(--dtl-space-2)"
|
|
784
|
-
} }, (0, react.createElement)(
|
|
991
|
+
} }, (0, react.createElement)(Segmented, {
|
|
785
992
|
id: "dtl-tab",
|
|
786
993
|
value: tab,
|
|
787
994
|
label: t("panelTitle"),
|
|
788
|
-
|
|
995
|
+
items: [{
|
|
789
996
|
value: "tasks",
|
|
790
997
|
label: t("tabTasks")
|
|
791
998
|
}, {
|
|
@@ -793,95 +1000,126 @@ body[data-ds-dark-theme]{
|
|
|
793
1000
|
label: t("tabStats")
|
|
794
1001
|
}],
|
|
795
1002
|
onChange: setTab
|
|
796
|
-
}), (0, react.createElement)(
|
|
1003
|
+
}), (0, react.createElement)(Button, {
|
|
797
1004
|
variant: "outline",
|
|
798
1005
|
size: "md",
|
|
799
1006
|
icon: (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, { size: 16 }),
|
|
800
|
-
title: t("refresh"),
|
|
801
1007
|
disabled: state.refreshing,
|
|
1008
|
+
title: t("refresh"),
|
|
802
1009
|
onClick: () => {
|
|
803
1010
|
refresh();
|
|
804
1011
|
}
|
|
805
|
-
}, state.refreshing ? t("refreshing") : t("refresh")))), tab === "stats" ? (0, react.createElement)("div", { style: hintStyle }, t("statsPlaceholder")) : (0, react.createElement)("div", null,
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1012
|
+
}, state.refreshing ? t("refreshing") : t("refresh")))), tab === "stats" ? (0, react.createElement)("div", { style: hintStyle }, t("statsPlaceholder")) : (0, react.createElement)("div", null, stats === void 0 ? null : (0, react.createElement)("div", { style: statsAreaStyle }, (0, react.createElement)("div", { style: cardsGridStyle }, card("statMyActive", String(stats.myActive), t("statMyActiveHint", { n: stats.myBlocked }), {
|
|
1013
|
+
scope: "mine",
|
|
1014
|
+
status: "all",
|
|
1015
|
+
workspace: ""
|
|
1016
|
+
}), card("statOpen", String(stats.open), t("statOpenHint", { n: days(stats.oldestOpenDays) }), {
|
|
1017
|
+
scope: "all",
|
|
1018
|
+
status: "open",
|
|
1019
|
+
workspace: ""
|
|
1020
|
+
}), card("statDoing", String(stats.doing), t("statDoingHint", { n: stats.doing - stats.myActive + stats.myBlocked }), {
|
|
1021
|
+
scope: "all",
|
|
1022
|
+
status: "active",
|
|
1023
|
+
workspace: ""
|
|
1024
|
+
}), card("statBlocked", String(stats.blocked), t("statBlockedHint", { n: days(stats.longestBlockedDays) }), {
|
|
1025
|
+
scope: "all",
|
|
1026
|
+
status: "blocked",
|
|
1027
|
+
workspace: ""
|
|
1028
|
+
}), card("statWeekDone", String(board?.closed.weekDone ?? 0), t("statWeekDoneHint"), {
|
|
1029
|
+
scope: "all",
|
|
1030
|
+
status: "all",
|
|
1031
|
+
workspace: ""
|
|
1032
|
+
}), card("statStale", String(stats.stale), t("statStaleHint", { n: stats.staleDays }), {
|
|
1033
|
+
scope: "all",
|
|
1034
|
+
status: "stale",
|
|
1035
|
+
workspace: ""
|
|
1036
|
+
})), (0, react.createElement)("div", {
|
|
1037
|
+
className: "dtl-card",
|
|
1038
|
+
style: chartCardStyle
|
|
1039
|
+
}, (0, react.createElement)("div", { style: {
|
|
1040
|
+
fontSize: "var(--dtl-font-md)",
|
|
1041
|
+
fontWeight: 600
|
|
1042
|
+
} }, t("chartTitle")), chartBuckets.every((bucket) => bucket.value === 0) ? (0, react.createElement)("div", { style: {
|
|
1043
|
+
...hintStyle,
|
|
1044
|
+
padding: "var(--dtl-space-4) 0"
|
|
1045
|
+
} }, t("chartEmpty")) : (0, react.createElement)(BarChart, {
|
|
1046
|
+
buckets: chartBuckets,
|
|
1047
|
+
ariaLabel: t("chartTitle")
|
|
1048
|
+
}), (0, react.createElement)("div", { style: {
|
|
1049
|
+
fontSize: "var(--dtl-font-xs)",
|
|
1050
|
+
color: "var(--dtl-fg-3)"
|
|
1051
|
+
} }, t("chartCaption")))), (0, react.createElement)("div", { style: filterRowStyle }, (0, react.createElement)(Segmented, {
|
|
1052
|
+
id: "dtl-scope",
|
|
1053
|
+
value: state.query.scope,
|
|
1054
|
+
label: t("scopeLabel"),
|
|
1055
|
+
items: [{
|
|
1056
|
+
value: "mine",
|
|
1057
|
+
label: t("scopeMine")
|
|
1058
|
+
}, {
|
|
1059
|
+
value: "all",
|
|
1060
|
+
label: t("scopeAll")
|
|
1061
|
+
}],
|
|
1062
|
+
onChange: (next) => {
|
|
1063
|
+
setQuery({ scope: next });
|
|
1064
|
+
}
|
|
1065
|
+
}), (0, react.createElement)(Segmented, {
|
|
1066
|
+
id: "dtl-status",
|
|
1067
|
+
value: state.query.status,
|
|
1068
|
+
label: t("statusLabel"),
|
|
1069
|
+
items: [
|
|
810
1070
|
{
|
|
811
1071
|
value: "all",
|
|
812
|
-
label: t("
|
|
1072
|
+
label: t("statusAll")
|
|
813
1073
|
},
|
|
814
1074
|
{
|
|
815
|
-
value: "
|
|
816
|
-
label: t("
|
|
1075
|
+
value: "active",
|
|
1076
|
+
label: t("statusActive")
|
|
817
1077
|
},
|
|
818
1078
|
{
|
|
819
1079
|
value: "open",
|
|
820
|
-
label: t("
|
|
821
|
-
},
|
|
822
|
-
{
|
|
823
|
-
value: "closed",
|
|
824
|
-
label: t("bucketClosed")
|
|
825
|
-
}
|
|
826
|
-
],
|
|
827
|
-
onChange: (next) => {
|
|
828
|
-
setQuery({
|
|
829
|
-
bucket: next,
|
|
830
|
-
limit: 500
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
}), (0, react.createElement)(Select, {
|
|
834
|
-
value: state.query.range,
|
|
835
|
-
ariaLabel: t("rangeLabel"),
|
|
836
|
-
options: [
|
|
837
|
-
{
|
|
838
|
-
value: "all",
|
|
839
|
-
label: t("rangeAll")
|
|
1080
|
+
label: t("statusOpen")
|
|
840
1081
|
},
|
|
841
1082
|
{
|
|
842
|
-
value: "
|
|
843
|
-
label: t("
|
|
1083
|
+
value: "blocked",
|
|
1084
|
+
label: t("statusBlocked")
|
|
844
1085
|
},
|
|
845
1086
|
{
|
|
846
|
-
value: "
|
|
847
|
-
label: t("
|
|
848
|
-
},
|
|
849
|
-
{
|
|
850
|
-
value: "year",
|
|
851
|
-
label: t("rangeYear")
|
|
1087
|
+
value: "stale",
|
|
1088
|
+
label: t("statusStale")
|
|
852
1089
|
}
|
|
853
1090
|
],
|
|
854
1091
|
onChange: (next) => {
|
|
855
|
-
setQuery({
|
|
856
|
-
range: next,
|
|
857
|
-
limit: 500
|
|
858
|
-
});
|
|
1092
|
+
setQuery({ status: next });
|
|
859
1093
|
}
|
|
860
1094
|
}), (0, react.createElement)(Select, {
|
|
861
|
-
value: state.query.
|
|
862
|
-
ariaLabel: t("
|
|
863
|
-
|
|
1095
|
+
value: state.query.workspace,
|
|
1096
|
+
ariaLabel: t("workspaceLabel"),
|
|
1097
|
+
width: 180,
|
|
1098
|
+
options: [{
|
|
1099
|
+
value: "",
|
|
1100
|
+
label: t("workspaceAll")
|
|
1101
|
+
}, ...(board?.workspaces ?? []).map((workspace) => ({
|
|
1102
|
+
value: workspace.id,
|
|
1103
|
+
label: workspace.title
|
|
1104
|
+
}))],
|
|
864
1105
|
onChange: (next) => {
|
|
865
|
-
setQuery({
|
|
866
|
-
tag: next,
|
|
867
|
-
limit: 500
|
|
868
|
-
});
|
|
1106
|
+
setQuery({ workspace: next });
|
|
869
1107
|
}
|
|
870
|
-
}), (0, react.createElement)(SearchInput, {
|
|
1108
|
+
}), (0, react.createElement)("div", { style: searchWrapStyle }, (0, react.createElement)(SearchInput, {
|
|
871
1109
|
value: state.query.keyword,
|
|
872
1110
|
placeholder: t("searchPlaceholder"),
|
|
873
1111
|
ariaLabel: t("searchPlaceholder"),
|
|
874
1112
|
onChange: (next) => {
|
|
875
1113
|
setQuery({ keyword: next });
|
|
876
1114
|
}
|
|
877
|
-
})), state.status === "loading" && board === null ? null : state.status === "error" && board === null ? (0, react.createElement)("div", { style: {
|
|
1115
|
+
}))), state.status === "loading" && board === null ? null : state.status === "error" && board === null ? (0, react.createElement)("div", { style: {
|
|
878
1116
|
display: "flex",
|
|
879
1117
|
alignItems: "center",
|
|
880
1118
|
gap: "var(--dtl-space-2)"
|
|
881
1119
|
} }, (0, react.createElement)("span", { style: {
|
|
882
1120
|
color: "var(--dtl-danger)",
|
|
883
1121
|
fontSize: "var(--dtl-font-md)"
|
|
884
|
-
} }, `${t("loadFailed")}:${state.error ?? ""}`), (0, react.createElement)(
|
|
1122
|
+
} }, `${t("loadFailed")}:${state.error ?? ""}`), (0, react.createElement)(Button, {
|
|
885
1123
|
variant: "outline",
|
|
886
1124
|
size: "sm",
|
|
887
1125
|
onClick: () => {
|
|
@@ -891,182 +1129,216 @@ body[data-ds-dark-theme]{
|
|
|
891
1129
|
tone: "guide",
|
|
892
1130
|
title: t("emptyNoWorkspace"),
|
|
893
1131
|
hint: t("emptyNoWorkspaceHint")
|
|
894
|
-
}) :
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
} }, (0, react.createElement)("span", { style: {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
size: "sm",
|
|
904
|
-
onClick: () => {
|
|
905
|
-
setQuery({});
|
|
906
|
-
}
|
|
907
|
-
}, t("retry"))) : rows.length === 0 ? (0, react.createElement)(EmptyState, {
|
|
908
|
-
tone: isPristine(state) ? "guide" : "filtered",
|
|
909
|
-
title: isPristine(state) ? t("emptyNoTask") : t("emptyFiltered"),
|
|
910
|
-
hint: isPristine(state) ? t("emptyNoTaskHint") : void 0
|
|
911
|
-
}) : (0, react.createElement)("div", null, rows.map((row) => (0, react.createElement)(TaskRowView, {
|
|
1132
|
+
}) : view.total === 0 ? (0, react.createElement)(EmptyState, {
|
|
1133
|
+
tone: isPristine(state.query) ? "guide" : "filtered",
|
|
1134
|
+
title: isPristine(state.query) ? t("emptyNoTask") : t("emptyFiltered"),
|
|
1135
|
+
hint: isPristine(state.query) ? t("emptyNoTaskHint") : void 0
|
|
1136
|
+
}) : (0, react.createElement)("div", null, view.mine.length === 0 ? null : (0, react.createElement)("div", null, (0, react.createElement)("div", { style: regionHeadStyle }, (0, react.createElement)("span", null, `${t("regionMine")} ${view.mine.length}`), (0, react.createElement)("span", { style: {
|
|
1137
|
+
fontSize: "var(--dtl-font-xs)",
|
|
1138
|
+
color: "var(--dtl-fg-3)",
|
|
1139
|
+
fontWeight: 400
|
|
1140
|
+
} }, t("regionMineHint"))), view.mine.map((row) => (0, react.createElement)(TaskRowView, {
|
|
912
1141
|
key: row.key,
|
|
913
1142
|
t,
|
|
914
|
-
row
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1143
|
+
row,
|
|
1144
|
+
showWorkspace: true
|
|
1145
|
+
}))), view.groups.length === 0 ? null : (0, react.createElement)("div", null, (0, react.createElement)("div", { style: regionHeadStyle }, (0, react.createElement)("span", null, t("regionWorkspaces")), (0, react.createElement)("span", { style: {
|
|
1146
|
+
fontSize: "var(--dtl-font-xs)",
|
|
918
1147
|
color: "var(--dtl-fg-3)",
|
|
919
|
-
|
|
920
|
-
} },
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1148
|
+
fontWeight: 400
|
|
1149
|
+
} }, t("showingCount", { n: view.total - view.mine.length }))), view.groups.map((group) => (0, react.createElement)(WorkspaceGroupView, {
|
|
1150
|
+
key: group.workspaceId,
|
|
1151
|
+
t,
|
|
1152
|
+
group
|
|
1153
|
+
}))))))));
|
|
1154
|
+
}
|
|
1155
|
+
/** 一个工作区分组:标题行(可点折叠)+ 行列表。 */
|
|
1156
|
+
function WorkspaceGroupView(props) {
|
|
1157
|
+
const { t, group } = props;
|
|
1158
|
+
const [open, setOpen] = (0, react.useState)(group.rows.length <= GROUP_COLLAPSE_OVER);
|
|
1159
|
+
return (0, react.createElement)("div", { style: { marginBottom: "var(--dtl-space-3)" } }, (0, react.createElement)("button", {
|
|
1160
|
+
type: "button",
|
|
1161
|
+
className: "dtl-row-head",
|
|
1162
|
+
style: groupHeadStyle,
|
|
1163
|
+
"aria-expanded": open,
|
|
1164
|
+
title: open ? t("collapseAria") : t("expandAria"),
|
|
924
1165
|
onClick: () => {
|
|
925
|
-
|
|
1166
|
+
setOpen(!open);
|
|
1167
|
+
}
|
|
1168
|
+
}, (0, react.createElement)("span", {
|
|
1169
|
+
className: "dtl-ellipsis",
|
|
1170
|
+
style: {
|
|
1171
|
+
flex: "1 1 auto",
|
|
1172
|
+
fontSize: "var(--dtl-font-md)",
|
|
1173
|
+
fontWeight: 600
|
|
926
1174
|
}
|
|
927
|
-
},
|
|
1175
|
+
}, group.title), (0, react.createElement)("span", { style: groupCountStyle }, t("groupHint", {
|
|
1176
|
+
doing: group.doingCount,
|
|
1177
|
+
open: group.openCount
|
|
1178
|
+
}))), open ? (0, react.createElement)("div", { style: { marginTop: "var(--dtl-space-2)" } }, group.rows.map((row) => (0, react.createElement)(TaskRowView, {
|
|
1179
|
+
key: row.key,
|
|
1180
|
+
t,
|
|
1181
|
+
row
|
|
1182
|
+
}))) : null);
|
|
1183
|
+
}
|
|
1184
|
+
/** 卡片高亮:当前 query 是否正好落在该卡对应的视角上。 */
|
|
1185
|
+
function isActive(query, patch) {
|
|
1186
|
+
return Object.entries(patch).every(([key, value]) => query[key] === value);
|
|
928
1187
|
}
|
|
929
1188
|
/** 是否「什么都没筛」:真空才给引导卡,一有筛选就只能是「筛选空」。 */
|
|
930
|
-
function isPristine(
|
|
931
|
-
|
|
932
|
-
return query.bucket === "all" && query.range === "all" && query.tag === "" && query.keyword.trim() === "";
|
|
1189
|
+
function isPristine(query) {
|
|
1190
|
+
return query.scope === "all" && query.status === "all" && query.workspace === "" && query.keyword.trim() === "";
|
|
933
1191
|
}
|
|
934
1192
|
//#endregion
|
|
935
1193
|
//#region src/client/locales.ts
|
|
936
|
-
/**
|
|
937
|
-
function interpolateTranslate(base) {
|
|
938
|
-
return (key, params) => {
|
|
939
|
-
const raw = base(key);
|
|
940
|
-
if (params === void 0) return raw;
|
|
941
|
-
return raw.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match);
|
|
942
|
-
};
|
|
943
|
-
}
|
|
1194
|
+
/** 中文(默认档)。 */
|
|
944
1195
|
const zh = {
|
|
945
|
-
panelTitle: "任务台账",
|
|
946
1196
|
backToConversation: "返回会话",
|
|
1197
|
+
panelTitle: "任务台账",
|
|
947
1198
|
tabTasks: "任务",
|
|
948
1199
|
tabStats: "统计",
|
|
949
|
-
statsPlaceholder: "统计报表尚未开建(本版只有任务首页)",
|
|
950
|
-
statMyActive: "我的待办",
|
|
951
|
-
statOpen: "待认领",
|
|
952
|
-
statWeekDone: "本周已完成",
|
|
953
|
-
statYearDone: "近一年完成",
|
|
954
|
-
countCapped: "1万+",
|
|
955
|
-
bucketLabel: "状态档",
|
|
956
|
-
bucketAll: "全部",
|
|
957
|
-
bucketMine: "我的待办",
|
|
958
|
-
bucketOpen: "待认领",
|
|
959
|
-
bucketClosed: "已结案",
|
|
960
|
-
rangeLabel: "时间",
|
|
961
|
-
rangeAll: "全部时间",
|
|
962
|
-
rangeWeek: "本周",
|
|
963
|
-
rangeMonth: "本月",
|
|
964
|
-
rangeYear: "近一年",
|
|
965
|
-
tagAll: "全部标签",
|
|
966
|
-
searchPlaceholder: "搜索标题 / 负责人 / 任务号",
|
|
967
1200
|
refresh: "刷新",
|
|
968
|
-
refreshing: "
|
|
969
|
-
loading: "
|
|
1201
|
+
refreshing: "刷新中",
|
|
1202
|
+
loading: "读取中…",
|
|
970
1203
|
retry: "重试",
|
|
971
1204
|
loadFailed: "读取失败",
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1205
|
+
statsPlaceholder: "统计报表尚未开建(另立任务):这里以后放分类分布、按人完成数、时间线。",
|
|
1206
|
+
statMyActive: "我的待办",
|
|
1207
|
+
statMyActiveHint: "其中阻塞 {n}",
|
|
1208
|
+
statOpen: "待认领",
|
|
1209
|
+
statOpenHint: "最老一条躺了 {n}",
|
|
1210
|
+
statDoing: "进行中",
|
|
1211
|
+
statDoingHint: "别人在做 {n}",
|
|
1212
|
+
statBlocked: "阻塞中",
|
|
1213
|
+
statBlockedHint: "卡住最久 {n}",
|
|
1214
|
+
statWeekDone: "本周完成",
|
|
1215
|
+
statWeekDoneHint: "按结案时间",
|
|
1216
|
+
statStale: "停滞任务",
|
|
1217
|
+
statStaleHint: "超过 {n} 天没动",
|
|
1218
|
+
daysNone: "—",
|
|
1219
|
+
daysValue: "{n} 天",
|
|
1220
|
+
chartTitle: "近 8 周完成",
|
|
1221
|
+
chartCaption: "按结案时间、周一起桶;悬停看该周条数",
|
|
1222
|
+
chartBucket: "{week} 那一周:完成 {n} 条",
|
|
1223
|
+
chartEmpty: "近 8 周没有已结案记录",
|
|
1224
|
+
scopeLabel: "范围",
|
|
1225
|
+
scopeMine: "我的",
|
|
1226
|
+
scopeAll: "全部工作区",
|
|
1227
|
+
statusLabel: "状态",
|
|
1228
|
+
statusAll: "全部",
|
|
1229
|
+
statusActive: "进行中",
|
|
1230
|
+
statusOpen: "待认领",
|
|
1231
|
+
statusBlocked: "阻塞",
|
|
1232
|
+
statusStale: "停滞",
|
|
1233
|
+
workspaceLabel: "工作区",
|
|
1234
|
+
workspaceAll: "全部工作区",
|
|
1235
|
+
searchPlaceholder: "搜索标题 / 任务号 / 人 / 分类 / 标签",
|
|
1236
|
+
regionMine: "我的待办",
|
|
1237
|
+
regionMineHint: "跨工作区,按创建时间倒序",
|
|
1238
|
+
regionWorkspaces: "按工作区",
|
|
1239
|
+
groupHint: "进行中 {doing} | 待认领 {open}",
|
|
977
1240
|
showingCount: "显示 {n} 条",
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
collapseAria: "收起详情",
|
|
982
|
-
fieldOwner: "负责人",
|
|
983
|
-
fieldCreator: "创建人",
|
|
1241
|
+
expandAria: "展开过程文档",
|
|
1242
|
+
collapseAria: "收起",
|
|
1243
|
+
ownerUnclaimed: "待认领",
|
|
984
1244
|
fieldCreated: "创建",
|
|
985
1245
|
fieldStarted: "开始",
|
|
986
1246
|
fieldFinished: "结案",
|
|
987
|
-
fieldWorkspace: "工作区",
|
|
988
|
-
fieldCategory: "分类",
|
|
989
|
-
ownerUnclaimed: "未认领",
|
|
990
1247
|
timeUnknown: "时间未知",
|
|
991
|
-
statusOpen: "待认领",
|
|
992
|
-
statusDoing: "进行中",
|
|
993
|
-
statusBlocked: "受阻",
|
|
994
|
-
statusClosed: "已结案",
|
|
995
1248
|
worklogTitle: "过程文档",
|
|
996
|
-
worklogMissing: "过程文档缺失(文件已不在盘上)",
|
|
997
1249
|
worklogPath: "路径",
|
|
998
|
-
|
|
999
|
-
openBoard: "打开任务台账",
|
|
1250
|
+
worklogMissing: "(缺失)",
|
|
1000
1251
|
mdCopy: "复制",
|
|
1001
1252
|
mdCopied: "已复制",
|
|
1002
|
-
mdFootnotes: "脚注"
|
|
1253
|
+
mdFootnotes: "脚注",
|
|
1254
|
+
emptyNoWorkspace: "还没有台账工作区",
|
|
1255
|
+
emptyNoWorkspaceHint: "在某个项目里跑 task-ledger skill 的 init 建档(生成 ledger/progress.json),这里就会出现它。",
|
|
1256
|
+
emptyNoTask: "一条任务都没有",
|
|
1257
|
+
emptyNoTaskHint: "用 task-ledger skill 的 new 开一个任务,或让别人开在这里等你认领。",
|
|
1258
|
+
emptyFiltered: "没有符合当前筛选的任务",
|
|
1259
|
+
badgeTitle: "我名下待办",
|
|
1260
|
+
openBoard: "打开任务台账"
|
|
1003
1261
|
};
|
|
1262
|
+
/** English(与 zh 键完全对齐)。 */
|
|
1004
1263
|
const en = {
|
|
1264
|
+
backToConversation: "Back to chat",
|
|
1005
1265
|
panelTitle: "Task ledger",
|
|
1006
|
-
backToConversation: "Back to conversation",
|
|
1007
1266
|
tabTasks: "Tasks",
|
|
1008
1267
|
tabStats: "Stats",
|
|
1009
|
-
statsPlaceholder: "The stats report is not built yet (this release ships the task home only)",
|
|
1010
|
-
statMyActive: "My active",
|
|
1011
|
-
statOpen: "Unclaimed",
|
|
1012
|
-
statWeekDone: "Closed this week",
|
|
1013
|
-
statYearDone: "Closed in the last year",
|
|
1014
|
-
countCapped: "10k+",
|
|
1015
|
-
bucketLabel: "Status",
|
|
1016
|
-
bucketAll: "All",
|
|
1017
|
-
bucketMine: "My active",
|
|
1018
|
-
bucketOpen: "Unclaimed",
|
|
1019
|
-
bucketClosed: "Closed",
|
|
1020
|
-
rangeLabel: "Time",
|
|
1021
|
-
rangeAll: "Any time",
|
|
1022
|
-
rangeWeek: "This week",
|
|
1023
|
-
rangeMonth: "This month",
|
|
1024
|
-
rangeYear: "Last year",
|
|
1025
|
-
tagAll: "All tags",
|
|
1026
|
-
searchPlaceholder: "Search title / owner / task id",
|
|
1027
1268
|
refresh: "Refresh",
|
|
1028
|
-
refreshing: "
|
|
1269
|
+
refreshing: "Refreshing",
|
|
1029
1270
|
loading: "Loading…",
|
|
1030
1271
|
retry: "Retry",
|
|
1031
|
-
loadFailed: "
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1272
|
+
loadFailed: "Load failed",
|
|
1273
|
+
statsPlaceholder: "Stats reports are not built yet (separate task): category breakdown, completions by person, timeline.",
|
|
1274
|
+
statMyActive: "My todos",
|
|
1275
|
+
statMyActiveHint: "{n} blocked",
|
|
1276
|
+
statOpen: "Unclaimed",
|
|
1277
|
+
statOpenHint: "oldest waiting {n}",
|
|
1278
|
+
statDoing: "In progress",
|
|
1279
|
+
statDoingHint: "{n} by others",
|
|
1280
|
+
statBlocked: "Blocked",
|
|
1281
|
+
statBlockedHint: "stuck for {n}",
|
|
1282
|
+
statWeekDone: "Done this week",
|
|
1283
|
+
statWeekDoneHint: "by close time",
|
|
1284
|
+
statStale: "Stale tasks",
|
|
1285
|
+
statStaleHint: "no movement for {n}+ days",
|
|
1286
|
+
daysNone: "—",
|
|
1287
|
+
daysValue: "{n}d",
|
|
1288
|
+
chartTitle: "Last 8 weeks",
|
|
1289
|
+
chartCaption: "by close time, weeks start Monday; hover a bar for its count",
|
|
1290
|
+
chartBucket: "week of {week}: {n} done",
|
|
1291
|
+
chartEmpty: "Nothing closed in the last 8 weeks",
|
|
1292
|
+
scopeLabel: "Scope",
|
|
1293
|
+
scopeMine: "Mine",
|
|
1294
|
+
scopeAll: "All workspaces",
|
|
1295
|
+
statusLabel: "Status",
|
|
1296
|
+
statusAll: "All",
|
|
1297
|
+
statusActive: "Active",
|
|
1051
1298
|
statusOpen: "Unclaimed",
|
|
1052
|
-
statusDoing: "In progress",
|
|
1053
1299
|
statusBlocked: "Blocked",
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1300
|
+
statusStale: "Stale",
|
|
1301
|
+
workspaceLabel: "Workspace",
|
|
1302
|
+
workspaceAll: "All workspaces",
|
|
1303
|
+
searchPlaceholder: "Search title / id / person / category / tag",
|
|
1304
|
+
regionMine: "My todos",
|
|
1305
|
+
regionMineHint: "across workspaces, newest first",
|
|
1306
|
+
regionWorkspaces: "By workspace",
|
|
1307
|
+
groupHint: "{doing} active | {open} unclaimed",
|
|
1308
|
+
showingCount: "{n} shown",
|
|
1309
|
+
expandAria: "Expand worklog",
|
|
1310
|
+
collapseAria: "Collapse",
|
|
1311
|
+
ownerUnclaimed: "unclaimed",
|
|
1312
|
+
fieldCreated: "created",
|
|
1313
|
+
fieldStarted: "started",
|
|
1314
|
+
fieldFinished: "closed",
|
|
1315
|
+
timeUnknown: "unknown time",
|
|
1316
|
+
worklogTitle: "Worklog",
|
|
1317
|
+
worklogPath: "path",
|
|
1318
|
+
worklogMissing: "(missing)",
|
|
1060
1319
|
mdCopy: "Copy",
|
|
1061
1320
|
mdCopied: "Copied",
|
|
1062
|
-
mdFootnotes: "Footnotes"
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1321
|
+
mdFootnotes: "Footnotes",
|
|
1322
|
+
emptyNoWorkspace: "No ledger workspace yet",
|
|
1323
|
+
emptyNoWorkspaceHint: "Run the task-ledger skill init in a project (creates ledger/progress.json) and it shows up here.",
|
|
1324
|
+
emptyNoTask: "No tasks yet",
|
|
1325
|
+
emptyNoTaskHint: "Create one with the task-ledger skill new, or leave one here for others to claim.",
|
|
1326
|
+
emptyFiltered: "No tasks match the current filters",
|
|
1327
|
+
badgeTitle: "my todos",
|
|
1328
|
+
openBoard: "Open task ledger"
|
|
1069
1329
|
};
|
|
1330
|
+
/** 把宿主给的无参 t 包成带占位符替换的 t(模板一律 `{name}`,不依赖宿主是否支持 params)。 */
|
|
1331
|
+
function interpolateTranslate(base) {
|
|
1332
|
+
return (key, params) => {
|
|
1333
|
+
const raw = base(key);
|
|
1334
|
+
if (params === void 0) return raw;
|
|
1335
|
+
return raw.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match);
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
/** 键名兜底:宿主没给 t 席位时用(键名绝不上界面)。 */
|
|
1339
|
+
function fallbackTranslate(key, params) {
|
|
1340
|
+
return interpolateTranslate((name) => zh[name] ?? name)(key, params);
|
|
1341
|
+
}
|
|
1070
1342
|
//#endregion
|
|
1071
1343
|
//#region src/client/overlay-entry.ts
|
|
1072
1344
|
/**
|