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