@yaoxiu/marketing-dsl 1.1.0 → 1.3.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.
package/README.md CHANGED
@@ -47,7 +47,7 @@ import { createRuntime } from '@yaoxiu/marketing-dsl';
47
47
 
48
48
  const runtime = createRuntime(dsl, {
49
49
  user: { shopName: '麦爆了旗舰店', version: '专业版' },
50
- sources: { listRenewTiers: (params) => api.getTiers(params) },
50
+ sources: { listRenewTiers: params => api.getTiers(params) },
51
51
  handlers: { setNeverRemind: () => localStorage.setItem('never', '1') },
52
52
  emit: (event, payload) => console.log(event, payload),
53
53
  });
@@ -56,11 +56,18 @@ const unsubscribe = runtime.subscribe(() => {
56
56
  render(runtime.getTree()); // 每次通知后重新取树画一遍
57
57
  });
58
58
 
59
+ // 用户信息异步到了、或者要切换编辑态,用 update 就地更新。
60
+ // 不要为此重建 runtime——重建会重新拉数据源、重放曝光埋点,
61
+ // 还会把用户已经切到的 tab 打回默认值。
62
+ runtime.update({ user: { shopName: '麦爆了旗舰店' } });
63
+
59
64
  // 卸载时必须调,否则倒计时定时器不会停
60
65
  runtime.destroy();
61
66
  unsubscribe();
