| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470 |
- --- a/src/components/kf-opportunity-card.js
- +++ b/src/components/kf-opportunity-card.js
- @@ -4,14 +4,12 @@
- * 一屏读完五要素(任务书 3.1):
- * 1. 优先级 / 品类 / 阶段 2. 机会名 3. 一句话
- * 4. 综合评分(多人平均)/ 证据强度 / 引用条数
- - * 5. 决策记录 + 来源(谁 · 哪个 Agent · 什么方式 · 什么时候)
- + * 5. 来源(谁 · 哪个 Agent · 什么方式 · 什么时候)
- *
- * wc 层不做业务:评分平均、状态文字都由调用方把「已归一化的机会卡对象」传进来(.opp)。
- - * 交互只派发事件,由 page 决定跳转 / 改状态:
- + * 交互只派发阅读事件,由 page 决定跳转:
- * card-open { id } 点整卡阅读报告
- - * status-change { id, status } 状态按钮(悬停出现的快捷操作)
- - *
- - * 状态操作与决策记录不触发整卡跳转。
- + * 决策操作、说明与历史统一在报告阅读页展示。
- */
- import { LitElement, html, css } from 'lit';
- import { useTokens, define } from './base.js';
- @@ -43,28 +41,17 @@
- }
-
- static styles = css`
- - /* v11:高度只由内容决定(v11 10 的 padding 规则换掉了 face 内衬后,「100% + 栅格 stretch」会把三张卡拉成同一高、内容被纵向拉开) */
- - :host { display: block; min-width: 0; }
- - /* 卡内三段:标题区(top / title / one,定高内容)/ 指标区 / 决策 + 来源 + 操作。
- - 同一行的卡被拉成等高后,多出来的高度放进 .nums 与 .dec 之间的弹性间隙,
- - 而不是让来源行「沉底 + 中间一大块空」。标题、一句话、决策行各自都有定高(单 / 两行统一)。
- - .dec 的 margin-top: auto 把 .dec / .src / .acts 三行贴着底边走 —— 同一排卡的「分隔线 / 按钮 / 来源行」落同一 y。 */
- - /* v14 定稿:.wrap 是 slotted 到 <kf-glass-card> 的子元素,父级(宿主 kf-glass-card)高 389px,
- - 但 slotted 子元素默认 flex:0 1 auto,height:100% 在 flex item 上解析不出百分比 → 实测只有内容高 303px,
- - 导致无描述卡片的 .nums margin-top:auto 没有余量可分(底部差 53.6px)。用 box-sizing + min-height 撑满。 */
- - .wrap { flex: 1 1 auto; min-height: 0; box-sizing: border-box; display: flex; flex-direction: column; gap: 12px; }
- + /* 决策区移入报告后,卡面按实际内容收缩,不预留空描述或操作区。 */
- + :host { display: block; min-width: 0; align-self: start; }
- + .wrap { min-width: 0; box-sizing: border-box; display: flex; flex-direction: column; gap: 12px; }
- .top { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
- .top .right { margin-left: auto; display: flex; gap: 6px; }
- /* 长标题不截断(5.6):折两行;正文行高 1.6、字距 0(1.5) */
- .title { margin: 0; font-size: 16px; line-height: 1.35; color: var(--kf-ink); font-weight: 600; letter-spacing: var(--ls-cn-display);
- word-break: keep-all; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-width: 0; }
- /* 行盒修剪 + -webkit-box 行数截断会把行盒压成 ~1 行高(实测 11.6px);按两行高度收(2 × 1.35em) */
- - .title { min-height: calc(2.7em + 0.25em); padding-block: 0.125em; }
- - .one { margin: 0; font-size: 14px; line-height: 1.6; letter-spacing: var(--ls-cn-body); color: var(--kf-ink-2); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-height: calc(2 * 1.6em); }
- - /* 卡面余量摊给无描述卡(v14 定稿):.one 不出文字但保留等高空块(aria-hidden,无文字节点,读屏与搜索都看不见「未填写」)。
- - 实测约束——余量 53px,只在单缝做 margin:auto 那缝必 >24px;平分到多缝则 .dec/.acts 整体上移、与邻卡底边错位 53px。
- - 保留等高空块是唯一能同时满足「不显示文字 / 卡内无大缝 / 底边逐 px 对齐」的做法。 */
- - .one-empty { min-height: calc(2 * 1.6em); }
- + .title { padding-block: 0.125em; }
- + .one { margin: 0; font-size: 14px; line-height: 1.6; letter-spacing: var(--ls-cn-body); color: var(--kf-ink-2); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
- .nums { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
- .n { min-width: 0; box-sizing: border-box; padding: 12px 10px; border: 1px solid #B4C1D5; border-radius: 8px; background: #EEF2F9; }
- /* 三个数值(1.2 单位分层):大数字 = Inter 数字 + 行盒修剪;单位 12px 灰,留 0.12em */
- @@ -74,33 +61,13 @@
- .n .l { font-size: 12px; 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.none { color: var(--kf-ink-3); text-shadow: none; font-weight: 600; font-size: 16px; }
- - /* 决策行 = 状态 + 一句理由。理由最多两行,理由一长卡就被撑高、同排卡的分隔线跟着错位,
- - 所以这里定高两行(理由两行截断);「谁 · 何时」在标题上浮出全文,不靠这一行承载。 */
- - /* v15:卡面不再被拉伸(fill-to 只用于需要等高铺满的调用方),这里不从 auto 取水平余量 */
- - .dec { font-size: 14px; color: var(--kf-ink-2); margin-top: 4px; 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; }
- .src { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding-top: 12px; border-top: 1px solid var(--kf-line); font-size: 12px; 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 .agent { color: var(--kf-accent-ink); font-weight: 500; }
- .src .miss { color: var(--kf-warn-text); }
- - .acts { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
- - .decision-history { font-size: 12px; line-height: 1.7; color: var(--kf-ink-2); }
- - .decision-history summary { cursor: pointer; width: fit-content; padding: 4px 0; }
- - .decision-history ol { margin: 8px 0 0; padding-left: 18px; }
- - .decision-history li { margin-top: 6px; overflow-wrap: anywhere; }
- - .st { display: inline-flex; border: 1px solid var(--kf-line-strong); border-radius: var(--r-sm); overflow: hidden; background: var(--kf-surface); }
- - .st button { border: 0; background: transparent; font: 500 12px/1 var(--kf-font); color: var(--kf-ink-2); padding: 0 10px; height: 26px; cursor: pointer; transition: background var(--kf-dur) var(--kf-ease); }
- - .st button:hover { background: var(--kf-surface-btn); }
- - .st button[aria-pressed="true"]:hover { background: var(--kf-ink); }
- - .st button[aria-pressed="true"] { background: var(--kf-ink); color: #fff; }
- - .st button[aria-pressed="true"].ok { background: #0f5232; }
- - .st button[aria-pressed="true"].no { background: #8a1a14; }
- - .sample { font-size: 12px; line-height: 1; color: #5c4300; background: rgba(255,236,170,0.55); border: 1px solid rgba(170,130,0,0.35); border-radius: var(--r-sm); padding: 1px 6px; }
- :host([compact]) .title { font-size: 14px; }
- :host([compact]) .nums .v { font-size: 16px; }
- - /* 手机(任务书 1.3 / 1.5 / 2.4):标题 17/600、一句话 15/1.55、大数字 24/700、按钮 44 触摸高 */
- + /* 手机保留指标布局,来源拆成两行。 */
- @media (max-width: 767px) {
- .wrap { gap: 12px; }
- .title { font-size: 16px; font-weight: 600; line-height: 1.4; }
- @@ -115,13 +82,6 @@
- .src .l1, .src .l2 { display: flex; gap: 6px; flex-wrap: wrap; }
- .src .l2 { color: var(--kf-ink-2); }
- .src .who, .src .agent { white-space: nowrap; }
- - .dec { font-size: 14px; }
- - .acts { gap: 8px; }
- - .st { height: 44px; }
- - .st button { height: 44px; padding: 0 14px; font-size: 14px; }
- - .st button:active { background: var(--kf-surface-btn); }
- - .decision-history summary { min-height: 44px; display: flex; align-items: center; }
- - .sample { font-size: 12px; padding: 3px 8px; }
- }
- @media (max-width: 359px) {
- .nums { gap: 6px; }
- @@ -137,43 +97,25 @@
- this.dispatchEvent(new CustomEvent('card-open', { detail: { id: this.opp.id }, bubbles: true, composed: true }));
- };
-
- - #status(status) {
- - return (e) => {
- - e.preventDefault();
- - e.stopPropagation();
- - this.dispatchEvent(new CustomEvent('status-change', { detail: { id: this.opp.id, status }, bubbles: true, composed: true }));
- - };
- - }
- -
- render() {
- const o = this.opp;
- if (!o) return html`<div class="wrap"><slot></slot></div>`;
- - // v14 定稿:has-one 必须挂在宿主 <kf-opportunity-card> 上,
- - // 因为 :host(:not([has-one])) .nums 的选择器作用域是宿主自身;
- - // 早前挂在 <kf-glass-card> 上,属性与 :host 不在同一元素,规则永不命中(margin-top 恒为 0px)。
- - if (o.oneLiner) {
- - if (!this.hasAttribute('has-one')) this.setAttribute('has-one', '');
- - } else if (this.hasAttribute('has-one')) {
- - this.removeAttribute('has-one');
- - }
- const s = o.stats || { avg: null, n: 0 };
- const u = o.upload || {};
- - const dec = o.decision;
- return html`
- - <kf-glass-card lift fill-to surface="2" tone=${o.status === 'rejected' ? 'danger' : o.status === 'approved' ? 'ok' : ''} @card-activate=${this.#open}>
- + <kf-glass-card lift surface="2" tone=${o.status === 'rejected' ? 'danger' : o.status === 'approved' ? 'ok' : ''} @card-activate=${this.#open}>
- <div class="wrap">
- <div class="top">
- ${o.priority ? html`<kf-badge kind="priority" .value=${o.priority}></kf-badge>` : ''}
- ${o.category ? html`<kf-badge kind="plain" .value=${o.category}></kf-badge>` : ''}
- ${o.stageObj ? html`<kf-badge kind="stage" .value=${o.stageObj.code} label=${`${o.stageObj.code} ${o.stageObj.name}`}></kf-badge>` : ''}
- <span class="right" style="align-items:center">
- - ${o.seedKey ? html`<span class="sample" title="示例数据(seed):用于演示,不代表真实学员提交">示例</span>` : ''}
- <kf-badge kind="status" .value=${o.status}></kf-badge>
- </span>
- </div>
-
- <h3 class="title">${o.title}</h3>
- - ${o.oneLiner ? html`<p class="one">${o.oneLiner}</p>` : html`<p class="one one-empty" aria-hidden="true"></p>`}
- + ${o.oneLiner ? html`<p class="one">${o.oneLiner}</p>` : ''}
-
- <div class="nums">
- <div class="n score">
- @@ -190,17 +132,6 @@
- </div>
- </div>
-
- - <div class="dec">
- - ${dec
- - ? html`${t('决策:')}<b>${dec.label}</b>${dec.reason ? html`<span class="kf-muted"> · ${dec.reason}</span>` : ''}<span class="kf-muted">${t(`(${dec.by || '未署名'} · ${fmtDate(dec.at, true)})`)}</span>`
- - : html`<span class="kf-muted">${t('决策:尚无决策,')}${o.status === 'candidate' ? '等待评估' : '等待补充记录'}</span>`}
- - </div>
- - ${o.decisionLog?.length ? html`<details class="decision-history" data-no-activate>
- - <summary>${t(`决策记录 ${o.decisionLog.length} 条(最近 3 条)`)}</summary>
- - <ol>${o.decisionLog.slice(-3).reverse().map(x => html`<li><b>${x.label}</b> ${x.reason}
- - <span class="kf-muted">${t(`${x.by || '未署名'} · ${fmtDate(x.at, true)}`)}</span></li>`)}</ol>
- - </details>` : ''}
- -
- <div class="src">
- <span class="l1">
- ${u.uploadedBy ? html`<span class="who">${u.uploadedBy}</span>` : html`<span class="miss">上传人未记录</span>`}
- @@ -212,13 +143,6 @@
- </span>
- </div>
-
- - <div class="acts" data-no-activate>
- - <div class="st" role="group" aria-label="改状态">
- - <button aria-pressed=${o.status === 'candidate' ? 'true' : 'false'} @click=${this.#status('candidate')}>候选</button>
- - <button class="ok" aria-pressed=${o.status === 'approved' ? 'true' : 'false'} @click=${this.#status('approved')}>立项</button>
- - <button class="no" aria-pressed=${o.status === 'rejected' ? 'true' : 'false'} @click=${this.#status('rejected')}>否决</button>
- - </div>
- - </div>
- </div>
- </kf-glass-card>
- `;
- --- a/src/pages/view-report.js
- +++ b/src/pages/view-report.js
- @@ -202,16 +202,20 @@
- </div>`;
- }
-
- + #selectedOpportunity() {
- + return this.artifactId ? reportOpportunity(this.db, this.artifactId, this.opportunityId)
- + : this.db.opportunityById?.[this.opportunityId];
- + }
- +
- #rating() {
- const linked = this.artifactId ? opportunitiesForReport(this.db, this.artifactId) : [];
- - const selected = this.artifactId ? reportOpportunity(this.db, this.artifactId, this.opportunityId)
- - : this.db.opportunityById?.[this.opportunityId];
- + const selected = this.#selectedOpportunity();
- if (!linked.length && !selected && !this.opportunityId) return '';
- return html`<kf-glass-card surface="1" class="r rating-card" id="opportunity-rating" data-opportunity-rating><section class="r-in">
- <h2 class="rating-title">多人评分</h2>
- ${linked.length > 1 || (this.artifactId && this.opportunityId && !selected) ? html`
- - <label class="rating-label" for="rating-opportunity">选择要评分的机会</label>
- - <select id="rating-opportunity" aria-label="评分机会" @change=${e => {
- + <label class="rating-label" for="rating-opportunity">选择评分与决策的机会</label>
- + <select id="rating-opportunity" aria-label="评分与决策机会" @change=${e => {
- const q = new URLSearchParams(this.routeQuery || {});
- q.set('opportunity', e.target.value);
- navigate(`/report/${encodeURIComponent(this.artifactId)}?${q}`, { replace: true });
- @@ -220,7 +224,36 @@
- </select>` : ''}
- ${selected ? html`<p class="rating-target">${selected.title}</p>
- ${keyed(selected.id, html`<kf-score-panel compact .opportunityId=${selected.id} .stats=${selected.stats} .user=${this.me}></kf-score-panel>`)}`
- - : html`<p class="evidence-note">${this.opportunityId ? '当前机会与这份报告没有有效关联,请重新选择后评分。' : '这份报告关联多个机会,请先选择要评分的机会。'}</p>`}
- + : html`<p class="evidence-note">${this.opportunityId ? '当前机会与这份报告没有有效关联,请重新选择后评分和决策。' : '这份报告关联多个机会,请先选择要评分和决策的机会。'}</p>`}
- + </section></kf-glass-card>`;
- + }
- +
- + #decision() {
- + const selected = this.#selectedOpportunity();
- + if (!selected) return '';
- + const decision = selected.decision;
- + const history = selected.decisionLog || [];
- + return html`<kf-glass-card surface="1" class="r decision-card" data-opportunity-decision=${selected.id}><section class="r-in">
- + <div class="decision-heading"><h2 class="rating-title">机会决策</h2><kf-badge kind="status" .value=${selected.status}></kf-badge></div>
- + <p class="rating-target">${selected.title}</p>
- + <div class="decision-actions" role="group" aria-label="机会决策">
- + ${[['candidate', '候选'], ['approved', '立项'], ['rejected', '否决']].map(([status, label]) => html`
- + <button type="button" class=${status} aria-pressed=${selected.status === status ? 'true' : 'false'}
- + @click=${() => this.dispatchEvent(new CustomEvent('status-change', { detail: { id: selected.id, status }, bubbles: true, composed: true }))}>${label}</button>`)}
- + </div>
- + <section class="decision-summary"><h3>决策说明</h3>
- + ${decision ? html`<p class="decision-label">${decision.label}</p>
- + ${decision.reason ? html`<p class="decision-reason">${decision.reason}</p>` : html`<p class="evidence-note">未填写决策说明。</p>`}
- + <p class="decision-meta">${decision.by || '未署名'} · ${fmtDate(decision.at, true) || '时间未记录'}</p>`
- + : html`<p class="evidence-note">${selected.status === 'candidate' ? '尚无决策,等待评估。' : '尚无决策说明,等待补充记录。'}</p>`}
- + </section>
- + <details class="decision-history">
- + <summary>决策记录 ${history.length} 条</summary>
- + ${history.length ? html`<ol>${[...history].reverse().map(item => html`<li>
- + <p class="decision-label">${item.label}</p>${item.reason ? html`<p class="decision-reason">${item.reason}</p>` : ''}
- + <p class="decision-meta">${item.by || '未署名'} · ${fmtDate(item.at, true) || '时间未记录'}</p>
- + </li>`)}</ol>` : html`<p class="evidence-note">暂无决策记录。</p>`}
- + </details>
- </section></kf-glass-card>`;
- }
-
- @@ -239,14 +272,14 @@
- return html`<nav class="reading-nav" aria-label="报告导航"><a href=${href(back)} data-link data-back>← 返回${back.startsWith('/pool') ? '机会卡池' : '列表'}</a>${this.#ratingJump()}</nav>
- <kf-glass-card surface="2" class="cover"><div class="cover-in"><h1>${opportunity.title}</h1><p class="sub">暂无完整报告</p></div></kf-glass-card>
- <div class="layout"><div class="col">
- - <article class="ch kf-card" data-report-pending><h2>报告尚未收录</h2><p class="para">这张机会卡尚未关联可读取的报告正文。以下保留已提交的机会说明与来源,可先阅读并参与评分。</p>
- + <article class="ch kf-card" data-report-pending><h2>报告尚未收录</h2><p class="para">这张机会卡尚未关联可读取的报告正文。以下保留已提交的机会说明与来源,可先阅读并参与评分和决策。</p>
- ${opportunity.oneLiner ? html`<section class="sec"><h4>机会说明</h4><p class="para">${opportunity.oneLiner}</p></section>` : ''}
- <section class="sec"><h4>提交来源</h4><p class="para">${opportunity.upload.uploadedBy || '提交人未记录'} · ${opportunity.upload.agentName || '数字人未记录'} · ${opportunity.upload.methodLabel || '方式未记录'} · ${fmtDate(opportunity.upload.uploadedAt, true) || '时间未记录'}</p></section>
- </article></div>
- <aside class="rail with-rating"><kf-glass-card surface="1" class="r"><div class="r-in"><h2 class="rating-title">证据链</h2>
- ${opportunity.linkedArtifacts.length ? opportunity.linkedArtifacts.map(a => a.missing ? html`<p class="evidence-note">${a.title}</p>` : html`<button class="linked-evidence" @click=${() => this.#artifact(a.id)}>${a.shortTitle || a.title}</button>`)
- : html`<p class="evidence-note">尚未关联成果来源。</p>`}
- - </div></kf-glass-card>${this.#rating()}</aside>
- + </div></kf-glass-card>${this.#rating()}${this.#decision()}</aside>
- </div>`;
- }
-
- @@ -367,6 +400,7 @@
- <button class="kf-btn" @click=${() => this.#artifact(this.artifactId)}>证据链</button>
- </div></kf-glass-card>
- ${this.#rating()}
- + ${this.#decision()}
- </aside>
- </div>
- </div>
- --- a/src/pages/report-styles.js
- +++ b/src/pages/report-styles.js
- @@ -137,6 +137,25 @@
- .rating-label { font-size:12px; color:var(--kf-ink-3); }
- .rating-card { scroll-margin-top:var(--report-anchor,84px); }
- .rating-card select { width:100%; min-width:0; min-height:40px; padding:8px; border:1px solid var(--kf-line-strong); border-radius:8px; background:var(--kf-surface); color:var(--kf-ink); font:12px var(--kf-font); }
- + .decision-heading { display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; }
- + .decision-actions { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); border:1px solid var(--kf-line-strong); border-radius:8px; overflow:hidden; }
- + .decision-actions button { appearance:none; border:0; margin:0; min-width:0; min-height:40px; padding:8px; background:var(--kf-surface); color:var(--kf-ink-2); font:500 13px/1.5 var(--kf-font); cursor:pointer; }
- + .decision-actions button+button { border-left:1px solid var(--kf-line-strong); }
- + .decision-actions button:hover { background:var(--kf-surface-btn); }
- + .decision-actions button[aria-pressed="true"] { color:#fff; background:var(--kf-ink); }
- + .decision-actions button.approved[aria-pressed="true"] { background:#0f5232; }
- + .decision-actions button.rejected[aria-pressed="true"] { background:#8a1a14; }
- + .decision-actions button:focus-visible { outline:2px solid var(--kf-focus); outline-offset:-3px; }
- + .decision-summary { border-top:1px solid var(--kf-line); padding-top:14px; }
- + .decision-summary h3 { margin:0 0 8px; font-size:13px; font-weight:600; color:var(--kf-ink); }
- + .decision-label,.decision-reason,.decision-meta { margin:0; overflow-wrap:anywhere; }
- + .decision-label { font-size:13px; line-height:1.8; font-weight:600; color:var(--kf-ink); }
- + .decision-reason { margin-top:6px; font-size:13px; line-height:1.9; color:var(--kf-ink-2); white-space:pre-wrap; }
- + .decision-meta { margin-top:6px; font-size:12px; line-height:1.8; color:var(--kf-ink-3); }
- + .decision-history { border-top:1px solid var(--kf-line); padding-top:10px; }
- + .decision-history summary { cursor:pointer; min-height:30px; font-size:13px; line-height:1.8; color:var(--kf-ink-2); }
- + .rail .decision-history ol { gap:12px; margin-top:10px; }
- + .decision-history li { border:1px solid var(--kf-line); border-radius:8px; padding:12px; min-width:0; background:var(--kf-surface-btn); }
- .linked-evidence { border:0; padding:6px 0; background:transparent; color:var(--kf-accent-ink); text-align:left; font:12px/1.8 var(--kf-font); cursor:pointer; overflow-wrap:anywhere; }
- .rail ol { display:grid; gap:2px; list-style:none; margin:0; padding:0; }
- .rail a.jump { display:block; padding:9px 0; font-size:12px; line-height:1.9; color:var(--kf-ink-3); text-decoration:none; }
- @@ -147,7 +166,7 @@
- .chain i { font-style:normal; font-size:11px; color:var(--kf-ink-3); }
-
- @media(min-width:1500px) { .cover { --kf-card-pad:32px; } article.ch { padding:32px; } }
- - @media(max-width:1100px) { .layout { grid-template-columns:1fr; } aside.rail { position:static; grid-template-columns:repeat(auto-fit,minmax(min(210px,100%),1fr)); } }
- + @media(max-width:1100px) { .layout { grid-template-columns:1fr; } aside.rail { position:static; grid-template-columns:repeat(auto-fit,minmax(min(210px,100%),1fr)); } .rail.with-rating .rating-card,.rail.with-rating .decision-card { grid-column:1/-1; } }
- @media(max-width:767px) {
- :host { --report-gap:18px; --report-radius:12px; }
- .cover { --kf-card-pad:20px; }
- @@ -183,6 +202,8 @@
- .attachment-grid { grid-template-columns:1fr; }
- .file-entry { padding:16px; }
- .chain { grid-template-columns:repeat(4,1fr); }
- + .decision-actions button { min-height:44px; }
- + .decision-history summary { min-height:44px; display:flex; align-items:center; }
- }
- @media(max-width:360px) { .cover { --kf-card-pad:16px; } article.ch { padding:16px; } .cover h1 { font-size:21px; } .finding { padding:12px; } }
- @media print {
- --- a/src/pages/report-card.js
- +++ b/src/pages/report-card.js
- @@ -185,9 +185,9 @@
- .cfoot .kf-btn { height: 28px; padding: 0 10px; font-size: 12px; }
-
- /* v15:工作台的机会卡(.rc.play)—— 比类型页的成果卡矮一档,只讲「是什么 + 三个数 + 状态」 */
- - .rc.play { display: flex; flex-direction: column; gap: 10px; height: 100%; min-width: 0; }
- + .rc.play { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
- .rc.play h3 { font-size: 16px; }
- - .rc.play .lead { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-height: calc(2 * 1.6em); }
- + .rc.play .lead { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
- .rc.play .top .right { margin-left: auto; }
- .rc.play .opp-st { font-size: 12px; line-height: 20px; padding: 0 8px; border-radius: var(--r-sm); border: 1px solid var(--kf-line-soft); color: var(--kf-ink-2); white-space: nowrap; }
- .rc.play .opp-st.st-approved { color: var(--kf-ok-text); border-color: rgba(21,92,58,0.35); background: rgba(21,92,58,0.06); }
- @@ -197,27 +197,6 @@
- .rc.play .play-nums > div:first-child { border-left: 0; padding-left: 0; }
- .rc.play .play-nums b { display: block; font-size: 20px; font-weight: 600; line-height: 1.1; color: var(--kf-ink); font-variant-numeric: tabular-nums; white-space: nowrap; }
- .rc.play .play-nums span { display: block; margin-top: 2px; font-size: 12px; color: var(--kf-ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
- - .rc.play .play-acts { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
- - /* v16:只有按钮文字的样式放这里;布局(内边距 / 间隙 / 是否撑满)由页面给 —— 同一条 ">
- - 规则写进 CARD_CSS 后,工作台的 flex 页脚(空 span 把「详情 →」推到最右)会被 gap 撑出 84px。 */
- - .card-play .quiet button { white-space: nowrap; }
- - .rc.play .quiet button { border: 0; background: none; padding: 0; height: 20px; font: 500 12px/1 var(--kf-font); color: var(--kf-ink-3); cursor: pointer;
- - text-box-trim: trim-both; text-box-edge: cap alphabetic; }
- - .rc.play .quiet button:hover { color: var(--kf-ink); }
- - .rc.play .quiet button[aria-pressed="true"] { color: var(--kf-accent-ink); font-weight: 600; box-shadow: inset 0 -1px 0 var(--kf-accent); }
- - .rc.play .sp { flex: 1; }
- - .rc.play .more { font-size: 12px; color: var(--kf-ink-3); }
- - @media(max-width:1023px){
- - .rc.play .play-acts{flex-wrap:wrap;gap:6px}
- - .rc.play .quiet{display:flex;flex-wrap:wrap;gap:4px 10px;min-width:0}
- - .rc.play .sp{display:none}
- - .rc.play .more{margin-left:auto;white-space:nowrap}
- - }
- - @media(max-width:767px){
- - .rc.play .quiet{gap:4px}
- - .rc.play .quiet button{min-height:44px;min-width:44px;height:auto;padding:8px 6px;touch-action:manipulation}
- - .rc.play .more{display:inline-flex;align-items:center;min-height:44px}
- - }
- `;
-
- /** 证据档位的文字(跟在数字后面当单位,1.2):2 → 「中」 */
- @@ -308,14 +287,12 @@
-
- /**
- * 工作台用的「安静」机会卡(v15):和卡池那张卡同源(同一个 normalized opportunity),
- - * 只留「这张卡是什么 + 关键数字 + 快捷决策」三块,整卡直接阅读报告。
- + * 只留机会信息、关键数字与当前状态,整卡直接阅读报告;决策在报告侧栏操作。
- * opp 机会卡对象(core normalizeOpportunity 的产物)
- - * status 当前状态(渲染方持有;点击只回 onStatus,由页面去写)
- - * onStatus (status) => void
- - */
- -export function playCard({ opp: o, status, onStatus }) {
- + */
- +export function playCard({ opp: o }) {
- const s = o.stats || { avg: null, n: 0 };
- - const st = status || o.status;
- + const st = o.status;
- return html`
- <div class="rc play card-play" data-opp=${o.id}>
- <div class="top">
- @@ -331,13 +308,6 @@
- <div><b>${o.evidenceLabel}</b><span>证据</span></div>
- <div><b>${o.quoteCount}</b><span>条引用</span></div>
- </div>
- - <!-- v16:三连灰底按钮 → 静默文字行(dr6/dr1 的卡片底部没有按钮组)。字重 500,选中项加粗 + 下划线 -->
- - <div class="play-acts" data-no-activate>
- - <div class="quiet" role="group" aria-label="改状态">
- - ${['candidate', 'approved', 'rejected'].map((k) => html`<button type="button" ?aria-pressed=${st === k}
- - @click=${(e) => { e.preventDefault(); e.stopPropagation(); onStatus?.(k); }}>${OPP_STATUS_LABEL[k]}</button>`)}
- - </div>
- - </div>
- </div>`;
- }
-
- @@ -351,8 +321,7 @@
- const f = cardFacts(e);
- const lead = String(e.lead || '').trim();
- const q = (e.quotes || [])[0];
- - /* 不编文案(v13 二):没有描述就不渲染描述区,`.nolead` 让卡内纵向重新均分。
- - 注意 /pool 的机会卡不是这个组件(它在 kf-opportunity-card.js,一句话恒占两行高)。 */
- + /* 没有描述就不渲染描述区,`.nolead` 调整卡内排布。 */
- return html`
- <div class="rc ${fit ? 'fit' : ''} ${lead ? '' : 'nolead'}" data-entry=${e.id} data-kind=${e.kind} data-stage=${f.stage} data-owner=${e.owner || ''}>
- <div class="top">
- --- a/src/pages/view-pool.js
- +++ b/src/pages/view-pool.js
- @@ -99,7 +99,7 @@
- .frow.toolbar .cfoot[data-foot] { margin: 0 0 0 4px; justify-content: flex-start;
- padding: 0 0 0 12px; border-top: 1px solid var(--kf-line); border-right: 0; border-bottom: 0; border-left: 1px solid var(--kf-line); }
- .sfield input[type="search"] { margin-left: 0; }
- - .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 16px; }
- + .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 16px; align-items: start; }
- .cards kf-opportunity-card.is-focus { outline: 2px solid var(--kf-accent); outline-offset: 3px; border-radius: var(--r-md); }
- .count { font-size: 14px; color: var(--kf-ink-3); font-variant-numeric: tabular-nums; }
- .count b { color: var(--kf-ink); font-variant-numeric: tabular-nums; }
- @@ -182,11 +182,6 @@
- const c = { ...this.#criteria, q };
- // 搜索输入用 replace 节流:每敲一个字不新增历史
- this.#apply(c);
- - }
- -
- - #changeStatus(e) {
- - e.stopPropagation();
- - this.dispatchEvent(new CustomEvent('status-change', { detail: e.detail, bubbles: true, composed: true }));
- }
-
- render() {
- @@ -249,7 +244,7 @@
- ${filtered.length ? html`<div class="cards kf-enter" data-list-ids=${filtered.map((o) => o.id).join(',')}>
- ${filtered.map((o) => html`<kf-opportunity-card key=${o.id} class=${o.id === this.focusId ? 'is-focus' : ''} .opp=${o} base="/pool"
- @card-open=${(e) => { e.stopPropagation(); this.dispatchEvent(new CustomEvent('card-open', { detail: e.detail, bubbles: true, composed: true })); }}
- - @status-change=${this.#changeStatus}></kf-opportunity-card>`)}
- + ></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>
- --- a/src/pages/view-workbench.js
- +++ b/src/pages/view-workbench.js
- @@ -192,11 +192,8 @@
- .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: 16px; align-items: stretch; }
- + .cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; align-items: start; }
- .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%; gap: 8px; }
- .side-in [data-feed] { flex: 1 1 auto; }
- @@ -295,7 +292,7 @@
- <div class="cards-main">
- <div class="feed-head"><span class="lbl">${t('最近动过')}</span>
- <a class="go" href=${href('/pool')} data-link aria-label="全部机会卡">全部 ${pool.total} →</a></div>
- - ${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>`
- + ${play.length ? html`<div class="cards">${play.map((o) => html`<kf-glass-card lift @card-activate=${() => this.#openCard(o.id)}>${playCard({ opp: o })}</kf-glass-card>`)}</div>`
- : html`<p class="feed-empty">还没有机会卡</p>`}
- </div>
- <kf-glass-card class="side-stack" data-role="g" fill-to>
- @@ -319,11 +316,6 @@
- /** 与卡池共用机会报告入口,不再经过详情弹窗。 */
- #openCard(id) {
- this.dispatchEvent(new CustomEvent('opportunity-open', { detail: { id }, bubbles: true, composed: true }));
- - }
- -
- - /** 快捷决策:复用 app 的 status-change(带操作人校验与写入),与卡池同一路径 */
- - #setStatus({ id, status }) {
- - this.dispatchEvent(new CustomEvent('status-change', { detail: { id, status }, bubbles: true, composed: true }));
- }
-
- #stagebar(rows, maxSub, picked) {
|