@tbox.cn/app-module-member 0.1.0 → 0.8.6

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.
Files changed (65) hide show
  1. package/AGENTS.md +16 -0
  2. package/README.md +99 -11
  3. package/dist/chunk-KEBILLQ5.js +771 -0
  4. package/dist/chunk-T4SYPQA5.js +1565 -0
  5. package/dist/client/index.d.ts +11 -2
  6. package/dist/client/index.js +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +14 -4
  9. package/dist/login-mascot-5D4X6YZ7.jpg +0 -0
  10. package/dist/member-QFCIYILN.css +1734 -0
  11. package/dist/server/index.d.ts +928 -41
  12. package/dist/server/index.js +13 -3
  13. package/package.json +25 -16
  14. package/skills/member/SKILL.md +141 -0
  15. package/skills/member/references/enrollment.md +34 -0
  16. package/skills/member/references/points-and-card.md +24 -0
  17. package/src/client/assets/login-mascot.jpg +0 -0
  18. package/src/client/assets.d.ts +5 -0
  19. package/src/client/cards/index.ts +10 -0
  20. package/src/client/cards/member-card-summary/index.tsx +375 -0
  21. package/src/client/cards/member-enrollment-cta/index.tsx +61 -0
  22. package/src/client/cards/member-enrollment-form/index.tsx +181 -0
  23. package/src/client/cards/member-enrollment-status/index.tsx +65 -0
  24. package/src/client/cards/member-info/index.tsx +60 -0
  25. package/src/client/cards/member-login-cta/index.tsx +81 -0
  26. package/src/client/cards/member-point-usage-list/index.tsx +53 -0
  27. package/src/client/cards/view.ts +39 -0
  28. package/src/client/components/ImageWithFallback.tsx +38 -0
  29. package/src/client/index.ts +21 -4
  30. package/src/client/styles/member.css +1734 -0
  31. package/src/client/types/css.d.ts +2 -0
  32. package/src/server/action.ts +348 -0
  33. package/src/server/cards/index.ts +31 -0
  34. package/src/server/cards/member-card-summary/meta.ts +42 -0
  35. package/src/server/cards/member-card-summary/samples.ts +272 -0
  36. package/src/server/cards/member-enrollment-cta/meta.ts +19 -0
  37. package/src/server/cards/member-enrollment-form/meta.ts +26 -0
  38. package/src/server/cards/member-enrollment-status/meta.ts +18 -0
  39. package/src/server/cards/member-info/meta.ts +19 -0
  40. package/src/server/cards/member-login-cta/meta.ts +16 -0
  41. package/src/server/cards/member-point-usage-list/meta.ts +19 -0
  42. package/src/server/handler.ts +170 -16
  43. package/src/server/index.ts +136 -24
  44. package/src/server/ports.ts +66 -0
  45. package/src/server/service.ts +188 -36
  46. package/src/server/status.ts +87 -0
  47. package/src/server/subject-binding.ts +19 -0
  48. package/src/server/tool.ts +514 -19
  49. package/src/server/util.ts +4 -0
  50. package/tbox.module.json +124 -0
  51. package/tests/cards-render.test.tsx +526 -0
  52. package/tests/enrollment.test.ts +121 -0
  53. package/tests/ports-resolve.test.ts +95 -0
  54. package/tests/preference-memory.test.ts +86 -0
  55. package/tests/samples.test.ts +61 -0
  56. package/tests/service.test.ts +597 -36
  57. package/tests/skills.test.ts +65 -0
  58. package/tests/status.test.ts +127 -0
  59. package/tests/view.test.ts +33 -0
  60. package/tsup.config.ts +9 -1
  61. package/dist/chunk-F6FHBH3N.js +0 -160
  62. package/dist/chunk-OXXT6F5B.js +0 -33
  63. package/src/client/cards/points/index.tsx +0 -19
  64. package/src/server/cards/points/meta.ts +0 -22
  65. package/tbox.component.json +0 -20
