@windcoder/astro-theme-orchid 0.2.1 → 0.4.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/README.md +5 -3
- package/package.json +4 -2
- package/scripts/check-friends.mjs +227 -0
- package/scripts/generate-lqips.mjs +234 -20
- package/scripts/migrate-posts.mjs +375 -0
- package/src/components/AlbumCard.astro +4 -1
- package/src/components/Analytics.astro +3 -0
- package/src/components/BaseLayout.astro +222 -39
- package/src/components/DisplaySettings.astro +383 -0
- package/src/components/Header.astro +56 -2
- package/src/components/MusicPlayer.astro +45 -5
- package/src/components/MusicWidget.astro +265 -0
- package/src/components/PhotoCard.astro +13 -1
- package/src/components/PostCard.astro +4 -1
- package/src/components/SakuraEffect.astro +126 -28
- package/src/components/Sidebar.astro +28 -7
- package/src/components/analytics/WebVitals.astro +72 -0
- package/src/i18n/en.ts +42 -0
- package/src/i18n/ja.ts +213 -0
- package/src/i18n/ko.ts +212 -0
- package/src/i18n/resolve.ts +10 -1
- package/src/i18n/ru.ts +213 -0
- package/src/i18n/zh-CN.ts +43 -0
- package/src/i18n/zh-TW.ts +208 -0
- package/src/index.ts +23 -0
- package/src/plugins/rehype.ts +76 -4
- package/src/routes/bangumi.astro +110 -0
- package/src/routes/friends.astro +10 -2
- package/src/routes/manifest.webmanifest.ts +33 -0
- package/src/routes/page/[page].astro +3 -0
- package/src/routes/search.astro +24 -0
- package/src/routes/sponsor.astro +17 -4
- package/src/routes/sw.js.ts +127 -0
- package/src/schema.ts +446 -45
- package/src/scripts/app.ts +805 -37
- package/src/styles/global.css +939 -18
- package/src/utils/gallery.ts +3 -1
- package/src/utils/image.ts +79 -0
- package/src/utils/lqip.ts +21 -12
- package/src/utils/thumb.ts +26 -0
- package/src/vite.ts +95 -8
package/src/i18n/ru.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Русский языковой пакет:Record<UiKey, string> 强制与中文键源一一对应,缺译会在编译期报错。
|
|
3
|
+
*/
|
|
4
|
+
import type { UiKey } from "./zh-CN.js";
|
|
5
|
+
|
|
6
|
+
const ru: Record<UiKey, string> = {
|
|
7
|
+
skipToContent: "Перейти к основному содержанию",
|
|
8
|
+
scrollDown: "Прокрутить вниз",
|
|
9
|
+
backToTop: "Наверх",
|
|
10
|
+
mainNav: "Главная навигация",
|
|
11
|
+
toggleMode: "Переключить тёмную тему",
|
|
12
|
+
menu: "Меню",
|
|
13
|
+
palette: "Цвет темы",
|
|
14
|
+
paletteTitle: "Акцентный цвет",
|
|
15
|
+
reset: "Сброс",
|
|
16
|
+
hueValue: "Оттенок {n}",
|
|
17
|
+
pinned: "Закреплено",
|
|
18
|
+
wordsCount: "{n} слов",
|
|
19
|
+
minutesCount: "≈ {n} мин",
|
|
20
|
+
prevNext: "Предыдущая / следующая",
|
|
21
|
+
prevPost: "Предыдущая",
|
|
22
|
+
nextPost: "Следующая",
|
|
23
|
+
firstPost: "Это самая ранняя запись",
|
|
24
|
+
lastPost: "Это самая свежая запись",
|
|
25
|
+
relatedPosts: "Похожие записи",
|
|
26
|
+
randomPosts: "Случайные записи",
|
|
27
|
+
smartRecommend: "Умное",
|
|
28
|
+
randomRecommend: "Случайное",
|
|
29
|
+
noRelatedPosts: "Нет похожих записей",
|
|
30
|
+
noRandomPosts: "Нет случайных записей",
|
|
31
|
+
licensePrefix: "Эта запись распространяется по лицензии",
|
|
32
|
+
licenseSuffix: ". При перепубликации указывайте автора и первоисточник.",
|
|
33
|
+
toc: "Оглавление",
|
|
34
|
+
tocTitle: "Оглавление",
|
|
35
|
+
pagination: "Постраничная навигация",
|
|
36
|
+
prevPage: "Назад",
|
|
37
|
+
nextPage: "Вперёд",
|
|
38
|
+
pageOf: "Страница {current} из {total}",
|
|
39
|
+
closeAnnouncement: "Закрыть объявление",
|
|
40
|
+
closeAd: "Закрыть рекламу",
|
|
41
|
+
categories: "Категории",
|
|
42
|
+
tags: "Метки",
|
|
43
|
+
friendLinks: "Друзья",
|
|
44
|
+
siteInfo: "О сайте",
|
|
45
|
+
posts: "Записи",
|
|
46
|
+
running: "Работает",
|
|
47
|
+
daysCount: "{n} дн.",
|
|
48
|
+
dayPostsCount: "{n}",
|
|
49
|
+
heatWeekTip: "{m}, неделя {w} · {n}",
|
|
50
|
+
tagsMore: "…ещё",
|
|
51
|
+
calendarHome: "Текущий месяц",
|
|
52
|
+
calendarPick: "Выбрать год и месяц",
|
|
53
|
+
layoutSwitch: "Переключить список / сетку",
|
|
54
|
+
layoutList: "Список",
|
|
55
|
+
layoutGrid: "Сетка",
|
|
56
|
+
emptyPosts: "Записей пока нет. Напишите первую в content/posts.",
|
|
57
|
+
pageN: "Страница {n}",
|
|
58
|
+
archive: "Архив",
|
|
59
|
+
home: "Главная",
|
|
60
|
+
categoryNav: "Навигация по категориям",
|
|
61
|
+
more: "Ещё",
|
|
62
|
+
totalPosts: "Всего записей: {n}",
|
|
63
|
+
postsInYear: "{n}",
|
|
64
|
+
yearUnit: "{n} г.",
|
|
65
|
+
totalTags: "Всего меток: {n}",
|
|
66
|
+
tagTitle: "Метка: {tag}",
|
|
67
|
+
totalCategories: "Всего категорий: {n}",
|
|
68
|
+
categoryTitle: "Категория: {category}",
|
|
69
|
+
publishedOn: "Опубликовано {date}",
|
|
70
|
+
updatedOn: "Обновлено {date}",
|
|
71
|
+
pageviews: "Просмотры",
|
|
72
|
+
notFound: "Страница не найдена",
|
|
73
|
+
notFoundDesc: "Страница не существует или была перемещена.",
|
|
74
|
+
backHome: "На главную",
|
|
75
|
+
about: "О себе",
|
|
76
|
+
aboutEmpty: "Напишите эту страницу в src/content/spec/about.md",
|
|
77
|
+
avatarAlt: "Аватар {name}",
|
|
78
|
+
noFriends: "Ссылок пока нет",
|
|
79
|
+
applyFriend: "Заявка на обмен ссылками",
|
|
80
|
+
/* Страница отслеживания аниме */
|
|
81
|
+
bangumiEmpty: "Пока ничего не смотрю",
|
|
82
|
+
bangumiFail:
|
|
83
|
+
"Не удалось получить данные. Пересоберите сайт и повторите попытку",
|
|
84
|
+
epProgress: "Серия {n} / {total}",
|
|
85
|
+
epProgressOpen: "Серия {n}",
|
|
86
|
+
myRate: "Моя оценка {n}",
|
|
87
|
+
guestbook: "Гостевая книга",
|
|
88
|
+
guestbookSub: "Оставьте свой след",
|
|
89
|
+
commentComingSoon: "Комментарии скоро появятся. Оставайтесь с нами.",
|
|
90
|
+
qrAlt: "QR-код {name}",
|
|
91
|
+
goSponsor: "Поддержать",
|
|
92
|
+
qrPlaceholder: "Заполнитель QR-кода",
|
|
93
|
+
sponsorList: "Спонсоры",
|
|
94
|
+
noSponsors: "Пока никто не поддержал. Будьте первым",
|
|
95
|
+
search: "Поиск",
|
|
96
|
+
searchShortcut: "Нажмите / или Ctrl+K для поиска",
|
|
97
|
+
searchLoading: "Поиск…",
|
|
98
|
+
searchNoResults: "Ничего не найдено",
|
|
99
|
+
searchUnavailable: "Поисковый индекс доступен после сборки",
|
|
100
|
+
searchTypeSomething: "Введите ключевые слова",
|
|
101
|
+
searchMore: "Показать все результаты ({n})",
|
|
102
|
+
searchResultsTotal: "Результатов: {n}",
|
|
103
|
+
galleryPhotos: "{n} фото",
|
|
104
|
+
backToAlbums: "К альбомам",
|
|
105
|
+
searchAlbums: "Поиск альбомов…",
|
|
106
|
+
allTags: "Все",
|
|
107
|
+
galleryNoAlbums:
|
|
108
|
+
"Альбомов пока нет. Добавьте их в настройках темы gallery.albums.",
|
|
109
|
+
galleryNoPhotos: "В этом альбоме нет фотографий",
|
|
110
|
+
/* Страница заметок */
|
|
111
|
+
momentsTitle: "Заметки",
|
|
112
|
+
momentsEmpty: "Заметок пока нет.",
|
|
113
|
+
encryptedAlbum: "Зашифрованный альбом",
|
|
114
|
+
searchAlbumNoResults: "Подходящих альбомов нет",
|
|
115
|
+
postEncrypted: "Зашифровано",
|
|
116
|
+
passwordProtected: "Защита паролем",
|
|
117
|
+
passwordProtectedDesc:
|
|
118
|
+
"Содержимое защищено паролем. Введите пароль для просмотра.",
|
|
119
|
+
passwordHint: "Подсказка",
|
|
120
|
+
passwordPlaceholder: "Введите пароль",
|
|
121
|
+
passwordSubmit: "Разблокировать",
|
|
122
|
+
passwordError: "Неверный пароль, попробуйте снова.",
|
|
123
|
+
close: "Закрыть",
|
|
124
|
+
prevImage: "Предыдущее изображение",
|
|
125
|
+
nextImage: "Следующее изображение",
|
|
126
|
+
copied: "Скопировано",
|
|
127
|
+
fullscreen: "Полный экран",
|
|
128
|
+
slideshow: "Слайдшоу",
|
|
129
|
+
thumbs: "Миниатюры",
|
|
130
|
+
/* Постер для-sharing */
|
|
131
|
+
sharePoster: "Создать постер",
|
|
132
|
+
generatingPoster: "Создание постера…",
|
|
133
|
+
copyLink: "Копировать ссылку",
|
|
134
|
+
savePoster: "Сохранить постер",
|
|
135
|
+
scanToRead: "Сканируйте, чтобы читать",
|
|
136
|
+
author: "Автор",
|
|
137
|
+
supportTitle: "Поддержать",
|
|
138
|
+
supportDesc:
|
|
139
|
+
"Если запись была полезной, поддержите автора или поделитесь ею.",
|
|
140
|
+
sponsorMe: "Поддержать",
|
|
141
|
+
lastModifiedPrefix: "Последнее обновление: ",
|
|
142
|
+
lastModifiedDaysAgo: ", {days} дн. назад",
|
|
143
|
+
lastModifiedOutdated: "Часть информации может быть устаревшей.",
|
|
144
|
+
/* Серии */
|
|
145
|
+
series: "Серии",
|
|
146
|
+
seriesTotal: "Всего серий: {n}",
|
|
147
|
+
seriesPostsCount: "{n}",
|
|
148
|
+
seriesUpdated: "Обновлено {date}",
|
|
149
|
+
seriesEmpty:
|
|
150
|
+
"Серий пока нет. Заполните поле series в frontmatter записи, чтобы создать.",
|
|
151
|
+
seriesProgress: "{cur} / {total}",
|
|
152
|
+
seriesCatalog: "Содержание серии",
|
|
153
|
+
/* Музыкальный плеер */
|
|
154
|
+
musicOpen: "Музыкальный плеер",
|
|
155
|
+
musicClose: "Свернуть плеер",
|
|
156
|
+
musicPlay: "Воспроизвести",
|
|
157
|
+
musicPause: "Пауза",
|
|
158
|
+
musicPrev: "Предыдущий трек",
|
|
159
|
+
musicNext: "Следующий трек",
|
|
160
|
+
musicModeList: "По списку",
|
|
161
|
+
musicModeOne: "Повтор трека",
|
|
162
|
+
musicModeRandom: "Случайно",
|
|
163
|
+
musicLyrics: "Текст песни",
|
|
164
|
+
musicPlaylist: "Плейлист",
|
|
165
|
+
musicNoLyrics: "Нет текста",
|
|
166
|
+
musicEmpty: "Плейлист пуст",
|
|
167
|
+
musicSeek: "Прогресс воспроизведения",
|
|
168
|
+
musicVolume: "Громкость",
|
|
169
|
+
l2dHome: "На главную",
|
|
170
|
+
l2dScrollTop: "Наверх",
|
|
171
|
+
l2dSleep: "Отдохнуть",
|
|
172
|
+
l2dSwitchModel: "Сменить модель",
|
|
173
|
+
/* Сворачивание кода */
|
|
174
|
+
codeFoldExpand: "Развернуть код",
|
|
175
|
+
codeFoldCollapse: "Свернуть код",
|
|
176
|
+
codeFoldExpanded: "Блок кода развёрнут",
|
|
177
|
+
codeFoldCollapsed: "Блок кода свёрнут",
|
|
178
|
+
/* Обои */
|
|
179
|
+
wpPlay: "Воспроизвести видео обоев",
|
|
180
|
+
wpPause: "Приостановить видео обоев",
|
|
181
|
+
wpPrev: "Предыдущее видео",
|
|
182
|
+
wpNext: "Следующее видео",
|
|
183
|
+
wpMode: "Переключить режим обоев",
|
|
184
|
+
/* Панель настроек отображения(§4.103):шестерёнка + три вкладки + группы */
|
|
185
|
+
displaySettings: "Настройки отображения",
|
|
186
|
+
tabAppearance: "Внешний вид",
|
|
187
|
+
tabWallpaper: "Обои",
|
|
188
|
+
tabEffects: "Эффекты",
|
|
189
|
+
wallpaperMode: "Режим обоев",
|
|
190
|
+
modeBanner: "Баннер",
|
|
191
|
+
modeFullscreen: "Полноэкранные",
|
|
192
|
+
modeOverlay: "Прозрачный оверлей",
|
|
193
|
+
modeNone: "Сплошной цвет",
|
|
194
|
+
fullscreenLayout: "Полноэкранный макет",
|
|
195
|
+
layoutClassic: "Классический",
|
|
196
|
+
layoutHero: "Hero",
|
|
197
|
+
wallpaperSettings: "Настройки обоев",
|
|
198
|
+
homeWallpaperTitle: "Заголовок на обоях",
|
|
199
|
+
wallpaperCarousel: "Карусель обоев",
|
|
200
|
+
wavesAnimation: "Анимация волн",
|
|
201
|
+
gradientTransition: "Градиентный переход",
|
|
202
|
+
overlaySettings: "Прозрачность",
|
|
203
|
+
wallpaperOpacity: "Непрозрачность обоев",
|
|
204
|
+
backgroundBlur: "Размытие фона",
|
|
205
|
+
cardOpacity: "Непрозрачность карточек",
|
|
206
|
+
sakuraEffect: "Эффект сакуры",
|
|
207
|
+
/* Всего слов в информации о сайте */
|
|
208
|
+
totalWords: "Всего слов",
|
|
209
|
+
/* Музыкальный виджет в боковой панели */
|
|
210
|
+
musicWidget: "Музыка",
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export default ru;
|
package/src/i18n/zh-CN.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const zhCN = {
|
|
7
7
|
/* 全局框架(BaseLayout) */
|
|
8
8
|
skipToContent: "跳到主要内容",
|
|
9
|
+
scrollDown: "向下滚动",
|
|
9
10
|
backToTop: "回到顶部",
|
|
10
11
|
/* Header */
|
|
11
12
|
mainNav: "主导航",
|
|
@@ -90,6 +91,12 @@ const zhCN = {
|
|
|
90
91
|
avatarAlt: "{name} 的头像",
|
|
91
92
|
noFriends: "还没有友链",
|
|
92
93
|
applyFriend: "申请友链",
|
|
94
|
+
/* 番组追番页(§4.93) */
|
|
95
|
+
bangumiEmpty: "还没有在看中的番剧",
|
|
96
|
+
bangumiFail: "番组数据拉取失败,请重新构建站点后重试",
|
|
97
|
+
epProgress: "看到 {n} / {total} 集",
|
|
98
|
+
epProgressOpen: "看到 {n} 集",
|
|
99
|
+
myRate: "我的评分 {n}",
|
|
93
100
|
guestbook: "留言板",
|
|
94
101
|
guestbookSub: "欢迎留下你的足迹",
|
|
95
102
|
commentComingSoon: "评论区即将开放,敬请期待。",
|
|
@@ -105,6 +112,9 @@ const zhCN = {
|
|
|
105
112
|
searchNoResults: "没有找到相关结果",
|
|
106
113
|
searchUnavailable: "搜索索引需构建后可用",
|
|
107
114
|
searchTypeSomething: "输入关键词开始搜索",
|
|
115
|
+
/* 独立搜索页(§4.104):浮层「查看全部」入口 + 结果页计数;分页文案复用 pagination 键 */
|
|
116
|
+
searchMore: "查看全部 {n} 条结果",
|
|
117
|
+
searchResultsTotal: "共 {n} 条结果",
|
|
108
118
|
/* 画廊(M3b-b8):title/description 走主题配置,这里只放 UI 通用文案 */
|
|
109
119
|
galleryPhotos: "{n} 张照片",
|
|
110
120
|
backToAlbums: "返回相册",
|
|
@@ -181,6 +191,39 @@ const zhCN = {
|
|
|
181
191
|
codeFoldCollapse: "收起代码",
|
|
182
192
|
codeFoldExpanded: "代码已展开",
|
|
183
193
|
codeFoldCollapsed: "代码已收起",
|
|
194
|
+
/* 壁纸(§4.98):Header 控制按钮 aria-label/title */
|
|
195
|
+
wpPlay: "播放壁纸视频",
|
|
196
|
+
wpPause: "暂停壁纸视频",
|
|
197
|
+
wpPrev: "上一段视频",
|
|
198
|
+
wpNext: "下一段视频",
|
|
199
|
+
wpMode: "切换壁纸模式",
|
|
200
|
+
/* 显示设置面板(§4.103):齿轮入口 + 三 Tab + 各组控件 */
|
|
201
|
+
displaySettings: "显示设置",
|
|
202
|
+
tabAppearance: "外观",
|
|
203
|
+
tabWallpaper: "壁纸",
|
|
204
|
+
tabEffects: "特效",
|
|
205
|
+
wallpaperMode: "壁纸模式",
|
|
206
|
+
modeBanner: "横幅壁纸",
|
|
207
|
+
modeFullscreen: "全屏壁纸",
|
|
208
|
+
modeOverlay: "覆盖透明",
|
|
209
|
+
modeNone: "纯色背景",
|
|
210
|
+
fullscreenLayout: "全屏布局",
|
|
211
|
+
layoutClassic: "经典模式",
|
|
212
|
+
layoutHero: "Hero 模式",
|
|
213
|
+
wallpaperSettings: "壁纸设置",
|
|
214
|
+
homeWallpaperTitle: "首页壁纸标题",
|
|
215
|
+
wallpaperCarousel: "壁纸轮播",
|
|
216
|
+
wavesAnimation: "水波纹动画",
|
|
217
|
+
gradientTransition: "渐变过渡",
|
|
218
|
+
overlaySettings: "透明设置",
|
|
219
|
+
wallpaperOpacity: "壁纸透明度",
|
|
220
|
+
backgroundBlur: "背景模糊度",
|
|
221
|
+
cardOpacity: "卡片透明度",
|
|
222
|
+
sakuraEffect: "樱花特效",
|
|
223
|
+
/* 站点信息总字数(§4.99):Sidebar siteInfo 卡 */
|
|
224
|
+
totalWords: "总字数",
|
|
225
|
+
/* 侧栏音乐 widget(§4.100):标题;播放/切歌等文案复用 music* 键 */
|
|
226
|
+
musicWidget: "音乐",
|
|
184
227
|
} as const;
|
|
185
228
|
|
|
186
229
|
export type UiKey = keyof typeof zhCN;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 繁体中文语言包:Record<UiKey, string> 强制与中文键源一一对应,缺译会在编译期报错。
|
|
3
|
+
*/
|
|
4
|
+
import type { UiKey } from "./zh-CN.js";
|
|
5
|
+
|
|
6
|
+
const zhTW: Record<UiKey, string> = {
|
|
7
|
+
skipToContent: "跳至主要內容",
|
|
8
|
+
scrollDown: "向下捲動",
|
|
9
|
+
backToTop: "回到頂端",
|
|
10
|
+
mainNav: "主導覽",
|
|
11
|
+
toggleMode: "切換深淺模式",
|
|
12
|
+
menu: "選單",
|
|
13
|
+
palette: "調色",
|
|
14
|
+
paletteTitle: "主題色",
|
|
15
|
+
reset: "重設",
|
|
16
|
+
hueValue: "色相 {n}",
|
|
17
|
+
pinned: "置頂",
|
|
18
|
+
wordsCount: "{n} 字",
|
|
19
|
+
minutesCount: "約 {n} 分鐘",
|
|
20
|
+
prevNext: "上下篇",
|
|
21
|
+
prevPost: "上一篇",
|
|
22
|
+
nextPost: "下一篇",
|
|
23
|
+
firstPost: "已經是最早的一篇",
|
|
24
|
+
lastPost: "已經是最新的一篇",
|
|
25
|
+
relatedPosts: "相關文章",
|
|
26
|
+
randomPosts: "隨機文章",
|
|
27
|
+
smartRecommend: "智慧推薦",
|
|
28
|
+
randomRecommend: "隨機推薦",
|
|
29
|
+
noRelatedPosts: "暫無相關文章",
|
|
30
|
+
noRandomPosts: "暫無隨機文章",
|
|
31
|
+
licensePrefix: "本文採用",
|
|
32
|
+
licenseSuffix: "授權,轉載請註明出處與作者。",
|
|
33
|
+
toc: "目錄",
|
|
34
|
+
tocTitle: "目 錄",
|
|
35
|
+
pagination: "分頁",
|
|
36
|
+
prevPage: "上一頁",
|
|
37
|
+
nextPage: "下一頁",
|
|
38
|
+
pageOf: "第 {current} / {total} 頁",
|
|
39
|
+
closeAnnouncement: "關閉公告",
|
|
40
|
+
closeAd: "關閉廣告",
|
|
41
|
+
categories: "分類",
|
|
42
|
+
tags: "標籤",
|
|
43
|
+
friendLinks: "友鏈",
|
|
44
|
+
siteInfo: "網站資訊",
|
|
45
|
+
posts: "文章",
|
|
46
|
+
running: "已運行",
|
|
47
|
+
daysCount: "{n} 天",
|
|
48
|
+
dayPostsCount: "{n} 篇",
|
|
49
|
+
heatWeekTip: "{m}第{w}週 · {n}",
|
|
50
|
+
tagsMore: "…更多",
|
|
51
|
+
calendarHome: "回到本月",
|
|
52
|
+
calendarPick: "選擇年月",
|
|
53
|
+
layoutSwitch: "切換列表 / 網格檢視",
|
|
54
|
+
layoutList: "列表檢視",
|
|
55
|
+
layoutGrid: "網格檢視",
|
|
56
|
+
emptyPosts: "還沒有文章,去 content/posts 寫一篇吧。",
|
|
57
|
+
pageN: "第 {n} 頁",
|
|
58
|
+
archive: "彙整",
|
|
59
|
+
home: "首頁",
|
|
60
|
+
categoryNav: "分類導覽",
|
|
61
|
+
more: "更多",
|
|
62
|
+
totalPosts: "共 {n} 篇文章",
|
|
63
|
+
postsInYear: "{n} 篇",
|
|
64
|
+
yearUnit: "{n} 年",
|
|
65
|
+
totalTags: "共 {n} 個標籤",
|
|
66
|
+
tagTitle: "標籤:{tag}",
|
|
67
|
+
totalCategories: "共 {n} 個分類",
|
|
68
|
+
categoryTitle: "分類:{category}",
|
|
69
|
+
publishedOn: "{date} 發布",
|
|
70
|
+
updatedOn: "{date} 更新",
|
|
71
|
+
pageviews: "瀏覽量",
|
|
72
|
+
notFound: "頁面不存在",
|
|
73
|
+
notFoundDesc: "頁面不存在,或已被移動到別處。",
|
|
74
|
+
backHome: "返回首頁",
|
|
75
|
+
about: "關於",
|
|
76
|
+
aboutEmpty: "在 src/content/spec/about.md 中撰寫本頁內容",
|
|
77
|
+
avatarAlt: "{name} 的頭像",
|
|
78
|
+
noFriends: "還沒有友鏈",
|
|
79
|
+
applyFriend: "申請友鏈",
|
|
80
|
+
/* 番組追番頁 */
|
|
81
|
+
bangumiEmpty: "還沒有在看中的番劇",
|
|
82
|
+
bangumiFail: "番組資料拉取失敗,請重新建置網站後重試",
|
|
83
|
+
epProgress: "看到 {n} / {total} 集",
|
|
84
|
+
epProgressOpen: "看到 {n} 集",
|
|
85
|
+
myRate: "我的評分 {n}",
|
|
86
|
+
guestbook: "留言板",
|
|
87
|
+
guestbookSub: "歡迎留下你的足跡",
|
|
88
|
+
commentComingSoon: "留言區即將開放,敬請期待。",
|
|
89
|
+
qrAlt: "{name} 收款碼",
|
|
90
|
+
goSponsor: "前往打賞",
|
|
91
|
+
qrPlaceholder: "收款碼佔位",
|
|
92
|
+
sponsorList: "打賞名單",
|
|
93
|
+
noSponsors: "還沒有打賞,快來做第一個吧",
|
|
94
|
+
search: "搜尋",
|
|
95
|
+
searchShortcut: "按 / 或 Ctrl+K 快速搜尋",
|
|
96
|
+
searchLoading: "搜尋中…",
|
|
97
|
+
searchNoResults: "沒有找到相關結果",
|
|
98
|
+
searchUnavailable: "搜尋索引需建置後可用",
|
|
99
|
+
searchTypeSomething: "輸入關鍵字開始搜尋",
|
|
100
|
+
searchMore: "查看全部 {n} 筆結果",
|
|
101
|
+
searchResultsTotal: "共 {n} 筆結果",
|
|
102
|
+
galleryPhotos: "{n} 張照片",
|
|
103
|
+
backToAlbums: "返回相簿",
|
|
104
|
+
searchAlbums: "搜尋相簿…",
|
|
105
|
+
allTags: "全部",
|
|
106
|
+
galleryNoAlbums: "還沒有相簿,在主題設定 gallery.albums 中新增。",
|
|
107
|
+
galleryNoPhotos: "相簿裡還沒有照片",
|
|
108
|
+
/* 說說頁 */
|
|
109
|
+
momentsTitle: "說說",
|
|
110
|
+
momentsEmpty: "還沒有說說。",
|
|
111
|
+
encryptedAlbum: "加密相簿",
|
|
112
|
+
searchAlbumNoResults: "沒有符合的相簿",
|
|
113
|
+
postEncrypted: "已加密",
|
|
114
|
+
passwordProtected: "密碼保護",
|
|
115
|
+
passwordProtectedDesc: "此內容已被密碼保護,請輸入密碼查看。",
|
|
116
|
+
passwordHint: "提示",
|
|
117
|
+
passwordPlaceholder: "請輸入密碼",
|
|
118
|
+
passwordSubmit: "解鎖",
|
|
119
|
+
passwordError: "密碼錯誤,請重試。",
|
|
120
|
+
close: "關閉",
|
|
121
|
+
prevImage: "上一張",
|
|
122
|
+
nextImage: "下一張",
|
|
123
|
+
copied: "已複製",
|
|
124
|
+
fullscreen: "全螢幕",
|
|
125
|
+
slideshow: "自動播放",
|
|
126
|
+
thumbs: "縮圖列",
|
|
127
|
+
/* 分享海報 */
|
|
128
|
+
sharePoster: "產生分享海報",
|
|
129
|
+
generatingPoster: "海報產生中…",
|
|
130
|
+
copyLink: "複製連結",
|
|
131
|
+
savePoster: "儲存海報",
|
|
132
|
+
scanToRead: "掃碼閱讀全文",
|
|
133
|
+
author: "作者",
|
|
134
|
+
supportTitle: "支持一下",
|
|
135
|
+
supportDesc: "如果這篇文章對你有幫助,歡迎打賞或分享。",
|
|
136
|
+
sponsorMe: "打賞支持",
|
|
137
|
+
lastModifiedPrefix: "最後更新於 ",
|
|
138
|
+
lastModifiedDaysAgo: ",距今已過 {days} 天",
|
|
139
|
+
lastModifiedOutdated: "部分內容可能已過時,請注意資訊時效。",
|
|
140
|
+
/* 專題 */
|
|
141
|
+
series: "專題",
|
|
142
|
+
seriesTotal: "共 {n} 個專題",
|
|
143
|
+
seriesPostsCount: "{n} 篇",
|
|
144
|
+
seriesUpdated: "更新於 {date}",
|
|
145
|
+
seriesEmpty: "還沒有專題,在文章 frontmatter 中填寫 series 欄位即可建立。",
|
|
146
|
+
seriesProgress: "{cur} / {total}",
|
|
147
|
+
seriesCatalog: "專題目錄",
|
|
148
|
+
/* 音樂播放器 */
|
|
149
|
+
musicOpen: "音樂播放器",
|
|
150
|
+
musicClose: "收起播放器",
|
|
151
|
+
musicPlay: "播放",
|
|
152
|
+
musicPause: "暫停",
|
|
153
|
+
musicPrev: "上一首",
|
|
154
|
+
musicNext: "下一首",
|
|
155
|
+
musicModeList: "清單循環",
|
|
156
|
+
musicModeOne: "單曲循環",
|
|
157
|
+
musicModeRandom: "隨機播放",
|
|
158
|
+
musicLyrics: "歌詞",
|
|
159
|
+
musicPlaylist: "播放清單",
|
|
160
|
+
musicNoLyrics: "暫無歌詞",
|
|
161
|
+
musicEmpty: "播放清單為空",
|
|
162
|
+
musicSeek: "播放進度",
|
|
163
|
+
musicVolume: "音量",
|
|
164
|
+
l2dHome: "回到首頁",
|
|
165
|
+
l2dScrollTop: "回到頂端",
|
|
166
|
+
l2dSleep: "休息一下",
|
|
167
|
+
l2dSwitchModel: "換個模型",
|
|
168
|
+
/* 程式碼摺疊 */
|
|
169
|
+
codeFoldExpand: "展開程式碼",
|
|
170
|
+
codeFoldCollapse: "收合程式碼",
|
|
171
|
+
codeFoldExpanded: "程式碼已展開",
|
|
172
|
+
codeFoldCollapsed: "程式碼已收合",
|
|
173
|
+
/* 桌布 */
|
|
174
|
+
wpPlay: "播放桌布影片",
|
|
175
|
+
wpPause: "暫停桌布影片",
|
|
176
|
+
wpPrev: "上一段影片",
|
|
177
|
+
wpNext: "下一段影片",
|
|
178
|
+
wpMode: "切換桌布模式",
|
|
179
|
+
/* 顯示設定面板(§4.103):齒輪入口 + 三 Tab + 各組控制項 */
|
|
180
|
+
displaySettings: "顯示設定",
|
|
181
|
+
tabAppearance: "外觀",
|
|
182
|
+
tabWallpaper: "桌布",
|
|
183
|
+
tabEffects: "特效",
|
|
184
|
+
wallpaperMode: "桌布模式",
|
|
185
|
+
modeBanner: "橫幅桌布",
|
|
186
|
+
modeFullscreen: "全螢幕桌布",
|
|
187
|
+
modeOverlay: "覆蓋透明",
|
|
188
|
+
modeNone: "純色背景",
|
|
189
|
+
fullscreenLayout: "全螢幕佈局",
|
|
190
|
+
layoutClassic: "經典模式",
|
|
191
|
+
layoutHero: "Hero 模式",
|
|
192
|
+
wallpaperSettings: "桌布設定",
|
|
193
|
+
homeWallpaperTitle: "首頁桌布標題",
|
|
194
|
+
wallpaperCarousel: "桌布輪播",
|
|
195
|
+
wavesAnimation: "水波紋動畫",
|
|
196
|
+
gradientTransition: "漸變過渡",
|
|
197
|
+
overlaySettings: "透明設定",
|
|
198
|
+
wallpaperOpacity: "桌布透明度",
|
|
199
|
+
backgroundBlur: "背景模糊度",
|
|
200
|
+
cardOpacity: "卡片透明度",
|
|
201
|
+
sakuraEffect: "櫻花特效",
|
|
202
|
+
/* 網站資訊總字數 */
|
|
203
|
+
totalWords: "總字數",
|
|
204
|
+
/* 側欄音樂小工具 */
|
|
205
|
+
musicWidget: "音樂",
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export default zhTW;
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
rehypeExternalLinks,
|
|
42
42
|
rehypeFigure,
|
|
43
43
|
rehypeFirstImage,
|
|
44
|
+
rehypeImageEnhance,
|
|
44
45
|
rehypeNoReferrer,
|
|
45
46
|
} from "./plugins/rehype.js";
|
|
46
47
|
import {
|
|
@@ -59,6 +60,7 @@ import { remarkReadingTime } from "./plugins/remark-reading-time.js";
|
|
|
59
60
|
import { type ThemeConfig, themeConfigSchema } from "./schema.js";
|
|
60
61
|
import {
|
|
61
62
|
overridesPlugin,
|
|
63
|
+
pagefindDevPlugin,
|
|
62
64
|
themeConfigPlugin,
|
|
63
65
|
themeLqipsPlugin,
|
|
64
66
|
} from "./vite.js";
|
|
@@ -128,6 +130,8 @@ export function defineTheme(userConfig: ThemeConfig): AstroIntegration[] {
|
|
|
128
130
|
rehypeExternalLinks,
|
|
129
131
|
// 图片防盗链(§4.87):命中域名外链图加 no-referrer(domains 空时插件空转)
|
|
130
132
|
[rehypeNoReferrer, { domains: config.noReferrer.domains }],
|
|
133
|
+
// 正文图片增强(§4.106):lazy/decoding 注入 + public 图 LQIP 渐变背景
|
|
134
|
+
rehypeImageEnhance,
|
|
131
135
|
rehypeCallouts,
|
|
132
136
|
rehypeFigure,
|
|
133
137
|
rehypeFirstImage,
|
|
@@ -310,6 +314,8 @@ export function defineTheme(userConfig: ThemeConfig): AstroIntegration[] {
|
|
|
310
314
|
["/", "index.astro"],
|
|
311
315
|
["/page/[page]", "page/[page].astro"],
|
|
312
316
|
["/archive", "archive.astro"],
|
|
317
|
+
// 独立搜索页(§4.104):浮层「查看全部」入口的目标页,客户端查询分页
|
|
318
|
+
["/search", "search.astro"],
|
|
313
319
|
["/tags", "tags/index.astro"],
|
|
314
320
|
// §4.87 分类/标签分页:第一页 /tags/<tag>/(page 段省略),后续 /2/ 起
|
|
315
321
|
["/tags/[tag]/[...page]", "tags/[tag]/[...page].astro"],
|
|
@@ -344,6 +350,10 @@ export function defineTheme(userConfig: ThemeConfig): AstroIntegration[] {
|
|
|
344
350
|
if (config.pages.moments) {
|
|
345
351
|
routes.push(["/moments/[...page]", "moments/[...page].astro"]);
|
|
346
352
|
}
|
|
353
|
+
// 番组追番页(§4.93):构建期拉 bgm.tv「在看」收藏;enabled + userId 双条件才注入
|
|
354
|
+
if (config.bangumi.enabled && config.bangumi.userId.trim()) {
|
|
355
|
+
routes.push(["/bangumi", "bangumi.astro"]);
|
|
356
|
+
}
|
|
347
357
|
// 相关/随机文章推荐(P1-12):随机栏数据源
|
|
348
358
|
if (config.relatedPosts.enabled) {
|
|
349
359
|
routes.push(["/api/allPostMeta.json", "api/allPostMeta.json.ts"]);
|
|
@@ -352,6 +362,12 @@ export function defineTheme(userConfig: ThemeConfig): AstroIntegration[] {
|
|
|
352
362
|
if (config.og.enabled) {
|
|
353
363
|
routes.push(["/og/[...slug]", "og/[...slug].ts"]);
|
|
354
364
|
}
|
|
365
|
+
// PWA(§4.95):manifest 与 Service Worker 为构建期生成的端点,enabled 才注入;
|
|
366
|
+
// head 里的 manifest 链接与 SW 注册脚本由 BaseLayout 按 pwa.enabled 输出
|
|
367
|
+
if (config.pwa.enabled) {
|
|
368
|
+
routes.push(["/manifest.webmanifest", "manifest.webmanifest.ts"]);
|
|
369
|
+
routes.push(["/sw.js", "sw.js.ts"]);
|
|
370
|
+
}
|
|
355
371
|
for (const [pattern, file] of routes) {
|
|
356
372
|
injectRoute({
|
|
357
373
|
pattern,
|
|
@@ -368,6 +384,9 @@ export function defineTheme(userConfig: ThemeConfig): AstroIntegration[] {
|
|
|
368
384
|
themeConfigPlugin(config),
|
|
369
385
|
themeLqipsPlugin(),
|
|
370
386
|
overridesPlugin(),
|
|
387
|
+
// dev 下把 dist/pagefind 经前置中间件直接服务(astro-pagefind
|
|
388
|
+
// 同款方案),开发期无需构建即可搜索;索引缺失时放行走 404
|
|
389
|
+
pagefindDevPlugin(),
|
|
371
390
|
],
|
|
372
391
|
},
|
|
373
392
|
});
|
|
@@ -385,6 +404,10 @@ export function defineTheme(userConfig: ThemeConfig): AstroIntegration[] {
|
|
|
385
404
|
` const lqips: Record<string, string>;`,
|
|
386
405
|
` export default lqips;`,
|
|
387
406
|
`}`,
|
|
407
|
+
`declare module 'virtual:theme/thumbs' {`,
|
|
408
|
+
` const thumbs: Record<string, { thumb: string; width: number; height: number }>;`,
|
|
409
|
+
` export default thumbs;`,
|
|
410
|
+
`}`,
|
|
388
411
|
`declare module 'virtual:theme/*' {`,
|
|
389
412
|
` const Component: any;`,
|
|
390
413
|
` export default Component;`,
|
package/src/plugins/rehype.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* rehype 插件集(M3a,全部零依赖手写 hast 遍历)
|
|
3
3
|
*
|
|
4
|
-
* 挂载顺序(index.ts):externalLinks →
|
|
4
|
+
* 挂载顺序(index.ts):externalLinks → noReferrer → imageEnhance → callouts
|
|
5
|
+
* → figure → firstImage → anchor
|
|
5
6
|
* 代码块渲染自 §4.61 起由 Expressive Code 接管(index.ts 直连 rehype 内核),
|
|
6
7
|
* 本文件不再包含代码块插件。
|
|
7
8
|
* 注意:hast 节点类名有两种形态——shiki/EC 产出用 HTML 属性名直接写 properties
|
|
8
9
|
* (class 为字符串而非 className 数组);remark-rehype 原始节点则是 className
|
|
9
10
|
* 数组。读取类名务必走 getClassList 兼容两种形态。
|
|
10
11
|
*/
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
|
|
11
15
|
type Transformer = (tree: any, file: any) => void;
|
|
12
16
|
|
|
13
17
|
/** 浅遍历辅助:对 node.children 逐个调用 fn,fn 可通过返回 'skip' 跳过子树 */
|
|
@@ -35,12 +39,20 @@ function getClassList(props: any): string[] {
|
|
|
35
39
|
/* ---------------------------------------------------------------- 外链 */
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
|
-
* 外链新窗口打开:http(s) 绝对链接追加 target="_blank" rel="noopener noreferrer"
|
|
42
|
+
* 外链新窗口打开:http(s) 绝对链接追加 target="_blank" rel="noopener noreferrer";
|
|
43
|
+
* 外链 <img> 追加 data-external 标记(暗色外链图适配 §4.89 的 CSS 挂点,标记本身零副作用)
|
|
39
44
|
*/
|
|
40
45
|
export function rehypeExternalLinks(): Transformer {
|
|
41
46
|
return (tree) => {
|
|
42
47
|
visit(tree, (node) => {
|
|
43
|
-
if (node.type !== "element"
|
|
48
|
+
if (node.type !== "element") return;
|
|
49
|
+
if (node.tagName === "img") {
|
|
50
|
+
if (/^https?:\/\//i.test(String(node.properties?.src ?? ""))) {
|
|
51
|
+
node.properties["data-external"] = "true";
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (node.tagName !== "a") return;
|
|
44
56
|
const href = String(node.properties?.href ?? "");
|
|
45
57
|
if (!/^https?:\/\//i.test(href)) return;
|
|
46
58
|
node.properties.target = "_blank";
|
|
@@ -225,7 +237,67 @@ export function rehypeCallouts(): Transformer {
|
|
|
225
237
|
};
|
|
226
238
|
}
|
|
227
239
|
|
|
228
|
-
/*
|
|
240
|
+
/* -------------------------------------------------------- 正文图片增强 */
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 正文图片增强(§4.106;§4.107 扩展外链):
|
|
244
|
+
* 1. 注入 loading="lazy" + decoding="async"(已有值不覆盖——作者显式声明优先);
|
|
245
|
+
* 2. 图片注入 LQIP 渐变背景:直接内联在 img style 上,图片加载后位图自然
|
|
246
|
+
* 盖住渐变,无需 wrapper 结构与客户端 JS。本地 public 图(src 以 / 开头)
|
|
247
|
+
* 查 `public:` 键,外链图(http(s) 绝对 URL)查 `url:` 键——键规则与
|
|
248
|
+
* scripts/generate-lqips.mjs 一致,外链 URL 须与渲染期逐字符一致。
|
|
249
|
+
* lqips.json 读站点根(file.cwd)并模块级缓存——rehype transformer 运行于
|
|
250
|
+
* 纯 Node,拿不到 vite 虚拟模块;缓存按构建进程生命周期,dev 下改 lqips.json
|
|
251
|
+
* 需重启 dev server(与 vite 插件 configResolved 的读取时机一致)。
|
|
252
|
+
*/
|
|
253
|
+
let lqipTable: Record<string, string> | null = null;
|
|
254
|
+
|
|
255
|
+
function loadLqipTable(cwd: string): Record<string, string> {
|
|
256
|
+
if (lqipTable) return lqipTable;
|
|
257
|
+
// content layer 的 VFile cwd 不一定是站点根,依次尝试 file.cwd 与 process.cwd()
|
|
258
|
+
for (const dir of [cwd, process.cwd()]) {
|
|
259
|
+
try {
|
|
260
|
+
lqipTable = JSON.parse(
|
|
261
|
+
fs.readFileSync(path.join(dir, "lqips.json"), "utf8"),
|
|
262
|
+
);
|
|
263
|
+
break;
|
|
264
|
+
} catch {
|
|
265
|
+
/* 目录不对,尝试下一个 */
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
lqipTable ??= {};
|
|
269
|
+
return lqipTable;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** src → lqips.json 键(本地 public: 前缀 / 外链 url: 前缀),非两者返回 null */
|
|
273
|
+
function lqipKey(src: string): string | null {
|
|
274
|
+
if (src.startsWith("/")) return `public:${src.replace(/^\//, "")}`;
|
|
275
|
+
if (/^https?:\/\//i.test(src)) return `url:${src}`;
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function rehypeImageEnhance(): Transformer {
|
|
280
|
+
return (tree, file) => {
|
|
281
|
+
const lqips = loadLqipTable(String(file.cwd ?? ""));
|
|
282
|
+
visit(tree, (node) => {
|
|
283
|
+
if (node.type !== "element" || node.tagName !== "img") return;
|
|
284
|
+
node.properties.loading ??= "lazy";
|
|
285
|
+
node.properties.decoding ??= "async";
|
|
286
|
+
const src = String(node.properties.src ?? "");
|
|
287
|
+
const key = lqipKey(src);
|
|
288
|
+
if (!key) return;
|
|
289
|
+
const hex = lqips[key];
|
|
290
|
+
if (!hex || !/^[0-9a-f]{18}$/i.test(hex)) return;
|
|
291
|
+
const grad = `linear-gradient(135deg, #${hex.slice(0, 6)} 0%, #${hex.slice(6, 12)} 50%, #${hex.slice(12, 18)} 100%)`;
|
|
292
|
+
const style = String(node.properties.style ?? "");
|
|
293
|
+
node.properties.style = style
|
|
294
|
+
? `${style};background:${grad}`
|
|
295
|
+
: `background:${grad}`;
|
|
296
|
+
});
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* -------------------------------------------------------------- figure */
|
|
229
301
|
|
|
230
302
|
/**
|
|
231
303
|
* 独立段落中的单图 → <figure>;img 的 title 属性 → <figcaption>。
|