Explorar o código

feat(v12-P3): 工作台未选身份时左卡改为全员阶段进度(真实数据),左右等高;池子加收尾页脚

FmodeCode hai 5 días
pai
achega
a27ded3a9d

+ 209 - 203
docs/v12-after-p2.json

@@ -1,13 +1,13 @@
 {
 {
   "tag": "before",
   "tag": "before",
-  "at": "2026-10-30T06:31:15.263Z",
+  "at": "2026-10-30T06:58:30.641Z",
   "pages": [
   "pages": [
     {
     {
       "page": "/",
       "page": "/",
       "radii": {
       "radii": {
-        "0": 808,
-        "8": 108,
-        "12": 112,
+        "0": 944,
+        "8": 128,
+        "12": 132,
         "circle": 4
         "circle": 4
       },
       },
       "controls": [
       "controls": [
@@ -202,10 +202,10 @@
         }
         }
       ],
       ],
       "fontSizes": {
       "fontSizes": {
-        "11": 69,
-        "13": 23,
+        "11": 76,
+        "13": 28,
         "14": 8,
         "14": 8,
-        "16": 6,
+        "16": 11,
         "20": 13,
         "20": 13,
         "32": 4
         "32": 4
       },
       },
@@ -216,13 +216,26 @@
           "n": 2,
           "n": 2,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            142,
-            142
+            267,
+            267
+          ]
+        },
+        {
+          "container": "div.stagebar",
+          "top": 248,
+          "n": 5,
+          "spread": 0,
+          "heights": [
+            73,
+            73,
+            73,
+            73,
+            73
           ]
           ]
         },
         },
         {
         {
           "container": "div.grid4",
           "container": "div.grid4",
-          "top": 352,
+          "top": 476,
           "n": 4,
           "n": 4,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
@@ -234,7 +247,7 @@
         },
         },
         {
         {
           "container": "div.latest",
           "container": "div.latest",
-          "top": 444,
+          "top": 568,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
@@ -245,7 +258,7 @@
         },
         },
         {
         {
           "container": "div.nums",
           "container": "div.nums",
-          "top": 620,
+          "top": 748,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
@@ -256,15 +269,15 @@
         }
         }
       ],
       ],
       "blanks": [],
       "blanks": [],
-      "clickables": 27,
+      "clickables": 14,
       "hoverFail": [],
       "hoverFail": [],
-      "scrollH": 903,
+      "scrollH": 1028,
       "lastContentBottom": 0,
       "lastContentBottom": 0,
       "filterLayout": null,
       "filterLayout": null,
       "wbCols": {
       "wbCols": {
         "heights": [
         "heights": [
-          142,
-          142
+          267,
+          267
         ],
         ],
         "diff": 0,
         "diff": 0,
         "leftMaxGap": 0
         "leftMaxGap": 0
@@ -279,7 +292,7 @@
     {
     {
       "page": "/pool",
       "page": "/pool",
       "radii": {
       "radii": {
-        "0": 1620,
+        "0": 1636,
         "8": 372,
         "8": 372,
         "12": 112,
         "12": 112,
         "circle": 4
         "circle": 4
@@ -1021,6 +1034,36 @@
           "font": "13px",
           "font": "13px",
           "reasons": [],
           "reasons": [],
           "text": "详情 →"
           "text": "详情 →"
+        },
+        {
+          "tag": "button.cfb",
+          "w": 86,
+          "h": 24,
+          "bg": "rgb(239, 239, 239)",
+          "bw": 2,
+          "r": "0px",
+          "font": "13.3333px",
+          "reasons": [
+            "灰底rgb(239, 239, 239)",
+            "浏览器浮雕边 outset2px",
+            "无圆角有边框"
+          ],
+          "text": "调整筛选"
+        },
+        {
+          "tag": "button.cfb",
+          "w": 86,
+          "h": 24,
+          "bg": "rgb(239, 239, 239)",
+          "bw": 2,
+          "r": "0px",
+          "font": "13.3333px",
+          "reasons": [
+            "灰底rgb(239, 239, 239)",
+            "浏览器浮雕边 outset2px",
+            "无圆角有边框"
+          ],
+          "text": "回到顶部"
         }
         }
       ],
       ],
       "thickBorders": [
       "thickBorders": [
@@ -1029,14 +1072,35 @@
           "w": 0,
           "w": 0,
           "ow": 3,
           "ow": 3,
           "o": "0px"
           "o": "0px"
+        },
+        {
+          "tag": "button.cfb",
+          "w": 2,
+          "ow": 3,
+          "o": "0px"
+        },
+        {
+          "tag": "button.cfb",
+          "w": 2,
+          "ow": 3,
+          "o": "0px"
+        }
+      ],
+      "blackText": [
+        {
+          "tag": "button.cfb",
+          "text": "调整筛选"
+        },
+        {
+          "tag": "button.cfb",
+          "text": "回到顶部"
         }
         }
       ],
       ],
-      "blackText": [],
       "fontSizes": {
       "fontSizes": {
         "11": 158,
         "11": 158,
         "12": 6,
         "12": 6,
         "13": 44,
         "13": 44,
-        "14": 7,
+        "14": 8,
         "16": 11,
         "16": 11,
         "20": 29
         "20": 29
       },
       },
@@ -1047,20 +1111,20 @@
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            368,
-            368,
-            368
+            389,
+            389,
+            389
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards.kf-enter",
           "container": "div.cards.kf-enter",
