@yaoxiu/marketing-dsl 1.5.2 → 2.0.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 +120 -17
- package/dist/{chunk-FKZKLCVV.js → chunk-BSJDNS6C.js} +121 -2
- package/dist/docs/index.cjs +191 -43
- package/dist/docs/index.d.cts +1 -1
- package/dist/docs/index.d.ts +1 -1
- package/dist/docs/index.js +164 -44
- package/dist/index.cjs +167 -15
- package/dist/index.d.cts +63 -4
- package/dist/index.d.ts +63 -4
- package/dist/index.js +46 -16
- package/dist/report/index.cjs +47 -30
- package/dist/report/index.d.cts +80 -31
- package/dist/report/index.d.ts +80 -31
- package/dist/report/index.js +46 -31
- package/dist/{types-BgUaJUAu.d.cts → types-Bj0xyWnx.d.cts} +54 -2
- package/dist/{types-BgUaJUAu.d.ts → types-Bj0xyWnx.d.ts} +54 -2
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -327,13 +327,13 @@ function safeImageUrl(input) {
|
|
|
327
327
|
// src/actions.ts
|
|
328
328
|
function createDispatcher(options) {
|
|
329
329
|
const { setState, emit, openView, closeTop, closeAll } = options;
|
|
330
|
-
function
|
|
330
|
+
function run(action, context, interaction) {
|
|
331
331
|
if (!action || !action.type) return;
|
|
332
332
|
const handlers = options.handlers || {};
|
|
333
333
|
const editMode = !!options.editMode;
|
|
334
334
|
switch (action.type) {
|
|
335
335
|
case "sequence":
|
|
336
|
-
(action.actions || []).forEach((item) =>
|
|
336
|
+
(action.actions || []).forEach((item) => run(item, context, interaction));
|
|
337
337
|
return;
|
|
338
338
|
case "navigate": {
|
|
339
339
|
const url = safeUrl(interpolate(action.url, context));
|
|
@@ -343,6 +343,7 @@ function createDispatcher(options) {
|
|
|
343
343
|
}
|
|
344
344
|
const target = action.target || "_blank";
|
|
345
345
|
emit("navigate", { url, target });
|
|
346
|
+
if (!interaction.navigate) interaction.navigate = { url, target };
|
|
346
347
|
if (editMode) return;
|
|
347
348
|
if (typeof window === "undefined") return;
|
|
348
349
|
if (target === "_self") window.location.href = url;
|
|
@@ -356,20 +357,25 @@ function createDispatcher(options) {
|
|
|
356
357
|
return;
|
|
357
358
|
// 关掉栈顶那层;只剩一层时就是整体关闭
|
|
358
359
|
case "close":
|
|
360
|
+
interaction.closes = true;
|
|
359
361
|
closeTop(action.reason);
|
|
360
362
|
return;
|
|
361
363
|
case "closeAll":
|
|
364
|
+
interaction.closes = true;
|
|
362
365
|
closeAll(action.reason);
|
|
363
366
|
return;
|
|
364
367
|
case "setState":
|
|
365
368
|
setState(action.key, interpolate(action.value, context));
|
|
366
369
|
return;
|
|
367
|
-
case "track":
|
|
368
|
-
|
|
370
|
+
case "track": {
|
|
371
|
+
const payload = {
|
|
369
372
|
event: String(interpolate(action.event, context)),
|
|
370
373
|
params: interpolateDeep(action.params || {}, context)
|
|
371
|
-
}
|
|
374
|
+
};
|
|
375
|
+
emit("track", payload);
|
|
376
|
+
if (!interaction.track) interaction.track = payload;
|
|
372
377
|
return;
|
|
378
|
+
}
|
|
373
379
|
case "call": {
|
|
374
380
|
const handler = handlers[action.name];
|
|
375
381
|
if (typeof handler !== "function") {
|
|
@@ -389,6 +395,13 @@ function createDispatcher(options) {
|
|
|
389
395
|
});
|
|
390
396
|
}
|
|
391
397
|
}
|
|
398
|
+
function dispatch(action, context, meta) {
|
|
399
|
+
if (!action || !action.type) return;
|
|
400
|
+
const interaction = { trigger: (meta == null ? void 0 : meta.trigger) || "action" };
|
|
401
|
+
if ((meta == null ? void 0 : meta.reason) !== void 0) interaction.reason = meta.reason;
|
|
402
|
+
run(action, context, interaction);
|
|
403
|
+
emit("interaction", interaction);
|
|
404
|
+
}
|
|
392
405
|
return dispatch;
|
|
393
406
|
}
|
|
394
407
|
|
|
@@ -619,9 +632,7 @@ function resolveNodeStyle(node, layout) {
|
|
|
619
632
|
function resolveTree(input) {
|
|
620
633
|
const { normalized, viewStack, ready } = input;
|
|
621
634
|
const layers = viewStack.map((name) => ({ name, view: normalized.views[name] })).filter((item) => !!item.view);
|
|
622
|
-
const hasPopup =
|
|
623
|
-
(name) => normalized.views[name].type === "popup"
|
|
624
|
-
);
|
|
635
|
+
const hasPopup = layers.some((item) => item.view.type === "popup");
|
|
625
636
|
const countdowns = { endTimes: [], precision: "s" };
|
|
626
637
|
const renderLayers = ready ? layers.map(
|
|
627
638
|
(item, index) => resolveLayer(
|
|
@@ -635,8 +646,10 @@ function resolveTree(input) {
|
|
|
635
646
|
return {
|
|
636
647
|
ready: ready && layers.length > 0,
|
|
637
648
|
hasPopup,
|
|
638
|
-
//
|
|
639
|
-
|
|
649
|
+
// 根容器恒为 relative:它要待在宿主给的位置上(banner / 公告坑位是流式的普通 div,
|
|
650
|
+
// 根容器一脱流坑位就塌成 0、内容按最近定位祖先飘走)。弹窗铺满视口由弹窗层自己
|
|
651
|
+
// `position: fixed` 完成,不再靠根容器撑开,见 layerStyle。
|
|
652
|
+
rootStyle: { position: "relative" },
|
|
640
653
|
layers: renderLayers,
|
|
641
654
|
countdownEndTimes: countdowns.endTimes,
|
|
642
655
|
countdownPrecision: countdowns.precision
|
|
@@ -682,7 +695,15 @@ function resolveLayer(name, view, isTop, input, countdowns) {
|
|
|
682
695
|
};
|
|
683
696
|
}
|
|
684
697
|
function layerStyle(isPopup, isTop) {
|
|
685
|
-
const style = isPopup ? {
|
|
698
|
+
const style = isPopup ? {
|
|
699
|
+
position: "fixed",
|
|
700
|
+
top: "0",
|
|
701
|
+
right: "0",
|
|
702
|
+
bottom: "0",
|
|
703
|
+
left: "0",
|
|
704
|
+
// 弹窗层可能挂在页面中段的坑位里,不提层会被后面的内容盖住
|
|
705
|
+
zIndex: "1000"
|
|
706
|
+
} : {};
|
|
686
707
|
if (!isTop) style.pointerEvents = "none";
|
|
687
708
|
return style;
|
|
688
709
|
}
|
|
@@ -710,7 +731,7 @@ function resolveStageStyle(stage, isPopup, context) {
|
|
|
710
731
|
base.margin = "auto";
|
|
711
732
|
base.flex = "none";
|
|
712
733
|
}
|
|
713
|
-
|
|
734
|
+
const style = Object.assign(
|
|
714
735
|
base,
|
|
715
736
|
{
|
|
716
737
|
// width / height 支持数字(px)、'100%' / 'auto' / calc(...) 等相对单位,也支持 {{ }}
|
|
@@ -724,6 +745,11 @@ function resolveStageStyle(stage, isPopup, context) {
|
|
|
724
745
|
// stage.style 也走一遍插值,这样切 tab 时弹窗本身的背景能跟着变
|
|
725
746
|
toCssStyle(interpolateDeep(stage.style, context))
|
|
726
747
|
);
|
|
748
|
+
if (!isPopup && style.maxWidth !== void 0 && style.margin === void 0) {
|
|
749
|
+
style.marginLeft = "auto";
|
|
750
|
+
style.marginRight = "auto";
|
|
751
|
+
}
|
|
752
|
+
return style;
|
|
727
753
|
}
|
|
728
754
|
function resolveCloseButton(config, layerName, closeTop) {
|
|
729
755
|
if (!config || config.show === false) return void 0;
|
|
@@ -1187,7 +1213,11 @@ function createRuntime(dsl, options = {}) {
|
|
|
1187
1213
|
const view = normalized.views[viewName];
|
|
1188
1214
|
const action = view && view.stage && view.stage[hook];
|
|
1189
1215
|
if (!action) return;
|
|
1190
|
-
|
|
1216
|
+
const reason = extra && extra.closeReason;
|
|
1217
|
+
dispatch(action, Object.assign({}, buildContext(), extra), {
|
|
1218
|
+
trigger: hook === "onShow" ? "show" : "close",
|
|
1219
|
+
reason: typeof reason === "string" ? reason : void 0
|
|
1220
|
+
});
|
|
1191
1221
|
}
|
|
1192
1222
|
function loadData() {
|
|
1193
1223
|
const data = dsl.data || {};
|
|
@@ -1218,8 +1248,8 @@ function createRuntime(dsl, options = {}) {
|
|
|
1218
1248
|
resolvedData = resolved;
|
|
1219
1249
|
loading = false;
|
|
1220
1250
|
notify();
|
|
1221
|
-
emit("ready", { keys: Object.keys(resolved) });
|
|
1222
1251
|
fireLifecycle(normalized.entry, "onShow");
|
|
1252
|
+
emit("ready", { keys: Object.keys(resolved) });
|
|
1223
1253
|
}).catch((error) => {
|
|
1224
1254
|
if (destroyed) return;
|
|
1225
1255
|
loading = false;
|
|
@@ -1266,6 +1296,120 @@ function createRuntime(dsl, options = {}) {
|
|
|
1266
1296
|
};
|
|
1267
1297
|
}
|
|
1268
1298
|
|
|
1299
|
+
// src/user-fields.ts
|
|
1300
|
+
var DEFAULT_USER_FIELDS = [
|
|
1301
|
+
"shopId",
|
|
1302
|
+
"shopName",
|
|
1303
|
+
"collectCount",
|
|
1304
|
+
"albbCollectCount",
|
|
1305
|
+
"isNewUser",
|
|
1306
|
+
"levelId",
|
|
1307
|
+
"levelName",
|
|
1308
|
+
"serviceExpireTime",
|
|
1309
|
+
"remainDays",
|
|
1310
|
+
"userTypeNew",
|
|
1311
|
+
"serviceInfo"
|
|
1312
|
+
];
|
|
1313
|
+
var USER_FIELD_LABELS = {
|
|
1314
|
+
shopId: "\u5E97\u94FA ID",
|
|
1315
|
+
shopName: "\u5E97\u94FA\u540D\u79F0",
|
|
1316
|
+
collectCount: "\u5269\u4F59\u91C7\u96C6\u6B21\u6570",
|
|
1317
|
+
albbCollectCount: "\u5269\u4F59 1688 \u91C7\u96C6\u6B21\u6570",
|
|
1318
|
+
isNewUser: "\u662F\u5426\u65B0\u7528\u6237\uFF080 / 1\uFF0C\u53EF\u76F4\u63A5\u7528\u4E8E visibleWhen\uFF09",
|
|
1319
|
+
levelId: "\u7248\u672C ID",
|
|
1320
|
+
levelName: "\u7248\u672C\u540D\u79F0\uFF0C\u5982\u300C\u4E13\u4E1A\u7248\u300D",
|
|
1321
|
+
serviceExpireTime: "\u670D\u52A1\u5230\u671F\u65F6\u95F4\uFF0C\u53EF\u76F4\u63A5\u5582\u7ED9 countdown \u7684 to",
|
|
1322
|
+
remainDays: "\u670D\u52A1\u5269\u4F59\u5929\u6570",
|
|
1323
|
+
userTypeNew: "\u65B0\u5546\u7528\u6237\u7C7B\u578B\uFF080 \u672A\u8BBE\u7F6E / 1 \u6709\u8D27\u6E90 / 2 \u65E0\u8D27\u6E90 / 3 \u4E24\u8005\u517C\u5907\uFF09",
|
|
1324
|
+
serviceInfo: "\u5BA2\u670D\u4E0E\u793E\u7FA4\u4FE1\u606F\uFF08\u5BF9\u8C61\uFF0C\u6309\u9700\u53D6\u5B50\u5B57\u6BB5\uFF09"
|
|
1325
|
+
};
|
|
1326
|
+
var USER_FIELD_RE = /\buser\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
|
1327
|
+
var INTERPOLATION_RE2 = /\{\{([\s\S]*?)\}\}/g;
|
|
1328
|
+
var RAW_EXPRESSION_KEYS = ["visibleWhen"];
|
|
1329
|
+
function editDistance(a, b) {
|
|
1330
|
+
const prev = [];
|
|
1331
|
+
for (let j = 0; j <= b.length; j++) prev[j] = j;
|
|
1332
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1333
|
+
let diagonal = prev[0];
|
|
1334
|
+
prev[0] = i;
|
|
1335
|
+
for (let j = 1; j <= b.length; j++) {
|
|
1336
|
+
const temp = prev[j];
|
|
1337
|
+
prev[j] = Math.min(
|
|
1338
|
+
prev[j] + 1,
|
|
1339
|
+
prev[j - 1] + 1,
|
|
1340
|
+
diagonal + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
1341
|
+
);
|
|
1342
|
+
diagonal = temp;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return prev[b.length];
|
|
1346
|
+
}
|
|
1347
|
+
function closestField(name, allowed) {
|
|
1348
|
+
const lower = name.toLowerCase();
|
|
1349
|
+
let best = "";
|
|
1350
|
+
let bestScore = Infinity;
|
|
1351
|
+
allowed.forEach((field) => {
|
|
1352
|
+
const score = editDistance(lower, field.toLowerCase());
|
|
1353
|
+
if (score < bestScore) {
|
|
1354
|
+
bestScore = score;
|
|
1355
|
+
best = field;
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
return bestScore <= Math.max(3, Math.ceil(name.length / 2)) ? best : "";
|
|
1359
|
+
}
|
|
1360
|
+
function collectFromExpression(source, into) {
|
|
1361
|
+
USER_FIELD_RE.lastIndex = 0;
|
|
1362
|
+
let matched = USER_FIELD_RE.exec(source);
|
|
1363
|
+
while (matched) {
|
|
1364
|
+
into.push(matched[1]);
|
|
1365
|
+
matched = USER_FIELD_RE.exec(source);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function collectFromValue(value, key, into) {
|
|
1369
|
+
if (RAW_EXPRESSION_KEYS.indexOf(key) > -1) {
|
|
1370
|
+
collectFromExpression(value, into);
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
INTERPOLATION_RE2.lastIndex = 0;
|
|
1374
|
+
let matched = INTERPOLATION_RE2.exec(value);
|
|
1375
|
+
while (matched) {
|
|
1376
|
+
collectFromExpression(matched[1], into);
|
|
1377
|
+
matched = INTERPOLATION_RE2.exec(value);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function validateUserFields(dsl, allowed) {
|
|
1381
|
+
const issues = [];
|
|
1382
|
+
const seen = [];
|
|
1383
|
+
const walk = (node, path, key) => {
|
|
1384
|
+
if (typeof node === "string") {
|
|
1385
|
+
const names = [];
|
|
1386
|
+
collectFromValue(node, key, names);
|
|
1387
|
+
names.forEach((name) => {
|
|
1388
|
+
if (allowed.indexOf(name) > -1) return;
|
|
1389
|
+
const guess = closestField(name, allowed);
|
|
1390
|
+
issues.push({
|
|
1391
|
+
path,
|
|
1392
|
+
message: `\u7528\u6237\u4FE1\u606F\u91CC\u6CA1\u6709 "user.${name}" \u8FD9\u4E2A\u5B57\u6BB5` + (guess ? `\uFF0C\u662F\u4E0D\u662F\u60F3\u5199 "user.${guess}"\uFF1F` : "") + `\u3002\u53EF\u7528\u5B57\u6BB5\uFF1A${allowed.join(" / ")}`
|
|
1393
|
+
});
|
|
1394
|
+
});
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
if (!node || typeof node !== "object") return;
|
|
1398
|
+
if (seen.indexOf(node) > -1) return;
|
|
1399
|
+
seen.push(node);
|
|
1400
|
+
if (Array.isArray(node)) {
|
|
1401
|
+
node.forEach((item, index) => walk(item, `${path}[${index}]`, key));
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
Object.keys(node).forEach((childKey) => {
|
|
1405
|
+
const childPath = path ? `${path}.${childKey}` : childKey;
|
|
1406
|
+
walk(node[childKey], childPath, childKey);
|
|
1407
|
+
});
|
|
1408
|
+
};
|
|
1409
|
+
walk(dsl, "", "");
|
|
1410
|
+
return issues;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1269
1413
|
// src/validate.ts
|
|
1270
1414
|
var LENGTH_STYLE_KEYS = [
|
|
1271
1415
|
"width",
|
|
@@ -1317,7 +1461,7 @@ var CLOSE_POSITIONS = [
|
|
|
1317
1461
|
"bottom-center"
|
|
1318
1462
|
];
|
|
1319
1463
|
var CONTAINER_TYPES = ["box", "flex"];
|
|
1320
|
-
function validate(dsl) {
|
|
1464
|
+
function validate(dsl, options) {
|
|
1321
1465
|
const errors = [];
|
|
1322
1466
|
const warnings = [];
|
|
1323
1467
|
const add = (path, message) => errors.push({ path, message });
|
|
@@ -1347,6 +1491,11 @@ function validate(dsl) {
|
|
|
1347
1491
|
viewNames.forEach((name) => {
|
|
1348
1492
|
validateView(views[name], isMulti ? `views.${name}` : "", ctx);
|
|
1349
1493
|
});
|
|
1494
|
+
if (options && options.userFields) {
|
|
1495
|
+
validateUserFields(doc, options.userFields).forEach(
|
|
1496
|
+
(issue) => add(issue.path, issue.message)
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1350
1499
|
return { valid: errors.length === 0, errors, warnings };
|
|
1351
1500
|
}
|
|
1352
1501
|
function validateMultiView(dsl, add) {
|
|
@@ -1707,10 +1856,12 @@ function formatIssues(issues) {
|
|
|
1707
1856
|
exports.ACTION_TYPES = ACTION_TYPES;
|
|
1708
1857
|
exports.ALLOWED_STYLE_KEYS = ALLOWED_STYLE_KEYS;
|
|
1709
1858
|
exports.CLOSE_POSITIONS = CLOSE_POSITIONS;
|
|
1859
|
+
exports.DEFAULT_USER_FIELDS = DEFAULT_USER_FIELDS;
|
|
1710
1860
|
exports.DSL_VERSION = DSL_VERSION;
|
|
1711
1861
|
exports.NODE_TYPES = NODE_TYPES;
|
|
1712
1862
|
exports.SINGLE_VIEW_NAME = SINGLE_VIEW_NAME;
|
|
1713
1863
|
exports.TRACK_EVENT_PATTERN = TRACK_EVENT_PATTERN;
|
|
1864
|
+
exports.USER_FIELD_LABELS = USER_FIELD_LABELS;
|
|
1714
1865
|
exports.check = check;
|
|
1715
1866
|
exports.computeParts = computeParts;
|
|
1716
1867
|
exports.createRuntime = createRuntime;
|
|
@@ -1728,3 +1879,4 @@ exports.safeUrl = safeUrl;
|
|
|
1728
1879
|
exports.toCssStyle = toCssStyle;
|
|
1729
1880
|
exports.toLength = toLength;
|
|
1730
1881
|
exports.validate = validate;
|
|
1882
|
+
exports.validateUserFields = validateUserFields;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as Dsl, R as RuntimeOptions, a as RenderTree, b as DslView, c as DslStyle, C as CssStyle } from './types-
|
|
2
|
-
export { q as DslAction, h as DslActionBase, o as DslCallAction, j as DslCloseAction, k as DslCloseAllAction, t as DslCloseButton, s as DslClosePosition, w as DslDerived, A as DslHandler, f as DslLength, i as DslNavigateAction, r as DslNode, e as DslNodeType, l as DslOpenAction, g as DslRect, p as DslSequenceAction, m as DslSetStateAction, z as DslSource, v as DslSourceRef, u as DslStage, n as DslTrackAction, d as DslViewType, x as RenderElement, y as RenderLayer,
|
|
1
|
+
import { D as Dsl, R as RuntimeOptions, a as RenderTree, b as DslView, c as DslStyle, C as CssStyle } from './types-Bj0xyWnx.cjs';
|
|
2
|
+
export { q as DslAction, h as DslActionBase, o as DslCallAction, j as DslCloseAction, k as DslCloseAllAction, t as DslCloseButton, s as DslClosePosition, w as DslDerived, A as DslHandler, E as DslInteractionEvent, B as DslInteractionTrigger, f as DslLength, i as DslNavigateAction, r as DslNode, e as DslNodeType, l as DslOpenAction, g as DslRect, p as DslSequenceAction, m as DslSetStateAction, z as DslSource, v as DslSourceRef, u as DslStage, n as DslTrackAction, d as DslViewType, x as RenderElement, y as RenderLayer, H as RuntimeEmit, G as RuntimeEventName, F as RuntimeEvents } from './types-Bj0xyWnx.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* 运行时。
|
|
@@ -46,6 +46,17 @@ interface Issue {
|
|
|
46
46
|
path: string;
|
|
47
47
|
message: string;
|
|
48
48
|
}
|
|
49
|
+
/** 校验选项,全部可选;不传时行为与旧版完全一致 */
|
|
50
|
+
interface ValidateOptions {
|
|
51
|
+
/**
|
|
52
|
+
* 允许出现在 `{{ user.xxx }}` 里的字段白名单。
|
|
53
|
+
*
|
|
54
|
+
* **不传就不校验**(宿主注入什么字段由宿主决定,核心包不该替它们写死)。
|
|
55
|
+
* 传了则所有插值位置里引用的 `user` 字段都必须在表内,否则报 error。
|
|
56
|
+
* 默认可用的一套见 `DEFAULT_USER_FIELDS`。
|
|
57
|
+
*/
|
|
58
|
+
userFields?: string[];
|
|
59
|
+
}
|
|
49
60
|
interface ValidateResult {
|
|
50
61
|
valid: boolean;
|
|
51
62
|
errors: Issue[];
|
|
@@ -62,10 +73,58 @@ declare const ACTION_TYPES: string[];
|
|
|
62
73
|
*/
|
|
63
74
|
declare const TRACK_EVENT_PATTERN: RegExp;
|
|
64
75
|
declare const CLOSE_POSITIONS: string[];
|
|
65
|
-
|
|
76
|
+
/**
|
|
77
|
+
* 校验一份 DSL。
|
|
78
|
+
*
|
|
79
|
+
* @param dsl 待校验的配置
|
|
80
|
+
* @param options 可选项;不传时与旧版行为完全一致
|
|
81
|
+
*/
|
|
82
|
+
declare function validate(dsl: Dsl | unknown, options?: ValidateOptions): ValidateResult;
|
|
66
83
|
/** 把校验结果拼成可以直接展示的文本 */
|
|
67
84
|
declare function formatIssues(issues: Issue[]): string;
|
|
68
85
|
|
|
86
|
+
/**
|
|
87
|
+
* `{{ user.xxx }}` 字段白名单校验
|
|
88
|
+
*
|
|
89
|
+
* `user` 是宿主注入的登录用户信息对象,字段集由宿主决定、前端 DSL 只能读。
|
|
90
|
+
* 运营写错字段名(`user.expireDays` 而不是 `user.remainDays`)时,
|
|
91
|
+
* 运行时只会求值为空——页面上就是一处空白 / 倒计时归零,
|
|
92
|
+
* 投到线上才发现。所以在保存前就按白名单拦一道。
|
|
93
|
+
*
|
|
94
|
+
* 白名单本身是**可选**的:`validate(dsl)` 不传就完全不查(向后兼容),
|
|
95
|
+
* 调用方(后台调试台 / 后端保存)自己决定用哪套字段。
|
|
96
|
+
*/
|
|
97
|
+
/** 一条校验问题,结构与 `validate.ts` 的 `Issue` 一致(避免两个文件循环依赖类型) */
|
|
98
|
+
interface FieldIssue {
|
|
99
|
+
/** 出问题的位置,如 `views.main.nodes[0].content` */
|
|
100
|
+
path: string;
|
|
101
|
+
/** 给运营看的中文说明 */
|
|
102
|
+
message: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 默认可用的 `user` 字段白名单。
|
|
106
|
+
*
|
|
107
|
+
* 取自各平台统一的登录用户信息模型(`UserInfo`),这套字段名是全平台通用契约。
|
|
108
|
+
* 刻意排除两个字段:
|
|
109
|
+
* - `sourceData`:后端原始响应整包,结构随平台漂移,不对运营开放
|
|
110
|
+
* - `mcToken`:令牌,不该出现在运营配置里,更不该渲染到页面上
|
|
111
|
+
*/
|
|
112
|
+
declare const DEFAULT_USER_FIELDS: string[];
|
|
113
|
+
/** 白名单字段的中文含义,文档和编辑器提示共用一份,避免说明各写各的 */
|
|
114
|
+
declare const USER_FIELD_LABELS: Record<string, string>;
|
|
115
|
+
/**
|
|
116
|
+
* 深度遍历整份 DSL,校验所有 `{{ user.xxx }}` 引用的字段是否在白名单内。
|
|
117
|
+
*
|
|
118
|
+
* 遍历的是**全部字符串值**而不是某几个已知字段(content / to / src …),
|
|
119
|
+
* 因为能写插值的位置太多(样式值、动作 params、stage 尺寸…),
|
|
120
|
+
* 正向列举必漏;反过来「凡是字符串就查一遍 `{{ }}`」不会漏。
|
|
121
|
+
*
|
|
122
|
+
* @param dsl 待校验的 DSL(任意结构,坏数据交给主校验流程报错,这里只负责不崩)
|
|
123
|
+
* @param allowed 允许的字段名列表
|
|
124
|
+
* @returns 每个非法引用一条 error 级问题
|
|
125
|
+
*/
|
|
126
|
+
declare function validateUserFields(dsl: unknown, allowed: string[]): FieldIssue[];
|
|
127
|
+
|
|
69
128
|
/**
|
|
70
129
|
* 视图归一化。
|
|
71
130
|
*
|
|
@@ -177,4 +236,4 @@ declare function computeParts(endTime: number, now?: number): CountdownParts;
|
|
|
177
236
|
/** 不给 children 时退化成一行文本,占位符 {d} {h} {hAll} {m} {s} {cs} */
|
|
178
237
|
declare function formatParts(parts: CountdownParts, format?: string): string;
|
|
179
238
|
|
|
180
|
-
export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, CssStyle, DSL_VERSION, Dsl, type DslRuntime, DslStyle, DslView, type Issue, NODE_TYPES, type NormalizedViews, RenderTree, RuntimeOptions, SINGLE_VIEW_NAME, TRACK_EVENT_PATTERN, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
|
|
239
|
+
export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, CssStyle, DEFAULT_USER_FIELDS, DSL_VERSION, Dsl, type DslRuntime, DslStyle, DslView, type Issue, NODE_TYPES, type NormalizedViews, RenderTree, RuntimeOptions, SINGLE_VIEW_NAME, TRACK_EVENT_PATTERN, USER_FIELD_LABELS, type ValidateOptions, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate, validateUserFields };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as Dsl, R as RuntimeOptions, a as RenderTree, b as DslView, c as DslStyle, C as CssStyle } from './types-
|
|
2
|
-
export { q as DslAction, h as DslActionBase, o as DslCallAction, j as DslCloseAction, k as DslCloseAllAction, t as DslCloseButton, s as DslClosePosition, w as DslDerived, A as DslHandler, f as DslLength, i as DslNavigateAction, r as DslNode, e as DslNodeType, l as DslOpenAction, g as DslRect, p as DslSequenceAction, m as DslSetStateAction, z as DslSource, v as DslSourceRef, u as DslStage, n as DslTrackAction, d as DslViewType, x as RenderElement, y as RenderLayer,
|
|
1
|
+
import { D as Dsl, R as RuntimeOptions, a as RenderTree, b as DslView, c as DslStyle, C as CssStyle } from './types-Bj0xyWnx.js';
|
|
2
|
+
export { q as DslAction, h as DslActionBase, o as DslCallAction, j as DslCloseAction, k as DslCloseAllAction, t as DslCloseButton, s as DslClosePosition, w as DslDerived, A as DslHandler, E as DslInteractionEvent, B as DslInteractionTrigger, f as DslLength, i as DslNavigateAction, r as DslNode, e as DslNodeType, l as DslOpenAction, g as DslRect, p as DslSequenceAction, m as DslSetStateAction, z as DslSource, v as DslSourceRef, u as DslStage, n as DslTrackAction, d as DslViewType, x as RenderElement, y as RenderLayer, H as RuntimeEmit, G as RuntimeEventName, F as RuntimeEvents } from './types-Bj0xyWnx.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* 运行时。
|
|
@@ -46,6 +46,17 @@ interface Issue {
|
|
|
46
46
|
path: string;
|
|
47
47
|
message: string;
|
|
48
48
|
}
|
|
49
|
+
/** 校验选项,全部可选;不传时行为与旧版完全一致 */
|
|
50
|
+
interface ValidateOptions {
|
|
51
|
+
/**
|
|
52
|
+
* 允许出现在 `{{ user.xxx }}` 里的字段白名单。
|
|
53
|
+
*
|
|
54
|
+
* **不传就不校验**(宿主注入什么字段由宿主决定,核心包不该替它们写死)。
|
|
55
|
+
* 传了则所有插值位置里引用的 `user` 字段都必须在表内,否则报 error。
|
|
56
|
+
* 默认可用的一套见 `DEFAULT_USER_FIELDS`。
|
|
57
|
+
*/
|
|
58
|
+
userFields?: string[];
|
|
59
|
+
}
|
|
49
60
|
interface ValidateResult {
|
|
50
61
|
valid: boolean;
|
|
51
62
|
errors: Issue[];
|
|
@@ -62,10 +73,58 @@ declare const ACTION_TYPES: string[];
|
|
|
62
73
|
*/
|
|
63
74
|
declare const TRACK_EVENT_PATTERN: RegExp;
|
|
64
75
|
declare const CLOSE_POSITIONS: string[];
|
|
65
|
-
|
|
76
|
+
/**
|
|
77
|
+
* 校验一份 DSL。
|
|
78
|
+
*
|
|
79
|
+
* @param dsl 待校验的配置
|
|
80
|
+
* @param options 可选项;不传时与旧版行为完全一致
|
|
81
|
+
*/
|
|
82
|
+
declare function validate(dsl: Dsl | unknown, options?: ValidateOptions): ValidateResult;
|
|
66
83
|
/** 把校验结果拼成可以直接展示的文本 */
|
|
67
84
|
declare function formatIssues(issues: Issue[]): string;
|
|
68
85
|
|
|
86
|
+
/**
|
|
87
|
+
* `{{ user.xxx }}` 字段白名单校验
|
|
88
|
+
*
|
|
89
|
+
* `user` 是宿主注入的登录用户信息对象,字段集由宿主决定、前端 DSL 只能读。
|
|
90
|
+
* 运营写错字段名(`user.expireDays` 而不是 `user.remainDays`)时,
|
|
91
|
+
* 运行时只会求值为空——页面上就是一处空白 / 倒计时归零,
|
|
92
|
+
* 投到线上才发现。所以在保存前就按白名单拦一道。
|
|
93
|
+
*
|
|
94
|
+
* 白名单本身是**可选**的:`validate(dsl)` 不传就完全不查(向后兼容),
|
|
95
|
+
* 调用方(后台调试台 / 后端保存)自己决定用哪套字段。
|
|
96
|
+
*/
|
|
97
|
+
/** 一条校验问题,结构与 `validate.ts` 的 `Issue` 一致(避免两个文件循环依赖类型) */
|
|
98
|
+
interface FieldIssue {
|
|
99
|
+
/** 出问题的位置,如 `views.main.nodes[0].content` */
|
|
100
|
+
path: string;
|
|
101
|
+
/** 给运营看的中文说明 */
|
|
102
|
+
message: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 默认可用的 `user` 字段白名单。
|
|
106
|
+
*
|
|
107
|
+
* 取自各平台统一的登录用户信息模型(`UserInfo`),这套字段名是全平台通用契约。
|
|
108
|
+
* 刻意排除两个字段:
|
|
109
|
+
* - `sourceData`:后端原始响应整包,结构随平台漂移,不对运营开放
|
|
110
|
+
* - `mcToken`:令牌,不该出现在运营配置里,更不该渲染到页面上
|
|
111
|
+
*/
|
|
112
|
+
declare const DEFAULT_USER_FIELDS: string[];
|
|
113
|
+
/** 白名单字段的中文含义,文档和编辑器提示共用一份,避免说明各写各的 */
|
|
114
|
+
declare const USER_FIELD_LABELS: Record<string, string>;
|
|
115
|
+
/**
|
|
116
|
+
* 深度遍历整份 DSL,校验所有 `{{ user.xxx }}` 引用的字段是否在白名单内。
|
|
117
|
+
*
|
|
118
|
+
* 遍历的是**全部字符串值**而不是某几个已知字段(content / to / src …),
|
|
119
|
+
* 因为能写插值的位置太多(样式值、动作 params、stage 尺寸…),
|
|
120
|
+
* 正向列举必漏;反过来「凡是字符串就查一遍 `{{ }}`」不会漏。
|
|
121
|
+
*
|
|
122
|
+
* @param dsl 待校验的 DSL(任意结构,坏数据交给主校验流程报错,这里只负责不崩)
|
|
123
|
+
* @param allowed 允许的字段名列表
|
|
124
|
+
* @returns 每个非法引用一条 error 级问题
|
|
125
|
+
*/
|
|
126
|
+
declare function validateUserFields(dsl: unknown, allowed: string[]): FieldIssue[];
|
|
127
|
+
|
|
69
128
|
/**
|
|
70
129
|
* 视图归一化。
|
|
71
130
|
*
|
|
@@ -177,4 +236,4 @@ declare function computeParts(endTime: number, now?: number): CountdownParts;
|
|
|
177
236
|
/** 不给 children 时退化成一行文本,占位符 {d} {h} {hAll} {m} {s} {cs} */
|
|
178
237
|
declare function formatParts(parts: CountdownParts, format?: string): string;
|
|
179
238
|
|
|
180
|
-
export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, CssStyle, DSL_VERSION, Dsl, type DslRuntime, DslStyle, DslView, type Issue, NODE_TYPES, type NormalizedViews, RenderTree, RuntimeOptions, SINGLE_VIEW_NAME, TRACK_EVENT_PATTERN, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
|
|
239
|
+
export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, CssStyle, DEFAULT_USER_FIELDS, DSL_VERSION, Dsl, type DslRuntime, DslStyle, DslView, type Issue, NODE_TYPES, type NormalizedViews, RenderTree, RuntimeOptions, SINGLE_VIEW_NAME, TRACK_EVENT_PATTERN, USER_FIELD_LABELS, type ValidateOptions, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate, validateUserFields };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { normalizeViews, createTicker,
|
|
2
|
-
export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, DSL_VERSION, NODE_TYPES, SINGLE_VIEW_NAME, TRACK_EVENT_PATTERN, check, computeParts, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, toCssStyle, toLength, validate } from './chunk-
|
|
1
|
+
import { normalizeViews, createTicker, evaluate, resolveNodeStyle, interpolateDeep, interpolate, toLength, toCssStyle, parseEndTime, computeParts, formatParts } from './chunk-BSJDNS6C.js';
|
|
2
|
+
export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, DEFAULT_USER_FIELDS, DSL_VERSION, NODE_TYPES, SINGLE_VIEW_NAME, TRACK_EVENT_PATTERN, USER_FIELD_LABELS, check, computeParts, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, toCssStyle, toLength, validate, validateUserFields } from './chunk-BSJDNS6C.js';
|
|
3
3
|
|
|
4
4
|
// src/url.ts
|
|
5
5
|
var SAFE_PROTOCOLS = ["http:", "https:", "mailto:", "tel:"];
|
|
@@ -22,13 +22,13 @@ function safeImageUrl(input) {
|
|
|
22
22
|
// src/actions.ts
|
|
23
23
|
function createDispatcher(options) {
|
|
24
24
|
const { setState, emit, openView, closeTop, closeAll } = options;
|
|
25
|
-
function
|
|
25
|
+
function run(action, context, interaction) {
|
|
26
26
|
if (!action || !action.type) return;
|
|
27
27
|
const handlers = options.handlers || {};
|
|
28
28
|
const editMode = !!options.editMode;
|
|
29
29
|
switch (action.type) {
|
|
30
30
|
case "sequence":
|
|
31
|
-
(action.actions || []).forEach((item) =>
|
|
31
|
+
(action.actions || []).forEach((item) => run(item, context, interaction));
|
|
32
32
|
return;
|
|
33
33
|
case "navigate": {
|
|
34
34
|
const url = safeUrl(interpolate(action.url, context));
|
|
@@ -38,6 +38,7 @@ function createDispatcher(options) {
|
|
|
38
38
|
}
|
|
39
39
|
const target = action.target || "_blank";
|
|
40
40
|
emit("navigate", { url, target });
|
|
41
|
+
if (!interaction.navigate) interaction.navigate = { url, target };
|
|
41
42
|
if (editMode) return;
|
|
42
43
|
if (typeof window === "undefined") return;
|
|
43
44
|
if (target === "_self") window.location.href = url;
|
|
@@ -51,20 +52,25 @@ function createDispatcher(options) {
|
|
|
51
52
|
return;
|
|
52
53
|
// 关掉栈顶那层;只剩一层时就是整体关闭
|
|
53
54
|
case "close":
|
|
55
|
+
interaction.closes = true;
|
|
54
56
|
closeTop(action.reason);
|
|
55
57
|
return;
|
|
56
58
|
case "closeAll":
|
|
59
|
+
interaction.closes = true;
|
|
57
60
|
closeAll(action.reason);
|
|
58
61
|
return;
|
|
59
62
|
case "setState":
|
|
60
63
|
setState(action.key, interpolate(action.value, context));
|
|
61
64
|
return;
|
|
62
|
-
case "track":
|
|
63
|
-
|
|
65
|
+
case "track": {
|
|
66
|
+
const payload = {
|
|
64
67
|
event: String(interpolate(action.event, context)),
|
|
65
68
|
params: interpolateDeep(action.params || {}, context)
|
|
66
|
-
}
|
|
69
|
+
};
|
|
70
|
+
emit("track", payload);
|
|
71
|
+
if (!interaction.track) interaction.track = payload;
|
|
67
72
|
return;
|
|
73
|
+
}
|
|
68
74
|
case "call": {
|
|
69
75
|
const handler = handlers[action.name];
|
|
70
76
|
if (typeof handler !== "function") {
|
|
@@ -84,6 +90,13 @@ function createDispatcher(options) {
|
|
|
84
90
|
});
|
|
85
91
|
}
|
|
86
92
|
}
|
|
93
|
+
function dispatch(action, context, meta) {
|
|
94
|
+
if (!action || !action.type) return;
|
|
95
|
+
const interaction = { trigger: (meta == null ? void 0 : meta.trigger) || "action" };
|
|
96
|
+
if ((meta == null ? void 0 : meta.reason) !== void 0) interaction.reason = meta.reason;
|
|
97
|
+
run(action, context, interaction);
|
|
98
|
+
emit("interaction", interaction);
|
|
99
|
+
}
|
|
87
100
|
return dispatch;
|
|
88
101
|
}
|
|
89
102
|
|
|
@@ -91,9 +104,7 @@ function createDispatcher(options) {
|
|
|
91
104
|
function resolveTree(input) {
|
|
92
105
|
const { normalized, viewStack, ready } = input;
|
|
93
106
|
const layers = viewStack.map((name) => ({ name, view: normalized.views[name] })).filter((item) => !!item.view);
|
|
94
|
-
const hasPopup =
|
|
95
|
-
(name) => normalized.views[name].type === "popup"
|
|
96
|
-
);
|
|
107
|
+
const hasPopup = layers.some((item) => item.view.type === "popup");
|
|
97
108
|
const countdowns = { endTimes: [], precision: "s" };
|
|
98
109
|
const renderLayers = ready ? layers.map(
|
|
99
110
|
(item, index) => resolveLayer(
|
|
@@ -107,8 +118,10 @@ function resolveTree(input) {
|
|
|
107
118
|
return {
|
|
108
119
|
ready: ready && layers.length > 0,
|
|
109
120
|
hasPopup,
|
|
110
|
-
//
|
|
111
|
-
|
|
121
|
+
// 根容器恒为 relative:它要待在宿主给的位置上(banner / 公告坑位是流式的普通 div,
|
|
122
|
+
// 根容器一脱流坑位就塌成 0、内容按最近定位祖先飘走)。弹窗铺满视口由弹窗层自己
|
|
123
|
+
// `position: fixed` 完成,不再靠根容器撑开,见 layerStyle。
|
|
124
|
+
rootStyle: { position: "relative" },
|
|
112
125
|
layers: renderLayers,
|
|
113
126
|
countdownEndTimes: countdowns.endTimes,
|
|
114
127
|
countdownPrecision: countdowns.precision
|
|
@@ -154,7 +167,15 @@ function resolveLayer(name, view, isTop, input, countdowns) {
|
|
|
154
167
|
};
|
|
155
168
|
}
|
|
156
169
|
function layerStyle(isPopup, isTop) {
|
|
157
|
-
const style = isPopup ? {
|
|
170
|
+
const style = isPopup ? {
|
|
171
|
+
position: "fixed",
|
|
172
|
+
top: "0",
|
|
173
|
+
right: "0",
|
|
174
|
+
bottom: "0",
|
|
175
|
+
left: "0",
|
|
176
|
+
// 弹窗层可能挂在页面中段的坑位里,不提层会被后面的内容盖住
|
|
177
|
+
zIndex: "1000"
|
|
178
|
+
} : {};
|
|
158
179
|
if (!isTop) style.pointerEvents = "none";
|
|
159
180
|
return style;
|
|
160
181
|
}
|
|
@@ -182,7 +203,7 @@ function resolveStageStyle(stage, isPopup, context) {
|
|
|
182
203
|
base.margin = "auto";
|
|
183
204
|
base.flex = "none";
|
|
184
205
|
}
|
|
185
|
-
|
|
206
|
+
const style = Object.assign(
|
|
186
207
|
base,
|
|
187
208
|
{
|
|
188
209
|
// width / height 支持数字(px)、'100%' / 'auto' / calc(...) 等相对单位,也支持 {{ }}
|
|
@@ -196,6 +217,11 @@ function resolveStageStyle(stage, isPopup, context) {
|
|
|
196
217
|
// stage.style 也走一遍插值,这样切 tab 时弹窗本身的背景能跟着变
|
|
197
218
|
toCssStyle(interpolateDeep(stage.style, context))
|
|
198
219
|
);
|
|
220
|
+
if (!isPopup && style.maxWidth !== void 0 && style.margin === void 0) {
|
|
221
|
+
style.marginLeft = "auto";
|
|
222
|
+
style.marginRight = "auto";
|
|
223
|
+
}
|
|
224
|
+
return style;
|
|
199
225
|
}
|
|
200
226
|
function resolveCloseButton(config, layerName, closeTop) {
|
|
201
227
|
if (!config || config.show === false) return void 0;
|
|
@@ -659,7 +685,11 @@ function createRuntime(dsl, options = {}) {
|
|
|
659
685
|
const view = normalized.views[viewName];
|
|
660
686
|
const action = view && view.stage && view.stage[hook];
|
|
661
687
|
if (!action) return;
|
|
662
|
-
|
|
688
|
+
const reason = extra && extra.closeReason;
|
|
689
|
+
dispatch(action, Object.assign({}, buildContext(), extra), {
|
|
690
|
+
trigger: hook === "onShow" ? "show" : "close",
|
|
691
|
+
reason: typeof reason === "string" ? reason : void 0
|
|
692
|
+
});
|
|
663
693
|
}
|
|
664
694
|
function loadData() {
|
|
665
695
|
const data = dsl.data || {};
|
|
@@ -690,8 +720,8 @@ function createRuntime(dsl, options = {}) {
|
|
|
690
720
|
resolvedData = resolved;
|
|
691
721
|
loading = false;
|
|
692
722
|
notify();
|
|
693
|
-
emit("ready", { keys: Object.keys(resolved) });
|
|
694
723
|
fireLifecycle(normalized.entry, "onShow");
|
|
724
|
+
emit("ready", { keys: Object.keys(resolved) });
|
|
695
725
|
}).catch((error) => {
|
|
696
726
|
if (destroyed) return;
|
|
697
727
|
loading = false;
|