@tnotesjs/core 0.0.6 → 0.0.8

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.
Files changed (116) hide show
  1. package/README.md +87 -84
  2. package/dist/{chunk-JDVN2QYW.js → chunk-JUMVJKWV.js} +615 -614
  3. package/dist/cli/index.js +125 -125
  4. package/dist/vitepress/config/index.js +184 -210
  5. package/package.json +15 -3
  6. package/types/config.ts +61 -61
  7. package/types/index.ts +11 -11
  8. package/types/note.ts +33 -33
  9. package/types/shims.d.ts +3 -4
  10. package/vitepress/assets/icons/icon__check.svg +3 -3
  11. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  12. package/vitepress/assets/icons/icon__close.svg +1 -1
  13. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  14. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  15. package/vitepress/assets/icons/icon__copy.svg +4 -4
  16. package/vitepress/assets/icons/icon__focus.svg +1 -1
  17. package/vitepress/assets/icons/icon__fold.svg +3 -3
  18. package/vitepress/assets/icons/icon__folder.svg +1 -1
  19. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  20. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  21. package/vitepress/assets/icons/icon__github.svg +3 -3
  22. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  23. package/vitepress/assets/icons/icon__next.svg +1 -1
  24. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  25. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  26. package/vitepress/assets/icons/icon__prev.svg +1 -1
  27. package/vitepress/assets/icons/icon__restore.svg +1 -1
  28. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  29. package/vitepress/assets/icons/icon__search.svg +1 -1
  30. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  31. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  32. package/vitepress/assets/icons/icon__totop.svg +5 -5
  33. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  34. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  35. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  36. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  37. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  38. package/vitepress/assets/icons/index.ts +38 -38
  39. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  40. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +373 -372
  41. package/vitepress/components/CodeBlockFullscreen/index.ts +115 -114
  42. package/vitepress/components/CodeBlockFullscreen/styles.css +64 -64
  43. package/vitepress/components/Discussions/Discussions.module.scss +32 -32
  44. package/vitepress/components/Discussions/Discussions.vue +211 -210
  45. package/vitepress/components/EnWordList/EnWordList.module.scss +124 -124
  46. package/vitepress/components/EnWordList/EnWordList.vue +543 -542
  47. package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -22
  48. package/vitepress/components/EnWordList/RightClickMenu.vue +66 -66
  49. package/vitepress/components/Footprints/Footprints.module.scss +93 -93
  50. package/vitepress/components/Footprints/Footprints.vue +377 -377
  51. package/vitepress/components/Layout/AboutModal.module.scss +233 -233
  52. package/vitepress/components/Layout/AboutModal.vue +105 -105
  53. package/vitepress/components/Layout/AboutPanel.vue +270 -265
  54. package/vitepress/components/Layout/ContentCollapse.vue +603 -602
  55. package/vitepress/components/Layout/CustomSidebar.vue +608 -605
  56. package/vitepress/components/Layout/DocBeforeControls.vue +149 -139
  57. package/vitepress/components/Layout/DocFooter.vue +233 -232
  58. package/vitepress/components/Layout/ImagePreview.module.scss +201 -201
  59. package/vitepress/components/Layout/ImagePreview.vue +281 -280
  60. package/vitepress/components/Layout/Layout.module.scss +661 -661
  61. package/vitepress/components/Layout/Layout.vue +539 -542
  62. package/vitepress/components/Layout/NoteStatus.vue +140 -139
  63. package/vitepress/components/Layout/SidebarItems.vue +267 -262
  64. package/vitepress/components/Layout/SidebarNavBefore.vue +92 -92
  65. package/vitepress/components/Layout/Swiper.vue +167 -167
  66. package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -11
  67. package/vitepress/components/Layout/ToggleFullContent.vue +35 -34
  68. package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -11
  69. package/vitepress/components/Layout/ToggleSidebar.vue +36 -35
  70. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  71. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -121
  72. package/vitepress/components/Layout/composables/useNoteSave.ts +173 -172
  73. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  74. package/vitepress/components/Layout/composables/useRedirect.ts +113 -110
  75. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +86 -83
  76. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  77. package/vitepress/components/LoadingPage/LoadingPage.vue +192 -192
  78. package/vitepress/components/MarkMap/MarkMap.module.scss +159 -159
  79. package/vitepress/components/MarkMap/MarkMap.vue +405 -404
  80. package/vitepress/components/Mermaid/Mermaid.module.scss +275 -275
  81. package/vitepress/components/Mermaid/Mermaid.vue +365 -363
  82. package/vitepress/components/NotesTable/NotesTable.module.scss +77 -77
  83. package/vitepress/components/NotesTable/NotesTable.vue +98 -97
  84. package/vitepress/components/NotesTable/README.md +67 -67
  85. package/vitepress/components/Settings/Settings.module.scss +433 -433
  86. package/vitepress/components/Settings/Settings.vue +306 -305
  87. package/vitepress/components/SidebarCard/MindMapView.vue +484 -483
  88. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  89. package/vitepress/components/SidebarCard/SidebarCard.vue +946 -948
  90. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  91. package/vitepress/components/constants.ts +91 -91
  92. package/vitepress/components/notesConfig.data.ts +73 -73
  93. package/vitepress/components/sidebar.data.ts +59 -59
  94. package/vitepress/components/tnotes-config.data.ts +21 -21
  95. package/vitepress/components/utils.ts +26 -26
  96. package/vitepress/config/index.ts +160 -146
  97. package/vitepress/configs/constants.ts +26 -26
  98. package/vitepress/configs/head.config.ts +25 -24
  99. package/vitepress/configs/index.ts +9 -9
  100. package/vitepress/configs/markdown-it.d.ts +23 -20
  101. package/vitepress/configs/markdown.config.ts +366 -365
  102. package/vitepress/configs/theme.config.ts +108 -108
  103. package/vitepress/plugins/buildProgressPlugin.ts +434 -470
  104. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -106
  105. package/vitepress/plugins/renameNotePlugin.ts +60 -59
  106. package/vitepress/plugins/updateConfigPlugin.ts +63 -62
  107. package/vitepress/theme/index.ts +95 -93
  108. package/vitepress/theme/styles/base.scss +50 -50
  109. package/vitepress/theme/styles/components/404.scss +31 -31
  110. package/vitepress/theme/styles/components/collapse.scss +175 -175
  111. package/vitepress/theme/styles/components/markmap.scss +101 -101
  112. package/vitepress/theme/styles/components/swiper.scss +255 -255
  113. package/vitepress/theme/styles/index.scss +25 -25
  114. package/vitepress/theme/styles/layout.scss +62 -62
  115. package/vitepress/theme/styles/utilities.scss +39 -39
  116. package/vitepress/theme/styles/vitepress-override.scss +25 -25
