@yaoxiu/marketing-dsl 2.1.1 → 2.2.0

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.
@@ -1488,6 +1488,12 @@ var sections_default = sections9;
1488
1488
 
1489
1489
  // src/docs/meta.ts
1490
1490
  var changelog = [
1491
+ {
1492
+ date: "2026-08-24",
1493
+ items: [
1494
+ "\u3010\u4FEE\u590D\u3011\u5173\u95ED\u6309\u94AE\u4E0A\u914D\u7684\u57CB\u70B9\uFF08sequence: [track, close]\uFF09\u4E4B\u524D\u4F1A\u4E22\u5931\u3001\u8FD8\u4F1A\u4E32\u5230\u4E0B\u4E00\u6B21\u5173\u95ED\u7684\u4E0A\u62A5\u91CC \u2014\u2014 \u89E3\u91CA\u5668\u6D3E\u53D1 close \u4E8B\u4EF6\u65F6\u6162\u4E86\u4E00\u62CD\uFF0C\u5BBF\u4E3B\u8BFB\u4E0D\u5230\u57CB\u70B9\u540D\u3002\u73B0\u5DF2\u4FEE\u6B63\u987A\u5E8F\uFF0C\u8FD0\u8425\u7684\u914D\u7F6E\u5199\u6CD5\u4E0D\u7528\u6539"
1495
+ ]
1496
+ },
1491
1497
  {
1492
1498
  date: "2026-08-22",
1493
1499
  items: [
@@ -3205,6 +3211,133 @@ var imageBanner = {
3205
3211
  };
3206
3212
  var image_banner_default = imageBanner;
3207
3213
 
3214
+ // src/docs/examples/drawer-banner.ts
3215
+ function buildBannerImage(options) {
3216
+ const [from, to] = options.colors;
3217
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="640" height="72" viewBox="0 0 640 72">
3218
+ <defs>
3219
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="0">
3220
+ <stop offset="0" stop-color="${from}"/>
3221
+ <stop offset="1" stop-color="${to}"/>
3222
+ </linearGradient>
3223
+ </defs>
3224
+ <rect width="640" height="72" fill="url(#g)"/>
3225
+ <text x="20" y="44" font-family="sans-serif" font-size="22" fill="#ffffff">${options.title}</text>
3226
+ <rect x="400" y="18" width="150" height="36" rx="18" fill="#ffffff" fill-opacity="0.92"/>
3227
+ <text x="475" y="43" font-family="sans-serif" font-size="20" fill="${options.ctaColor}" text-anchor="middle">${options.cta}</text>
3228
+ </svg>`;
3229
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
3230
+ }
3231
+ function createDrawerBanner(options) {
3232
+ return {
3233
+ version: 1,
3234
+ type: "banner",
3235
+ meta: { name: options.name },
3236
+ stage: {
3237
+ width: "100%",
3238
+ // 高度写死:坑位宽度是宿主固定的,不需要靠 aspectRatio 维持等比(见文件头注释)
3239
+ height: options.height,
3240
+ style: { radius: 6, overflow: "hidden" },
3241
+ onShow: {
3242
+ type: "track",
3243
+ event: "drawer_banner_show",
3244
+ params: { scene: options.scene }
3245
+ },
3246
+ onClose: {
3247
+ type: "track",
3248
+ event: "drawer_banner_dismiss",
3249
+ params: { scene: options.scene, reason: "{{ closeReason }}" }
3250
+ }
3251
+ },
3252
+ // 高度已确定,节点直接铺在 stage 上按 rect 绝对定位,不需要额外套一层定位容器
3253
+ nodes: [
3254
+ {
3255
+ type: "image",
3256
+ rect: [0, 0, "100%", "100%"],
3257
+ src: buildBannerImage(options),
3258
+ // 画布比例与坑位一致,cover 既不裁也不拉,图里元素与百分比热区天然对齐
3259
+ style: { objectFit: "cover" }
3260
+ },
3261
+ {
3262
+ // 左半边文案区:图里文字从 20/640 起,热区框大一圈
3263
+ type: "box",
3264
+ rect: ["2%", "15%", "56%", "70%"],
3265
+ style: { radius: 4, background: "rgba(255,255,255,0.14)", cursor: "pointer" },
3266
+ action: {
3267
+ type: "sequence",
3268
+ actions: [
3269
+ {
3270
+ type: "track",
3271
+ event: "drawer_banner_click",
3272
+ params: { scene: options.scene, area: "title" }
3273
+ },
3274
+ { type: "navigate", url: options.url, target: "_blank" }
3275
+ ]
3276
+ }
3277
+ },
3278
+ {
3279
+ // 右侧按钮:图里按钮是 400~550(62.5%~85.9%),热区照着框
3280
+ type: "box",
3281
+ rect: ["62%", "20%", "25%", "60%"],
3282
+ style: {
3283
+ radius: 999,
3284
+ background: "rgba(255,255,255,0.16)",
3285
+ cursor: "pointer"
3286
+ },
3287
+ action: {
3288
+ type: "sequence",
3289
+ actions: [
3290
+ {
3291
+ type: "track",
3292
+ event: "drawer_banner_click",
3293
+ params: { scene: options.scene, area: "cta" }
3294
+ },
3295
+ { type: "navigate", url: options.url, target: "_blank" }
3296
+ ]
3297
+ }
3298
+ },
3299
+ {
3300
+ // 关闭按钮贴右:x / y 用 calc 而不是百分比 —— 百分比按容器尺寸算,
3301
+ // 坑位宽度一调它就飘;calc 让它永远距右边缘 8px、垂直居中
3302
+ type: "text",
3303
+ content: "\u2715",
3304
+ rect: ["calc(100% - 20px)", "calc(50% - 6px)", 12, 12],
3305
+ style: {
3306
+ textAlign: "center",
3307
+ lineHeight: 12,
3308
+ radius: 6,
3309
+ background: "rgba(0,0,0,0.32)",
3310
+ color: "#fff",
3311
+ fontSize: 9,
3312
+ cursor: "pointer"
3313
+ },
3314
+ // 「不再提醒」写在 close 的 reason 上,上报给后端即可,不配 call 写本地存储
3315
+ action: { type: "close", reason: "user-dismiss" }
3316
+ }
3317
+ ]
3318
+ };
3319
+ }
3320
+ var drawerTopBanner = createDrawerBanner({
3321
+ name: "\u62BD\u5C49\u9876\u90E8 Banner\uFF08320\xD736\uFF09",
3322
+ height: 36,
3323
+ title: "AI \u4E00\u952E\u586B\u5199\u5C5E\u6027 \xB7 \u9650\u65F6\u514D\u8D39",
3324
+ cta: "\u53BB\u8BD5\u8BD5",
3325
+ colors: ["#2b6cf6", "#5aa0ff"],
3326
+ ctaColor: "#1a4fd0",
3327
+ scene: "product_edit_top",
3328
+ url: "https://maibaole.example.com/ai-attr"
3329
+ });
3330
+ var drawerBottomBanner = createDrawerBanner({
3331
+ name: "\u62BD\u5C49\u5E95\u90E8 Banner\uFF08400\xD745\uFF09",
3332
+ height: 45,
3333
+ title: "\u642C\u5BB6\u9047\u5230\u95EE\u9898\uFF1F\u4E13\u5C5E\u987E\u95EE 1v1",
3334
+ cta: "\u7ACB\u5373\u54A8\u8BE2",
3335
+ colors: ["#ff6a3d", "#ffa15c"],
3336
+ ctaColor: "#d43a1f",
3337
+ scene: "product_edit_bottom",
3338
+ url: "https://maibaole.example.com/support"
3339
+ });
3340
+
3208
3341
  // src/docs/examples/index.ts
3209
3342
  var HOTSPOT_BG = "rgba(255,68,51,0.16)";
3210
3343
  var imageHotspot = {
@@ -3436,7 +3569,17 @@ var examples = [
3436
3569
  dsl: confirmAndSwitch
3437
3570
  },
3438
3571
  { label: "\u2466 \u5207 tab \u6362\u80A4\u6362\u5E03\u5C40", value: "tabTheme", dsl: tab_theme_default },
3439
- { label: "\u2467 \u6574\u56FE Banner\uFF08\u7A84\u901A\u680F + \u70ED\u533A\uFF09", value: "imageBanner", dsl: image_banner_default }
3572
+ { label: "\u2467 \u6574\u56FE Banner\uFF08\u7A84\u901A\u680F + \u70ED\u533A\uFF09", value: "imageBanner", dsl: image_banner_default },
3573
+ {
3574
+ label: "\u2468 \u62BD\u5C49\u9876\u90E8 Banner\uFF08\u56FA\u5B9A 320\xD736\uFF09",
3575
+ value: "drawerTopBanner",
3576
+ dsl: drawerTopBanner
3577
+ },
3578
+ {
3579
+ label: "\u2469 \u62BD\u5C49\u5E95\u90E8 Banner\uFF08\u56FA\u5B9A 400\xD745\uFF09",
3580
+ value: "drawerBottomBanner",
3581
+ dsl: drawerBottomBanner
3582
+ }
3440
3583
  ];
3441
3584
  var examples_default = examples;
3442
3585
 
@@ -94,7 +94,8 @@ declare const meta: DocMeta;
94
94
  /**
95
95
  * 调试用示例 DSL。
96
96
  *
97
- * ①② 是最小骨架,③④ 是运营 demo 真实弹窗的完整复刻,⑤⑥ 是多视图,⑦ 是换肤换布局。
97
+ * ①② 是最小骨架,③④ 是运营 demo 真实弹窗的完整复刻,⑤⑥ 是多视图,⑦ 是换肤换布局,
98
+ * ⑧ 是通栏 banner(宽度由页面决定、只能声明比例),⑨⑩ 是商品编辑抽屉里两个**固定尺寸**坑位。
98
99
  * 后端数据源($source)用法见 ③。
99
100
  */
100
101
 
@@ -94,7 +94,8 @@ declare const meta: DocMeta;
94
94
  /**
95
95
  * 调试用示例 DSL。
96
96
  *
97
- * ①② 是最小骨架,③④ 是运营 demo 真实弹窗的完整复刻,⑤⑥ 是多视图,⑦ 是换肤换布局。
97
+ * ①② 是最小骨架,③④ 是运营 demo 真实弹窗的完整复刻,⑤⑥ 是多视图,⑦ 是换肤换布局,
98
+ * ⑧ 是通栏 banner(宽度由页面决定、只能声明比例),⑨⑩ 是商品编辑抽屉里两个**固定尺寸**坑位。
98
99
  * 后端数据源($source)用法见 ③。
99
100
  */
100
101
 
@@ -1363,6 +1363,12 @@ var sections_default = sections9;
1363
1363
 
1364
1364
  // src/docs/meta.ts
1365
1365
  var changelog = [
1366
+ {
1367
+ date: "2026-08-24",
1368
+ items: [
1369
+ "\u3010\u4FEE\u590D\u3011\u5173\u95ED\u6309\u94AE\u4E0A\u914D\u7684\u57CB\u70B9\uFF08sequence: [track, close]\uFF09\u4E4B\u524D\u4F1A\u4E22\u5931\u3001\u8FD8\u4F1A\u4E32\u5230\u4E0B\u4E00\u6B21\u5173\u95ED\u7684\u4E0A\u62A5\u91CC \u2014\u2014 \u89E3\u91CA\u5668\u6D3E\u53D1 close \u4E8B\u4EF6\u65F6\u6162\u4E86\u4E00\u62CD\uFF0C\u5BBF\u4E3B\u8BFB\u4E0D\u5230\u57CB\u70B9\u540D\u3002\u73B0\u5DF2\u4FEE\u6B63\u987A\u5E8F\uFF0C\u8FD0\u8425\u7684\u914D\u7F6E\u5199\u6CD5\u4E0D\u7528\u6539"
1370
+ ]
1371
+ },
1366
1372
  {
1367
1373
  date: "2026-08-22",
1368
1374
  items: [
@@ -3080,6 +3086,133 @@ var imageBanner = {
3080
3086
  };
3081
3087
  var image_banner_default = imageBanner;
3082
3088
 
3089
+ // src/docs/examples/drawer-banner.ts
3090
+ function buildBannerImage(options) {
3091
+ const [from, to] = options.colors;
3092
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="640" height="72" viewBox="0 0 640 72">
3093
+ <defs>
3094
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="0">
3095
+ <stop offset="0" stop-color="${from}"/>
3096
+ <stop offset="1" stop-color="${to}"/>
3097
+ </linearGradient>
3098
+ </defs>
3099
+ <rect width="640" height="72" fill="url(#g)"/>
3100
+ <text x="20" y="44" font-family="sans-serif" font-size="22" fill="#ffffff">${options.title}</text>
3101
+ <rect x="400" y="18" width="150" height="36" rx="18" fill="#ffffff" fill-opacity="0.92"/>
3102
+ <text x="475" y="43" font-family="sans-serif" font-size="20" fill="${options.ctaColor}" text-anchor="middle">${options.cta}</text>
3103
+ </svg>`;
3104
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
3105
+ }
3106
+ function createDrawerBanner(options) {
3107
+ return {
3108
+ version: 1,
3109
+ type: "banner",
3110
+ meta: { name: options.name },
3111
+ stage: {
3112
+ width: "100%",
3113
+ // 高度写死:坑位宽度是宿主固定的,不需要靠 aspectRatio 维持等比(见文件头注释)
3114
+ height: options.height,
3115
+ style: { radius: 6, overflow: "hidden" },
3116
+ onShow: {
3117
+ type: "track",
3118
+ event: "drawer_banner_show",
3119
+ params: { scene: options.scene }
3120
+ },
3121
+ onClose: {
3122
+ type: "track",
3123
+ event: "drawer_banner_dismiss",
3124
+ params: { scene: options.scene, reason: "{{ closeReason }}" }
3125
+ }
3126
+ },
3127
+ // 高度已确定,节点直接铺在 stage 上按 rect 绝对定位,不需要额外套一层定位容器
3128
+ nodes: [
3129
+ {
3130
+ type: "image",
3131
+ rect: [0, 0, "100%", "100%"],
3132
+ src: buildBannerImage(options),
3133
+ // 画布比例与坑位一致,cover 既不裁也不拉,图里元素与百分比热区天然对齐
3134
+ style: { objectFit: "cover" }
3135
+ },
3136
+ {
3137
+ // 左半边文案区:图里文字从 20/640 起,热区框大一圈
3138
+ type: "box",
3139
+ rect: ["2%", "15%", "56%", "70%"],
3140
+ style: { radius: 4, background: "rgba(255,255,255,0.14)", cursor: "pointer" },
3141
+ action: {
3142
+ type: "sequence",
3143
+ actions: [
3144
+ {
3145
+ type: "track",
3146
+ event: "drawer_banner_click",
3147
+ params: { scene: options.scene, area: "title" }
3148
+ },
3149
+ { type: "navigate", url: options.url, target: "_blank" }
3150
+ ]
3151
+ }
3152
+ },
3153
+ {
3154
+ // 右侧按钮:图里按钮是 400~550(62.5%~85.9%),热区照着框
3155
+ type: "box",
3156
+ rect: ["62%", "20%", "25%", "60%"],
3157
+ style: {
3158
+ radius: 999,
3159
+ background: "rgba(255,255,255,0.16)",
3160
+ cursor: "pointer"
3161
+ },
3162
+ action: {
3163
+ type: "sequence",
3164
+ actions: [
3165
+ {
3166
+ type: "track",
3167
+ event: "drawer_banner_click",
3168
+ params: { scene: options.scene, area: "cta" }
3169
+ },
3170
+ { type: "navigate", url: options.url, target: "_blank" }
3171
+ ]
3172
+ }
3173
+ },
3174
+ {
3175
+ // 关闭按钮贴右:x / y 用 calc 而不是百分比 —— 百分比按容器尺寸算,
3176
+ // 坑位宽度一调它就飘;calc 让它永远距右边缘 8px、垂直居中
3177
+ type: "text",
3178
+ content: "\u2715",
3179
+ rect: ["calc(100% - 20px)", "calc(50% - 6px)", 12, 12],
3180
+ style: {
3181
+ textAlign: "center",
3182
+ lineHeight: 12,
3183
+ radius: 6,
3184
+ background: "rgba(0,0,0,0.32)",
3185
+ color: "#fff",
3186
+ fontSize: 9,
3187
+ cursor: "pointer"
3188
+ },
3189
+ // 「不再提醒」写在 close 的 reason 上,上报给后端即可,不配 call 写本地存储
3190
+ action: { type: "close", reason: "user-dismiss" }
3191
+ }
3192
+ ]
3193
+ };
3194
+ }
3195
+ var drawerTopBanner = createDrawerBanner({
3196
+ name: "\u62BD\u5C49\u9876\u90E8 Banner\uFF08320\xD736\uFF09",
3197
+ height: 36,
3198
+ title: "AI \u4E00\u952E\u586B\u5199\u5C5E\u6027 \xB7 \u9650\u65F6\u514D\u8D39",
3199
+ cta: "\u53BB\u8BD5\u8BD5",
3200
+ colors: ["#2b6cf6", "#5aa0ff"],
3201
+ ctaColor: "#1a4fd0",
3202
+ scene: "product_edit_top",
3203
+ url: "https://maibaole.example.com/ai-attr"
3204
+ });
3205
+ var drawerBottomBanner = createDrawerBanner({
3206
+ name: "\u62BD\u5C49\u5E95\u90E8 Banner\uFF08400\xD745\uFF09",
3207
+ height: 45,
3208
+ title: "\u642C\u5BB6\u9047\u5230\u95EE\u9898\uFF1F\u4E13\u5C5E\u987E\u95EE 1v1",
3209
+ cta: "\u7ACB\u5373\u54A8\u8BE2",
3210
+ colors: ["#ff6a3d", "#ffa15c"],
3211
+ ctaColor: "#d43a1f",
3212
+ scene: "product_edit_bottom",
3213
+ url: "https://maibaole.example.com/support"
3214
+ });
3215
+
3083
3216
  // src/docs/examples/index.ts
3084
3217
  var HOTSPOT_BG = "rgba(255,68,51,0.16)";
3085
3218
  var imageHotspot = {
@@ -3311,7 +3444,17 @@ var examples = [
3311
3444
  dsl: confirmAndSwitch
3312
3445
  },
3313
3446
  { label: "\u2466 \u5207 tab \u6362\u80A4\u6362\u5E03\u5C40", value: "tabTheme", dsl: tab_theme_default },
3314
- { label: "\u2467 \u6574\u56FE Banner\uFF08\u7A84\u901A\u680F + \u70ED\u533A\uFF09", value: "imageBanner", dsl: image_banner_default }
3447
+ { label: "\u2467 \u6574\u56FE Banner\uFF08\u7A84\u901A\u680F + \u70ED\u533A\uFF09", value: "imageBanner", dsl: image_banner_default },
3448
+ {
3449
+ label: "\u2468 \u62BD\u5C49\u9876\u90E8 Banner\uFF08\u56FA\u5B9A 320\xD736\uFF09",
3450
+ value: "drawerTopBanner",
3451
+ dsl: drawerTopBanner
3452
+ },
3453
+ {
3454
+ label: "\u2469 \u62BD\u5C49\u5E95\u90E8 Banner\uFF08\u56FA\u5B9A 400\xD745\uFF09",
3455
+ value: "drawerBottomBanner",
3456
+ dsl: drawerBottomBanner
3457
+ }
3315
3458
  ];
3316
3459
  var examples_default = examples;
3317
3460
 
package/dist/index.cjs CHANGED
@@ -1172,6 +1172,12 @@ function createRuntime(dsl, options = {}) {
1172
1172
  notify();
1173
1173
  fireLifecycle(name, "onShow");
1174
1174
  }
1175
+ function emitClose(why) {
1176
+ Promise.resolve().then(() => {
1177
+ if (destroyed) return;
1178
+ emit("close", { reason: why });
1179
+ });
1180
+ }
1175
1181
  function closeTop(reason) {
1176
1182
  const why = reason || "action";
1177
1183
  if (viewStack.length > 1) {
@@ -1188,12 +1194,12 @@ function createRuntime(dsl, options = {}) {
1188
1194
  return;
1189
1195
  }
1190
1196
  fireLifecycle(viewStack[0], "onClose", { closeReason: why });
1191
- emit("close", { reason: why });
1197
+ emitClose(why);
1192
1198
  }
1193
1199
  function closeAll(reason) {
1194
1200
  const why = reason || "action";
1195
1201
  viewStack.slice().reverse().forEach((name) => fireLifecycle(name, "onClose", { closeReason: why }));
1196
- emit("close", { reason: why });
1202
+ emitClose(why);
1197
1203
  }
1198
1204
  const dispatch = createDispatcher({
1199
1205
  setState,
package/dist/index.js CHANGED
@@ -644,6 +644,12 @@ function createRuntime(dsl, options = {}) {
644
644
  notify();
645
645
  fireLifecycle(name, "onShow");
646
646
  }
647
+ function emitClose(why) {
648
+ Promise.resolve().then(() => {
649
+ if (destroyed) return;
650
+ emit("close", { reason: why });
651
+ });
652
+ }
647
653
  function closeTop(reason) {
648
654
  const why = reason || "action";
649
655
  if (viewStack.length > 1) {
@@ -660,12 +666,12 @@ function createRuntime(dsl, options = {}) {
660
666
  return;
661
667
  }
662
668
  fireLifecycle(viewStack[0], "onClose", { closeReason: why });
663
- emit("close", { reason: why });
669
+ emitClose(why);
664
670
  }
665
671
  function closeAll(reason) {
666
672
  const why = reason || "action";
667
673
  viewStack.slice().reverse().forEach((name) => fireLifecycle(name, "onClose", { closeReason: why }));
668
- emit("close", { reason: why });
674
+ emitClose(why);
669
675
  }
670
676
  const dispatch = createDispatcher({
671
677
  setState,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaoxiu/marketing-dsl",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "description": "营销弹窗 DSL 解释器核心,纯逻辑无框架依赖",
6
6
  "license": "MIT",