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