kf-empty.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. /**
  2. * components/kf-empty.js —— <kf-empty> 空态 / 错误态 / 加载骨架。
  3. * 空态 = 图标 + 一句主文案(≤15 字)+ 一个按钮;不放复合解释。
  4. *
  5. * 属性:title / hint / action-label(有值时渲染按钮,点击派发 empty-action)
  6. * kind: empty | error | loading
  7. */
  8. import { LitElement, html, css } from 'lit';
  9. import { useTokens, define } from './base.js';
  10. import { icon } from './icons.js';
  11. export class KfEmpty extends LitElement {
  12. static properties = {
  13. title: { type: String },
  14. hint: { type: String },
  15. actionLabel: { type: String, attribute: 'action-label' },
  16. kind: { type: String, reflect: true },
  17. };
  18. constructor() {
  19. super();
  20. this.title = '这里将来会出现内容';
  21. this.hint = '';
  22. this.actionLabel = '';
  23. this.kind = 'empty';
  24. }
  25. connectedCallback() {
  26. super.connectedCallback();
  27. useTokens(this);
  28. }
  29. static styles = css`
  30. :host { display: block; min-width: 0; }
  31. /* 空态(v10 任务书 2.9):图标 24px opacity .6 + 一句 13px 说明 + 次级按钮;
  32. 最小高度 = 正常内容高度(168px),切换筛选时页面不跳。 */
  33. .e { min-height: 168px; box-sizing: border-box; display: grid; justify-items: center; align-content: center; justify-content: center;
  34. gap: 8px; padding: 32px 16px; text-align: center; }
  35. .ico { display: grid; place-items: center; width: 24px; height: 24px; opacity: 0.6; }
  36. .ico svg { width: 24px; height: 24px; stroke: var(--kf-ink-3); }
  37. :host([kind="error"]) .ico svg { stroke: var(--kf-danger); }
  38. .t { color: var(--kf-ink); font-weight: 500; font-size: 14px; line-height: 1.4; letter-spacing: 0; overflow-wrap: anywhere; }
  39. .h { color: var(--kf-ink-3); font-size: 14px; max-width: 46ch; min-width: 0; line-height: 1.6; overflow-wrap: anywhere; }
  40. .e .kf-btn { margin-top: 8px; height: 28px; font-size: 14px; }
  41. /* 加载骨架:与空态同高,数据回来不跳 */
  42. .e.load { justify-items: stretch; justify-content: stretch; gap: 12px; padding: 24px; }
  43. .bar { width: 100%; height: 12px; }
  44. `;
  45. render() {
  46. if (this.kind === 'loading') {
  47. return html`<div class="e load" aria-busy="true" aria-live="polite" aria-label=${this.title || '加载中'}>
  48. <div class="kf-skel" style="height:20px;width:40%"></div>
  49. <div class="kf-skel bar"></div><div class="kf-skel bar"></div><div class="kf-skel bar" style="width:64%"></div>
  50. </div>`;
  51. }
  52. const ico = this.kind === 'error' ? icon('alert', 24) : icon('inbox', 24);
  53. return html`
  54. <div class="e" role=${this.kind === 'error' ? 'alert' : 'status'}>
  55. <div class="ico" aria-hidden="true">${ico}</div>
  56. <div class="t">${this.title}</div>
  57. ${this.hint ? html`<div class="h">${this.hint}</div>` : ''}
  58. ${this.actionLabel ? html`<button class="kf-btn" @click=${() => this.dispatchEvent(new CustomEvent('empty-action', { bubbles: true, composed: true }))}>${this.actionLabel}</button>` : ''}
  59. </div>
  60. `;
  61. }
  62. }
  63. define('kf-empty', KfEmpty);