-          "top": 892,
+          "top": 912,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            410,
-            410,
-            410
+            389,
+            389,
+            389
           ]
           ]
         },
         },
         {
         {
@@ -1069,9 +1133,9 @@
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            410,
-            410,
-            410
+            389,
+            389,
+            389
           ]
           ]
         },
         },
         {
         {
@@ -1087,7 +1151,7 @@
         },
         },
         {
         {
           "container": "div.nums",
           "container": "div.nums",
-          "top": 1072,
+          "top": 1092,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
@@ -1111,7 +1175,7 @@
       "blanks": [],
       "blanks": [],
       "clickables": 50,
       "clickables": 50,
       "hoverFail": [],
       "hoverFail": [],
-      "scrollH": 1776,
+      "scrollH": 1779,
       "lastContentBottom": 0,
       "lastContentBottom": 0,
       "filterLayout": {
       "filterLayout": {
         "container": true,
         "container": true,
@@ -1175,9 +1239,9 @@
     {
     {
       "page": "/stages",
       "page": "/stages",
       "radii": {
       "radii": {
-        "0": 3456,
+        "0": 3448,
         "2": 12,
         "2": 12,
-        "8": 516,
+        "8": 524,
         "12": 336,
         "12": 336,
         "circle": 4
         "circle": 4
       },
       },
@@ -1294,32 +1358,24 @@
         },
         },
         {
         {
           "tag": "button.cfb",
           "tag": "button.cfb",
-          "w": 86,
-          "h": 24,
-          "bg": "rgb(239, 239, 239)",
-          "bw": 2,
-          "r": "0px",
-          "font": "13.3333px",
-          "reasons": [
-            "灰底rgb(239, 239, 239)",
-            "浏览器浮雕边 outset2px",
-            "无圆角有边框"
-          ],
+          "w": 88,
+          "h": 28,
+          "bg": "rgba(0, 0, 0, 0)",
+          "bw": 1,
+          "r": "8px",
+          "font": "12px",
+          "reasons": [],
           "text": "调整筛选"
           "text": "调整筛选"
         },
         },
         {
         {
           "tag": "button.cfb",
           "tag": "button.cfb",
-          "w": 86,
-          "h": 24,
-          "bg": "rgb(239, 239, 239)",
-          "bw": 2,
-          "r": "0px",
-          "font": "13.3333px",
-          "reasons": [
-            "灰底rgb(239, 239, 239)",
-            "浏览器浮雕边 outset2px",
-            "无圆角有边框"
-          ],
+          "w": 88,
+          "h": 28,
+          "bg": "rgba(0, 0, 0, 0)",
+          "bw": 1,
+          "r": "8px",
+          "font": "12px",
+          "reasons": [],
           "text": "回到顶部"
           "text": "回到顶部"
         }
         }
       ],
       ],
@@ -1329,35 +1385,14 @@
           "w": 0,
           "w": 0,
           "ow": 3,
           "ow": 3,
           "o": "0px"
           "o": "0px"
-        },
-        {
-          "tag": "button.cfb",
-          "w": 2,
-          "ow": 3,
-          "o": "0px"
-        },
-        {
-          "tag": "button.cfb",
-          "w": 2,
-          "ow": 3,
-          "o": "0px"
-        }
-      ],
-      "blackText": [
-        {
-          "tag": "button.cfb",
-          "text": "调整筛选"
-        },
-        {
-          "tag": "button.cfb",
-          "text": "回到顶部"
         }
         }
       ],
       ],
+      "blackText": [],
       "fontSizes": {
       "fontSizes": {
         "11": 416,
         "11": 416,
-        "12": 35,
+        "12": 36,
         "13": 21,
         "13": 21,
-        "14": 8,
+        "14": 7,
         "16": 105,
         "16": 105,
         "20": 7
         "20": 7
       },
       },
@@ -1381,136 +1416,136 @@
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            300,
-            300,
-            300
+            283,
+            283,
+            283
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards",
           "container": "div.cards",
-          "top": 608,
+          "top": 592,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            279,
-            279,
-            279
+            262,
+            262,
+            262
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards",
           "container": "div.cards",
-          "top": 900,
+          "top": 864,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            246,
-            246,
-            246
+            229,
+            229,
+            229
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards",
           "container": "div.cards",
-          "top": 1156,
+          "top": 1104,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            246,
-            246,
-            246
+            229,
+            229,
+            229
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards",
           "container": "div.cards",
-          "top": 1416,
+          "top": 1348,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            246,
-            246,
-            246
+            229,
+            229,
+            229
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards",
           "container": "div.cards",
-          "top": 1672,
+          "top": 1588,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            246,
-            246,
-            246
+            229,
+            229,
+            229
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards",
           "container": "div.cards",
-          "top": 1932,
+          "top": 1828,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            246,
-            246,
-            246
+            229,
+            229,
+            229
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards",
           "container": "div.cards",
-          "top": 2188,
+          "top": 2072,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            246,
-            246,
-            246
+            229,
+            229,
+            229
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards",
           "container": "div.cards",
-          "top": 2448,
+          "top": 2312,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            376,
-            376,
-            376
+            359,
+            359,
+            359
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards",
           "container": "div.cards",
-          "top": 2836,
+          "top": 2684,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            300,
-            300,
-            300
+            283,
+            283,
+            283
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards",
           "container": "div.cards",
-          "top": 3148,
+          "top": 2976,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            300,
-            300,
-            300
+            283,
+            283,
+            283
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards",
           "container": "div.cards",
