vitepress-plugin-announcement 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 sugar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # vitepress-plugin-announcement
2
+
3
+ [English](https://github.com/ATQQ/sugar-blog/blob/master/packages/vitepress-plugin-announcement/README-en.md) | 简体中文
4
+
5
+ ## 使用
6
+ 安装依赖 `pnpm/npm/yarn`
7
+ ```sh
8
+ pnpm add vitepress-plugin-announcement
9
+ ```
10
+
11
+ 引入插件在 `.vitepress/config.ts` 配置文件中
12
+
13
+ ```ts
14
+ import { defineConfig } from 'vitepress'
15
+ import { AnnouncementPlugin } from 'vitepress-plugin-announcement'
16
+
17
+ export default defineConfig({
18
+ vite: {
19
+ // ↓↓↓↓↓
20
+ plugins: [
21
+ AnnouncementPlugin({
22
+ title: '公告',
23
+ body: [
24
+ { type: 'text', content: '下方插入了二维码' },
25
+ {
26
+ type: 'image',
27
+ src: 'https://img.cdn.sugarat.top/mdImg/MTYxNTAxODc2NTIxMA==615018765210~fmt.webp'
28
+ },
29
+ {
30
+ type: 'button',
31
+ content: '作者博客',
32
+ link: 'https://sugarat.top'
33
+ },
34
+ ],
35
+ footer: [
36
+ {
37
+ type: 'text',
38
+ content: '底部内容'
39
+ },
40
+ ],
41
+ })
42
+ ]
43
+ // ↑↑↑↑↑
44
+ }
45
+ })
46
+ ```
47
+
48
+ ## 更多用法
49
+ ### 关闭后不再弹出
50
+ *tip:下次配置修改后再展示*
51
+ ```js
52
+ AnnouncementPlugin({
53
+ duration: -1
54
+ })
55
+ ```
56
+
57
+ ### 不展示再次打开按钮
58
+ ```js
59
+ AnnouncementPlugin({
60
+ reopen: false
61
+ })
62
+ ```
63
+
64
+ ### 引导闪烁
65
+
66
+ ```js
67
+ AnnouncementPlugin({
68
+ twinkle: true
69
+ })
70
+ ```
71
+
72
+ ## 完整配置
73
+ ```ts
74
+ import type { Ref } from 'vue'
75
+ import type { Route } from 'vitepress'
76
+
77
+ export interface AnnouncementOptions {
78
+ /**
79
+ * 公告标题
80
+ */
81
+ title: string
82
+ /**
83
+ * 公告主要内容
84
+ */
85
+ body?: Announcement.Value[]
86
+ /**
87
+ * 公告底部内容
88
+ */
89
+ footer?: Announcement.Value[]
90
+
91
+ /**
92
+ * 是否只在浏览器环境渲染组件
93
+ * @default false
94
+ * @doc https://vitepress.dev/guide/ssr-compat#clientonly
95
+ */
96
+ clientOnly?: boolean
97
+
98
+ /**
99
+ * 展示时机控制
100
+ *
101
+ * -1 只展示1次;>= 0 每次都展示,一定时间后自动消失,0 不自动消失
102
+ *
103
+ * 配置发生改变时,会重新触发此规则
104
+ * @default 0
105
+ */
106
+ duration?: number
107
+
108
+ /**
109
+ * 移动端自动最小化
110
+ * @default false
111
+ */
112
+ mobileMinify?: boolean
113
+
114
+ /**
115
+ * 支持重新打开(右上角 icon 悬浮)
116
+ * @default true
117
+ */
118
+ reopen?: boolean
119
+
120
+ /**
121
+ * 是否打开闪烁提示,通常需要和 reopen 搭配使用
122
+ * @default false
123
+ */
124
+ twinkle?: boolean
125
+
126
+ /**
127
+ * 设置展示图标,svg
128
+ */
129
+ icon?: string
130
+
131
+ /**
132
+ * 设置关闭图标,svg
133
+ */
134
+ closeIcon?: string
135
+
136
+ /**
137
+ * 自定义展示策略
138
+ * @param to 切换到的目标路由
139
+ */
140
+ onRouteChanged?: (to: Route, show: Ref<boolean>) => void
141
+ }
142
+
143
+ export declare namespace Announcement {
144
+ export interface Title {
145
+ type: 'title'
146
+ content: string
147
+ style?: string
148
+ }
149
+
150
+ export interface Text {
151
+ type: 'text'
152
+ content: string
153
+ style?: string
154
+ }
155
+
156
+ export interface Image {
157
+ type: 'image'
158
+ src: string
159
+ style?: string
160
+ }
161
+
162
+ export interface Button {
163
+ type: 'button'
164
+ link: string
165
+ content: string
166
+ style?: string
167
+ props?: any
168
+ }
169
+
170
+ export type Value = Title | Text | Image | Button
171
+ }
172
+ ```
173
+
174
+ ## Thanks
175
+ 样式参考了 [reco-1.x 主题](https://github.com/vuepress-reco/vuepress-theme-reco-1.x) 中的 [@vuepress-reco/vuepress-plugin-bulletin-popover](https://github.com/vuepress-reco/vuepress-theme-reco-1.x/tree/master/packages/%40vuepress-reco/vuepress-plugin-bulletin-popover) 插件
@@ -0,0 +1,185 @@
1
+ <script lang="ts" setup>
2
+ import { computed, h, onMounted, ref, watch } from 'vue'
3
+ import { useRoute, useRouter } from 'vitepress'
4
+ import type { Announcement, AnnouncementOptions } from 'vitepress-plugin-announcement'
5
+
6
+ // @ts-expect-error
7
+ import announcementOptions from 'virtual:announcement-options'
8
+ import AnnouncementButton from './AnnouncementButton.vue'
9
+ import AnnouncementIcon from './AnnouncementIcon.vue'
10
+ import { inBrowser, parseStringStyle, useDebounceFn, useWindowSize } from './util'
11
+
12
+ const popoverProps: AnnouncementOptions = announcementOptions
13
+
14
+ const show = ref((popoverProps?.duration ?? 0) >= 0)
15
+
16
+ const bodyContent = computed(() => {
17
+ return popoverProps?.body || []
18
+ })
19
+
20
+ const footerContent = computed(() => {
21
+ return popoverProps?.footer || []
22
+ })
23
+ const storageKey = 'vitepress-plugin-announcement'
24
+ const closeFlag = `${storageKey}-close`
25
+
26
+ // 移动端最小化
27
+ const { width } = useWindowSize()
28
+ const router = useRouter()
29
+ const route = useRoute()
30
+ onMounted(() => {
31
+ if (!popoverProps?.title) {
32
+ return
33
+ }
34
+
35
+ if (!inBrowser) {
36
+ return
37
+ }
38
+
39
+ // 取旧值
40
+ const oldValue = localStorage.getItem(storageKey)
41
+ const newValue = JSON.stringify(popoverProps)
42
+ localStorage.setItem(storageKey, newValue)
43
+
44
+ // 移动端最小化
45
+ if (width.value < 768 && popoverProps?.mobileMinify) {
46
+ show.value = false
47
+ return
48
+ }
49
+
50
+ // >= 0 每次都展示,区别是否自动消失
51
+ if (Number(popoverProps?.duration ?? '') >= 0) {
52
+ show.value = true
53
+ if (popoverProps?.duration) {
54
+ setTimeout(() => {
55
+ show.value = false
56
+ }, popoverProps?.duration)
57
+ }
58
+ return
59
+ }
60
+
61
+ if (oldValue !== newValue && popoverProps?.duration === -1) {
62
+ // 当做新值处理
63
+ show.value = true
64
+ localStorage.removeItem(closeFlag)
65
+ return
66
+ }
67
+
68
+ // 新旧相等,判断是否点击过close,没点击关闭依然展示
69
+ if (oldValue === newValue && popoverProps?.duration === -1 && !localStorage.getItem(closeFlag)) {
70
+ show.value = true
71
+ }
72
+ })
73
+
74
+ const onAfterRouteChanged = useDebounceFn(() => {
75
+ popoverProps?.onRouteChanged?.(route, show)
76
+ }, 10)
77
+
78
+ watch(route, onAfterRouteChanged, { immediate: true })
79
+
80
+ function handleClose() {
81
+ show.value = false
82
+ if (popoverProps?.duration === -1) {
83
+ localStorage.setItem(closeFlag, `${+new Date()}`)
84
+ }
85
+ }
86
+
87
+ function PopoverValue(props: { key: number; item: Announcement.Value },
88
+ { slots }: any) {
89
+ const { key, item } = props
90
+ if (item.type === 'title') {
91
+ return h(
92
+ 'h4',
93
+ {
94
+ style: parseStringStyle(item.style || '')
95
+ },
96
+ item.content
97
+ )
98
+ }
99
+ if (item.type === 'text') {
100
+ return h(
101
+ 'p',
102
+ {
103
+ style: parseStringStyle(item.style || '')
104
+ },
105
+ item.content
106
+ )
107
+ }
108
+ if (item.type === 'image') {
109
+ return h('img', {
110
+ src: item.src,
111
+ style: parseStringStyle(item.style || '')
112
+ })
113
+ }
114
+ if (item.type === 'button') {
115
+ return h(
116
+ AnnouncementButton,
117
+ {
118
+ type: 'primary',
119
+ onClick: () => {
120
+ if (/^\s*http(s)?:\/\//.test(item.link)) {
121
+ window.open(item.link)
122
+ }
123
+ else {
124
+ router.go(item.link)
125
+ }
126
+ },
127
+ style: parseStringStyle(item.style || ''),
128
+ ...item.props
129
+ },
130
+ slots
131
+ )
132
+ }
133
+ return h(
134
+ 'div',
135
+ {
136
+ key
137
+ },
138
+ ''
139
+ )
140
+ }
141
+
142
+ const showReopen = computed(() => {
143
+ return !show.value && (popoverProps?.reopen ?? true) && !!popoverProps.title
144
+ })
145
+ </script>
146
+
147
+ <template>
148
+ <div v-show="show" class="theme-blog-popover" data-pagefind-ignore="all">
149
+ <div class="header">
150
+ <div class="title-wrapper">
151
+ <AnnouncementIcon size="20px" :icon="popoverProps.icon">
152
+ <svg t="1716085184855" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4274" width="200" height="200"><path d="M660.48 872.448q6.144 0-3.584 15.36t-29.696 33.792-47.104 33.792-57.856 15.36q-27.648 0-53.248-15.36t-45.056-33.792-29.696-33.792-6.144-15.36l272.384 0zM914.432 785.408q7.168 9.216 6.656 17.92t-4.608 14.848-10.24 9.728-12.288 3.584l-747.52 0q-14.336 0-20.992-11.776t4.608-29.184q17.408-30.72 40.96-68.608t44.544-81.408 36.352-92.16 15.36-101.888q0-51.2 14.336-92.16t37.376-71.68 53.248-52.224 62.976-32.768q-16.384-26.624-16.384-55.296 0-41.984 28.672-70.656t70.656-28.672 70.656 28.672 28.672 70.656q0 14.336-4.096 28.16t-11.264 25.088q34.816 11.264 66.048 32.768t54.272 53.248 36.864 72.704 13.824 91.136q0 51.2 15.36 100.864t36.864 94.208 45.568 81.408 43.52 63.488zM478.208 142.336q0 16.384 11.264 28.16t27.648 11.776l2.048 0q16.384-1.024 27.648-12.288t11.264-27.648q0-17.408-11.264-28.672t-28.672-11.264-28.672 11.264-11.264 28.672z" p-id="4275" /></svg>
153
+ </AnnouncementIcon>
154
+ <span class="title">{{ popoverProps?.title }}</span>
155
+ </div>
156
+ <AnnouncementIcon class="close-icon" size="20px" :icon="popoverProps?.closeIcon" @click="handleClose">
157
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 393.664L407.936 353.6a38.4 38.4 0 1 0-54.336 54.336L457.664 512 353.6 616.064a38.4 38.4 0 1 0 54.336 54.336L512 566.336 616.064 670.4a38.4 38.4 0 1 0 54.336-54.336L566.336 512 670.4 407.936a38.4 38.4 0 1 0-54.336-54.336z" /></svg>
158
+ </AnnouncementIcon>
159
+ </div>
160
+ <div v-if="bodyContent.length" class="body content">
161
+ <PopoverValue v-for="(v, idx) in bodyContent" :key="idx" :item="v">
162
+ {{ v.type !== 'image' ? v.content : '' }}
163
+ </PopoverValue>
164
+ <hr v-if="footerContent.length">
165
+ </div>
166
+ <div class="footer content">
167
+ <PopoverValue v-for="(v, idx) in footerContent" :key="idx" :item="v">
168
+ {{ v.type !== 'image' ? v.content : '' }}
169
+ </PopoverValue>
170
+ </div>
171
+ </div>
172
+ <div
173
+ v-show="showReopen" class="theme-blog-popover-close"
174
+ :class="{ twinkle: popoverProps?.twinkle }"
175
+ @click="show = true"
176
+ >
177
+ <AnnouncementIcon :icon="popoverProps?.icon">
178
+ <svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4274" width="200" height="200"><path d="M660.48 872.448q6.144 0-3.584 15.36t-29.696 33.792-47.104 33.792-57.856 15.36q-27.648 0-53.248-15.36t-45.056-33.792-29.696-33.792-6.144-15.36l272.384 0zM914.432 785.408q7.168 9.216 6.656 17.92t-4.608 14.848-10.24 9.728-12.288 3.584l-747.52 0q-14.336 0-20.992-11.776t4.608-29.184q17.408-30.72 40.96-68.608t44.544-81.408 36.352-92.16 15.36-101.888q0-51.2 14.336-92.16t37.376-71.68 53.248-52.224 62.976-32.768q-16.384-26.624-16.384-55.296 0-41.984 28.672-70.656t70.656-28.672 70.656 28.672 28.672 70.656q0 14.336-4.096 28.16t-11.264 25.088q34.816 11.264 66.048 32.768t54.272 53.248 36.864 72.704 13.824 91.136q0 51.2 15.36 100.864t36.864 94.208 45.568 81.408 43.52 63.488zM478.208 142.336q0 16.384 11.264 28.16t27.648 11.776l2.048 0q16.384-1.024 27.648-12.288t11.264-27.648q0-17.408-11.264-28.672t-28.672-11.264-28.672 11.264-11.264 28.672z" p-id="4275" /></svg>
179
+ </AnnouncementIcon>
180
+ </div>
181
+ </template>
182
+
183
+ <style lang="css" scoped>
184
+ @import url(./style.css);
185
+ </style>
@@ -0,0 +1,85 @@
1
+ <script lang="ts" setup>
2
+ import { computed } from 'vue'
3
+
4
+ const props = defineProps<{
5
+ type: 'primary' | 'success' | 'danger' | 'warning' | 'info' | 'text' | 'default'
6
+ onClick?: () => void
7
+ style?: Record<string, any>
8
+ }>()
9
+
10
+ const type = computed(() => props.type ?? 'primary')
11
+ </script>
12
+
13
+ <template>
14
+ <button class="announcement-button" :class="`announcement-button-${type}`" @click="props.onClick">
15
+ <span>
16
+ <slot />
17
+ </span>
18
+ </button>
19
+ </template>
20
+
21
+ <style lang="css" scoped>
22
+ .announcement-button+.announcement-button{
23
+ margin-left: 12px;
24
+ }
25
+ .announcement-button{
26
+ --bgc: #409eff;
27
+ --bc:var(--bgc);
28
+ --c: #fff;
29
+ }
30
+ .announcement-button {
31
+ align-items: center;
32
+ -webkit-appearance: none;
33
+ background-color: var(--bgc);
34
+ border: 1px solid #dcdfe6;
35
+ border-color: var(--bc);
36
+ border-radius: 4px;
37
+ box-sizing: border-box;
38
+ color: var(--c);
39
+ cursor: pointer;
40
+ display: inline-flex;
41
+ font-size: 14px;
42
+ font-weight: 500;
43
+ height: 32px;
44
+ justify-content: center;
45
+ line-height: 1;
46
+ outline: none;
47
+ padding: 8px 15px;
48
+ text-align: center;
49
+ transition: .1s;
50
+ -webkit-user-select: none;
51
+ -moz-user-select: none;
52
+ -ms-user-select: none;
53
+ user-select: none;
54
+ vertical-align: middle;
55
+ white-space: nowrap;
56
+ }
57
+
58
+ .announcement-button-primary {
59
+ --bgc: var(--vp-c-brand-2);
60
+ }
61
+ .announcement-button-success {
62
+ --bgc: #67c23a;
63
+ }
64
+ .announcement-button-danger {
65
+ --bgc: #f56c6c;
66
+ }
67
+ .announcement-button-warning {
68
+ --bgc: #e6a23c;
69
+ }
70
+ .announcement-button-info {
71
+ --bgc: #909399;
72
+ }
73
+ .announcement-button-text {
74
+ --bgc: transparent;
75
+ --bc: transparent;
76
+ --c: var(--vp-c-brand-2);
77
+ padding-left: 0;
78
+ padding-right: 0;
79
+ }
80
+ .announcement-button-default {
81
+ --bgc: #fff;
82
+ --bc: #dcdfe6;
83
+ --c: #606266;
84
+ }
85
+ </style>
@@ -0,0 +1,33 @@
1
+ <script lang="ts" setup>
2
+ import { computed } from 'vue'
3
+
4
+ const props = defineProps<{
5
+ size?: string
6
+ icon?: string
7
+ }>()
8
+
9
+ const size = computed(() => props.size && (typeof props.size === 'number' ? `${props.size}px` : props.size))
10
+ </script>
11
+
12
+ <template>
13
+ <i v-if="props.icon" class="announcement-icon" :style="{ fontSize: size }" v-html="props.icon" />
14
+ <i v-else class="announcement-icon" :style="{ fontSize: size }">
15
+ <slot />
16
+ </i>
17
+ </template>
18
+
19
+ <style lang="css" scoped>
20
+ .announcement-icon {
21
+ --color: inherit;
22
+ align-items: center;
23
+ display: inline-flex;
24
+ height: 1em;
25
+ justify-content: center;
26
+ line-height: 1em;
27
+ position: relative;
28
+ width: 1em;
29
+ fill: currentColor;
30
+ color: var(--color);
31
+ font-size: inherit;
32
+ }
33
+ </style>
@@ -0,0 +1,3 @@
1
+ export declare const vOuterHtml: {
2
+ mounted: (el: HTMLElement, binding: any) => void;
3
+ };
@@ -0,0 +1,5 @@
1
+ export const vOuterHtml = {
2
+ mounted: (el, binding) => {
3
+ el.outerHTML = binding.value;
4
+ }
5
+ };
@@ -0,0 +1,103 @@
1
+ .theme-blog-popover {
2
+ width: 258px;
3
+ position: fixed;
4
+ top: 80px;
5
+ right: 20px;
6
+ z-index: 22;
7
+ box-sizing: border-box;
8
+ border: 1px solid var(--vp-c-brand-3);
9
+ border-radius: 6px;
10
+ background-color: rgba(var(--bg-gradient-home));
11
+ box-shadow: var(--box-shadow);
12
+ }
13
+
14
+ .header {
15
+ background-color: var(--vp-c-brand-3);
16
+ color: #fff;
17
+ padding: 6px 4px;
18
+ display: flex;
19
+ justify-content: space-between;
20
+ align-items: center;
21
+ }
22
+ .header .close-icon {
23
+ cursor: pointer;
24
+ }
25
+
26
+ .title-wrapper {
27
+ display: flex;
28
+ align-items: center;
29
+ }
30
+ .title-wrapper .title {
31
+ font-size: 14px;
32
+ padding-left: 6px;
33
+ }
34
+
35
+ .body {
36
+ box-sizing: border-box;
37
+ padding: 10px 10px 0;
38
+ }
39
+ .body hr {
40
+ border: none;
41
+ border-bottom: 1px solid #eaecef;
42
+ }
43
+
44
+ .footer {
45
+ box-sizing: border-box;
46
+ padding: 10px;
47
+ }
48
+
49
+ .body.content,
50
+ .footer.content {
51
+ text-align: center;
52
+ }
53
+ .body.content h4,
54
+ .footer.content h4 {
55
+ text-align: center;
56
+ font-size: 12px;
57
+ }
58
+ .body.content p,
59
+ .footer.content p {
60
+ text-align: center;
61
+ padding: 10px 0;
62
+ font-size: 14px;
63
+ }
64
+ .body.content img,
65
+ .footer.content img {
66
+ width: 100%;
67
+ height: 100px;
68
+ object-fit: contain;
69
+ margin: 0 auto;
70
+ }
71
+
72
+ .theme-blog-popover-close {
73
+ cursor: pointer;
74
+ opacity: 0.5;
75
+ position: fixed;
76
+ z-index: 22;
77
+ top: 80px;
78
+ right: 10px;
79
+ position: fixed;
80
+ background-color: var(--vp-c-brand-3);
81
+ padding: 8px;
82
+ color: #fff;
83
+ font-size: 12px;
84
+ border-radius: 50%;
85
+ display: flex;
86
+ flex-direction: column;
87
+ }
88
+
89
+ .theme-blog-popover-close.twinkle {
90
+ animation: twinkle 1s ease-in-out infinite;
91
+ }
92
+
93
+ @keyframes twinkle {
94
+ 0% {
95
+ opacity: 0.5;
96
+ }
97
+ 50% {
98
+ opacity: 0;
99
+ }
100
+ 100% {
101
+ opacity: 0.5;
102
+ }
103
+ }
@@ -0,0 +1,7 @@
1
+ export declare function parseStringStyle(cssText: string): Record<string, string | number>;
2
+ export declare function useDebounceFn<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: any[]) => void;
3
+ export declare const inBrowser: boolean;
4
+ export declare function useWindowSize(): {
5
+ width: import("vue").Ref<number, number>;
6
+ height: import("vue").Ref<number, number>;
7
+ };
@@ -0,0 +1,46 @@
1
+ import { onMounted, onUnmounted, ref } from 'vue';
2
+ const listDelimiterRE = /;(?![^(]*\))/g;
3
+ const propertyDelimiterRE = /:([^]+)/;
4
+ const styleCommentRE = /\/\*[^]*?\*\//g;
5
+ export function parseStringStyle(cssText) {
6
+ const ret = {};
7
+ cssText.replace(styleCommentRE, '').split(listDelimiterRE).forEach((item) => {
8
+ if (item) {
9
+ const tmp = item.split(propertyDelimiterRE);
10
+ tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim());
11
+ }
12
+ });
13
+ return ret;
14
+ }
15
+ export function useDebounceFn(fn, delay) {
16
+ let timer;
17
+ return (...args) => {
18
+ clearTimeout(timer);
19
+ timer = setTimeout(() => {
20
+ fn(...args);
21
+ }, delay);
22
+ };
23
+ }
24
+ export const inBrowser = typeof document !== 'undefined';
25
+ export function useWindowSize() {
26
+ const width = ref(Number.POSITIVE_INFINITY);
27
+ const height = ref(Number.POSITIVE_INFINITY);
28
+ const updateSize = useDebounceFn(() => {
29
+ if (inBrowser) {
30
+ width.value = window.innerWidth;
31
+ height.value = window.innerHeight;
32
+ }
33
+ }, 100);
34
+ onMounted(() => {
35
+ if (inBrowser) {
36
+ window.addEventListener('resize', updateSize, { passive: true });
37
+ }
38
+ });
39
+ onUnmounted(() => {
40
+ if (inBrowser) {
41
+ window.removeEventListener('resize', updateSize);
42
+ }
43
+ });
44
+ updateSize();
45
+ return { width, height };
46
+ }
@@ -0,0 +1,89 @@
1
+ import { Ref } from 'vue';
2
+ import { Route } from 'vitepress';
3
+
4
+ interface AnnouncementOptions {
5
+ /**
6
+ * 公告标题
7
+ */
8
+ title: string;
9
+ /**
10
+ * 公告主要内容
11
+ */
12
+ body?: Announcement.Value[];
13
+ /**
14
+ * 公告底部内容
15
+ */
16
+ footer?: Announcement.Value[];
17
+ /**
18
+ * 是否只在浏览器环境渲染组件
19
+ * @default false
20
+ * @doc https://vitepress.dev/guide/ssr-compat#clientonly
21
+ */
22
+ clientOnly?: boolean;
23
+ /**
24
+ * 展示时机控制
25
+ *
26
+ * -1 只展示1次;>= 0 每次都展示,一定时间后自动消失,0 不自动消失
27
+ *
28
+ * 配置发生改变时,会重新触发此规则
29
+ * @default 0
30
+ */
31
+ duration?: number;
32
+ /**
33
+ * 移动端自动最小化
34
+ * @default false
35
+ */
36
+ mobileMinify?: boolean;
37
+ /**
38
+ * 支持重新打开(右上角 icon 悬浮)
39
+ * @default true
40
+ */
41
+ reopen?: boolean;
42
+ /**
43
+ * 是否打开闪烁提示,通常需要和 reopen 搭配使用
44
+ * @default false
45
+ */
46
+ twinkle?: boolean;
47
+ /**
48
+ * 设置展示图标,svg
49
+ */
50
+ icon?: string;
51
+ /**
52
+ * 设置关闭图标,svg
53
+ */
54
+ closeIcon?: string;
55
+ /**
56
+ * 自定义展示策略
57
+ * @param to 切换到的目标路由
58
+ */
59
+ onRouteChanged?: (to: Route, show: Ref<boolean>) => void;
60
+ }
61
+ declare namespace Announcement {
62
+ interface Title {
63
+ type: 'title';
64
+ content: string;
65
+ style?: string;
66
+ }
67
+ interface Text {
68
+ type: 'text';
69
+ content: string;
70
+ style?: string;
71
+ }
72
+ interface Image {
73
+ type: 'image';
74
+ src: string;
75
+ style?: string;
76
+ }
77
+ interface Button {
78
+ type: 'button';
79
+ link: string;
80
+ content: string;
81
+ style?: string;
82
+ props?: any;
83
+ }
84
+ type Value = Title | Text | Image | Button;
85
+ }
86
+
87
+ declare function AnnouncementPlugin(options: AnnouncementOptions): any;
88
+
89
+ export { Announcement, AnnouncementOptions, AnnouncementPlugin };
@@ -0,0 +1,89 @@
1
+ import { Ref } from 'vue';
2
+ import { Route } from 'vitepress';
3
+
4
+ interface AnnouncementOptions {
5
+ /**
6
+ * 公告标题
7
+ */
8
+ title: string;
9
+ /**
10
+ * 公告主要内容
11
+ */
12
+ body?: Announcement.Value[];
13
+ /**
14
+ * 公告底部内容
15
+ */
16
+ footer?: Announcement.Value[];
17
+ /**
18
+ * 是否只在浏览器环境渲染组件
19
+ * @default false
20
+ * @doc https://vitepress.dev/guide/ssr-compat#clientonly
21
+ */
22
+ clientOnly?: boolean;
23
+ /**
24
+ * 展示时机控制
25
+ *
26
+ * -1 只展示1次;>= 0 每次都展示,一定时间后自动消失,0 不自动消失
27
+ *
28
+ * 配置发生改变时,会重新触发此规则
29
+ * @default 0
30
+ */
31
+ duration?: number;
32
+ /**
33
+ * 移动端自动最小化
34
+ * @default false
35
+ */
36
+ mobileMinify?: boolean;
37
+ /**
38
+ * 支持重新打开(右上角 icon 悬浮)
39
+ * @default true
40
+ */
41
+ reopen?: boolean;
42
+ /**
43
+ * 是否打开闪烁提示,通常需要和 reopen 搭配使用
44
+ * @default false
45
+ */
46
+ twinkle?: boolean;
47
+ /**
48
+ * 设置展示图标,svg
49
+ */
50
+ icon?: string;
51
+ /**
52
+ * 设置关闭图标,svg
53
+ */
54
+ closeIcon?: string;
55
+ /**
56
+ * 自定义展示策略
57
+ * @param to 切换到的目标路由
58
+ */
59
+ onRouteChanged?: (to: Route, show: Ref<boolean>) => void;
60
+ }
61
+ declare namespace Announcement {
62
+ interface Title {
63
+ type: 'title';
64
+ content: string;
65
+ style?: string;
66
+ }
67
+ interface Text {
68
+ type: 'text';
69
+ content: string;
70
+ style?: string;
71
+ }
72
+ interface Image {
73
+ type: 'image';
74
+ src: string;
75
+ style?: string;
76
+ }
77
+ interface Button {
78
+ type: 'button';
79
+ link: string;
80
+ content: string;
81
+ style?: string;
82
+ props?: any;
83
+ }
84
+ type Value = Title | Text | Image | Button;
85
+ }
86
+
87
+ declare function AnnouncementPlugin(options: AnnouncementOptions): any;
88
+
89
+ export { Announcement, AnnouncementOptions, AnnouncementPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ AnnouncementPlugin: () => AnnouncementPlugin
34
+ });
35
+ module.exports = __toCommonJS(src_exports);
36
+ var import_node_path = __toESM(require("path"));
37
+ var import_node_url = require("url");
38
+ var import_javascript_stringify = require("javascript-stringify");
39
+ var import_meta = {};
40
+ function isESM() {
41
+ return typeof __filename === "undefined" || typeof __dirname === "undefined";
42
+ }
43
+ function getDirname() {
44
+ return isESM() ? import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)) : __dirname;
45
+ }
46
+ var componentName = "Announcement";
47
+ var componentFile = `${componentName}.vue`;
48
+ var aliasComponentFile = `${getDirname()}/components/${componentFile}`;
49
+ var virtualModuleId = "virtual:announcement-options";
50
+ var resolvedVirtualModuleId = `\0${virtualModuleId}`;
51
+ function AnnouncementPlugin(options) {
52
+ const componentOptions = {
53
+ clientOnly: false,
54
+ duration: 0,
55
+ mobileMinify: false,
56
+ reopen: true,
57
+ twinkle: false,
58
+ ...options
59
+ };
60
+ const pluginOps = {
61
+ name: "vitepress-plugin-announcement",
62
+ enforce: "pre",
63
+ config: () => {
64
+ return {
65
+ resolve: {
66
+ alias: {
67
+ [`./${componentFile}`]: aliasComponentFile
68
+ }
69
+ }
70
+ };
71
+ },
72
+ transform(code, id) {
73
+ if (id.endsWith("vitepress/dist/client/theme-default/Layout.vue")) {
74
+ const slotPosition = '<slot name="layout-top" />';
75
+ let transformResult = code.replace(slotPosition, `${slotPosition}<Announcement/>`);
76
+ if (componentOptions.clientOnly) {
77
+ transformResult = transformResult.replace("<Announcement/>", "<ClientOnly><Announcement/></ClientOnly>");
78
+ }
79
+ const setupPosition = '<script setup lang="ts">';
80
+ transformResult = transformResult.replace(setupPosition, `${setupPosition}
81
+ import Announcement from './Announcement.vue'`);
82
+ return transformResult;
83
+ }
84
+ },
85
+ resolveId(id) {
86
+ if (id === virtualModuleId) {
87
+ return resolvedVirtualModuleId;
88
+ }
89
+ },
90
+ load(id) {
91
+ if (id === resolvedVirtualModuleId) {
92
+ return `export default ${(0, import_javascript_stringify.stringify)(options)}`;
93
+ }
94
+ }
95
+ };
96
+ return pluginOps;
97
+ }
98
+ // Annotate the CommonJS export names for ESM import in node:
99
+ 0 && (module.exports = {
100
+ AnnouncementPlugin
101
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,65 @@
1
+ // src/index.ts
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { stringify } from "javascript-stringify";
5
+ function isESM() {
6
+ return typeof __filename === "undefined" || typeof __dirname === "undefined";
7
+ }
8
+ function getDirname() {
9
+ return isESM() ? path.dirname(fileURLToPath(import.meta.url)) : __dirname;
10
+ }
11
+ var componentName = "Announcement";
12
+ var componentFile = `${componentName}.vue`;
13
+ var aliasComponentFile = `${getDirname()}/components/${componentFile}`;
14
+ var virtualModuleId = "virtual:announcement-options";
15
+ var resolvedVirtualModuleId = `\0${virtualModuleId}`;
16
+ function AnnouncementPlugin(options) {
17
+ const componentOptions = {
18
+ clientOnly: false,
19
+ duration: 0,
20
+ mobileMinify: false,
21
+ reopen: true,
22
+ twinkle: false,
23
+ ...options
24
+ };
25
+ const pluginOps = {
26
+ name: "vitepress-plugin-announcement",
27
+ enforce: "pre",
28
+ config: () => {
29
+ return {
30
+ resolve: {
31
+ alias: {
32
+ [`./${componentFile}`]: aliasComponentFile
33
+ }
34
+ }
35
+ };
36
+ },
37
+ transform(code, id) {
38
+ if (id.endsWith("vitepress/dist/client/theme-default/Layout.vue")) {
39
+ const slotPosition = '<slot name="layout-top" />';
40
+ let transformResult = code.replace(slotPosition, `${slotPosition}<Announcement/>`);
41
+ if (componentOptions.clientOnly) {
42
+ transformResult = transformResult.replace("<Announcement/>", "<ClientOnly><Announcement/></ClientOnly>");
43
+ }
44
+ const setupPosition = '<script setup lang="ts">';
45
+ transformResult = transformResult.replace(setupPosition, `${setupPosition}
46
+ import Announcement from './Announcement.vue'`);
47
+ return transformResult;
48
+ }
49
+ },
50
+ resolveId(id) {
51
+ if (id === virtualModuleId) {
52
+ return resolvedVirtualModuleId;
53
+ }
54
+ },
55
+ load(id) {
56
+ if (id === resolvedVirtualModuleId) {
57
+ return `export default ${stringify(options)}`;
58
+ }
59
+ }
60
+ };
61
+ return pluginOps;
62
+ }
63
+ export {
64
+ AnnouncementPlugin
65
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "vitepress-plugin-announcement",
3
+ "version": "0.1.0",
4
+ "description": "vitepress plugin, Announcement, 公告窗口",
5
+ "author": "sugar",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/ATQQ/sugar-blog/tree/master/packages/vitepress-plugin-announcement",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/ATQQ/sugar-blog.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/ATQQ/sugar-blog/issues"
14
+ },
15
+ "keywords": [
16
+ "vitepress",
17
+ "plugin",
18
+ "announcement",
19
+ "公告窗口",
20
+ "公告"
21
+ ],
22
+ "exports": {
23
+ ".": {
24
+ "import": "./dist/index.mjs",
25
+ "require": "./dist/index.js"
26
+ }
27
+ },
28
+ "main": "dist/index.js",
29
+ "module": "dist/index.mjs",
30
+ "types": "dist/index.d.ts",
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "peerDependencies": {
35
+ "vitepress": "^1"
36
+ },
37
+ "dependencies": {
38
+ "javascript-stringify": "^2.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "chokidar": "^3.6.0",
42
+ "fs-extra": "^11.1.1",
43
+ "tinyglobby": "^0.2.6",
44
+ "typescript": "^5.5.4",
45
+ "vite": "^5",
46
+ "vitepress": "^1.3.4",
47
+ "vue": "^3.4.26"
48
+ },
49
+ "scripts": {
50
+ "dev": "pnpm run /^dev:.*/",
51
+ "dev:plugin": "npx tsup src/index.ts --dts --watch --format esm,cjs --external vitepress",
52
+ "dev:component": "tsc --sourcemap -w --preserveWatchOutput -p src/components",
53
+ "dev:watch": "node scripts/watchAndCopy.mjs",
54
+ "build": "pnpm run /^build:.*/",
55
+ "build:plugin": "npx tsup src/index.ts --dts --format esm,cjs --external vitepress --silent",
56
+ "build:component": "tsc -p src/components && node scripts/copyComponents.mjs"
57
+ }
58
+ }