@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.
- package/dist/{chunk-34PVJEKH.js → chunk-GMHJMLRV.js} +5 -4
- package/dist/{chunk-K4VM7JZT.js → chunk-YQZ64C6P.js} +8 -5
- package/dist/client/index.js +1 -1
- package/dist/index.js +2 -2
- package/dist/server/index.js +1 -1
- package/package.json +4 -4
- package/src/client/components/ImageWithFallback.tsx +2 -1
- package/src/server/cards/member-card-summary/meta.ts +4 -4
- package/src/server/cards/member-card-summary/samples.ts +15 -6
- package/tests/samples.test.ts +19 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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: "\
|
|
294
|
+
cardDisplay: "\u9ED1\u5361\u4F1A\u5458",
|
|
292
295
|
status: "active",
|
|
293
296
|
availablePoints: 860,
|
|
294
|
-
cardImageUrl:
|
|
297
|
+
cardImageUrl: BLACK_CARD_IMAGE,
|
|
295
298
|
pointsRuleImageUrl: "https://assets.example.com/member/points-rule.jpg",
|
|
296
|
-
presentation: { variant: "
|
|
299
|
+
presentation: { variant: "black", labelsEmbedded: true },
|
|
297
300
|
progress: {
|
|
298
301
|
daySpentCents: 12345,
|
|
299
302
|
yearSpentCents: 5e5,
|
package/dist/client/index.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
resolveMemberEnrollmentPort,
|
|
8
8
|
resolveMemberProfilePort,
|
|
9
9
|
serverModule
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-YQZ64C6P.js";
|
|
11
11
|
import {
|
|
12
12
|
clientModule
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-GMHJMLRV.js";
|
|
14
14
|
export {
|
|
15
15
|
InMemoryMemberService,
|
|
16
16
|
clientModule,
|
package/dist/server/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tbox.cn/app-module-member",
|
|
3
|
-
"version": "0.9.
|
|
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.
|
|
23
|
-
"@tbox.cn/app-agent-sdk-client": "0.1.
|
|
24
|
-
"@tbox.cn/app-agent-sdk-server": "0.1.
|
|
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:
|
|
16
|
+
cardImageUrl: BLACK_CARD_IMAGE,
|
|
17
17
|
pointsRuleImageUrl: 'https://assets.example.com/member/points-rule.jpg',
|
|
18
|
-
presentation: { variant: '
|
|
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
|
-
*
|
|
11
|
-
* -
|
|
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
|
-
/**
|
|
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:
|
|
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:
|
|
52
|
+
black: BLACK_CARD_IMAGE,
|
|
44
53
|
};
|
|
45
54
|
|
|
46
55
|
/** 积分规则长图占位(弹层内容):只需一张有高度的图验证弹层滚动与关闭 */
|
package/tests/samples.test.ts
CHANGED
|
@@ -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');
|