Просмотр исходного кода

feat(v15): kf-glass-card 加 fill-to(卡面按内容收缩,A/B 显式铺满同高);B 卡补各阶段已交;A 卡填满 8 列

- 默认卡面收缩到内容:行里一张长卡不再把所有卡的内容拉散(P3 修根因,不在页面上打补丁)
- B 卡:深底分区要(标签 + 阶段 + 细条 + n/总人数),与 A 卡末行对齐;无人卡住时空的分区不渲染
- A 卡:阶段名、卡池进度 / 待评分两列、说明行;桌面与手机两套身份行
- 反跨」F 卡片右端列对齐 .feed li 改 2fr/1fr/64px
FmodeCode 4 дней назад
Родитель
Сommit
56dffac813
2 измененных файлов с 68 добавлено и 31 удалено
  1. 7 1
      src/components/kf-glass-card.js
  2. 61 30
      src/pages/view-workbench.js

+ 7 - 1
src/components/kf-glass-card.js

@@ -24,6 +24,8 @@ export class KfGlassCard extends LitElement {
     dark: { type: Boolean, reflect: true },
     /** v15:卡片内边距默认 20(dr1 的呼吸感)。仍可用 CSS 覆盖 --kf-card-pad */
     pad: { type: Number, attribute: 'pad' },
+    /** v15:卡面吃满宿主高度(栅格等高时用)。不写给 fillTo 时卡面按内容收缩、多余高度留在透明宿主上 */
+    fillTo: { type: Boolean, reflect: true, attribute: 'fill-to' },
   };
 
   constructor() {
@@ -33,6 +35,7 @@ export class KfGlassCard extends LitElement {
     this.tone = '';
     this.dark = false;
     this.pad = 20;
+    this.fillTo = false;
   }
 
   connectedCallback() {
@@ -57,7 +60,10 @@ export class KfGlassCard extends LitElement {
     :host { height: 100%; }
     .frame { width: 100%; height: 100%; min-height: 0; display: flex; flex-direction: column; }
     .frame { box-sizing: border-box; isolation: isolate; overflow: clip; min-height: 0; transition-property: none; }
-    .frame > .face { position: relative; z-index: 2; box-sizing: border-box; padding: var(--kf-card-pad, 20px); border-radius: var(--r-md); flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
+    /* v15:默认「内容多高卡就多高」—— 卡面收缩到内容,宿主被栅格拉高时多出的高度留在透明宿主上。
+       fill-to:卡面吃满宿主(.face 撑满、slot 撑满),同一行的卡等高时用它。 */
+    .frame > .face { position: relative; z-index: 2; box-sizing: border-box; padding: var(--kf-card-pad, 20px); border-radius: var(--r-md); flex: 0 1 auto; align-self: flex-start; min-height: 0; display: flex; flex-direction: column; }
+    :host([fill-to]) .frame > .face { flex: 1 1 auto; align-self: stretch; }
     /* v14 定稿:slot 必须是 flex column 且撑满 .face,否则 slotted 子元素(机会卡 .wrap)
        的 height/min-height:100% 解析不出包含块高度 → 无描述卡片内容只有 303px,底部差 53.6px。
        改成 flex 后 slotted 子元素自动 stretch 到 .face 的高度。 */

+ 61 - 30
src/pages/view-workbench.js

@@ -55,6 +55,8 @@ export class ViewWorkbench extends LitElement {
     .bento > .row { display: grid; gap: var(--kf-space-card-gap); align-items: stretch; }
     .bento > .row.ab { grid-template-columns: repeat(12, minmax(0, 1fr)); }
     .bento > .row.ab > kf-glass-card { height: auto; }
+    /* A/B 用 fill-to 吃满行高:卡面 .face 是 flex 列,slotted 的第一层(.me / .live)伸到底(下方各行的实际高度由本页 CSS 排) */
+    .bento > .row.ab > kf-glass-card[fill-to] .me, .bento > .row.ab > kf-glass-card[fill-to] .live { flex: 1 1 auto; }
     .bento > .row.ab > kf-glass-card[data-role="a"] { grid-column: span 8; }
     .bento > .row.ab > kf-glass-card[data-role="b"] { grid-column: span 4; }
     .bento > .row.cde { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@@ -76,44 +78,57 @@ export class ViewWorkbench extends LitElement {
     .empty .go { margin-left: auto; }
 
     /* ---------- A 卡:我的推进 ---------- */
-    .me { display: grid; gap: var(--kf-space-in); height: 100%; align-content: start; }
-    .me-id { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
+    /* A 卡内容按行贴顶排;行距 12;下半的两个分区用 1fr 行把底边对齐(待评分区贴底,不再在卡片中段留 59px 空洞) */
+    /* A 卡是 flex 列:阶段行以「剩余高度」行出现,多出来的空间从它这里再分给三格(格内进度条沉底)。 */
+    .me { display: flex; flex-direction: column; gap: var(--kf-space-in); }
+    .me > .me-id { flex: none; }
+    .me > .stagebar { flex: none; }
+    .me > .empty { flex: none; }
+    .me > .me-split { flex: none; margin-top: auto; }
+    .me-id { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 2px 12px; }
+    .me-id .me-title { display: grid; align-content: center; min-width: 0; }
     .avatar { width: 44px; height: 44px; border-radius: var(--r-md); display: grid; place-items: center; font-size: 20px; font-weight: 600; color: #fff;
       background: var(--kf-ink); flex: none; }
     .avatar.q { background: var(--kf-fill); color: var(--kf-ink-3); border: 1px dashed color-mix(in srgb, var(--kf-ink-3) 34%, transparent); }
     .me-id h3 { margin: 0; font-size: 20px; line-height: var(--lh-title); font-weight: 600; color: var(--kf-ink); letter-spacing: var(--ls-cn-display); }
     .me-id .sub { font-size: 14px; color: var(--kf-ink-3); }
     .me-id .go { margin-left: auto; }
-    .me-nums { margin-left: auto; display: flex; gap: 24px; }
-    .me-nums div { text-align: right; }
-    .me-nums b { display: block; font-size: 20px; font-weight: 600; line-height: 1; letter-spacing: var(--ls-cn-display); color: var(--gray-12);
+    .me-nums { display: flex; gap: 24px; justify-self: end; }
+    .me-nums div { display: flex; align-items: baseline; gap: 4px; }
+    .me-nums b { font-size: 20px; font-weight: 600; line-height: 1; letter-spacing: var(--ls-cn-display); color: var(--gray-12);
       font-variant-numeric: tabular-nums; }
-    .me-nums span { font-size: 12px; font-weight: 500; color: var(--kf-ink-3); }
+    .me-nums span { font-size: 12px; font-weight: 500; color: var(--kf-ink-3); white-space: nowrap; }
 
     /* 阶段条:五格一格一句「S1 · 份数」,徽标 + 大数字 + 一根细进度条 */
-    .stagebar { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--kf-space-in); }
+    .stagebar { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--kf-space-in); margin-top: calc(-1 * var(--kf-space-in) + 4px); }
     .stg { padding: 12px; border-radius: var(--r-md); background: var(--kf-fill); border: 1px solid transparent;
-      display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 4px 8px; min-width: 0; }
+      /* 阶段格铺满分到的高度带:上面 3 行贴顶,细进度条沉到底边(不留「格子里空一截」) */
+      display: grid; grid-template-columns: auto 1fr; align-content: start; align-items: center; gap: 2px 8px; min-width: 0; }
     .stg .b { font-size: 12px; font-weight: 600; line-height: 1; color: #fff; padding: 4px 6px; border-radius: var(--r-sm); letter-spacing: 0.02em; justify-self: start; }
     .stg .b.blue { background: var(--kf-s1); } .stg .b.violet { background: var(--kf-s2); } .stg .b.teal { background: var(--kf-s3); }
     .stg .b.amber { background: var(--kf-s4); } .stg .b.rose { background: var(--kf-s5); }
     .stg .n { justify-self: end; font-size: 20px; font-weight: 600; line-height: 1; letter-spacing: var(--ls-cn-display); color: var(--gray-12); font-variant-numeric: tabular-nums; }
-    .stg .sf { grid-column: 1 / -1; font-size: 12px; line-height: 1.3; color: var(--kf-ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-    .stg .f { grid-column: 1 / -1; height: 4px; border-radius: 2px; background: rgba(16, 24, 40, 0.07); overflow: hidden; margin-top: 4px; }
+    .stg .sf { grid-column: 1 / -1; min-height: 16px; font-size: 12px; line-height: 1.3; color: var(--kf-ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+    .stg .f { grid-column: 1 / -1; height: 4px; border-radius: 2px; background: rgba(16, 24, 40, 0.07); overflow: hidden; margin-top: 3px; }
     .stg .f i { display: block; height: 100%; border-radius: 2px; background: currentColor; width: calc(var(--v) * 100%); }
     .stg.blue { color: var(--kf-s1); } .stg.violet { color: var(--kf-s2); } .stg.teal { color: var(--kf-s3); } .stg.amber { color: var(--kf-s4); } .stg.rose { color: var(--kf-s5); }
     .stg.todo { color: var(--kf-ink-3); background: transparent; border-style: dashed; border-color: color-mix(in srgb, var(--kf-ink-3) 22%, transparent); }
     .stg.todo .n { color: var(--kf-ink-3); font-weight: 500; }
     /* A 卡下半:待评分(左)| 卡池进度(右)。两个分区头共用 .lbl,行底对齐 12 */
-    .me-split { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); gap: 32px; }
+    .me-split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; align-items: start; }
     .me-split > div { min-width: 0; }
     .me-split .lbl { min-height: 22px; }
     .pending { display: grid; gap: 0; margin-top: 4px; }
+    /* 待评分行贴底:与左列的三行进度在同一底线上 */
+    .me-split > div:last-child { display: flex; flex-direction: column; }
+    .me-split > div:last-child .pending, .me-split > div:last-child .me-none { margin-top: auto; }
     .pending li { display: flex; align-items: baseline; gap: 8px; min-height: 32px; font-size: 14px; }
     .pending li a { color: var(--kf-ink); text-decoration: none; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
     .pending li a:hover { color: var(--kf-accent-ink); }
-    .pending li .s { margin-left: auto; font-weight: 600; color: var(--kf-accent-ink); font-variant-numeric: tabular-nums; }
+    .pending li .d { font-size: 12px; color: var(--kf-ink-3); flex: none; }
+    .pending li .s { margin-left: auto; font-weight: 600; color: var(--kf-accent-ink); font-variant-numeric: tabular-nums; flex: none; }
     .me-none { font-size: 14px; color: var(--kf-ink-3); padding-top: 10px; }
+    .me-note { font-size: 12px; line-height: 1.5; color: var(--kf-ink-3); padding-top: 2px; }
     .prog { display: grid; gap: 0; margin-top: 4px; }
     .prog li { display: grid; grid-template-columns: 6px auto 1fr auto; align-items: baseline; gap: 10px; min-height: 32px; font-size: 14px; }
     .prog .pk { color: var(--kf-ink-2); }
@@ -122,7 +137,7 @@ export class ViewWorkbench extends LitElement {
     .prog .pp { color: var(--kf-ink-3); font-size: 12px; font-variant-numeric: tabular-nums; }
 
     /* ---------- B 卡:现场(深色) ---------- */
-    .live { display: grid; gap: var(--kf-space-in); height: 100%; align-content: start;
+    .live { display: flex; flex-direction: column; gap: var(--kf-space-in); align-content: start;
       --live-ink: #F4F4F5; --live-ink-3: #B4B4BC; --live-line: rgba(255, 255, 255, 0.12); }
     .live .lbl { color: var(--live-ink-3); }
     .live-head { display: flex; align-items: center; gap: 8px; }
@@ -139,7 +154,14 @@ export class ViewWorkbench extends LitElement {
     .stalled .meta { color: var(--live-ink-3); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
     .stalled .h { margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums; }
     .live-ok { display: flex; align-items: center; gap: 8px; margin-top: 4px; font-size: 14px; font-weight: 500; color: var(--live-ink); }
-    .live-foot { margin-top: auto; font-size: 12px; color: var(--live-ink-3); }
+    .live-rank { display: grid; gap: 0; border-top: 1px solid var(--live-line); padding-top: 8px; }
+    .live-rank .lbl { margin-bottom: 4px; }
+    .rk { display: grid; grid-template-columns: 6px minmax(0, 1fr) 56px auto; align-items: center; gap: 8px; min-height: 26px; font-size: 12px; color: var(--live-ink); }
+    .rk-k { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+    .rk-bar { height: 4px; border-radius: 2px; background: var(--live-line); overflow: hidden; }
+    .rk-bar i { display: block; height: 100%; border-radius: 2px; background: var(--live-ink); }
+    .rk-n { font-variant-numeric: tabular-nums; color: var(--live-ink-3); }
+    .live-foot { margin-top: auto; padding-top: 8px; font-size: 12px; color: var(--live-ink-3); }
 
     /* ---------- C/D/E:三张短数据卡 ---------- */
     .stat { display: grid; grid-template-rows: auto auto auto; gap: 8px; height: 100%; align-content: start; }
@@ -157,9 +179,9 @@ export class ViewWorkbench extends LitElement {
     .feed-head { display: flex; align-items: center; gap: 8px; min-height: 22px; }
     .feed-head .go { margin-left: auto; }
     /* 行:左标题 / 中阶段 / 右时间。网格而不是 flex —— 中间那列 auto 宽,右边永远贴右 */
-    .feed { display: grid; margin-top: 8px; }
-    .feed li { display: grid; grid-template-columns: minmax(0, 1fr) auto 64px; align-items: baseline; gap: 16px; padding: 11px 0; border-top: 1px solid var(--kf-line); font-size: 14px; }
-    .feed li:first-child { border-top: 0; padding-top: 4px; }
+    .feed { display: grid; }
+    .feed li { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) 64px; align-items: baseline; gap: 16px; padding: 11px 0; border-top: 1px solid var(--kf-line); font-size: 14px; }
+    .feed li:first-child { border-top: 1px solid var(--kf-line); padding-top: 11px; margin-top: 8px; }
     .feed li:last-child { padding-bottom: 4px; border-bottom: 0; }
     .feed li:last-child a { font-weight: 500; }
     .feed-more { display: inline-flex; align-items: center; gap: 4px; margin-top: 6px; font-size: 14px; }
@@ -182,8 +204,9 @@ export class ViewWorkbench extends LitElement {
       .bento > .row { gap: var(--kf-space-card); }
       .bento > .row.cde { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
       .me { gap: 12px; }
-      .me-id h3, .me-id .sub { display: inline; }
-      .me-id > div { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
+      .me-id { grid-template-columns: auto minmax(0, 1fr) !important; }
+      .me-id .avatar { width: 40px; height: 40px; font-size: 20px; }
+      .me-nums { grid-column: 1 / -1; justify-self: start; }
       .me-nums { margin-left: 0; width: 100%; justify-content: flex-start; gap: 24px; }
       .me-nums div { text-align: left; display: flex; align-items: baseline; gap: 4px; }
       /* 五阶段一行放不下英文名:两行三列(第五格占满第二行左半),英文名折行两行后省略 */
@@ -224,8 +247,8 @@ export class ViewWorkbench extends LitElement {
       ${head('我的推进', '五阶段进度与现场状态', html`<a class="go" href=${href('/pool')} data-link>机会卡池 →</a>`)}
       <div class="bento">
         <div class="row ab grid2 kf-enter" style="--i:0">
-          <kf-glass-card data-role="a">${this.#mine(me, pool, live)}</kf-glass-card>
-          <kf-glass-card data-role="b" dark>${this.#live(live, pool)}</kf-glass-card>
+          <kf-glass-card data-role="a" fill-to>${this.#mine(me, pool, live)}</kf-glass-card>
+          <kf-glass-card data-role="b" dark fill-to>${this.#live(live, pool)}</kf-glass-card>
         </div>
 
         <div class="row cde kf-enter" style="--i:1">
@@ -249,13 +272,9 @@ export class ViewWorkbench extends LitElement {
     if (!this.me) {
       const maxSub = Math.max(1, ...live.stages.map((r) => r.submitted));
       return html`<div class="me" data-empty="no-me">
-        <div class="me-id"><div class="avatar q">?</div>
-          <div><h3>还没选「我是」</h3><span class="sub">先看全员:${live.memberCount} 人</span></div>
+        <div class="me-id" style="grid-template-columns:auto minmax(0,1fr) auto"><div class="avatar q">?</div>
+          <div class="me-title"><h3>还没选「我是」</h3><span class="sub">先看全员:${live.memberCount} 人 · 已交 ${live.submittedPeople} 人</span></div>
           <a class="go" href=${href('/members')} data-link>${icon('user', 14)} ${t('选我')}</a></div>
-        <div class="me-nums">
-          <div><b>${live.memberCount}</b><span>人</span></div>
-          <div><b>${live.submittedPeople}</b><span>已交</span></div>
-        </div>
         <div class="stagebar" data-allstages>
           ${live.stages.map((r, i) => html`<div class="stg ${TONE[r.stage.code]}" style=${`--i:${i + 1}`} data-stage=${r.stage.code}>
             <span class="b ${TONE[r.stage.code]}">${r.stage.code}</span>
@@ -272,7 +291,7 @@ export class ViewWorkbench extends LitElement {
     return html`<div class="me" data-empty=${me.count === 0 ? 'no-result' : 'none'}>
       <div class="me-id">
         <div class="avatar">${Array.from(me.me).slice(-1).join('')}</div>
-        <div><h3>${me.me}</h3><span class="sub">已完成 ${me.stagesDone.length} / 5 个阶段</span></div>
+        <div class="me-title"><h3>${me.me}</h3><span class="sub">已完成 ${me.stagesDone.length} / 5 个阶段 · 累计 ${me.count} 份成果</span></div>
         <div class="me-nums">
           <div><b>${me.count}</b><span>份成果</span></div>
           <div><b>${me.opps.length}</b><span>张卡</span></div>
@@ -299,8 +318,12 @@ export class ViewWorkbench extends LitElement {
         </div>
         <div>
           <div class="lbl">待评分${pending.length ? ` ${pending.length}` : ''}</div>
-          ${pending.length ? html`<ul class="pending">${pending.slice(0, 3).map((o) => html`<li>
-            <a href=${href(`/pool/${o.id}`)} data-link>${o.title}</a><span class="s">${o.stats.avg != null ? Math.round(o.stats.avg) : '—'}</span></li>`)}</ul>`
+          <p class="me-note">候选卡里还没有你打分的卡,按最近上传排</p>
+          ${pending.length ? html`<ul class="pending">${pending.slice(0, 4).map((o) => html`<li>
+            <a href=${href(`/pool/${o.id}`)} data-link>${o.title}</a>
+            <span class="d">${o.stats.n ? `${o.stats.n} 人 · 均 ${o.stats.avg != null ? Math.round(o.stats.avg) : '—'}` : '尚未评分'}</span>
+            <a class="s" href=${href(`/pool/${o.id}`)} data-link aria-label="去评分">${o.stats.avg != null ? Math.round(o.stats.avg) : '去评'}</a></li>`)}</ul>
+            ${pending.length > 4 ? html`<a class="feed-more go" href=${href('/pool')} data-link>还有 ${pending.length - 4} 张待评分 →</a>` : ''}`
             : html`<p class="me-none">没有等你评分的卡</p>`}
         </div>
       </div>
@@ -333,6 +356,14 @@ export class ViewWorkbench extends LitElement {
       : html`
         <div class="live-title">大家都在动</div>
         <p class="live-ok">${icon('check', 16)} 没有超过 8 小时没交成果的人</p>`}
+      <div class="live-rank">
+        <div class="lbl">各阶段已交</div>
+        ${live.stages.map((r, i) => html`<div class="rk">
+          <i class="dot" style=${`background:var(--kf-${['s1', 's2', 's3', 's4', 's5'][i]})`}></i>
+          <span class="rk-k">${r.stage.code} ${r.stage.name}</span>
+          <span class="rk-bar"><i style=${`width:${Math.round((r.submitted / Math.max(1, live.memberCount)) * 100)}%`}></i></span>
+          <span class="rk-n">${r.submitted}/${live.memberCount}</span></div>`)}
+      </div>
       <div class="live-foot">${t(`${live.memberCount} 人 · ${pool.total} 张卡在推进`)}</div>
     </div>`;
   }