@done-coding/forge-plugin-guide 0.0.1-alpha.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 +3 -0
- package/es/index.mjs +436 -0
- package/package.json +76 -0
- package/types/anchor.d.ts +12 -0
- package/types/index.d.ts +3 -0
- package/types/plugin.d.ts +19 -0
- package/types/progress.d.ts +24 -0
- package/types/runner.d.ts +15 -0
- package/types/steps.d.ts +48 -0
- package/types/theme.d.ts +22 -0
- package/types/types.d.ts +192 -0
package/README.md
ADDED
package/es/index.mjs
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { shallowRef as S, readonly as B, computed as A } from "vue";
|
|
2
|
+
import { BADGE_KEY_ATTR as C, BADGE_HOST_ATTR as P, BADGE_MASK_KINDS as O } from "@done-coding/admin-core";
|
|
3
|
+
import { driver as M } from "driver.js";
|
|
4
|
+
import "driver.js/dist/driver.css";
|
|
5
|
+
const N = (e) => {
|
|
6
|
+
if (!e) return [];
|
|
7
|
+
try {
|
|
8
|
+
const r = JSON.parse(e);
|
|
9
|
+
return Array.isArray(r) ? r.filter((o) => typeof o == "string") : [];
|
|
10
|
+
} catch {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
}, L = (e, r) => {
|
|
14
|
+
const o = S(
|
|
15
|
+
N(e.getItem(r))
|
|
16
|
+
), s = (n) => {
|
|
17
|
+
o.value = n, e.setItem(r, JSON.stringify(n));
|
|
18
|
+
}, c = (n) => o.value.includes(n), t = (n) => {
|
|
19
|
+
c(n) || s([...o.value, n]);
|
|
20
|
+
}, i = () => {
|
|
21
|
+
s([]);
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
seen: B(o),
|
|
25
|
+
isSeen: c,
|
|
26
|
+
markSeen: t,
|
|
27
|
+
reset: i
|
|
28
|
+
};
|
|
29
|
+
}, U = (e) => e.replace(/(["\\])/g, "\\$1"), F = (e) => `[${C}="${U(e)}"]`, j = (e) => {
|
|
30
|
+
if (typeof document > "u") return;
|
|
31
|
+
const r = document.querySelector(
|
|
32
|
+
F(e)
|
|
33
|
+
);
|
|
34
|
+
if (!r) return;
|
|
35
|
+
const o = r.closest(`[${P}]`) ?? r;
|
|
36
|
+
return Y(o) ? o : void 0;
|
|
37
|
+
}, Y = (e) => e.getClientRects().length > 0, z = "[dc-plugin-guide]", H = O, V = (e, r) => r.startsWith("/") ? r : e ? `${e.replace(/\/$/, "")}/${r}` : `/${r}`, W = (e) => {
|
|
38
|
+
const r = /* @__PURE__ */ new Map();
|
|
39
|
+
let o = 0;
|
|
40
|
+
const s = (c, t) => {
|
|
41
|
+
c.forEach((i) => {
|
|
42
|
+
var v;
|
|
43
|
+
const n = V(t, i.path), l = o++;
|
|
44
|
+
r.has(n) || r.set(n, l), typeof i.name == "string" && !r.has(i.name) && r.set(i.name, l), (v = i.children) != null && v.length && s(i.children, n);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
return s(e ?? [], ""), r;
|
|
48
|
+
}, J = (e, r) => !!e.expireAt && e.expireAt <= r, y = (e, r, o) => {
|
|
49
|
+
if (e) {
|
|
50
|
+
if (e.has(r)) return;
|
|
51
|
+
e.add(r);
|
|
52
|
+
}
|
|
53
|
+
console.warn(`${z} ${o}`);
|
|
54
|
+
}, R = (e, r, o) => {
|
|
55
|
+
const s = e.key ?? e.badgeKey;
|
|
56
|
+
if (!s) {
|
|
57
|
+
y(
|
|
58
|
+
r.warned,
|
|
59
|
+
"__nokey__",
|
|
60
|
+
"步骤既无 key 也无 badgeKey,无从记进度,已丢弃"
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const c = e.badgeKey ? r.marks[e.badgeKey] : void 0;
|
|
65
|
+
if (e.badgeKey && (!c || J(c, o))) return;
|
|
66
|
+
if (c && H.includes(c.kind)) {
|
|
67
|
+
y(
|
|
68
|
+
r.warned,
|
|
69
|
+
`mask:${s}`,
|
|
70
|
+
`「${s}」的标是 ${c.kind},它会遮挡宿主交互(用户点不动),与引导正面冲突,已跳过。[MUST NOT] 把会遮挡的档(${O.join(
|
|
71
|
+
" / "
|
|
72
|
+
)})配进 badgeKinds。`
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const t = e.description ?? (c == null ? void 0 : c.description);
|
|
77
|
+
if (!t) {
|
|
78
|
+
y(
|
|
79
|
+
r.warned,
|
|
80
|
+
`nodesc:${s}`,
|
|
81
|
+
`「${s}」没有讲解正文(step.description 与 BadgeEntry.description 都为空),已跳过。[MUST NOT] 拿菜单 title 硬凑——没内容的引导步骤是噪音。`
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (e.route && !r.hasRouter) {
|
|
86
|
+
y(
|
|
87
|
+
r.warned,
|
|
88
|
+
`norouter:${s}`,
|
|
89
|
+
`「${s}」配了 route 但 createGuidePlugin 没拿到 router,无法导航,已跳过。`
|
|
90
|
+
);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const i = e.badgeKey, n = c == null ? void 0 : c.version, l = n === void 0 || n === "" ? s : `${s}@${n}`;
|
|
94
|
+
return {
|
|
95
|
+
...e,
|
|
96
|
+
key: l,
|
|
97
|
+
sourceKey: s,
|
|
98
|
+
description: t,
|
|
99
|
+
/**
|
|
100
|
+
* 锚点:显式 `element` 优先,否则由 `badgeKey` 解析。
|
|
101
|
+
*
|
|
102
|
+
* 取**函数**而非选择器字符串:解析链最后一跳要 `closest` 到**宿主**
|
|
103
|
+
* (菜单项 / 按钮),而属性选择器只能选中角标那个十几 px 的 pill ——
|
|
104
|
+
* 高亮一个小圆点说「看这里」不是想要的。driver.js 的 `element` 支持
|
|
105
|
+
* `() => Element`,且 `waitForElement` 会反复重算它 ⇒ 跨路由 / 懒挂载也吃得下。
|
|
106
|
+
*/
|
|
107
|
+
element: e.element ?? (i ? () => j(i) : void 0)
|
|
108
|
+
};
|
|
109
|
+
}, T = (e) => {
|
|
110
|
+
var c;
|
|
111
|
+
const r = e.now ?? Date.now();
|
|
112
|
+
if ((c = e.steps) != null && c.length)
|
|
113
|
+
return e.steps.map((t) => R(t, e, r)).filter((t) => !!t);
|
|
114
|
+
const o = W(e.routes);
|
|
115
|
+
return Object.keys(e.marks).filter((t) => e.badgeKinds.includes(e.marks[t].kind)).sort((t, i) => {
|
|
116
|
+
const n = o.get(t), l = o.get(i);
|
|
117
|
+
return n !== void 0 && l !== void 0 ? n - l : n !== void 0 ? -1 : l !== void 0 ? 1 : t < i ? -1 : t > i ? 1 : 0;
|
|
118
|
+
}).map((t) => R({ badgeKey: t }, e, r)).filter((t) => !!t);
|
|
119
|
+
}, q = `dc-guide-theme-${Math.random().toString(36).slice(2, 8)}`, X = "data-dc-guide-theme", h = "var(--dc-core-surface, var(--el-bg-color-overlay))", m = "var(--dc-core-primary-color, var(--el-color-primary))", Z = `
|
|
120
|
+
.driver-popover {
|
|
121
|
+
color: var(--dc-core-base-color, var(--el-text-color-primary));
|
|
122
|
+
background-color: ${h};
|
|
123
|
+
border: 1px solid var(--dc-core-border-color, var(--el-border-color));
|
|
124
|
+
border-radius: var(--dc-core-radius, var(--el-border-radius-base));
|
|
125
|
+
box-shadow: var(--dc-core-shadow, var(--el-box-shadow));
|
|
126
|
+
}
|
|
127
|
+
.driver-popover-title { color: var(--dc-core-base-color, var(--el-text-color-primary)); }
|
|
128
|
+
.driver-popover-description { color: var(--el-text-color-regular); }
|
|
129
|
+
.driver-popover-progress-text { color: var(--el-text-color-secondary); }
|
|
130
|
+
.driver-popover-close-btn { color: var(--el-text-color-placeholder); }
|
|
131
|
+
.driver-popover-close-btn:hover,
|
|
132
|
+
.driver-popover-close-btn:focus { color: var(--el-text-color-primary); }
|
|
133
|
+
|
|
134
|
+
/*
|
|
135
|
+
* 两个按钮对齐 element-plus 的 Button(甲方 2026-09-16)——driver.js 原样式是
|
|
136
|
+
* all:unset + padding:3px 7px + font-size:12px + border-radius:3px,
|
|
137
|
+
* 一副"原生按钮"的相,混在本仓界面里很突兀。
|
|
138
|
+
*
|
|
139
|
+
* 注意:EP 的 --el-button-* 变量定义在 .el-button 选择器内部(局部作用域),这里够不着,
|
|
140
|
+
* 必须引它们的源变量(--el-fill-color-blank / --el-border-color / --el-text-color-regular …),
|
|
141
|
+
* 写 var(--el-button-bg-color) 会取不到值。
|
|
142
|
+
* 尺寸取 EP default 档实测值:32px 高 / 8px 15px 内距 / 14px 字号。
|
|
143
|
+
*
|
|
144
|
+
* ⚠️ 本块在**模板字符串内**,注释里不能出现反引号——会当场终止字符串(已踩)。
|
|
145
|
+
*/
|
|
146
|
+
.driver-popover-footer-btn {
|
|
147
|
+
display: inline-flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
justify-content: center;
|
|
150
|
+
height: 32px;
|
|
151
|
+
padding: 8px 15px;
|
|
152
|
+
font-size: var(--el-font-size-base, 14px);
|
|
153
|
+
font-weight: var(--el-font-weight-primary, 500);
|
|
154
|
+
line-height: 1;
|
|
155
|
+
color: var(--el-text-color-regular);
|
|
156
|
+
background-color: var(--el-fill-color-blank);
|
|
157
|
+
border: 1px solid var(--dc-core-border-color, var(--el-border-color));
|
|
158
|
+
border-radius: var(--dc-core-radius, var(--el-border-radius-base));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* hover 对齐 EP default 档:用户主动悬停才染主色 */
|
|
162
|
+
.driver-popover-footer-btn:hover {
|
|
163
|
+
color: ${m};
|
|
164
|
+
background-color: var(--el-color-primary-light-9);
|
|
165
|
+
border-color: var(--el-color-primary-light-7);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/*
|
|
169
|
+
* focus 保持中性(不跟着 EP 把 :focus 与 :hover 同规则处理):
|
|
170
|
+
* driver.js 会自动 focus 弹窗里的第一个按钮 —— 那不是用户意图,
|
|
171
|
+
* 「跳过」会因此常驻高亮 = 在引导用户跳过(甲方两度指出)。
|
|
172
|
+
* 用 :not(:hover) 让真悬停仍走上面那条。
|
|
173
|
+
*/
|
|
174
|
+
.driver-popover-footer-btn:focus:not(:hover) {
|
|
175
|
+
color: var(--el-text-color-regular);
|
|
176
|
+
background-color: var(--el-fill-color-blank);
|
|
177
|
+
border-color: var(--dc-core-border-color, var(--el-border-color));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* 主按钮(下一步 / 知道了)——对齐 EP primary 档实心,随主题切换 */
|
|
181
|
+
.driver-popover-next-btn,
|
|
182
|
+
.driver-popover-next-btn:focus:not(:hover) {
|
|
183
|
+
color: var(--el-color-white, #fff);
|
|
184
|
+
background-color: ${m};
|
|
185
|
+
border-color: ${m};
|
|
186
|
+
}
|
|
187
|
+
.driver-popover-next-btn:hover {
|
|
188
|
+
color: var(--el-color-white, #fff);
|
|
189
|
+
background-color: var(--el-color-primary-light-3);
|
|
190
|
+
border-color: var(--el-color-primary-light-3);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/*
|
|
194
|
+
* 镂空区描边(甲方 2026-09-16)。
|
|
195
|
+
*
|
|
196
|
+
* 为什么必须加:driver.js 的遮罩是 #000 + opacity .7。亮色下「遮住 = 白 + 黑半透明 = 灰」,
|
|
197
|
+
* 与镂空处形成强对比;而暗色下页面本就是 #141414 / #0a0a0a,
|
|
198
|
+
* 「深色 + 黑半透明 ≈ 还是深色」,镂空与不镂空几乎看不出差别。
|
|
199
|
+
*
|
|
200
|
+
* 🔴 描在**遮罩的 path 上**,[MUST NOT] 改用目标元素的 outline / box-shadow ——
|
|
201
|
+
* 那两者都画在元素**外部空间**,会被祖先的 overflow 裁掉:
|
|
202
|
+
* 实测菜单项左缘与 .app-sidebar-shim(overflow-x: hidden)的余量是 0px,
|
|
203
|
+
* outline-offset:3 + width:2 整个被吃掉 ⇒ 只剩上下两条边(甲方截图);
|
|
204
|
+
* 弹窗里的表单四周都在滚动容器内,则四条边全没。
|
|
205
|
+
* 遮罩 SVG 是 fixed 全屏、不受任何祖先裁剪,且它的 path 已经算好了
|
|
206
|
+
* stagePadding / stageRadius,描边天然与洞严丝合缝。
|
|
207
|
+
*
|
|
208
|
+
* 外框那一圈:path 的 d 是「视口矩形 + 洞」两段
|
|
209
|
+
* (M1280,0L0,0L0,800L1280,800L1280,0Z 之后才是 M3,432 h309 a5,5…),
|
|
210
|
+
* stroke 会一并描到视口矩形 —— 但它正好落在视口四边上,stroke 居中对齐后
|
|
211
|
+
* 有一半在视口外、另一半被窗口裁掉,实测不可见,故无需为它做任何规避。
|
|
212
|
+
*/
|
|
213
|
+
svg.driver-overlay path {
|
|
214
|
+
stroke: ${m};
|
|
215
|
+
stroke-width: 2;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* 箭头:用 border 画的三角,四个方向各自只有一条边可见,逐条跟面色 */
|
|
219
|
+
.driver-popover-arrow-side-left { border-left-color: ${h}; }
|
|
220
|
+
.driver-popover-arrow-side-right { border-right-color: ${h}; }
|
|
221
|
+
.driver-popover-arrow-side-top { border-top-color: ${h}; }
|
|
222
|
+
.driver-popover-arrow-side-bottom { border-bottom-color: ${h}; }
|
|
223
|
+
`;
|
|
224
|
+
let K = !1;
|
|
225
|
+
const Q = () => {
|
|
226
|
+
if (K || typeof document > "u") return;
|
|
227
|
+
K = !0;
|
|
228
|
+
const e = document.createElement("style");
|
|
229
|
+
e.id = q, e.setAttribute(X, ""), e.textContent = Z, document.head.appendChild(e);
|
|
230
|
+
}, ee = {
|
|
231
|
+
showProgress: !0,
|
|
232
|
+
progressText: "{{current}} / {{total}}",
|
|
233
|
+
/**
|
|
234
|
+
* 两个按钮:「跳过」+「下一步 / 知道了」(甲方 2026-09-16)。
|
|
235
|
+
*
|
|
236
|
+
* 🔴 driver.js 的 `"close"` 按钮是**右上角那个 ×**(无文案配置项),做不出带文字的
|
|
237
|
+
* 「跳过」⇒ 复用 `previous` 位并把它的行为改写成销毁(见下方 `onPrevClick`)。
|
|
238
|
+
* 同时不放 `"close"`:既然有了显式的「跳过」,右上角 × 就是语义重复的第二出口。
|
|
239
|
+
*/
|
|
240
|
+
showButtons: ["previous", "next"],
|
|
241
|
+
nextBtnText: "下一步",
|
|
242
|
+
prevBtnText: "跳过",
|
|
243
|
+
doneBtnText: "知道了",
|
|
244
|
+
/**
|
|
245
|
+
* 🔴 **点蒙层 / 按 ESC 都不退出**(甲方 2026-09-16):引导是一条有始有终的链路,
|
|
246
|
+
* 误触蒙层就整条消失、且进度已按「露过面」记下 ⇒ 用户再也等不到它。
|
|
247
|
+
* 要退出 [MUST] 显式点「跳过」。
|
|
248
|
+
*/
|
|
249
|
+
allowClose: !1,
|
|
250
|
+
/**
|
|
251
|
+
* 等锚点挂载(ms)。跨路由 / 懒加载页的锚点不会立刻在 DOM 里,driver.js 自带
|
|
252
|
+
* MutationObserver 等它出现 ⇒ [MUST NOT] 自己写轮询。
|
|
253
|
+
*/
|
|
254
|
+
waitForElement: 3e3,
|
|
255
|
+
/** 等超时仍没有 → 跳过该步继续,[MUST NOT] 让它退化成「屏幕中央的空高亮」 */
|
|
256
|
+
skipMissingElement: !0
|
|
257
|
+
}, re = (e) => {
|
|
258
|
+
var r, o;
|
|
259
|
+
return {
|
|
260
|
+
element: e.element,
|
|
261
|
+
advanceOnClick: e.advanceOnClick,
|
|
262
|
+
disableActiveInteraction: e.disableActiveInteraction,
|
|
263
|
+
waitForElement: e.waitForElement,
|
|
264
|
+
popover: {
|
|
265
|
+
title: e.title,
|
|
266
|
+
description: e.description,
|
|
267
|
+
side: (r = e.popover) == null ? void 0 : r.side,
|
|
268
|
+
align: (o = e.popover) == null ? void 0 : o.align,
|
|
269
|
+
/**
|
|
270
|
+
* 🔴 **[MUST] 显式清空**:driver.js 在**首步硬编码禁用 `previous`**
|
|
271
|
+
* (源码 `disableButtons:[...isFirst?[]:["previous"]]`),而本包把那一位复用成了
|
|
272
|
+
* 「跳过」⇒ 不覆盖的话,第一步的「跳过」是灰的、点不动 —— 偏偏第一步最需要它。
|
|
273
|
+
*
|
|
274
|
+
* 覆盖点 [MUST] 在 **step 级**:driver.js 取值是
|
|
275
|
+
* `step.disableButtons || globalConfig.disableButtons || []`,
|
|
276
|
+
* step 级的首步默认值 `["previous"]` 非空、会把全局配置整个吃掉。
|
|
277
|
+
* 传空数组即可(`[]` 是 truthy,取得到)。
|
|
278
|
+
*/
|
|
279
|
+
disableButtons: [],
|
|
280
|
+
/**
|
|
281
|
+
* 「点目标才前进」的步**隐去「下一步」**:推进方式已经换成点高亮元素,
|
|
282
|
+
* 再留一个能绕过去的按钮,这一步的引导就白做了。「跳过」保留 —— 用户随时可退。
|
|
283
|
+
* (driver.js 的 `previous` 位在本包是「跳过」,见 DEFAULT_DRIVER_CONFIG。)
|
|
284
|
+
*/
|
|
285
|
+
showButtons: e.advanceOnClick ? ["previous"] : void 0
|
|
286
|
+
},
|
|
287
|
+
data: { guideKey: e.key, guideRoute: e.route }
|
|
288
|
+
};
|
|
289
|
+
}, oe = (e) => {
|
|
290
|
+
var r;
|
|
291
|
+
return (r = e == null ? void 0 : e.data) == null ? void 0 : r.guideRoute;
|
|
292
|
+
}, te = (e) => {
|
|
293
|
+
var r;
|
|
294
|
+
return (r = e == null ? void 0 : e.data) == null ? void 0 : r.guideKey;
|
|
295
|
+
}, ne = (e, r) => {
|
|
296
|
+
if (!e || typeof MutationObserver > "u") return () => {
|
|
297
|
+
};
|
|
298
|
+
const o = () => e.getClientRects().length === 0;
|
|
299
|
+
if (o()) return () => {
|
|
300
|
+
};
|
|
301
|
+
const s = new MutationObserver(() => {
|
|
302
|
+
o() && (s.disconnect(), r());
|
|
303
|
+
});
|
|
304
|
+
return s.observe(document.body, {
|
|
305
|
+
childList: !0,
|
|
306
|
+
subtree: !0,
|
|
307
|
+
attributes: !0,
|
|
308
|
+
attributeFilter: ["style", "class", "hidden"]
|
|
309
|
+
}), () => s.disconnect();
|
|
310
|
+
}, ie = (e) => {
|
|
311
|
+
const { steps: r, router: o, driverConfig: s, onStepSeen: c } = e;
|
|
312
|
+
Q();
|
|
313
|
+
const t = r.map(re), i = (v, g) => {
|
|
314
|
+
const u = oe(v);
|
|
315
|
+
if (!u || !o || o.currentRoute.value.path === u) {
|
|
316
|
+
g();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
o.push(u).catch(() => {
|
|
320
|
+
}).then(g, g);
|
|
321
|
+
};
|
|
322
|
+
let n = () => {
|
|
323
|
+
};
|
|
324
|
+
const l = M({
|
|
325
|
+
...ee,
|
|
326
|
+
...s,
|
|
327
|
+
steps: t,
|
|
328
|
+
onNextClick: (v, g, u) => {
|
|
329
|
+
const x = u.index ?? 0;
|
|
330
|
+
i(t[x + 1], () => u.driver.moveNext());
|
|
331
|
+
},
|
|
332
|
+
// 「上一步」位已复用为「跳过」⇒ 点它即销毁整条导览,[MUST NOT] 再 movePrevious
|
|
333
|
+
onPrevClick: (v, g, u) => {
|
|
334
|
+
u.driver.destroy();
|
|
335
|
+
},
|
|
336
|
+
onHighlighted: (v, g) => {
|
|
337
|
+
const u = te(g);
|
|
338
|
+
u && c(u), n(), n = ne(v, () => {
|
|
339
|
+
l.isActive() && (l.hasPreviousStep() ? l.movePrevious() : l.destroy());
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
return i(t[0], () => l.drive()), {
|
|
344
|
+
destroy: () => {
|
|
345
|
+
n(), l.isActive() && l.destroy();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}, I = "dc-plugin-guide", se = ["new", "updated"], ce = "guide/seen", de = (e) => {
|
|
349
|
+
const r = e == null ? void 0 : e.bridge;
|
|
350
|
+
if (!(r != null && r.badge) || typeof r.getStorage != "function")
|
|
351
|
+
throw new Error(
|
|
352
|
+
`${I}: 安装面是 core bridge 的插件面,请用 coreBridge.use(guide),[MUST NOT] 用 app.use(guide)(两者 install 签名不同)`
|
|
353
|
+
);
|
|
354
|
+
return r;
|
|
355
|
+
}, ge = (e = {}) => {
|
|
356
|
+
const r = e.badgeKinds ?? se, o = e.storageKey ?? ce, s = e.name ?? I, c = /* @__PURE__ */ new Set(), t = S(), i = S();
|
|
357
|
+
let n;
|
|
358
|
+
const l = A(
|
|
359
|
+
() => {
|
|
360
|
+
var d, a;
|
|
361
|
+
return T({
|
|
362
|
+
marks: ((d = t.value) == null ? void 0 : d.badge.marks.value) ?? {},
|
|
363
|
+
badgeKinds: r,
|
|
364
|
+
steps: e.steps,
|
|
365
|
+
routes: (a = t.value) == null ? void 0 : a.getRoutes(),
|
|
366
|
+
hasRouter: !!e.router,
|
|
367
|
+
warned: c
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
), v = A(() => {
|
|
371
|
+
const d = i.value;
|
|
372
|
+
if (!d) return l.value;
|
|
373
|
+
const a = d.seen.value;
|
|
374
|
+
return l.value.filter((f) => !a.includes(f.key));
|
|
375
|
+
}), g = (d) => {
|
|
376
|
+
var w, E, _, $;
|
|
377
|
+
const a = d.keys && Array.from(new Set(d.keys));
|
|
378
|
+
if (!a)
|
|
379
|
+
return d.force ? l.value : v.value;
|
|
380
|
+
const f = (b) => a.map((k) => b.find((G) => G.sourceKey === k)).filter((k) => !!k);
|
|
381
|
+
if ((w = e.steps) != null && w.length)
|
|
382
|
+
return f(d.force ? l.value : v.value);
|
|
383
|
+
const p = T({
|
|
384
|
+
marks: ((E = t.value) == null ? void 0 : E.badge.marks.value) ?? {},
|
|
385
|
+
badgeKinds: r,
|
|
386
|
+
steps: a.map((b) => ({ badgeKey: b })),
|
|
387
|
+
routes: (_ = t.value) == null ? void 0 : _.getRoutes(),
|
|
388
|
+
hasRouter: !!e.router,
|
|
389
|
+
warned: c
|
|
390
|
+
});
|
|
391
|
+
if (d.force) return p;
|
|
392
|
+
const D = (($ = i.value) == null ? void 0 : $.seen.value) ?? [];
|
|
393
|
+
return p.filter((b) => !D.includes(b.key));
|
|
394
|
+
}, u = () => {
|
|
395
|
+
n == null || n.destroy(), n = void 0;
|
|
396
|
+
};
|
|
397
|
+
return {
|
|
398
|
+
name: s,
|
|
399
|
+
install: (d) => {
|
|
400
|
+
const a = de(d);
|
|
401
|
+
t.value = a, i.value = L(a.getStorage(), o);
|
|
402
|
+
},
|
|
403
|
+
pendingSteps: v,
|
|
404
|
+
start: (d = {}) => {
|
|
405
|
+
const a = g(d);
|
|
406
|
+
return a.length ? (u(), n = ie({
|
|
407
|
+
steps: a,
|
|
408
|
+
router: e.router,
|
|
409
|
+
driverConfig: e.driverConfig,
|
|
410
|
+
onStepSeen: (f) => {
|
|
411
|
+
var p;
|
|
412
|
+
return (p = i.value) == null ? void 0 : p.markSeen(f);
|
|
413
|
+
}
|
|
414
|
+
}), !0) : !1;
|
|
415
|
+
},
|
|
416
|
+
stop: u,
|
|
417
|
+
isSeen: (d) => {
|
|
418
|
+
var a;
|
|
419
|
+
return ((a = i.value) == null ? void 0 : a.isSeen(d)) ?? !1;
|
|
420
|
+
},
|
|
421
|
+
markSeen: (d) => {
|
|
422
|
+
var a;
|
|
423
|
+
return (a = i.value) == null ? void 0 : a.markSeen(d);
|
|
424
|
+
},
|
|
425
|
+
reset: () => {
|
|
426
|
+
var d;
|
|
427
|
+
return (d = i.value) == null ? void 0 : d.reset();
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
};
|
|
431
|
+
export {
|
|
432
|
+
I as GUIDE_PLUGIN_NAME,
|
|
433
|
+
ge as createGuidePlugin,
|
|
434
|
+
F as guideBadgeAnchorSelector,
|
|
435
|
+
j as guideResolveBadgeAnchor
|
|
436
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@done-coding/forge-plugin-guide",
|
|
3
|
+
"version": "0.0.1-alpha.0",
|
|
4
|
+
"description": "内部前端库",
|
|
5
|
+
"private": false,
|
|
6
|
+
"main": "es/index.mjs",
|
|
7
|
+
"module": "es/index.mjs",
|
|
8
|
+
"types": "types/index.d.ts",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./es/index.mjs",
|
|
13
|
+
"types": "./types/index.d.ts"
|
|
14
|
+
},
|
|
15
|
+
"./style.css": "./es/style.css"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"es",
|
|
19
|
+
"types"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"serve": "vite",
|
|
23
|
+
"predev": "dc-inject",
|
|
24
|
+
"dev": "vite --config vite.demo.config.ts",
|
|
25
|
+
"prehotBuild": "dc-inject",
|
|
26
|
+
"hotBuild": "vite build -w",
|
|
27
|
+
"prebuild": "rimraf types && dc-inject",
|
|
28
|
+
"build": "vue-tsc && vite build",
|
|
29
|
+
"prepack": "pnpm build",
|
|
30
|
+
"test": "vitest --config vite.demo.config.ts",
|
|
31
|
+
"coverage": "vitest run --coverage --config vite.demo.config.ts"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/done-coding/done-coding-admin.git",
|
|
36
|
+
"directory": "packages/plugin-guide"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"registry": "https://registry.npmjs.org/"
|
|
41
|
+
},
|
|
42
|
+
"author": "JustSoSu",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"sideEffects": [
|
|
45
|
+
"**/*.css",
|
|
46
|
+
"**/*.less"
|
|
47
|
+
],
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@done-coding/admin-core": "^0.28.1-alpha.0",
|
|
50
|
+
"@done-coding/cli-inject": "0.6.3",
|
|
51
|
+
"@types/node": "^18.0.0",
|
|
52
|
+
"@vitejs/plugin-vue": "^5.0.4",
|
|
53
|
+
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
|
54
|
+
"@vitest/coverage-istanbul": "^1.6.1",
|
|
55
|
+
"happy-dom": "^14.12.3",
|
|
56
|
+
"rimraf": "^5.0.1",
|
|
57
|
+
"typescript": "^5.8.3",
|
|
58
|
+
"vite": "^5.2.0",
|
|
59
|
+
"vite-plugin-dts": "^4.5.4",
|
|
60
|
+
"vitest": "^1.6.1",
|
|
61
|
+
"vue-router": "^4.3.0",
|
|
62
|
+
"vue-tsc": "^2.2.12"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@done-coding/admin-core": "workspace:^",
|
|
66
|
+
"vue": "^3.4.0"
|
|
67
|
+
},
|
|
68
|
+
"engines": {
|
|
69
|
+
"node": ">=18.0.0",
|
|
70
|
+
"pnpm": ">=9.0.0"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"driver.js": "^1.8.0"
|
|
74
|
+
},
|
|
75
|
+
"gitHead": "89cee2eb458d349b9a6f2f1acb6088f811cd5e8d"
|
|
76
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** 徽标 key 对应的角标元素选择器(driver.js 的 `element` 吃字符串选择器,故独立导出) */
|
|
2
|
+
export declare const guideBadgeAnchorSelector: (badgeKey: string) => string;
|
|
3
|
+
/**
|
|
4
|
+
* 解析徽标 key 的高亮锚点。
|
|
5
|
+
*
|
|
6
|
+
* `[data-dc-badge-key="K"]` → 向上 `closest` 到宿主 → **够不到就用角标元素自身**。
|
|
7
|
+
*
|
|
8
|
+
* 取宿主而非角标:角标只是个十几 px 的 pill,高亮它等于指着一个小圆点说「看这里」;
|
|
9
|
+
* 宿主才是用户真正要点的那个东西(菜单项 / 按钮 / 卡片)。宿主够不到时降级用角标,
|
|
10
|
+
* [MUST NOT] 返回 `undefined` —— 有个偏小的高亮也远好过整步消失。
|
|
11
|
+
*/
|
|
12
|
+
export declare const guideResolveBadgeAnchor: (badgeKey: string) => HTMLElement | undefined;
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createGuidePlugin, GUIDE_PLUGIN_NAME } from './plugin';
|
|
2
|
+
export { guideBadgeAnchorSelector, guideResolveBadgeAnchor } from './anchor';
|
|
3
|
+
export type { GuideBadgeKind, GuidePlugin, GuidePluginOptions, GuideResolvedStep, GuideRouterLike, GuideStartOptions, GuideStep, } from './types';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { GuidePlugin, GuidePluginOptions } from './types';
|
|
2
|
+
/** 插件名(bridge 插件宿主按它做跨对象实例的幂等安装) */
|
|
3
|
+
export declare const GUIDE_PLUGIN_NAME = "dc-plugin-guide";
|
|
4
|
+
/**
|
|
5
|
+
* 引导插件工厂。
|
|
6
|
+
*
|
|
7
|
+
* 返回的对象**同时是 core bridge 插件**(`name` + `install`)**和控制器**
|
|
8
|
+
* (`start` / `stop` / 进度读写)——应用层自己持这个引用,本插件不做 provide/inject。
|
|
9
|
+
*
|
|
10
|
+
* 实例状态全在本次调用的闭包里,零模块级单例(守实例隔离,与 core bridge 同规矩)。
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const guide = createGuidePlugin({ badgeKinds: ["new", "updated"], router });
|
|
15
|
+
* appCoreBridge.use(guide);
|
|
16
|
+
* if (!guide.start()) ElMessage.info("暂无新功能");
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare const createGuidePlugin: (options?: GuidePluginOptions) => GuidePlugin;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ShallowRef } from 'vue';
|
|
2
|
+
import { StorageLike } from '@done-coding/admin-core';
|
|
3
|
+
/**
|
|
4
|
+
* 导览进度(「走到第几步 / 看过没」)。
|
|
5
|
+
*
|
|
6
|
+
* 🔴 **[MUST NOT] 写回 `bridge.badge`** —— 那是**展示表**(谁该显示什么标),
|
|
7
|
+
* 不是用户状态表。把「这个用户看过了」写进去,会让一个人的浏览行为改掉全表的显示,
|
|
8
|
+
* 且轮询 `setAll` 一来就被冲掉。
|
|
9
|
+
*
|
|
10
|
+
* 存储走 **core bridge 的 `getStorage()`**(已按 app 的 namespace 派生 key 前缀),
|
|
11
|
+
* [MUST NOT] 自带一套 storage —— 会和 app 的命名空间分家,清缓存时清不干净。
|
|
12
|
+
*
|
|
13
|
+
* 每次 `createGuidePlugin` 在闭包内构造一份,零模块级单例(守实例隔离)。
|
|
14
|
+
*/
|
|
15
|
+
/** 进度模块 */
|
|
16
|
+
export interface GuideProgress {
|
|
17
|
+
/** 已看过的 step key(响应式只读;`pendingSteps` 依赖它重算) */
|
|
18
|
+
readonly seen: Readonly<ShallowRef<readonly string[]>>;
|
|
19
|
+
isSeen: (key: string) => boolean;
|
|
20
|
+
markSeen: (key: string) => void;
|
|
21
|
+
reset: () => void;
|
|
22
|
+
}
|
|
23
|
+
/** 进度模块工厂 */
|
|
24
|
+
export declare const createGuideProgress: (storage: StorageLike, storageKey: string) => GuideProgress;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Config as DriverConfig } from 'driver.js';
|
|
2
|
+
import { GuideResolvedStep, GuideRouterLike } from './types';
|
|
3
|
+
/** runner 入参 */
|
|
4
|
+
export interface GuideRunnerParams {
|
|
5
|
+
steps: readonly GuideResolvedStep[];
|
|
6
|
+
router?: GuideRouterLike;
|
|
7
|
+
driverConfig?: DriverConfig;
|
|
8
|
+
/** 某步被高亮 = 用户看到了它 */
|
|
9
|
+
onStepSeen: (key: string) => void;
|
|
10
|
+
}
|
|
11
|
+
/** 正在跑的一次导览 */
|
|
12
|
+
export interface GuideRunner {
|
|
13
|
+
destroy: () => void;
|
|
14
|
+
}
|
|
15
|
+
export declare const guideStartRunner: (params: GuideRunnerParams) => GuideRunner;
|
package/types/steps.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BadgeMarkMap } from '@done-coding/admin-core';
|
|
2
|
+
import { GuideBadgeKind, GuideResolvedStep, GuideStep } from './types';
|
|
3
|
+
/** 路由树最小结构面(`bridge.getRoutes()` 的返回结构上满足它,不焊 vue-router 类型) */
|
|
4
|
+
interface GuideRouteLike {
|
|
5
|
+
path: string;
|
|
6
|
+
/** vue-router 允许 symbol;非字符串的忽略(当不了徽标 key) */
|
|
7
|
+
name?: string | symbol;
|
|
8
|
+
children?: readonly GuideRouteLike[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 路由树 → `key → 次序` 表(深度优先)。
|
|
12
|
+
*
|
|
13
|
+
* 🔴 排序基准取路由树而非对象 key 顺序 / 注册顺序:菜单侧的 badge key 就是路由标识,
|
|
14
|
+
* 路由树次序 = 菜单次序 = 用户心智次序,且**稳定可复现**;
|
|
15
|
+
* 对象 key 顺序会随「谁先写进表里」漂移,加一条标就换序。
|
|
16
|
+
*
|
|
17
|
+
* **`path` 与 `name` 都登记到同一张表、共享同一个序号**:core 的菜单徽标是
|
|
18
|
+
* 「`name` 优先、`path` 兜底」两套 key 并存,只登记 path 会让以 name 注册的标
|
|
19
|
+
* 全部落进「非路由 key」那档、被排到末尾按字典序 —— 不报错,只是顺序悄悄不对了。
|
|
20
|
+
*/
|
|
21
|
+
export declare const guideBuildRouteOrder: (routes: readonly GuideRouteLike[] | undefined) => Map<string, number>;
|
|
22
|
+
/** 解析入参 */
|
|
23
|
+
export interface GuideResolveStepsParams {
|
|
24
|
+
/** 徽标表快照(`bridge.badge.marks.value`) */
|
|
25
|
+
marks: BadgeMarkMap;
|
|
26
|
+
/** 进导览的 kind */
|
|
27
|
+
badgeKinds: readonly GuideBadgeKind[];
|
|
28
|
+
/** 显式编排;传了就以它为准 */
|
|
29
|
+
steps?: readonly GuideStep[];
|
|
30
|
+
/** 路由树(排序基准) */
|
|
31
|
+
routes?: readonly GuideRouteLike[];
|
|
32
|
+
/** 是否配了 router(没配则带 `route` 的步骤跳过 + 警告) */
|
|
33
|
+
hasRouter: boolean;
|
|
34
|
+
/** 当前时刻(可注入,便于测过期判定) */
|
|
35
|
+
now?: number;
|
|
36
|
+
/** 一次性警告去重(同一实例同一 key 只吵一次) */
|
|
37
|
+
warned?: Set<string>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 解析出**有序**的引导步骤。
|
|
41
|
+
*
|
|
42
|
+
* - 传了 `steps` → **以它为准**(数组顺序即步序);
|
|
43
|
+
* - 没传 → 由 badge 表自动串:筛 `badgeKinds` → 按路由树次序排 → 成步。
|
|
44
|
+
*
|
|
45
|
+
* [MUST NOT] 做「显式 + 自动」拼接:那会让顺序落在两套规则的交界上,正是要避的坑。
|
|
46
|
+
*/
|
|
47
|
+
export declare const guideResolveSteps: (params: GuideResolveStepsParams) => GuideResolvedStep[];
|
|
48
|
+
export {};
|
package/types/theme.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把 driver.js 的弹窗接进本仓主题(本包「长得像不像自家组件」那一半)。
|
|
3
|
+
*
|
|
4
|
+
* 🔴 **为什么必须自己写**:driver.js 只暴露两个 CSS 变量(`--driver-animation-duration`
|
|
5
|
+
* / `--driver-popover-font-family`),**颜色全是写死的**——`.driver-popover` 是
|
|
6
|
+
* `#fff` 底 `#2d2d2d` 字、按钮 `#ccc` 边。暗色主题下就是一块白底黑字的弹窗浮在深色页面上。
|
|
7
|
+
*
|
|
8
|
+
* 🔴 **为什么是运行时注入而不是 `.less` 文件**:driver.js 的弹窗 teleport 到 `body`,
|
|
9
|
+
* scoped 样式够不着;而本包的 `driver.css` 是走 **external**(`driver.js/` 前缀命中
|
|
10
|
+
* vite external 规则)由消费方打包器解析的,本包自身**没有 style.css 产物**。
|
|
11
|
+
* 若为这段样式新开一个 css 入口,消费方就得多 `import` 一次样式文件(硬规则 6 那个坑)
|
|
12
|
+
* ⇒ 改用运行时注入,零接入成本,且 `var()` 让它随主题切换自动跟。
|
|
13
|
+
*
|
|
14
|
+
* 颜色一律走 `--dc-core-*` 优先、`--el-*` 兜底的双源(规则 14),[MUST NOT] 写死色值。
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* 确保主题样式已注入(幂等;SSR / 无 document 环境静默跳过)。
|
|
18
|
+
*
|
|
19
|
+
* 在 runner 启动时调用而非模块期——模块期副作用会让 SSR 导入即写 DOM,
|
|
20
|
+
* 且妨碍 tree-shaking。
|
|
21
|
+
*/
|
|
22
|
+
export declare const guideEnsureTheme: () => void;
|
package/types/types.d.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { BadgeEntry } from '@done-coding/admin-core';
|
|
2
|
+
import { Config as DriverConfig, DriveStep, Popover } from 'driver.js';
|
|
3
|
+
import { ComputedRef } from 'vue';
|
|
4
|
+
/**
|
|
5
|
+
* 进导览的徽标类型。
|
|
6
|
+
*
|
|
7
|
+
* 取 `BadgeEntry["kind"]`(对「交叉 + 联合」做索引访问即得全集)——
|
|
8
|
+
* [MUST NOT] 为此在 core 新增 kind 全集类型,也 [MUST NOT] 用 `BadgeBuiltinKind`
|
|
9
|
+
* (它不含 `count` / `custom`,会让应用层配不了自定义标)。
|
|
10
|
+
*/
|
|
11
|
+
export type GuideBadgeKind = BadgeEntry["kind"];
|
|
12
|
+
/**
|
|
13
|
+
* 路由器最小结构面。
|
|
14
|
+
*
|
|
15
|
+
* 只取本插件真正用到的两件事,[MUST NOT] 把 vue-router 的 `Router` 焊进对外类型——
|
|
16
|
+
* 那会让本包的 `types` 强依赖 vue-router(消费方未必装,也未必是同一大版本)。
|
|
17
|
+
* vue-router 的 `Router` 结构上满足本接口,直接传即可。
|
|
18
|
+
*/
|
|
19
|
+
export interface GuideRouterLike {
|
|
20
|
+
/** 当前路由(只读 path,用于判断「是不是已经在那一页了」) */
|
|
21
|
+
currentRoute: {
|
|
22
|
+
value: {
|
|
23
|
+
path: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* 跳转。
|
|
28
|
+
*
|
|
29
|
+
* 写成函数属性(而非方法简写)仍吃得下 vue-router 的 `Router.push` ——
|
|
30
|
+
* 它的入参 `RouteLocationRaw` 比 `string` 宽,函数属性在 `strictFunctionTypes`
|
|
31
|
+
* 下按逆变判定,宽入参赋给窄入参是合法的。
|
|
32
|
+
*/
|
|
33
|
+
push: (to: string) => Promise<unknown>;
|
|
34
|
+
}
|
|
35
|
+
/** 单个引导步骤(显式编排用;不传 `steps` 时由 badge 表自动串出同型对象) */
|
|
36
|
+
export interface GuideStep {
|
|
37
|
+
/**
|
|
38
|
+
* 步骤 key —— **进度表以它记「看过没」**。
|
|
39
|
+
* 缺省取 `badgeKey`;两者都没有则该步被丢弃(无从记进度)。
|
|
40
|
+
*/
|
|
41
|
+
key?: string;
|
|
42
|
+
/**
|
|
43
|
+
* 引用 badge 表里的一条:继承它的 `description`(讲解正文)与锚点。
|
|
44
|
+
* 这是本插件的杠杆点——**[MUST NOT] 让业务再埋一套 `data-tour-step`**。
|
|
45
|
+
*/
|
|
46
|
+
badgeKey?: string;
|
|
47
|
+
/** 显式锚点(CSS 选择器 / 元素 / 取元素的函数,透传 driver.js),覆盖 `badgeKey` 的解析结果 */
|
|
48
|
+
element?: DriveStep["element"];
|
|
49
|
+
/**
|
|
50
|
+
* **点高亮元素即进下一步**(默认 `false` = 点「下一步」才走)。
|
|
51
|
+
*
|
|
52
|
+
* 交互式引导用它串真实操作链:点菜单 → 跳页 → 点「创建」→ 弹窗出现 → 填表 → 提交。
|
|
53
|
+
* 每一步的锚点由**用户的真实点击**推进,而不是照着念。
|
|
54
|
+
*
|
|
55
|
+
* 配套:声明了本项的步会**自动隐去「下一步」**(推进方式已经变成「点目标」,
|
|
56
|
+
* 再留一个能绕过去的按钮,引导就白做了);「跳过」仍在,用户随时可退。
|
|
57
|
+
* 要覆盖这个配套就显式传 `showButtons`。
|
|
58
|
+
*
|
|
59
|
+
* ⚠️ 本步之后的导航由**用户的操作自然完成**(点菜单本来就会跳页),
|
|
60
|
+
* [MUST NOT] 再给下一步配 `route` 指望插件替他跳 —— 那会跳两次。
|
|
61
|
+
* 下一步的锚点没那么快出现是正常的,交给 {@link waitForElement} 等。
|
|
62
|
+
*/
|
|
63
|
+
advanceOnClick?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* **禁止**用户与高亮元素交互(默认 `false` = 镂空区**可以点**)。
|
|
66
|
+
*
|
|
67
|
+
* 纯讲解、又怕用户误触时才打开。与 {@link advanceOnClick} 互斥——
|
|
68
|
+
* 禁了交互就点不动,也就无从「点了才前进」。
|
|
69
|
+
*/
|
|
70
|
+
disableActiveInteraction?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* 该步等锚点挂载的超时(ms),覆盖全局默认 3000。
|
|
73
|
+
*
|
|
74
|
+
* 交互式链路里调大它:点「创建」到弹窗渲染完、或跨路由页面加载,都可能超过默认值。
|
|
75
|
+
* 等超时仍没有 → 按 `skipMissingElement` 跳过该步继续。
|
|
76
|
+
*/
|
|
77
|
+
waitForElement?: number;
|
|
78
|
+
/** 气泡标题 */
|
|
79
|
+
title?: string;
|
|
80
|
+
/** 气泡正文;缺省取 `badgeKey` 对应标的 `description` */
|
|
81
|
+
description?: string;
|
|
82
|
+
/**
|
|
83
|
+
* 锚点所在路由 —— 不在当前页时先 `router.push(route)` 再等元素挂载。
|
|
84
|
+
* 菜单项角标恒在侧边栏、一般不需要本字段;「锚点长在某页内部」才用。
|
|
85
|
+
*/
|
|
86
|
+
route?: string;
|
|
87
|
+
/** 透传 driver.js 气泡的方位(其余 popover 能力走 `driverConfig`,不在此重复开洞) */
|
|
88
|
+
popover?: Pick<Popover, "side" | "align">;
|
|
89
|
+
}
|
|
90
|
+
/** 解析后的步骤(已定序、已补齐正文与锚点,内部形态) */
|
|
91
|
+
export interface GuideResolvedStep extends GuideStep {
|
|
92
|
+
/**
|
|
93
|
+
* 进度 key —— 已确保存在;标配了 `version` 时形如 `原key@版本`。
|
|
94
|
+
* 🔴 按模块筛选 [MUST NOT] 拿它比对(版本一改就对不上),用 {@link sourceKey}。
|
|
95
|
+
*/
|
|
96
|
+
key: string;
|
|
97
|
+
/**
|
|
98
|
+
* 配置里写的那个 key(`step.key ?? step.badgeKey`,**未拼版本**)。
|
|
99
|
+
*
|
|
100
|
+
* 专供「按模块筛选」用(`start({ keys })`):应用层手上只有自己配的 key,
|
|
101
|
+
* 不知道也不该关心当前版本号。与 {@link key} 的分工——`key` 记进度、`sourceKey` 做引用。
|
|
102
|
+
*/
|
|
103
|
+
sourceKey: string;
|
|
104
|
+
/** 已确保非空(无正文的步骤在解析期就被剔除了) */
|
|
105
|
+
description: string;
|
|
106
|
+
}
|
|
107
|
+
/** `createGuidePlugin` 入参 */
|
|
108
|
+
export interface GuidePluginOptions {
|
|
109
|
+
/**
|
|
110
|
+
* 进导览的徽标类型,默认 `["new", "updated"]`。
|
|
111
|
+
*
|
|
112
|
+
* 默认只收「资讯档」两项:`beta` 语义是「不稳定需谨慎」、`wip` 是「还不能用」、`locked` 会遮挡宿主交互、
|
|
113
|
+
* `deprecated` 是待下线、`count` 是待办数、`custom` 语义 core 不知道 —— 都不该主动导览。
|
|
114
|
+
*
|
|
115
|
+
* `anchor`(纯锚点伪标)同样**不进默认值**:它本就没有「新功能」语义,
|
|
116
|
+
* 只提供 DOM 挂点,[MUST] 由显式 `steps` 用 `badgeKey` 引用。
|
|
117
|
+
*/
|
|
118
|
+
badgeKinds?: GuideBadgeKind[];
|
|
119
|
+
/**
|
|
120
|
+
* 显式编排。**传了就以它为准**(数组顺序即步序),不传才由 badge 表自动串。
|
|
121
|
+
* [MUST NOT] 期望「显式 + 自动」拼接——那会让顺序落在两套规则的交界上。
|
|
122
|
+
*/
|
|
123
|
+
steps?: GuideStep[];
|
|
124
|
+
/** 路由器(只有带 `route` 的步骤才需要;缺它时那些步骤会被跳过 + dev 警告) */
|
|
125
|
+
router?: GuideRouterLike;
|
|
126
|
+
/** 进度在 bridge storage 里的 key,默认 `"guide/seen"` */
|
|
127
|
+
storageKey?: string;
|
|
128
|
+
/**
|
|
129
|
+
* 插件名,默认 `"dc-plugin-guide"`。
|
|
130
|
+
*
|
|
131
|
+
* bridge 插件宿主**按名幂等**——同名的第二个实例会被静默跳过。要在同一个 app 里
|
|
132
|
+
* 装多份导览(如「新手上路」与「本期新功能」各一份),[MUST] 给它们不同的 `name`
|
|
133
|
+
* **和不同的 `storageKey`**(否则两份进度会互相污染)。
|
|
134
|
+
*/
|
|
135
|
+
name?: string;
|
|
136
|
+
/** 透传 driver.js 全局配置(本包只预设少数几项,其余原样交给 driver.js) */
|
|
137
|
+
driverConfig?: DriverConfig;
|
|
138
|
+
}
|
|
139
|
+
/** `start()` 入参 */
|
|
140
|
+
export interface GuideStartOptions {
|
|
141
|
+
/** 忽略进度、把所有入选步骤重跑一遍 */
|
|
142
|
+
force?: boolean;
|
|
143
|
+
/**
|
|
144
|
+
* **只跑这几个 key,且步序就是本数组的顺序**(不传 = 全部、按自动串的次序)——
|
|
145
|
+
* 模块化导览:a 模块 `start({ keys: [...] })` 只讲自己那几步,b 模块讲另外几步。
|
|
146
|
+
*
|
|
147
|
+
* 🔴 **顺序以本数组为准**,[MUST NOT] 指望它还按路由树次序排:显式传 keys 的调用方
|
|
148
|
+
* 知道自己要按什么次序讲。数组内重复的 key 会先去重(同一步讲两遍是噪音);
|
|
149
|
+
* 指向「没打标 / 已看过 / 已过期」的 key 会被静默略过(不报错,也不占位)。
|
|
150
|
+
*
|
|
151
|
+
* 比对的是 {@link GuideResolvedStep.sourceKey}(配置里写的 key,**不带版本号**)
|
|
152
|
+
* ⇒ 应用层照自己配的那个 key 传即可,[MUST NOT] 自己去拼 `key@version`。
|
|
153
|
+
*
|
|
154
|
+
* 🔴 **共用同一份进度表**(同一实例):同一个人在 a 模块看过的那步,
|
|
155
|
+
* b 模块再调也不会重复讲 —— 这正是「筛选」相对「每模块各建一个实例」的好处,
|
|
156
|
+
* 后者会让进度按实例分家、同一步被讲两遍。
|
|
157
|
+
*/
|
|
158
|
+
keys?: readonly string[];
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 引导控制器 —— `createGuidePlugin` 的返回值。
|
|
162
|
+
*
|
|
163
|
+
* 它**同时是 core bridge 插件对象**(`name` + `install`),经
|
|
164
|
+
* `coreBridge.use(guide)` 安装;应用层自己持这个引用调 `start` / `reset`
|
|
165
|
+
* (本插件不做 provide/inject —— 应用层本就在自己的模块里持有它,YAGNI)。
|
|
166
|
+
*/
|
|
167
|
+
export interface GuidePlugin {
|
|
168
|
+
/** 插件名(bridge 插件宿主按它做跨对象实例的幂等安装) */
|
|
169
|
+
name: string;
|
|
170
|
+
/**
|
|
171
|
+
* bridge 插件安装函数 —— 由 `coreBridge.use(guide)` 调用。
|
|
172
|
+
*
|
|
173
|
+
* ⚠️ [MUST NOT] 自己调,也 [MUST NOT] `app.use(guide)`:那会把 Vue 的 `app`
|
|
174
|
+
* 当 `ctx` 传进来(同名不同签名),本函数有守卫会抛出带指引的错误。
|
|
175
|
+
*/
|
|
176
|
+
install: (ctx: unknown) => void;
|
|
177
|
+
/** 待导览步骤(响应式只读;未入选 / 已看过的已剔除。未安装前恒为空) */
|
|
178
|
+
readonly pendingSteps: ComputedRef<GuideResolvedStep[]>;
|
|
179
|
+
/**
|
|
180
|
+
* 启动导览。返回**是否真的启动**——没有可跑的步骤时返回 `false`
|
|
181
|
+
* (应用层据此决定要不要提示「暂无新功能」,[MUST NOT] 让它弹一个空导览)。
|
|
182
|
+
*/
|
|
183
|
+
start: (options?: GuideStartOptions) => boolean;
|
|
184
|
+
/** 停止并销毁 driver 实例(幂等) */
|
|
185
|
+
stop: () => void;
|
|
186
|
+
/** 该步骤是否已看过 */
|
|
187
|
+
isSeen: (key: string) => boolean;
|
|
188
|
+
/** 标记看过(随手持久化) */
|
|
189
|
+
markSeen: (key: string) => void;
|
|
190
|
+
/** 清空进度 */
|
|
191
|
+
reset: () => void;
|
|
192
|
+
}
|