@zerodep-dev/vue-tsx-pro-mini 0.0.0-stage → 0.4.1
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/LICENSE +21 -0
- package/README.md +69 -2
- package/dist/build.d.ts +104 -0
- package/dist/build.js +430 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +123 -0
- package/dist/elements-BDIG7-jU.js +63 -0
- package/dist/globals.d.ts +1 -0
- package/dist/globals.js +16 -0
- package/dist/index.d.ts +188 -0
- package/dist/index.js +15 -0
- package/dist/page-upnBSowU.js +1219 -0
- package/dist/testing.d.ts +50 -0
- package/dist/testing.js +109 -0
- package/package.json +71 -3
- package/template/gitignore +2 -0
- package/template/index.html +17 -0
- package/template/mini.config.ts +12 -0
- package/template/package.json +22 -0
- package/template/src/app.ts +5 -0
- package/template/src/main.ts +5 -0
- package/template/src/pages/HomePage.tsx +25 -0
- package/template/tsconfig.json +16 -0
- package/template/vite.config.ts +5 -0
|
@@ -0,0 +1,1219 @@
|
|
|
1
|
+
import { i as templateOf, n as FORM_CONTROLS, r as baseClassOf, t as BOOLEAN_ATTRS } from "./elements-BDIG7-jU.js";
|
|
2
|
+
import { createRenderer, inject, onBeforeUnmount } from "vue";
|
|
3
|
+
import { inlineStyle } from "@zerodep-dev/vue-tsx-pro-tsx/styles";
|
|
4
|
+
//#region src/node.ts
|
|
5
|
+
/**
|
|
6
|
+
* 小程序里的节点树:Vue 渲染器(renderer.ts)往这里增删改,页面(page.ts)把变化攒成一次 setData。
|
|
7
|
+
* 只实现组件和样式引擎实际用到的那点 DOM:属性、事件、className、style.setProperty、classList;
|
|
8
|
+
* 没有布局信息,测量类的 API 返回零值。
|
|
9
|
+
*/
|
|
10
|
+
let nextId = 0;
|
|
11
|
+
var MiniNode = class MiniNode {
|
|
12
|
+
/** 模板里的 data-s,事件按它找回节点 */
|
|
13
|
+
sid = (nextId++).toString(36);
|
|
14
|
+
parent = null;
|
|
15
|
+
children = [];
|
|
16
|
+
/** 文本节点的文字 */
|
|
17
|
+
text = "";
|
|
18
|
+
/** 普通属性:aria-*、data-* 统一成连字符写法 */
|
|
19
|
+
attrs = /* @__PURE__ */ new Map();
|
|
20
|
+
/** 渲染时的事件属性(onClick 等):事件名 → 处理函数;随重新渲染整体替换 */
|
|
21
|
+
handlers = /* @__PURE__ */ new Map();
|
|
22
|
+
/** addEventListener 加上的 */
|
|
23
|
+
listeners = /* @__PURE__ */ new Map();
|
|
24
|
+
className = "";
|
|
25
|
+
/** 行内样式:Vue 的 style 绑定、动态样式的变量、样式绑定直接改的变量;排在类展开的样式后面 */
|
|
26
|
+
inline = /* @__PURE__ */ new Map();
|
|
27
|
+
/** 正被按着(:active) */
|
|
28
|
+
pressed = false;
|
|
29
|
+
/** 根节点才有:所在的页面 */
|
|
30
|
+
host = null;
|
|
31
|
+
/** Web 标签(div、span、button),文本节点是 #text,注释是 #comment */
|
|
32
|
+
tag;
|
|
33
|
+
constructor(tag) {
|
|
34
|
+
this.tag = tag;
|
|
35
|
+
}
|
|
36
|
+
static text(text) {
|
|
37
|
+
const node = new MiniNode("#text");
|
|
38
|
+
node.text = text;
|
|
39
|
+
return node;
|
|
40
|
+
}
|
|
41
|
+
get nextSibling() {
|
|
42
|
+
if (!this.parent) return null;
|
|
43
|
+
const siblings = this.parent.children;
|
|
44
|
+
return siblings[siblings.indexOf(this) + 1] ?? null;
|
|
45
|
+
}
|
|
46
|
+
/** 沿父节点找到页面 */
|
|
47
|
+
get page() {
|
|
48
|
+
return this.parent ? this.parent.page : this.host;
|
|
49
|
+
}
|
|
50
|
+
/** 自己的字段变了 */
|
|
51
|
+
changed() {
|
|
52
|
+
this.page?.touch(this);
|
|
53
|
+
}
|
|
54
|
+
insertBefore(child, anchor) {
|
|
55
|
+
child.parent?.removeChild(child);
|
|
56
|
+
const index = anchor ? this.children.indexOf(anchor) : -1;
|
|
57
|
+
if (index < 0) this.children.push(child);
|
|
58
|
+
else this.children.splice(index, 0, child);
|
|
59
|
+
child.parent = this;
|
|
60
|
+
this.page?.restructure(this);
|
|
61
|
+
}
|
|
62
|
+
removeChild(child) {
|
|
63
|
+
const index = this.children.indexOf(child);
|
|
64
|
+
if (index < 0) return;
|
|
65
|
+
this.children.splice(index, 1);
|
|
66
|
+
child.parent = null;
|
|
67
|
+
this.page?.restructure(this);
|
|
68
|
+
}
|
|
69
|
+
setChildren(children) {
|
|
70
|
+
for (const child of this.children) child.parent = null;
|
|
71
|
+
this.children.splice(0, this.children.length, ...children);
|
|
72
|
+
for (const child of children) child.parent = this;
|
|
73
|
+
this.page?.restructure(this);
|
|
74
|
+
}
|
|
75
|
+
/** 属性的字符串值;没有这个属性时 undefined(布尔属性为 false 时也算没有) */
|
|
76
|
+
attribute(name) {
|
|
77
|
+
const value = this.attrs.get(name);
|
|
78
|
+
if (value === void 0 || value === null) return void 0;
|
|
79
|
+
if (value === false && BOOLEAN_ATTRS.has(name)) return void 0;
|
|
80
|
+
if (value === true) return BOOLEAN_ATTRS.has(name) ? "" : "true";
|
|
81
|
+
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
82
|
+
}
|
|
83
|
+
setAttribute(name, value) {
|
|
84
|
+
this.attrs.set(name, value);
|
|
85
|
+
this.changed();
|
|
86
|
+
}
|
|
87
|
+
/** 输入框的值:用户输入时运行时随事件写回(page.ts),组件照 DOM 的写法读 `event.target.value` */
|
|
88
|
+
get value() {
|
|
89
|
+
return this.attribute("value") ?? "";
|
|
90
|
+
}
|
|
91
|
+
/** 让输入框获得焦点:模板的 focus 属性;失焦时运行时去掉 */
|
|
92
|
+
focus() {
|
|
93
|
+
this.setAttribute("autofocus", true);
|
|
94
|
+
}
|
|
95
|
+
/** 让输入框失去焦点 */
|
|
96
|
+
blur() {
|
|
97
|
+
this.removeAttribute("autofocus");
|
|
98
|
+
}
|
|
99
|
+
getAttribute(name) {
|
|
100
|
+
return this.attribute(name) ?? null;
|
|
101
|
+
}
|
|
102
|
+
removeAttribute(name) {
|
|
103
|
+
this.attrs.delete(name);
|
|
104
|
+
this.changed();
|
|
105
|
+
}
|
|
106
|
+
addEventListener(type, listener) {
|
|
107
|
+
const list = this.listeners.get(type);
|
|
108
|
+
if (list) list.push(listener);
|
|
109
|
+
else this.listeners.set(type, [listener]);
|
|
110
|
+
}
|
|
111
|
+
removeEventListener(type, listener) {
|
|
112
|
+
const list = this.listeners.get(type);
|
|
113
|
+
const index = list?.indexOf(listener) ?? -1;
|
|
114
|
+
if (index >= 0) list.splice(index, 1);
|
|
115
|
+
}
|
|
116
|
+
/** 样式引擎的绑定(bindings.ts)直接改变量用;赋值写法(vShow 的 style.display)也走这里 */
|
|
117
|
+
style = new Proxy({
|
|
118
|
+
setProperty: (name, value) => {
|
|
119
|
+
this.inline.set(name, value);
|
|
120
|
+
this.changed();
|
|
121
|
+
},
|
|
122
|
+
removeProperty: (name) => {
|
|
123
|
+
this.inline.delete(name);
|
|
124
|
+
this.changed();
|
|
125
|
+
}
|
|
126
|
+
}, { set: (target, key, value) => {
|
|
127
|
+
if (typeof key !== "string") return false;
|
|
128
|
+
const name = key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
129
|
+
if (value === "" || value === null) target.removeProperty(name);
|
|
130
|
+
else target.setProperty(name, String(value));
|
|
131
|
+
return true;
|
|
132
|
+
} });
|
|
133
|
+
/** 样式引擎的 css.select 就地换组合类用 */
|
|
134
|
+
classList = {
|
|
135
|
+
add: (...names) => this.updateClasses((list) => list.push(...names)),
|
|
136
|
+
remove: (...names) => this.updateClasses((list) => {
|
|
137
|
+
for (const name of names) {
|
|
138
|
+
const index = list.indexOf(name);
|
|
139
|
+
if (index >= 0) list.splice(index, 1);
|
|
140
|
+
}
|
|
141
|
+
}),
|
|
142
|
+
replace: (from, to) => this.updateClasses((list) => {
|
|
143
|
+
const index = list.indexOf(from);
|
|
144
|
+
if (index >= 0) list[index] = to;
|
|
145
|
+
}),
|
|
146
|
+
contains: (name) => this.className.split(/\s+/).includes(name)
|
|
147
|
+
};
|
|
148
|
+
updateClasses(edit) {
|
|
149
|
+
const list = this.className.split(/\s+/).filter(Boolean);
|
|
150
|
+
edit(list);
|
|
151
|
+
this.className = list.join(" ");
|
|
152
|
+
this.changed();
|
|
153
|
+
}
|
|
154
|
+
getBoundingClientRect() {
|
|
155
|
+
return {
|
|
156
|
+
x: 0,
|
|
157
|
+
y: 0,
|
|
158
|
+
top: 0,
|
|
159
|
+
left: 0,
|
|
160
|
+
right: 0,
|
|
161
|
+
bottom: 0,
|
|
162
|
+
width: 0,
|
|
163
|
+
height: 0
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/renderer.ts
|
|
169
|
+
/**
|
|
170
|
+
* Vue 的自定义渲染器:虚拟节点落到 MiniNode 树上(node.ts),组件、组合式 API、样式引擎都照常工作。
|
|
171
|
+
* 属性按 Vue 运行时 DOM 的规则归类:class、style、on* 事件,其余是元素属性。
|
|
172
|
+
*/
|
|
173
|
+
/** 'onClick' → 'click','onTouchstart' → 'touchstart';去掉 Once / Capture / Passive 修饰(同 Vue 运行时 DOM) */
|
|
174
|
+
const eventName = (key) => key.slice(2).replace(/(?:Once|Capture|Passive)+$/, "").replace(/\B([A-Z])/g, "-$1").toLowerCase();
|
|
175
|
+
/** 'ariaBusy' → 'aria-busy':组件里两种写法都有,条件判定按连字符查 */
|
|
176
|
+
const attributeName = (key) => /^(?:aria|data)[A-Z]/.test(key) ? key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`) : key;
|
|
177
|
+
/** style 绑定的值 → [属性, 值];字符串按分号切开,对象的 camelCase 转连字符 */
|
|
178
|
+
function styleEntries(value) {
|
|
179
|
+
if (!value) return [];
|
|
180
|
+
if (typeof value === "string") return value.split(";").map((item) => item.split(/:(.*)/s).map((part) => part.trim())).filter(([name, text]) => name && text).map(([name, text]) => [name, text]);
|
|
181
|
+
if (Array.isArray(value)) return value.flatMap(styleEntries);
|
|
182
|
+
return Object.entries(value).filter(([, text]) => text !== void 0 && text !== null && text !== "").map(([name, text]) => [name.startsWith("--") ? name : name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`), String(text)]);
|
|
183
|
+
}
|
|
184
|
+
function patchStyle(node, previous, next) {
|
|
185
|
+
for (const [name] of styleEntries(previous)) node.inline.delete(name);
|
|
186
|
+
for (const [name, value] of styleEntries(next)) node.inline.set(name, value);
|
|
187
|
+
node.changed();
|
|
188
|
+
}
|
|
189
|
+
function patchHandler(node, key, next) {
|
|
190
|
+
const type = eventName(key);
|
|
191
|
+
const list = (Array.isArray(next) ? next : [next]).filter((handler) => typeof handler === "function");
|
|
192
|
+
if (list.length) node.handlers.set(type, list);
|
|
193
|
+
else node.handlers.delete(type);
|
|
194
|
+
}
|
|
195
|
+
const renderer = createRenderer({
|
|
196
|
+
createElement: (tag) => new MiniNode(tag),
|
|
197
|
+
createText: (text) => MiniNode.text(text),
|
|
198
|
+
createComment: () => new MiniNode("#comment"),
|
|
199
|
+
setText(node, text) {
|
|
200
|
+
node.text = text;
|
|
201
|
+
node.changed();
|
|
202
|
+
},
|
|
203
|
+
setElementText(node, text) {
|
|
204
|
+
node.setChildren(text ? [MiniNode.text(text)] : []);
|
|
205
|
+
},
|
|
206
|
+
insert: (child, parent, anchor) => parent.insertBefore(child, anchor ?? null),
|
|
207
|
+
remove: (child) => child.parent?.removeChild(child),
|
|
208
|
+
parentNode: (node) => node.parent,
|
|
209
|
+
nextSibling: (node) => node.nextSibling,
|
|
210
|
+
patchProp(node, key, previous, next) {
|
|
211
|
+
if (key === "class") {
|
|
212
|
+
node.className = next ?? "";
|
|
213
|
+
node.changed();
|
|
214
|
+
} else if (key === "style") patchStyle(node, previous, next);
|
|
215
|
+
else if (/^on[A-Z]/.test(key)) patchHandler(node, key, next);
|
|
216
|
+
else {
|
|
217
|
+
const name = attributeName(key);
|
|
218
|
+
if (next === void 0 || next === null) node.attrs.delete(name);
|
|
219
|
+
else node.attrs.set(name, next);
|
|
220
|
+
node.changed();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
/** 建一个渲染到 MiniNode 树上的 Vue 应用 */
|
|
225
|
+
const createMiniApp = (component, props) => renderer.createApp(component, props);
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/capability.ts
|
|
228
|
+
/** 设备权限 → 微信的 scope */
|
|
229
|
+
const WX_SCOPES = {
|
|
230
|
+
location: "scope.userLocation",
|
|
231
|
+
fuzzyLocation: "scope.userFuzzyLocation",
|
|
232
|
+
camera: "scope.camera",
|
|
233
|
+
microphone: "scope.record",
|
|
234
|
+
album: "scope.writePhotosAlbum",
|
|
235
|
+
bluetooth: "scope.bluetooth"
|
|
236
|
+
};
|
|
237
|
+
/** 设备权限 → 支付宝 authSetting 的键;蓝牙没有小程序这一层的授权(只有系统的) */
|
|
238
|
+
const MY_SCOPES = {
|
|
239
|
+
location: "location",
|
|
240
|
+
fuzzyLocation: "location",
|
|
241
|
+
camera: "camera",
|
|
242
|
+
microphone: "audioRecord",
|
|
243
|
+
album: "album"
|
|
244
|
+
};
|
|
245
|
+
/** 支付宝申请时调一次就会弹授权、又不打开界面的接口 */
|
|
246
|
+
const MY_TRIGGERS = {
|
|
247
|
+
location: (api) => call(api, "getLocation"),
|
|
248
|
+
fuzzyLocation: (api) => call(api, "getLocation"),
|
|
249
|
+
bluetooth: async (api) => {
|
|
250
|
+
await call(api, "openBluetoothAdapter");
|
|
251
|
+
await call(api, "closeBluetoothAdapter").catch(() => {});
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
/** 回调式接口转 Promise;接口不存在时报出名字 */
|
|
255
|
+
function call(api, name, options = {}) {
|
|
256
|
+
const method = api[name];
|
|
257
|
+
if (!method) return Promise.reject(/* @__PURE__ */ new Error(`当前平台没有 ${name}`));
|
|
258
|
+
return new Promise((resolve, reject) => method.call(api, {
|
|
259
|
+
...options,
|
|
260
|
+
success: resolve,
|
|
261
|
+
fail: reject
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
const stateOf = (value) => value === true ? "granted" : value === false ? "denied" : "prompt";
|
|
265
|
+
/** 支付宝 tradePay 的结果码 */
|
|
266
|
+
const ALIPAY_RESULTS = {
|
|
267
|
+
"9000": "success",
|
|
268
|
+
"8000": "pending",
|
|
269
|
+
"6004": "pending",
|
|
270
|
+
"6001": "cancel"
|
|
271
|
+
};
|
|
272
|
+
/** 隐私协议:平台一个回调,页面里可能有多个监听(每个页面的弹窗) */
|
|
273
|
+
const privacyListeners = /* @__PURE__ */ new Set();
|
|
274
|
+
let privacyHooked = false;
|
|
275
|
+
function hookPrivacy(api) {
|
|
276
|
+
if (privacyHooked || !api.onNeedPrivacyAuthorization) return;
|
|
277
|
+
privacyHooked = true;
|
|
278
|
+
api.onNeedPrivacyAuthorization((resolve) => {
|
|
279
|
+
let answered = false;
|
|
280
|
+
const reply = (agreed, buttonId) => {
|
|
281
|
+
if (answered) return;
|
|
282
|
+
answered = true;
|
|
283
|
+
resolve(agreed ? {
|
|
284
|
+
event: "agree",
|
|
285
|
+
buttonId
|
|
286
|
+
} : { event: "disagree" });
|
|
287
|
+
};
|
|
288
|
+
if (!privacyListeners.size) return reply(false);
|
|
289
|
+
for (const listener of privacyListeners) listener(reply);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/** 授权类能力;current 不在小程序里(测试)时,用到才报错 */
|
|
293
|
+
function createCapabilities(current) {
|
|
294
|
+
const api = () => {
|
|
295
|
+
if (!current) throw new Error("不在小程序里:宿主能力不可用");
|
|
296
|
+
return current.api;
|
|
297
|
+
};
|
|
298
|
+
const scopeOf = (name) => current?.name === "my" ? MY_SCOPES[name] : WX_SCOPES[name];
|
|
299
|
+
const status = async (name) => {
|
|
300
|
+
const scope = scopeOf(name);
|
|
301
|
+
if (!scope) return "prompt";
|
|
302
|
+
const { authSetting } = await call(api(), "getSetting");
|
|
303
|
+
return stateOf(authSetting[scope]);
|
|
304
|
+
};
|
|
305
|
+
const isWx = () => current?.name !== "my";
|
|
306
|
+
return {
|
|
307
|
+
permissions: {
|
|
308
|
+
status,
|
|
309
|
+
async request(name) {
|
|
310
|
+
const current = await status(name);
|
|
311
|
+
if (current !== "prompt") return current;
|
|
312
|
+
if (isWx()) try {
|
|
313
|
+
await call(api(), "authorize", { scope: WX_SCOPES[name] });
|
|
314
|
+
return "granted";
|
|
315
|
+
} catch {
|
|
316
|
+
return status(name);
|
|
317
|
+
}
|
|
318
|
+
const trigger = MY_TRIGGERS[name];
|
|
319
|
+
if (!trigger) return "prompt";
|
|
320
|
+
try {
|
|
321
|
+
await trigger(api());
|
|
322
|
+
return "granted";
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (error.error === 14) return "denied";
|
|
325
|
+
return status(name);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
async openSettings(name) {
|
|
329
|
+
const scope = scopeOf(name);
|
|
330
|
+
const { authSetting } = await call(api(), "openSetting");
|
|
331
|
+
return scope ? stateOf(authSetting[scope]) : "prompt";
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
storage: {
|
|
335
|
+
get(key) {
|
|
336
|
+
const raw = isWx() ? api().getStorageSync?.(key) : (api().getStorageSync?.({ key }))?.data;
|
|
337
|
+
if (typeof raw !== "string" || raw === "") return void 0;
|
|
338
|
+
try {
|
|
339
|
+
return JSON.parse(raw);
|
|
340
|
+
} catch {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
set(key, value) {
|
|
345
|
+
const data = JSON.stringify(value);
|
|
346
|
+
if (isWx()) api().setStorageSync?.(key, data);
|
|
347
|
+
else api().setStorageSync?.({
|
|
348
|
+
key,
|
|
349
|
+
data
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
remove(key) {
|
|
353
|
+
api().removeStorageSync?.(isWx() ? key : { key });
|
|
354
|
+
},
|
|
355
|
+
keys: () => api().getStorageInfoSync?.().keys ?? []
|
|
356
|
+
},
|
|
357
|
+
async login(options) {
|
|
358
|
+
if (isWx()) {
|
|
359
|
+
const { code } = await call(api(), "login");
|
|
360
|
+
return {
|
|
361
|
+
platform: "wx",
|
|
362
|
+
code
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const { authCode } = await call(api(), "getAuthCode", { scopes: options?.scope === "user" ? "auth_user" : "auth_base" });
|
|
366
|
+
return {
|
|
367
|
+
platform: "my",
|
|
368
|
+
code: authCode
|
|
369
|
+
};
|
|
370
|
+
},
|
|
371
|
+
async pay(order) {
|
|
372
|
+
if (order.provider === "wechat" && isWx()) {
|
|
373
|
+
const { provider: _, appId: __, ...params } = order;
|
|
374
|
+
try {
|
|
375
|
+
return {
|
|
376
|
+
status: "success",
|
|
377
|
+
raw: await call(api(), "requestPayment", params)
|
|
378
|
+
};
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return {
|
|
381
|
+
status: (error.errMsg ?? "").includes("cancel") ? "cancel" : "fail",
|
|
382
|
+
raw: error
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (order.provider === "alipay" && !isWx()) {
|
|
387
|
+
const result = await call(api(), "tradePay", { tradeNO: order.tradeNO });
|
|
388
|
+
return {
|
|
389
|
+
status: ALIPAY_RESULTS[result.resultCode] ?? "fail",
|
|
390
|
+
raw: result
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
throw new Error(`${current?.name ?? "小程序"} 里不能用 ${order.provider} 方式支付`);
|
|
394
|
+
},
|
|
395
|
+
async subscribe(templateIds) {
|
|
396
|
+
if (isWx()) {
|
|
397
|
+
const result = await call(api(), "requestSubscribeMessage", { tmplIds: templateIds });
|
|
398
|
+
return Object.fromEntries(templateIds.map((id) => {
|
|
399
|
+
const value = result[id];
|
|
400
|
+
return [id, value === "accept" || value === "ban" ? value : "reject"];
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
const result = await call(api(), "requestSubscribeMessage", { entityIds: templateIds });
|
|
404
|
+
const accepted = new Set(result.result?.subscribeEntityIds ?? []);
|
|
405
|
+
return Object.fromEntries(templateIds.map((id) => [id, accepted.has(id) ? "accept" : "reject"]));
|
|
406
|
+
},
|
|
407
|
+
async phoneNumber(detail) {
|
|
408
|
+
if (isWx()) {
|
|
409
|
+
const { code, errMsg } = detail ?? {};
|
|
410
|
+
if (!code) throw new Error(errMsg ?? "用户没有同意手机号授权");
|
|
411
|
+
return {
|
|
412
|
+
platform: "wx",
|
|
413
|
+
code
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const { response } = await call(api(), "getPhoneNumber");
|
|
417
|
+
return {
|
|
418
|
+
platform: "my",
|
|
419
|
+
response
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
onPrivacyRequest(listener) {
|
|
423
|
+
if (current) hookPrivacy(current.api);
|
|
424
|
+
privacyListeners.add(listener);
|
|
425
|
+
return () => privacyListeners.delete(listener);
|
|
426
|
+
},
|
|
427
|
+
async openPrivacyContract() {
|
|
428
|
+
await call(api(), "openPrivacyContract");
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/host.ts
|
|
434
|
+
/**
|
|
435
|
+
* 宿主约定的小程序实现:组件库(@zerodep-dev/vue-tsx-pro-mobile 的 useHost)要用的平台能力。
|
|
436
|
+
* 每个页面一份,在页面的 Vue 应用上用全局 Symbol 键 provide,两边互不导入;接口的形状以组件库的 Host 为准。
|
|
437
|
+
*/
|
|
438
|
+
/** 注入键:与组件库约定的同一个全局 Symbol */
|
|
439
|
+
const HOST_KEY = Symbol.for("vue-tsx-pro.host");
|
|
440
|
+
/** 当前平台;不在小程序里(测试)时 undefined */
|
|
441
|
+
function platform() {
|
|
442
|
+
if (typeof wx !== "undefined") return {
|
|
443
|
+
name: "wx",
|
|
444
|
+
api: wx
|
|
445
|
+
};
|
|
446
|
+
if (typeof my !== "undefined") return {
|
|
447
|
+
name: "my",
|
|
448
|
+
api: my
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
/** 窗口大小 */
|
|
452
|
+
function windowSize(api) {
|
|
453
|
+
const info = api.getWindowInfo?.() ?? api.getSystemInfoSync();
|
|
454
|
+
return {
|
|
455
|
+
width: info.windowWidth,
|
|
456
|
+
height: info.windowHeight
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
/** 一个页面的平台能力 */
|
|
460
|
+
function createMiniHost(page) {
|
|
461
|
+
const current = platform();
|
|
462
|
+
const api = () => {
|
|
463
|
+
if (!current) throw new Error("不在小程序里:宿主能力不可用");
|
|
464
|
+
return current.api;
|
|
465
|
+
};
|
|
466
|
+
return {
|
|
467
|
+
platform: current?.name ?? "wx",
|
|
468
|
+
/** 量节点:节点没有 id 时临时给一个(平时不发 id,省 setData 的体积),等它上了模板再查 */
|
|
469
|
+
async measure(element) {
|
|
470
|
+
const node = element;
|
|
471
|
+
if (!node?.attrs || !current) return null;
|
|
472
|
+
let id = node.attribute("id");
|
|
473
|
+
if (!id) {
|
|
474
|
+
id = `vtm-${node.sid}`;
|
|
475
|
+
node.setAttribute("id", id);
|
|
476
|
+
}
|
|
477
|
+
await page.flushed();
|
|
478
|
+
return new Promise((resolve) => api().createSelectorQuery().select(`#${id}`).boundingClientRect().exec((results) => resolve(results[0] ?? null)));
|
|
479
|
+
},
|
|
480
|
+
window: () => current ? windowSize(current.api) : {
|
|
481
|
+
width: 375,
|
|
482
|
+
height: 667
|
|
483
|
+
},
|
|
484
|
+
scrollTo(top, options) {
|
|
485
|
+
api().pageScrollTo({
|
|
486
|
+
scrollTop: top,
|
|
487
|
+
duration: options?.smooth ? 300 : 0
|
|
488
|
+
});
|
|
489
|
+
},
|
|
490
|
+
scrollTop: () => page.scrollTop,
|
|
491
|
+
onScroll(listener) {
|
|
492
|
+
page.scrollListeners.add(listener);
|
|
493
|
+
return () => page.scrollListeners.delete(listener);
|
|
494
|
+
},
|
|
495
|
+
nextFrame(callback) {
|
|
496
|
+
let cancelled = false;
|
|
497
|
+
page.flushed().then(() => setTimeout(() => !cancelled && callback(), 16));
|
|
498
|
+
return () => {
|
|
499
|
+
cancelled = true;
|
|
500
|
+
};
|
|
501
|
+
},
|
|
502
|
+
lockScroll: () => () => {},
|
|
503
|
+
overlayRoot: () => page.overlay,
|
|
504
|
+
vibrate() {
|
|
505
|
+
api().vibrateShort?.({ type: "light" });
|
|
506
|
+
},
|
|
507
|
+
copy: (text) => new Promise((resolve, reject) => api().setClipboardData({
|
|
508
|
+
data: text,
|
|
509
|
+
success: () => resolve(),
|
|
510
|
+
fail: reject
|
|
511
|
+
})),
|
|
512
|
+
chooseImages: ({ count }) => new Promise((resolve, reject) => {
|
|
513
|
+
const mini = api();
|
|
514
|
+
if (mini.chooseMedia) mini.chooseMedia({
|
|
515
|
+
count,
|
|
516
|
+
mediaType: ["image"],
|
|
517
|
+
success: ({ tempFiles }) => resolve(tempFiles.map((file) => ({
|
|
518
|
+
url: file.tempFilePath,
|
|
519
|
+
size: file.size
|
|
520
|
+
}))),
|
|
521
|
+
fail: reject
|
|
522
|
+
});
|
|
523
|
+
else mini.chooseImage?.({
|
|
524
|
+
count,
|
|
525
|
+
success: (result) => resolve((result.apFilePaths ?? result.tempFilePaths ?? []).map((url) => ({
|
|
526
|
+
url,
|
|
527
|
+
size: 0
|
|
528
|
+
}))),
|
|
529
|
+
fail: reject
|
|
530
|
+
});
|
|
531
|
+
}),
|
|
532
|
+
...createCapabilities(current)
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/style.ts
|
|
537
|
+
/**
|
|
538
|
+
* 元素的最终样式:引擎的组合类展开成行内样式(小程序不能在运行时插规则),条件由这里按节点状态判定;
|
|
539
|
+
* 其余类名原样保留给使用方自己的样式表。
|
|
540
|
+
*
|
|
541
|
+
* 条件只认组件库实际写得出来的几种(见 tsx 的 declarations.ts 的条件别名):
|
|
542
|
+
* - 伪类::active 看节点是否按着;:disabled、:checked 看布尔属性;:first-child、:last-child、:only-child 看元素兄弟;
|
|
543
|
+
* :hover、:focus 系列在触屏上不成立
|
|
544
|
+
* - 属性选择器 [name]、[name="value"],以及 :is / :where / :not 包着的组合
|
|
545
|
+
* - @media:宽度按窗口算,prefers-color-scheme 按系统的深浅色;减少动效、强制颜色、悬停能力一律不成立
|
|
546
|
+
* 别的写法(标签与类名选择器、:nth-child、@supports、伪元素……)展开不了,按不成立处理,开发构建里每种提示一次。
|
|
547
|
+
*/
|
|
548
|
+
/** 窗口宽度(px),@media 的宽度条件用;页面初始化时设一次(page.ts) */
|
|
549
|
+
let windowWidth = 375;
|
|
550
|
+
const setWindowWidth = (width) => {
|
|
551
|
+
windowWidth = width;
|
|
552
|
+
};
|
|
553
|
+
/** 系统的深浅色,prefers-color-scheme 用;页面初始化与系统切换时设(page.ts) */
|
|
554
|
+
let colorScheme = "light";
|
|
555
|
+
const setColorScheme = (scheme) => {
|
|
556
|
+
colorScheme = scheme;
|
|
557
|
+
};
|
|
558
|
+
function resolveStyle(node) {
|
|
559
|
+
const classes = [baseClassOf(node.tag)];
|
|
560
|
+
const declarations = [];
|
|
561
|
+
for (const name of node.className.split(/\s+/)) {
|
|
562
|
+
if (!name) continue;
|
|
563
|
+
const inline = inlineStyle(name, (condition) => matchCondition(node, condition), (selector) => unsupported(selector, "伪元素进不了行内样式"));
|
|
564
|
+
if (inline === void 0) classes.push(name);
|
|
565
|
+
else if (inline) declarations.push(inline);
|
|
566
|
+
}
|
|
567
|
+
for (const [name, value] of node.inline) declarations.push(`${name}:${value}`);
|
|
568
|
+
return {
|
|
569
|
+
className: classes.filter(Boolean).join(" "),
|
|
570
|
+
style: declarations.join(";")
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
/** 已经提示过的写法 */
|
|
574
|
+
const warned = /* @__PURE__ */ new Set();
|
|
575
|
+
/** 开发构建里提示一次展开不了的样式写法,返回 false(按不成立处理);生产构建里 NODE_ENV 换成了常量,提示整段去掉 */
|
|
576
|
+
function unsupported(condition, reason) {
|
|
577
|
+
if (process.env.NODE_ENV !== "production" && !warned.has(condition)) {
|
|
578
|
+
warned.add(condition);
|
|
579
|
+
console.warn(`[vtp-mini] 样式条件 ${condition} 在小程序里展开不了(${reason}),按不成立处理`);
|
|
580
|
+
}
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
/** 判定条件链里的一段:@ 规则或选择器(伪类、属性选择器的组合) */
|
|
584
|
+
function matchCondition(node, condition) {
|
|
585
|
+
if (condition.startsWith("@media")) return matchMedia(condition.slice(6));
|
|
586
|
+
if (condition.startsWith("@")) return unsupported(condition, "只认 @media");
|
|
587
|
+
return matchCompound(node, condition);
|
|
588
|
+
}
|
|
589
|
+
/** 小程序里当作没有的媒体能力:不提示,直接不成立 */
|
|
590
|
+
const ABSENT_MEDIA = /^\(\s*(?:prefers-reduced-motion|prefers-contrast|forced-colors|any-hover|hover|any-pointer|pointer)\b/;
|
|
591
|
+
/** 媒体查询:and 连接的各项都成立才成立;只认宽度与深浅色,其余能力在小程序里当作没有 */
|
|
592
|
+
function matchMedia(query) {
|
|
593
|
+
return query.trim().split(/\s+and\s+/).every((feature) => {
|
|
594
|
+
if (feature === "all" || feature === "screen") return true;
|
|
595
|
+
if (feature === "print" || ABSENT_MEDIA.test(feature)) return false;
|
|
596
|
+
const scheme = /^\(\s*prefers-color-scheme\s*:\s*(light|dark)\s*\)$/.exec(feature);
|
|
597
|
+
if (scheme) return scheme[1] === colorScheme;
|
|
598
|
+
const match = /^\(\s*(min|max)-width\s*:\s*([\d.]+)px\s*\)$/.exec(feature);
|
|
599
|
+
if (!match) return unsupported(`@media ${feature}`, "宽度只认 px 的 min-width / max-width");
|
|
600
|
+
const limit = Number(match[2]);
|
|
601
|
+
return match[1] === "min" ? windowWidth >= limit : windowWidth <= limit;
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
/** 复合选择器(`:hover:not([x])`、`[a="b"]`):每一段都成立才成立 */
|
|
605
|
+
function matchCompound(node, selector) {
|
|
606
|
+
let index = 0;
|
|
607
|
+
while (index < selector.length) {
|
|
608
|
+
const char = selector[index];
|
|
609
|
+
if (char === "[") {
|
|
610
|
+
const end = closing(selector, index, "[", "]");
|
|
611
|
+
if (!matchAttribute(node, selector.slice(index + 1, end))) return false;
|
|
612
|
+
index = end + 1;
|
|
613
|
+
} else if (char === ":") {
|
|
614
|
+
const name = /^:([a-z-]+)/.exec(selector.slice(index))[1];
|
|
615
|
+
index += name.length + 1;
|
|
616
|
+
let argument = "";
|
|
617
|
+
if (selector[index] === "(") {
|
|
618
|
+
const end = closing(selector, index, "(", ")");
|
|
619
|
+
argument = selector.slice(index + 1, end);
|
|
620
|
+
index = end + 1;
|
|
621
|
+
}
|
|
622
|
+
if (!matchPseudo(node, name, argument)) return false;
|
|
623
|
+
} else return unsupported(selector, "只认伪类与属性选择器");
|
|
624
|
+
}
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
function matchPseudo(node, name, argument) {
|
|
628
|
+
switch (name) {
|
|
629
|
+
case "is":
|
|
630
|
+
case "where": return splitList(argument).some((part) => matchCompound(node, part));
|
|
631
|
+
case "not": return !splitList(argument).some((part) => matchCompound(node, part));
|
|
632
|
+
case "active": return node.pressed;
|
|
633
|
+
case "first-child": return elementSiblings(node)[0] === node;
|
|
634
|
+
case "last-child": return elementSiblings(node).at(-1) === node;
|
|
635
|
+
case "only-child": return elementSiblings(node).length === 1;
|
|
636
|
+
case "disabled":
|
|
637
|
+
case "checked":
|
|
638
|
+
case "indeterminate": return node.attribute(name) !== void 0;
|
|
639
|
+
case "hover":
|
|
640
|
+
case "focus":
|
|
641
|
+
case "focus-visible":
|
|
642
|
+
case "focus-within":
|
|
643
|
+
case "dir": return false;
|
|
644
|
+
default: return unsupported(`:${name}${argument ? `(${argument})` : ""}`, "运行时不认这个伪类");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/** 元素兄弟(含自己):结构伪类同 CSS,不算文本与注释节点(Vue 的片段锚点) */
|
|
648
|
+
const elementSiblings = (node) => node.parent ? node.parent.children.filter((sibling) => !sibling.tag.startsWith("#")) : [node];
|
|
649
|
+
/** `name`、`name="value"`、`name=value` */
|
|
650
|
+
function matchAttribute(node, body) {
|
|
651
|
+
const equals = body.indexOf("=");
|
|
652
|
+
if (equals < 0) return node.attribute(body.trim()) !== void 0;
|
|
653
|
+
const name = body.slice(0, equals).trim();
|
|
654
|
+
const expected = body.slice(equals + 1).trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
655
|
+
return node.attribute(name) === expected;
|
|
656
|
+
}
|
|
657
|
+
/** 从 open 处找到配对的 close(跳过引号里的内容) */
|
|
658
|
+
function closing(text, start, open, close) {
|
|
659
|
+
let depth = 0;
|
|
660
|
+
let quote = "";
|
|
661
|
+
for (let index = start; index < text.length; index++) {
|
|
662
|
+
const char = text[index];
|
|
663
|
+
if (quote) {
|
|
664
|
+
if (char === quote) quote = "";
|
|
665
|
+
} else if (char === "\"" || char === "'") quote = char;
|
|
666
|
+
else if (char === open) depth++;
|
|
667
|
+
else if (char === close && --depth === 0) return index;
|
|
668
|
+
}
|
|
669
|
+
throw new Error(`样式条件的括号不配对:${text}`);
|
|
670
|
+
}
|
|
671
|
+
/** 选择器列表按顶层逗号切开 */
|
|
672
|
+
function splitList(list) {
|
|
673
|
+
const parts = [];
|
|
674
|
+
let depth = 0;
|
|
675
|
+
let start = 0;
|
|
676
|
+
for (let index = 0; index < list.length; index++) {
|
|
677
|
+
const char = list[index];
|
|
678
|
+
if (char === "(" || char === "[") depth++;
|
|
679
|
+
else if (char === ")" || char === "]") depth--;
|
|
680
|
+
else if (char === "," && depth === 0) {
|
|
681
|
+
parts.push(list.slice(start, index).trim());
|
|
682
|
+
start = index + 1;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
parts.push(list.slice(start).trim());
|
|
686
|
+
return parts;
|
|
687
|
+
}
|
|
688
|
+
//#endregion
|
|
689
|
+
//#region src/svg.ts
|
|
690
|
+
/** svg 节点的行内样式:遮罩加底色,排在组合类展开的样式前面(尺寸、动画由后者给) */
|
|
691
|
+
function svgMaskStyle(svg) {
|
|
692
|
+
const uri = `url("data:image/svg+xml,${encode(markup(svg, true))}")`;
|
|
693
|
+
return [
|
|
694
|
+
"display:inline-block",
|
|
695
|
+
"background-color:currentColor",
|
|
696
|
+
`-webkit-mask:${uri} center/100% 100% no-repeat`,
|
|
697
|
+
`mask:${uri} center/100% 100% no-repeat`
|
|
698
|
+
].join(";");
|
|
699
|
+
}
|
|
700
|
+
function markup(node, root) {
|
|
701
|
+
if (node.tag === "#text") return escapeText(node.text);
|
|
702
|
+
if (node.tag === "#comment") return "";
|
|
703
|
+
const attrs = root ? [" xmlns=\"http://www.w3.org/2000/svg\""] : [];
|
|
704
|
+
for (const name of node.attrs.keys()) {
|
|
705
|
+
if (name === "innerHTML" || name === "class" || name === "style" || name.startsWith("aria-")) continue;
|
|
706
|
+
const value = node.attribute(name);
|
|
707
|
+
if (value === void 0 || value === "false") continue;
|
|
708
|
+
const text = value === "currentColor" ? "#000" : value;
|
|
709
|
+
attrs.push(` ${svgName(name)}="${escapeAttribute(text)}"`);
|
|
710
|
+
}
|
|
711
|
+
if (root && !node.attrs.has("stroke-width") && !node.attrs.has("strokeWidth")) attrs.push(" stroke-width=\"2\"");
|
|
712
|
+
const inner = node.attrs.get("innerHTML");
|
|
713
|
+
const children = typeof inner === "string" ? inner : node.children.map((child) => markup(child, false)).join("");
|
|
714
|
+
return `<${node.tag}${attrs.join("")}>${children}</${node.tag}>`;
|
|
715
|
+
}
|
|
716
|
+
/** 属性名:camelCase 转连字符,SVG 自己区分大小写的(viewBox)保留 */
|
|
717
|
+
const svgName = (name) => name === "viewBox" ? name : name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
718
|
+
const escapeAttribute = (text) => text.replaceAll("&", "&").replaceAll("\"", """);
|
|
719
|
+
const escapeText = (text) => text.replaceAll("&", "&").replaceAll("<", "<");
|
|
720
|
+
/** data URI 里只转义必须转义的字符,比 encodeURIComponent 短,可读 */
|
|
721
|
+
const encode = (svg) => svg.replace(/["%#<>{}]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/lifecycle.ts
|
|
724
|
+
/**
|
|
725
|
+
* 页面的生命周期与跳转:组件里注册页面显示、隐藏、触底的回调;按 definePages 的页面名跳转。
|
|
726
|
+
*
|
|
727
|
+
* onPageShow(() => refresh()); // 从下一页返回时也会调用
|
|
728
|
+
* onReachBottom(() => loadMore());
|
|
729
|
+
* navigateTo('detail', { id: '42' }); // 页面组件收到 query 属性 { id: '42' }
|
|
730
|
+
* navigateBack();
|
|
731
|
+
*
|
|
732
|
+
* 注册类的函数在 H5(不在小程序页面里)调用时什么也不做,同一个组件两端都能用。
|
|
733
|
+
*/
|
|
734
|
+
/** 注入键:页面的 Vue 应用上 provide(page.ts) */
|
|
735
|
+
const PAGE_HOOKS_KEY = Symbol("vue-tsx-pro.mini.page-hooks");
|
|
736
|
+
/** 新建一个页面的回调表 */
|
|
737
|
+
const createPageHooks = () => /* @__PURE__ */ new Map([
|
|
738
|
+
["show", /* @__PURE__ */ new Set()],
|
|
739
|
+
["hide", /* @__PURE__ */ new Set()],
|
|
740
|
+
["reachBottom", /* @__PURE__ */ new Set()]
|
|
741
|
+
]);
|
|
742
|
+
/** 调用一个页面上某个事件的回调 */
|
|
743
|
+
function emitPageEvent(hooks, event) {
|
|
744
|
+
for (const listener of hooks.get(event)) listener();
|
|
745
|
+
}
|
|
746
|
+
function register(event, listener) {
|
|
747
|
+
const hooks = inject(PAGE_HOOKS_KEY, null);
|
|
748
|
+
if (!hooks) return;
|
|
749
|
+
hooks.get(event).add(listener);
|
|
750
|
+
onBeforeUnmount(() => hooks.get(event).delete(listener));
|
|
751
|
+
}
|
|
752
|
+
/** 页面显示(第一次进入,以及从别的页面、后台回来)时;组件 setup 里调用 */
|
|
753
|
+
const onPageShow = (listener) => register("show", listener);
|
|
754
|
+
/** 页面隐藏(进入下一页、切到后台)时 */
|
|
755
|
+
const onPageHide = (listener) => register("hide", listener);
|
|
756
|
+
/** 页面滚到底时(加载更多);离底部多近算到底在页面配置的 onReachBottomDistance */
|
|
757
|
+
const onReachBottom = (listener) => register("reachBottom", listener);
|
|
758
|
+
/** 页面名 → 路径(构建时页面在 pages/<名字>/index) */
|
|
759
|
+
function pageUrl(page, query = {}) {
|
|
760
|
+
const search = Object.entries(query).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`).join("&");
|
|
761
|
+
return `/pages/${page}/index${search ? `?${search}` : ""}`;
|
|
762
|
+
}
|
|
763
|
+
const router = () => {
|
|
764
|
+
const current = platform();
|
|
765
|
+
if (!current) throw new Error("不在小程序里:页面跳转不可用(H5 用自己的路由)");
|
|
766
|
+
return current.api;
|
|
767
|
+
};
|
|
768
|
+
/** 打开另一个页面(压进页面栈,可以返回) */
|
|
769
|
+
const navigateTo = (page, query) => router().navigateTo({ url: pageUrl(page, query) });
|
|
770
|
+
/** 换成另一个页面(不能返回到当前页) */
|
|
771
|
+
const redirectTo = (page, query) => router().redirectTo({ url: pageUrl(page, query) });
|
|
772
|
+
/** 关掉所有页面,打开这一个 */
|
|
773
|
+
const reLaunch = (page, query) => router().reLaunch({ url: pageUrl(page, query) });
|
|
774
|
+
/** 返回上一页(或上几页) */
|
|
775
|
+
const navigateBack = (delta = 1) => router().navigateBack({ delta });
|
|
776
|
+
//#endregion
|
|
777
|
+
//#region src/page.ts
|
|
778
|
+
const toTouch = (touch, index) => ({
|
|
779
|
+
identifier: touch.identifier ?? index,
|
|
780
|
+
clientX: touch.clientX ?? touch.pageX ?? 0,
|
|
781
|
+
clientY: touch.clientY ?? touch.pageY ?? 0,
|
|
782
|
+
pageX: touch.pageX ?? touch.clientX ?? 0,
|
|
783
|
+
pageY: touch.pageY ?? touch.clientY ?? 0
|
|
784
|
+
});
|
|
785
|
+
/** 有子节点的模板类型 */
|
|
786
|
+
const CONTAINERS = /* @__PURE__ */ new Set([
|
|
787
|
+
"v",
|
|
788
|
+
"lock",
|
|
789
|
+
"touch",
|
|
790
|
+
"btn"
|
|
791
|
+
]);
|
|
792
|
+
/** 小程序事件 → 组件收到的事件名 */
|
|
793
|
+
const EVENT_TYPES = {
|
|
794
|
+
tap: "click",
|
|
795
|
+
touchstart: "touchstart",
|
|
796
|
+
touchend: "touchend",
|
|
797
|
+
touchcancel: "touchcancel",
|
|
798
|
+
touchmove: "touchmove",
|
|
799
|
+
longpress: "longpress",
|
|
800
|
+
longtap: "longpress",
|
|
801
|
+
input: "input",
|
|
802
|
+
confirm: "keydown",
|
|
803
|
+
blur: "blur",
|
|
804
|
+
focus: "focus",
|
|
805
|
+
load: "load",
|
|
806
|
+
error: "error",
|
|
807
|
+
scroll: "scroll",
|
|
808
|
+
getphonenumber: "opentype",
|
|
809
|
+
chooseavatar: "opentype",
|
|
810
|
+
agreeprivacyauthorization: "opentype",
|
|
811
|
+
getauthorize: "opentype"
|
|
812
|
+
};
|
|
813
|
+
/** 模板上 bind 的事件(会冒泡到每一层祖先),见 build/platforms.ts 的 bubble */
|
|
814
|
+
const BUBBLING_EVENTS = /* @__PURE__ */ new Set([
|
|
815
|
+
"touchstart",
|
|
816
|
+
"touchmove",
|
|
817
|
+
"touchend",
|
|
818
|
+
"touchcancel"
|
|
819
|
+
]);
|
|
820
|
+
/**
|
|
821
|
+
* 透传到模板上的元素属性:元素属性名 → 节点数据里的字段(模板里写 {{i.字段}},见 build/templates.ts);
|
|
822
|
+
* 其余属性只参与样式条件
|
|
823
|
+
*/
|
|
824
|
+
const TEMPLATE_ATTRS = {
|
|
825
|
+
id: "id",
|
|
826
|
+
src: "src",
|
|
827
|
+
mode: "mode",
|
|
828
|
+
"lazy-load": "lz",
|
|
829
|
+
value: "value",
|
|
830
|
+
placeholder: "placeholder",
|
|
831
|
+
type: "type",
|
|
832
|
+
maxlength: "maxlength",
|
|
833
|
+
enterkeyhint: "ct",
|
|
834
|
+
"data-autosize": "ah",
|
|
835
|
+
autofocus: "af"
|
|
836
|
+
};
|
|
837
|
+
/**
|
|
838
|
+
* 读屏的属性:元素属性名 → 节点数据里的字段(模板上的 aria-* 见 build/platforms.ts 的 a11y)。
|
|
839
|
+
* 只发微信认的几个;role 在微信里叫 aria-role
|
|
840
|
+
*/
|
|
841
|
+
const A11Y_ATTRS = {
|
|
842
|
+
role: "ar",
|
|
843
|
+
"aria-label": "al",
|
|
844
|
+
"aria-hidden": "ahi",
|
|
845
|
+
"aria-checked": "ack",
|
|
846
|
+
"aria-disabled": "adi",
|
|
847
|
+
"aria-selected": "ase",
|
|
848
|
+
"aria-expanded": "aex"
|
|
849
|
+
};
|
|
850
|
+
/** 透传到模板的属性:支付宝的模板上没有读屏属性,不发 */
|
|
851
|
+
let passed;
|
|
852
|
+
const passedAttrs = () => passed ??= Object.entries(platform()?.name === "my" ? TEMPLATE_ATTRS : {
|
|
853
|
+
...TEMPLATE_ATTRS,
|
|
854
|
+
...A11Y_ATTRS
|
|
855
|
+
});
|
|
856
|
+
/** 输入框的 type(加上 inputmode)→ 小程序 input 的 type 与是否密码 */
|
|
857
|
+
function inputType(node) {
|
|
858
|
+
const type = node.attribute("type");
|
|
859
|
+
const mode = node.attribute("inputmode");
|
|
860
|
+
if (type === "password") return {
|
|
861
|
+
type: "text",
|
|
862
|
+
pw: 1
|
|
863
|
+
};
|
|
864
|
+
if (type === "number" || mode === "decimal") return { type: "digit" };
|
|
865
|
+
if (type === "tel" || mode === "numeric" || mode === "tel") return { type: "number" };
|
|
866
|
+
if (node.attribute("autocomplete") === "nickname" && platform()?.name !== "my") return { type: "nickname" };
|
|
867
|
+
return { type: "text" };
|
|
868
|
+
}
|
|
869
|
+
/** 输入框、多行输入的事件:用户输入的值写回节点(不触发 setData,组件把同一个值写回时才发) */
|
|
870
|
+
const VALUE_EVENTS = /* @__PURE__ */ new Set([
|
|
871
|
+
"input",
|
|
872
|
+
"focus",
|
|
873
|
+
"blur",
|
|
874
|
+
"confirm"
|
|
875
|
+
]);
|
|
876
|
+
var PageHost = class {
|
|
877
|
+
nodes = /* @__PURE__ */ new Map();
|
|
878
|
+
sent = /* @__PURE__ */ new WeakMap();
|
|
879
|
+
/**
|
|
880
|
+
* 行内样式的字典:同一个样式串只发一次(页面数据的 ss),节点只带它的编号(st),模板里写 ss[i.st]。
|
|
881
|
+
* 列表里成百上千个节点的样式往往只有几种,首屏的 setData 因此小一个数量级
|
|
882
|
+
*/
|
|
883
|
+
styleIds = /* @__PURE__ */ new Map();
|
|
884
|
+
/** 这一轮新登记、还没发出去的样式 */
|
|
885
|
+
newStyles = {};
|
|
886
|
+
touched = /* @__PURE__ */ new Set();
|
|
887
|
+
restructured = /* @__PURE__ */ new Set();
|
|
888
|
+
scheduled = false;
|
|
889
|
+
/** 等这一轮 setData 生效的回调(量尺寸要等节点上了模板) */
|
|
890
|
+
waiting = [];
|
|
891
|
+
page;
|
|
892
|
+
root;
|
|
893
|
+
/** 浮层根:挂在根节点的最后,弹出层、轻提示 Teleport 到这里 */
|
|
894
|
+
overlay = new MiniNode("div");
|
|
895
|
+
/** 页面滚动位置(onPageScroll 记下),与滚动的监听 */
|
|
896
|
+
scrollTop = 0;
|
|
897
|
+
/** 上一次处理的冒泡事件:类型@时间(去掉冒泡带来的重复) */
|
|
898
|
+
lastBubbled = "";
|
|
899
|
+
/** 递归组件里按下的节点与位置(合成点击用) */
|
|
900
|
+
pressStart = null;
|
|
901
|
+
scrollListeners = /* @__PURE__ */ new Set();
|
|
902
|
+
constructor(page, root) {
|
|
903
|
+
this.page = page;
|
|
904
|
+
this.root = root;
|
|
905
|
+
root.host = this;
|
|
906
|
+
}
|
|
907
|
+
/** 等这一轮的变化发出去并生效;没有待发的变化时下一个微任务就好 */
|
|
908
|
+
flushed() {
|
|
909
|
+
return new Promise((resolve) => {
|
|
910
|
+
this.waiting.push(resolve);
|
|
911
|
+
this.schedule();
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
/** 样式串 → 字典里的编号(空样式不占编号) */
|
|
915
|
+
styleId(style) {
|
|
916
|
+
if (!style) return void 0;
|
|
917
|
+
let id = this.styleIds.get(style);
|
|
918
|
+
if (id === void 0) {
|
|
919
|
+
id = this.styleIds.size.toString(36);
|
|
920
|
+
this.styleIds.set(style, id);
|
|
921
|
+
this.newStyles[id] = style;
|
|
922
|
+
}
|
|
923
|
+
return id;
|
|
924
|
+
}
|
|
925
|
+
/** 所有节点重新展开样式(系统换了深浅色:跟随系统的主题类展开出的变量变了) */
|
|
926
|
+
restyle() {
|
|
927
|
+
for (const node of this.nodes.values()) this.touch(node);
|
|
928
|
+
}
|
|
929
|
+
touch(node) {
|
|
930
|
+
this.touched.add(svgRoot(node) ?? node);
|
|
931
|
+
this.schedule();
|
|
932
|
+
}
|
|
933
|
+
restructure(node) {
|
|
934
|
+
const svg = svgRoot(node);
|
|
935
|
+
if (svg) this.touched.add(svg);
|
|
936
|
+
else this.restructured.add(node);
|
|
937
|
+
this.schedule();
|
|
938
|
+
}
|
|
939
|
+
schedule() {
|
|
940
|
+
if (this.scheduled) return;
|
|
941
|
+
this.scheduled = true;
|
|
942
|
+
Promise.resolve().then(() => this.flush());
|
|
943
|
+
}
|
|
944
|
+
/** 把这一轮的变化合成一次 setData:子节点变了的整段重发,只是字段变了的按路径发变了的那几个 */
|
|
945
|
+
flush() {
|
|
946
|
+
this.scheduled = false;
|
|
947
|
+
const payload = {};
|
|
948
|
+
for (const node of this.restructured) {
|
|
949
|
+
const path = this.pathOf(node);
|
|
950
|
+
if (path === void 0 || this.coveredBy(node, this.restructured)) continue;
|
|
951
|
+
payload[`${path}.cn`] = node.children.map((child) => this.serialize(child));
|
|
952
|
+
}
|
|
953
|
+
for (const node of this.touched) {
|
|
954
|
+
const path = this.pathOf(node);
|
|
955
|
+
if (path === void 0 || node === this.root || this.coveredBy(node, this.restructured)) continue;
|
|
956
|
+
const previous = this.sent.get(node) ?? {};
|
|
957
|
+
const next = this.fieldsOf(node);
|
|
958
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(next)])) if (previous[key] !== next[key]) payload[`${path}.${key}`] = next[key] ?? null;
|
|
959
|
+
this.sent.set(node, next);
|
|
960
|
+
}
|
|
961
|
+
for (const [id, style] of Object.entries(this.newStyles)) payload[`ss.${id}`] = style;
|
|
962
|
+
this.newStyles = {};
|
|
963
|
+
this.restructured.clear();
|
|
964
|
+
this.touched.clear();
|
|
965
|
+
const waiting = this.waiting;
|
|
966
|
+
this.waiting = [];
|
|
967
|
+
const done = () => waiting.forEach((resolve) => resolve());
|
|
968
|
+
if (Object.keys(payload).length) this.page.setData(payload, done);
|
|
969
|
+
else done();
|
|
970
|
+
}
|
|
971
|
+
/** 祖先(不含自己)这一轮整段重发了 */
|
|
972
|
+
coveredBy(node, set) {
|
|
973
|
+
for (let parent = node.parent; parent; parent = parent.parent) if (set.has(parent)) return true;
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
/** 节点在页面数据里的路径;不在这个页面上时 undefined */
|
|
977
|
+
pathOf(node) {
|
|
978
|
+
const indexes = [];
|
|
979
|
+
let current = node;
|
|
980
|
+
while (current !== this.root) {
|
|
981
|
+
const parent = current.parent;
|
|
982
|
+
if (!parent) return void 0;
|
|
983
|
+
indexes.unshift(parent.children.indexOf(current));
|
|
984
|
+
current = parent;
|
|
985
|
+
}
|
|
986
|
+
return indexes.reduce((path, index) => `${path}.cn[${index}]`, "root");
|
|
987
|
+
}
|
|
988
|
+
serialize(node) {
|
|
989
|
+
const fields = this.fieldsOf(node);
|
|
990
|
+
this.sent.set(node, fields);
|
|
991
|
+
if (!CONTAINERS.has(fields.t)) return fields;
|
|
992
|
+
return {
|
|
993
|
+
...fields,
|
|
994
|
+
cn: node.children.map((child) => this.serialize(child))
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
/** 节点自己的字段(不含子节点) */
|
|
998
|
+
fieldsOf(node) {
|
|
999
|
+
this.nodes.set(node.sid, node);
|
|
1000
|
+
let type = templateOf(node.tag);
|
|
1001
|
+
if (type === "v" && node.tag === "button" && node.attribute("open-type") !== void 0) type = "btn";
|
|
1002
|
+
else if (type === "v" && node.attribute("data-scroll-lock") !== void 0) type = "lock";
|
|
1003
|
+
else if (type === "v" && node.attribute("data-touch-move") !== void 0) type = "touch";
|
|
1004
|
+
if (type === "x") return {
|
|
1005
|
+
s: node.sid,
|
|
1006
|
+
t: type,
|
|
1007
|
+
v: node.text
|
|
1008
|
+
};
|
|
1009
|
+
if (type === "n") return {
|
|
1010
|
+
s: node.sid,
|
|
1011
|
+
t: type
|
|
1012
|
+
};
|
|
1013
|
+
const { className, style } = resolveStyle(node);
|
|
1014
|
+
const data = {
|
|
1015
|
+
s: node.sid,
|
|
1016
|
+
t: type,
|
|
1017
|
+
st: this.styleId(type === "svg" ? `${svgMaskStyle(node)};${style}` : style)
|
|
1018
|
+
};
|
|
1019
|
+
if (type === "btn") {
|
|
1020
|
+
data.c = className ? `${className} vtp-btn` : "vtp-btn";
|
|
1021
|
+
data.ot = node.attribute("open-type");
|
|
1022
|
+
const scope = node.attribute("scope");
|
|
1023
|
+
if (scope !== void 0) data.os = scope;
|
|
1024
|
+
} else if (className) data.c = className;
|
|
1025
|
+
if (type === "touch" && node.attribute("data-touch-move") === "catch") data.cm = 1;
|
|
1026
|
+
for (const [name, field] of passedAttrs()) {
|
|
1027
|
+
const value = node.attribute(name);
|
|
1028
|
+
if (value !== void 0) data[field] = value;
|
|
1029
|
+
}
|
|
1030
|
+
if (type === "input" || type === "textarea") {
|
|
1031
|
+
if (type === "input") Object.assign(data, inputType(node));
|
|
1032
|
+
if (node.attribute("disabled") !== void 0 || node.attribute("readonly") !== void 0) data.dis = 1;
|
|
1033
|
+
}
|
|
1034
|
+
return data;
|
|
1035
|
+
}
|
|
1036
|
+
/** 小程序事件 → 组件事件:模板用 catch 绑定,只有最内层的节点收到,从它起按节点树冒泡 */
|
|
1037
|
+
dispatch(native) {
|
|
1038
|
+
const sid = native.currentTarget?.dataset?.s;
|
|
1039
|
+
if (!sid) return;
|
|
1040
|
+
const target = this.nodes.get(sid);
|
|
1041
|
+
const nativeType = native.type.toLowerCase();
|
|
1042
|
+
if (BUBBLING_EVENTS.has(nativeType) && native.timeStamp !== void 0) {
|
|
1043
|
+
const key = `${nativeType}@${native.timeStamp}`;
|
|
1044
|
+
if (key === this.lastBubbled) return;
|
|
1045
|
+
this.lastBubbled = key;
|
|
1046
|
+
}
|
|
1047
|
+
const type = EVENT_TYPES[nativeType];
|
|
1048
|
+
if (!target || !type) return;
|
|
1049
|
+
const deep = levelOf(target) >= 24;
|
|
1050
|
+
if (deep && nativeType === "tap") return;
|
|
1051
|
+
if (nativeType === "touchstart" || nativeType === "touchend" || nativeType === "touchcancel") this.press(target, nativeType === "touchstart");
|
|
1052
|
+
if (type === "click" && insideDisabledControl(target)) return;
|
|
1053
|
+
if (VALUE_EVENTS.has(nativeType)) {
|
|
1054
|
+
const value = native.detail?.value;
|
|
1055
|
+
if (typeof value === "string") target.attrs.set("value", value);
|
|
1056
|
+
if (nativeType === "blur" && target.attrs.has("autofocus")) target.removeAttribute("autofocus");
|
|
1057
|
+
}
|
|
1058
|
+
this.deliver(type, target, native);
|
|
1059
|
+
if (deep && this.isTap(target, nativeType, native) && !insideDisabledControl(target)) this.deliver("click", target, native);
|
|
1060
|
+
}
|
|
1061
|
+
/** 递归组件里的点击:按下与松开在同一个节点、移动不到 10px、不到 700ms */
|
|
1062
|
+
isTap(target, nativeType, native) {
|
|
1063
|
+
const point = native.changedTouches?.[0] ?? native.touches?.[0];
|
|
1064
|
+
if (nativeType === "touchstart" && point) {
|
|
1065
|
+
const { clientX: x, clientY: y } = toTouch(point, 0);
|
|
1066
|
+
this.pressStart = {
|
|
1067
|
+
node: target,
|
|
1068
|
+
x,
|
|
1069
|
+
y,
|
|
1070
|
+
time: Date.now()
|
|
1071
|
+
};
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
if (nativeType !== "touchend" || !this.pressStart || !point) return false;
|
|
1075
|
+
const start = this.pressStart;
|
|
1076
|
+
this.pressStart = null;
|
|
1077
|
+
const { clientX: x, clientY: y } = toTouch(point, 0);
|
|
1078
|
+
return start.node === target && Math.hypot(x - start.x, y - start.y) < 10 && Date.now() - start.time < 700;
|
|
1079
|
+
}
|
|
1080
|
+
/** 交给节点上的处理函数,从目标往上冒泡 */
|
|
1081
|
+
deliver(type, target, native) {
|
|
1082
|
+
const nativeType = native.type.toLowerCase();
|
|
1083
|
+
let stopped = false;
|
|
1084
|
+
const event = {
|
|
1085
|
+
type,
|
|
1086
|
+
target,
|
|
1087
|
+
currentTarget: target,
|
|
1088
|
+
defaultPrevented: false,
|
|
1089
|
+
detail: native.detail,
|
|
1090
|
+
touches: (native.touches ?? []).map(toTouch),
|
|
1091
|
+
changedTouches: (native.changedTouches ?? []).map(toTouch),
|
|
1092
|
+
key: nativeType === "confirm" ? "Enter" : void 0,
|
|
1093
|
+
preventDefault() {
|
|
1094
|
+
event.defaultPrevented = true;
|
|
1095
|
+
},
|
|
1096
|
+
stopPropagation() {
|
|
1097
|
+
stopped = true;
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
for (let node = target; node && !stopped; node = node.parent) {
|
|
1101
|
+
event.currentTarget = node;
|
|
1102
|
+
for (const listener of [...node.handlers.get(type) ?? [], ...node.listeners.get(type) ?? []]) listener(event);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/** :active:按下时目标与祖先都算按着(同浏览器) */
|
|
1106
|
+
press(target, pressed) {
|
|
1107
|
+
for (let node = target; node; node = node.parent) {
|
|
1108
|
+
if (node.pressed === pressed) continue;
|
|
1109
|
+
node.pressed = pressed;
|
|
1110
|
+
node.changed();
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
/** 节点在模板里的层级:根节点的子节点是 0;不小于模板层数的画在递归组件里 */
|
|
1115
|
+
function levelOf(node) {
|
|
1116
|
+
let level = -1;
|
|
1117
|
+
for (let current = node.parent; current; current = current.parent) level++;
|
|
1118
|
+
return level;
|
|
1119
|
+
}
|
|
1120
|
+
/** svg 子树里的节点 → 那个 svg;不在 svg 里时 undefined */
|
|
1121
|
+
function svgRoot(node) {
|
|
1122
|
+
for (let current = node; current; current = current.parent) if (current.tag === "svg") return current;
|
|
1123
|
+
}
|
|
1124
|
+
/** 浏览器不给禁用的表单控件(及其里面的元素)派发 click */
|
|
1125
|
+
function insideDisabledControl(node) {
|
|
1126
|
+
for (let current = node; current; current = current.parent) if (FORM_CONTROLS.has(current.tag) && current.attribute("disabled") !== void 0) return true;
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
/** 页面实例 → 它的 Vue 应用与宿主;同一个页面可能在页面栈里开多次,不能放在闭包里 */
|
|
1130
|
+
const live = /* @__PURE__ */ new Set();
|
|
1131
|
+
let watchingTheme = false;
|
|
1132
|
+
/** 读系统的深浅色,并在系统切换时让所有页面重新展开样式(只订阅一次) */
|
|
1133
|
+
function followColorScheme(api) {
|
|
1134
|
+
const info = api.getAppBaseInfo?.() ?? api.getSystemInfoSync();
|
|
1135
|
+
setColorScheme(info.theme === "dark" ? "dark" : "light");
|
|
1136
|
+
if (watchingTheme || !api.onThemeChange) return;
|
|
1137
|
+
watchingTheme = true;
|
|
1138
|
+
api.onThemeChange(({ theme }) => {
|
|
1139
|
+
setColorScheme(theme === "dark" ? "dark" : "light");
|
|
1140
|
+
for (const host of live) host.restyle();
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
const mounted = /* @__PURE__ */ new WeakMap();
|
|
1144
|
+
function dispatchToCurrentPage(event) {
|
|
1145
|
+
const pages = typeof getCurrentPages === "function" ? getCurrentPages() : [];
|
|
1146
|
+
const page = pages[pages.length - 1];
|
|
1147
|
+
if (page) mounted.get(page)?.host.dispatch(event);
|
|
1148
|
+
}
|
|
1149
|
+
/** 页面参数:跳转时编码过(lifecycle.ts 的 pageUrl),这里解开;解不开的原样给 */
|
|
1150
|
+
function decodeQuery(query = {}) {
|
|
1151
|
+
const result = {};
|
|
1152
|
+
for (const [key, value] of Object.entries(query)) try {
|
|
1153
|
+
result[key] = decodeURIComponent(value);
|
|
1154
|
+
} catch {
|
|
1155
|
+
result[key] = value;
|
|
1156
|
+
}
|
|
1157
|
+
return result;
|
|
1158
|
+
}
|
|
1159
|
+
/** 页面的选项:交给小程序的 Page() */
|
|
1160
|
+
function pageOptions(component, setup) {
|
|
1161
|
+
return {
|
|
1162
|
+
data: {
|
|
1163
|
+
root: { cn: [] },
|
|
1164
|
+
ss: {}
|
|
1165
|
+
},
|
|
1166
|
+
onLoad(query) {
|
|
1167
|
+
const current = platform();
|
|
1168
|
+
if (current) {
|
|
1169
|
+
setWindowWidth(windowSize(current.api).width);
|
|
1170
|
+
followColorScheme(current.api);
|
|
1171
|
+
}
|
|
1172
|
+
const host = new PageHost(this, new MiniNode("#root"));
|
|
1173
|
+
const app = createMiniApp(component, { query: decodeQuery(query) });
|
|
1174
|
+
const hooks = createPageHooks();
|
|
1175
|
+
app.provide(HOST_KEY, createMiniHost(host));
|
|
1176
|
+
app.provide(PAGE_HOOKS_KEY, hooks);
|
|
1177
|
+
setup?.(app);
|
|
1178
|
+
app.mount(host.root);
|
|
1179
|
+
host.root.insertBefore(host.overlay, null);
|
|
1180
|
+
mounted.set(this, {
|
|
1181
|
+
app,
|
|
1182
|
+
host,
|
|
1183
|
+
hooks
|
|
1184
|
+
});
|
|
1185
|
+
live.add(host);
|
|
1186
|
+
},
|
|
1187
|
+
onUnload() {
|
|
1188
|
+
const page = mounted.get(this);
|
|
1189
|
+
if (page) live.delete(page.host);
|
|
1190
|
+
page?.app.unmount();
|
|
1191
|
+
mounted.delete(this);
|
|
1192
|
+
},
|
|
1193
|
+
onShow() {
|
|
1194
|
+
const page = mounted.get(this);
|
|
1195
|
+
if (page) emitPageEvent(page.hooks, "show");
|
|
1196
|
+
},
|
|
1197
|
+
onHide() {
|
|
1198
|
+
const page = mounted.get(this);
|
|
1199
|
+
if (page) emitPageEvent(page.hooks, "hide");
|
|
1200
|
+
},
|
|
1201
|
+
onReachBottom() {
|
|
1202
|
+
const page = mounted.get(this);
|
|
1203
|
+
if (page) emitPageEvent(page.hooks, "reachBottom");
|
|
1204
|
+
},
|
|
1205
|
+
/** 模板上所有事件的入口(build/templates.ts) */
|
|
1206
|
+
eh(event) {
|
|
1207
|
+
mounted.get(this)?.host.dispatch(event);
|
|
1208
|
+
},
|
|
1209
|
+
/** 页面滚动:记下位置,通知组件(吸顶、回到顶部、下拉刷新) */
|
|
1210
|
+
onPageScroll({ scrollTop }) {
|
|
1211
|
+
const host = mounted.get(this)?.host;
|
|
1212
|
+
if (!host) return;
|
|
1213
|
+
host.scrollTop = scrollTop;
|
|
1214
|
+
for (const listener of host.scrollListeners) listener(scrollTop);
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
//#endregion
|
|
1219
|
+
export { onPageHide as a, pageUrl as c, createMiniApp as d, MiniNode as f, navigateTo as i, reLaunch as l, pageOptions as n, onPageShow as o, navigateBack as r, onReachBottom as s, dispatchToCurrentPage as t, redirectTo as u };
|