@mete0r/minimal-homepage 2.0.1

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,138 @@
1
+ <template>
2
+ <section class="py-14 md:py-20" id="articles">
3
+ <h2 class="text-2xl md:text-3xl font-bold mb-6 md:mb-10 dark:text-white">我的文章</h2>
4
+
5
+ <div v-if="isLoading" class="text-sm text-gray-500 dark:text-gray-400">正在加载 RSS...</div>
6
+ <div v-else-if="loadError" class="text-sm text-red-500">RSS 加载失败:{{ loadError }}</div>
7
+
8
+ <template v-else>
9
+ <transition-group name="article-list" tag="div" class="grid grid-cols-1 gap-4 md:gap-6">
10
+ <a
11
+ v-for="(post, index) in visibleArticles"
12
+ :key="post.link || `${index}-${post.title}`"
13
+ :href="post.link"
14
+ target="_blank"
15
+ rel="noopener noreferrer"
16
+ class="flex flex-col md:flex-row gap-4 md:gap-6 p-4 md:p-6 bg-white/40 dark:bg-gray-800/40 backdrop-blur-md rounded-3xl md:rounded-[2.5rem] border border-white/20 hover:bg-white/60 transition-all"
17
+ >
18
+ <img :src="config.blog.randomCoverApi + '?r=' + index" class="w-full md:w-48 h-32 object-cover rounded-2xl" />
19
+ <div class="flex-1">
20
+ <h3 class="text-lg md:text-xl font-bold dark:text-white mb-2">{{ post.title }}</h3>
21
+ <p class="text-gray-500 dark:text-gray-400 line-clamp-2 text-sm">{{ post.description }}</p>
22
+ <p class="mt-4 text-blue-500 font-medium text-sm md:text-base">跳转阅读全文 →</p>
23
+ </div>
24
+ </a>
25
+ </transition-group>
26
+
27
+ <div class="mt-10 md:mt-12 text-center" v-if="articles.length > 3">
28
+ <button
29
+ @click="showAll = !showAll"
30
+ class="px-8 md:px-12 py-3 md:py-4 rounded-full bg-white dark:bg-black text-black dark:text-white font-bold shadow-lg hover:scale-105 transition-transform border border-gray-200 dark:border-gray-700"
31
+ >
32
+ {{ showAll ? '收起文章' : '查看所有文章' }}
33
+ </button>
34
+ </div>
35
+ </template>
36
+ </section>
37
+ </template>
38
+
39
+ <script setup>
40
+ import { computed, ref, onMounted } from 'vue'
41
+ import config from '../../main.config.js'
42
+
43
+ const articles = ref([])
44
+ const showAll = ref(false)
45
+ const isLoading = ref(true)
46
+ const loadError = ref('')
47
+
48
+ const visibleArticles = computed(() => (showAll.value ? articles.value : articles.value.slice(0, 3)))
49
+
50
+ const toAbsoluteLink = (href) => {
51
+ if (!href) return '#'
52
+ try {
53
+ return new URL(href, config.blog.url).toString()
54
+ } catch {
55
+ return href
56
+ }
57
+ }
58
+
59
+ const getNodeText = (node, selector) => {
60
+ const target = node.querySelector(selector)
61
+ return target?.textContent?.trim() || ''
62
+ }
63
+
64
+ const getFeedUrl = () => {
65
+ const feed = config.blog.rssFeed || ''
66
+ if (!feed) return ''
67
+ if (/^https?:\/\//i.test(feed)) return feed
68
+ if (feed.startsWith('/') && config.blog.url) {
69
+ return new URL(feed, config.blog.url).toString()
70
+ }
71
+ return feed
72
+ }
73
+
74
+ const parseItem = (node) => {
75
+ const linkNode = node.querySelector('link')
76
+ const rawLink = linkNode?.getAttribute('href') || linkNode?.textContent || '#'
77
+
78
+ const contentHtml =
79
+ getNodeText(node, 'content') ||
80
+ getNodeText(node, 'content\\:encoded') ||
81
+ getNodeText(node, 'summary') ||
82
+ getNodeText(node, 'description')
83
+
84
+ const plainDescription = contentHtml
85
+ .replace(/<[^>]+>/g, ' ')
86
+ .replace(/\s+/g, ' ')
87
+ .trim()
88
+ .slice(0, 160)
89
+
90
+ return {
91
+ title: getNodeText(node, 'title') || 'Untitled',
92
+ link: toAbsoluteLink(rawLink.trim()),
93
+ description: plainDescription || '暂无摘要'
94
+ }
95
+ }
96
+
97
+ onMounted(async () => {
98
+ const feedUrl = getFeedUrl()
99
+ const cacheKey = `blog_rss_cache:${feedUrl}`
100
+ const cacheTime = 15 * 60 * 1000
101
+ const shouldBypassCache = import.meta.env.DEV
102
+ const cachedData = JSON.parse(localStorage.getItem(cacheKey) || 'null')
103
+
104
+ if (!shouldBypassCache && cachedData && Date.now() - cachedData.timestamp < cacheTime) {
105
+ articles.value = cachedData.items
106
+ isLoading.value = false
107
+ return
108
+ }
109
+
110
+ try {
111
+ if (!feedUrl) {
112
+ throw new Error('rssFeed 未配置')
113
+ }
114
+
115
+ const res = await fetch(feedUrl, { cache: 'no-store' })
116
+ if (!res.ok) {
117
+ throw new Error(`请求失败 ${res.status}`)
118
+ }
119
+
120
+ const text = await res.text()
121
+ const xmlDoc = new DOMParser().parseFromString(text, 'text/xml')
122
+ if (xmlDoc.querySelector('parsererror')) {
123
+ throw new Error('返回内容不是有效 XML')
124
+ }
125
+
126
+ const items = Array.from(xmlDoc.querySelectorAll('item, entry')).slice(0, 12)
127
+ const parsedItems = items.map(parseItem)
128
+
129
+ articles.value = parsedItems
130
+ localStorage.setItem(cacheKey, JSON.stringify({ items: parsedItems, timestamp: Date.now() }))
131
+ } catch (error) {
132
+ loadError.value = error instanceof Error ? error.message : '未知错误'
133
+ console.error('RSS 解析失败', error)
134
+ } finally {
135
+ isLoading.value = false
136
+ }
137
+ })
138
+ </script>
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <div class="theme-toggle">
3
+ <theme-button :value="initialTheme" size="2" @change="handleThemeChange"></theme-button>
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ import { onMounted } from 'vue'
9
+ import './theme-button-element.js'
10
+
11
+ const THEME_KEY = 'preferred-theme'
12
+
13
+ const getInitialTheme = () => {
14
+ if (typeof window === 'undefined') {
15
+ return 'light'
16
+ }
17
+
18
+ const savedTheme = localStorage.getItem(THEME_KEY)
19
+ if (savedTheme === 'light' || savedTheme === 'dark') {
20
+ return savedTheme
21
+ }
22
+
23
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
24
+ }
25
+
26
+ const initialTheme = getInitialTheme()
27
+
28
+ const applyTheme = (theme) => {
29
+ const isDark = theme === 'dark'
30
+ document.documentElement.classList.toggle('dark', isDark)
31
+ document.body.style.backgroundColor = isDark ? '#424242' : 'aliceblue'
32
+ }
33
+
34
+ const handleThemeChange = (e) => {
35
+ const theme = e.detail === 'dark' ? 'dark' : 'light'
36
+ applyTheme(theme)
37
+ localStorage.setItem(THEME_KEY, theme)
38
+ }
39
+
40
+ onMounted(() => {
41
+ applyTheme(initialTheme)
42
+ })
43
+ </script>
44
+
45
+ <style scoped>
46
+ .theme-toggle {
47
+ /* 调整按钮位置 */
48
+ position: relative;
49
+ left: -45px;
50
+ }
51
+ </style>
@@ -0,0 +1,245 @@
1
+ (() => {
2
+ const func = (root, initTheme, changeTheme) => {
3
+ const $ = (s) => {
4
+ let dom = root.querySelectorAll(s);
5
+ return dom.length == 1 ? dom[0] : dom;
6
+ };
7
+ let mainButton = $(".main-button");
8
+ let daytimeBackground = $(".daytime-background");
9
+ let cloud = $(".cloud");
10
+ let cloudList = $(".cloud-son");
11
+ let cloudLight = $(".cloud-light");
12
+ let components = $(".components");
13
+ let moon = $(".moon");
14
+ let stars = $(".stars");
15
+ let star = $(".star");
16
+ let isMoved = false;
17
+ let isClicked = false;
18
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
19
+ toggleThemeBasedOnSystem();
20
+ });
21
+ const toggleThemeBasedOnSystem = () => {
22
+ if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
23
+ if (!isMoved) {
24
+ components.onclick();
25
+ }
26
+ } else {
27
+ if (isMoved) {
28
+ components.onclick();
29
+ }
30
+ }
31
+ };
32
+ components.onclick = () => {
33
+ if (isMoved) {
34
+ mainButton.style.transform = "translateX(0)";
35
+ mainButton.style.backgroundColor = "rgba(255, 195, 35,1)";
36
+
37
+ mainButton.style.boxShadow =
38
+ "3em 3em 5em rgba(0, 0, 0, 0.5), inset -3em -5em 3em -3em rgba(0, 0, 0, 0.5), inset 4em 5em 2em -2em rgba(255, 230, 80,1)";
39
+
40
+ daytimeBackground[0].style.transform = "translateX(0)";
41
+ daytimeBackground[1].style.transform = "translateX(0)";
42
+ daytimeBackground[2].style.transform = "translateX(0)";
43
+ cloud.style.transform = "translateY(10em)";
44
+ cloudLight.style.transform = "translateY(10em)";
45
+ components.style.backgroundColor = "rgba(70, 133, 192,1)";
46
+
47
+ moon[0].style.opacity = "0";
48
+ moon[1].style.opacity = "0";
49
+ moon[2].style.opacity = "0";
50
+
51
+ stars.style.transform = "translateY(-125em)";
52
+ stars.style.opacity = "0";
53
+
54
+ changeTheme("light");
55
+ } else {
56
+ mainButton.style.transform = "translateX(110em)";
57
+ mainButton.style.backgroundColor = "rgba(195, 200,210,1)";
58
+
59
+ mainButton.style.boxShadow =
60
+ "3em 3em 5em rgba(0, 0, 0, 0.5), inset -3em -5em 3em -3em rgba(0, 0, 0, 0.5), inset 4em 5em 2em -2em rgba(255, 255, 210,1)";
61
+
62
+ daytimeBackground[0].style.transform = "translateX(110em)";
63
+ daytimeBackground[1].style.transform = "translateX(80em)";
64
+ daytimeBackground[2].style.transform = "translateX(50em)";
65
+ cloud.style.transform = "translateY(80em)";
66
+ cloudLight.style.transform = "translateY(80em)";
67
+ components.style.backgroundColor = "rgba(25,30,50,1)";
68
+
69
+ moon[0].style.opacity = "1";
70
+ moon[1].style.opacity = "1";
71
+ moon[2].style.opacity = "1";
72
+
73
+ stars.style.transform = "translateY(-62.5em)";
74
+ stars.style.opacity = "1";
75
+
76
+ changeTheme("dark");
77
+ }
78
+
79
+ isClicked = true;
80
+
81
+ setTimeout(function () {
82
+ isClicked = false;
83
+ }, 500);
84
+ isMoved = !isMoved;
85
+ };
86
+
87
+ mainButton.addEventListener("mousemove", function () {
88
+ if (isClicked) return;
89
+
90
+ if (isMoved) {
91
+ mainButton.style.transform = "translateX(100em)";
92
+ daytimeBackground[0].style.transform = "translateX(100em)";
93
+ daytimeBackground[1].style.transform = "translateX(73em)";
94
+ daytimeBackground[2].style.transform = "translateX(46em)";
95
+
96
+ star[0].style.top = "10em";
97
+ star[0].style.left = "36em";
98
+ star[1].style.top = "40em";
99
+ star[1].style.left = "87em";
100
+ star[2].style.top = "26em";
101
+ star[2].style.left = "16em";
102
+ star[3].style.top = "38em";
103
+ star[3].style.left = "63em";
104
+ star[4].style.top = "20.5em";
105
+ star[4].style.left = "72em";
106
+ star[5].style.top = "51.5em";
107
+ star[5].style.left = "35em";
108
+ } else {
109
+ mainButton.style.transform = "translateX(10em)";
110
+ daytimeBackground[0].style.transform = "translateX(10em)";
111
+ daytimeBackground[1].style.transform = "translateX(7em)";
112
+ daytimeBackground[2].style.transform = "translateX(4em)";
113
+
114
+ cloudList[0].style.right = "-24em";
115
+ cloudList[0].style.bottom = "10em";
116
+ cloudList[1].style.right = "-12em";
117
+ cloudList[1].style.bottom = "-27em";
118
+ cloudList[2].style.right = "17em";
119
+ cloudList[2].style.bottom = "-43em";
120
+ cloudList[3].style.right = "46em";
121
+ cloudList[3].style.bottom = "-39em";
122
+ cloudList[4].style.right = "70em";
123
+ cloudList[4].style.bottom = "-65em";
124
+ cloudList[5].style.right = "109em";
125
+ cloudList[5].style.bottom = "-54em";
126
+ cloudList[6].style.right = "-23em";
127
+ cloudList[6].style.bottom = "10em";
128
+ cloudList[7].style.right = "-11em";
129
+ cloudList[7].style.bottom = "-26em";
130
+ cloudList[8].style.right = "18em";
131
+ cloudList[8].style.bottom = "-42em";
132
+ cloudList[9].style.right = "47em";
133
+ cloudList[9].style.bottom = "-38em";
134
+ cloudList[10].style.right = "74em";
135
+ cloudList[10].style.bottom = "-64em";
136
+ cloudList[11].style.right = "110em";
137
+ cloudList[11].style.bottom = "-55em";
138
+ }
139
+ });
140
+
141
+ mainButton.addEventListener("mouseout", function () {
142
+ if (isClicked) {
143
+ return;
144
+ }
145
+ if (isMoved) {
146
+ mainButton.style.transform = "translateX(110em)";
147
+ daytimeBackground[0].style.transform = "translateX(110em)";
148
+ daytimeBackground[1].style.transform = "translateX(80em)";
149
+ daytimeBackground[2].style.transform = "translateX(50em)";
150
+
151
+ star[0].style.top = "11em";
152
+ star[0].style.left = "39em";
153
+ star[1].style.top = "39em";
154
+ star[1].style.left = "91em";
155
+ star[2].style.top = "26em";
156
+ star[2].style.left = "19em";
157
+ star[3].style.top = "37em";
158
+ star[3].style.left = "66em";
159
+ star[4].style.top = "21em";
160
+ star[4].style.left = "75em";
161
+ star[5].style.top = "51em";
162
+ star[5].style.left = "38em";
163
+ } else {
164
+ mainButton.style.transform = "translateX(0em)";
165
+ daytimeBackground[0].style.transform = "translateX(0em)";
166
+ daytimeBackground[1].style.transform = "translateX(0em)";
167
+ daytimeBackground[2].style.transform = "translateX(0em)";
168
+
169
+ cloudList[0].style.right = "-20em";
170
+ cloudList[0].style.bottom = "10em";
171
+ cloudList[1].style.right = "-10em";
172
+ cloudList[1].style.bottom = "-25em";
173
+ cloudList[2].style.right = "20em";
174
+ cloudList[2].style.bottom = "-40em";
175
+ cloudList[3].style.right = "50em";
176
+ cloudList[3].style.bottom = "-35em";
177
+ cloudList[4].style.right = "75em";
178
+ cloudList[4].style.bottom = "-60em";
179
+ cloudList[5].style.right = "110em";
180
+ cloudList[5].style.bottom = "-50em";
181
+ cloudList[6].style.right = "-20em";
182
+ cloudList[6].style.bottom = "10em";
183
+ cloudList[7].style.right = "-10em";
184
+ cloudList[7].style.bottom = "-25em";
185
+ cloudList[8].style.right = "20em";
186
+ cloudList[8].style.bottom = "-40em";
187
+ cloudList[9].style.right = "50em";
188
+ cloudList[9].style.bottom = "-35em";
189
+ cloudList[10].style.right = "75em";
190
+ cloudList[10].style.bottom = "-60em";
191
+ cloudList[11].style.right = "110em";
192
+ cloudList[11].style.bottom = "-50em";
193
+ }
194
+ });
195
+
196
+ const getRandomDirection = () => {
197
+ const directions = ["2em", "-2em"];
198
+ return directions[Math.floor(Math.random() * directions.length)];
199
+ };
200
+
201
+ const moveElementRandomly = (element) => {
202
+ const randomDirectionX = getRandomDirection();
203
+ const randomDirectionY = getRandomDirection();
204
+ element.style.transform = `translate(${randomDirectionX}, ${randomDirectionY})`;
205
+ };
206
+
207
+ const cloudSons = root.querySelectorAll(".cloud-son");
208
+ setInterval(() => {
209
+ cloudSons.forEach(moveElementRandomly);
210
+ }, 1000);
211
+
212
+ if (initTheme === "dark") {
213
+ components.onclick();
214
+ }
215
+ };
216
+
217
+ class ThemeButton extends HTMLElement {
218
+ constructor() {
219
+ super();
220
+ }
221
+ connectedCallback() {
222
+ const initTheme = this.getAttribute("value") || "light";
223
+ const size = +this.getAttribute("size") || 3;
224
+ const shadow = this.attachShadow({ mode: "closed" });
225
+ const container = document.createElement("div");
226
+ container.setAttribute("class", "container");
227
+ container.setAttribute("style", `font-size: ${(size / 3).toFixed(2)}px`);
228
+ container.innerHTML =
229
+ '<div class="components"><div class="main-button"><div class="moon"></div><div class="moon"></div><div class="moon"></div></div><div class="daytime-background"></div><div class="daytime-background"></div><div class="daytime-background"></div><div class="cloud"><div class="cloud-son"></div><div class="cloud-son"></div><div class="cloud-son"></div><div class="cloud-son"></div><div class="cloud-son"></div><div class="cloud-son"></div></div><div class="cloud-light"><div class="cloud-son"></div><div class="cloud-son"></div><div class="cloud-son"></div><div class="cloud-son"></div><div class="cloud-son"></div><div class="cloud-son"></div></div><div class="stars"><div class="star big"><div class="star-son"></div><div class="star-son"></div><div class="star-son"></div><div class="star-son"></div></div><div class="star big"><div class="star-son"></div><div class="star-son"></div><div class="star-son"></div><div class="star-son"></div></div><div class="star medium"><div class="star-son"></div><div class="star-son"></div><div class="star-son"></div><div class="star-son"></div></div><div class="star medium"><div class="star-son"></div><div class="star-son"></div><div class="star-son"></div><div class="star-son"></div></div><div class="star small"><div class="star-son"></div><div class="star-son"></div><div class="star-son"></div><div class="star-son"></div></div><div class="star small"><div class="star-son"></div><div class="star-son"></div><div class="star-son"></div><div class="star-son"></div></div></div></div>';
230
+ const style = document.createElement("style");
231
+ style.textContent =
232
+ "* { margin: 0; padding: 0; transition: 0.7s; -webkit-tap-highlight-color:rgba(0,0,0,0); } .container { position: absolute;top: 50%;left: 50%;margin-top: -35em;margin-left: -90em;width: 180em; height: 70em; display: inline-block; vertical-align: bottom; transform: translate3d(0, 0, 0); } .components{ position:fixed; width: 180em; height: 70em; background-color: rgba(70, 133, 192,1); border-radius: 100em; box-shadow: inset 0 0 5em 3em rgba(0, 0, 0, 0.5); overflow: hidden; transition: 0.7s; transition-timing-function: cubic-bezier( 0,0.5, 1,1); cursor: pointer; } .main-button{ margin: 7.5em 0 0 7.5em; width: 55em; height:55em; background-color: rgba(255, 195, 35,1); border-radius: 50%; box-shadow:3em 3em 5em rgba(0, 0, 0, 0.5), inset -3em -5em 3em -3em rgba(0, 0, 0, 0.5), inset 4em 5em 2em -2em rgba(255, 230, 80,1); transition: 1.0s; transition-timing-function: cubic-bezier(0.56, 1.35, 0.52, 1.00); } .moon{ position: absolute; background-color: rgba(150, 160, 180, 1); box-shadow:inset 0em 0em 1em 1em rgba(0, 0, 0, 0.3) ; border-radius: 50%; transition: 0.5s; opacity: 0; } .moon:nth-child(1){ top: 7.5em; left: 25em; width: 12.5em; height: 12.5em; } .moon:nth-child(2){ top: 20em; left: 7.5em; width: 20em; height: 20em; } .moon:nth-child(3){ top: 32.5em; left: 32.5em; width: 12.5em; height: 12.5em; } .daytime-background { position: absolute; border-radius: 50%; transition: 1.0s; transition-timing-function: cubic-bezier(0.56, 1.35, 0.52, 1.00); } .daytime-background:nth-child(2){ top: -20em; left: -20em; width: 110em; height:110em; background-color: rgba(255, 255, 255,0.2); z-index: -2; } .daytime-background:nth-child(3){ top: -32.5em; left: -17.5em; width: 135em; height:135em; background-color: rgba(255, 255, 255,0.1); z-index: -3; } .daytime-background:nth-child(4){ top: -45em; left: -15em; width: 160em; height:160em; background-color: rgba(255, 255, 255,0.05); z-index: -4; } .cloud,.cloud-light{ transform: translateY(10em); transition: 1.0s; transition-timing-function: cubic-bezier(0.56, 1.35, 0.52, 1.00); } .cloud-son{ position: absolute; background-color: #fff; border-radius: 50%; z-index: -1; transition: transform 6s,right 1s,bottom 1s; } .cloud-son:nth-child(6n+1){ right: -20em; bottom: 10em; width: 50em; height: 50em; } .cloud-son:nth-child(6n+2) { right: -10em; bottom: -25em; width: 60em; height: 60em; } .cloud-son:nth-child(6n+3) { right: 20em; bottom: -40em; width: 60em; height: 60em; } .cloud-son:nth-child(6n+4) { right: 50em; bottom: -35em; width: 60em; height: 60em; } .cloud-son:nth-child(6n+5) { right: 75em; bottom: -60em; width: 75em; height: 75em; } .cloud-son:nth-child(6n+6) { right: 110em; bottom: -50em; width: 60em; height: 60em; } .cloud{ z-index: -2; } .cloud-light{ position: absolute; right: 0em; bottom: 25em; opacity: 0.5; z-index: -3; /*transform: rotate(-5deg);*/ } .stars{ transform: translateY(-125em); z-index: -2; transition: 1.0s; transition-timing-function: cubic-bezier(0.56, 1.35, 0.52, 1.00); } .big { --size: 7.5em; } .medium { --size: 5em; } .small { --size: 3em; } .star { position: absolute; width: calc(2*var(--size)); height: calc(2*var(--size)); } .star:nth-child(1){ top: 11em; left: 39em; animation-name: star; animation-duration: 3.5s; } .star:nth-child(2){ top: 39em; left: 91em; animation-name: star; animation-duration: 4.1s; } .star:nth-child(3){ top: 26em; left: 19em; animation-name: star; animation-duration: 4.9s; } .star:nth-child(4){ top: 37em; left: 66em; animation-name: star; animation-duration: 5.3s; } .star:nth-child(5){ top: 21em; left: 75em; animation-name: star; animation-duration: 3s; } .star:nth-child(6){ top: 51em; left: 38em; animation-name: star; animation-duration: 2.2s; } @keyframes star { 0%,20%{ transform: scale(0); } 20%,100% { transform: scale(1); } } .star-son{ float: left; } .star-son:nth-child(1) { --pos: left 0; } .star-son:nth-child(2) { --pos: right 0; } .star-son:nth-child(3) { --pos: 0 bottom; } .star-son:nth-child(4) { --pos: right bottom; } .star-son { width: var(--size); height: var(--size); background-image: radial-gradient(circle var(--size) at var(--pos), transparent var(--size), #fff); } .star{ transform: scale(1); transition-timing-function: cubic-bezier(0.56, 1.35, 0.52, 1.00); transition: 1s; animation-iteration-count:infinite; animation-direction: alternate; animation-timing-function: linear; } .twinkle { transform: scale(0); }";
233
+ const changeTheme = (detail) => {
234
+ this.dispatchEvent(new CustomEvent("change", { detail }));
235
+ };
236
+ func(container, initTheme, changeTheme);
237
+ shadow.appendChild(style);
238
+ shadow.appendChild(container);
239
+ }
240
+ }
241
+
242
+ if (!customElements.get("theme-button")) {
243
+ customElements.define("theme-button", ThemeButton);
244
+ }
245
+ })();
package/src/config.js ADDED
@@ -0,0 +1 @@
1
+ export { default } from '../main.config.js'
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import App from './App.vue'
2
+ export { App }
3
+ export function install(app) {
4
+ app.component('App', App)
5
+ }
package/src/main.js ADDED
@@ -0,0 +1,48 @@
1
+ import { createApp } from 'vue'
2
+ import App from './App.vue'
3
+ import router, { preloadAboutPage } from './router'
4
+ import config from '../main.config.js'
5
+ import './style.css'
6
+
7
+ const applySiteMeta = () => {
8
+ if (config.site?.title) {
9
+ document.title = config.site.title
10
+ }
11
+
12
+ if (config.site?.icon) {
13
+ let iconLink = document.querySelector("link[rel='icon']")
14
+ if (!iconLink) {
15
+ iconLink = document.createElement('link')
16
+ iconLink.setAttribute('rel', 'icon')
17
+ document.head.appendChild(iconLink)
18
+ }
19
+
20
+ const iconUrl = config.site.icon
21
+ iconLink.setAttribute('href', iconUrl)
22
+ iconLink.setAttribute('type', iconUrl.endsWith('.svg') ? 'image/svg+xml' : 'image/png')
23
+ }
24
+ }
25
+
26
+ const mountHeartClickEffect = () => {
27
+ const scriptSrc = 'https://gcore.jsdelivr.net/gh/qsya/MouseClickEffect@main/js/heart.js'
28
+ if (document.querySelector(`script[src='${scriptSrc}']`)) return
29
+
30
+ const script = document.createElement('script')
31
+ script.src = scriptSrc
32
+ script.defer = true
33
+ document.body.appendChild(script)
34
+ }
35
+
36
+ applySiteMeta()
37
+
38
+ const app = createApp(App)
39
+ app.use(router)
40
+ app.mount('#app')
41
+
42
+ mountHeartClickEffect()
43
+
44
+ if ('requestIdleCallback' in window) {
45
+ window.requestIdleCallback(() => preloadAboutPage())
46
+ } else {
47
+ window.setTimeout(() => preloadAboutPage(), 500)
48
+ }
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <div class="relative">
3
+ <AboutMe />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ import AboutMe from '../components/AboutMe.vue'
9
+ </script>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <div class="relative">
3
+ <HeroSection />
4
+ <CardSection />
5
+ <MyProject />
6
+ <RSSSection />
7
+ </div>
8
+ </template>
9
+
10
+ <script setup>
11
+ import HeroSection from '../components/HeroSection.vue'
12
+ import CardSection from '../components/CardSection.vue'
13
+ import MyProject from '../components/MyProject.vue'
14
+ import RSSSection from '../components/RSSSection.vue'
15
+ </script>
package/src/router.js ADDED
@@ -0,0 +1,27 @@
1
+ import { createRouter, createWebHistory } from 'vue-router'
2
+ import HomePage from './pages/HomePage.vue'
3
+
4
+ const AboutPage = () => import('./pages/AboutPage.vue')
5
+
6
+ const router = createRouter({
7
+ history: createWebHistory(),
8
+ routes: [
9
+ { path: '/', name: 'home', component: HomePage },
10
+ { path: '/about', name: 'about', component: AboutPage }
11
+ ],
12
+ scrollBehavior(to, from, savedPosition) {
13
+ if (savedPosition) return savedPosition
14
+ if (to.hash) {
15
+ return {
16
+ el: to.hash,
17
+ top: 84,
18
+ behavior: 'smooth'
19
+ }
20
+ }
21
+ return { top: 0, behavior: 'smooth' }
22
+ }
23
+ })
24
+
25
+ export const preloadAboutPage = () => import('./pages/AboutPage.vue')
26
+
27
+ export default router
package/src/style.css ADDED
@@ -0,0 +1,96 @@
1
+ /* src/style.css */
2
+ @import "tailwindcss";
3
+
4
+ @custom-variant dark (&:where(.dark, .dark *));
5
+
6
+ @theme {
7
+ --font-sans: "PingFang SC", "Microsoft YaHei", sans-serif;
8
+ }
9
+
10
+ :root {
11
+ --background: aliceblue;
12
+ }
13
+
14
+ .dark {
15
+ --background: #424242;
16
+ }
17
+
18
+ /* 跳转框背景色 */
19
+ .bg-blue-600 {
20
+ --un-bg-opacity: 1;
21
+ background-color: rgb(128 128 128 / var(--un-bg-opacity));
22
+ }
23
+
24
+ .hover\:bg-blue-700:hover {
25
+ --un-bg-opacity: 1;
26
+ background-color: rgb(128 128 128 / var(--un-bg-opacity));
27
+ }
28
+
29
+ body {
30
+ background-color: var(--background);
31
+ transition: background-color 0.5s ease;
32
+ }
33
+
34
+ .route-fade-enter-active,
35
+ .route-fade-leave-active {
36
+ transition: opacity 0.35s ease, transform 0.35s ease;
37
+ }
38
+
39
+ .route-fade-enter-from {
40
+ opacity: 0;
41
+ transform: translateY(12px) scale(0.995);
42
+ }
43
+
44
+ .route-fade-leave-to {
45
+ opacity: 0;
46
+ transform: translateY(-10px) scale(0.995);
47
+ }
48
+
49
+ .article-list-enter-active,
50
+ .article-list-leave-active {
51
+ transition: all 0.32s cubic-bezier(0.2, 0.7, 0.2, 1);
52
+ }
53
+
54
+ .article-list-enter-from,
55
+ .article-list-leave-to {
56
+ opacity: 0;
57
+ transform: translateY(18px) scale(0.98);
58
+ }
59
+
60
+ .article-list-move {
61
+ transition: transform 0.32s cubic-bezier(0.2, 0.7, 0.2, 1);
62
+ }
63
+
64
+ .name-chip {
65
+ padding: 0.35rem 0.85rem;
66
+ border-radius: 0.9rem;
67
+ transition: background-color 0.2s ease, box-shadow 0.2s ease;
68
+ }
69
+
70
+ .name-chip:hover {
71
+ background: rgba(15, 23, 42, 0.08);
72
+ box-shadow: inset 0 -1px 0 rgba(15, 23, 42, 0.18);
73
+ }
74
+
75
+ .dark .name-chip:hover {
76
+ background: rgba(255, 255, 255, 0.14);
77
+ box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.22);
78
+ }
79
+
80
+ .no-scrollbar {
81
+ -ms-overflow-style: none;
82
+ scrollbar-width: none;
83
+ }
84
+
85
+ .no-scrollbar::-webkit-scrollbar {
86
+ display: none;
87
+ }
88
+
89
+ @media (prefers-reduced-motion: reduce) {
90
+ *, *::before, *::after {
91
+ animation-duration: 0.01ms !important;
92
+ animation-iteration-count: 1 !important;
93
+ transition-duration: 0.01ms !important;
94
+ scroll-behavior: auto !important;
95
+ }
96
+ }
package/vite.config.js ADDED
@@ -0,0 +1,32 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+
5
+ export default defineConfig({
6
+ plugins: [
7
+ tailwindcss(),
8
+ vue({
9
+ template: {
10
+ compilerOptions: {
11
+ isCustomElement: (tag) => tag === 'theme-button'
12
+ }
13
+ }
14
+ })
15
+ ],
16
+ build: {
17
+ lib: {
18
+ entry: 'src/index.js',
19
+ name: 'MinimalHomepage',
20
+ fileName: (format) => `index.${format}.js`
21
+ },
22
+ rollupOptions: {
23
+ external: ['vue', 'vue-router'],
24
+ output: {
25
+ globals: {
26
+ vue: 'Vue',
27
+ 'vue-router': 'VueRouter'
28
+ }
29
+ }
30
+ }
31
+ }
32
+ })