@@ -1,233 +1,233 @@
1
- // AboutModal 组件样式
2
-
3
- // 模态框背景遮罩
4
- .modalBackdrop {
5
- position: fixed;
6
- inset: 0;
7
- background: rgba(0, 0, 0, 0.75);
8
- backdrop-filter: blur(4px);
9
- display: flex;
10
- align-items: center;
11
- justify-content: center;
12
- z-index: 1200;
13
- padding: 1rem;
14
- animation: fadeIn 0.2s ease-out;
15
- }
16
-
17
- @keyframes fadeIn {
18
- from {
19
- opacity: 0;
20
- }
21
- to {
22
- opacity: 1;
23
- }
24
- }
25
-
26
- @keyframes slideUp {
27
- from {
28
- opacity: 0;
29
- transform: translateY(20px);
30
- }
31
- to {
32
- opacity: 1;
33
- transform: translateY(0);
34
- }
35
- }
36
-
37
- // 模态框主体
38
- .modal {
39
- background: var(--vp-c-bg);
40
- color: var(--vp-c-text-1);
41
- border-radius: 16px;
42
- max-width: 580px;
43
- width: 100%;
44
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4),
45
- 0 0 0 1px rgba(255, 255, 255, 0.05);
46
- overflow: hidden;
47
- outline: none;
48
- animation: slideUp 0.3s ease-out;
49
-
50
- // 暗色模式优化
51
- :global(.dark) & {
52
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6),
53
- 0 0 0 1px rgba(255, 255, 255, 0.08);
54
- }
55
- }
56
-
57
- // 模态框头部
58
- .modalHeader {
59
- display: flex;
60
- align-items: center;
61
- justify-content: space-between;
62
- padding: 1.25rem 1.5rem;
63
- border-bottom: 1px solid var(--vp-c-divider);
64
- background: linear-gradient(
65
- 180deg,
66
- var(--vp-c-bg-soft) 0%,
67
- var(--vp-c-bg) 100%
68
- );
69
- position: relative;
70
-
71
- // 添加顶部装饰条
72
- &::before {
73
- content: '';
74
- position: absolute;
75
- top: 0;
76
- left: 0;
77
- right: 0;
78
- height: 3px;
79
- background: linear-gradient(
80
- 90deg,
81
- var(--vp-c-brand-1),
82
- var(--vp-c-brand-2)
83
- );
84
- }
85
- }
86
-
87
- // 模态框标题
88
- .modalTitle {
89
- margin: 0;
90
- font-size: 1.125rem;
91
- font-weight: 600;
92
- color: var(--vp-c-text-1);
93
- letter-spacing: -0.01em;
94
- display: flex;
95
- align-items: center;
96
- gap: 0.5rem;
97
-
98
- // 添加图标装饰(可选,如果标题前有 emoji)
99
- &::before {
100
- content: '📝';
101
- font-size: 1.25rem;
102
- }
103
- }
104
-
105
- // 关闭按钮
106
- .closeBtn {
107
- background: transparent;
108
- border: none;
109
- cursor: pointer;
110
- font-size: 1.25rem;
111
- line-height: 1;
112
- padding: 0.5rem;
113
- border-radius: 8px;
114
- color: var(--vp-c-text-2);
115
- transition: all 0.2s ease;
116
- display: flex;
117
- align-items: center;
118
- justify-content: center;
119
- width: 32px;
120
- height: 32px;
121
-
122
- &:hover {
123
- background: var(--vp-c-bg-soft);
124
- color: var(--vp-c-text-1);
125
- transform: rotate(90deg);
126
- }
127
-
128
- &:active {
129
- transform: rotate(90deg) scale(0.95);
130
- }
131
- }
132
-
133
- // 模态框主体内容
134
- .modalBody {
135
- padding: 1.5rem;
136
- font-size: 0.95rem;
137
- color: var(--vp-c-text-2);
138
- line-height: 1.7;
139
- max-height: 60vh;
140
- overflow-y: auto;
141
-
142
- // 美化滚动条
143
- &::-webkit-scrollbar {
144
- width: 6px;
145
- }
146
-
147
- &::-webkit-scrollbar-track {
148
- background: transparent;
149
- }
150
-
151
- &::-webkit-scrollbar-thumb {
152
- background: var(--vp-c-divider);
153
- border-radius: 3px;
154
-
155
- &:hover {
156
- background: var(--vp-c-text-3);
157
- }
158
- }
159
-
160
- // 优化内容样式
161
- p {
162
- margin: 0.75rem 0;
163
-
164
- &:first-child {
165
- margin-top: 0;
166
- }
167
-
168
- &:last-child {
169
- margin-bottom: 0;
170
- }
171
- }
172
-
173
- // 如果内容中有图标,增加间距
174
- :deep(span) {
175
- margin-right: 0.25rem;
176
- }
177
-
178
- // 强调文本
179
- strong {
180
- color: var(--vp-c-text-1);
181
- font-weight: 600;
182
- }
183
-
184
- // 链接样式
185
- a {
186
- color: var(--vp-c-brand-1);
187
- text-decoration: none;
188
- transition: color 0.2s;
189
-
190
- &:hover {
191
- color: var(--vp-c-brand-2);
192
- text-decoration: underline;
193
- }
194
- }
195
- }
196
-
197
- // 模态框底部(可选)
198
- .modalFooter {
199
- padding: 1rem 1.5rem;
200
- border-top: 1px solid var(--vp-c-divider);
201
- background: var(--vp-c-bg-soft);
202
- text-align: right;
203
- display: flex;
204
- gap: 0.75rem;
205
- justify-content: flex-end;
206
- align-items: center;
207
- }
208
-
209
- // 响应式优化
210
- @media (max-width: 640px) {
211
- .modal {
212
- max-width: 100%;
213
- margin: 1rem;
214
- border-radius: 12px;
215
- }
216
-
217
- .modalHeader {
218
- padding: 1rem 1.25rem;
219
- }
220
-
221
- .modalTitle {
222
- font-size: 1rem;
223
- }
224
-
225
- .modalBody {
226
- padding: 1.25rem;
227
- max-height: 50vh;
228
- }
229
-
230
- .modalFooter {
231
- padding: 0.875rem 1.25rem;
232
- }
233
- }
1
+ // AboutModal 组件样式
2
+
3
+ // 模态框背景遮罩
4
+ .modalBackdrop {
5
+ position: fixed;
6
+ inset: 0;
7
+ background: rgba(0, 0, 0, 0.75);
8
+ backdrop-filter: blur(4px);
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ z-index: 1200;
13
+ padding: 1rem;
14
+ animation: fadeIn 0.2s ease-out;
15
+ }
16
+
17
+ @keyframes fadeIn {
18
+ from {
19
+ opacity: 0;
20
+ }
21
+ to {
22
+ opacity: 1;
23
+ }
24
+ }
25
+
26
+ @keyframes slideUp {
27
+ from {
28
+ opacity: 0;
29
+ transform: translateY(20px);
30
+ }
31
+ to {
32
+ opacity: 1;
33
+ transform: translateY(0);
34
+ }
35
+ }
36
+
37
+ // 模态框主体
38
+ .modal {
39
+ background: var(--vp-c-bg);
40
+ color: var(--vp-c-text-1);
41
+ border-radius: 16px;
42
+ max-width: 580px;
43
+ width: 100%;
44
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4),
45
+ 0 0 0 1px rgba(255, 255, 255, 0.05);
46
+ overflow: hidden;
47
+ outline: none;
48
+ animation: slideUp 0.3s ease-out;
49
+
50
+ // 暗色模式优化
51
+ :global(.dark) & {
52
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6),
53
+ 0 0 0 1px rgba(255, 255, 255, 0.08);
54
+ }
55
+ }
56
+
57
+ // 模态框头部
58
+ .modalHeader {
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: space-between;
62
+ padding: 1.25rem 1.5rem;
63
+ border-bottom: 1px solid var(--vp-c-divider);
64
+ background: linear-gradient(
65
+ 180deg,
66
+ var(--vp-c-bg-soft) 0%,
67
+ var(--vp-c-bg) 100%
68
+ );
69
+ position: relative;
70
+
71
+ // 添加顶部装饰条
72
+ &::before {
73
+ content: '';
74
+ position: absolute;
75
+ top: 0;
76
+ left: 0;
77
+ right: 0;
78
+ height: 3px;
79
+ background: linear-gradient(
80
+ 90deg,
81
+ var(--vp-c-brand-1),
82
+ var(--vp-c-brand-2)
83
+ );
84
+ }
85
+ }
86
+
87
+ // 模态框标题
88
+ .modalTitle {
89
+ margin: 0;
90
+ font-size: 1.125rem;
91
+ font-weight: 600;
92
+ color: var(--vp-c-text-1);
93
+ letter-spacing: -0.01em;
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 0.5rem;
97
+
98
+ // 添加图标装饰(可选,如果标题前有 emoji)
99
+ &::before {
100
+ content: '📝';
101
+ font-size: 1.25rem;
102
+ }
103
+ }
104
+
105
+ // 关闭按钮
106
+ .closeBtn {
107
+ background: transparent;
108
+ border: none;
109
+ cursor: pointer;
110
+ font-size: 1.25rem;
111
+ line-height: 1;
112
+ padding: 0.5rem;
113
+ border-radius: 8px;
114
+ color: var(--vp-c-text-2);
115
+ transition: all 0.2s ease;
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ width: 32px;
120
+ height: 32px;
121
+
122
+ &:hover {
123
+ background: var(--vp-c-bg-soft);
124
+ color: var(--vp-c-text-1);
125
+ transform: rotate(90deg);
126
+ }
127
+
128
+ &:active {
129
+ transform: rotate(90deg) scale(0.95);
130
+ }
131
+ }
132
+
133
+ // 模态框主体内容
134
+ .modalBody {
135
+ padding: 1.5rem;
136
+ font-size: 0.95rem;
137
+ color: var(--vp-c-text-2);
138
+ line-height: 1.7;
139
+ max-height: 60vh;
140
+ overflow-y: auto;
141
+
142
+ // 美化滚动条
143
+ &::-webkit-scrollbar {
144
+ width: 6px;
145
+ }
146
+
147
+ &::-webkit-scrollbar-track {
148
+ background: transparent;
149
+ }
150
+
151
+ &::-webkit-scrollbar-thumb {
152
+ background: var(--vp-c-divider);
153
+ border-radius: 3px;
154
+
155
+ &:hover {
156
+ background: var(--vp-c-text-3);
157
+ }
158
+ }
159
+
160
+ // 优化内容样式
161
+ p {
162
+ margin: 0.75rem 0;
163
+
164
+ &:first-child {
165
+ margin-top: 0;
166
+ }
167
+
168
+ &:last-child {
169
+ margin-bottom: 0;
170
+ }
171
+ }
172
+
173
+ // 如果内容中有图标,增加间距
174
+ :deep(span) {
175
+ margin-right: 0.25rem;
176
+ }
177
+
178
+ // 强调文本
179
+ strong {
180
+ color: var(--vp-c-text-1);
181
+ font-weight: 600;
182
+ }
183
+
184
+ // 链接样式
185
+ a {
186
+ color: var(--vp-c-brand-1);
187
+ text-decoration: none;
188
+ transition: color 0.2s;
189
+
190
+ &:hover {
191
+ color: var(--vp-c-brand-2);
192
+ text-decoration: underline;
193
+ }
194
+ }
195
+ }
196
+
197
+ // 模态框底部(可选)
198
+ .modalFooter {
199
+ padding: 1rem 1.5rem;
200
+ border-top: 1px solid var(--vp-c-divider);
201
+ background: var(--vp-c-bg-soft);
202
+ text-align: right;
203
+ display: flex;
204
+ gap: 0.75rem;
205
+ justify-content: flex-end;
206
+ align-items: center;
207
+ }
208
+
209
+ // 响应式优化
210
+ @media (max-width: 640px) {
211
+ .modal {
212
+ max-width: 100%;
213
+ margin: 1rem;
214
+ border-radius: 12px;
215
+ }
216
+
217
+ .modalHeader {
218
+ padding: 1rem 1.25rem;
219
+ }
220
+
221
+ .modalTitle {
222
+ font-size: 1rem;
223
+ }
224
+
225
+ .modalBody {
226
+ padding: 1.25rem;
227
+ max-height: 50vh;
228
+ }
229
+
230
+ .modalFooter {
231
+ padding: 0.875rem 1.25rem;
232
+ }
233
+ }
@@ -1,105 +1,105 @@
1
- <template>
2
- <!-- 使用 teleport 将 modal 放到 body 之下(避免父容器样式影响) -->
3
- <teleport to="body">
4
- <div v-if="modelValue" :class="$style.modalBackdrop" @click.self="close">
5
- <div
6
- :class="$style.modal"
7
- role="dialog"
8
- :aria-labelledby="titleId"
9
- aria-modal="true"
10
- ref="modalRef"
11
- >
12
- <header :class="$style.modalHeader">
13
- <h3 :id="titleId" :class="$style.modalTitle">
14
- <!-- 标题通过 slot #title 提供,未提供则使用 prop title -->
15
- <slot name="title">
16
- {{ title }}
17
- </slot>
18
- </h3>
19
- <button
20
- :class="$style.closeBtn"
21
- @click="close"
22
- aria-label="Close"
23
- title="Close"
24
- type="button"
25
- >
26
-
27
- </button>
28
- </header>
29
-
30
- <section :class="$style.modalBody">
31
- <!-- 默认插槽用于传入内容 -->
32
- <slot>
33
- <!-- fallback 内容(如果父组件没有传入插槽) -->
34
- <p>No content provided.</p>
35
- </slot>
36
- </section>
37
-
38
- <footer :class="$style.modalFooter" v-if="$slots.footer">
39
- <slot name="footer"></slot>
40
- </footer>
41
- </div>
42
- </div>
43
- </teleport>
44
- </template>
45
-
46
- <script setup>
47
- import { ref, watch, onMounted, onUnmounted } from 'vue'
48
-
49
- const props = defineProps({
50
- modelValue: { type: Boolean, required: true },
51
- title: { type: String, default: '' },
52
- })
53
-
54
- const emit = defineEmits(['update:modelValue', 'close'])
55
-
56
- const modalRef = ref(null)
57
- const titleId = `tm-title-${Math.random().toString(36).slice(2, 8)}`
58
-
59
- // close modal (emit update and close)
60
- function close() {
61
- emit('update:modelValue', false)
62
- emit('close')
63
- }
64
-
65
- // handle ESC to close
66
- function onKeyDown(e) {
67
- if (e.key === 'Escape' || e.key === 'Esc') {
68
- close()
69
- }
70
- }
71
-
72
- // focus trap: basic - focus modal when opened
73
- watch(
74
- () => props.modelValue,
75
- (open) => {
76
- if (open) {
77
- // small timeout to ensure element exists
78
- setTimeout(() => {
79
- if (modalRef.value) modalRef.value.focus && modalRef.value.focus()
80
- }, 0)
81
- document.addEventListener('keydown', onKeyDown)
82
- // prevent body scroll
83
- document.body.style.overflow = 'hidden'
84
- } else {
85
- document.removeEventListener('keydown', onKeyDown)
86
- document.body.style.overflow = ''
87
- }
88
- },
89
- { immediate: false },
90
- )
91
-
92
- onMounted(() => {
93
- if (props.modelValue) {
94
- document.addEventListener('keydown', onKeyDown)
95
- document.body.style.overflow = 'hidden'
96
- }
97
- })
98
-
99
- onUnmounted(() => {
100
- document.removeEventListener('keydown', onKeyDown)
101
- document.body.style.overflow = ''
102
- })
103
- </script>
104
-
105
- <style module src="./AboutModal.module.scss"></style>
1
+ <template>
2
+ <!-- 使用 teleport 将 modal 放到 body 之下(避免父容器样式影响) -->
3
+ <teleport to="body">
4
+ <div v-if="modelValue" :class="$style.modalBackdrop" @click.self="close">
5
+ <div
6
+ :class="$style.modal"
7
+ role="dialog"
8
+ :aria-labelledby="titleId"
9
+ aria-modal="true"
10
+ ref="modalRef"
11
+ >
12
+ <header :class="$style.modalHeader">
13
+ <h3 :id="titleId" :class="$style.modalTitle">
14
+ <!-- 标题通过 slot #title 提供,未提供则使用 prop title -->
15
+ <slot name="title">
16
+ {{ title }}
17
+ </slot>
18
+ </h3>
19
+ <button
20
+ :class="$style.closeBtn"
21
+ @click="close"
22
+ aria-label="Close"
23
+ title="Close"
24
+ type="button"
25
+ >
26
+
27
+ </button>
28
+ </header>
29
+
30
+ <section :class="$style.modalBody">
31
+ <!-- 默认插槽用于传入内容 -->
32
+ <slot>
33
+ <!-- fallback 内容(如果父组件没有传入插槽) -->
34
+ <p>No content provided.</p>
35
+ </slot>
36
+ </section>
37
+
38
+ <footer :class="$style.modalFooter" v-if="$slots.footer">
39
+ <slot name="footer"></slot>
40
+ </footer>
41
+ </div>
42
+ </div>
43
+ </teleport>
44
+ </template>
45
+
46
+ <script setup>
47
+ import { ref, watch, onMounted, onUnmounted } from 'vue'
48
+
49
+ const props = defineProps({
50
+ modelValue: { type: Boolean, required: true },
51
+ title: { type: String, default: '' },
52
+ })
53
+
54
+ const emit = defineEmits(['update:modelValue', 'close'])
55
+
56
+ const modalRef = ref(null)
57
+ const titleId = `tm-title-${Math.random().toString(36).slice(2, 8)}`
58
+
59
+ // close modal (emit update and close)
60
+ function close() {
61
+ emit('update:modelValue', false)
62
+ emit('close')
63
+ }
64
+
65
+ // handle ESC to close
66
+ function onKeyDown(e) {
67
+ if (e.key === 'Escape' || e.key === 'Esc') {
68
+ close()
69
+ }
70
+ }
71
+
72
+ // focus trap: basic - focus modal when opened
73
+ watch(
74
+ () => props.modelValue,
75
+ (open) => {
76
+ if (open) {
77
+ // small timeout to ensure element exists
78
+ setTimeout(() => {
79
+ modalRef.value?.focus?.()
80
+ }, 0)
81
+ document.addEventListener('keydown', onKeyDown)
82
+ // prevent body scroll
83
+ document.body.style.overflow = 'hidden'
84
+ } else {
85
+ document.removeEventListener('keydown', onKeyDown)
86
+ document.body.style.overflow = ''
87
+ }
88
+ },
89
+ { immediate: false },
90
+ )
91
+
92
+ onMounted(() => {
93
+ if (props.modelValue) {
94
+ document.addEventListener('keydown', onKeyDown)
95
+ document.body.style.overflow = 'hidden'
96
+ }
97
+ })
98
+
99
+ onUnmounted(() => {
100
+ document.removeEventListener('keydown', onKeyDown)
101
+ document.body.style.overflow = ''
102
+ })
103
+ </script>
104
+
105
+ <style module src="./AboutModal.module.scss"></style>