项目管理界面:修列宽塌陷 + 工作区维度纠偏 + 卡片信息重排
一、修「卡片挤到一起去了」(用户 2026-10-06 报) 真因=**嵌套 grid**:renderAll 写 `<div class="cols">` 而容器 `#cols` 本身就带 `class="cols"` ⇒ 内层作为外层一个 grid item 先被压到 `min-width:auto`(399px), 再在 399px 里自己切 4 列 ⇒ 每列 89px、卡片 67px,整块挤在左边一条。 实测:col 89px / card 67px / 页高 3192px ⇒ 修后 card 377px / 页高 1415px。 处置:renderAll 只铺列,⛔ 不再包一层。 二、修「卡片上下之间的距离太远」 grid 默认 `align-items:stretch` ⇒ 12 张卡都在「已完成」、另三列空着却四列等高, 整页虚胖。改 `align-items:start` ⇒ 每列各自撑高。卡片 gap 9→6、padding 10→8、 `.col` 的 `min-height:160px`→0。 三、**工作区 / 目标 两个维度纠偏**(用户纠正:「下拉框是选工作区的 跟目标没关系」) 改前:下拉写「全部(N 个目标)」而 N 取的是 `projects.length`=工作区条数 ⇒ 混了维度。 改后:① 标签「项目」→「工作区」;② 选项文字=**目录名(+目标名)**; ③ 计数=「N 个工作区」;④ 概要条分开报「工作区 3 个 / 其中已登记目标 2 个」; ⑤ 装了机制的工作区**一律列出**(含未登记目标的,用户:「加载机制就可以显示」)。 四、修「显示的是目录名,应该是真正名称」 `name = short or title` ⇒ 登记过 short 的区一律显示短名(`vibe-product`)。 改 `title or short`;都没有 ⇒ 「未登记目标 · <dir>」(⛔ 不再印「(未命名 · …)」—— 用户原话「未命名 看不懂个是什么」)。 ⚠️ 核实过:这不是收工操作的副作用(goal.json 的 title/short 收工前后逐字一致)。 五、卡片信息重排(用户:「工作区名称放到卡片下面」) 末行单列工作区名(等宽字体)+ 本工作区角标;工作区名从 badge 堆里挪出来。 本工作区卡片换色突出:判定来自服务端 `self`(启动器注入 `DSH_PM_BOARD_SELF_WS`=启动器上两级),⛔ 前端不硬编码目录名。 六、修时间戳显示成 `1791957640.7257342` 台账 `t_start`/`t_end` 有两种形态(ISO 串 / **纯数字 Unix 秒**),原 fmtWhen 只认前者, 不匹配就**原样返回** ⇒ 印出一串乱数字。补:纯数字(1e9~4e9)走 Date 转本地时间。 另:A 方案(收工 vibe-product)已执行并取证 —— `--set-life 已完成` 自动调 supervise_stop,pid 432 真退出(tasklist 查无)、日志 35s 增量 0 字节(收工前每 32s +1357B)。
This commit is contained in:
1 parent
ee9b4f673b
commit
5d0745c470
3 files changed
+127
-34
No files matched your search
@@ -35,24 +35,43 @@
|
||||
.chip b{color:var(--fg);font-weight:600}
|
||||
|
||||
/* ── 列 ── */
|
||||
main{padding:16px 18px 60px}
|
||||
.cols{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}
|
||||
main{padding:16px 18px 40px}
|
||||
/* 🔴🔴 `align-items:start` —— 用户 2026-10-06:「卡片上下之间的距离太远了」。
|
||||
真因:grid **默认 `stretch`** ⇒ 12 张卡片全在「已完成」、另三列是空的,
|
||||
但四列**被拉成等高** ⇒ 空列白占 1100px、整体高度虚胖。
|
||||
✅ 改 `start` ⇒ 每列**各自撑自己的高度**,空列就是一个小盒子。*/
|
||||
.cols{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;
|
||||
align-items:start}
|
||||
@media(max-width:1100px){.cols{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
@media(max-width:640px){.cols{grid-template-columns:1fr}}
|
||||
.col{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);
|
||||
display:flex;flex-direction:column;min-height:160px}
|
||||
display:flex;flex-direction:column;min-height:0}
|
||||
.colhead{display:flex;align-items:center;gap:8px;padding:11px 13px;
|
||||
border-bottom:1px solid var(--line)}
|
||||
.dot{width:8px;height:8px;border-radius:50%;flex:none}
|
||||
.coltitle{font-weight:600;font-size:13px}
|
||||
.count{margin-left:auto;background:var(--panel2);border:1px solid var(--line);
|
||||
border-radius:999px;padding:1px 9px;font-size:12px;color:var(--dim)}
|
||||
.cards{padding:10px;display:flex;flex-direction:column;gap:9px;flex:1}
|
||||
/* 🔴 卡片间距压紧(原 `gap:9px` + `padding:10px` 在长列里显得很空)*/
|
||||
.cards{padding:8px;display:flex;flex-direction:column;gap:6px}
|
||||
|
||||
/* ── 统一视图:卡片上的「目标」标签(🔴 归属靠标签,⛔ 不靠分区)── */
|
||||
.badge.goal{border-color:var(--accent);color:var(--accent)}
|
||||
/* ── 统一视图:卡片**末行**显示工作区(🔴 归属靠这一行,⛔ 不靠分区)── */
|
||||
/* 用户 2026-10-06:「卡片 标签要显示 工作区名称,不要显示 本机协作这种」
|
||||
+「工作区名称要不要 放到卡片下面」⇒ 末行单列,⛔ 不混进 badge 堆里 */
|
||||
.wsrow{display:flex;align-items:center;gap:6px;margin-top:7px;padding-top:6px;
|
||||
border-top:1px solid var(--line)}
|
||||
.wsname{font-size:11.5px;color:var(--dim);font-family:ui-monospace,Consolas,monospace;
|
||||
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.wsself{margin-left:auto;flex:none;font-size:10.5px;padding:0 5px;border-radius:4px;
|
||||
background:rgba(76,110,245,.2);border:1px solid rgba(76,110,245,.5);
|
||||
color:#b9c8ff}
|
||||
.badge.when{font-family:ui-monospace,Consolas,monospace}
|
||||
/* 🔴 本工作区的卡片换色突出(用户 2026-10-06:「本工作区的卡片是不得 换个颜色」)*/
|
||||
.card.is-self{background:rgba(76,110,245,.13);border-color:rgba(76,110,245,.55)}
|
||||
.card.is-self:hover{border-color:var(--accent)}
|
||||
.card.is-self .t{color:#c9d4ff}
|
||||
.card{background:var(--panel2);border:1px solid var(--line);border-left-width:3px;
|
||||
border-radius:8px;padding:10px 11px;cursor:pointer;
|
||||
border-radius:8px;padding:9px 11px;cursor:pointer;
|
||||
transition:transform .08s ease,border-color .12s ease}
|
||||
.card:hover{transform:translateY(-1px);border-color:var(--accent)}
|
||||
.card .t{font-weight:500;word-break:break-word}
|
||||
@@ -160,9 +179,9 @@
|
||||
<header>
|
||||
<div class="hrow">
|
||||
<div class="brand">项目管理<small id="ws">—</small></div>
|
||||
<label for="psel" class="stamp">项目</label>
|
||||
<select id="psel" aria-label="选择项目"></select>
|
||||
<button id="btndel" type="button" class="danger" title="删除当前项目(可选粒度)">删除</button>
|
||||
<label for="psel" class="stamp">工作区</label>
|
||||
<select id="psel" aria-label="选择工作区"></select>
|
||||
<button id="btndel" type="button" class="danger" title="删除当前工作区(可选粒度)">删除</button>
|
||||
<div class="spacer"></div>
|
||||
<span class="stamp" id="stamp">加载中…</span>
|
||||
<span class="info fancy" id="infobtn" tabindex="0" role="button" aria-label="状态说明"
|
||||
@@ -223,8 +242,23 @@ function esc(s){ return String(s==null?"":s).replace(/[&<>"']/g, function(c){
|
||||
|
||||
function fmtWhen(s){
|
||||
if(!s) return "—";
|
||||
var m = String(s).match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})/);
|
||||
if(!m) return String(s);
|
||||
var str = String(s).trim();
|
||||
// 🔴🔴 2026-10-06 实测补:台账里的 `t_start`/`t_end` 有两种形态 ——
|
||||
// ① `"2026-10-06T09:26:53"`(ISO 字符串)② **纯数字 Unix 秒**(`1791957640.7257342`)。
|
||||
// 原实现只认 ①,遇到 ② 正则不匹配 ⇒ **原样返回** ⇒ 卡片上印出
|
||||
// `1791957640.7257342` 一整串(用户看就是一堆乱数字)。
|
||||
// ✅ 先判纯数字(10 位上下)⇒ 转本地时间;⛔ 不猜、⛔ 不硬编时区。
|
||||
var n = Number(str);
|
||||
if(isFinite(n) && n > 1e9 && n < 4e9){ // 2001-09 ~ 2096-11 之间的秒级时间戳
|
||||
var d = new Date(n * 1000);
|
||||
if(!isNaN(d.getTime())){
|
||||
var p = function(x){ return (x<10?"0":"")+x; };
|
||||
return d.getFullYear()+"-"+p(d.getMonth()+1)+"-"+p(d.getDate())+" "+
|
||||
p(d.getHours())+":"+p(d.getMinutes());
|
||||
}
|
||||
}
|
||||
var m = str.match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})/);
|
||||
if(!m) return str;
|
||||
return m[1]+"-"+m[2]+"-"+m[3]+" "+m[4]+":"+m[5];
|
||||
}
|
||||
|
||||
@@ -320,10 +354,18 @@ function load(manual){
|
||||
sel.innerHTML = '<option value="-1">(本机暂无装了机制的工作区)</option>';
|
||||
STATE.cur = -1;
|
||||
} else {
|
||||
// 🔴 默认「全部」:一屏看完本机所有目标(⛔ 不再默认只显示一个)
|
||||
sel.innerHTML = '<option value="-1">全部('+d.projects.length+' 个目标)</option>'
|
||||
// 🔴🔴 **本下拉=工作区选择器**(用户 2026-10-06 明确纠正:
|
||||
// 「下拉框是选工作区的 跟目标没关系」)。
|
||||
// ⇒ ① 选项文字用 **工作区名**(`dir` 目录名),⛔ 不是目标名;
|
||||
// ② 计数说 **N 个工作区**,⛔ 不说"N 个目标"(那是另一个维度);
|
||||
// ③ 装了机制的工作区**一律列出**(含还没登记目标的)—— 用户明令
|
||||
// 「加载机制就可以显示」(⛔ 不许因为没目标就藏起来)。
|
||||
// ⚠️ 我上一版写成「全部(N 个目标)」,是把两个维度混了 —— 已纠正。
|
||||
sel.innerHTML = '<option value="-1">全部('+d.projects.length+' 个工作区)</option>'
|
||||
+ d.projects.map(function(p,i){
|
||||
return '<option value="'+i+'">'+esc(p.name)+'</option>'; }).join("");
|
||||
// 有目标 ⇒ 目录名 + 目标名;没目标 ⇒ 只目录名(⛔ 不印「未命名」那种看不懂的话)
|
||||
var lab = p.dir + (p.has_goal && p.name ? '('+p.name+')' : '');
|
||||
return '<option value="'+i+'">'+esc(lab)+'</option>'; }).join("");
|
||||
if(STATE.cur >= d.projects.length) STATE.cur = -1;
|
||||
sel.value = String(STATE.cur);
|
||||
}
|
||||
@@ -343,17 +385,26 @@ function _stateDot(key){
|
||||
|
||||
function _cardHtml(t){
|
||||
var dot = _stateDot(t.state), bits = [];
|
||||
// 🔴 统一视图里,卡片自带「目标 · 工作区」标签(⛔ 不靠分区来体现归属)
|
||||
if(t._goal) bits.push('<span class="badge goal">'+esc(t._goal)+'</span>');
|
||||
// 🔴 用户 2026-10-06:「工作区名称要不要 放到卡片下面」⇒ 采纳。
|
||||
// 卡片结构改成**三段**:① 标题 ② 受阻原因(有才显示)③ **卡片下方一行**=
|
||||
// `工作区名` + 线/执行者/时间。
|
||||
// ⚠️ 工作区名**从 `.meta` 里挪出去单列** —— 它是这一屏唯一的"归属"信息,
|
||||
// 混在其它 badge 里会被忽略。
|
||||
// 标签取 `_ws`(目录名,如 `ai1net-dsh-server`),⛔ 不是 `_goal`(目标短名,
|
||||
// 用户明确「不要显示 本机协作这种」)。
|
||||
if(t.line) bits.push('<span class="badge">'+esc(t.line)+'</span>');
|
||||
if(t.by) bits.push('<span class="badge">'+esc(t.by)+'</span>');
|
||||
if(t.t_end) bits.push(esc(fmtWhen(t.t_end)));
|
||||
else if(t.t_start) bits.push(esc(fmtWhen(t.t_start)));
|
||||
return '<div class="card" style="border-left-color:'+dot+'" data-tid="'+esc(t.id)+'"'+
|
||||
' data-pw="'+esc(t._ws||"")+'">'+
|
||||
var when = t.t_end ? fmtWhen(t.t_end) : (t.t_start ? fmtWhen(t.t_start) : "");
|
||||
if(when) bits.push('<span class="badge when">'+esc(when)+'</span>');
|
||||
// 🔴 本工作区的卡片换色突出(2026-10-06 用户要);判定来自服务端 `_self`,⛔ 前端不猜
|
||||
return '<div class="card'+(t._self?" is-self":"")+'" style="border-left-color:'+dot+
|
||||
'" data-tid="'+esc(t.id)+'" data-pw="'+esc(t._ws||"")+'">'+
|
||||
'<div class="t">'+esc(t.title||t.id)+'</div>'+
|
||||
(t.block_reason ? '<div class="meta" style="color:#f2a09a">受阻:'+esc(t.block_reason)+'</div>' : '')+
|
||||
(bits.length ? '<div class="meta">'+bits.join("")+'</div>' : '')+
|
||||
// 🔴 末行=工作区(用户要的「放到卡片下面」)
|
||||
(t._ws ? '<div class="wsrow"><span class="wsname">'+esc(t._ws)+'</span>'+
|
||||
(t._self ? '<span class="wsself">本工作区</span>' : '')+'</div>' : '')+
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -379,16 +430,20 @@ function renderAll(){
|
||||
var d = STATE.data, ps = d.projects;
|
||||
var sums = document.getElementById("summary");
|
||||
var cols = document.getElementById("cols");
|
||||
document.getElementById("btndel").disabled = true; // ⛔ 「全部」不是单个项目,删不得
|
||||
document.getElementById("btndel").disabled = true; // ⛔ 「全部」不是单个工作区,删不得
|
||||
|
||||
var tot = {total:0, done:0, open:0, blocked:0};
|
||||
ps.forEach(function(p){
|
||||
tot.total += p.queue.total; tot.done += p.queue.done;
|
||||
tot.open += p.queue.open; tot.blocked += p.queue.blocked;
|
||||
});
|
||||
// 🔴 两个维度**分开报**(用户 2026-10-06 纠正:「下拉框是选工作区的 跟目标没关系」):
|
||||
// 工作区=目录(装了机制的);目标=工作区里登记的那件事。
|
||||
// ⚠️ 我上一版写「目标 N 个」用的是**工作区条数** ⇒ 把两个维度混成了一个数(已改)。
|
||||
var nWs = ps.length, nGoal = ps.filter(function(p){ return p.has_goal; }).length;
|
||||
var s = [];
|
||||
s.push('<span class="chip">目标 <b>'+ps.length+'</b> 个</span>');
|
||||
s.push('<span class="chip">工作区 <b>'+(d.roots||[]).join(" · ")+'</b></span>');
|
||||
s.push('<span class="chip">工作区 <b>'+nWs+'</b> 个</span>');
|
||||
s.push('<span class="chip">其中已登记目标 <b>'+nGoal+'</b> 个</span>');
|
||||
s.push('<span class="chip">台账合计 <b>'+tot.total+'</b> 件</span>');
|
||||
s.push('<span class="chip">已完成 <b>'+tot.done+'</b></span>');
|
||||
s.push('<span class="chip">未完 <b>'+tot.open+'</b></span>');
|
||||
@@ -399,12 +454,13 @@ function renderAll(){
|
||||
var nt = [];
|
||||
var noGoal = ps.filter(function(x){ return !x.has_goal; });
|
||||
if(noGoal.length)
|
||||
nt.push('<div class="iw">有 <b>'+noGoal.length+'</b> 个工作区<b>还没登记目标</b>(只有机制骨架):'+
|
||||
esc(noGoal.map(function(x){ return x.name; }).join("、"))+'</div>');
|
||||
nt.push('<div class="iw">有 <b>'+noGoal.length+'</b> 个工作区<b>装了机制但还没登记目标</b>:'+
|
||||
esc(noGoal.map(function(x){ return x.dir; }).join("、"))+
|
||||
' ⇒ 在那些工作区里用 <code>goalctl declare --title "…"</code> 登记</div>');
|
||||
var notRun = ps.filter(function(x){ return x.life && x.life !== "进行中"; });
|
||||
if(notRun.length)
|
||||
nt.push('<div class="iw">有 <b>'+notRun.length+'</b> 个目标状态<b>不是「进行中」</b>:'+
|
||||
esc(notRun.map(function(x){ return x.name+"("+x.life+")"; }).join("、"))+'</div>');
|
||||
esc(notRun.map(function(x){ return x.dir+"("+(x.name||"—")+":"+x.life+")"; }).join("、"))+'</div>');
|
||||
setInfo(nt);
|
||||
|
||||
if(!ps.length){
|
||||
@@ -418,16 +474,21 @@ function renderAll(){
|
||||
(p.cards || []).forEach(function(c){
|
||||
var x = {};
|
||||
for(var k in c){ x[k] = c[k]; }
|
||||
x._ws = p.dir; // 供 openDetail 反查所属项目
|
||||
x._goal = p.name; // 卡片上的标签
|
||||
x._ws = p.dir; // 🔴 卡片标签=**工作区名**(用户口径);也是 openDetail 反查键
|
||||
x._goal = p.name; // 目标短名(⛔ 不上卡片标签,仅供详情/排查)
|
||||
x._self = !!p.self; // 🔴 本工作区 ⇒ 卡片换色突出
|
||||
x._life = p.life || "";
|
||||
(byState[c.state] = byState[c.state] || []).push(x);
|
||||
});
|
||||
});
|
||||
|
||||
cols.innerHTML = '<div class="cols">' + d.cols.map(function(c){
|
||||
// 🔴🔴 容器 `#cols` **本身**就带 `class="cols"`(已是 4 列 grid)⇒ 这里**只铺列**,
|
||||
// ⛔ 绝不能再包一层 `<div class="cols">`。2026-10-06 实测踩到:套两层 grid 后,
|
||||
// 内层作为外层的一个 grid item 先被压到 `min-width:auto`(=内容宽 399px),
|
||||
// 再在 399px 里自己切 4 列 ⇒ 每列 89px、卡片 67px,**整块挤在左边一条**。
|
||||
cols.innerHTML = d.cols.map(function(c){
|
||||
return _colHtml(c.key, c.label, byState[c.key] || []);
|
||||
}).join("") + '</div>';
|
||||
}).join("");
|
||||
_bindCards();
|
||||
}
|
||||
|
||||
@@ -464,9 +525,13 @@ function renderOne(){
|
||||
nt.push('<div class="iw">该目标状态为「'+esc(p.life)+'」('+esc(fmtWhen(p.life_at))+')。</div>');
|
||||
setInfo(nt);
|
||||
|
||||
// 列
|
||||
// 列(🔴 同样给每张卡片补 `_ws` / `_self`:单项目视图也要工作区标签+本区高亮)
|
||||
var byState = {};
|
||||
p.cards.forEach(function(c){ (byState[c.state] = byState[c.state] || []).push(c); });
|
||||
p.cards.forEach(function(c){
|
||||
var x = {}; for(var k in c){ x[k] = c[k]; }
|
||||
x._ws = p.dir; x._goal = p.name; x._self = !!p.self; x._life = p.life || "";
|
||||
(byState[c.state] = byState[c.state] || []).push(x);
|
||||
});
|
||||
cols.innerHTML = d.cols.map(function(c){
|
||||
return _colHtml(c.key, c.label, byState[c.key] || []);
|
||||
}).join("");
|
||||
|
||||
Reference in new issue
Block a user