quote-calc.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. /**
  2. * wc/quote-calc.js —— 组件库(wc):<quote-calc> 自定义元素。
  3. *
  4. * 定位:仅浏览器交互层。逻辑(算钱)来自 core `quote()`;I/O 由宿主注入 transport。
  5. * 契约(跨框架就靠这几样):
  6. * 属性 : items(Array, property)、discount(Number, attribute)、disabled(Boolean, attribute)
  7. * 事件 : quote-done / quote-error(bubbles + composed,穿透 Shadow DOM)
  8. * 插槽 : <span slot="title"> 覆盖标题
  9. * 主题 : CSS 变量 --qc-accent / --qc-bg;对外结构用 ::part()
  10. *
  11. * 无构建铁律:无装饰器 —— static properties + customElements.define()(判重)。
  12. * 不 import 任何业务 services/routes;不直接 fetch。
  13. */
  14. import { LitElement, html, css } from 'lit';
  15. import { quote, formatMoney } from '../core/index.js'; // ← 核心库(core)
  16. export class QuoteCalc extends LitElement {
  17. static properties = {
  18. items: { type: Array }, // 复杂值走 property(.items=${...})
  19. discount: { type: Number }, // 简单值可走 attribute(discount="0.1")
  20. disabled: { type: Boolean, reflect: true },
  21. _result: { state: true }, // 内部渲染状态
  22. _busy: { state: true },
  23. _error: { state: true },
  24. };
  25. static styles = css`
  26. :host {
  27. display: block;
  28. --qc-accent: #2b6cb0;
  29. --qc-bg: #fff;
  30. --qc-border: #e2e8f0;
  31. font: 14px/1.5 system-ui, sans-serif;
  32. }
  33. .card { background: var(--qc-bg); border: 1px solid var(--qc-border); border-radius: 10px; padding: 14px; }
  34. h3 { margin: 0 0 10px; font-size: 15px; }
  35. button { background: var(--qc-accent); color: #fff; border: 0; border-radius: 6px; padding: 7px 14px; cursor: pointer; }
  36. button[disabled] { opacity: .5; cursor: not-allowed; }
  37. table { width: 100%; border-collapse: collapse; margin-top: 10px; }
  38. td { padding: 4px 0; }
  39. td.r { text-align: right; }
  40. .total { font-weight: 700; border-top: 1px solid var(--qc-border); }
  41. .err { color: #c53030; margin-top: 8px; }
  42. `;
  43. #transport = null;
  44. constructor() {
  45. super();
  46. this.items = [];
  47. this.discount = 0;
  48. this.disabled = false;
  49. this._result = null;
  50. this._busy = false;
  51. this._error = '';
  52. }
  53. /** 宿主注入 I/O(如 createBrowserTransport());未注入时本地用 core 计算。 */
  54. set transport(t) { this.#transport = t; }
  55. get transport() { return this.#transport; }
  56. render() {
  57. const busy = this._busy || this.disabled;
  58. const cur = this._result?.currency ?? '¥';
  59. return html`
  60. <div class="card">
  61. <h3><slot name="title">报价</slot></h3>
  62. <table part="lines">
  63. ${(this.items || []).map((it) => html`
  64. <tr>
  65. <td>${it.sku} × ${it.qty}</td>
  66. <td class="r">${formatMoney(it.price * it.qty, cur)}</td>
  67. </tr>
  68. `)}
  69. </table>
  70. <p>
  71. <button ?disabled=${busy} @click=${this.#onCalc}>${busy ? '计算中…' : '计算报价'}</button>
  72. <span class="disc">折扣:${Math.round((this.discount || 0) * 100)}%</span>
  73. </p>
  74. ${this._result ? html`
  75. <table part="result">
  76. <tr><td>小计</td><td class="r">${formatMoney(this._result.subtotal, cur)}</td></tr>
  77. <tr><td>优惠</td><td class="r">-${formatMoney(this._result.discount, cur)}</td></tr>
  78. <tr><td>税</td><td class="r">${formatMoney(this._result.tax, cur)}</td></tr>
  79. <tr class="total"><td>合计</td><td class="r">${formatMoney(this._result.total, cur)}</td></tr>
  80. </table>
  81. ` : ''}
  82. ${this._error ? html`<p class="err" part="error" role="alert">${this._error}</p>` : ''}
  83. </div>
  84. `;
  85. }
  86. async #onCalc() {
  87. this._error = '';
  88. this._busy = true;
  89. try {
  90. // 未注入 transport → 本地用 core(演示模式);真实宿主会注入 transport
  91. const result = this.#transport
  92. ? await this.#transport.call({ items: this.items, discount: this.discount })
  93. : quote(this.items, { discount: this.discount });
  94. this._result = result;
  95. /** 子 → 父:语义事件,composed 穿透 Shadow DOM */
  96. this.dispatchEvent(new CustomEvent('quote-done', {
  97. detail: { result },
  98. bubbles: true,
  99. composed: true,
  100. }));
  101. } catch (err) {
  102. this._error = err.message || '计算失败';
  103. this.dispatchEvent(new CustomEvent('quote-error', {
  104. detail: { code: err.code, message: this._error },
  105. bubbles: true,
  106. composed: true,
  107. }));
  108. } finally {
  109. this._busy = false;
  110. }
  111. }
  112. }
  113. // 注册幂等:同一页面重复加载不抛 NotSupportedError
  114. if (!customElements.get('quote-calc')) customElements.define('quote-calc', QuoteCalc);