-          "top": 3460,
+          "top": 3272,
           "n": 2,
           "n": 2,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            300,
-            300
+            283,
+            283
           ]
           ]
         }
         }
       ],
       ],
       "blanks": [],
       "blanks": [],
       "clickables": 26,
       "clickables": 26,
       "hoverFail": [],
       "hoverFail": [],
-      "scrollH": 3831,
+      "scrollH": 3724,
       "lastContentBottom": 0,
       "lastContentBottom": 0,
       "filterLayout": null,
       "filterLayout": null,
       "wbCols": null,
       "wbCols": null,
@@ -1525,8 +1560,8 @@
     {
     {
       "page": "/reports",
       "page": "/reports",
       "radii": {
       "radii": {
-        "0": 1992,
-        "8": 380,
+        "0": 1984,
+        "8": 388,
         "12": 176,
         "12": 176,
         "circle": 4
         "circle": 4
       },
       },
@@ -1995,32 +2030,24 @@
         },
         },
         {
         {
           "tag": "button.cfb",
           "tag": "button.cfb",
-          "w": 86,
-          "h": 24,
-          "bg": "rgb(239, 239, 239)",
-          "bw": 2,
-          "r": "0px",
-          "font": "13.3333px",
-          "reasons": [
-            "灰底rgb(239, 239, 239)",
-            "浏览器浮雕边 outset2px",
-            "无圆角有边框"
-          ],
+          "w": 52,
+          "h": 28,
+          "bg": "rgba(0, 0, 0, 0)",
+          "bw": 1,
+          "r": "8px",
+          "font": "12px",
+          "reasons": [],
           "text": "调整筛选"
           "text": "调整筛选"
         },
         },
         {
         {
           "tag": "button.cfb",
           "tag": "button.cfb",
-          "w": 86,
-          "h": 24,
-          "bg": "rgb(239, 239, 239)",
-          "bw": 2,
-          "r": "0px",
-          "font": "13.3333px",
-          "reasons": [
-            "灰底rgb(239, 239, 239)",
-            "浏览器浮雕边 outset2px",
-            "无圆角有边框"
-          ],
+          "w": 52,
+          "h": 28,
+          "bg": "rgba(0, 0, 0, 0)",
+          "bw": 1,
+          "r": "8px",
+          "font": "12px",
+          "reasons": [],
           "text": "回到顶部"
           "text": "回到顶部"
         }
         }
       ],
       ],
@@ -2030,35 +2057,14 @@
           "w": 0,
           "w": 0,
           "ow": 3,
           "ow": 3,
           "o": "0px"
           "o": "0px"
-        },
-        {
-          "tag": "button.cfb",
-          "w": 2,
-          "ow": 3,
-          "o": "0px"
-        },
-        {
-          "tag": "button.cfb",
-          "w": 2,
-          "ow": 3,
-          "o": "0px"
-        }
-      ],
-      "blackText": [
-        {
-          "tag": "button.cfb",
-          "text": "调整筛选"
-        },
-        {
-          "tag": "button.cfb",
-          "text": "回到顶部"
         }
         }
       ],
       ],
+      "blackText": [],
       "fontSizes": {
       "fontSizes": {
         "11": 194,
         "11": 194,
-        "12": 68,
+        "12": 69,
         "13": 7,
         "13": 7,
-        "14": 16,
+        "14": 15,
         "16": 37,
         "16": 37,
         "20": 2,
         "20": 2,
         "32": 5
         "32": 5
@@ -2083,49 +2089,49 @@
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            284,
-            284,
-            284
+            271,
+            271,
+            271
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards",
           "container": "div.cards",
-          "top": 908,
+          "top": 892,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            284,
-            284,
-            284
+            271,
+            271,
+            271
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards",
           "container": "div.cards",
-          "top": 1204,
+          "top": 1176,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            263,
-            263,
-            263
+            250,
+            250,
+            250
           ]
           ]
         },
         },
         {
         {
           "container": "div.cards",
           "container": "div.cards",
-          "top": 1480,
+          "top": 1440,
           "n": 3,
           "n": 3,
           "spread": 0,
           "spread": 0,
           "heights": [
           "heights": [
-            231,
-            231,
-            231
+            218,
+            218,
+            218
           ]
           ]
         }
         }
       ],
       ],
       "blanks": [],
       "blanks": [],
       "clickables": 62,
       "clickables": 62,
       "hoverFail": [],
       "hoverFail": [],
-      "scrollH": 1870,
+      "scrollH": 1874,
       "lastContentBottom": 0,
       "lastContentBottom": 0,
       "filterLayout": null,
       "filterLayout": null,
       "wbCols": null,
       "wbCols": null,
