项目管理界面:修列宽塌陷 + 工作区维度纠偏 + 卡片信息重排
一、修「卡片挤到一起去了」(用户 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
@@ -31,6 +31,13 @@ TARGET = os.path.join(HERE, "project-board.py")
|
|||||||
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
|
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
|
||||||
os.environ.setdefault("DSH_PROJECT_BOARD_UPSTREAM", "http://127.0.0.1:20099/board.json")
|
os.environ.setdefault("DSH_PROJECT_BOARD_UPSTREAM", "http://127.0.0.1:20099/board.json")
|
||||||
|
|
||||||
|
# 🔴 「本工作区」=**拥有一这层的那个工作区**(用户 2026-10-06 要:本工作区的卡片换色突出)。
|
||||||
|
# ⛔ 不许靠 `Path.cwd()`:计划任务的 **工作目录是 `交付物/项目管理界面`**(实测),
|
||||||
|
# 不是工作区根 ⇒ 那样算出来的"本工作区"是错的。
|
||||||
|
# 正解=从本文件位置**往上两级**:`<ws>/交付物/项目管理界面/project-board-launch.py`。
|
||||||
|
os.environ.setdefault("DSH_PM_BOARD_SELF_WS",
|
||||||
|
os.path.dirname(os.path.dirname(HERE)))
|
||||||
|
|
||||||
|
|
||||||
def port_listening(port: int) -> bool:
|
def port_listening(port: int) -> bool:
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
|||||||
@@ -35,24 +35,43 @@
|
|||||||
.chip b{color:var(--fg);font-weight:600}
|
.chip b{color:var(--fg);font-weight:600}
|
||||||
|
|
||||||
/* ── 列 ── */
|
/* ── 列 ── */
|
||||||
main{padding:16px 18px 60px}
|
main{padding:16px 18px 40px}
|
||||||
.cols{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}
|
/* 🔴🔴 `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:1100px){.cols{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||||
@media(max-width:640px){.cols{grid-template-columns:1fr}}
|
@media(max-width:640px){.cols{grid-template-columns:1fr}}
|
||||||
.col{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);
|
.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;
|
.colhead{display:flex;align-items:center;gap:8px;padding:11px 13px;
|
||||||
border-bottom:1px solid var(--line)}
|
border-bottom:1px solid var(--line)}
|
||||||
.dot{width:8px;height:8px;border-radius:50%;flex:none}
|
.dot{width:8px;height:8px;border-radius:50%;flex:none}
|
||||||
.coltitle{font-weight:600;font-size:13px}
|
.coltitle{font-weight:600;font-size:13px}
|
||||||
.count{margin-left:auto;background:var(--panel2);border:1px solid var(--line);
|
.count{margin-left:auto;background:var(--panel2);border:1px solid var(--line);
|
||||||
border-radius:999px;padding:1px 9px;font-size:12px;color:var(--dim)}
|
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;
|
.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}
|
transition:transform .08s ease,border-color .12s ease}
|
||||||
.card:hover{transform:translateY(-1px);border-color:var(--accent)}
|
.card:hover{transform:translateY(-1px);border-color:var(--accent)}
|
||||||
.card .t{font-weight:500;word-break:break-word}
|
.card .t{font-weight:500;word-break:break-word}
|
||||||
@@ -160,9 +179,9 @@
|
|||||||
<header>
|
<header>
|
||||||
<div class="hrow">
|
<div class="hrow">
|
||||||
<div class="brand">项目管理<small id="ws">—</small></div>
|
<div class="brand">项目管理<small id="ws">—</small></div>
|
||||||
<label for="psel" class="stamp">项目</label>
|
<label for="psel" class="stamp">工作区</label>
|
||||||
<select id="psel" aria-label="选择项目"></select>
|
<select id="psel" aria-label="选择工作区"></select>
|
||||||
<button id="btndel" type="button" class="danger" title="删除当前项目(可选粒度)">删除</button>
|
<button id="btndel" type="button" class="danger" title="删除当前工作区(可选粒度)">删除</button>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<span class="stamp" id="stamp">加载中…</span>
|
<span class="stamp" id="stamp">加载中…</span>
|
||||||
<span class="info fancy" id="infobtn" tabindex="0" role="button" aria-label="状态说明"
|
<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){
|
function fmtWhen(s){
|
||||||
if(!s) return "—";
|
if(!s) return "—";
|
||||||
var m = String(s).match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})/);
|
var str = String(s).trim();
|
||||||
if(!m) return String(s);
|
// 🔴🔴 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];
|
return m[1]+"-"+m[2]+"-"+m[3]+" "+m[4]+":"+m[5];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,10 +354,18 @@ function load(manual){
|
|||||||
sel.innerHTML = '<option value="-1">(本机暂无装了机制的工作区)</option>';
|
sel.innerHTML = '<option value="-1">(本机暂无装了机制的工作区)</option>';
|
||||||
STATE.cur = -1;
|
STATE.cur = -1;
|
||||||
} else {
|
} else {
|
||||||
// 🔴 默认「全部」:一屏看完本机所有目标(⛔ 不再默认只显示一个)
|
// 🔴🔴 **本下拉=工作区选择器**(用户 2026-10-06 明确纠正:
|
||||||
sel.innerHTML = '<option value="-1">全部('+d.projects.length+' 个目标)</option>'
|
// 「下拉框是选工作区的 跟目标没关系」)。
|
||||||
|
// ⇒ ① 选项文字用 **工作区名**(`dir` 目录名),⛔ 不是目标名;
|
||||||
|
// ② 计数说 **N 个工作区**,⛔ 不说"N 个目标"(那是另一个维度);
|
||||||
|
// ③ 装了机制的工作区**一律列出**(含还没登记目标的)—— 用户明令
|
||||||
|
// 「加载机制就可以显示」(⛔ 不许因为没目标就藏起来)。
|
||||||
|
// ⚠️ 我上一版写成「全部(N 个目标)」,是把两个维度混了 —— 已纠正。
|
||||||
|
sel.innerHTML = '<option value="-1">全部('+d.projects.length+' 个工作区)</option>'
|
||||||
+ d.projects.map(function(p,i){
|
+ 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;
|
if(STATE.cur >= d.projects.length) STATE.cur = -1;
|
||||||
sel.value = String(STATE.cur);
|
sel.value = String(STATE.cur);
|
||||||
}
|
}
|
||||||
@@ -343,17 +385,26 @@ function _stateDot(key){
|
|||||||
|
|
||||||
function _cardHtml(t){
|
function _cardHtml(t){
|
||||||
var dot = _stateDot(t.state), bits = [];
|
var dot = _stateDot(t.state), bits = [];
|
||||||
// 🔴 统一视图里,卡片自带「目标 · 工作区」标签(⛔ 不靠分区来体现归属)
|
// 🔴 用户 2026-10-06:「工作区名称要不要 放到卡片下面」⇒ 采纳。
|
||||||
if(t._goal) bits.push('<span class="badge goal">'+esc(t._goal)+'</span>');
|
// 卡片结构改成**三段**:① 标题 ② 受阻原因(有才显示)③ **卡片下方一行**=
|
||||||
|
// `工作区名` + 线/执行者/时间。
|
||||||
|
// ⚠️ 工作区名**从 `.meta` 里挪出去单列** —— 它是这一屏唯一的"归属"信息,
|
||||||
|
// 混在其它 badge 里会被忽略。
|
||||||
|
// 标签取 `_ws`(目录名,如 `ai1net-dsh-server`),⛔ 不是 `_goal`(目标短名,
|
||||||
|
// 用户明确「不要显示 本机协作这种」)。
|
||||||
if(t.line) bits.push('<span class="badge">'+esc(t.line)+'</span>');
|
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.by) bits.push('<span class="badge">'+esc(t.by)+'</span>');
|
||||||
if(t.t_end) bits.push(esc(fmtWhen(t.t_end)));
|
var when = t.t_end ? fmtWhen(t.t_end) : (t.t_start ? fmtWhen(t.t_start) : "");
|
||||||
else if(t.t_start) bits.push(esc(fmtWhen(t.t_start)));
|
if(when) bits.push('<span class="badge when">'+esc(when)+'</span>');
|
||||||
return '<div class="card" style="border-left-color:'+dot+'" data-tid="'+esc(t.id)+'"'+
|
// 🔴 本工作区的卡片换色突出(2026-10-06 用户要);判定来自服务端 `_self`,⛔ 前端不猜
|
||||||
' data-pw="'+esc(t._ws||"")+'">'+
|
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>'+
|
'<div class="t">'+esc(t.title||t.id)+'</div>'+
|
||||||
(t.block_reason ? '<div class="meta" style="color:#f2a09a">受阻:'+esc(t.block_reason)+'</div>' : '')+
|
(t.block_reason ? '<div class="meta" style="color:#f2a09a">受阻:'+esc(t.block_reason)+'</div>' : '')+
|
||||||
(bits.length ? '<div class="meta">'+bits.join("")+'</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>';
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,16 +430,20 @@ function renderAll(){
|
|||||||
var d = STATE.data, ps = d.projects;
|
var d = STATE.data, ps = d.projects;
|
||||||
var sums = document.getElementById("summary");
|
var sums = document.getElementById("summary");
|
||||||
var cols = document.getElementById("cols");
|
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};
|
var tot = {total:0, done:0, open:0, blocked:0};
|
||||||
ps.forEach(function(p){
|
ps.forEach(function(p){
|
||||||
tot.total += p.queue.total; tot.done += p.queue.done;
|
tot.total += p.queue.total; tot.done += p.queue.done;
|
||||||
tot.open += p.queue.open; tot.blocked += p.queue.blocked;
|
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 = [];
|
var s = [];
|
||||||
s.push('<span class="chip">目标 <b>'+ps.length+'</b> 个</span>');
|
s.push('<span class="chip">工作区 <b>'+nWs+'</b> 个</span>');
|
||||||
s.push('<span class="chip">工作区 <b>'+(d.roots||[]).join(" · ")+'</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.total+'</b> 件</span>');
|
||||||
s.push('<span class="chip">已完成 <b>'+tot.done+'</b></span>');
|
s.push('<span class="chip">已完成 <b>'+tot.done+'</b></span>');
|
||||||
s.push('<span class="chip">未完 <b>'+tot.open+'</b></span>');
|
s.push('<span class="chip">未完 <b>'+tot.open+'</b></span>');
|
||||||
@@ -399,12 +454,13 @@ function renderAll(){
|
|||||||
var nt = [];
|
var nt = [];
|
||||||
var noGoal = ps.filter(function(x){ return !x.has_goal; });
|
var noGoal = ps.filter(function(x){ return !x.has_goal; });
|
||||||
if(noGoal.length)
|
if(noGoal.length)
|
||||||
nt.push('<div class="iw">有 <b>'+noGoal.length+'</b> 个工作区<b>还没登记目标</b>(只有机制骨架):'+
|
nt.push('<div class="iw">有 <b>'+noGoal.length+'</b> 个工作区<b>装了机制但还没登记目标</b>:'+
|
||||||
esc(noGoal.map(function(x){ return x.name; }).join("、"))+'</div>');
|
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 !== "进行中"; });
|
var notRun = ps.filter(function(x){ return x.life && x.life !== "进行中"; });
|
||||||
if(notRun.length)
|
if(notRun.length)
|
||||||
nt.push('<div class="iw">有 <b>'+notRun.length+'</b> 个目标状态<b>不是「进行中」</b>:'+
|
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);
|
setInfo(nt);
|
||||||
|
|
||||||
if(!ps.length){
|
if(!ps.length){
|
||||||
@@ -418,16 +474,21 @@ function renderAll(){
|
|||||||
(p.cards || []).forEach(function(c){
|
(p.cards || []).forEach(function(c){
|
||||||
var x = {};
|
var x = {};
|
||||||
for(var k in c){ x[k] = c[k]; }
|
for(var k in c){ x[k] = c[k]; }
|
||||||
x._ws = p.dir; // 供 openDetail 反查所属项目
|
x._ws = p.dir; // 🔴 卡片标签=**工作区名**(用户口径);也是 openDetail 反查键
|
||||||
x._goal = p.name; // 卡片上的标签
|
x._goal = p.name; // 目标短名(⛔ 不上卡片标签,仅供详情/排查)
|
||||||
|
x._self = !!p.self; // 🔴 本工作区 ⇒ 卡片换色突出
|
||||||
x._life = p.life || "";
|
x._life = p.life || "";
|
||||||
(byState[c.state] = byState[c.state] || []).push(x);
|
(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] || []);
|
return _colHtml(c.key, c.label, byState[c.key] || []);
|
||||||
}).join("") + '</div>';
|
}).join("");
|
||||||
_bindCards();
|
_bindCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,9 +525,13 @@ function renderOne(){
|
|||||||
nt.push('<div class="iw">该目标状态为「'+esc(p.life)+'」('+esc(fmtWhen(p.life_at))+')。</div>');
|
nt.push('<div class="iw">该目标状态为「'+esc(p.life)+'」('+esc(fmtWhen(p.life_at))+')。</div>');
|
||||||
setInfo(nt);
|
setInfo(nt);
|
||||||
|
|
||||||
// 列
|
// 列(🔴 同样给每张卡片补 `_ws` / `_self`:单项目视图也要工作区标签+本区高亮)
|
||||||
var byState = {};
|
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){
|
cols.innerHTML = d.cols.map(function(c){
|
||||||
return _colHtml(c.key, c.label, byState[c.key] || []);
|
return _colHtml(c.key, c.label, byState[c.key] || []);
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ SKILL_SCRIPTS = Path(os.environ.get("DSH_SKILL_SCRIPTS")
|
|||||||
# 可选:某个区的旧看板(**只作补充**,主力是直读文件)
|
# 可选:某个区的旧看板(**只作补充**,主力是直读文件)
|
||||||
UPSTREAM = os.environ.get("DSH_PROJECT_BOARD_UPSTREAM") or ""
|
UPSTREAM = os.environ.get("DSH_PROJECT_BOARD_UPSTREAM") or ""
|
||||||
|
|
||||||
|
# 🔴 「本工作区」=**起这个服务的那个工作区**(2026-10-06 用户要:本工作区的卡片换色突出)。
|
||||||
|
# ⛔ 不许在前端硬编码目录名 —— 换机 / 改名就漂。
|
||||||
|
# 取法:环境变量优先(launch 脚本可显式注入),否则用**进程 cwd**(计划任务的动作 cwd
|
||||||
|
# 就是工作区根)⇒ 两者都拿不到就 `""`(前端一律不高亮,⛔ 不猜)。
|
||||||
|
SELF_WS = (os.environ.get("DSH_PM_BOARD_SELF_WS") or "").strip() or str(Path.cwd())
|
||||||
|
|
||||||
_OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({})) # ⛔ 绕过 HTTP_PROXY
|
_OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({})) # ⛔ 绕过 HTTP_PROXY
|
||||||
|
|
||||||
|
|
||||||
@@ -200,7 +206,12 @@ def build_project(ws: Path) -> dict:
|
|||||||
|
|
||||||
short = str(goal.get("short") or "").strip()
|
short = str(goal.get("short") or "").strip()
|
||||||
title = str(goal.get("title") or "").strip()
|
title = str(goal.get("title") or "").strip()
|
||||||
name = short or title or ("(未命名 · %s)" % ws.name)
|
# 🔴🔴 **`title` 优先,`short` 兜底**(2026-10-06 用户纠正)。
|
||||||
|
# 改前是 `short or title` ⇒ 凡登记过 `short` 的区,界面一律显示**短名**
|
||||||
|
# (`vibe-product` 这种目录名),用户原话:「上面的这个…显示的是 目录名,
|
||||||
|
# 应该是真正名称」。`short` 是给**文件名/路径**用的短标识,⛔ 不是给人读的名字。
|
||||||
|
# ⇒ 有 `title` 就显示 `title`;没 `title` 才回落 `short`;都没有 ⇒ 标「未登记目标」。
|
||||||
|
name = title or short or ("未登记目标 · %s" % ws.name)
|
||||||
|
|
||||||
# ── 卡片(台账条目;只取四态里的)
|
# ── 卡片(台账条目;只取四态里的)
|
||||||
keys = {c[0] for c in COLS}
|
keys = {c[0] for c in COLS}
|
||||||
@@ -246,10 +257,20 @@ def build_project(ws: Path) -> dict:
|
|||||||
f["open"] += 1
|
f["open"] += 1
|
||||||
|
|
||||||
topics = [str(x) for x in (goal.get("topics") or []) if str(x).strip()]
|
topics = [str(x) for x in (goal.get("topics") or []) if str(x).strip()]
|
||||||
|
# 🔴 「本工作区」标记(2026-10-06 用户要:本工作区的卡片换个颜色突出)。
|
||||||
|
# ⛔ 不在前端硬编码目录名 —— 那份名单会随换机/改名漂掉。
|
||||||
|
# 判据=**服务进程自己的启动目录**(launch 脚本就是从该工作区起的),
|
||||||
|
# 与 `SelfWs` 同源;比不出来就一律 False(⛔ 不猜)。
|
||||||
|
try:
|
||||||
|
_self_ws = Path(SELF_WS).resolve()
|
||||||
|
_is_self = ws.resolve() == _self_ws
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
_is_self = False
|
||||||
return {
|
return {
|
||||||
"ws": str(ws).replace("\\", "/"),
|
"ws": str(ws).replace("\\", "/"),
|
||||||
"dir": ws.name,
|
"dir": ws.name,
|
||||||
"name": name,
|
"name": name,
|
||||||
|
"self": _is_self,
|
||||||
"title": title,
|
"title": title,
|
||||||
"topics": topics,
|
"topics": topics,
|
||||||
"life": str(goal.get("lifecycle") or "").strip(),
|
"life": str(goal.get("lifecycle") or "").strip(),
|
||||||
|
|||||||
Reference in new issue
Block a user