|
|
@@ -1,13 +1,15 @@
|
|
|
/**
|
|
|
- * pages/view-workbench.js —— <view-workbench> 工作台首页(v15 Bento 重构)。
|
|
|
+ * pages/view-workbench.js —— <view-workbench> 工作台首页(v16:按 docs/v16-research.md 的可执行参数重排)。
|
|
|
*
|
|
|
* 骨架(12 栅格):
|
|
|
- * A 我的推进(8 列,浅色) | B 现场(4 列,深色:全页唯一一块深卡,锚定注意力)
|
|
|
- * C 卡池 | D 最高分 | E 已决策 ← 三张短数据卡,一行等高:标签 / 大数字 / 一行说明
|
|
|
- * F 最近动过(通栏,最多 3 条)
|
|
|
+ * A 我的推进(8 列,浅色,单列列表) | B 现场(4 列,深色:全页唯一一块深卡,锚定注意力)
|
|
|
+ * C/D/E/F 四张短数据卡(各 3 列) ← 标签 / 大数字 28·500 / 一行结论(dr1 的四卡结构)
|
|
|
+ * G 最近动过(机会卡)+ 右侧时间线
|
|
|
*
|
|
|
- * 视觉纪律(v15):浅底嵌深卡、单一强调色(--kf-accent 只给一个关键动作)、字号只留 12/14/16/20/32、
|
|
|
- * 区块之间 32、卡与卡 24、卡内边距 20、卡内 gap 12。不再放柱状装饰(kf-spark 已移除)。
|
|
|
+ * 视觉纪律(v16,数字出处见研究文档 §6):
|
|
|
+ * 字号 12/14/16/20/28/32;字重 400/500/600,大数字 500(dr1/dr6 的大数字比标题细);
|
|
|
+ * 区块之间 32、卡与卡 16(dr1 8 / dr6 14 取中)、卡内边距 20、卡内组与组 ≥ 24;
|
|
|
+ * 深色卡只允许 1 个强调色相(琥珀 = 卡住),阶段进度用灰阶条(dr6 深卡靠形态不靠颜色)。
|
|
|
*/
|
|
|
import { LitElement, css, html } from 'lit';
|
|
|
import { useTokens } from '../components/base.js';
|
|
|
@@ -20,6 +22,7 @@ import { cardFooter, playCard, CARD_CSS } from './report-card.js';
|
|
|
import { icon } from '../components/icons.js';
|
|
|
|
|
|
const TONE = { S1: 'blue', S2: 'violet', S3: 'teal', S4: 'amber', S5: 'rose' };
|
|
|
+const DOT = { approved: '#F4F4F5', rejected: 'rgba(255,255,255,0.28)', candidate: '#B4B4BC' };
|
|
|
/** 「最近动过」的时间写法:今天 / 昨天 / N 天前 / 日期 */
|
|
|
const ago = (iso) => {
|
|
|
const t0 = Date.parse(iso || '');
|
|
|
@@ -27,18 +30,21 @@ const ago = (iso) => {
|
|
|
const d = Math.floor((Date.now() - t0) / 86400000);
|
|
|
if (d <= 0) return '今天';
|
|
|
if (d === 1) return '昨天';
|
|
|
- if (d < 7) return `${d} 天前`;
|
|
|
+ if (d < 7) return t(`${d} 天前`);
|
|
|
const dt = new Date(t0);
|
|
|
return `${dt.getMonth() + 1}月${dt.getDate()}日`;
|
|
|
};
|
|
|
|
|
|
export class ViewWorkbench extends LitElement {
|
|
|
+ #pending = [];
|
|
|
+ #zero = false;
|
|
|
+
|
|
|
static properties = {
|
|
|
db: { type: Object },
|
|
|
me: { type: String },
|
|
|
};
|
|
|
|
|
|
- #mineLatest = null;
|
|
|
+
|
|
|
|
|
|
constructor() {
|
|
|
super();
|
|
|
@@ -52,211 +58,209 @@ export class ViewWorkbench extends LitElement {
|
|
|
}
|
|
|
|
|
|
static styles = [VIEW_CSS, CARD_CSS, css`
|
|
|
- /* 纵向节奏(v15):区块之间 32 / 卡与卡 24 / 卡内元素 12。页头到第一块也是 32(Bento 第一块就是内容)。 */
|
|
|
+ /* 纵向节奏(v16):区块之间 32 / 卡与卡 16(dr1 8、dr6 14,取中)/ 卡内组与组 ≥ 24。 */
|
|
|
.bento { display: flex; flex-direction: column; gap: var(--kf-space-section); margin-top: var(--kf-space-section); }
|
|
|
- .bento > .row { display: grid; gap: var(--kf-space-card-gap); align-items: stretch; }
|
|
|
+ .bento > .row { display: grid; gap: 16px; 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 排) */
|
|
|
+ /* A/B 用 fill-to 吃满行高:卡面 .face 是 flex 列,slotted 的第一层(.me / .live)伸到底 */
|
|
|
.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)); }
|
|
|
+ /* 四张短卡:12 栅格里各占 3 列(1440 下 ≈ 268px,贴 dr1 的 232–249) */
|
|
|
+ .bento > .row.stats { grid-template-columns: repeat(12, minmax(0, 1fr)); }
|
|
|
+ .bento > .row.stats > kf-glass-card { grid-column: span 3; }
|
|
|
+ /* 短卡内边距 12(dr1 量得 18–20、卡宽 249;我们卡宽 270,按面积折算约 12–16):12 时「28 字 / 卡面积」才回到 dr1 的 8.6 附近。
|
|
|
+ dr6 的 20 是它 270–425px 宽卡用的;卡变窄后同一 padding 会把可用面积吃掉一截。 */
|
|
|
+ .bento > .row.stats > kf-glass-card { --kf-card-pad: 16px; }
|
|
|
.p-head { padding-top: var(--kf-space-top); padding-bottom: 0; }
|
|
|
- /* 页头 → Bento 第一块:32(v15 区块之间统一 32) */
|
|
|
.p-head + .bento { margin-top: 32px; }
|
|
|
|
|
|
/* ---------- 通用小件 ---------- */
|
|
|
.lbl { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 500; line-height: 1.4; letter-spacing: var(--ls-cn-small); color: var(--kf-ink-3); }
|
|
|
.dot { width: 6px; height: 6px; border-radius: 50%; flex: none; display: inline-block; }
|
|
|
+ /* 文字链接:14/500,不带底色。dr6/dr1 的卡内动作都是文字 + 箭头,强调色实心按钮只留给深色 / 彩色卡 */
|
|
|
.go { display: inline-flex; align-items: center; gap: 4px; font-size: 14px; font-weight: 500; color: var(--kf-accent-ink); text-decoration: none; }
|
|
|
.go:hover { color: var(--kf-accent); }
|
|
|
- /* 全页唯一一个强调色实心元素:C 卡上的「全部机会卡 →」 */
|
|
|
- .btn-key { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 14px; border-radius: var(--r-sm);
|
|
|
- background: var(--kf-accent); color: #fff; font-size: 14px; font-weight: 500; text-decoration: none; flex: none; }
|
|
|
- .btn-key:hover { background: var(--kf-accent-strong); }
|
|
|
.empty { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; padding: 12px 14px; color: var(--kf-ink-3); font-size: 14px; line-height: 1.5;
|
|
|
border: 1px dashed color-mix(in srgb, var(--kf-ink-3) 24%, transparent); border-radius: var(--r-md); }
|
|
|
.empty .go { margin-left: auto; }
|
|
|
|
|
|
- /* ---------- A 卡:我的推进 ---------- */
|
|
|
- /* 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; }
|
|
|
+ /* ---------- A 卡:我的推进(单列列表,dr6 Onboarding / dr1 Project 的形态) ---------- */
|
|
|
+ .me { display: flex; flex-direction: column; gap: 24px; }
|
|
|
.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;
|
|
|
+ .avatar { width: 40px; height: 40px; 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 h3 { margin: 0; font-size: 16px; line-height: var(--lh-title); font-weight: 600; color: var(--kf-ink); letter-spacing: var(--ls-cn-display); }
|
|
|
+ .me-id .sub { font-size: 12px; line-height: 1.5; color: var(--kf-ink-3); }
|
|
|
.me-id .go { margin-left: auto; }
|
|
|
.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);
|
|
|
+ /* 大数字 28/500(dr1 数字 28、字重比标题细) */
|
|
|
+ .me-nums b { font-size: 28px; font-weight: 500; 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); white-space: nowrap; }
|
|
|
|
|
|
- /* 阶段条:五格一格一句「S1 · 份数」,徽标 + 大数字 + 一根细进度条 */
|
|
|
- .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); }
|
|
|
+ /* 阶段条:五格一格一句「S1 · 份数」,徽标 + 数字 + 细进度条;颜色只用在这排徽标上 */
|
|
|
+ .stagebar { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }
|
|
|
.stg { padding: 12px; border-radius: var(--r-md); background: var(--kf-fill); border: 1px solid transparent;
|
|
|
- /* 阶段格铺满分到的高度带:上面 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 .n { justify-self: end; font-size: 20px; font-weight: 500; line-height: 1; letter-spacing: var(--ls-cn-display); color: var(--gray-12); font-variant-numeric: tabular-nums; }
|
|
|
.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: 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 .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; }
|
|
|
- .pending li .s.dim { color: var(--kf-ink-3); font-weight: 500; }
|
|
|
- .pending li .nm { font-weight: 500; }
|
|
|
- .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); }
|
|
|
- .prog .pv { color: var(--kf-ink); font-variant-numeric: tabular-nums; }
|
|
|
- .prog .pv b { font-weight: 600; }
|
|
|
- .prog .pp { color: var(--kf-ink-3); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
|
|
|
|
- /* ---------- B 卡:现场(深色) ---------- */
|
|
|
- .live { display: flex; flex-direction: column; gap: var(--kf-space-in); align-content: start;
|
|
|
+ /* A 卡下部:一条通栏列表(v16)。每行「左名 + 右值」两端对齐,中间多余宽度归名称,不放两栏大空白。 */
|
|
|
+ .me-list { display: grid; }
|
|
|
+ .sec { display: grid; }
|
|
|
+ .sec + .sec { margin-top: 16px; }
|
|
|
+ .sec .lbl { min-height: 20px; }
|
|
|
+ .row { display: flex; align-items: center; gap: 12px; min-height: 34px; font-size: 14px; border-top: 1px solid var(--kf-line); }
|
|
|
+ .sec .row:first-of-type { border-top: 0; }
|
|
|
+ .row .t { min-width: 0; flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--kf-ink); text-decoration: none; }
|
|
|
+ a.t:hover { color: var(--kf-accent-ink); }
|
|
|
+ .row .meta { flex: none; font-size: 12px; color: var(--kf-ink-3); font-variant-numeric: tabular-nums; }
|
|
|
+ .row .pill { flex: none; display: inline-flex; align-items: center; justify-content: center; min-width: 44px; height: 24px; padding: 0 10px; border-radius: var(--r-sm);
|
|
|
+ border: 1px solid var(--kf-line-soft); font-size: 12px; font-weight: 500; color: var(--kf-accent-ink); text-decoration: none; font-variant-numeric: tabular-nums; }
|
|
|
+ .row .pill:hover { border-color: var(--kf-accent); }
|
|
|
+ .row .pill.dim { color: var(--kf-ink-3); pointer-events: none; }
|
|
|
+ .row .rate { flex: none; display: inline-grid; grid-template-columns: 48px auto; align-items: center; gap: 8px; }
|
|
|
+ .row .rate .bar { height: 4px; border-radius: 2px; background: rgba(16, 24, 40, 0.08); overflow: hidden; }
|
|
|
+ .row .rate .bar i { display: block; height: 100%; border-radius: 2px; background: var(--kf-ink-2); }
|
|
|
+ .row .rate .v { font-size: 12px; color: var(--kf-ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
|
+ .row .rate .v b { font-weight: 600; color: var(--kf-ink); }
|
|
|
+ .me-none { font-size: 14px; color: var(--kf-ink-3); padding-bottom: 6px; }
|
|
|
+ /* 列表是成组信息:放在卡面下半(dr6 深卡 / dr1 Project 卡都是「标题区在上、列表压在底」)。
|
|
|
+ 没选身份时 A 卡内容本来就满,这里不会再生出空白;去掉它就会出现 56px 级别的连续空白。 */
|
|
|
+ .me > .me-list { margin-top: auto; }
|
|
|
+ /* 列表只有一两行时,别把整块 A 卡的高度往深色卡那边抬(B 卡跟行高走) */
|
|
|
+ .me > .me-list .row { min-height: 30px; }
|
|
|
+
|
|
|
+ /* ---------- B 卡:现场(深色;1 个强调色相 = 琥珀;其余灰阶) ---------- */
|
|
|
+ .live { display: flex; flex-direction: column; gap: 20px; 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; }
|
|
|
.live-head .go { margin-left: auto; color: var(--live-ink-3); }
|
|
|
.live-head .go:hover { color: var(--live-ink); }
|
|
|
.live-num { display: flex; align-items: baseline; gap: 8px; color: var(--live-ink); }
|
|
|
- .live-num b { font-size: 32px; font-weight: 600; line-height: 1; letter-spacing: var(--ls-cn-display); font-variant-numeric: tabular-nums; }
|
|
|
+ .live-num b { font-size: 32px; font-weight: 500; line-height: 1; letter-spacing: var(--ls-cn-display); font-variant-numeric: tabular-nums; }
|
|
|
.live-num span { font-size: 14px; color: var(--live-ink-3); }
|
|
|
.live-title { font-size: 16px; font-weight: 600; line-height: 1.3; color: var(--live-ink); }
|
|
|
- .stalled { border-top: 1px solid var(--live-line); }
|
|
|
- .stalled li { display: flex; gap: 8px; align-items: baseline; padding: 8px 0; border-bottom: 1px solid var(--live-line); font-size: 14px; color: var(--live-ink); }
|
|
|
- .stalled li:last-child { border-bottom: 0; }
|
|
|
+ .stalled { display: grid; gap: 12px; margin: -4px 0 0; padding: 0; list-style: none; }
|
|
|
+ .stalled li { display: flex; gap: 8px; align-items: baseline; padding: 8px 0; border-top: 1px solid var(--live-line); font-size: 14px; color: var(--live-ink); }
|
|
|
.stalled .nm { font-weight: 500; }
|
|
|
.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-rank { display: grid; gap: 0; border-top: 1px solid var(--live-line); padding-top: 8px; }
|
|
|
+ .stalled .h { margin-left: auto; font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
|
+ .live-ok { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 500; color: var(--live-ink); margin: 0; }
|
|
|
+ /* 各阶段已交:灰阶细条(轨道 12% 白、填充 90% 白),状态靠长度,不靠颜色 */
|
|
|
+ .live-rank { display: grid; gap: 0; }
|
|
|
.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 { display: grid; grid-template-columns: minmax(0, 1fr) 44px auto; align-items: center; gap: 10px; min-height: 26px; font-size: 12px; color: var(--live-ink); }
|
|
|
+ .rk-k { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--live-ink-3); }
|
|
|
.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); }
|
|
|
+ .rk-n { font-variant-numeric: tabular-nums; color: var(--live-ink-3); text-align: right; }
|
|
|
+ .live-foot { margin-top: auto; font-size: 12px; color: var(--live-ink-3); }
|
|
|
|
|
|
- /* ---------- C/D/E:三张短数据卡 ---------- */
|
|
|
- /* 三张短数据卡:近方形(dr6 的 KPI 行),卡内上标签 / 下数字说明 */
|
|
|
- .stat { display: grid; grid-template-rows: auto 1fr auto; gap: 8px; height: 100%; align-content: start; min-height: 152px; }
|
|
|
- .stat b { align-self: end; }
|
|
|
- .stat .note { min-height: 0; }
|
|
|
- .stat .top { display: flex; align-items: center; gap: 8px; min-height: 24px; }
|
|
|
- .stat .top .go { margin-left: auto; font-size: 14px; }
|
|
|
- .stat b { font-size: 32px; font-weight: 600; line-height: 1; letter-spacing: var(--ls-cn-display); color: var(--gray-12);
|
|
|
- font-variant-numeric: tabular-nums; white-space: nowrap; min-width: 0; }
|
|
|
+ /* ---------- C/D/E/F:四张短数据卡(dr1:标题 16/600 · 数字 28/500 · 结论 12) ---------- */
|
|
|
+ .stat { display: flex; flex-direction: column; gap: 12px; height: 100%; min-height: 132px; }
|
|
|
+ .stat .head { display: flex; align-items: center; gap: 8px; min-height: 24px; }
|
|
|
+ .stat .head .lbl { font-size: 16px; font-weight: 600; letter-spacing: 0; color: var(--kf-ink); }
|
|
|
+ .stat .head .go { margin-left: auto; font-size: 12px; }
|
|
|
+ .stat .num { display: flex; align-items: baseline; gap: 4px; }
|
|
|
+ .stat b { font-size: 28px; font-weight: 500; line-height: 1; letter-spacing: var(--ls-cn-display); color: var(--gray-12);
|
|
|
+ font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
|
.stat b.dim { color: var(--kf-ink-3); }
|
|
|
- .stat b .u { margin-left: 0.14em; font-size: 14px; font-weight: 500; color: var(--kf-ink-3); letter-spacing: 0; }
|
|
|
- .stat .note { font-size: 12px; line-height: 1.5; letter-spacing: var(--ls-cn-small); color: var(--kf-ink-3);
|
|
|
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-height: 18px; }
|
|
|
-
|
|
|
- /* ---------- 底行:左侧机会卡(3 张,与池子同一张卡)+ 右侧「最近动过」时间线 ---------- */
|
|
|
+ .stat .num .u { font-size: 12px; font-weight: 500; color: var(--kf-ink-3); letter-spacing: 0; }
|
|
|
+ /* 结论行:底边对齐,像 dr1 的「Increased from last month」 */
|
|
|
+ .stat .note { margin-top: auto; display: flex; align-items: center; gap: 6px; min-width: 0; font-size: 12px; line-height: 1.5; letter-spacing: var(--ls-cn-small); color: var(--kf-ink-3); }
|
|
|
+ .stat .note .t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
+ /* 趋势徽章:dr1 的「⌃6」= 20×14 小方块 + 箭头 + 数字,绿色描边 */
|
|
|
+ .stat .delta { flex: none; display: inline-flex; align-items: center; gap: 2px; height: 18px; padding: 0 6px; border-radius: 8px; border: 1px solid rgba(47, 125, 91, 0.4); color: #2F7D5B; font-size: 12px; font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums; }
|
|
|
+ .stat .delta.zero { border-color: var(--kf-line-soft); color: var(--kf-ink-3); }
|
|
|
+ /* ---------- 底行:左侧机会卡 + 右侧「最近动过」时间线 ---------- */
|
|
|
.cards-row { grid-template-columns: repeat(12, minmax(0, 1fr)); align-items: start; }
|
|
|
.cards-main { grid-column: span 8; min-width: 0; }
|
|
|
.cards-main .feed-head { margin-bottom: 0; }
|
|
|
.cards-main .cards { margin-top: 12px; }
|
|
|
- .cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--kf-space-card-gap); align-items: stretch; }
|
|
|
+ .cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; align-items: stretch; }
|
|
|
.cards > kf-glass-card { min-width: 0; }
|
|
|
+ /* 机会卡底部「候选 / 已立项 / 已否决」:静默文字行(20px、无底),页脚仍是「左段 + 详情 →」两端对齐 */
|
|
|
+ .cards .quiet { display: inline-flex; gap: 14px; }
|
|
|
+ .cards .quiet button { height: 20px; }
|
|
|
.side-stack { grid-column: span 4; min-width: 0; }
|
|
|
- .side-in { display: flex; flex-direction: column; height: 100%; }
|
|
|
+ .side-in { display: flex; flex-direction: column; height: 100%; gap: 8px; }
|
|
|
.side-in [data-feed] { flex: 1 1 auto; }
|
|
|
.feed-head { display: flex; align-items: center; gap: 8px; min-height: 22px; }
|
|
|
.feed-head .go { margin-left: auto; }
|
|
|
- .feed-head .lbl { font-size: 14px; font-weight: 600; color: var(--kf-ink); }
|
|
|
- /* 时间线:时间在左(12 灰),标题在右;竖向虚线把时间与内容分开 */
|
|
|
+ .feed-head .lbl { font-size: 16px; font-weight: 600; color: var(--kf-ink); }
|
|
|
+ /* 时间线:时间在右(12 灰),标题 14,副行 12 */
|
|
|
.feed { display: grid; margin-top: 8px; }
|
|
|
- .feed li { display: grid; grid-template-columns: 12px minmax(0, 1fr); column-gap: 12px; row-gap: 2px; padding: 10px 0; border-top: 1px solid var(--kf-line); position: relative; }
|
|
|
+ .feed li { display: grid; grid-template-columns: 12px minmax(0, 1fr) auto; column-gap: 12px; row-gap: 2px; padding: 10px 0; border-top: 1px solid var(--kf-line); position: relative; }
|
|
|
.feed li:first-child { border-top: 0; }
|
|
|
- .feed li::before { content: ""; grid-row: 1 / 3; grid-column: 1; width: 7px; height: 7px; border-radius: 50%; border: 1.5px solid var(--kf-line-strong); background: var(--kf-surface); margin-top: 6px; justify-self: center; }
|
|
|
- .feed li:first-child::before { background: var(--kf-ink); border-color: var(--kf-ink); }
|
|
|
+ /* 状态点 = 灰阶明度(已立项最亮 / 候选中灰 / 已否决暗);没有第二种色相 */
|
|
|
+ .feed .fd { grid-row: 1 / 3; grid-column: 1; width: 7px; height: 7px; border-radius: 50%; border: 1px solid rgba(0, 0, 0, 0.14); margin-top: 6px; justify-self: center; }
|
|
|
.feed li a { grid-column: 2; color: var(--kf-ink); font-weight: 500; font-size: 14px; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
.feed li a:hover { color: var(--kf-accent-ink); }
|
|
|
.feed .st { grid-column: 2; font-size: 12px; color: var(--kf-ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
- .feed .tm { position: absolute; right: 0; top: 10px; font-size: 12px; color: var(--kf-ink-3); font-variant-numeric: tabular-nums; }
|
|
|
+ .feed .tm { grid-column: 3; grid-row: 1; font-size: 12px; color: var(--kf-ink-3); font-variant-numeric: tabular-nums; }
|
|
|
.feed-empty { font-size: 14px; color: var(--kf-ink-3); padding: 8px 0 0; }
|
|
|
- /* 抽屉:机会卡全貌 */
|
|
|
- .dr { display: grid; gap: 16px; }
|
|
|
- .dr .act-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
|
- .dr .act-row .go { margin-left: auto; }
|
|
|
.foot-links { display: none; }
|
|
|
|
|
|
- /* 平板:A/B 变 7/5 太挤,直接上下堆;C/D/E 保持一行 */
|
|
|
+ /* 平板:A/B 上下堆;四张短卡两列 */
|
|
|
@media (max-width: 1100px) {
|
|
|
.bento > .row.ab { grid-template-columns: minmax(0, 1fr); }
|
|
|
.bento > .row.ab > kf-glass-card[data-role="a"], .bento > .row.ab > kf-glass-card[data-role="b"] { grid-column: auto; }
|
|
|
+ .bento > .row.stats > kf-glass-card { grid-column: span 6; }
|
|
|
}
|
|
|
- /* 手机 393:A/B 竖排,C/D/E 三卡一行(每卡 3 行文字仍放得下),可点区域 44px */
|
|
|
+ /* 手机 393:A/B 竖排;短卡两列;可点区域 44px */
|
|
|
@media (max-width: 767px) {
|
|
|
.bento { gap: var(--kf-space-card); margin-top: 24px; }
|
|
|
.p-head + .bento { margin-top: 24px; }
|
|
|
.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 { 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; }
|
|
|
- /* 五阶段一行放不下英文名:两行三列(第五格占满第二行左半),英文名折行两行后省略 */
|
|
|
+ .bento > .row.stats { gap: 12px; }
|
|
|
+ /* 手机上两列短卡去掉「全部 →」小链接:结论行留给信息,入口在页头「机会卡池 →」 */
|
|
|
+ .stat .head .go { display: none; }
|
|
|
+ /* 结论行在窄卡里允许两行,不截断成「5 人提交 · 候选 4 · …」 */
|
|
|
+ .stat .note .t { white-space: normal; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
|
|
|
+ .bento > .row.stats > kf-glass-card { grid-column: span 6; }
|
|
|
+ .me { gap: 16px; }
|
|
|
+ .me-id { grid-template-columns: auto minmax(0, 1fr) !important; gap: 2px 10px; }
|
|
|
+ /* 「选我」放回标题那一行右侧(v16 实测:桌面那条 !important 覆盖把手机上的按钮挤到了下一行独立一行) */
|
|
|
+ .me-id > .go { grid-column: 3; grid-row: 1; margin-left: 0; }
|
|
|
+ .me-id .avatar { width: 40px; height: 40px; }
|
|
|
+ .me-nums { grid-column: 1 / -1; justify-self: start; margin-left: 0; width: 100%; gap: 24px; }
|
|
|
+ .me-nums div { display: flex; align-items: baseline; gap: 4px; }
|
|
|
+ /* 五阶段:两行三列(第五格占满第二行左半) */
|
|
|
.stagebar { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
|
|
.stg { padding: 10px; }
|
|
|
.stg .sf { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.35; }
|
|
|
- .me-split { grid-template-columns: minmax(0, 1fr); gap: 12px; }
|
|
|
- .prog li { min-height: 32px; }
|
|
|
- .stat { gap: 6px; }
|
|
|
+ .stat { gap: 8px; min-height: 0; }
|
|
|
+ .stat .head .lbl { font-size: 14px; }
|
|
|
.stat b { font-size: 20px; }
|
|
|
- .stat b .u { font-size: 12px; }
|
|
|
- .stat .top { min-height: 18px; }
|
|
|
- .stat .top .go { display: none; }
|
|
|
/* 手机上一律 14:说明文字在 393 宽下 12px 读不清;只是「文字」不换档,数字仍按档位 */
|
|
|
- .me-none, .empty { font-size: 14px; }
|
|
|
- .btn-key { display: none; }
|
|
|
- /* F:两行 —— 标题占满第一行,作者 / 时间在第二行两端 */
|
|
|
- .feed li { grid-template-columns: minmax(0, 1fr) auto; row-gap: 2px; }
|
|
|
- .feed li a { grid-column: 1 / -1; white-space: normal; }
|
|
|
+ .me-none, .empty, .row { font-size: 14px; }
|
|
|
+ .row { min-height: 44px; }
|
|
|
+ .feed li { grid-template-columns: 12px minmax(0, 1fr); }
|
|
|
+ .feed li a { grid-column: 2; white-space: normal; }
|
|
|
+ .feed .tm { grid-column: 2; grid-row: 3; }
|
|
|
.go, .hd .go, .me-id .go { min-height: 44px; min-width: 44px; justify-content: center; }
|
|
|
- .feed-more { min-height: 44px; align-items: center; }
|
|
|
- /* 底行:手机上卡片一列(与卡池同一张卡,可整张点开抽屉),时间线在下面 */
|
|
|
.bento > .row.cards-row { grid-template-columns: minmax(0, 1fr); gap: var(--kf-space-card); }
|
|
|
.cards-main, .side-stack { grid-column: auto; }
|
|
|
.cards { grid-template-columns: minmax(0, 1fr); gap: var(--kf-space-card); }
|
|
|
.cards-main .feed-head { margin-bottom: 8px; }
|
|
|
- .feed .tm { position: static; grid-column: 2; justify-self: end; grid-row: 1; }
|
|
|
- .pending li { min-height: 44px; }
|
|
|
.empty { font-size: 14px; }
|
|
|
.foot-links { display: flex; gap: 8px; margin-top: var(--kf-space-card); }
|
|
|
- .foot-links a { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 14px; border-radius: var(--r-md); font-size: 14px; font-weight: 600;
|
|
|
+ .foot-links a { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 14px; border-radius: var(--r-md); font-size: 14px; font-weight: 500;
|
|
|
color: var(--kf-accent-ink); text-decoration: none; background: var(--kf-surface); border: 1px solid var(--kf-line-soft); }
|
|
|
}
|
|
|
`];
|
|
|
@@ -267,13 +271,11 @@ export class ViewWorkbench extends LitElement {
|
|
|
const me = forMe(db, this.me);
|
|
|
const pool = poolOverview(db);
|
|
|
const live = liveBoard(db);
|
|
|
- /* 左卡片:我的待评分卡在前,再补最近上传,最多 3 张;右侧时间线取最近 6 条 */
|
|
|
+ /* 左卡片:我的待评分卡在前,再补最近上传,最多 2 张;右侧时间线取最近 5 条 */
|
|
|
const latest = sortByNewest(pool.sorted);
|
|
|
const seen = new Set();
|
|
|
const play = [...(me.pendingScores || []), ...latest].filter((o) => (seen.has(o.id) ? false : seen.add(o.id))).slice(0, 2);
|
|
|
const recent = latest.slice(0, 5);
|
|
|
- /* C 卡「我的环节」:我最近一条成果 */
|
|
|
- this.#mineLatest = me.mine?.[0] || null;
|
|
|
return html`
|
|
|
${head('我的推进', '五阶段进度与现场状态', html`<a class="go" href=${href('/pool')} data-link>机会卡池 →</a>`)}
|
|
|
<div class="bento">
|
|
|
@@ -282,10 +284,11 @@ export class ViewWorkbench extends LitElement {
|
|
|
<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">
|
|
|
- <kf-glass-card data-role="c" fill-to>${this.#stat('open', pool)}</kf-glass-card>
|
|
|
- <kf-glass-card data-role="d" fill-to>${this.#stat('top', pool)}</kf-glass-card>
|
|
|
- <kf-glass-card data-role="e" fill-to>${this.#stat('decided', pool)}</kf-glass-card>
|
|
|
+ <div class="row stats kf-enter" style="--i:1">
|
|
|
+ <kf-glass-card data-role="c" fill-to>${this.#stat('total', pool, live)}</kf-glass-card>
|
|
|
+ <kf-glass-card data-role="d" fill-to>${this.#stat('avg', pool)}</kf-glass-card>
|
|
|
+ <kf-glass-card data-role="e" fill-to>${this.#stat('submitted', pool, live)}</kf-glass-card>
|
|
|
+ <kf-glass-card data-role="f" fill-to>${this.#stat('decided', pool)}</kf-glass-card>
|
|
|
</div>
|
|
|
|
|
|
<div class="row cards-row kf-enter" style="--i:2">
|
|
|
@@ -295,7 +298,7 @@ export class ViewWorkbench extends LitElement {
|
|
|
${play.length ? html`<div class="cards">${play.map((o) => html`<kf-glass-card lift fill-to @card-activate=${() => this.#openCard(o.id)}>${playCard({ opp: o, status: o.status, onStatus: (st) => this.#setStatus({ id: o.id, status: st }) })}</kf-glass-card>`)}</div>`
|
|
|
: html`<p class="feed-empty">还没有机会卡</p>`}
|
|
|
</div>
|
|
|
- <kf-glass-card class="side-stack" data-role="f" fill-to>
|
|
|
+ <kf-glass-card class="side-stack" data-role="g" fill-to>
|
|
|
<div class="side-in">
|
|
|
<div class="feed-head"><span class="lbl">${t('最近动过')}</span>
|
|
|
<a class="go" href=${href('/pool')} data-link aria-label="全部机会卡">全部 →</a></div>
|
|
|
@@ -304,7 +307,7 @@ export class ViewWorkbench extends LitElement {
|
|
|
</kf-glass-card>
|
|
|
</div>
|
|
|
</div>
|
|
|
- ${cardFooter({ shown: play.length, all: pool.total, filtered: false, label: '张机会卡,最近 3 张列在上方', onTop: () => window.scrollTo({ top: 0, behavior: 'smooth' }) })}
|
|
|
+ ${cardFooter({ shown: play.length, all: pool.total, filtered: false, label: t('张机会卡,最近 2 张列在上方'), onTop: () => window.scrollTo({ top: 0, behavior: 'smooth' }) })}
|
|
|
<!-- 只有手机显示:证据台账从底部 Tab 移到工作台页脚(真实链接,部署前缀同源) -->
|
|
|
<nav class="foot-links" aria-label="更多入口">
|
|
|
<a href=${href('/ledger')} data-link>${icon('ledger', 16)} 证据台账</a>
|
|
|
@@ -312,7 +315,6 @@ export class ViewWorkbench extends LitElement {
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
- /** A 卡:选了「我是」→ 我的进度;没选 → 全员进度(v12 定稿:空态也放真实内容,不摆空框) */
|
|
|
/** 打开机会卡全貌:交给 app 的抽屉(机会卡详情 = 决策 / 多人评分 / 来源 / 关联报告),与卡池点开是同一个 */
|
|
|
#openCard(id) {
|
|
|
this.dispatchEvent(new CustomEvent('opportunity-open', { detail: { id }, bubbles: true, composed: true }));
|
|
|
@@ -323,96 +325,95 @@ export class ViewWorkbench extends LitElement {
|
|
|
this.dispatchEvent(new CustomEvent('status-change', { detail: { id, status }, bubbles: true, composed: true }));
|
|
|
}
|
|
|
|
|
|
+ #stagebar(rows, maxSub, picked) {
|
|
|
+ return html`<div class="stagebar" data-allstages>
|
|
|
+ ${rows.map((r, i) => {
|
|
|
+ const ok = picked ? picked.has(r.stage.code) : true;
|
|
|
+ return html`<div class="stg ${TONE[r.stage.code]} ${ok ? '' : 'todo'}" 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>
|
|
|
+ <span class="sf">${r.stage.name}</span>
|
|
|
+ <span class="f" aria-hidden="true"><i style=${`--v:${(r.submitted / maxSub).toFixed(2)}`}></i></span></div>`;
|
|
|
+ })}
|
|
|
+ </div>`;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** A 卡:选了「我是」→ 我的进度;没选 → 全员进度(v12 定稿:空态也放真实内容,不摆空框) */
|
|
|
#mine(me, pool, live) {
|
|
|
+ this.#pending = me?.pendingScores || [];
|
|
|
+ this.#zero = Boolean(this.me) && me.count === 0;
|
|
|
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" 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>
|
|
|
+ <div class="me-id"><div class="avatar q">?</div>
|
|
|
+ <div class="me-title"><h3>还没选「我是」</h3><span class="sub">${t(`先看全员:${live.memberCount} 人 · 已交 ${live.submittedPeople} 人`)}</span></div>
|
|
|
<a class="go" href=${href('/members')} data-link>${icon('user', 14)} ${t('选我')}</a></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>
|
|
|
- <span class="sf">${r.stage.name}</span>
|
|
|
- <span class="f" aria-hidden="true"><i style=${`--v:${(r.submitted / maxSub).toFixed(2)}`}></i></span></div>`)}
|
|
|
- </div>
|
|
|
- <div class="empty"><span>已交 ${live.submittedPeople} 人 · 选了自己,这里换成你的阶段进度与待办</span></div>
|
|
|
+ ${this.#stagebar(live.stages, maxSub, null)}
|
|
|
+ ${this.#meList(pool)}
|
|
|
</div>`;
|
|
|
}
|
|
|
const done = new Set(me.stagesDone);
|
|
|
- const max = Math.max(1, ...STAGES.map((s) => me.mine.filter((a) => a.stageObj?.code === s.code).length));
|
|
|
- const pending = me.pendingScores || [];
|
|
|
- /* 队友动态:没卡住、但一段时间没动静的人(按空闲时长),3 人以内 */
|
|
|
- const idle = (this.db?.members || []).filter((m) => !m.stuck && m.lastActiveAt)
|
|
|
- .sort((a2, b2) => (b2.idleHours || 0) - (a2.idleHours || 0)).slice(0, 3);
|
|
|
+ const rows = STAGES.map((s) => ({ stage: s, submitted: me.mine.filter((a) => a.stageObj?.code === s.code).length }));
|
|
|
+ const maxSub = Math.max(1, ...rows.map((r) => r.submitted));
|
|
|
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 class="me-title"><h3>${me.me}</h3><span class="sub">已完成 ${me.stagesDone.length} / 5 个阶段 · 累计 ${me.count} 份成果</span></div>
|
|
|
+ <div class="me-title"><h3>${me.me}</h3><span class="sub">${t(`已完成 ${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>
|
|
|
- <div><b>${pending.length}</b><span>待评分</span></div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="stagebar">
|
|
|
- ${STAGES.map((s, i) => {
|
|
|
- const n = me.mine.filter((a) => a.stageObj?.code === s.code).length;
|
|
|
- const ok = done.has(s.code);
|
|
|
- return html`<div class="stg kf-enter ${TONE[s.code]} ${ok ? '' : 'todo'}" style=${`--i:${i + 1}`} data-stage=${s.code}>
|
|
|
- <span class="b ${TONE[s.code]}">${s.code}</span>
|
|
|
- <span class="n">${n || '—'}</span>
|
|
|
- <span class="sf">${s.name}</span>
|
|
|
- <span class="f" aria-hidden="true"><i style=${`--v:${(n / max).toFixed(2)}`}></i></span></div>`;
|
|
|
- })}
|
|
|
- </div>
|
|
|
- ${me.count === 0 ? html`<div class="empty"><span>本月还没有你的成果</span>
|
|
|
- <a class="go" href=${href('/stages')} data-link>看五阶段产出 →</a></div>` : ''}
|
|
|
- <div class="me-split">
|
|
|
- <div>
|
|
|
- <div class="lbl">卡池进度</div>
|
|
|
- ${this.#progress(pool)}
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <div class="lbl">待评分${pending.length ? ` ${pending.length}` : ''}</div>
|
|
|
- <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>`}
|
|
|
- ${idle.length ? html`<div class="lbl" style="margin-top:14px">${t('队友动态')}</div>
|
|
|
- <ul class="pending">${idle.map((m) => html`<li><span class="nm">${m.name}</span>
|
|
|
- <span class="d">${m.stageObj ? `${m.stageObj.code} · ${m.stageObj.name}` : t('未开工')}</span>
|
|
|
- <span class="s dim">${m.idleHours == null ? '—' : m.idleHours + 'h'}</span></li>`)}</ul>` : ''}
|
|
|
+ <div><b>${me.pendingScores.length}</b><span>待评分</span></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ ${this.#stagebar(rows, maxSub, done)}
|
|
|
+
|
|
|
+ ${this.#meList(pool)}
|
|
|
</div>`;
|
|
|
}
|
|
|
|
|
|
- /** 卡池进度:候选 / 立项 / 否决 三段 + 百分比(状态用色点,不用整块色底) */
|
|
|
- #progress(pool) {
|
|
|
- const s = pool.byStatus;
|
|
|
+ /**
|
|
|
+ * A 卡下部:一条通栏列表(v16 去掉左右两栏的 32px 中缝)。
|
|
|
+ * 没选「我是」→ 卡池进度(候选 / 已立项 / 已否决各一行,两端对齐)
|
|
|
+ * 选了 → 待评分(卡名吃掉剩余宽度 + 右侧分数胶囊)—— dr1 的「左标题 + 右值」
|
|
|
+ */
|
|
|
+ #meList(pool) {
|
|
|
+ const st = pool.byStatus;
|
|
|
const total = Math.max(1, pool.total);
|
|
|
- const rows = [['候选', s.candidate, '#8A8A93'], ['已立项', s.approved, '#2F7D5B'], ['已否决', s.rejected, '#A1A1AA']];
|
|
|
- return html`<ul class="prog">${rows.map(([k, v, c]) => html`<li>
|
|
|
- <i class="dot" style=${`background:${c}`}></i><span class="pk">${k}</span>
|
|
|
- <span class="pv"><b>${v}</b> 张</span><span class="pp">${Math.round((v / total) * 100)}%</span></li>`)}</ul>`;
|
|
|
+ const pending = this.#pending;
|
|
|
+ if (!this.me) {
|
|
|
+ return html`<div class="me-list"><div class="sec">
|
|
|
+ <div class="lbl">卡池进度</div>
|
|
|
+ ${[['候选', st.candidate], ['已立项', st.approved], ['已否决', st.rejected]].map(([k, v]) => html`<div class="row">
|
|
|
+ <i class="dot" style=${`background:${DOT[k === '已立项' ? 'approved' : k === '已否决' ? 'rejected' : 'candidate']};border:1px solid rgba(0,0,0,.14)`}></i>
|
|
|
+ <span class="t">${k}</span>
|
|
|
+ <span class="rate"><span class="bar"><i style=${`width:${Math.round((v / total) * 100)}%`}></i></span>
|
|
|
+ <span class="v"><b>${v}</b> 张 · ${Math.round((v / total) * 100)}%</span></span></div>`)}
|
|
|
+ </div></div>`;
|
|
|
+ }
|
|
|
+ return html`<div class="me-list">
|
|
|
+ <div class="sec">
|
|
|
+ <div class="lbl">待评分${pending.length ? ` ${pending.length}` : ''}</div>
|
|
|
+ ${pending.length ? pending.slice(0, 4).map((o) => html`<div class="row">
|
|
|
+ <a class="t" href=${href(`/pool/${o.id}`)} data-link title=${o.title}>${t(o.title)}</a>
|
|
|
+ <span class="meta">${o.stats.n ? t(`${o.stats.n} 人 · 均 ${o.stats.avg != null ? Math.round(o.stats.avg) : '—'}`) : '尚未评分'}</span>
|
|
|
+ <a class="pill" href=${href(`/pool/${o.id}`)} data-link aria-label="去评分">${o.stats.avg != null ? Math.round(o.stats.avg) : '去评'}</a></div>`)
|
|
|
+ : html`<div class="me-none">没有等你评分的卡</div>`}
|
|
|
+ ${pending.length > 4 ? html`<a class="go" style="margin-top:8px" href=${href('/pool')} data-link>还有 ${pending.length - 4} 张待评分 →</a>` : ''}
|
|
|
+ </div>
|
|
|
+ </div>`;
|
|
|
}
|
|
|
|
|
|
- /** B 卡:现场。深色底 + 反白字;有卡住的人 → 橙点 + 人数 + 名单;没人卡住 → 绿点 + 正向文案。 */
|
|
|
+ /** B 卡:现场。深色底 + 反白字;唯一强调色 = 琥珀(卡住);阶段进度是灰阶细条 */
|
|
|
#live(live, pool) {
|
|
|
const stalled = live.stalled;
|
|
|
return html`<div class="live" data-stalled-count=${stalled.length}>
|
|
|
<div class="live-head"><span class="lbl">${stalled.length
|
|
|
- ? html`<i class="dot" style="background:#F59E0B"></i>现场`
|
|
|
- : html`<i class="dot" style="background:#4ADE80"></i>现场`}</span>
|
|
|
+ ? html`<i class="dot" style="background:var(--kf-s4)"></i>现场`
|
|
|
+ : html`<i class="dot" style="background:var(--live-ink-3)"></i>现场`}</span>
|
|
|
<a class="go" href=${href('/pool')} data-link aria-label="去机会卡池">${icon('arrow', 14)}</a></div>
|
|
|
${stalled.length ? html`
|
|
|
<div class="live-num"><b>${stalled.length}</b><span>人卡住</span></div>
|
|
|
- <ul class="stalled">${stalled.slice(0, 4).map((s) => html`<li data-stalled=${s.name}>
|
|
|
+ <ul class="stalled">${stalled.slice(0, 3).map((s) => html`<li data-stalled=${s.name}>
|
|
|
<span class="nm">${s.name}</span><span class="meta">${t(`${s.stage} · ${s.msgCount} 条`)}</span>
|
|
|
<span class="h">${s.stuckHours}h</span></li>`)}</ul>`
|
|
|
: html`
|
|
|
@@ -420,9 +421,8 @@ export class ViewWorkbench extends LitElement {
|
|
|
<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>
|
|
|
+ ${live.stages.map((r) => html`<div class="rk">
|
|
|
+ <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>
|
|
|
@@ -430,36 +430,59 @@ export class ViewWorkbench extends LitElement {
|
|
|
</div>`;
|
|
|
}
|
|
|
|
|
|
- /** C/D/E:标签 → 大数字 → 一行说明 */
|
|
|
- #stat(kind, pool) {
|
|
|
- if (kind === 'open') {
|
|
|
+ /**
|
|
|
+ * 短数据卡:标题 16/600 · 大数字 28/500 · 形状 · 结论行 12(dr1 的四卡结构)。
|
|
|
+ * 结论行必须有信息量(和什么比 / 什么占比),不写形容词;没有可用数据时就只写事实。
|
|
|
+ */
|
|
|
+ #stat(kind, pool, live) {
|
|
|
+ /* 选了人、但本月没成果:把「最高分」换成本人的待办(空态也要有信息,不摆空框 —— v12 的原则) */
|
|
|
+ if (kind === 'avg' && this.#zero) return html`<div class="stat" data-kpi="mine">
|
|
|
+ <div class="head"><span class="lbl">本月成果</span><a class="go" href=${href('/stages')} data-link>五阶段 →</a></div>
|
|
|
+ <div class="num"><b>0</b><span class="u">份</span></div>
|
|
|
+ <span class="note"><span class="t">还没交成果,先去五阶段补一份</span></span></div>`;
|
|
|
+ if (kind === 'total') {
|
|
|
return html`<div class="stat" data-kpi="total">
|
|
|
- <div class="top"><span class="lbl">卡池</span><a class="btn-key" href=${href('/pool')} data-link>全部 ${icon('arrow', 14)}</a></div>
|
|
|
- <b>${pool.total}<span class="u">张</span></b>
|
|
|
- <span class="note">${pool.uploaders} 人提交 · ${pool.agents} 个 Agent</span></div>`;
|
|
|
+ <div class="head"><span class="lbl">卡池</span><a class="go" href=${href('/pool')} data-link>全部 →</a></div>
|
|
|
+ <div class="num"><b>${pool.total}</b><span class="u">张</span></div>
|
|
|
+ <span class="note"><span class="t">${t(`${pool.uploaders} 人提交 · 候选 ${pool.byStatus.candidate || 0} · 已立项 ${pool.byStatus.approved || 0}`)}</span></span></div>`;
|
|
|
}
|
|
|
- if (kind === 'top') {
|
|
|
+ if (kind === 'avg') {
|
|
|
const top = pool.top;
|
|
|
const avg = top?.stats?.avg;
|
|
|
+ const scored = pool.sorted.filter((o) => o.stats.avg != null);
|
|
|
+ const mean = scored.length ? Math.round(scored.reduce((a, o) => a + o.stats.avg, 0) / scored.length) : null;
|
|
|
+ const avgDelta = avg != null && mean != null ? { gap: Math.abs(Math.round(avg) - mean), mean, up: Math.round(avg) >= mean } : null;
|
|
|
return html`<div class="stat" data-kpi="avg">
|
|
|
- <div class="top"><span class="lbl">最高分</span></div>
|
|
|
- <b class=${avg == null ? 'dim' : ''}>${avg != null ? Math.round(avg) : '—'}<span class="u">分</span></b>
|
|
|
- <span class="note" title=${top?.title || ''}>${top?.title || '暂无评分'}</span></div>`;
|
|
|
+ <div class="head"><span class="lbl">最高分</span></div>
|
|
|
+ <div class="num"><b class=${avg == null ? 'dim' : ''}>${avg != null ? Math.round(avg) : '—'}</b>${avg != null ? html`<span class="u">分</span>` : ''}</div>
|
|
|
+ <span class="note">${avgDelta ? html`<span class="delta ${avgDelta.up ? '' : 'zero'}">${avgDelta.up ? '↑' : '↓'}${avgDelta.gap}</span>` : ''}
|
|
|
+ <span class="t">${avgDelta ? t(`${avgDelta.up ? '高于' : '低于'}卡池均分 ${avgDelta.mean} · 最高一张`) : '还没有人评分'}</span></span></div>`;
|
|
|
+ }
|
|
|
+ if (kind === 'submitted') {
|
|
|
+ /* 有数据画形状(各阶段已交数),没数据不画 —— 空态照样测得到 */
|
|
|
+ const bars = live?.stages?.map((r) => r.artifacts) || [];
|
|
|
+ const lead = live?.stages?.reduce((a, r) => (r.artifacts > (a?.artifacts ?? -1) ? r : a), null);
|
|
|
+ return html`<div class="stat" data-kpi="submitted">
|
|
|
+ <div class="head"><span class="lbl">已交成果</span></div>
|
|
|
+ <div class="num"><b>${live?.totalSubmitted ?? bars.reduce((a, b) => a + b, 0)}</b><span class="u">份</span></div>
|
|
|
+ <span class="note"><span class="t">${t(`${live?.submittedPeople ?? 0} 人交了 · 共 ${live?.memberCount ?? 0} 人${lead?.stage ? ` · 最多是 ${lead.stage.code} ${lead.stage.name}(${lead.submitted} 份)` : ''}`)}</span></span></div>`;
|
|
|
}
|
|
|
- /* C:我的环节 —— 最近一条成果落在哪个阶段(没选 / 没成果时如实说) */
|
|
|
- const last = this.#mineLatest;
|
|
|
- return html`<div class="stat" data-kpi="stage">
|
|
|
- <div class="top"><span class="lbl">我的环节</span></div>
|
|
|
- <b class=${last ? '' : 'dim'}>${last ? last.stageObj.code : '—'}</b>
|
|
|
- <span class="note">${last ? `${last.stageObj.name} · ${ago(last.createdAt)}` : this.me ? '本月还没有你的成果' : '还没选「我是」'}</span></div>`;
|
|
|
+ /* deciding:还在候选里、没决策的张数(和 stat('total') 互补;替换掉内部视角的「我的环节」) */
|
|
|
+ const n = pool.byStatus.candidate || 0;
|
|
|
+ const done = (pool.byStatus.approved || 0) + (pool.byStatus.rejected || 0);
|
|
|
+ return html`<div class="stat" data-kpi="deciding">
|
|
|
+ <div class="head"><span class="lbl">待决策</span></div>
|
|
|
+ <div class="num"><b class=${n ? '' : 'dim'}>${n}</b><span class="u">张</span></div>
|
|
|
+ <span class="note"><span class="t">${done ? t(`${done} 张已立项 / 否决 · 候选还剩 ${n} 张`) : '还没有已决策的卡'}</span></span></div>`;
|
|
|
}
|
|
|
|
|
|
- /** 右侧「最近动过」:时间线(6 条) */
|
|
|
+ /** 右侧「最近动过」:时间线(5 条) */
|
|
|
#feed(latest, pool) {
|
|
|
return html`<div data-feed>
|
|
|
- ${latest.length ? html`<ul class="feed">${latest.map((o) => html`<li>
|
|
|
+ ${latest.length ? html`<ul class="feed">${latest.map((o) => html`<li data-status=${o.status}>
|
|
|
+ <i class="fd" style=${`background:${DOT[o.status] || DOT.candidate}`} aria-hidden="true"></i>
|
|
|
<a href=${href(`/pool/${o.id}`)} data-link>${o.title}</a>
|
|
|
- <span class="st">${(o.upload?.uploadedBy || '') + (o.stageObj ? ' · ' + o.stageObj.name : '')}</span>
|
|
|
+ <span class="st">${t((o.upload?.uploadedBy || '') + (o.stageObj ? ' · ' + o.stageObj.name : ''))}</span>
|
|
|
<span class="tm">${ago(o.upload?.uploadedAt || o.createdAt)}</span></li>`)}</ul>
|
|
|
<a class="feed-more go" href=${href('/pool')} data-link>看全部 ${pool.total} 张机会卡 →</a>`
|
|
|
: html`<p class="feed-empty">还没有机会卡</p>`}
|