@@ -2141,32 +2147,32 @@
     {
     {
       "name": "圆角种类 ≤ 3 且 ∈ {8,12,20}(0 只允许分割线/非容器)",
       "name": "圆角种类 ≤ 3 且 ∈ {8,12,20}(0 只允许分割线/非容器)",
       "ok": false,
       "ok": false,
-      "detail": "容器圆角(≤6px 的量条/色点不计)5:0px×7876、2px×12、8px×1376、12px×736、circlepx×16\n    违禁:2px×12(如 i)"
+      "detail": "容器圆角(≤6px 的量条/色点不计)5:0px×8012、2px×12、8px×1412、12px×756、circlepx×16\n    违禁:2px×12(如 i)"
     },
     },
     {
     {
       "name": "原生外观控件 = 0",
       "name": "原生外观控件 = 0",
       "ok": false,
       "ok": false,
-      "detail": "控件总数 138,原生外观 5\n    / button.cfb 「回到顶部」灰底rgb(239, 239, 239),浏览器浮雕边 outset2px,无圆角有边框\n    /stages button.cfb 「调整筛选」灰底rgb(239, 239, 239),浏览器浮雕边 outset2px,无圆角有边框\n    /stages button.cfb 「回到顶部」灰底rgb(239, 239, 239),浏览器浮雕边 outset2px,无圆角有边框\n    /reports button.cfb 「调整筛选」灰底rgb(239, 239, 239),浏览器浮雕边 outset2px,无圆角有边框\n    /reports button.cfb 「回到顶部」灰底rgb(239, 239, 239),浏览器浮雕边 outset2px,无圆角有边框"
+      "detail": "控件总数 140,原生外观 3\n    / button.cfb 「回到顶部」灰底rgb(239, 239, 239),浏览器浮雕边 outset2px,无圆角有边框\n    /pool button.cfb 「调整筛选」灰底rgb(239, 239, 239),浏览器浮雕边 outset2px,无圆角有边框\n    /pool button.cfb 「回到顶部」灰底rgb(239, 239, 239),浏览器浮雕边 outset2px,无圆角有边框"
     },
     },
     {
     {
       "name": "描边 > 1px = 0",
       "name": "描边 > 1px = 0",
       "ok": false,
       "ok": false,
-      "detail": "> 1px border 5:/ button.cfb 2px、/stages button.cfb 2px、/stages button.cfb 2px、/reports button.cfb 2px、/reports button.cfb 2px"
+      "detail": "> 1px border 3:/ button.cfb 2px、/pool button.cfb 2px、/pool button.cfb 2px"
     },
     },
     {
     {
       "name": "纯黑 rgb(0,0,0) = 0",
       "name": "纯黑 rgb(0,0,0) = 0",
       "ok": false,
       "ok": false,
-      "detail": "5:/ button.cfb「回到顶部」、/stages button.cfb「调整筛选」、/stages button.cfb「回到顶部」、/reports button.cfb「调整筛选」、/reports button.cfb「回到顶部」"
+      "detail": "3:/ button.cfb「回到顶部」、/pool button.cfb「调整筛选」、/pool button.cfb「回到顶部」"
     },
     },
     {
     {
       "name": "字号档 ≤ 5 且无 13.3333px",
       "name": "字号档 ≤ 5 且无 13.3333px",
       "ok": false,
       "ok": false,
-      "detail": "7 档:11×837 / 12×109 / 13×95 / 14×39 / 16×159 / 20×51 / 32×9"
+      "detail": "7 档:11×844 / 12×111 / 13×100 / 14×38 / 16×164 / 20×51 / 32×9"
     },
     },
     {
     {
       "name": "同排卡片高度差 ≤ 2px",
       "name": "同排卡片高度差 ≤ 2px",
       "ok": true,
       "ok": true,
-      "detail": "测了 28 行,超差 0"
+      "detail": "测了 29 行,超差 0"
     },
     },
     {
     {
       "name": "卡内连续空白 ≤ 24px",
       "name": "卡内连续空白 ≤ 24px",
@@ -2176,7 +2182,7 @@
     {
     {
       "name": "可点元素 hover 反馈",
       "name": "可点元素 hover 反馈",
       "ok": true,
       "ok": true,
-      "detail": "可点 165,无反馈 0"
+      "detail": "可点 152,无反馈 0"
     },
     },
     {
     {
       "name": "P1 筛选栏:一个容器 + 分段排序 + 与搜索框等高底对齐",
       "name": "P1 筛选栏:一个容器 + 分段排序 + 与搜索框等高底对齐",
@@ -2186,22 +2192,22 @@
     {
     {
       "name": "P3 工作台两栏等高(差 ≤ 2px)且左卡无 > 32px 连续空白",
       "name": "P3 工作台两栏等高(差 ≤ 2px)且左卡无 > 32px 连续空白",
       "ok": true,
       "ok": true,
-      "detail": "两栏高 [142,142],差 0px,左卡最大连续空白 0px"
+      "detail": "两栏高 [267,267],差 0px,左卡最大连续空白 0px"
     },
     },
     {
     {
       "name": "滚动后视口外可点元素 hover 反馈",
       "name": "滚动后视口外可点元素 hover 反馈",
       "ok": false,
       "ok": false,
-      "detail": "/ 测 47 个,无反馈 0;/pool 测 110 个,无反馈 0;/stages 测 79 个,无反馈 0;/reports 测 89 个,无反馈 1(summary「排序:最新」)"
+      "detail": "/ 测 33 个,无反馈 0;/pool 测 112 个,无反馈 0;/stages 测 66 个,无反馈 0;/reports 测 89 个,无反馈 1(summary「排序:最新」)"
     },
     },
     {
     {
       "name": "P2 同排卡「来源行 / 操作条」底边差 ≤ 2px",
       "name": "P2 同排卡「来源行 / 操作条」底边差 ≤ 2px",
-      "ok": false,
-      "detail": "/ src 20.8px、/ acts 20.8px、/pool src 41.6px、/pool acts 41.6px、/pool src 41.6px、/pool acts 41.6px"
+      "ok": true,
+      "detail": "测了 4 个多卡行:全部 ≤ 2px"
     },
     },
     {
     {
       "name": "页面底部空白 ≤ 120px(视口下 + 滚到底两处都量)",
       "name": "页面底部空白 ≤ 120px(视口下 + 滚到底两处都量)",
-      "ok": false,
-      "detail": "/ 滚动余量 903px / 滚到底视口留白 903px;/pool 滚动余量 1776px / 滚到底视口留白 1776px;/stages 滚动余量 3831px / 滚到底视口留白 3831px;/reports 滚动余量 1870px / 滚到底视口留白 1870px"
+      "ok": true,
+      "detail": "/ 滚动余量 1028px / 滚到底视口留白 48px;/pool 滚动余量 1779px / 滚到底视口留白 48px;/stages 滚动余量 3724px / 滚到底视口留白 104px;/reports 滚动余量 1874px / 滚到底视口留白 104px"
     }
     }
   ],
   ],
   "errors": []
   "errors": []

+ 1 - 0
package.json

@@ -7,6 +7,7 @@
   "sideEffects": false,
   "sideEffects": false,
   "scripts": {
   "scripts": {
     "verify:visual-v12": "node scripts/verify-visual-v12.mjs",
     "verify:visual-v12": "node scripts/verify-visual-v12.mjs",
+    "verify:p2": "node scripts/verify-visual-v12.mjs --tag before --json docs/v12-after-p2.json",
     "config": "node scripts/config.mjs local",
     "config": "node scripts/config.mjs local",
     "vendor": "node scripts/vendor.mjs",
     "vendor": "node scripts/vendor.mjs",
     "serve": "node src/dev/serve.mjs",
     "serve": "node src/dev/serve.mjs",

+ 13 - 15
scripts/verify-visual-v12.mjs

@@ -482,24 +482,22 @@ for (const p of PAGES) {
   await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
   await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
   await page.waitForTimeout(150);
   await page.waitForTimeout(150);
   const viewportGap = await page.evaluate(() => {
   const viewportGap = await page.evaluate(() => {
+    /* 页面底部留白 = 视口底(innerHeight)− 主列里最后一个**画出来的东西**的底边(文字节点 / 卡片 / 按钮)。
+       纯布局容器(main / #view / .shell)的盒子不算内容 —— 它们的盒子本来就到视口底。 */
+    const main = document.querySelector('kf-app').shadowRoot.querySelector('main#view');
+    if (!main) return -1;
     let bottom = 0;
     let bottom = 0;
-    /* 只看主列(main#view)里落在底部那一屏的元素;盒子底部低于视口底的,按「刚好在视口底」算 */
-    /* 只认「真正画出来的东西」:带文字的叶子、卡片、按钮、页脚;纯布局容器(.shell / #view)的盒子不算内容 */
-    const content = (el) => [...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim())
-      || el.matches('svg, img, kf-glass-card, kf-opportunity-card, kf-empty, canvas, .cfoot, button, a[href], .kf-card');
-    const scan = (r) => {
-      for (const el of r.querySelectorAll('*')) {
-        if (!content(el)) continue;
-        const r2 = el.getBoundingClientRect();
-        if (r2.height < 1) continue;
-        if (r2.bottom >= innerHeight - 4) bottom = Math.max(bottom, Math.min(r2.bottom, innerHeight));
-        else if (r2.bottom > bottom) bottom = Math.max(bottom, r2.bottom);
-        if (el.shadowRoot) scan(el.shadowRoot);
+    const walk = (r) => {
+      for (const n of r.querySelectorAll('*')) {
+        if (n.localName === 'script' || n.localName === 'style') continue;
+        const b = n.getBoundingClientRect();
+        if (b.height < 1 || b.bottom <= 0) continue;
+        const own = [...n.childNodes].some((c) => c.nodeType === 3 && c.textContent.trim());
+        if (own) bottom = Math.max(bottom, Math.min(b.bottom, innerHeight));
+        if (n.shadowRoot) walk(n.shadowRoot);
       }
       }
     };
     };
-    const rootMain = document.querySelector('kf-app').shadowRoot.querySelector('main#view');
-    if (!rootMain) return -1;
-    scan(rootMain);
+    walk(main);
     return Math.max(0, Math.round(innerHeight - bottom));
     return Math.max(0, Math.round(innerHeight - bottom));
   });
   });
   const byRow = new Map();
   const byRow = new Map();

+ 8 - 6
src/components/kf-opportunity-card.js

@@ -46,8 +46,9 @@ export class KfOpportunityCard extends LitElement {
     /* v11:高度只由内容决定(v11 10 的 padding 规则换掉了 face 内衬后,「100% + 栅格 stretch」会把三张卡拉成同一高、内容被纵向拉开) */
     /* v11:高度只由内容决定(v11 10 的 padding 规则换掉了 face 内衬后,「100% + 栅格 stretch」会把三张卡拉成同一高、内容被纵向拉开) */
     :host { display: block; min-width: 0; }
     :host { display: block; min-width: 0; }
     /* 卡内三段:标题区(top / title / one,定高内容)/ 指标区 / 决策 + 来源 + 操作。
     /* 卡内三段:标题区(top / title / one,定高内容)/ 指标区 / 决策 + 来源 + 操作。
-       同一行的卡被拉成等高后,多出来的高度放进 .dec 与 .one 之间的弹性间隙(.nums 之后),
-       而不是让来源行「沉底 + 中间一大块空」—— 两行标题 / 两行一句话本身就吃掉了高度差。 */
+       同一行的卡被拉成等高后,多出来的高度放进 .nums 与 .dec 之间的弹性间隙,
+       而不是让来源行「沉底 + 中间一大块空」。标题、一句话、决策行各自都有定高(单 / 两行统一)。
+       .dec 的 margin-top: auto 把 .dec / .src / .acts 三行贴着底边走 —— 同一排卡的「分隔线 / 按钮 / 来源行」落同一 y。 */
     .wrap { height: 100%; display: flex; flex-direction: column; gap: 12px; }
     .wrap { height: 100%; display: flex; flex-direction: column; gap: 12px; }
     .top { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
     .top { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
     .top .right { margin-left: auto; display: flex; gap: 6px; }
     .top .right { margin-left: auto; display: flex; gap: 6px; }
@@ -66,10 +67,11 @@ export class KfOpportunityCard extends LitElement {
     .n .l { font-size: 11px; color: var(--kf-ink-3); margin-top: 4px; letter-spacing: var(--ls-cn-small); }
     .n .l { font-size: 11px; color: var(--kf-ink-3); margin-top: 4px; letter-spacing: var(--ls-cn-small); }
     .n.score .v { color: var(--kf-accent-ink); }
     .n.score .v { color: var(--kf-accent-ink); }
     .n.score .v.none { color: var(--kf-ink-3); text-shadow: none; font-weight: 600; font-size: 16px; }
     .n.score .v.none { color: var(--kf-ink-3); text-shadow: none; font-weight: 600; font-size: 16px; }
-    /* .dec 的 13px 字,行数不定(有决策 / 无决策两种文案)。多出来的高度交给它前面的弹性间隙,
-       .dec / .src / .acts 三行贴着底边走 —— 同一排卡的「分隔线 / 按钮 / 来源行」落同一 y。 */
-    .dec { font-size: 13px; color: var(--kf-ink-2); }
-    .dec { margin-top: auto; }
+    /* 决策行 = 状态 + 一句理由。理由最多两行,理由一长卡就被撑高、同排卡的分隔线跟着错位,
+       所以这里定高两行(理由两行截断);「谁 · 何时」在标题上浮出全文,不靠这一行承载。 */
+    .dec { font-size: 13px; color: var(--kf-ink-2); margin-top: auto; line-height: 1.6; min-height: calc(2 * 1.6em); max-height: calc(2 * 1.6em); overflow: hidden;
+      display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
+    .dec .kf-muted { display: inline; }
     .dec b { color: var(--kf-ink); font-weight: 600; }
     .dec b { color: var(--kf-ink); font-weight: 600; }
     .src { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding-top: 12px; border-top: 1px solid var(--kf-line); font-size: 11px; line-height: 20px; letter-spacing: var(--ls-cn-small); color: var(--kf-ink-3); font-variant-numeric: tabular-nums; }
     .src { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding-top: 12px; border-top: 1px solid var(--kf-line); font-size: 11px; line-height: 20px; letter-spacing: var(--ls-cn-small); color: var(--kf-ink-3); font-variant-numeric: tabular-nums; }
     .src .who { color: var(--kf-ink); font-weight: 600; }
     .src .who { color: var(--kf-ink); font-weight: 600; }

+ 11 - 10
src/pages/report-card.js

@@ -118,7 +118,7 @@ export const CARD_CSS = css`
   .rc .q { font-size: 11px; color: var(--kf-ink-2); line-height: 1.6; display: flex; gap: 8px; padding: 8px 10px; border-radius: var(--r-sm);
   .rc .q { font-size: 11px; color: var(--kf-ink-2); line-height: 1.6; display: flex; gap: 8px; padding: 8px 10px; border-radius: var(--r-sm);
     background: color-mix(in srgb, var(--kf-ink) 4%, transparent); border: 1px solid color-mix(in srgb, var(--kf-ink) 8%, transparent); }
     background: color-mix(in srgb, var(--kf-ink) 4%, transparent); border: 1px solid color-mix(in srgb, var(--kf-ink) 8%, transparent); }
   .rc .q span { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
   .rc .q span { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
-    .rc .src { display: flex; align-items: center; gap: 6px; font-size: 11px; line-height: 1.4; color: var(--kf-ink-2); margin-top: auto; padding-top: 12px; border-top: 1px solid var(--kf-line); min-width: 0; flex-wrap: wrap; font-variant-numeric: tabular-nums; }
+  .rc .src { display: flex; align-items: center; gap: 6px; font-size: 11px; line-height: 1.4; color: var(--kf-ink-2); margin-top: auto; padding-top: 12px; border-top: 1px solid var(--kf-line); min-width: 0; flex-wrap: wrap; font-variant-numeric: tabular-nums; }
   .rc .src .av, .rc .src .who, .rc .src .it { flex: none; }
   .rc .src .av, .rc .src .who, .rc .src .it { flex: none; }
   /* 手机(任务书 1.3):来源四要素拆两行 —— 第一行「谁 + Agent」,第二行「方式 + 时间」;图标不变 */
   /* 手机(任务书 1.3):来源四要素拆两行 —— 第一行「谁 + Agent」,第二行「方式 + 时间」;图标不变 */
   .rc .src .l1, .rc .src .l2 { display: contents; }
   .rc .src .l1, .rc .src .l2 { display: contents; }
@@ -164,6 +164,15 @@ export const CARD_CSS = css`
   .row .sc b { color: var(--kf-ink); }
   .row .sc b { color: var(--kf-ink); }
   .row .mono.nowrap { white-space: nowrap; }
   .row .mono.nowrap { white-space: nowrap; }
   .row .up { display: inline-flex; color: var(--kf-ok-text); vertical-align: -0.15em; }
   .row .up { display: inline-flex; color: var(--kf-ok-text); vertical-align: -0.15em; }
+  /* 页脚(v12 P2):一条收尾,不是空白 —— 左:本页数量;右:两个轻操作。
+     下内边距同时吃掉页尾的滚动余量(卡片区下面是一条看得见的收尾,不是往下无限留白)。 */
+  .cfoot { display: flex; align-items: center; gap: 8px; margin: var(--kf-space-card-gap) 0 0; padding: 12px 4px calc(48px + 8px); border-top: 1px solid var(--kf-line);
+    font-size: 12px; color: var(--kf-ink-3); }
+  .cfoot .cfl { margin-right: auto; font-variant-numeric: tabular-nums; }
+  .cfoot .cfb { appearance: none; -webkit-appearance: none; display: inline-flex; align-items: center; gap: 4px; height: 28px; padding: 0 10px; margin: 0;
+    border: 1px solid rgba(255, 255, 255, 0.14); border-radius: var(--r-sm); background: transparent;
+    font: 500 12px/1 var(--kf-font); color: var(--kf-ink-2); cursor: pointer; transition: background var(--kf-dur) var(--kf-ease), color var(--kf-dur) var(--kf-ease); }
+  .cfoot .cfb:hover { background: rgba(255, 255, 255, 0.55); color: var(--kf-ink); }
 `;
 `;
 
 
 /** 证据档位的文字(跟在数字后面当单位,1.2):2 → 「中」 */
 /** 证据档位的文字(跟在数字后面当单位,1.2):2 → 「中」 */
@@ -210,7 +219,7 @@ function badges(f) {
  */
  */
 export function cardFooter({ shown, all, filtered, label = '条成果', onTop = () => {}, onFilter = null }) {
 export function cardFooter({ shown, all, filtered, label = '条成果', onTop = () => {}, onFilter = null }) {
   return html`<footer class="cfoot" data-foot>
   return html`<footer class="cfoot" data-foot>
-    <span class="cfl">${filtered ? t(`筛出 ${shown} / 共 ${all} `) : t(`共 ${all} `)}${label}</span>
+    <span class="cfl">${filtered ? t(`筛出 ${shown} / 共 ${all} `) : t(`共 ${all} `)}${label === null ? '' : String(label)}</span>
     ${onFilter ? html`<button type="button" class="cfb" @click=${onFilter}>${icon('filter', 14)} 调整筛选</button>` : ''}
     ${onFilter ? html`<button type="button" class="cfb" @click=${onFilter}>${icon('filter', 14)} 调整筛选</button>` : ''}
     <button type="button" class="cfb" @click=${onTop}>${icon('up', 14)} 回到顶部</button>
     <button type="button" class="cfb" @click=${onTop}>${icon('up', 14)} 回到顶部</button>
   </footer>`;
   </footer>`;
@@ -298,13 +307,5 @@ export function reportRow({ e, recent = false, onOpen = null }) {
       <span class="mono nowrap">${recent ? html`<span class="up">${icon('up', 11)}</span>` : ''}${whenText(e.at)}</span>
       <span class="mono nowrap">${recent ? html`<span class="up">${icon('up', 11)}</span>` : ''}${whenText(e.at)}</span>
     </div>
     </div>
   }
   }
-  /* 页脚(v12 P2):一条收尾,不是空白 —— 左:本页数量;右:两个轻操作 */
-  .cfoot { display: flex; align-items: center; gap: 8px; margin: var(--kf-space-card-gap) 0 0; padding: 12px 4px; border-top: 1px solid var(--kf-line);
-    font-size: 12px; color: var(--kf-ink-3); }
-  .cfoot .cfl { margin-right: auto; font-variant-numeric: tabular-nums; }
-  .cfb { appearance: none; -webkit-appearance: none; display: inline-flex; align-items: center; gap: 4px; height: 28px; padding: 0 10px; margin: 0;
-    border: 1px solid rgba(255, 255, 255, 0.14); border-radius: var(--r-sm); background: transparent; color: var(--kf-ink-2);
-    font: 500 12px/1 var(--kf-font); color: var(--kf-ink-2); cursor: pointer; transition: background var(--kf-dur) var(--kf-ease), color var(--kf-dur) var(--kf-ease); }
-  .cfb:hover { background: rgba(255, 255, 255, 0.55); color: var(--kf-ink); }
 `;
 `;
 }
 }

+ 5 - 0
src/pages/view-pool.js

@@ -14,6 +14,7 @@ import { navigate } from '../utils/router.js';
 import { ref } from 'lit/directives/ref.js';
 import { ref } from 'lit/directives/ref.js';
 import { t, head, section, VIEW_CSS } from './shared.js';
 import { t, head, section, VIEW_CSS } from './shared.js';
 import { icon } from '../components/icons.js';
 import { icon } from '../components/icons.js';
+import { cardFooter } from './report-card.js';
 import './filter-sheet.js';
 import './filter-sheet.js';
 
 
 /* 手机筛选弹层:机会卡池的五维;「状态」是单选维度(同一时间只能是候选 / 已立项 / 已否决之一) */
 /* 手机筛选弹层:机会卡池的五维;「状态」是单选维度(同一时间只能是候选 / 已立项 / 已否决之一) */
@@ -226,6 +227,10 @@ export class ViewPool extends LitElement {
           @card-open=${(e) => this.dispatchEvent(new CustomEvent('card-open', { detail: e.detail, bubbles: true, composed: true }))}
           @card-open=${(e) => this.dispatchEvent(new CustomEvent('card-open', { detail: e.detail, bubbles: true, composed: true }))}
           @status-change=${this.#changeStatus}></kf-opportunity-card>`)}
           @status-change=${this.#changeStatus}></kf-opportunity-card>`)}
       </div>` : html`<kf-glass-card surface="2"><kf-empty title="没有符合条件的卡" hint="试试点上面的「清除全部条件」,或换一个排序看看。" action-label="清除全部条件" @empty-action=${() => navigate('/pool', { replace: true })}></kf-empty></kf-glass-card>`}
       </div>` : html`<kf-glass-card surface="2"><kf-empty title="没有符合条件的卡" hint="试试点上面的「清除全部条件」,或换一个排序看看。" action-label="清除全部条件" @empty-action=${() => navigate('/pool', { replace: true })}></kf-empty></kf-glass-card>`}
+      ${/* v12 P2 (b):底部收尾 —— 条数 + 回筛选栏 + 回顶部(与报告中心同一个页脚组件) */ ''}
+      ${cardFooter({ shown: filtered.length, all: rows.length, filtered: active, label: '张机会卡',
+        onFilter: () => this.renderRoot.querySelector('.filters')?.scrollIntoView({ behavior: 'smooth', block: 'start' }),
+        onTop: () => window.scrollTo({ top: 0, behavior: 'smooth' }) })}
       </div>
       </div>
     `;
     `;
   }
   }

+ 16 - 7
src/pages/view-workbench.js

@@ -171,7 +171,7 @@ export class ViewWorkbench extends LitElement {
     return html`
     return html`
       ${head('我的推进', '五阶段进度与现场状态', html`<a class="go" href=${href('/pool')} data-link>机会卡池 →</a>`)}
       ${head('我的推进', '五阶段进度与现场状态', html`<a class="go" href=${href('/pool')} data-link>机会卡池 →</a>`)}
       <div class="grid2 kf-enter" style="margin-top:0; --i:0">
       <div class="grid2 kf-enter" style="margin-top:0; --i:0">
-        <kf-glass-card surface="2">${this.#mine(me, pool)}</kf-glass-card>
+        <kf-glass-card surface="2">${this.#mine(me, pool, live)}</kf-glass-card>
         <kf-glass-card surface="2" tone=${live.stalled.length ? 'danger' : 'ok'}>
         <kf-glass-card surface="2" tone=${live.stalled.length ? 'danger' : 'ok'}>
           <div class="who"><h3>${live.stalled.length ? t(`${live.stalled.length} 人卡住`) : '现场正常'}</h3>
           <div class="who"><h3>${live.stalled.length ? t(`${live.stalled.length} 人卡住`) : '现场正常'}</h3>
             <a class="go" style="margin-left:auto" href=${href('/pool')} data-link aria-label="去机会卡池">${icon('arrow', 14)}</a></div>
             <a class="go" style="margin-left:auto" href=${href('/pool')} data-link aria-label="去机会卡池">${icon('arrow', 14)}</a></div>
@@ -198,13 +198,22 @@ export class ViewWorkbench extends LitElement {
     `;
     `;
   }
   }
 
 
-  #mine(me, pool) {
+  #mine(me, pool, live) {
     if (!this.me) {
     if (!this.me) {
-      // 空态(板块八):说清为什么空 + 给一个动作 + 卡片框不消失(骨架就是上面这张卡)
-      // 空态(v11b 4.2):虚线框只占一行高度(≈38px + 间隔),不再放「五格空进度 + 半张卡高的占位」
-      return html`<div class="who"><div class="avatar">?</div><h3>还没选「我是」</h3></div>
-        <div class="empty"><span>选了自己,就显示你的阶段进度</span>
-          <a class="go" href=${href('/members')} data-link>${icon('user', 14)} 选我</a></div>`;
+      // 空态(板块八):说清为什么空 + 给一个动作 + 卡片框不消失。
+      // v12 P3:「未选」不再是一张空框 —— 左卡改放全员的真实进度(五阶段 × 在阶段人数 / 已交人数),
+      // 选人动作在卡片右上角;选完之后同一张卡换成「我的进度」。左右两卡等高(栅格 stretch)。
+      return html`<div class="who"><div class="avatar">?</div><h3>还没选「我是」</h3>
+          <a class="go" style="margin-left:auto" href=${href('/members')} data-link>${icon('user', 14)} 选我</a></div>
+        <div class="recent-h" style="margin-top:12px"><span>${t(`先看全员:${live.memberCount} 人`)}</span><span style="margin-left:auto;color:var(--kf-ink)">${t(`已交 ${live.submittedPeople} 人`)}</span></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>
+            <span class="n">${r.submitted || '—'}</span>
+            <kf-ring size=${40} .pct=${(r.submitted / Math.max(1, live.memberCount)) * 100} label=${String(r.submitted)}></kf-ring>
+            <span class="f" aria-hidden="true"><i style=${`--v:${(r.submitted / Math.max(1, live.memberCount)).toFixed(2)}`}></i></span></div>`)}
+        </div>
+        <div class="empty" style="margin-top:12px"><span>选了自己,这张卡就换成你的阶段进度与待办</span></div>`;
     }
     }
     const done = STAGES.filter((s) => me.stagesDone.includes(s.code));
     const done = STAGES.filter((s) => me.stagesDone.includes(s.code));
     const max = Math.max(1, ...STAGES.map((s) => me.mine.filter((a) => a.stageObj?.code === s.code).length));
     const max = Math.max(1, ...STAGES.map((s) => me.mine.filter((a) => a.stageObj?.code === s.code).length));