@tbox.cn/app-module-member 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,12 +5,13 @@ import "./member-372B74DY.css";
5
5
  import { CalendarBlank, Phone, Tag, UserCircle } from "@phosphor-icons/react";
6
6
 
7
7
  // src/client/components/ImageWithFallback.tsx
8
- import { useState } from "react";
8
+ import { useEffect, useState } from "react";
9
9
  import { ImageSquare } from "@phosphor-icons/react";
10
10
  import { jsx } from "react/jsx-runtime";
11
11
  function ImageWithFallback(props) {
12
12
  const { src, alt, className, size = 40, loading = "lazy", fallback } = props;
13
13
  const [failed, setFailed] = useState(false);
14
+ useEffect(() => setFailed(false), [src]);
14
15
  if (!src || failed) {
15
16
  return /* @__PURE__ */ jsx("div", { className: `img-fallback ${className ?? ""}`.trim(), "aria-label": alt, role: "img", children: fallback ?? /* @__PURE__ */ jsx(ImageSquare, { weight: "duotone", size: Math.round(size * 0.45) }) });
16
17
  }
@@ -103,7 +104,7 @@ function MemberInfoCard(props) {
103
104
  var member_info_default = MemberInfoCard;
104
105
 
105
106
  // src/client/cards/member-card-summary/index.tsx
106
- import { useEffect, useMemo, useRef, useState as useState2 } from "react";
107
+ import { useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
107
108
  import { createPortal } from "react-dom";
108
109
  import { X } from "@phosphor-icons/react";
109
110
  import { markSelfRendering, sendCardAction } from "@tbox.cn/app-agent-sdk-client";
@@ -158,7 +159,7 @@ function MemberCardSummaryCard(props) {
158
159
  const pointsRuleTriggerRef = useRef(null);
159
160
  const pointsRuleCloseRef = useRef(null);
160
161
  const pointsRuleCloseTimerRef = useRef();
161
- useEffect(() => {
162
+ useEffect2(() => {
162
163
  if (!pointsRuleOpen) return;
163
164
  const previousOverflow = document.body.style.overflow;
164
165
  document.body.style.overflow = "hidden";
@@ -173,7 +174,7 @@ function MemberCardSummaryCard(props) {
173
174
  window.removeEventListener("keydown", onKeyDown);
174
175
  };
175
176
  }, [pointsRuleOpen]);
176
- useEffect(() => () => {
177
+ useEffect2(() => () => {
177
178
  window.clearTimeout(pointsRuleCloseTimerRef.current);
178
179
  window.clearTimeout(benefitActionCloseTimerRef.current);
179
180
  }, []);
@@ -28,11 +28,14 @@ function cardFace(from, to) {
28
28
  const marks = '<rect x="58" y="52" width="210" height="42" rx="8" fill="%23000000" opacity="0.14"/><rect x="58" y="130" width="132" height="26" rx="6" fill="%23000000" opacity="0.10"/>';
29
29
  return `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="672" height="347"><defs><linearGradient id="f" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="%23${from}"/><stop offset="1" stop-color="%23${to}"/></linearGradient></defs><rect width="672" height="347" fill="url(%23f)"/><circle cx="548" cy="60" r="150" fill="%23ffffff" opacity="0.16"/>` + marks + "</svg>";
30
30
  }
31
+ var BLACK_CARD_IMAGE = "https://mdn.alipayobjects.com/huamei_b00jk5/afts/img/A*sdTfQ5NaRN8AAAAAgEAAAAgAegitAQ/fmt.webp";
31
32
  var FACE = {
32
- blue: cardFace("dce8fb", "aecdf5"),
33
+ blue: "https://mdn.alipayobjects.com/huamei_b00jk5/afts/img/A*OVn3SL73QbQAAAAAgDAAAAgAegitAQ/fmt.webp",
33
34
  silver: cardFace("eef1f7", "c4cddf"),
35
+ // gold 配浅金 SVG:恢复真实厂商世代叠字后金卡是浅底深字,黑金深图配金档=深字压深底不可读;
36
+ // 待世代规格(1344×694+透明边)浅金素材重制后再换平台 CDN 图。
34
37
  gold: cardFace("f9ecca", "e3c283"),
35
- black: cardFace("1b1e26", "4a412c")
38
+ black: BLACK_CARD_IMAGE
36
39
  };
37
40
  var POINTS_RULE_IMAGE = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="750" height="1600"><rect width="750" height="1600" fill="%23fffdf8"/><rect x="60" y="80" width="380" height="44" rx="10" fill="%23e2c78f"/><rect x="60" y="180" width="630" height="24" rx="8" fill="%23ece7dd"/><rect x="60" y="230" width="560" height="24" rx="8" fill="%23ece7dd"/><rect x="60" y="280" width="610" height="24" rx="8" fill="%23ece7dd"/><rect x="60" y="700" width="630" height="24" rx="8" fill="%23ece7dd"/><rect x="60" y="1500" width="400" height="24" rx="8" fill="%23ece7dd"/></svg>';
38
41
  var ICON_COLORS = {
@@ -288,12 +291,12 @@ var meta2 = {
288
291
  sampleData: {
289
292
  userId: "u_1001",
290
293
  cardRef: "card:u_1001",
291
- cardDisplay: "\u91D1\u5361\u4F1A\u5458",
294
+ cardDisplay: "\u9ED1\u5361\u4F1A\u5458",
292
295
  status: "active",
293
296
  availablePoints: 860,
294
- cardImageUrl: "https://assets.example.com/member/gold-card.png",
297
+ cardImageUrl: BLACK_CARD_IMAGE,
295
298
  pointsRuleImageUrl: "https://assets.example.com/member/points-rule.jpg",
296
- presentation: { variant: "gold", labelsEmbedded: true },
299
+ presentation: { variant: "black", labelsEmbedded: true },
297
300
  progress: {
298
301
  daySpentCents: 12345,
299
302
  yearSpentCents: 5e5,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  clientModule
3
- } from "../chunk-34PVJEKH.js";
3
+ } from "../chunk-GMHJMLRV.js";
4
4
  export {
5
5
  clientModule
6
6
  };
package/dist/index.js CHANGED
@@ -7,10 +7,10 @@ import {
7
7
  resolveMemberEnrollmentPort,
8
8
  resolveMemberProfilePort,
9
9
  serverModule
10
- } from "./chunk-K4VM7JZT.js";
10
+ } from "./chunk-YQZ64C6P.js";
11
11
  import {
12
12
  clientModule
13
- } from "./chunk-34PVJEKH.js";
13
+ } from "./chunk-GMHJMLRV.js";
14
14
  export {
15
15
  InMemoryMemberService,
16
16
  clientModule,
@@ -7,7 +7,7 @@ import {
7
7
  resolveMemberEnrollmentPort,
8
8
  resolveMemberProfilePort,
9
9
  serverModule
10
- } from "../chunk-K4VM7JZT.js";
10
+ } from "../chunk-YQZ64C6P.js";
11
11
  export {
12
12
  InMemoryMemberService,
13
13
  memberCards,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tbox.cn/app-module-member",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "type": "module",
5
5
  "description": "商圈会员模块:会员账户、会员卡、积分流水与入会流程(codegen 源)。",
6
6
  "exports": {
@@ -19,9 +19,9 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@tbox.cn/app-contracts": "0.12.0",
22
- "@tbox.cn/app-contracts-mall": "0.12.0",
23
- "@tbox.cn/app-agent-sdk-client": "0.1.0",
24
- "@tbox.cn/app-agent-sdk-server": "0.1.0"
22
+ "@tbox.cn/app-contracts-mall": "0.12.1",
23
+ "@tbox.cn/app-agent-sdk-client": "0.1.1",
24
+ "@tbox.cn/app-agent-sdk-server": "0.1.1"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "zod": "3.25.76",
@@ -2,7 +2,7 @@
2
2
  * 图片 + 加载失败占位(0.4.0 D6:模块自持 ~20 行,禁跨模块依赖/基元封顶不破)。
3
3
  * 源:POC shopping-guide 占位语义。src 缺省或加载失败 → 图标占位。
4
4
  */
5
- import { useState } from 'react';
5
+ import { useEffect, useState } from 'react';
6
6
  import type { ReactNode } from 'react';
7
7
  import { ImageSquare } from '@phosphor-icons/react';
8
8
 
@@ -18,6 +18,7 @@ export function ImageWithFallback(props: {
18
18
  }) {
19
19
  const { src, alt, className, size = 40, loading = 'lazy', fallback } = props;
20
20
  const [failed, setFailed] = useState(false);
21
+ useEffect(() => setFailed(false), [src]);
21
22
  if (!src || failed) {
22
23
  return (
23
24
  <div className={`img-fallback ${className ?? ''}`.trim()} aria-label={alt} role="img">
@@ -1,6 +1,6 @@
1
1
  import type { CardMeta } from '@tbox.cn/app-contracts';
2
2
  import { memberCardSummaryCardDataSchema } from '@tbox.cn/app-contracts-mall';
3
- import { samples } from './samples';
3
+ import { BLACK_CARD_IMAGE, samples } from './samples';
4
4
 
5
5
  const meta: CardMeta<typeof memberCardSummaryCardDataSchema> = {
6
6
  cardType: 'member-card-summary',
@@ -10,12 +10,12 @@ const meta: CardMeta<typeof memberCardSummaryCardDataSchema> = {
10
10
  sampleData: {
11
11
  userId: 'u_1001',
12
12
  cardRef: 'card:u_1001',
13
- cardDisplay: '金卡会员',
13
+ cardDisplay: '黑卡会员',
14
14
  status: 'active',
15
15
  availablePoints: 860,
16
- cardImageUrl: 'https://assets.example.com/member/gold-card.png',
16
+ cardImageUrl: BLACK_CARD_IMAGE,
17
17
  pointsRuleImageUrl: 'https://assets.example.com/member/points-rule.jpg',
18
- presentation: { variant: 'gold', labelsEmbedded: true },
18
+ presentation: { variant: 'black', labelsEmbedded: true },
19
19
  progress: {
20
20
  daySpentCents: 12_345,
21
21
  yearSpentCents: 500_000,
@@ -7,17 +7,21 @@
7
7
  * - 视觉等价的输入合并为一格,note 列全合并口径;
8
8
  * - 样本即契约:data 必须 dataSchema 校验通过且无 shape 外键(tests/samples.test.ts 双断言)。
9
9
  *
10
- * 素材中立化(画廊时期权益图标指向厂商 CDN,模块样本按仓库铁律净化):
11
- * - 卡面底图/积分规则长图:无网络依赖的内联 SVG data URI(画廊原实现随行);
10
+ * 素材边界(规范真源 = 厂商中立门禁:禁厂商租户字样/标识/深链——平台 CDN 演示素材不在禁列):
11
+ * - 卡面底图:平台 CDN 演示素材(mdn.alipayobjects.com,预览/截图保真)。样本只服务预览,
12
+ * 运行时素材恒由 provider 下发(mock 发 dynamic face 与真实厂商集成同构,见 provider-mock
13
+ * member);silver 档保留内联 SVG 作「无外部素材占位」分支;
12
14
  * - 权益图标:内联 SVG 按变体分色——验证的是「图标配色压不压得住卡面底色」与四列网格
13
15
  * 换行(低两档 14 项、高两档 15 项的数量差异照实铺),不是图标的图案还原度
14
16
  * (真实图标由 provider 运行时下发;图案语义兜底归组件内 ImageWithFallback);
15
- * - 卡图/图标的外链失败分支刻意保留 example.com 占位(该分支本身是合法预览态)。
17
+ * - 卡图/图标的外链失败分支刻意保留 example.com 占位(该分支本身是合法预览态);
18
+ * - 白名单由 tests/samples.test.ts 断言收口:卡面 URL 仅允许 mdn.alipayobjects.com /
19
+ * example.com / data:image/svg+xml,厂商租户素材回潜即 fail。
16
20
  */
17
21
  import type { CardSample } from '@tbox.cn/app-contracts';
18
22
  import type { MemberCardSummaryCardData } from '@tbox.cn/app-contracts-mall';
19
23
 
20
- /** 卡面底图:底色随 variant 走(前三档浅底深字、black 深底浅字——与模块 CSS 叠字色配对) */
24
+ /** 内联卡面底图:银/金档占位(浅底深字,与模块叠字配色配对)。 */
21
25
  function cardFace(from: string, to: string): string {
22
26
  // 真实卡图恒自带卡名与「我的积分」字样(labelsEmbedded 恒 true),前端只叠积分数字。
23
27
  // 两块半透明色块代表那两行字的占位——不写真文字是为了不依赖 SVG 内的中文字体,
@@ -36,11 +40,16 @@ function cardFace(from: string, to: string): string {
36
40
  + '</svg>';
37
41
  }
38
42
 
43
+ export const BLACK_CARD_IMAGE
44
+ = 'https://mdn.alipayobjects.com/huamei_b00jk5/afts/img/A*sdTfQ5NaRN8AAAAAgEAAAAgAegitAQ/fmt.webp';
45
+
39
46
  const FACE = {
40
- blue: cardFace('dce8fb', 'aecdf5'),
47
+ blue: 'https://mdn.alipayobjects.com/huamei_b00jk5/afts/img/A*OVn3SL73QbQAAAAAgDAAAAgAegitAQ/fmt.webp',
41
48
  silver: cardFace('eef1f7', 'c4cddf'),
49
+ // gold 配浅金 SVG:恢复真实厂商世代叠字后金卡是浅底深字,黑金深图配金档=深字压深底不可读;
50
+ // 待世代规格(1344×694+透明边)浅金素材重制后再换平台 CDN 图。
42
51
  gold: cardFace('f9ecca', 'e3c283'),
43
- black: cardFace('1b1e26', '4a412c'),
52
+ black: BLACK_CARD_IMAGE,
44
53
  };
45
54
 
46
55
  /** 积分规则长图占位(弹层内容):只需一张有高度的图验证弹层滚动与关闭 */
@@ -44,6 +44,25 @@ describe('member samples 双断言(样本即契约)', () => {
44
44
  expect(Math.min(...counts.filter((n) => n >= 10))).toBe(14);
45
45
  });
46
46
 
47
+ it('卡面素材白名单:平台 CDN 演示素材 / example.com 失败分支 / 内联 SVG(厂商租户素材回潜即 fail)', () => {
48
+ const allowed = (url: string) =>
49
+ url.startsWith('https://mdn.alipayobjects.com/')
50
+ || url.startsWith('https://assets.example.com/')
51
+ || url.startsWith('data:image/svg+xml');
52
+ for (const s of meta.samples ?? []) {
53
+ const url = (s.data as { cardImageUrl?: string }).cardImageUrl;
54
+ if (url) expect(allowed(url), s.label).toBe(true);
55
+ }
56
+ const initial = (meta.sampleData as { cardImageUrl?: string }).cardImageUrl;
57
+ if (initial) expect(allowed(initial), '初始态').toBe(true);
58
+ });
59
+
60
+ it('初始态深卡配对:black 变体配深底卡图(恢复真实厂商世代叠字后浅金字压深底可读)', () => {
61
+ const data = meta.sampleData as { presentation?: { variant?: string }; cardImageUrl?: string };
62
+ expect(data.presentation?.variant).toBe('black');
63
+ expect(data.cardImageUrl).toMatch(/^https:\/\/mdn\.alipayobjects\.com\//);
64
+ });
65
+
47
66
  it('素材中立:权益图标全部为内联 SVG 分色占位或 example.com 占位', () => {
48
67
  const serialized = JSON.stringify(meta.samples ?? []);
49
68
  expect(serialized).toContain('data:image/svg+xml');