@trustbaseai/shop-ui
v0.2.0
Published
TrustBase 店铺界面组件(框架无关 Web Components):商品列表 / 商品详情与规格选择 / 下单表单 / 我的账户;样式只引用 design tokens,零运行时依赖,不持私钥不签名
Maintainers
Readme
@trustbaseai/shop-ui
TrustBase 店铺界面组件 —— 框架无关的 Web Components,四个最小元素: 商品列表 / 商品详情(规格选择)/ 下单表单 / 我的账户。
- 零运行时依赖:自己写 Custom Elements,不引任何第三方 UI 框架/库(连
@trustbase/shop都不 import)。 - 样式只引用 design tokens(
src/tokens.ts是唯一允许出现视觉字面量的文件)。 - 不持私钥、不签名:组件只渲染 + 抛事件;下单只产出"意愿 + memo"。
- 移动优先:默认 375 宽可用(
demo/index.html就是按 375 做的)。 - 状态:已实现并测试通过(122 个用例);未验证/未做的清单见 §7。
1. 组件契约(元素名/属性/事件都是冻结的)
<tb-goods-list> — 商品列表
| 入参 data-* | 说明 |
|---|---|
| data-goods | JSON 数组:[{ goods_id, seller?, title, summary?, image?, status?, skus: [{ sku_id, title, price_per_unit, stock, spec? }] }] |
| data-state | loading / error / empty / ready(可选,不写按数据推断) |
| data-error | 错误文案(data-state="error" 时显示) |
| 出参事件 | detail |
|---|---|
| tb:goods-select | { goods } —— 被点的商品原样(宿主据此打开详情) |
<tb-goods-detail> — 详情 + 规格选择
| 入参 data-* | 说明 |
|---|---|
| data-goods | JSON 对象(单个商品,结构同上) |
| data-sku-id | 预选规格(可选;缺省 = 有货且最便宜的) |
| data-quantity | 初始数量(可选,默认 1,上限 = 库存) |
| data-state / data-error | 同上 |
| 出参事件 | detail |
|---|---|
| tb:sku-change | { goods_id, sku_id, sku, quantity } |
| tb:goods-buy | { goods_id, seller, sku_id, sku, quantity, price_per_unit, amount_micro_tct } —— 下单意愿的输入 |
单规格商品不渲染规格选择器(规范 §3.5「默认即最优」);缺货/下架时主按钮禁用。
<tb-order-form> — 下单表单
| 入参 data-* | 说明 |
|---|---|
| data-lines | JSON 数组:[{ goods_id?, sku_id, quantity, price_per_unit, title? }](title 只用于显示) |
| data-buyer | 买家 tct1 地址(预填可改) |
| data-seller | 卖家 tct1 地址(只读显示) |
| data-order-ref | 平台侧订单引用(必填:订单号由平台侧给,UI 不自己编) |
| data-session-ref | 会话引用(可选) |
| data-timestamp-ms | 固定毫秒时间戳(可选;重试请传同一个值 → 意愿 hash 不变 → 幂等) |
| data-state / data-error | 同上 |
| 出参事件 | detail |
|---|---|
| tb:order-submit | { buyer, seller, order_ref, timestamp_ms, session_ref, lines: [{ goods_id, sku_id, quantity, price_per_unit }], total_micro_tct, contact: { name, phone, address }, intent } |
| tb:order-error | { code, message } —— 表单不合法或意愿构造失败(此时不发 tb:order-submit) |
intent 由宿主注入的 intentBuilder 产出(属性,不是属性值):
import { buildOrderIntentHash } from '@trustbase/shop';
const form = document.querySelector('tb-order-form');
form.intentBuilder = (draft) =>
buildOrderIntentHash(
draft.lines.map((l) => ({ sku_id: l.sku_id, quantity: l.quantity, price_per_unit: l.price_per_unit })),
{ buyer: draft.buyer, seller: draft.seller, order_ref: draft.order_ref, timestamp_ms: draft.timestamp_ms, session_ref: draft.session_ref }
);
// → detail.intent = { intent, canonical, hash, memo };memo 形如 "tb-auth:<64 位小写 hex>"为什么是注入而不是 import:意愿口径只能有一个实现(@trustbase/shop),
而本包零依赖 —— 与其在 UI 里抄第二份、再指望两份不漂移,不如让调用方把真实现塞进来。
test/format.test.ts 与 test/order-form.test.ts 就是拿 @trustbase/shop 的真实现做的交叉验证。
<tb-account-panel> — 我的账户
| 入参 data-* | 说明 |
|---|---|
| data-account | JSON:{ address, balance_micro_tct?, balance_unit?, pending_orders? } |
| data-orders | JSON 数组(占位):[{ order_id, status, title?, total_micro_tct?, created_at? }] |
| data-state / data-error | 同上 |
| 出参事件 | detail |
|---|---|
| tb:account-action | { action, address? },action ∈ topup \| addresses \| orders \| copy-address |
2. 注册与使用
import { registerShopUi, TOKENS_CSS } from '@trustbaseai/shop-ui';
registerShopUi(); // 幂等;Node/SSR 里是安全空操作
document.head.append(Object.assign(document.createElement('style'), { textContent: TOKENS_CSS })); // 可选:壳层也用同一套变量<tb-goods-list data-goods='[{"goods_id":"g1","title":"茶","skus":[{"sku_id":"s1","title":"250g","price_per_unit":"19900000","stock":5}]}]'></tb-goods-list>事件是 bubbles: true + composed: true,所以在 document 上挂监听也能收到(有测试)。
3. Design tokens(src/tokens.ts,唯一来源)
《TrustBase-设计风格规范-2026-09-22》§1 的数字逐条落成代码 + 断言:
| 类别 | 取值 |
|---|---|
| 灰阶(恰好 5 级) | #1D1D1F / #6E6E73 / #8E8E93 / #C7C7CC / #E5E5EA |
| 主色 / 强调色 | 文字 #1D1D1F、底 #FFFFFF、分区底 #F5F5F7;强调色 #C4633C(只用于主操作与选中态) |
| 语义色(只用于状态) | 成功 #1D8A5B / 警告 #B26A00 / 错误 #C0392B(规范未指定色值,v0 取值写在这里) |
| 字号(≤5 级) | 34 / 22 / 17 / 15 / 13 |
| 字重 · 行高 | 400 · 600;正文 1.5、标题 1.2 |
| 圆角(3 种) | 16 / 10 / 6 |
| 间距(7 档) | 4 / 8 / 12 / 16 / 24 / 32 / 48 |
| 描边或阴影(二选一) | 组件统一用描边(--tb-border-width: 1px,规范未给宽度);--tb-shadow-card 留给将来的浮层 |
| 动效 | 120ms / 200ms,ease-out,只用于状态变化 |
tokens 由 tokensCss(':host') 注入每个组件的 shadow root —— 组件自带主题,不依赖宿主先引全局 CSS。
4. 证据(两条"防退化"的机器检查)
npm test # 122 个用例,8 个套件- 无魔法值(
test/styles.test.ts):扫描四个组件真渲染出的<style>(去掉 tokens 注入块), 禁止#hex/rgb()/hsl()/13px/1.5rem/200ms/ 内联style="; 并断言用到的每个var(--tb-*)都已在 tokens 里定义(写错变量名浏览器不报错,只有这条能发现); 另断言"描边与阴影不在同一条规则里叠加"。src/其余文件也扫(tokens.ts是唯一例外)。 - tokens 与规范一致(
test/tokens.test.ts):逐条断言上表的数字,并做一条通用兜底 —— 所有 token 里出现的 px 取值必须落在允许集合内(防"顺手加个 20px 圆角")。
5. 演示页(375 宽,纯静态,引用构建产物)
npm run build && npm run serve:demo # http://127.0.0.1:4173/demo/demo/index.html + demo/app.js 演示完整流程:商品列表 → 详情(选规格/改数量)→ 下单表单 →
意愿回执(展示要交给商户守护进程的 payload 与 tb-auth:<hash> memo)→ 我的账户(地址/余额/订单占位)。
- 演示页直接 import ESM 构建产物(
../dist/esm/index.js),不需要打包器; 唯一的裸依赖@noble/hashes(shop 的 sha256)用浏览器原生 import map 映射(见index.html注释)。 - 截图(Edge + puppeteer-core,375×812@2x)在
demo/shots/。
6. 公开 API
| 出口 | 内容 |
|---|---|
| 组件 | TbGoodsList TbGoodsDetail TbOrderForm TbAccountPanel TbElement |
| 注册 | registerShopUi() SHOP_UI_ELEMENTS shopUiElementNames() |
| tokens | TOKENS TOKENS_CSS tokensCss() HOST_STYLE AMOUNT_STYLE + 分组常量(gray color fontSize …) |
| 事件名 | SHOP_UI_EVENTS(GOODS_SELECT SKU_CHANGE GOODS_BUY ORDER_SUBMIT ORDER_ERROR ACCOUNT_ACTION) |
| 视图格式化 | formatAmount formatAmountParts multiplyAmounts sumAmounts shortAddress orderStatusLabel goodsMetaLine |
| 类型 | GoodsView GoodsDetailView OrderLineView OrderSubmitDetail IntentBuilder AccountView AccountOrderView UIState |
7. 未验证 / 未做(诚实清单)
- 未验证:手机真机上的表现(只有 Edge 375 宽截图为证);PWA 离线/安装(属
@trustbase/pwa-kit)。 - 未验证:组件与真实索引器数据的对接(现在只吃过演示数据与测试夹具,映射属 WP5)。
- 未做:购物车界面(
@trustbase/shop有购物车模型,但本期 UI 只有"立即购买"一条路 —— 一屏一个主操作);商品图上传/裁剪;库存实时刷新;分页/无限滚动;搜索。 - 未做:收货地址簿(
tb-account-panel的"收货地址"只抛事件); 订单详情页;评价与售后入口;优惠券、多仓。 - 未做:a11y 全面审计(已做基础项:
role/tabindex/键盘 Enter·空格/aria-pressed/aria-label,未过读屏实测)。 - 未做:i18n(文案现在直接是中文)。
- 未做:
shipping_address_hash的加密与提交 —— 由商户守护进程做(界面只把明文交给它)。
8. 开发
npm run build # CJS → dist/ ;ESM → dist/esm/(浏览器可直接 import)
npm test # jest + jsdom(122 用例)
npm run typecheck # tsc --noEmit
npm run serve:demo # 演示页(先 build)src/的相对导入写 ESM 形态(带.js后缀)→dist/esm/可被浏览器直接加载; jest 侧用moduleNameMapper摘后缀。- 测试用 jsdom;
test/setup.cjs补TextEncoder/TextDecoder(jsdom 没有、浏览器有), 这样"组件 +@trustbase/shop真意愿实现"能在同一个测试里跑通。 package.json的sideEffects只留register(customElements.define是副作用入口),其余模块可 tree-shake。
9. 约束(会被测试拦下的写法)
- 组件样式出现字面量 →
test/styles.test.ts失败; src/里 import 任何外部包(含@trustbase/*)→test/isomorphic.test.ts失败;- 源码出现
privateKey/mnemonic/signTransaction/keystore等标识符 → 同款测试失败; - 新增第 6 种圆角/字号/间距 →
test/tokens.test.ts失败。