@@ -0,0 +1,65 @@
1
+ /**
2
+ * K1:member 文件 skill 装配验收——loadModuleSkills 经 workspace link 解析包根 skills/,
3
+ * 声明式通道填回 serverModule.skills;routingTerms 域间不相交(券词归 promotion)。
4
+ */
5
+ import { describe, expect, it } from 'vitest';
6
+ import { serverModule } from '../src/server/index';
7
+
8
+ type LoadedSkill = { name: string; description: string; routingTerms: string[]; instructions: string };
9
+
10
+ describe('K1:member skills 装配', () => {
11
+ const skills = ((serverModule as { skills?: Record<string, LoadedSkill> }).skills ?? {});
12
+
13
+ it('skills 含 member(frontmatter name + 目录名一致)', () => {
14
+ expect(Object.keys(skills)).toContain('member');
15
+ expect(skills.member!.description.length).toBeGreaterThan(0);
16
+ expect(skills.member!.instructions).toContain('入会');
17
+ });
18
+
19
+ it('routingTerms 域内词齐备且不含券词(券域归 promotion,窗口期软迁移 K1 硬规则)', () => {
20
+ expect(skills.member!.routingTerms).toEqual([
21
+ '会员',
22
+ '积分',
23
+ '入会',
24
+ '会员卡',
25
+ '开卡',
26
+ '办卡',
27
+ '等级',
28
+ ]);
29
+ expect(skills.member!.routingTerms.some((t) => t.includes('券'))).toBe(false);
30
+ });
31
+
32
+ it('SOP 含语气指引与跨域直调(单轮单 skill 注入下的自含协作)', () => {
33
+ expect(skills.member!.instructions).toContain('角色与语气');
34
+ expect(skills.member!.instructions).toContain('数字不裸贴');
35
+ expect(skills.member!.instructions).toContain('引导至营销域工具');
36
+ expect(skills.member!.instructions).toContain('调 `searchOffers`');
37
+ expect(skills.member!.instructions).toContain('调停车域 `queryParkingFee`');
38
+ });
39
+
40
+ it('SOP 含域边界硬规则(券 → promotion 引导)', () => {
41
+ expect(skills.member!.instructions).toContain('不属于本域');
42
+ expect(skills.member!.instructions).toContain('引导至营销域');
43
+ });
44
+
45
+ it('SOP 以 C 端契约限制等级与权益陈述', () => {
46
+ expect(skills.member!.instructions).toContain('不得声称折扣、免费停车、生日特权');
47
+ expect(skills.member!.instructions).toContain('CardTitle` 是会员卡名');
48
+ });
49
+
50
+ it('SOP 查询路由与卡面展示规则', () => {
51
+ expect(skills.member!.instructions).toContain('泛会员信息或只查会员卡 → 调 `queryMemberCard`');
52
+ expect(skills.member!.instructions).toContain('明确问积分 → 只调一次 `queryPoints`');
53
+ expect(skills.member!.instructions).toContain('会员卡面优先');
54
+ expect(skills.member!.instructions).toContain('详细积分流水请点击会员卡面上的“我的积分”');
55
+ expect(skills.member!.instructions).toContain('会员卡面回答结构');
56
+ expect(skills.member!.instructions).toContain('自然推荐偏好服务');
57
+ });
58
+
59
+ it('SOP 明确限制为当前会话会员并拒绝代查、改资料和凭标识切号', () => {
60
+ expect(skills.member!.instructions).toContain('当前会员隐私边界');
61
+ expect(skills.member!.instructions).toContain('一律拒答且不调用任何会员工具');
62
+ expect(skills.member!.instructions).toContain('手机号、会员卡号或账号都不能覆盖当前登录身份');
63
+ expect(skills.member!.instructions).toContain('用我老婆的手机号 13900000000 查她有多少积分');
64
+ });
65
+ });
@@ -0,0 +1,127 @@
1
+ /**
2
+ * resolveMemberStatus 单源锁:匿名短路不触 provider、soft 单档(resolve/getAccount
3
+ * 任一失败收口 unavailable + warn 留痕)、active 三元齐备、通知表穷尽性。
4
+ */
5
+ import { describe, expect, it, vi } from 'vitest';
6
+ import type { RequestContext } from '@tbox.cn/app-contracts';
7
+ import type { ServerContext } from '@tbox.cn/app-sdk/server';
8
+ import { resolveMemberStatus, MEMBER_QUERY_NOTICES } from '../src/server/status';
9
+
10
+ const reqCtx = (source: 'dev' | 'anonymous' = 'dev'): RequestContext =>
11
+ ({
12
+ identity: { userId: 'u_1001', source },
13
+ attributes: { mall: { miniAppId: 'demo-app', mallId: 'mall-demo' } },
14
+ }) as RequestContext;
15
+
16
+ const fakeCtx = (integrations?: unknown, logger?: { warn: ReturnType<typeof vi.fn> }): ServerContext =>
17
+ ({ ...(integrations !== undefined ? { integrations } : {}), ...(logger ? { logger } : {}) }) as unknown as ServerContext;
18
+
19
+ const stubService = (account: unknown, extra: { enrollUrl?: string; loginUrl?: string } = {}) =>
20
+ ({ getAccount: async () => account, ...extra });
21
+
22
+ describe('resolveMemberStatus(IO 唯一入口)', () => {
23
+ it('匿名短路不触 provider(ctx 无 integrations 仍返回 unauthenticated,未走 fail-soft)', async () => {
24
+ await expect(resolveMemberStatus(reqCtx('anonymous'), fakeCtx())).resolves.toEqual({
25
+ outcome: 'unauthenticated',
26
+ });
27
+ });
28
+
29
+ it('reqCtx 缺席 → unavailable + warn(管路故障不冒充身份语义)', async () => {
30
+ const warn = vi.fn();
31
+ await expect(resolveMemberStatus(undefined, fakeCtx(undefined, { warn }))).resolves.toEqual({
32
+ outcome: 'unavailable',
33
+ });
34
+ expect(warn).toHaveBeenCalledTimes(1);
35
+ });
36
+
37
+ it('integrations 缺失 → unavailable + warn 留痕(不伪装状态)', async () => {
38
+ const warn = vi.fn();
39
+ await expect(resolveMemberStatus(reqCtx(), fakeCtx(undefined, { warn }))).resolves.toEqual({
40
+ outcome: 'unavailable',
41
+ });
42
+ expect(warn).toHaveBeenCalledTimes(1);
43
+ });
44
+
45
+ it('resolveService 抛错 → unavailable + warn', async () => {
46
+ const warn = vi.fn();
47
+ const ctx = fakeCtx(
48
+ { resolveService: async () => { throw new Error('registry down'); } },
49
+ { warn },
50
+ );
51
+ await expect(resolveMemberStatus(reqCtx(), ctx)).resolves.toEqual({ outcome: 'unavailable' });
52
+ expect(warn).toHaveBeenCalledTimes(1);
53
+ });
54
+
55
+ it('getAccount 抛错 → unavailable + warn', async () => {
56
+ const warn = vi.fn();
57
+ const ctx = fakeCtx(
58
+ { resolveService: async () => ({ getAccount: async () => { throw new Error('provider timeout'); } }) },
59
+ { warn },
60
+ );
61
+ await expect(resolveMemberStatus(reqCtx(), ctx)).resolves.toEqual({ outcome: 'unavailable' });
62
+ expect(warn).toHaveBeenCalledTimes(1);
63
+ });
64
+
65
+ it('active → outcome/account/深链三元齐备(017 N1 双槽:service 字段退役,enrollment 槽独立取深链)', async () => {
66
+ const service = stubService({
67
+ authState: 'authenticated',
68
+ state: 'active',
69
+ memberRef: 'member:C-1001',
70
+ primaryMallCardNo: 'C-1001',
71
+ observedAt: '2026-08-30T00:00:00.000Z',
72
+ });
73
+ const ctx = fakeCtx({ resolveService: async () => service });
74
+ const status = await resolveMemberStatus(reqCtx(), ctx);
75
+ expect(status.outcome).toBe('active');
76
+ expect(status.account).toMatchObject({ state: 'active', primaryMallCardNo: 'C-1001' });
77
+ // 深链透传缺省(stub 无 enrollUrl/loginUrl 字段 → 不注入)
78
+ expect(status.enrollUrl).toBeUndefined();
79
+ expect(status.loginUrl).toBeUndefined();
80
+ });
81
+
82
+ it('active → 已配置深链透传 enrollUrl/loginUrl(跨模块门禁卡片数据源)', async () => {
83
+ const service = stubService({ authState: 'authenticated', state: 'active', observedAt: '2026-08-30T00:00:00.000Z' });
84
+ service.enrollUrl = 'alipays://enroll-demo';
85
+ service.loginUrl = 'alipays://login-demo';
86
+ const ctx = fakeCtx({ resolveService: async () => service });
87
+ const status = await resolveMemberStatus(reqCtx(), ctx);
88
+ expect(status.enrollUrl).toBe('alipays://enroll-demo');
89
+ expect(status.loginUrl).toBe('alipays://login-demo');
90
+ });
91
+
92
+ it('N1 双槽:enrollment 槽解析失败不拖累身份判定(active 保持 + warn 留痕 + 无深链)', async () => {
93
+ const warn = vi.fn();
94
+ const ctx = fakeCtx(
95
+ {
96
+ resolveService: async (input: { service: string }) => {
97
+ if (input.service === 'member.enrollment') throw new Error('enrollment slot down');
98
+ return stubService({ authState: 'authenticated', state: 'active', observedAt: '2026-08-30T00:00:00.000Z' });
99
+ },
100
+ },
101
+ { warn },
102
+ );
103
+ const status = await resolveMemberStatus(reqCtx(), ctx);
104
+ expect(status.outcome).toBe('active');
105
+ expect(status.enrollUrl).toBeUndefined();
106
+ expect(warn).toHaveBeenCalledTimes(1);
107
+ });
108
+
109
+ it('凭证轴未认证 → unauthenticated(tokenless 合法登录形态)', async () => {
110
+ const ctx = fakeCtx({
111
+ resolveService: async () => stubService({ authState: 'unauthenticated', state: 'unknown', observedAt: '2026-08-30T00:00:00.000Z' }),
112
+ });
113
+ await expect(resolveMemberStatus(reqCtx(), ctx)).resolves.toMatchObject({ outcome: 'unauthenticated' });
114
+ });
115
+ });
116
+
117
+ describe('MEMBER_QUERY_NOTICES(直聊面穷尽表)', () => {
118
+ it('键集 = 非 active 全量(扩值缺键由编译器拦截)', () => {
119
+ expect(Object.keys(MEMBER_QUERY_NOTICES).sort()).toEqual([
120
+ 'closed',
121
+ 'not_enrolled',
122
+ 'unauthenticated',
123
+ 'unavailable',
124
+ 'unknown',
125
+ ]);
126
+ });
127
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * member 展示纯函数单测(0.4.0 D6:node 环境)。
3
+ */
4
+ import { describe, expect, it } from 'vitest';
5
+ import { enrollmentStateText, enrollmentTone, formatMemberDate, memberStateText, pointAmountClass } from '../src/client/cards/view';
6
+
7
+ describe('pointAmountClass 双色', () => {
8
+ it('负支出红 / 正收入绿', () => {
9
+ expect(pointAmountClass('-120')).toBe('member-pu__amount--spend');
10
+ expect(pointAmountClass('+50')).toBe('member-pu__amount--income');
11
+ });
12
+ });
13
+
14
+ describe('会员资料格式化', () => {
15
+ it('日期格式稳定且关闭态不误报未入会', () => {
16
+ expect(formatMemberDate('2026-08-19T10:30:00+08:00')).toBe('2026.08.19');
17
+ expect(formatMemberDate('待确认')).toBe('待确认');
18
+ expect(memberStateText('closed')).toBe('已注销');
19
+ expect(memberStateText('unknown', 'authenticated')).toBe('状态待确认');
20
+ expect(memberStateText('unknown', 'unauthenticated')).toBe('未登录');
21
+ });
22
+ });
23
+
24
+ describe('enrollment 状态', () => {
25
+ it('四态文案 + tone', () => {
26
+ expect(enrollmentStateText('active')).toBe('入会成功');
27
+ expect(enrollmentStateText('pending_confirmation')).toBe('厂商确认中');
28
+ expect(enrollmentStateText('confirmation_timeout')).toBe('确认超时');
29
+ expect(enrollmentTone('active')).toBe('success');
30
+ expect(enrollmentTone('pending_confirmation')).toBe('warning');
31
+ expect(enrollmentTone('failed')).toBe('neutral');
32
+ });
33
+ });
package/tsup.config.ts CHANGED
@@ -12,7 +12,9 @@ export default defineConfig({
12
12
  outDir: 'dist',
13
13
  clean: true,
14
14
  sourcemap: false,
15
- dts: true,
15
+ // dts worker 以命令行式传参会撞 TS5074(incremental 需单文件 emit 或 tsBuildInfoFile);
16
+ // 声明产物一次性构建无需增量——仅 dts 关闭,typecheck 的增量缓存不受影响。
17
+ dts: { compilerOptions: { incremental: false } },
16
18
  external: [
17
19
  '@tbox.cn/app-sdk',
18
20
  '@tbox.cn/app-contracts',
@@ -22,7 +24,13 @@ export default defineConfig({
22
24
  'zod',
23
25
  '@mastra/core',
24
26
  'express',
27
+ '@phosphor-icons/react',
25
28
  ],
29
+ // 0.3.2/0.4.0 D1:模块自持 CSS 出货链。
30
+ // 顶层 loader 写法(实测:esbuildOptions 内设 loader 被 tsup 覆盖失效——CSS 提取断链静默丢失)。
31
+ loader: {
32
+ '.css': 'copy',
33
+ },
26
34
  esbuildOptions(options) {
27
35
  options.jsx = 'automatic';
28
36
  },
@@ -1,160 +0,0 @@
1
- // src/server/service.ts
2
- var InMemoryMemberService = class {
3
- balances = /* @__PURE__ */ new Map();
4
- levels = /* @__PURE__ */ new Map();
5
- idempotentKeys = /* @__PURE__ */ new Set();
6
- audit = [];
7
- constructor(initial = {}) {
8
- for (const [userId, balance] of Object.entries(initial)) {
9
- this.balances.set(userId, balance);
10
- }
11
- }
12
- async queryPoints(userId) {
13
- return {
14
- userId,
15
- balance: this.balances.get(userId) ?? 0,
16
- level: this.levelOf(userId)
17
- };
18
- }
19
- async queryDiscount(userId) {
20
- const level = this.levelOf(userId);
21
- const map = {
22
- silver: { level: "silver", discount: 0.95 },
23
- gold: { level: "gold", discount: 0.9 },
24
- platinum: { level: "platinum", discount: 0.85 }
25
- };
26
- return map[level];
27
- }
28
- async deductPoints(userId, amount, idempotencyKey) {
29
- if (this.idempotentKeys.has(idempotencyKey)) return true;
30
- const current = this.balances.get(userId) ?? 0;
31
- if (amount <= 0 || current < amount) return false;
32
- this.balances.set(userId, current - amount);
33
- this.idempotentKeys.add(idempotencyKey);
34
- this.audit.push({ action: "deduct", userId, amount, key: idempotencyKey });
35
- this.maybeUpgrade(userId);
36
- return true;
37
- }
38
- async refundPoints(userId, amount, idempotencyKey) {
39
- if (this.idempotentKeys.has(`refund:${idempotencyKey}`)) return true;
40
- const current = this.balances.get(userId) ?? 0;
41
- this.balances.set(userId, current + amount);
42
- this.idempotentKeys.add(`refund:${idempotencyKey}`);
43
- this.audit.push({ action: "refund", userId, amount, key: idempotencyKey });
44
- return true;
45
- }
46
- async awardPoints(userId, amount) {
47
- const current = this.balances.get(userId) ?? 0;
48
- this.balances.set(userId, current + amount);
49
- this.audit.push({ action: "award", userId, amount });
50
- this.maybeUpgrade(userId);
51
- return true;
52
- }
53
- levelOf(userId) {
54
- const cached = this.levels.get(userId);
55
- if (cached) return cached;
56
- const balance = this.balances.get(userId) ?? 0;
57
- if (balance >= 1e3) return "platinum";
58
- if (balance >= 300) return "gold";
59
- return "silver";
60
- }
61
- maybeUpgrade(userId) {
62
- const newLevel = this.levelOf(userId);
63
- const prev = this.levels.get(userId);
64
- this.levels.set(userId, newLevel);
65
- if (prev && prev !== newLevel) {
66
- this.levelChangeListeners.forEach((fn) => fn({ userId, newLevel }));
67
- }
68
- }
69
- levelChangeListeners = /* @__PURE__ */ new Set();
70
- onLevelChange(fn) {
71
- this.levelChangeListeners.add(fn);
72
- }
73
- };
74
-
75
- // src/server/cards/points/meta.ts
76
- import { z } from "zod";
77
- var dataSchema = z.object({
78
- userId: z.string(),
79
- balance: z.number(),
80
- level: z.enum(["silver", "gold", "platinum"])
81
- });
82
- var meta = {
83
- cardType: "points",
84
- dataSchema,
85
- allowedTools: ["queryPoints"],
86
- displayName: "\u4F1A\u5458\u79EF\u5206\u5361",
87
- description: "\u5C55\u793A\u4F1A\u5458\u5F53\u524D\u79EF\u5206\u4F59\u989D\u4E0E\u7B49\u7EA7",
88
- sampleData: { userId: "u_1001", balance: 860, level: "gold" },
89
- schemaVersion: 1
90
- };
91
- var meta_default = meta;
92
-
93
- // src/server/tool.ts
94
- import { createTool } from "@tbox.cn/app-sdk/server";
95
- import { z as z2 } from "zod";
96
- function createQueryPointsTool(getMember) {
97
- return createTool({
98
- id: "queryPoints",
99
- description: "\u67E5\u8BE2\u4F1A\u5458\u5F53\u524D\u79EF\u5206\u4F59\u989D\u4E0E\u7B49\u7EA7",
100
- inputSchema: z2.object({
101
- userId: z2.string().describe("\u4F1A\u5458 userId")
102
- }),
103
- outputSchema: z2.object({
104
- userId: z2.string(),
105
- balance: z2.number(),
106
- level: z2.enum(["silver", "gold", "platinum"])
107
- }),
108
- execute: async ({ userId }) => {
109
- const member2 = getMember();
110
- if (!member2) return { userId, balance: 0, level: "silver" };
111
- return member2.queryPoints(userId);
112
- }
113
- });
114
- }
115
-
116
- // src/server/handler.ts
117
- function createMemberHandler(ctx) {
118
- return {
119
- id: "member",
120
- handle: async (_action, payload) => {
121
- const member2 = ctx.resolveService("member");
122
- if (!member2) return { cards: [], text: "\u4F1A\u5458\u670D\u52A1\u4E0D\u53EF\u7528" };
123
- const { userId } = payload ?? {};
124
- if (!userId) return { cards: [], text: "\u7F3A\u5C11 userId" };
125
- const points = await member2.queryPoints(userId);
126
- return {
127
- cards: [{ cardType: "points", data: points }],
128
- text: `\u5F53\u524D\u79EF\u5206 ${points.balance}\uFF0C\u7B49\u7EA7 ${points.level}`
129
- };
130
- }
131
- };
132
- }
133
-
134
- // src/server/index.ts
135
- var member = new InMemoryMemberService({ u_1001: 860, u_1002: 120 });
136
- var cards = {
137
- points: meta_default
138
- };
139
- var serverModule = {
140
- cards,
141
- register(ctx) {
142
- ctx.services.register("member", member);
143
- ctx.tools.register(createQueryPointsTool(() => ctx.resolveService("member")));
144
- ctx.cards.registerMap(cards);
145
- ctx.handlers.register(createMemberHandler(ctx));
146
- const mallBus = ctx.bus;
147
- mallBus.subscribe("parking.coupon.used", async ({ userId, amount }) => {
148
- await member.awardPoints(userId, Math.floor(amount / 10));
149
- });
150
- member.onLevelChange(({ userId, newLevel }) => {
151
- void mallBus.publish("member.level.changed", { userId, newLevel });
152
- });
153
- }
154
- };
155
-
156
- export {
157
- InMemoryMemberService,
158
- meta_default,
159
- serverModule
160
- };
@@ -1,33 +0,0 @@
1
- // src/client/cards/points/index.tsx
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
- function PointsCard({ data }) {
4
- const levelLabel = {
5
- silver: "\u94F6\u5361",
6
- gold: "\u91D1\u5361",
7
- platinum: "\u94C2\u91D1\u5361"
8
- };
9
- return /* @__PURE__ */ jsxs("div", { className: "mall-card mall-card--points", children: [
10
- /* @__PURE__ */ jsx("div", { className: "mall-card__title", children: "\u4F1A\u5458\u79EF\u5206" }),
11
- /* @__PURE__ */ jsx("div", { className: "mall-card__balance", children: data.balance }),
12
- /* @__PURE__ */ jsxs("div", { className: "mall-card__meta", children: [
13
- levelLabel[data.level],
14
- " \xB7 \u7528\u6237 ",
15
- data.userId
16
- ] })
17
- ] });
18
- }
19
-
20
- // src/client/index.ts
21
- var cards = {
22
- points: PointsCard
23
- };
24
- var clientModule = {
25
- cards,
26
- register(ctx) {
27
- ctx.cards.registerMap(cards);
28
- }
29
- };
30
-
31
- export {
32
- clientModule
33
- };
@@ -1,19 +0,0 @@
1
- import type { PointsCardData } from '../../../server/cards/points/meta';
2
-
3
- /** 会员积分卡组件 */
4
- export function PointsCard({ data }: { data: PointsCardData; cardId: string; isHistory: boolean }) {
5
- const levelLabel: Record<PointsCardData['level'], string> = {
6
- silver: '银卡',
7
- gold: '金卡',
8
- platinum: '铂金卡',
9
- };
10
- return (
11
- <div className="mall-card mall-card--points">
12
- <div className="mall-card__title">会员积分</div>
13
- <div className="mall-card__balance">{data.balance}</div>
14
- <div className="mall-card__meta">
15
- {levelLabel[data.level]} · 用户 {data.userId}
16
- </div>
17
- </div>
18
- );
19
- }
@@ -1,22 +0,0 @@
1
- import { z } from 'zod';
2
- import type { CardMeta } from '@tbox.cn/app-contracts';
3
-
4
- export const dataSchema = z.object({
5
- userId: z.string(),
6
- balance: z.number(),
7
- level: z.enum(['silver', 'gold', 'platinum']),
8
- });
9
-
10
- export type PointsCardData = z.infer<typeof dataSchema>;
11
-
12
- const meta: CardMeta<typeof dataSchema> = {
13
- cardType: 'points',
14
- dataSchema,
15
- allowedTools: ['queryPoints'],
16
- displayName: '会员积分卡',
17
- description: '展示会员当前积分余额与等级',
18
- sampleData: { userId: 'u_1001', balance: 860, level: 'gold' },
19
- schemaVersion: 1,
20
- };
21
-
22
- export default meta;
@@ -1,20 +0,0 @@
1
- {
2
- "schemaVersion": 1,
3
- "name": "module-member",
4
- "version": "0.1.0",
5
- "kind": "business",
6
- "risk": { "level": "medium", "writeBoundary": "source" },
7
- "distribution": { "defaultMode": "codegen" },
8
- "contributes": {
9
- "handlers": ["member"],
10
- "tools": ["queryPoints"],
11
- "cards": [{ "cardType": "points", "schemaVersion": 1 }],
12
- "routes": [],
13
- "pages": [],
14
- "tabs": []
15
- },
16
- "dependencies": {
17
- "modules": [{ "id": "contracts-mall", "required": true }]
18
- },
19
- "env": []
20
- }