1
+ <script setup lang="ts">
2
+ import { ref , onMounted , onUnmounted , computed , watch , nextTick } from ' vue'
3
+ import { useData , useRoute } from ' vitepress'
4
+ import type { PjtsThemeConfig , DisclaimerPathConfig } from ' ../config'
5
+
6
+ const props = defineProps <{
7
+ content? : string
8
+ }>()
9
+
10
+ const route = useRoute ()
11
+ const { theme } = useData <PjtsThemeConfig >()
12
+
13
+ const isExpanded = ref (false )
14
+ const isHidden = ref (false )
15
+
16
+ // Ref for the disclaimer element to measure its height
17
+ const disclaimerElement = ref <HTMLElement | null >(null )
18
+ // Ref to store the calculated height
19
+ const disclaimerHeight = ref (0 )
20
+
21
+ // Computed properties for customizable button texts with defaults
22
+ const expandText = computed (() => theme .value ?.disclaimerToggleExpandText || ' 展开' )
23
+ const collapseText = computed (() => theme .value ?.disclaimerToggleCollapseText || ' 收起' )
24
+ const hideText = computed (() => theme .value ?.disclaimerHideText || ' 我知道了' )
25
+ const showText = computed (() => theme .value ?.disclaimerShowText || ' 浮动显示' )
26
+
27
+ // Find the disclaimer configuration matching the current path
28
+ const currentDisclaimerConfig = computed <DisclaimerPathConfig | undefined >(() => {
29
+ if (! theme .value ?.disclaimerPaths || ! Array .isArray (theme .value .disclaimerPaths )) {
30
+ return undefined
31
+ }
32
+ // Find the first config where the route path starts with the config path
33
+ return theme .value .disclaimerPaths .find (config => route .path .startsWith (config .path ))
34
+ })
35
+
36
+ // Computed property to determine if the toggle button should be shown
37
+ const showToggleButton = computed (() => {
38
+ const config = currentDisclaimerConfig .value
39
+ // Show if detailHtml exists and is different from summaryHtml
40
+ return !! config ?.detailHtml && config .detailHtml !== config .summaryHtml
41
+ })
42
+
43
+ // Computed property for the current disclaimer text to display
44
+ const currentDisclaimerText = computed (() => {
45
+ const config = currentDisclaimerConfig .value
46
+ if (! config ) return ' ' // Should not happen if shouldRender is true, but good practice
47
+
48
+ return isExpanded .value || isHidden .value
49
+ ? config .detailHtml ?? config .summaryHtml // Fallback to summary if detail is missing but expanded
50
+ : config .summaryHtml ?? ' '
51
+ })
52
+
53
+ // 检查是否应该显示免责声明
54
+ const shouldRender = computed (() => {
55
+ if (! theme .value ?.enableDisclaimer ) {
56
+ return false
57
+ }
58
+ // if (route.data?.frontmatter?.hideDisclaimer) {
59
+ // return false
60
+ // }
61
+
62
+ // Check if a valid configuration is found for the current path
63
+ return !! currentDisclaimerConfig .value
64
+ })
65
+
66
+ // Computed property for the placeholder's height
67
+ const placeholderHeight = computed (() => {
68
+ // Placeholder should have height only when disclaimer is rendered and *not* hidden
69
+ return shouldRender .value && ! isHidden .value ? disclaimerHeight .value : 0
70
+ })
71
+
72
+ // Toggle the hidden state
73
+ const toggleHiddenState = () => {
74
+ if (isHidden .value ) {
75
+ // Show the disclaimer
76
+ isHidden .value = false
77
+ } else {
78
+ // Hide the disclaimer
79
+ isHidden .value = true
80
+ }
81
+ }
82
+
83
+ // Calculate the height of the disclaimer element
84
+ const calculateHeight = () => {
85
+ // Only calculate height if the disclaimer element exists
86
+ if (disclaimerElement .value ) {
87
+ const currentHeight = disclaimerElement .value .offsetHeight
88
+ // Only update if the disclaimer is collapsed OR if we haven't captured a height yet.
89
+ // This prevents overwriting the collapsed height when it expands.
90
+ if (! isExpanded .value || disclaimerHeight .value === 0 ) {
91
+ disclaimerHeight .value = currentHeight
92
+ }
93
+ }
94
+ // No need for an else to set height to 0, the computed property handles that.
95
+ }
96
+
97
+ onMounted (() => {
98
+ // Calculate initial height after component mounts and renders
99
+ nextTick (calculateHeight )
100
+ })
101
+
102
+ // Watch for changes that might affect the disclaimer's height
103
+ // We watch isExpanded to potentially capture the initial collapsed height if it wasn't ready on mount.
104
+ // We watch currentDisclaimerText because content changes can affect collapsed height.
105
+ watch ([isExpanded , currentDisclaimerText ], () => {
106
+ // Recalculate height after DOM updates
107
+ nextTick (calculateHeight )
108
+ }, {
109
+ flush: ' post' // Ensure calculation happens after DOM updates
110
+ })
111
+ </script >
112
+
113
+ <template >
114
+ <!-- Placeholder div to reserve space when the disclaimer is fixed -->
115
+ <div
116
+ v-if =" shouldRender && !isHidden"
117
+ class =" disclaimer-placeholder"
118
+ :style =" { height: placeholderHeight + 'px' }"
119
+ ></div >
120
+
121
+ <!-- The actual disclaimer element -->
122
+ <div
123
+ v-if =" shouldRender"
124
+ ref =" disclaimerElement"
125
+ :class =" [
126
+ 'disclaimer',
127
+ { 'is-expanded': isExpanded },
128
+ { 'is-hidden': isHidden }
129
+ ]"
130
+ >
131
+ <div class =" disclaimer-content" >
132
+ <!-- Use v-html for rich text content -->
133
+ <div class =" disclaimer-text" :class =" { 'expanded': isExpanded || isHidden }" v-html =" currentDisclaimerText" ></div >
134
+ <div class =" disclaimer-actions" >
135
+ <button v-if =" showToggleButton && !isHidden" @click =" isExpanded = !isExpanded" class =" disclaimer-toggle" >
136
+ {{ isExpanded ? collapseText : expandText }}
137
+ </button >
138
+ <!-- Toggle hidden state and change text accordingly -->
139
+ <button v-if =" !isHidden" @click =" toggleHiddenState" class =" disclaimer-hide" >
140
+ {{ isHidden ? showText : hideText }}
141
+ </button >
142
+ </div >
143
+ </div >
144
+ </div >
145
+ </template >
146
+
147
+ <style scoped>
148
+ .disclaimer {
149
+ position : fixed ;
150
+ bottom : 0 ;
151
+ left : 0 ;
152
+ right : 0 ;
153
+ background : var (--vp-c-bg-soft );
154
+ border-top : 1px solid var (--vp-c-border );
155
+ padding : 1rem ;
156
+ z-index : 100 ;
157
+ }
158
+
159
+ .disclaimer.is-hidden {
160
+ position : static ;
161
+ margin-top : 2rem ;
162
+ }
163
+
164
+ .disclaimer-content {
165
+ max-width : var (--vp-layout-max-width );
166
+ margin : 0 auto ;
167
+ display : flex ;
168
+ justify-content : space-between ;
169
+ align-items : center ;
170
+ gap : 1rem ;
171
+ }
172
+
173
+ .disclaimer-text {
174
+ flex : 1 ;
175
+ font-size : 0.9rem ;
176
+ line-height : 1.5 ;
177
+ max-height : 3em ;
178
+ overflow : hidden ;
179
+ transition : max-height 0.3s ease ;
180
+ }
181
+
182
+ .disclaimer-text.expanded {
183
+ max-height : 300px ; /* Allow more space for rich content */
184
+ }
185
+
186
+ .disclaimer-actions {
187
+ display : flex ;
188
+ gap : 0.5rem ;
189
+ }
190
+
191
+ .disclaimer-toggle ,
192
+ .disclaimer-hide {
193
+ padding : 0.25rem 0.75rem ;
194
+ border : 1px solid var (--vp-c-border );
195
+ border-radius : 4px ;
196
+ background : var (--vp-c-bg );
197
+ color : var (--vp-c-text-1 );
198
+ cursor : pointer ;
199
+ font-size : 0.9rem ;
200
+ transition : all 0.2s ease ;
201
+ }
202
+
203
+ .disclaimer-toggle :hover ,
204
+ .disclaimer-hide :hover {
205
+ background : var (--vp-c-bg-soft );
206
+ border-color : var (--vp-c-brand );
207
+ }
208
+
209
+ @media (max-width : 768px ) {
210
+ .disclaimer-content {
211
+ flex-direction : column ;
212
+ align-items : flex-start ;
213
+ }
214
+
215
+ .disclaimer-actions {
216
+ margin-top : 0.5rem ; /* Add some space when stacked */
217
+ width : 100% ;
218
+ justify-content : flex-end ;
219
+ }
220
+ }
221
+
222
+ /* Style for the placeholder - ensure it doesn't add extra margins/padding */
223
+ .disclaimer-placeholder {
224
+ position : relative ;
225
+ width : 100% ;
226
+ display : block ;
227
+ margin : 0 ;
228
+ padding : 0 ;
229
+ }
230
+ </style >
0 commit comments