62
67
  ```
63
68
 
69
+ `update` 能改 `user` / `handlers` / `editMode`。换 `dsl` 本身要重建 runtime。
70
+
64
71
  ## 渲染树
65
72
 
66
73
  `getTree()` 返回的是纯数据,所有解释工作都已经做完:插值算完了、条件判断过了、循环展开了、样式转成 CSS 了、坐标换算好了、动作绑好了。
@@ -97,14 +104,14 @@ unsubscribe();
97
104
 
98
105
  这几条是设计红线,不会因为「就差一点点」而放开:
99
106
 
100
- | 边界 | 做法 |
101
- | --- | --- |
102
- | 不执行任意代码 | 手写解析器,不用 `eval` / `new Function`;不支持函数调用和赋值 |
103
- | 不能沿原型链逃逸 | `constructor` / `__proto__` / `prototype` 一律读不到,函数值也读不到 |
104
- | 不能通过样式作恶 | 只有白名单里的属性会输出,`position: fixed` 这类会被丢弃 |
105
- | 不能执行 `javascript:` | 链接只允许 `http` / `https` / `mailto` / `tel` 和相对路径 |
106
- | 不能打任意接口 | 数据源用注册名引用,配置里写不了 URL |
107
- | 不能调任意方法 | `call` 只能调宿主注册在 `handlers` 里的方法 |
107
+ | 边界 | 做法 |
108
+ | ---------------------- | -------------------------------------------------------------------- |
109
+ | 不执行任意代码 | 手写解析器,不用 `eval` / `new Function`;不支持函数调用和赋值 |
110
+ | 不能沿原型链逃逸 | `constructor` / `__proto__` / `prototype` 一律读不到,函数值也读不到 |
111
+ | 不能通过样式作恶 | 只有白名单里的属性会输出,`position: fixed` 这类会被丢弃 |
112
+ | 不能执行 `javascript:` | 链接只允许 `http` / `https` / `mailto` / `tel` 和相对路径 |
113
+ | 不能打任意接口 | 数据源用注册名引用,配置里写不了 URL |
114
+ | 不能调任意方法 | `call` 只能调宿主注册在 `handlers` 里的方法 |
108
115
 
109
116
  ## 完整 DSL 语法
110
117
 
package/dist/index.cjs CHANGED
@@ -326,9 +326,11 @@ function safeImageUrl(input) {
326
326
 
327
327
  // src/actions.ts
328
328
  function createDispatcher(options) {
329
- const { setState, emit, openView, closeTop, closeAll, handlers = {}, editMode = false } = options;
329
+ const { setState, emit, openView, closeTop, closeAll } = options;
330
330
  function dispatch(action, context) {
331
331
  if (!action || !action.type) return;
332
+ const handlers = options.handlers || {};
333
+ const editMode = !!options.editMode;
332
334
  switch (action.type) {
333
335
  case "sequence":
334
336
  (action.actions || []).forEach((item) => dispatch(item, context));
@@ -398,10 +400,24 @@ function parseEndTime(to) {
398
400
  if (typeof to === "number") return to;
399
401
  if (to === void 0 || to === null || to === "") return 0;
400
402
  const raw = String(to).trim();
403
+ if (!raw) return 0;
401
404
  if (/^\d+$/.test(raw)) return Number(raw);
402
- const time = new Date(raw.replace(/-/g, "/")).getTime();
405
+ if (/^\d{4}年/.test(raw)) {
406
+ const normalizedZh = raw.replace(/年|月/g, "/").replace(/日/g, " ").replace(/时|点|分/g, ":").replace(/秒/g, "").replace(/:+/g, ":").replace(/\s+/g, " ").replace(/[:\s]+$/, "").replace(/(\s\d{1,2})$/, "$1:00").trim();
407
+ const time2 = new Date(normalizedZh).getTime();
408
+ return isNaN(time2) ? 0 : time2;
409
+ }
410
+ const isIso = /^\d{4}-\d{2}-\d{2}T/.test(raw);
411
+ const normalized = isIso ? raw : raw.replace(/-/g, "/");
412
+ const time = new Date(normalized).getTime();
403
413
  return isNaN(time) ? 0 : time;
404
414
  }
415
+ function isValidEndTime(to) {
416
+ if (typeof to === "number") return isFinite(to) && to > 0;
417
+ if (typeof to !== "string") return false;
418
+ if (to.indexOf("{{") > -1) return true;
419
+ return parseEndTime(to) > 0;
420
+ }
405
421
  function computeParts(endTime, now = Date.now()) {
406
422
  const raw = endTime - now;
407
423
  const remain = raw > 0 ? raw : 0;
@@ -608,7 +624,13 @@ function resolveTree(input) {
608
624
  );
609
625
  const countdowns = { endTimes: [], precision: "s" };
610
626
  const renderLayers = ready ? layers.map(
611
- (item, index) => resolveLayer(item.name, item.view, index === layers.length - 1, input, countdowns)
627
+ (item, index) => resolveLayer(
628
+ item.name,
629
+ item.view,
630
+ index === layers.length - 1,
631
+ input,
632
+ countdowns
633
+ )
612
634
  ) : [];
613
635
  return {
614
636
  ready: ready && layers.length > 0,
@@ -627,7 +649,9 @@ function resolveLayer(name, view, isTop, input, countdowns) {
627
649
  const rootLayout = stage.layout === "flow" ? "flow" : "absolute";
628
650
  const maskClosable = !!stage.maskClosable;
629
651
  const onMaskClick = maskClosable ? () => closeTop("mask") : void 0;
630
- const nodes = (view.nodes || []).map((node, index) => resolveNode(node, `${name}-${index}`, rootLayout, context, input, countdowns)).filter((el) => !!el);
652
+ const nodes = (view.nodes || []).map(
653
+ (node, index) => resolveNode(node, `${name}-${index}`, rootLayout, context, input, countdowns)
654
+ ).filter((el) => !!el);
631
655
  return {
632
656
  name,
633
657
  type: view.type,
@@ -690,8 +714,12 @@ function resolveStageStyle(stage, isPopup, context) {
690
714
  base,
691
715
  {
692
716
  // width / height 支持数字(px)、'100%' / 'auto' / calc(...) 等相对单位,也支持 {{ }}
693
- width: toLength(interpolate(stage.width === void 0 ? 320 : stage.width, context)),
694
- height: toLength(interpolate(stage.height === void 0 ? 400 : stage.height, context))
717
+ width: toLength(
718
+ interpolate(stage.width === void 0 ? 320 : stage.width, context)
719
+ ),
720
+ height: toLength(
721
+ interpolate(stage.height === void 0 ? 400 : stage.height, context)
722
+ )
695
723
  },
696
724
  // stage.style 也走一遍插值,这样切 tab 时弹窗本身的背景能跟着变
697
725
  toCssStyle(interpolateDeep(stage.style, context))
@@ -746,7 +774,12 @@ function resolveCloseButton(config, layerName, closeTop) {
746
774
  tag: "img",
747
775
  className: "dsl-close-img",
748
776
  src: image,
749
- style: { width: "100%", height: "100%", objectFit: "contain", display: "block" }
777
+ style: {
778
+ width: "100%",
779
+ height: "100%",
780
+ objectFit: "contain",
781
+ display: "block"
782
+ }
750
783
  }
751
784
  ]
752
785
  };
@@ -796,7 +829,14 @@ function resolveNode(node, key, layout, context, input, countdowns) {
796
829
  className: nodeClass("box", !!onClick),
797
830
  style: Object.assign({ position: "relative" }, style),
798
831
  onClick,
799
- children: resolveChildren(node.children, key, "absolute", context, input, countdowns)
832
+ children: resolveChildren(
833
+ node.children,
834
+ key,
835
+ "absolute",
836
+ context,
837
+ input,
838
+ countdowns
839
+ )
800
840
  };
801
841
  case "flex":
802
842
  return {
@@ -881,7 +921,14 @@ function resolveRepeat(node, key, context, input, countdowns) {
881
921
  [node.itemName || "item"]: item,
882
922
  [node.indexName || "index"]: index
883
923
  });
884
- return resolveNode(node.template, `${key}-${index}`, "flow", itemContext, input, countdowns);
924
+ return resolveNode(
925
+ node.template,
926
+ `${key}-${index}`,
927
+ "flow",
928
+ itemContext,
929
+ input,
930
+ countdowns
931
+ );
885
932
  }).filter((el) => !!el);
886
933
  }
887
934
  function resolveTabs(node, key, context, input) {
@@ -923,10 +970,13 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
923
970
  const parts = computeParts(endTime);
924
971
  countdowns.endTimes.push(endTime);
925
972
  if (node.precision === "cs") countdowns.precision = "cs";
926
- if (parts.ended && endTime && node.onEnd) {
973
+ if (endTime && node.onEnd) {
927
974
  const token = `${key}@${endTime}`;
928
- if (!input.firedEnds.has(token)) {
929
- input.firedEnds.add(token);
975
+ const status = input.countdownEnds.get(token);
976
+ if (status === void 0) {
977
+ input.countdownEnds.set(token, parts.ended ? "done" : "waiting");
978
+ } else if (status === "waiting" && parts.ended) {
979
+ input.countdownEnds.set(token, "done");
930
980
  input.dispatch(node.onEnd, context);
931
981
  }
932
982
  }
@@ -954,7 +1004,14 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
954
1004
  tag: "div",
955
1005
  className: "dsl-node dsl-flex dsl-countdown",
956
1006
  style: Object.assign({ display: "flex" }, style),
957
- children: resolveChildren(node.children, key, "flow", childContext, input, countdowns)
1007
+ children: resolveChildren(
1008
+ node.children,
1009
+ key,
1010
+ "flow",
1011
+ childContext,
1012
+ input,
1013
+ countdowns
1014
+ )
958
1015
  };
959
1016
  }
960
1017
  function toText(value) {
@@ -965,7 +1022,12 @@ function toText(value) {
965
1022
  var noopEmit = () => {
966
1023
  };
967
1024
  function createRuntime(dsl, options = {}) {
968
- const { user = {}, sources = {}, handlers = {}, editMode = false, emit = noopEmit } = options;
1025
+ const { sources = {}, emit = noopEmit } = options;
1026
+ const live = {
1027
+ user: options.user || {},
1028
+ handlers: options.handlers || {},
1029
+ editMode: !!options.editMode
1030
+ };
969
1031
  const normalized = normalizeViews(dsl);
970
1032
  let state = Object.assign({}, dsl.state || {});
971
1033
  let resolvedData = {};
@@ -974,14 +1036,17 @@ function createRuntime(dsl, options = {}) {
974
1036
  let loadFailed = false;
975
1037
  let destroyed = false;
976
1038
  const listeners = /* @__PURE__ */ new Set();
977
- const firedEnds = /* @__PURE__ */ new Set();
1039
+ const countdownEnds = /* @__PURE__ */ new Map();
978
1040
  const notify = () => {
979
1041
  if (destroyed) return;
980
1042
  listeners.forEach((fn) => fn());
981
1043
  };
982
1044
  const ticker = createTicker({ onTick: notify });
983
1045
  function buildContext() {
984
- const context = Object.assign({}, resolvedData, { state, user });
1046
+ const context = Object.assign({}, resolvedData, {
1047
+ state,
1048
+ user: live.user
1049
+ });
985
1050
  const derived = dsl.derived || {};
986
1051
  Object.keys(derived).forEach((name) => {
987
1052
  const config = derived[name] || {};
@@ -1042,8 +1107,12 @@ function createRuntime(dsl, options = {}) {
1042
1107
  // 配置里 close 可以自带 reason(如 'never-remind'),要透传给埋点,不能吞掉
1043
1108
  closeTop: (reason) => closeTop(reason),
1044
1109
  closeAll: (reason) => closeAll(reason),
1045
- handlers,
1046
- editMode
1110
+ get handlers() {
1111
+ return live.handlers;
1112
+ },
1113
+ get editMode() {
1114
+ return live.editMode;
1115
+ }
1047
1116
  });
1048
1117
  function fireLifecycle(viewName, hook, extra) {
1049
1118
  const view = normalized.views[viewName];
@@ -1070,7 +1139,7 @@ function createRuntime(dsl, options = {}) {
1070
1139
  return;
1071
1140
  }
1072
1141
  pending.push(
1073
- Promise.resolve(source(ref.params || {}, user)).then((result) => {
1142
+ Promise.resolve(source(ref.params || {}, live.user)).then((result) => {
1074
1143
  resolved[key] = result;
1075
1144
  })
1076
1145
  );
@@ -1101,13 +1170,19 @@ function createRuntime(dsl, options = {}) {
1101
1170
  dispatch,
1102
1171
  setState,
1103
1172
  closeTop,
1104
- firedEnds
1173
+ countdownEnds
1105
1174
  });
1106
1175
  ticker.sync(tree.countdownEndTimes, tree.countdownPrecision);
1107
1176
  return tree;
1108
1177
  }
1109
1178
  return {
1110
1179
  getTree,
1180
+ update(patch) {
1181
+ if (patch.user !== void 0) live.user = patch.user;
1182
+ if (patch.handlers !== void 0) live.handlers = patch.handlers;
1183
+ if (patch.editMode !== void 0) live.editMode = patch.editMode;
1184
+ notify();
1185
+ },
1111
1186
  subscribe(listener) {
1112
1187
  listeners.add(listener);
1113
1188
  return () => listeners.delete(listener);
@@ -1186,7 +1261,10 @@ function validate(dsl) {
1186
1261
  }
1187
1262
  const doc = dsl;
1188
1263
  if (doc.version !== DSL_VERSION) {
1189
- add("version", `version \u5FC5\u987B\u662F ${DSL_VERSION}\uFF0C\u5F53\u524D\u4E3A ${JSON.stringify(doc.version)}`);
1264
+ add(
1265
+ "version",
1266
+ `version \u5FC5\u987B\u662F ${DSL_VERSION}\uFF0C\u5F53\u524D\u4E3A ${JSON.stringify(doc.version)}`
1267
+ );
1190
1268
  }
1191
1269
  const dataKeys = validateData(doc.data, add);
1192
1270
  const stateKeys = validateState(doc.state, add);
@@ -1286,7 +1364,10 @@ function validateStage(stage, add, warn, ctx, prefix) {
1286
1364
  if (stage.onShow !== void 0) validateAction(stage.onShow, at("onShow"), ctx);
1287
1365
  if (stage.onClose !== void 0) validateAction(stage.onClose, at("onClose"), ctx);
1288
1366
  if (stage.height === "auto" && stage.layout !== "flow") {
1289
- add("stage.height", "height: 'auto' \u9700\u8981\u540C\u65F6\u8BBE\u7F6E stage.layout: 'flow'\uFF0C\u5426\u5219\u9AD8\u5EA6\u4F1A\u584C\u6210 0");
1367
+ add(
1368
+ "stage.height",
1369
+ "height: 'auto' \u9700\u8981\u540C\u65F6\u8BBE\u7F6E stage.layout: 'flow'\uFF0C\u5426\u5219\u9AD8\u5EA6\u4F1A\u584C\u6210 0"
1370
+ );
1290
1371
  }
1291
1372
  }
1292
1373
  function validateCloseButton(config, add, warn) {
@@ -1300,7 +1381,10 @@ function validateCloseButton(config, add, warn) {
1300
1381
  }
1301
1382
  if (config.offset !== void 0) {
1302
1383
  if (!Array.isArray(config.offset) || config.offset.length !== 2) {
1303
- add("stage.closeButton.offset", "offset \u5FC5\u987B\u662F [x, y] \u4E24\u9879\u6570\u7EC4\uFF0C\u8D1F\u503C\u4F1A\u628A\u6309\u94AE\u79FB\u5230\u5F39\u7A97\u5916\u9762");
1384
+ add(
1385
+ "stage.closeButton.offset",
1386
+ "offset \u5FC5\u987B\u662F [x, y] \u4E24\u9879\u6570\u7EC4\uFF0C\u8D1F\u503C\u4F1A\u628A\u6309\u94AE\u79FB\u5230\u5F39\u7A97\u5916\u9762"
1387
+ );
1304
1388
  } else {
1305
1389
  config.offset.forEach((value, index) => {
1306
1390
  if (!checkLength(value)) {
@@ -1321,7 +1405,10 @@ function validateCloseButton(config, add, warn) {
1321
1405
  if (config.style) {
1322
1406
  Object.keys(config.style).forEach((key) => {
1323
1407
  if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
1324
- warn(`stage.closeButton.style.${key}`, `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`);
1408
+ warn(
1409
+ `stage.closeButton.style.${key}`,
1410
+ `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`
1411
+ );
1325
1412
  }
1326
1413
  });
1327
1414
  }
@@ -1392,7 +1479,10 @@ function validateNode(node, path, layout, ctx) {
1392
1479
  } else {
1393
1480
  node.rect.forEach((value, index) => {
1394
1481
  if (value !== void 0 && value !== null && !checkLength(value)) {
1395
- add(`${path}.rect[${index}]`, `"${value}" \u4E0D\u662F\u5408\u6CD5\u957F\u5EA6\uFF0C\u652F\u6301\u6570\u5B57(px) / '100%' / 'auto' \u7B49`);
1482
+ add(
1483
+ `${path}.rect[${index}]`,
1484
+ `"${value}" \u4E0D\u662F\u5408\u6CD5\u957F\u5EA6\uFF0C\u652F\u6301\u6570\u5B57(px) / '100%' / 'auto' \u7B49`
1485
+ );
1396
1486
  }
1397
1487
  });
1398
1488
  }
@@ -1410,7 +1500,10 @@ function validateNode(node, path, layout, ctx) {
1410
1500
  );
1411
1501
  }
1412
1502
  if (key === "aspectRatio" && !checkAspectRatio(node.style[key])) {
1413
- add(`${path}.style.aspectRatio`, "aspectRatio \u8981\u5199\u6210 '750 / 200' \u6216 1.5 \u8FD9\u6837\u7684\u5BBD\u9AD8\u6BD4");
1503
+ add(
1504
+ `${path}.style.aspectRatio`,
1505
+ "aspectRatio \u8981\u5199\u6210 '750 / 200' \u6216 1.5 \u8FD9\u6837\u7684\u5BBD\u9AD8\u6BD4"
1506
+ );
1414
1507
  }
1415
1508
  });
1416
1509
  }
@@ -1440,13 +1533,21 @@ function validateNodeByType(node, path, ctx) {
1440
1533
  if (typeof node.bind !== "string") {
1441
1534
  add(`${path}.bind`, "tabs \u5FC5\u987B\u63D0\u4F9B bind\uFF08data \u4E2D\u7684\u6570\u7EC4\u5B57\u6BB5\u540D\uFF09");
1442
1535
  }
1443
- if (typeof node.stateKey !== "string") add(`${path}.stateKey`, "tabs \u5FC5\u987B\u63D0\u4F9B stateKey");
1536
+ if (typeof node.stateKey !== "string")
1537
+ add(`${path}.stateKey`, "tabs \u5FC5\u987B\u63D0\u4F9B stateKey");
1444
1538
  if (node.labelField !== void 0 && typeof node.labelField !== "string") {
1445
1539
  add(`${path}.labelField`, "labelField \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
1446
1540
  }
1447
1541
  }
1448
1542
  if (node.type === "countdown") {
1449
- if (!node.to) add(`${path}.to`, "countdown \u5FC5\u987B\u63D0\u4F9B to\uFF08\u7ED3\u675F\u65F6\u95F4\uFF09");
1543
+ if (!node.to) {
1544
+ add(`${path}.to`, "countdown \u5FC5\u987B\u63D0\u4F9B to\uFF08\u7ED3\u675F\u65F6\u95F4\uFF09");
1545
+ } else if (!isValidEndTime(node.to)) {
1546
+ add(
1547
+ `${path}.to`,
1548
+ `"${node.to}" \u4E0D\u662F\u80FD\u8BC6\u522B\u7684\u65F6\u95F4\u3002\u53EF\u4EE5\u5199 "2026-09-01 18:00:00" / "2026-09-01" / "2026\u5E749\u67081\u65E5 18\u70B9" / ISO \u683C\u5F0F / \u65F6\u95F4\u6233`
1549
+ );
1550
+ }
1450
1551
  if (node.precision !== void 0 && ["s", "cs"].indexOf(node.precision) === -1) {
1451
1552
  add(`${path}.precision`, "precision \u53EA\u80FD\u662F 's'\uFF08\u6BCF\u79D2\uFF09\u6216 'cs'\uFF08\u5398\u79D2\uFF0C\u9010\u5E27\u5237\u65B0\uFF09");
1452
1553
  }
@@ -1506,7 +1607,10 @@ function validateAction(action, path, ctx) {
1506
1607
  add(`${path}.view`, `views \u91CC\u4E0D\u5B58\u5728\u89C6\u56FE "${action.view}"`);
1507
1608
  }
1508
1609
  if (action.mode !== void 0 && ["stack", "replace"].indexOf(action.mode) === -1) {
1509
- add(`${path}.mode`, "mode \u53EA\u80FD\u662F 'stack'\uFF08\u9ED8\u8BA4\uFF0C\u53E0\u4E00\u5C42\uFF09\u6216 'replace'\uFF08\u6362\u6389\u5F53\u524D\u5C42\uFF09");
1610
+ add(
1611
+ `${path}.mode`,
1612
+ "mode \u53EA\u80FD\u662F 'stack'\uFF08\u9ED8\u8BA4\uFF0C\u53E0\u4E00\u5C42\uFF09\u6216 'replace'\uFF08\u6362\u6389\u5F53\u524D\u5C42\uFF09"
1613
+ );
1510
1614
  }
1511
1615
  }
1512
1616
  if (action.type === "sequence") {
@@ -1538,6 +1642,7 @@ exports.formatParts = formatParts;
1538
1642
  exports.interpolate = interpolate;
1539
1643
  exports.interpolateDeep = interpolateDeep;
1540
1644
  exports.isLength = isLength;
1645
+ exports.isValidEndTime = isValidEndTime;
1541
1646
  exports.normalizeViews = normalizeViews;
1542
1647
  exports.parseEndTime = parseEndTime;
1543
1648
  exports.safeImageUrl = safeImageUrl;
package/dist/index.d.cts CHANGED
@@ -293,6 +293,16 @@ interface RuntimeOptions {
293
293
  interface DslRuntime {
294
294
  /** 当前渲染树。每次 subscribe 回调后重新取 */
295
295
  getTree: () => RenderTree;
296
+ /**
297
+ * 更新宿主传进来的东西。
298
+ *
299
+ * user 常常是异步到的(弹窗先挂载、用户信息后到),
300
+ * 不给更新入口的话文案就永远停在初始值。
301
+ * editMode / handlers 同理,宿主可能中途换。
302
+ *
303
+ * 不会重新拉数据源,也不会重放 onShow 曝光埋点——那些是一次性的。
304
+ */
305
+ update: (patch: Pick<RuntimeOptions, 'user' | 'handlers' | 'editMode'>) => void;
296
306
  /** 状态变化时回调,返回取消订阅的函数 */
297
307
  subscribe: (listener: () => void) => () => void;
298
308
  /** 供调试台等外部读取,正常渲染不需要 */
@@ -428,9 +438,16 @@ interface CountdownParts {
428
438
  type CountdownPrecision = 's' | 'cs';
429
439
  /** 把 to 解析成时间戳。兼容 '2026-09-01 00:00:00' 这种 Safari 不认的格式 */
430
440
  declare function parseEndTime(to: unknown): number;
441
+ /**
442
+ * 判断一个 to 能不能解析。给校验用。
443
+ *
444
+ * 解析失败会当成「已结束」,页面上只会看到一行「已结束」,
445
+ * 运营根本意识不到是自己时间写错了——所以必须在保存前就拦下来。
446
+ */
447
+ declare function isValidEndTime(to: unknown): boolean;
431
448
  /** 算出某个时刻的时间片段 */
432
449
  declare function computeParts(endTime: number, now?: number): CountdownParts;
433
450
  /** 不给 children 时退化成一行文本,占位符 {d} {h} {hAll} {m} {s} {cs} */
434
451
  declare function formatParts(parts: CountdownParts, format?: string): string;
435
452
 
436
- export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, type CssStyle, DSL_VERSION, type Dsl, type DslAction, type DslActionBase, type DslCallAction, type DslCloseAction, type DslCloseAllAction, type DslCloseButton, type DslClosePosition, type DslDerived, type DslHandler, type DslLength, type DslNavigateAction, type DslNode, type DslNodeType, type DslOpenAction, type DslRect, type DslRuntime, type DslSequenceAction, type DslSetStateAction, type DslSource, type DslSourceRef, type DslStage, type DslStyle, type DslTrackAction, type DslView, type DslViewType, type Issue, NODE_TYPES, type NormalizedViews, type RenderElement, type RenderLayer, type RenderTree, type RuntimeEmit, type RuntimeEventName, type RuntimeEvents, type RuntimeOptions, SINGLE_VIEW_NAME, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
453
+ export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, type CssStyle, DSL_VERSION, type Dsl, type DslAction, type DslActionBase, type DslCallAction, type DslCloseAction, type DslCloseAllAction, type DslCloseButton, type DslClosePosition, type DslDerived, type DslHandler, type DslLength, type DslNavigateAction, type DslNode, type DslNodeType, type DslOpenAction, type DslRect, type DslRuntime, type DslSequenceAction, type DslSetStateAction, type DslSource, type DslSourceRef, type DslStage, type DslStyle, type DslTrackAction, type DslView, type DslViewType, type Issue, NODE_TYPES, type NormalizedViews, type RenderElement, type RenderLayer, type RenderTree, type RuntimeEmit, type RuntimeEventName, type RuntimeEvents, type RuntimeOptions, SINGLE_VIEW_NAME, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
package/dist/index.d.ts CHANGED
@@ -293,6 +293,16 @@ interface RuntimeOptions {
293
293
  interface DslRuntime {
294
294
  /** 当前渲染树。每次 subscribe 回调后重新取 */
295
295
  getTree: () => RenderTree;
296
+ /**
297
+ * 更新宿主传进来的东西。
298
+ *
299
+ * user 常常是异步到的(弹窗先挂载、用户信息后到),
300
+ * 不给更新入口的话文案就永远停在初始值。
301
+ * editMode / handlers 同理,宿主可能中途换。
302
+ *
303
+ * 不会重新拉数据源,也不会重放 onShow 曝光埋点——那些是一次性的。
304
+ */
305
+ update: (patch: Pick<RuntimeOptions, 'user' | 'handlers' | 'editMode'>) => void;
296
306
  /** 状态变化时回调,返回取消订阅的函数 */
297
307
  subscribe: (listener: () => void) => () => void;
298
308
  /** 供调试台等外部读取,正常渲染不需要 */
@@ -428,9 +438,16 @@ interface CountdownParts {
428
438
  type CountdownPrecision = 's' | 'cs';
429
439
  /** 把 to 解析成时间戳。兼容 '2026-09-01 00:00:00' 这种 Safari 不认的格式 */
430
440
  declare function parseEndTime(to: unknown): number;
441
+ /**
442
+ * 判断一个 to 能不能解析。给校验用。
443
+ *
444
+ * 解析失败会当成「已结束」,页面上只会看到一行「已结束」,
445
+ * 运营根本意识不到是自己时间写错了——所以必须在保存前就拦下来。
446
+ */
447
+ declare function isValidEndTime(to: unknown): boolean;
431
448
  /** 算出某个时刻的时间片段 */
432
449
  declare function computeParts(endTime: number, now?: number): CountdownParts;
433
450
  /** 不给 children 时退化成一行文本,占位符 {d} {h} {hAll} {m} {s} {cs} */
434
451
  declare function formatParts(parts: CountdownParts, format?: string): string;
435
452
 
436
- export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, type CssStyle, DSL_VERSION, type Dsl, type DslAction, type DslActionBase, type DslCallAction, type DslCloseAction, type DslCloseAllAction, type DslCloseButton, type DslClosePosition, type DslDerived, type DslHandler, type DslLength, type DslNavigateAction, type DslNode, type DslNodeType, type DslOpenAction, type DslRect, type DslRuntime, type DslSequenceAction, type DslSetStateAction, type DslSource, type DslSourceRef, type DslStage, type DslStyle, type DslTrackAction, type DslView, type DslViewType, type Issue, NODE_TYPES, type NormalizedViews, type RenderElement, type RenderLayer, type RenderTree, type RuntimeEmit, type RuntimeEventName, type RuntimeEvents, type RuntimeOptions, SINGLE_VIEW_NAME, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
453
+ export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, type CssStyle, DSL_VERSION, type Dsl, type DslAction, type DslActionBase, type DslCallAction, type DslCloseAction, type DslCloseAllAction, type DslCloseButton, type DslClosePosition, type DslDerived, type DslHandler, type DslLength, type DslNavigateAction, type DslNode, type DslNodeType, type DslOpenAction, type DslRect, type DslRuntime, type DslSequenceAction, type DslSetStateAction, type DslSource, type DslSourceRef, type DslStage, type DslStyle, type DslTrackAction, type DslView, type DslViewType, type Issue, NODE_TYPES, type NormalizedViews, type RenderElement, type RenderLayer, type RenderTree, type RuntimeEmit, type RuntimeEventName, type RuntimeEvents, type RuntimeOptions, SINGLE_VIEW_NAME, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
package/dist/index.js CHANGED
@@ -324,9 +324,11 @@ function safeImageUrl(input) {
324
324
 
325
325
  // src/actions.ts
326
326
  function createDispatcher(options) {
327
- const { setState, emit, openView, closeTop, closeAll, handlers = {}, editMode = false } = options;
327
+ const { setState, emit, openView, closeTop, closeAll } = options;
328
328
  function dispatch(action, context) {
329
329
  if (!action || !action.type) return;
330
+ const handlers = options.handlers || {};
331
+ const editMode = !!options.editMode;
330
332
  switch (action.type) {
331
333
  case "sequence":
332
334
  (action.actions || []).forEach((item) => dispatch(item, context));
@@ -396,10 +398,24 @@ function parseEndTime(to) {
396
398
  if (typeof to === "number") return to;
397
399
  if (to === void 0 || to === null || to === "") return 0;
398
400
  const raw = String(to).trim();
401
+ if (!raw) return 0;
399
402
  if (/^\d+$/.test(raw)) return Number(raw);
400
- const time = new Date(raw.replace(/-/g, "/")).getTime();
403
+ if (/^\d{4}年/.test(raw)) {
404
+ const normalizedZh = raw.replace(/年|月/g, "/").replace(/日/g, " ").replace(/时|点|分/g, ":").replace(/秒/g, "").replace(/:+/g, ":").replace(/\s+/g, " ").replace(/[:\s]+$/, "").replace(/(\s\d{1,2})$/, "$1:00").trim();
405
+ const time2 = new Date(normalizedZh).getTime();
406
+ return isNaN(time2) ? 0 : time2;
407
+ }
408
+ const isIso = /^\d{4}-\d{2}-\d{2}T/.test(raw);
409
+ const normalized = isIso ? raw : raw.replace(/-/g, "/");
410
+ const time = new Date(normalized).getTime();
401
411
  return isNaN(time) ? 0 : time;
402
412
  }
413
+ function isValidEndTime(to) {
414
+ if (typeof to === "number") return isFinite(to) && to > 0;
415
+ if (typeof to !== "string") return false;
416
+ if (to.indexOf("{{") > -1) return true;
417
+ return parseEndTime(to) > 0;
418
+ }
403
419
  function computeParts(endTime, now = Date.now()) {
404
420
  const raw = endTime - now;
405
421
  const remain = raw > 0 ? raw : 0;
@@ -606,7 +622,13 @@ function resolveTree(input) {
606
622
  );
607
623
  const countdowns = { endTimes: [], precision: "s" };
608
624
  const renderLayers = ready ? layers.map(
609
- (item, index) => resolveLayer(item.name, item.view, index === layers.length - 1, input, countdowns)
625
+ (item, index) => resolveLayer(
626
+ item.name,
627
+ item.view,
628
+ index === layers.length - 1,
629
+ input,
630
+ countdowns
631
+ )
610
632
  ) : [];
611
633
  return {
612
634
  ready: ready && layers.length > 0,
@@ -625,7 +647,9 @@ function resolveLayer(name, view, isTop, input, countdowns) {
625
647
  const rootLayout = stage.layout === "flow" ? "flow" : "absolute";
626
648
  const maskClosable = !!stage.maskClosable;
627
649
  const onMaskClick = maskClosable ? () => closeTop("mask") : void 0;
628
- const nodes = (view.nodes || []).map((node, index) => resolveNode(node, `${name}-${index}`, rootLayout, context, input, countdowns)).filter((el) => !!el);
650
+ const nodes = (view.nodes || []).map(
651
+ (node, index) => resolveNode(node, `${name}-${index}`, rootLayout, context, input, countdowns)
652
+ ).filter((el) => !!el);
629
653
  return {
630
654
  name,
631
655
  type: view.type,
@@ -688,8 +712,12 @@ function resolveStageStyle(stage, isPopup, context) {
688
712
  base,
689
713
  {
690
714
  // width / height 支持数字(px)、'100%' / 'auto' / calc(...) 等相对单位,也支持 {{ }}
691
- width: toLength(interpolate(stage.width === void 0 ? 320 : stage.width, context)),
692
- height: toLength(interpolate(stage.height === void 0 ? 400 : stage.height, context))
715
+ width: toLength(
716
+ interpolate(stage.width === void 0 ? 320 : stage.width, context)
717
+ ),
718
+ height: toLength(
719
+ interpolate(stage.height === void 0 ? 400 : stage.height, context)
720
+ )
693
721
  },
694
722
  // stage.style 也走一遍插值,这样切 tab 时弹窗本身的背景能跟着变
695
723
  toCssStyle(interpolateDeep(stage.style, context))
@@ -744,7 +772,12 @@ function resolveCloseButton(config, layerName, closeTop) {
744
772
  tag: "img",
745
773
  className: "dsl-close-img",
746
774
  src: image,
747
- style: { width: "100%", height: "100%", objectFit: "contain", display: "block" }
775
+ style: {
776
+ width: "100%",
777
+ height: "100%",
778
+ objectFit: "contain",
779
+ display: "block"
780
+ }
748
781
  }
749
782
  ]
750
783
  };
@@ -794,7 +827,14 @@ function resolveNode(node, key, layout, context, input, countdowns) {
794
827
  className: nodeClass("box", !!onClick),
795
828
  style: Object.assign({ position: "relative" }, style),
796
829
  onClick,
797
- children: resolveChildren(node.children, key, "absolute", context, input, countdowns)
830
+ children: resolveChildren(
831
+ node.children,
832
+ key,
833
+ "absolute",
834
+ context,
835
+ input,
836
+ countdowns
837
+ )
798
838
  };
799
839
  case "flex":
800
840
  return {
@@ -879,7 +919,14 @@ function resolveRepeat(node, key, context, input, countdowns) {
879
919
  [node.itemName || "item"]: item,
880
920
  [node.indexName || "index"]: index
881
921
  });
882
- return resolveNode(node.template, `${key}-${index}`, "flow", itemContext, input, countdowns);
922
+ return resolveNode(
923
+ node.template,
924
+ `${key}-${index}`,
925
+ "flow",
926
+ itemContext,
927
+ input,
928
+ countdowns
929
+ );
883
930
  }).filter((el) => !!el);
884
931
  }
885
932
  function resolveTabs(node, key, context, input) {
@@ -921,10 +968,13 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
921
968
  const parts = computeParts(endTime);
922
969
  countdowns.endTimes.push(endTime);
923
970
  if (node.precision === "cs") countdowns.precision = "cs";
924
- if (parts.ended && endTime && node.onEnd) {
971
+ if (endTime && node.onEnd) {
925
972
  const token = `${key}@${endTime}`;
926
- if (!input.firedEnds.has(token)) {
927
- input.firedEnds.add(token);
973
+ const status = input.countdownEnds.get(token);
974
+ if (status === void 0) {
975
+ input.countdownEnds.set(token, parts.ended ? "done" : "waiting");
976
+ } else if (status === "waiting" && parts.ended) {
977
+ input.countdownEnds.set(token, "done");
928
978
  input.dispatch(node.onEnd, context);
929
979
  }
930
980
  }
@@ -952,7 +1002,14 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
952
1002
  tag: "div",
953
1003
  className: "dsl-node dsl-flex dsl-countdown",
954
1004
  style: Object.assign({ display: "flex" }, style),
955
- children: resolveChildren(node.children, key, "flow", childContext, input, countdowns)
1005
+ children: resolveChildren(
1006
+ node.children,
1007
+ key,
1008
+ "flow",
1009
+ childContext,
1010
+ input,
1011
+ countdowns
1012
+ )
956
1013
  };
957
1014
  }
958
1015
  function toText(value) {
@@ -963,7 +1020,12 @@ function toText(value) {
963
1020
  var noopEmit = () => {
964
1021
  };
965
1022
  function createRuntime(dsl, options = {}) {
966
- const { user = {}, sources = {}, handlers = {}, editMode = false, emit = noopEmit } = options;
1023
+ const { sources = {}, emit = noopEmit } = options;
1024
+ const live = {
1025
+ user: options.user || {},
1026
+ handlers: options.handlers || {},
1027
+ editMode: !!options.editMode
1028
+ };
967
1029
  const normalized = normalizeViews(dsl);
968
1030
  let state = Object.assign({}, dsl.state || {});
969
1031
  let resolvedData = {};
@@ -972,14 +1034,17 @@ function createRuntime(dsl, options = {}) {
972
1034
  let loadFailed = false;
973
1035
  let destroyed = false;
974
1036
  const listeners = /* @__PURE__ */ new Set();
975
- const firedEnds = /* @__PURE__ */ new Set();
1037
+ const countdownEnds = /* @__PURE__ */ new Map();
976
1038
  const notify = () => {
977
1039
  if (destroyed) return;
978
1040
  listeners.forEach((fn) => fn());
979
1041
  };
980
1042
  const ticker = createTicker({ onTick: notify });
981
1043
  function buildContext() {
982
- const context = Object.assign({}, resolvedData, { state, user });
1044
+ const context = Object.assign({}, resolvedData, {
1045
+ state,
1046
+ user: live.user
1047
+ });
983
1048
  const derived = dsl.derived || {};
984
1049
  Object.keys(derived).forEach((name) => {
985
1050
  const config = derived[name] || {};
@@ -1040,8 +1105,12 @@ function createRuntime(dsl, options = {}) {
1040
1105
  // 配置里 close 可以自带 reason(如 'never-remind'),要透传给埋点,不能吞掉
1041
1106
  closeTop: (reason) => closeTop(reason),
1042
1107
  closeAll: (reason) => closeAll(reason),
1043
- handlers,
1044
- editMode
1108
+ get handlers() {
1109
+ return live.handlers;
1110
+ },
1111
+ get editMode() {
1112
+ return live.editMode;
1113
+ }
1045
1114
  });
1046
1115
  function fireLifecycle(viewName, hook, extra) {
1047
1116
  const view = normalized.views[viewName];
@@ -1068,7 +1137,7 @@ function createRuntime(dsl, options = {}) {
1068
1137
  return;
1069
1138
  }
1070
1139
  pending.push(
1071
- Promise.resolve(source(ref.params || {}, user)).then((result) => {
1140
+ Promise.resolve(source(ref.params || {}, live.user)).then((result) => {
1072
1141
  resolved[key] = result;
1073
1142
  })
1074
1143
  );
@@ -1099,13 +1168,19 @@ function createRuntime(dsl, options = {}) {
1099
1168
  dispatch,
1100
1169
  setState,
1101
1170
  closeTop,
1102
- firedEnds
1171
+ countdownEnds
1103
1172
  });
1104
1173
  ticker.sync(tree.countdownEndTimes, tree.countdownPrecision);
1105
1174
  return tree;
1106
1175
  }
1107
1176
  return {
1108
1177
  getTree,
1178
+ update(patch) {
1179
+ if (patch.user !== void 0) live.user = patch.user;
1180
+ if (patch.handlers !== void 0) live.handlers = patch.handlers;
1181
+ if (patch.editMode !== void 0) live.editMode = patch.editMode;
1182
+ notify();
1183
+ },
1109
1184
  subscribe(listener) {
1110
1185
  listeners.add(listener);
1111
1186
  return () => listeners.delete(listener);
@@ -1184,7 +1259,10 @@ function validate(dsl) {
1184
1259
  }
1185
1260
  const doc = dsl;
1186
1261
  if (doc.version !== DSL_VERSION) {
1187
- add("version", `version \u5FC5\u987B\u662F ${DSL_VERSION}\uFF0C\u5F53\u524D\u4E3A ${JSON.stringify(doc.version)}`);
1262
+ add(
1263
+ "version",
1264
+ `version \u5FC5\u987B\u662F ${DSL_VERSION}\uFF0C\u5F53\u524D\u4E3A ${JSON.stringify(doc.version)}`
1265
+ );
1188
1266
  }
1189
1267
  const dataKeys = validateData(doc.data, add);
1190
1268
  const stateKeys = validateState(doc.state, add);
@@ -1284,7 +1362,10 @@ function validateStage(stage, add, warn, ctx, prefix) {
1284
1362
  if (stage.onShow !== void 0) validateAction(stage.onShow, at("onShow"), ctx);
1285
1363
  if (stage.onClose !== void 0) validateAction(stage.onClose, at("onClose"), ctx);
1286
1364
  if (stage.height === "auto" && stage.layout !== "flow") {
1287
- add("stage.height", "height: 'auto' \u9700\u8981\u540C\u65F6\u8BBE\u7F6E stage.layout: 'flow'\uFF0C\u5426\u5219\u9AD8\u5EA6\u4F1A\u584C\u6210 0");
1365
+ add(
1366
+ "stage.height",
1367
+ "height: 'auto' \u9700\u8981\u540C\u65F6\u8BBE\u7F6E stage.layout: 'flow'\uFF0C\u5426\u5219\u9AD8\u5EA6\u4F1A\u584C\u6210 0"
1368
+ );
1288
1369
  }
1289
1370
  }
1290
1371
  function validateCloseButton(config, add, warn) {
@@ -1298,7 +1379,10 @@ function validateCloseButton(config, add, warn) {
1298
1379
  }
1299
1380
  if (config.offset !== void 0) {
1300
1381
  if (!Array.isArray(config.offset) || config.offset.length !== 2) {
1301
- add("stage.closeButton.offset", "offset \u5FC5\u987B\u662F [x, y] \u4E24\u9879\u6570\u7EC4\uFF0C\u8D1F\u503C\u4F1A\u628A\u6309\u94AE\u79FB\u5230\u5F39\u7A97\u5916\u9762");
1382
+ add(
1383
+ "stage.closeButton.offset",
1384
+ "offset \u5FC5\u987B\u662F [x, y] \u4E24\u9879\u6570\u7EC4\uFF0C\u8D1F\u503C\u4F1A\u628A\u6309\u94AE\u79FB\u5230\u5F39\u7A97\u5916\u9762"
1385
+ );
1302
1386
  } else {
1303
1387
  config.offset.forEach((value, index) => {
1304
1388
  if (!checkLength(value)) {
@@ -1319,7 +1403,10 @@ function validateCloseButton(config, add, warn) {
1319
1403
  if (config.style) {
1320
1404
  Object.keys(config.style).forEach((key) => {
1321
1405
  if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
1322
- warn(`stage.closeButton.style.${key}`, `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`);
1406
+ warn(
1407
+ `stage.closeButton.style.${key}`,
1408
+ `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`
1409
+ );
1323
1410
  }
1324
1411
  });
1325
1412
  }
@@ -1390,7 +1477,10 @@ function validateNode(node, path, layout, ctx) {
1390
1477
  } else {
1391
1478
  node.rect.forEach((value, index) => {
1392
1479
  if (value !== void 0 && value !== null && !checkLength(value)) {
1393
- add(`${path}.rect[${index}]`, `"${value}" \u4E0D\u662F\u5408\u6CD5\u957F\u5EA6\uFF0C\u652F\u6301\u6570\u5B57(px) / '100%' / 'auto' \u7B49`);
1480
+ add(
1481
+ `${path}.rect[${index}]`,
1482
+ `"${value}" \u4E0D\u662F\u5408\u6CD5\u957F\u5EA6\uFF0C\u652F\u6301\u6570\u5B57(px) / '100%' / 'auto' \u7B49`
1483
+ );
1394
1484
  }
1395
1485
  });
1396
1486
  }
@@ -1408,7 +1498,10 @@ function validateNode(node, path, layout, ctx) {
1408
1498
  );
1409
1499
  }
1410
1500
  if (key === "aspectRatio" && !checkAspectRatio(node.style[key])) {
1411
- add(`${path}.style.aspectRatio`, "aspectRatio \u8981\u5199\u6210 '750 / 200' \u6216 1.5 \u8FD9\u6837\u7684\u5BBD\u9AD8\u6BD4");
1501
+ add(
1502
+ `${path}.style.aspectRatio`,
1503
+ "aspectRatio \u8981\u5199\u6210 '750 / 200' \u6216 1.5 \u8FD9\u6837\u7684\u5BBD\u9AD8\u6BD4"
1504
+ );
1412
1505
  }
1413
1506
  });
1414
1507
  }
@@ -1438,13 +1531,21 @@ function validateNodeByType(node, path, ctx) {
1438
1531
  if (typeof node.bind !== "string") {
1439
1532
  add(`${path}.bind`, "tabs \u5FC5\u987B\u63D0\u4F9B bind\uFF08data \u4E2D\u7684\u6570\u7EC4\u5B57\u6BB5\u540D\uFF09");
1440
1533
  }
1441
- if (typeof node.stateKey !== "string") add(`${path}.stateKey`, "tabs \u5FC5\u987B\u63D0\u4F9B stateKey");
1534
+ if (typeof node.stateKey !== "string")
1535
+ add(`${path}.stateKey`, "tabs \u5FC5\u987B\u63D0\u4F9B stateKey");
1442
1536
  if (node.labelField !== void 0 && typeof node.labelField !== "string") {
1443
1537
  add(`${path}.labelField`, "labelField \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
1444
1538
  }
1445
1539
  }
1446
1540
  if (node.type === "countdown") {
1447
- if (!node.to) add(`${path}.to`, "countdown \u5FC5\u987B\u63D0\u4F9B to\uFF08\u7ED3\u675F\u65F6\u95F4\uFF09");
1541
+ if (!node.to) {
1542
+ add(`${path}.to`, "countdown \u5FC5\u987B\u63D0\u4F9B to\uFF08\u7ED3\u675F\u65F6\u95F4\uFF09");
1543
+ } else if (!isValidEndTime(node.to)) {
1544
+ add(
1545
+ `${path}.to`,
1546
+ `"${node.to}" \u4E0D\u662F\u80FD\u8BC6\u522B\u7684\u65F6\u95F4\u3002\u53EF\u4EE5\u5199 "2026-09-01 18:00:00" / "2026-09-01" / "2026\u5E749\u67081\u65E5 18\u70B9" / ISO \u683C\u5F0F / \u65F6\u95F4\u6233`
1547
+ );
1548
+ }
1448
1549
  if (node.precision !== void 0 && ["s", "cs"].indexOf(node.precision) === -1) {
1449
1550
  add(`${path}.precision`, "precision \u53EA\u80FD\u662F 's'\uFF08\u6BCF\u79D2\uFF09\u6216 'cs'\uFF08\u5398\u79D2\uFF0C\u9010\u5E27\u5237\u65B0\uFF09");
1450
1551
  }
@@ -1504,7 +1605,10 @@ function validateAction(action, path, ctx) {
1504
1605
  add(`${path}.view`, `views \u91CC\u4E0D\u5B58\u5728\u89C6\u56FE "${action.view}"`);
1505
1606
  }
1506
1607
  if (action.mode !== void 0 && ["stack", "replace"].indexOf(action.mode) === -1) {
1507
- add(`${path}.mode`, "mode \u53EA\u80FD\u662F 'stack'\uFF08\u9ED8\u8BA4\uFF0C\u53E0\u4E00\u5C42\uFF09\u6216 'replace'\uFF08\u6362\u6389\u5F53\u524D\u5C42\uFF09");
1608
+ add(
1609
+ `${path}.mode`,
1610
+ "mode \u53EA\u80FD\u662F 'stack'\uFF08\u9ED8\u8BA4\uFF0C\u53E0\u4E00\u5C42\uFF09\u6216 'replace'\uFF08\u6362\u6389\u5F53\u524D\u5C42\uFF09"
1611
+ );
1508
1612
  }
1509
1613
  }
1510
1614
  if (action.type === "sequence") {
@@ -1521,4 +1625,4 @@ function formatIssues(issues) {
1521
1625
  return issues.map((item) => item.path ? `${item.path}: ${item.message}` : item.message).join("\n");
1522
1626
  }
1523
1627
 
1524
- export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, DSL_VERSION, NODE_TYPES, SINGLE_VIEW_NAME, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
1628
+ export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, DSL_VERSION, NODE_TYPES, SINGLE_VIEW_NAME, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaoxiu/marketing-dsl",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "营销弹窗 DSL 解释器核心,纯逻辑无框架依赖",
6
6
  "license": "MIT",