@vthst/page-manual 1.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.
@@ -0,0 +1,530 @@
1
+ <template>
2
+ <!-- 帮助文档按钮:放在 button-bar 末尾,margin-left:auto 推到行右端 -->
3
+ <el-button class="pg-trigger" @click="openDrawer">
4
+ <template #icon><el-icon><Question /></el-icon></template>
5
+ 帮助文档
6
+ </el-button>
7
+
8
+ <!-- 帮助抽屉:左侧弹出 -->
9
+ <el-drawer
10
+ v-model="drawerOpen"
11
+ direction="rtl"
12
+ size="70%"
13
+ :show-close="false"
14
+ >
15
+ <template #header>
16
+ <div class="pg-title">
17
+ <span class="pg-title-main">帮助文档</span>
18
+ </div>
19
+ </template>
20
+ <div class="pg-body">
21
+ <div class="pg-scroll">
22
+ <div class="pg-doc-wrap">
23
+ <aside class="pg-toc">
24
+ <el-input v-model="tocKeyword" placeholder="搜索文档内容" clearable>
25
+ <template #prefix><el-icon><Search /></el-icon></template>
26
+ </el-input>
27
+ <div v-if="tocKeyword.trim()" class="pg-toc-hit">
28
+ <template v-if="hitCount">
29
+ <span>{{ activeHit + 1 }} / {{ hitCount }}</span>
30
+ <span class="pg-toc-hit-nav">
31
+ <el-icon class="pg-toc-hit-btn" @click="gotoHit(-1)"><ArrowUp /></el-icon>
32
+ <el-icon class="pg-toc-hit-btn" @click="gotoHit(1)"><ArrowDown /></el-icon>
33
+ </span>
34
+ </template>
35
+ <template v-else>无匹配内容</template>
36
+ </div>
37
+ <nav class="pg-toc-list">
38
+ <div
39
+ v-for="item in tocItems"
40
+ :key="item.text"
41
+ :class="['pg-toc-item', item.sub ? 'pg-toc-sub' : '', activeToc === item.text ? 'pg-toc-active' : '']"
42
+ @click="scrollToSection(item)"
43
+ >{{ item.text }}</div>
44
+ </nav>
45
+ </aside>
46
+ <div ref="docRef" class="pg-doc" @scroll="handleDocScroll">
47
+ <slot name="doc"></slot>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </el-drawer>
53
+ </template>
54
+
55
+ <script setup>
56
+ import { ref, nextTick, onMounted, watch } from 'vue'
57
+ import { Question, Search, ArrowUp, ArrowDown } from '@element-plus/icons-vue'
58
+
59
+ /**
60
+ * 页面帮助组件(button-bar 末尾通用)— Element Plus 版本
61
+ *
62
+ * 依赖:element-plus + @element-plus/icons-vue
63
+ * 配套模板:docs/page-guide-template.md
64
+ *
65
+ * 用法:
66
+ * <PageGuide>
67
+ * <template #doc> 操作文档内容(按 gd-* 类 + data-toc 标题约定书写) </template>
68
+ * </PageGuide>
69
+ */
70
+ const drawerOpen = ref(false)
71
+
72
+ /** ===== 操作文档目录(TOC)+ 全文搜索高亮 ===== */
73
+ const tocKeyword = ref('')
74
+ const tocItems = ref([])
75
+ const hitCount = ref(0)
76
+ const activeHit = ref(-1)
77
+ let hitRanges = []
78
+ const activeToc = ref('')
79
+ const docRef = ref(null)
80
+
81
+ const collectToc = () => {
82
+ const root = docRef.value
83
+ if (!root) {
84
+ tocItems.value = []
85
+ return
86
+ }
87
+ tocItems.value = [...root.querySelectorAll('[data-toc]')].map((el) => ({
88
+ text: (el.textContent || '').trim(),
89
+ sub: el.hasAttribute('data-toc-sub'),
90
+ el,
91
+ }))
92
+ }
93
+
94
+ const runHighlight = () => {
95
+ const root = docRef.value
96
+ if (!root) return
97
+ const kw = tocKeyword.value.trim()
98
+ hitCount.value = 0
99
+ activeHit.value = -1
100
+ hitRanges = []
101
+ const supportsHighlight = typeof window.Highlight !== 'undefined' && window.CSS?.highlights
102
+ if (supportsHighlight) {
103
+ CSS.highlights.delete('pg-hit')
104
+ CSS.highlights.delete('pg-hit-active')
105
+ }
106
+ if (!kw) return
107
+
108
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
109
+ acceptNode: (node) => {
110
+ const tag = node.parentElement?.tagName
111
+ if (tag === 'SCRIPT' || tag === 'STYLE') return NodeFilter.FILTER_REJECT
112
+ return node.nodeValue.includes(kw) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
113
+ },
114
+ })
115
+ const ranges = []
116
+ let textNode
117
+ while ((textNode = walker.nextNode())) {
118
+ let idx = textNode.nodeValue.indexOf(kw)
119
+ while (idx !== -1) {
120
+ const range = document.createRange()
121
+ range.setStart(textNode, idx)
122
+ range.setEnd(textNode, idx + kw.length)
123
+ ranges.push(range)
124
+ idx = textNode.nodeValue.indexOf(kw, idx + kw.length)
125
+ }
126
+ }
127
+ hitCount.value = ranges.length
128
+ if (ranges.length) {
129
+ hitRanges = ranges
130
+ if (supportsHighlight) CSS.highlights.set('pg-hit', new Highlight(...ranges))
131
+ activeHit.value = 0
132
+ applyActiveHighlight()
133
+ }
134
+ }
135
+
136
+ const applyActiveHighlight = () => {
137
+ const range = hitRanges[activeHit.value]
138
+ if (!range) return
139
+ if (typeof window.Highlight !== 'undefined' && window.CSS?.highlights) {
140
+ CSS.highlights.set('pg-hit-active', new Highlight(range))
141
+ }
142
+ range.startContainer.parentElement?.scrollIntoView({ behavior: 'smooth', block: 'center' })
143
+ }
144
+
145
+ const gotoHit = (dir) => {
146
+ if (!hitRanges.length) return
147
+ activeHit.value = (activeHit.value + dir + hitRanges.length) % hitRanges.length
148
+ applyActiveHighlight()
149
+ }
150
+
151
+ let highlightTimer = null
152
+ watch(tocKeyword, () => {
153
+ clearTimeout(highlightTimer)
154
+ highlightTimer = setTimeout(runHighlight, 300)
155
+ })
156
+
157
+ const scrollToSection = (item) => {
158
+ item.el?.scrollIntoView({ behavior: 'smooth', block: 'start' })
159
+ activeToc.value = item.text
160
+ }
161
+
162
+ const updateActiveToc = () => {
163
+ const root = docRef.value
164
+ if (!root || !tocItems.value.length) return
165
+ if (!root.offsetHeight) {
166
+ activeToc.value = tocItems.value[0].text
167
+ return
168
+ }
169
+ const pos = root.scrollTop + 40
170
+ let current = ''
171
+ for (const t of tocItems.value) {
172
+ if (t.el && t.el.offsetTop <= pos) current = t.text
173
+ }
174
+ activeToc.value = current || tocItems.value[0].text
175
+ }
176
+
177
+ let spyTimer = null
178
+ const handleDocScroll = () => {
179
+ clearTimeout(spyTimer)
180
+ spyTimer = setTimeout(updateActiveToc, 100)
181
+ }
182
+
183
+ onMounted(collectToc)
184
+
185
+ const openDrawer = () => {
186
+ drawerOpen.value = true
187
+ nextTick(() => {
188
+ requestAnimationFrame(() => {
189
+ requestAnimationFrame(() => {
190
+ collectToc()
191
+ if (tocKeyword.value.trim()) runHighlight()
192
+ updateActiveToc()
193
+ })
194
+ })
195
+ })
196
+ }
197
+ </script>
198
+
199
+ <style scoped>
200
+ /* ===== 标题区 ===== */
201
+ .pg-title {
202
+ display: flex;
203
+ align-items: center;
204
+ }
205
+
206
+ .pg-title-main {
207
+ font-size: 12px;
208
+ font-weight: 400;
209
+ color: #64748b;
210
+ }
211
+
212
+ /* 推到 button-bar 右端 */
213
+ .pg-trigger {
214
+ margin-left: auto;
215
+ }
216
+
217
+ /* ===== 内容区布局 ===== */
218
+ .pg-body {
219
+ display: flex;
220
+ flex-direction: column;
221
+ height: 100%;
222
+ }
223
+
224
+ .pg-scroll {
225
+ flex: 1;
226
+ min-height: 0;
227
+ margin-top: 16px;
228
+ }
229
+
230
+ /* ===== 操作文档双栏 ===== */
231
+ .pg-doc-wrap {
232
+ display: flex;
233
+ gap: 16px;
234
+ height: 100%;
235
+ }
236
+
237
+ .pg-toc {
238
+ display: flex;
239
+ flex-direction: column;
240
+ width: 190px;
241
+ flex-shrink: 0;
242
+ border-right: 1px solid #e5e7eb;
243
+ padding-right: 8px;
244
+ }
245
+
246
+ .pg-toc-list {
247
+ flex: 1;
248
+ min-height: 0;
249
+ overflow-y: auto;
250
+ margin-top: 8px;
251
+ }
252
+
253
+ .pg-toc-item {
254
+ padding: 4px 8px;
255
+ font-size: 13px;
256
+ color: #64748b;
257
+ border-left: 3px solid transparent;
258
+ border-radius: 0 4px 4px 0;
259
+ cursor: pointer;
260
+ white-space: nowrap;
261
+ overflow: hidden;
262
+ text-overflow: ellipsis;
263
+ }
264
+
265
+ .pg-toc-item:hover {
266
+ color: #3B82F6;
267
+ background: rgba(59, 130, 246, 0.10);
268
+ }
269
+
270
+ .pg-toc-item.pg-toc-active {
271
+ color: #3B82F6;
272
+ border-left-color: #3B82F6;
273
+ background: rgba(59, 130, 246, 0.10);
274
+ font-weight: 600;
275
+ }
276
+
277
+ .pg-toc-sub {
278
+ padding-left: 16px;
279
+ font-size: 12px;
280
+ }
281
+
282
+ .pg-toc-hit {
283
+ display: flex;
284
+ align-items: center;
285
+ justify-content: space-between;
286
+ padding: 4px 4px 0;
287
+ font-size: 12px;
288
+ color: #94a3b8;
289
+ }
290
+
291
+ .pg-toc-hit-nav {
292
+ display: inline-flex;
293
+ gap: 4px;
294
+ }
295
+
296
+ .pg-toc-hit-btn {
297
+ padding: 2px;
298
+ font-size: 12px;
299
+ color: #64748b;
300
+ cursor: pointer;
301
+ border-radius: 4px;
302
+ }
303
+
304
+ .pg-toc-hit-btn:hover {
305
+ color: #3B82F6;
306
+ background: rgba(59, 130, 246, 0.10);
307
+ }
308
+
309
+ /* ===== 文档正文 ===== */
310
+ .pg-doc {
311
+ position: relative;
312
+ flex: 1;
313
+ min-width: 0;
314
+ min-height: 0;
315
+ overflow-y: auto;
316
+ font-size: 15px;
317
+ color: #64748b;
318
+ line-height: 1.8;
319
+ }
320
+
321
+ .pg-doc :deep(p) {
322
+ margin: 0 0 8px;
323
+ }
324
+ .pg-doc :deep(p:last-child) {
325
+ margin-bottom: 0;
326
+ }
327
+ .pg-doc :deep(ul) {
328
+ margin: 0;
329
+ padding-left: 16px;
330
+ }
331
+ .pg-doc :deep(li) {
332
+ margin-bottom: 4px;
333
+ }
334
+ .pg-doc :deep(li:last-child) {
335
+ margin-bottom: 0;
336
+ }
337
+ .pg-doc :deep(b) {
338
+ color: #334155;
339
+ font-weight: 600;
340
+ }
341
+
342
+ .pg-doc :deep(.el-image) {
343
+ display: flex;
344
+ justify-content: center;
345
+ margin: 8px 0 12px;
346
+ }
347
+ .pg-doc :deep(.el-image__inner) {
348
+ max-width: 100%;
349
+ max-height: 320px;
350
+ object-fit: contain;
351
+ border-radius: 8px;
352
+ border: 1px solid #e5e7eb;
353
+ }
354
+
355
+ /* ===== gd-* 类库(由页面 slot 使用) ===== */
356
+
357
+ .pg-doc :deep(.gd-sec-title) {
358
+ display: flex;
359
+ align-items: center;
360
+ font-size: 17px;
361
+ font-weight: 700;
362
+ color: #334155;
363
+ margin: 26px 0 8px;
364
+ padding-left: 8px;
365
+ border-left: 3px solid #3B82F6;
366
+ }
367
+ .pg-doc :deep(.gd-sec-title:first-child) {
368
+ margin-top: 2px;
369
+ }
370
+
371
+ .pg-doc :deep(.gd-op) {
372
+ border: 1px solid #e5e7eb;
373
+ border-radius: 8px;
374
+ background: #fff;
375
+ padding: 12px 16px;
376
+ margin: 12px 0;
377
+ }
378
+
379
+ .pg-doc :deep(.gd-op-title) {
380
+ font-size: 16px;
381
+ font-weight: 700;
382
+ color: #334155;
383
+ }
384
+
385
+ .pg-doc :deep(.gd-op-goal) {
386
+ font-size: 14px;
387
+ color: #94a3b8;
388
+ margin: 2px 0 8px;
389
+ }
390
+
391
+ .pg-doc :deep(.gd-step) {
392
+ display: flex;
393
+ gap: 12px;
394
+ align-items: flex-start;
395
+ margin: 8px 0;
396
+ }
397
+
398
+ .pg-doc :deep(.gd-step-num) {
399
+ flex-shrink: 0;
400
+ display: inline-flex;
401
+ align-items: center;
402
+ justify-content: center;
403
+ width: 22px;
404
+ height: 22px;
405
+ border-radius: 50%;
406
+ background: #3B82F6;
407
+ color: #fff;
408
+ font-size: 12px;
409
+ font-weight: 600;
410
+ margin-top: 3px;
411
+ }
412
+
413
+ .pg-doc :deep(.gd-step-body) {
414
+ flex: 1;
415
+ min-width: 0;
416
+ }
417
+
418
+ .pg-doc :deep(.gd-step-title) {
419
+ font-weight: 600;
420
+ color: #334155;
421
+ margin-bottom: 2px;
422
+ }
423
+
424
+ .pg-doc :deep(.gd-callout) {
425
+ border: 1px solid #3B82F6;
426
+ border-left-width: 4px;
427
+ border-left-style: solid;
428
+ border-radius: 4px;
429
+ padding: 8px 12px;
430
+ margin: 8px 0;
431
+ font-size: 15px;
432
+ background: rgba(59, 130, 246, 0.10);
433
+ color: #2563eb;
434
+ }
435
+
436
+ .pg-doc :deep(.gd-callout-title) {
437
+ font-weight: 600;
438
+ margin-bottom: 2px;
439
+ }
440
+
441
+ .pg-doc :deep(.gd-tag-must),
442
+ .pg-doc :deep(.gd-tag-opt),
443
+ .pg-doc :deep(.gd-tag-role) {
444
+ display: inline-block;
445
+ padding: 2px 8px;
446
+ border-radius: 4px;
447
+ font-size: 12px;
448
+ font-weight: 600;
449
+ line-height: 1.6;
450
+ white-space: nowrap;
451
+ }
452
+ .pg-doc :deep(.gd-tag-must) {
453
+ background: rgba(220, 38, 38, 0.06);
454
+ color: #b91c1c;
455
+ }
456
+ .pg-doc :deep(.gd-tag-opt) {
457
+ background: rgba(59, 130, 246, 0.10);
458
+ color: #2563eb;
459
+ }
460
+ .pg-doc :deep(.gd-tag-role) {
461
+ background: rgba(234, 88, 12, 0.08);
462
+ color: #c2410c;
463
+ }
464
+
465
+ .pg-doc :deep(table) {
466
+ width: 100%;
467
+ border-collapse: collapse;
468
+ margin: 8px 0 12px;
469
+ font-size: 15px;
470
+ }
471
+ .pg-doc :deep(th),
472
+ .pg-doc :deep(td) {
473
+ border: 1px solid #e5e7eb;
474
+ padding: 4px 12px;
475
+ text-align: left;
476
+ vertical-align: top;
477
+ }
478
+ .pg-doc :deep(th) {
479
+ background: #f8fafc;
480
+ color: #334155;
481
+ font-weight: 600;
482
+ white-space: nowrap;
483
+ }
484
+ .pg-doc :deep(tbody tr:nth-child(even)) {
485
+ background: rgba(148, 163, 184, 0.06);
486
+ }
487
+
488
+ .pg-doc :deep(.gd-fig-cap) {
489
+ text-align: center;
490
+ font-size: 12px;
491
+ color: #94a3b8;
492
+ margin: -2px 0 12px;
493
+ }
494
+
495
+ .pg-doc :deep(.gd-breadcrumb) {
496
+ display: inline-flex;
497
+ align-items: center;
498
+ gap: 4px;
499
+ background: #f8fafc;
500
+ border: 1px solid #e5e7eb;
501
+ border-radius: 4px;
502
+ padding: 4px 12px;
503
+ font-size: 14px;
504
+ color: #334155;
505
+ margin: 4px 0 4px 12px;
506
+ }
507
+
508
+ .pg-doc :deep(code) {
509
+ font-family: monospace;
510
+ background: #f8fafc;
511
+ border: 1px solid #e5e7eb;
512
+ border-radius: 4px;
513
+ padding: 2px 8px;
514
+ font-size: 13px;
515
+ color: #2563eb;
516
+ }
517
+
518
+ </style>
519
+
520
+ <style>
521
+ /* 全文搜索命中高亮(CSS Custom Highlight API) */
522
+ .pg-doc ::highlight(pg-hit) {
523
+ background-color: #fde68a;
524
+ color: inherit;
525
+ }
526
+ .pg-doc ::highlight(pg-hit-active) {
527
+ background-color: #fb923c;
528
+ color: #fff;
529
+ }
530
+ </style>