Skip to content

Commit d5b23f2

Browse files
committed
增加免责声明 banner 功能
1 parent 2f8ce93 commit d5b23f2

File tree

6 files changed

+267
-2
lines changed

6 files changed

+267
-2
lines changed

example/docs/.vitepress/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ const themeConfig: ThemeContext = {
6363
// HideLastUpdated: true, /* 隐藏最后更新时间 */
6464
// HideAuthors: true, /* 隐藏作者信息 */
6565
// fontsBaseUrl: 'http://localhost:8788', // For local development with wrangler pages dev
66+
disclaimerPaths: [
67+
{
68+
path: '/campus/',
69+
summaryHtml: 'RLE.wiki「大学指南」中的内容,仅供参考。可能存在过时或不准确的信息,请谨慎甄别。',
70+
detailHtml: '<p>RLE.wiki「大学指南」中的内容,仅供参考。可能存在过时或不准确的信息,请谨慎甄别。</p>' +
71+
'<p>「大学指南」板块中的内容,多数来自于读者投稿,并经编辑简单整理和形式审查后登载,主要体现其投稿者主观观点。不代表 RLE.wiki 编辑团队及我们的任何相关维护人员立场。</p>' +
72+
'<p>若存在任何有误或不当内容,请联系 <a href="mailto:rlewiki@project-trans.org">rlewiki@project-trans.org</a>。</p>',
73+
},
74+
],
75+
enableDisclaimer: true,
6676
}
6777

6878
// https://vitepress.dev/reference/site-config

src/components.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import CopyrightInfo from './components/CopyrightInfo.vue'
66
import HomeContent from './components/HomeContent.vue'
77
import PageInfo from './components/PageInfo.vue'
88
import ReadingTime from './components/ReadingTime.vue'
9+
import Disclaimer from './components/Disclaimer.vue'
910

1011
export {
1112
AppearanceToggle,
@@ -16,4 +17,5 @@ export {
1617
HomeContent,
1718
PageInfo,
1819
ReadingTime,
20+
Disclaimer,
1921
}

src/components/AppFooter.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { PjtsThemeConfig } from '../config'
33
import { NolebaseGitChangelog } from '@nolebase/vitepress-plugin-git-changelog/client'
44
import { useData, useRoute } from 'vitepress'
55
import { ref, watch } from 'vue'
6-
import { AppSBox } from '../components'
6+
import { AppSBox, Disclaimer } from '../components'
77
88
const route = useRoute()
99
const { theme } = useData<PjtsThemeConfig>()
@@ -48,5 +48,6 @@ watch(
4848
&& !frontmatter.hideChangelog
4949
"
5050
/>
51+
<Disclaimer />
5152
</div>
5253
</template>

src/components/Disclaimer.vue

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
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>

src/config.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,20 @@ import Components from 'unplugin-vue-components/vite'
1515
import { defineConfigWithTheme } from 'vitepress'
1616

1717
import { generateSidebar } from './sidebar'
18-
import { useThemeContext } from './utils/themeContext'
18+
import { useThemeContext, DisclaimerPathConfig } from './utils/themeContext'
19+
1920

2021
export interface PjtsThemeConfig extends DefaultTheme.Config {
2122
enableChangeLog?: boolean
2223
enableSuggestionBox?: boolean
24+
enableDisclaimer?: boolean
25+
disclaimerPaths?: DisclaimerPathConfig[]
26+
disclaimerSummaryHtml?: string
27+
disclaimerDetailHtml?: string
28+
disclaimerToggleExpandText?: string
29+
disclaimerToggleCollapseText?: string
30+
disclaimerHideText?: string
31+
disclaimerShowText?: string
2332
org?: string
2433
HideReadingTime?: boolean
2534
HideLastUpdated?: boolean
@@ -76,6 +85,8 @@ function genConfig() {
7685
rootDir,
7786
hostName,
7887
fontsBaseUrl = 'https://fonts.project-trans.org',
88+
enableDisclaimer,
89+
disclaimerPaths,
7990
} = themeConfig
8091

8192
return defineConfigWithTheme<PjtsThemeConfig>({
@@ -145,6 +156,8 @@ function genConfig() {
145156
pattern: `${githubRepoLink}/edit/main/${sitePattern}/:path`,
146157
text: '在 GitHub 上编辑此页面', // label localization
147158
},
159+
enableDisclaimer: enableDisclaimer,
160+
disclaimerPaths: disclaimerPaths,
148161
// label localization
149162
outline: { label: '本页大纲', level: 'deep' },
150163
lastUpdated: { text: '最后更新' },

src/utils/themeContext.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import { AsyncLocalStorage } from 'node:async_hooks'
44

55
type NavConfig = DefaultTheme.NavItem[]
66

7+
8+
export interface DisclaimerPathConfig {
9+
path: string; // The path prefix to match
10+
summaryHtml: string; // HTML content for the summary view
11+
detailHtml?: string; // Optional HTML content for the detailed view
12+
}
13+
714
export interface ThemeContext {
815
siteTitle: string
916
siteLogo: string
@@ -24,6 +31,8 @@ export interface ThemeContext {
2431
HideAuthors?: boolean
2532
hostName: string
2633
fontsBaseUrl?: string
34+
enableDisclaimer?: boolean
35+
disclaimerPaths?: DisclaimerPathConfig[]
2736
}
2837

2938
const themeContext = new AsyncLocalStorage<ThemeContext>()

0 commit comments

Comments
 (0)