| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- /**
- * layout/kf-nav.js —— <kf-nav> 主导航,两种形态同一份路由表:
- * 桌面 / 平板(≥768px):左侧整片玻璃竖栏(图标 + 中英文 + 计数)
- * 手机(≤767px):底部固定 Tab 栏 —— 5 个主入口,图标上文字下;当前页实心图标 + 品牌色 + 顶部 2px 指示条;
- * 角标缩小压在图标右上;整条是上层玻璃 .78 + blur(32px),底部让出 iOS 手势条(safe-area)。
- * 证据台账在手机上从 Tab 移到「工作台」页脚入口(5 格是单手拇指舒适上限),链接仍是真实 <a>。
- * 只画路由表与计数;跳转交给 page 层的 click 处理(经 router.navigate,保留部署前缀)。
- * 属性:current(应用内路径)/ counts({ pool, stuck })
- */
- import { LitElement, html, css } from 'lit';
- import { useTokens, define } from '../components/base.js';
- import { icon } from '../components/icons.js';
- import { t } from '../components/cjk.js';
- const ITEMS = [
- { path: '/', label: '工作台', en: 'Workbench', icon: 'home' },
- { path: '/pool', label: '机会卡池', en: 'Opportunity pool', icon: 'pool', badge: 'pool', short: '卡池' },
- { path: '/stages', label: '五阶段产出', en: 'Stages S1–S5', icon: 'bars', short: '阶段' },
- { path: '/report', label: '分析报告', en: 'Reports', icon: 'doc', short: '报告' },
- { path: '/ledger', label: '证据台账', en: 'Ledger', icon: 'ledger', mobileHidden: true },
- { path: '/members', label: '成员', en: 'Members', icon: 'users', badge: 'stuck' },
- ];
- export const NAV_ITEMS = ITEMS;
- export class KfNav extends LitElement {
- static properties = {
- current: { type: String },
- counts: { type: Object },
- connection: { type: String },
- };
- constructor() {
- super();
- this.current = '/';
- this.counts = { pool: 0, stuck: 0 };
- this.connection = '';
- }
- connectedCallback() {
- super.connectedCallback();
- useTokens(this);
- }
- static styles = css`
- :host { display: block; height: 100%; }
- /* 大面板玻璃:和 chip 同档(.55 + blur 12),背后光场在动;背景 / 模糊 / 发丝边来自共享表 .kf-glass.is-panel */
- /* 浮层(侧栏)保留玻璃:.58 白面 + blur(28px) saturate(180%) —— 板块六「浮层保留、内容卡减薄」 */
- .side { height: 100%; box-sizing: border-box; display: flex; flex-direction: column; padding: 20px 12px 16px;
- --kf-glass-alpha: rgba(255, 255, 255, 0.58); --kf-glass-blur: 28px; --kf-glass-sat: 180%;
- border-radius: 0 var(--kf-radius-lg) var(--kf-radius-lg) 0; border-left: 0; }
- .brand { display: flex; gap: 12px; align-items: center; padding: 4px 8px 18px; }
- /* 品牌块:v10 收成一块实色强调色(过去是四色 conic 渐变 + 彩色投影,过饱和) */
- .mark { width: 40px; height: 40px; border-radius: 10px; display: grid; place-items: center; color: #fff; font-weight: 600; font-size: 20px;
- background: var(--kf-accent); }
- .name { font-size: 14px; font-weight: 600; color: var(--kf-ink); letter-spacing: -0.01em; }
- .sub { font-size: 11px; color: var(--kf-ink-3); }
- nav { display: grid; gap: 4px; }
- a { display: flex; align-items: center; gap: 12px; height: 40px; padding: 0 12px; border-radius: 10px; color: var(--kf-ink-2);
- text-decoration: none; font-size: 14px; font-weight: 500; position: relative; }
- a { transition: var(--kf-t); }
- a:hover { background: rgba(255,255,255,0.5); }
- a[aria-current="page"] { background: rgba(255,255,255,0.92); color: var(--kf-ink); box-shadow: var(--elev-1); }
- a[aria-current="page"]::before { content: ""; position: absolute; left: 0; top: 11px; bottom: 11px; width: 3px; border-radius: 0 3px 3px 0; background: var(--kf-accent); }
- svg.i { width: 18px; height: 18px; }
- .lbl { flex: 1; min-width: 0; }
- .en { display: block; font-size: 11px; color: var(--kf-ink-3); letter-spacing: 0.03em; font-weight: 500; }
- .short { display: none; }
- .badge { min-width: 20px; height: 20px; padding: 0 6px; border-radius: 6px; display: grid; place-items: center;
- font-size: 11px; font-weight: 500; letter-spacing: 0.02em; background: var(--kf-fill); color: var(--kf-ink-2); font-variant-numeric: tabular-nums; }
- .badge.danger { background-color: var(--kf-s); color: #fff; }
- .foot { margin-top: auto; padding: 12px 10px 0; border-top: 1px solid var(--kf-line); font-size: 11px; color: var(--kf-ink-3); display: flex; align-items: center; gap: 8px; }
- /* 状态点:实色圆点 + 细描边(v11:原来三种颜色的 3px 发光环是「装饰性阴影」) */
- .live { width: 8px; height: 8px; border-radius: 50%; background: #2e7d5b; outline: 3px solid rgba(46,125,91,0.12); flex: none; }
- .live.err { background: var(--kf-s); outline-color: rgba(179,38,30,0.12); }
- .live.loading { background: #b27a00; outline-color: rgba(178,122,0,0.12); animation: pulse 1.2s ease-in-out infinite; }
- @keyframes pulse { 50% { opacity: 0.4; } }
- /* ===== 手机:底部 Tab 栏 ===== */
- @media (max-width: 767px) {
- .side { display: none; }
- .tabbar {
- display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); align-items: start;
- box-sizing: border-box; padding: 6px 4px calc(6px + env(safe-area-inset-bottom, 0px));
- border-radius: 16px 16px 0 0; border-bottom: 0; border-left: 0; border-right: 0;
- background:
- linear-gradient(rgba(255, 255, 255, 0.78), rgba(255, 255, 255, 0.78)) padding-box,
- linear-gradient(0deg, rgba(10, 20, 40, 0.10), rgba(255, 255, 255, 0.95)) border-box;
- -webkit-backdrop-filter: blur(32px) saturate(190%);
- backdrop-filter: blur(32px) saturate(190%);
- box-shadow: var(--kf-shadow-tabbar); /* = elev-3 的上下镜像 */
- }
- .tabbar a { height: 52px; min-width: 0; padding: 0; display: grid; justify-items: center; align-content: center; gap: 4px;
- position: relative; border-radius: 10px; color: var(--kf-ink-3); background: none; box-shadow: none; }
- .tabbar a::before { display: none; }
- .tabbar a:active { background: rgba(18, 32, 64, 0.06); transform: scale(0.985); transition: transform 80ms cubic-bezier(0.4, 0, 0.2, 1); }
- .tabbar .ico { position: relative; width: 24px; height: 24px; display: grid; place-items: center; }
- .tabbar svg.i { width: 22px; height: 22px; stroke-width: 2.1; }
- .tabbar .lbl, .tabbar .en { display: none; }
- .tabbar .short { display: block; font-size: 11px; line-height: 12px; font-weight: 600; letter-spacing: 0.02em; white-space: nowrap; }
- .tabbar a[aria-current="page"] { color: var(--kf-accent-ink); background: none; box-shadow: none; }
- .tabbar .ico .sl { display: none; stroke-width: 2.6; }
- .tabbar a[aria-current="page"] .ico .ln { display: none; }
- .tabbar a[aria-current="page"] .ico .sl { display: block; }
- .tabbar a[aria-current="page"] .ico::after { content: ""; position: absolute; inset: 4px; border-radius: 7px; z-index: -1;
- background: var(--kf-accent-soft); }
- .tabbar a[aria-current="page"]::after { content: ""; position: absolute; top: -6px; left: 50%; width: 20px; height: 2px; margin-left: -10px;
- border-radius: 0 0 2px 2px; background: var(--kf-accent); }
- /* 移动 Tab 角标:不绝对定位出头(v10 实测悬在栏外 20px 处,形成一条悬空的小色块) */
- .tabbar .badge { position: static; margin: -4px 0 0 20px; min-width: 16px; height: 16px; padding: 0 4px; font-size: 11px; border-radius: 6px; }
- }
- @media (min-width: 768px) { .tabbar { display: none; } }
- `;
- #cur() {
- return this.current === '/' ? '/' : `/${this.current.split('/')[1] || ''}`;
- }
- render() {
- const cur = this.#cur();
- // 直接绑字符串:`?aria-current=${...}` 渲染成空的 aria-current="",当前页永远不高亮
- const active = (it) => (it.path === '/' ? cur === '/' : cur === it.path);
- const curAria = (it) => (active(it) ? 'page' : 'false');
- const badge = (it, cls) => {
- const n = it.badge ? this.counts?.[it.badge] : null;
- return n ? html`<span class="badge ${cls} ${it.badge === 'stuck' ? 'danger' : ''}" title=${it.badge === 'stuck' ? '卡住的人' : '池中卡片'} data-count=${it.badge}>${n}</span>` : '';
- };
- return html`
- <aside class="side kf-glass is-panel" aria-label="主导航">
- <div class="brand">
- <span class="mark" aria-hidden="true">康</span>
- <div><div class="name">产品创新看板</div><div class="sub">${t('研发中心 · AI 实战营')}</div></div>
- </div>
- <nav>
- ${ITEMS.map((it) => html`<a href=${it.path} data-link data-path=${it.path} aria-current=${curAria(it)}>
- ${icon(it.icon, 18)}
- <span class="lbl">${it.label}<span class="en">${it.en}</span></span>
- ${badge(it, '')}
- </a>`)}
- </nav>
- <div class="foot" role="status" aria-live="polite">
- <span class="live ${this.connection === 'error' ? 'err' : this.connection === 'loading' ? 'loading' : ''}"></span>
- <span>${this.connection === 'error' ? '数据源连接失败' : this.connection === 'loading' ? '正在同步…' : this.connection || '已同步'}</span>
- </div>
- </aside>
- <nav class="tabbar" aria-label="主导航(手机)" data-tabbar>
- ${ITEMS.filter((it) => !it.mobileHidden).map((it) => html`<a href=${it.path} data-link data-path=${it.path} aria-current=${curAria(it)}
- aria-label=${it.label}>
- <span class="ico">${icon(it.icon, 22, 'ln')}${icon(`${it.icon}Solid`, 22, 'sl')}${badge(it, '')}</span>
- <span class="short">${it.short || it.label}</span>
- </a>`)}
- </nav>
- `;
- }
- }
- define('kf-nav', KfNav);
|