@windcoder/astro-theme-orchid 0.4.0 → 0.4.2
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/package.json +1 -1
- package/scripts/generate-lqips.mjs +10 -5
- package/src/components/BaseLayout.astro +5 -2
- package/src/components/PostCard.astro +5 -1
- package/src/index.ts +11 -0
- package/src/routes/posts/[...slug].astro +7 -2
- package/src/schema.ts +2 -1
- package/src/scripts/app.ts +22 -10
- package/src/styles/global.css +45 -19
- package/src/utils/image.ts +23 -12
- package/src/utils/thumb.ts +6 -1
package/package.json
CHANGED
|
@@ -169,8 +169,10 @@ function collectRemoteUrls() {
|
|
|
169
169
|
const walkMd = (dir) => {
|
|
170
170
|
const out = [];
|
|
171
171
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
172
|
-
if (entry.isDirectory())
|
|
173
|
-
|
|
172
|
+
if (entry.isDirectory())
|
|
173
|
+
out.push(...walkMd(path.join(dir, entry.name)));
|
|
174
|
+
else if (/\.(md|mdx)$/i.test(entry.name))
|
|
175
|
+
out.push(path.join(dir, entry.name));
|
|
174
176
|
}
|
|
175
177
|
return out;
|
|
176
178
|
};
|
|
@@ -229,7 +231,9 @@ async function mapPool(items, limit, fn) {
|
|
|
229
231
|
try {
|
|
230
232
|
out[idx] = await fn(items[idx]);
|
|
231
233
|
} catch (err) {
|
|
232
|
-
console.warn(
|
|
234
|
+
console.warn(
|
|
235
|
+
`[lqip] 外链抓取失败,跳过: ${items[idx]} — ${err.message}`,
|
|
236
|
+
);
|
|
233
237
|
}
|
|
234
238
|
}
|
|
235
239
|
}
|
|
@@ -282,11 +286,12 @@ try {
|
|
|
282
286
|
|
|
283
287
|
fs.writeFileSync(
|
|
284
288
|
lqipFile,
|
|
285
|
-
|
|
289
|
+
// tab 缩进与 Biome format 一致:生成物入库即合规,重跑不会反复触发 CI format 报错
|
|
290
|
+
JSON.stringify(Object.fromEntries(lqipEntries), null, "\t") + "\n",
|
|
286
291
|
);
|
|
287
292
|
fs.writeFileSync(
|
|
288
293
|
thumbsFile,
|
|
289
|
-
JSON.stringify(Object.fromEntries(thumbEntries), null,
|
|
294
|
+
JSON.stringify(Object.fromEntries(thumbEntries), null, "\t") + "\n",
|
|
290
295
|
);
|
|
291
296
|
console.log(
|
|
292
297
|
`[lqip] public ${files.length} 张 + 外链 ${remoteUrls.length - pending.length + fetched.filter(Boolean).length} 条 → ${path.relative(cwd, lqipFile)} + ${path.relative(cwd, thumbsFile)}`,
|
|
@@ -77,7 +77,9 @@ const visualMode =
|
|
|
77
77
|
显示,不再隐藏整个 stage——保证非首页有页面标题横幅且刷新不丢失样式 */
|
|
78
78
|
const stageMode =
|
|
79
79
|
visualMode === "none" || stageDesktop.length === 0 ? null : visualMode;
|
|
80
|
-
|
|
80
|
+
/* stage 未渲染(无图/none)同样视为隐藏:html[data-wp-stage-hidden] 驱动 CSS 取消
|
|
81
|
+
banner/fullscreen 的正文上拉与 hero 首屏下推,避免空壁纸时内容顶入 Header 底下 */
|
|
82
|
+
const stageHidden = !stageMode;
|
|
81
83
|
const stageVisible = Boolean(stageMode);
|
|
82
84
|
/* hero 文案(§4.98):首页用 homeText 配置(标题+副标题/打字机),非首页用页面
|
|
83
85
|
传入的 title(归档→"归档"、文章→文章标题等)。仅 stage 可见时渲染,叠加在
|
|
@@ -97,7 +99,8 @@ const staticSub = typerOn ? "" : wpSubs.length ? wpPick(wpSubs) : "";
|
|
|
97
99
|
const heroTitle = isActualHome ? homeText.title : title;
|
|
98
100
|
const heroSub = isActualHome ? staticSub : "";
|
|
99
101
|
const heroTyperOn = isActualHome && typerOn;
|
|
100
|
-
const heroInStage =
|
|
102
|
+
const heroInStage =
|
|
103
|
+
Boolean(stageMode) && (isActualHome ? hasHeroText : Boolean(title));
|
|
101
104
|
/* 底部装饰分端归一化:布尔简写展开为两端同值 */
|
|
102
105
|
const wpWaves =
|
|
103
106
|
typeof wp.common.waves === "boolean"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import config from "virtual:theme/config";
|
|
3
3
|
import { t } from "../i18n/index.js";
|
|
4
|
+
import { resolveSiteImage } from "../utils/image.js";
|
|
4
5
|
import { getLqipStyle } from "../utils/lqip.js";
|
|
5
6
|
import { getThumbInfo } from "../utils/thumb.js";
|
|
6
7
|
import { formatDate, type PostLike, resolveCover } from "../utils.js";
|
|
@@ -25,7 +26,10 @@ const { cover: rawCover, retries } = resolveCover(
|
|
|
25
26
|
config.postList.randomCover.apis,
|
|
26
27
|
post.id,
|
|
27
28
|
);
|
|
28
|
-
|
|
29
|
+
/* §4.110:封面三格式统一语义(对齐 §4.108 其余消费点)——相对路径(站点 src/
|
|
30
|
+
下)构建期解析为产物 URL,public / 外链原样直通 */
|
|
31
|
+
const resolvedCover = rawCover ? await resolveSiteImage(rawCover) : rawCover;
|
|
32
|
+
const cover = config.postList.showCover ? resolvedCover : null;
|
|
29
33
|
/* §4.106:列表封面用缩略图(外链/未命中自动回退原图;点击进文章页看正文原图) */
|
|
30
34
|
const thumb = cover ? getThumbInfo(cover) : null;
|
|
31
35
|
const classes = [
|
package/src/index.ts
CHANGED
|
@@ -257,6 +257,17 @@ export function defineTheme(userConfig: ThemeConfig): AstroIntegration[] {
|
|
|
257
257
|
updateConfig({
|
|
258
258
|
// 主题路由全部按尾斜杠组织(如 /posts/xxx/),统一强制,dev 与 prod 行为一致
|
|
259
259
|
trailingSlash: "always",
|
|
260
|
+
// 尾斜杠强制的连带修复(§4.109):trailingSlash:"always" 会把 astro
|
|
261
|
+
// 内部图片端点 /_image 的注册 pattern 变成 ^/_image/$(routing/pattern.js
|
|
262
|
+
// getPattern 按 trailingSlash 生成),而图片服务生成的 URL 恒为
|
|
263
|
+
// /_image?href=...(assets/services/service.js getURL 不加尾斜杠),
|
|
264
|
+
// 两者永远匹配不上 → dev 下 /_image 全量 404(落到 404 兜底页,
|
|
265
|
+
// 头像/md 内联图/封面等所有走图片管线的图全部裂图)。
|
|
266
|
+
// 端点路由改为带尾斜杠的 /_image/:注册 pattern(parseRoute 对
|
|
267
|
+
// "/_image/" 同样生成 ^/_image/$)与生成 URL(getURL 直接取本配置
|
|
268
|
+
// 拼 query)自动同步;parseURL 只读 query 不看 pathname,不受影响。
|
|
269
|
+
// 静态构建不走该端点(构建期直接编码出图),仅影响 dev/SSR 行为。
|
|
270
|
+
image: { endpoint: { route: "/_image/" } },
|
|
260
271
|
vite: {
|
|
261
272
|
plugins: [tailwindcss()],
|
|
262
273
|
// 构建优化说明(§4.63):参考站的 vite.build 块(minify:"esbuild"、
|
|
@@ -15,6 +15,7 @@ import Sidebar from "virtual:theme/components/Sidebar.astro";
|
|
|
15
15
|
import Toc from "virtual:theme/components/Toc.astro";
|
|
16
16
|
import config from "virtual:theme/config";
|
|
17
17
|
import { t } from "../../i18n/index.js";
|
|
18
|
+
import { resolveSiteImage } from "../../utils/image.js";
|
|
18
19
|
import { getLqipStyle } from "../../utils/lqip.js";
|
|
19
20
|
import { getRelatedPosts } from "../../utils/related.js";
|
|
20
21
|
import {
|
|
@@ -98,14 +99,18 @@ export async function getStaticPaths() {
|
|
|
98
99
|
|
|
99
100
|
const { post, prev, next, related, posts, seriesInfo } = Astro.props;
|
|
100
101
|
const d = post.data;
|
|
101
|
-
/* 封面解析(§4.33):image:"api" 时取确定性随机图;figure 与分享海报共用同一 URL
|
|
102
|
-
|
|
102
|
+
/* 封面解析(§4.33):image:"api" 时取确定性随机图;figure 与分享海报共用同一 URL。
|
|
103
|
+
§4.110:三格式统一语义(同 PostCard)——相对路径构建期解析为产物 URL */
|
|
104
|
+
const { cover: rawArticleCover, retries: coverRetries } = resolveCover(
|
|
103
105
|
d.image,
|
|
104
106
|
d.firstImage,
|
|
105
107
|
config.postList.fallbackCover,
|
|
106
108
|
config.postList.randomCover.apis,
|
|
107
109
|
post.id,
|
|
108
110
|
);
|
|
111
|
+
const articleCover = rawArticleCover
|
|
112
|
+
? await resolveSiteImage(rawArticleCover)
|
|
113
|
+
: rawArticleCover;
|
|
109
114
|
const { Content, headings, remarkPluginFrontmatter } = await render(post);
|
|
110
115
|
const { words, minutes } = remarkPluginFrontmatter as {
|
|
111
116
|
words?: number;
|
package/src/schema.ts
CHANGED
|
@@ -42,7 +42,8 @@ const sidebarWidgetBase = z.object({
|
|
|
42
42
|
* 四种模式 html[data-wallpaper-mode] 驱动:banner 横幅 / fullscreen 全屏首屏 /
|
|
43
43
|
* overlay 覆盖透明垫底 / none 纯色不渲染。
|
|
44
44
|
* 图源遵循三格式约定(§4.97):"/" 开头按 public 路径原样输出;http(s)://、//、data:
|
|
45
|
-
* 外链原样输出;其余相对路径相对站点 src/
|
|
45
|
+
* 外链原样输出;其余相对路径相对站点 src/ 目录解析(构建期查表,支持图片/视频,
|
|
46
|
+
* §4.108:不写死 assets/ 前缀)。
|
|
46
47
|
* 提取为模块级 const 以便使用函数默认值 .default(() => wallpaperSchema.parse({})):
|
|
47
48
|
* Zod 的 .default(x) 在输入缺失时直接取 x、不做二次 parse(v3/v4 行为一致),
|
|
48
49
|
* 本段嵌套深达 4 层,最外层再写一份完整字面量维护成本过高;改用 parse({}) 在
|
package/src/scripts/app.ts
CHANGED
|
@@ -196,7 +196,10 @@ async function initLightbox() {
|
|
|
196
196
|
优先打开原图;无数据时沿用 img 自身 src 与 natural 尺寸 */
|
|
197
197
|
if (img.dataset.fullSrc) {
|
|
198
198
|
itemData.src = img.dataset.fullSrc;
|
|
199
|
-
if (
|
|
199
|
+
if (
|
|
200
|
+
Number(img.dataset.fullWidth) > 0 &&
|
|
201
|
+
Number(img.dataset.fullHeight) > 0
|
|
202
|
+
) {
|
|
200
203
|
itemData.width = Number(img.dataset.fullWidth);
|
|
201
204
|
itemData.height = Number(img.dataset.fullHeight);
|
|
202
205
|
}
|
|
@@ -685,13 +688,16 @@ function pagerNums(page: number, total: number): Array<number | "ellipsis"> {
|
|
|
685
688
|
|
|
686
689
|
function searchPageHref(q: string, page: number): string {
|
|
687
690
|
return (
|
|
688
|
-
"/search/?q=" +
|
|
689
|
-
encodeURIComponent(q) +
|
|
690
|
-
(page > 1 ? "&page=" + page : "")
|
|
691
|
+
"/search/?q=" + encodeURIComponent(q) + (page > 1 ? "&page=" + page : "")
|
|
691
692
|
);
|
|
692
693
|
}
|
|
693
694
|
|
|
694
|
-
function renderSearchPager(
|
|
695
|
+
function renderSearchPager(
|
|
696
|
+
pager: HTMLElement,
|
|
697
|
+
q: string,
|
|
698
|
+
cur: number,
|
|
699
|
+
pages: number,
|
|
700
|
+
) {
|
|
695
701
|
if (pages <= 1) {
|
|
696
702
|
pager.hidden = true;
|
|
697
703
|
return;
|
|
@@ -712,7 +718,8 @@ function renderSearchPager(pager: HTMLElement, q: string, cur: number, pages: nu
|
|
|
712
718
|
s.textContent = label;
|
|
713
719
|
return s;
|
|
714
720
|
};
|
|
715
|
-
if (cur > 1)
|
|
721
|
+
if (cur > 1)
|
|
722
|
+
nums.appendChild(mk(searchPageHref(q, cur - 1), "‹", "page-item"));
|
|
716
723
|
else nums.appendChild(span("‹"));
|
|
717
724
|
for (const n of pagerNums(cur, pages)) {
|
|
718
725
|
if (n === "ellipsis") {
|
|
@@ -730,19 +737,22 @@ function renderSearchPager(pager: HTMLElement, q: string, cur: number, pages: nu
|
|
|
730
737
|
nums.appendChild(mk(searchPageHref(q, n), String(n), "page-item"));
|
|
731
738
|
}
|
|
732
739
|
}
|
|
733
|
-
if (cur < pages)
|
|
740
|
+
if (cur < pages)
|
|
741
|
+
nums.appendChild(mk(searchPageHref(q, cur + 1), "›", "page-item"));
|
|
734
742
|
else nums.appendChild(span("›"));
|
|
735
743
|
|
|
736
744
|
const adj = doc.createElement("div");
|
|
737
745
|
adj.className = "pg-adj";
|
|
738
|
-
if (cur > 1)
|
|
746
|
+
if (cur > 1)
|
|
747
|
+
adj.appendChild(mk(searchPageHref(q, cur - 1), ui("prevPage"), "pg-prev"));
|
|
739
748
|
else adj.appendChild(span(ui("prevPage")));
|
|
740
749
|
const mid = doc.createElement("span");
|
|
741
750
|
mid.textContent = ui("pageOf")
|
|
742
751
|
.replace("{current}", String(cur))
|
|
743
752
|
.replace("{total}", String(pages));
|
|
744
753
|
adj.appendChild(mid);
|
|
745
|
-
if (cur < pages)
|
|
754
|
+
if (cur < pages)
|
|
755
|
+
adj.appendChild(mk(searchPageHref(q, cur + 1), ui("nextPage"), "pg-next"));
|
|
746
756
|
else adj.appendChild(span(ui("nextPage")));
|
|
747
757
|
|
|
748
758
|
pager.replaceChildren(nums, adj);
|
|
@@ -750,7 +760,9 @@ function renderSearchPager(pager: HTMLElement, q: string, cur: number, pages: nu
|
|
|
750
760
|
|
|
751
761
|
async function renderSearchPage() {
|
|
752
762
|
const seq = ++searchPageSeq;
|
|
753
|
-
const input = doc.getElementById(
|
|
763
|
+
const input = doc.getElementById(
|
|
764
|
+
"searchPageInput",
|
|
765
|
+
) as HTMLInputElement | null;
|
|
754
766
|
const meta = doc.getElementById("searchPageMeta");
|
|
755
767
|
const box = doc.getElementById("searchPageResults");
|
|
756
768
|
const pager = doc.getElementById("searchPagePager");
|
package/src/styles/global.css
CHANGED
|
@@ -710,7 +710,9 @@ html.dark #modeToggle .i-sun {
|
|
|
710
710
|
border-radius: 12px;
|
|
711
711
|
background: var(--surface);
|
|
712
712
|
color: var(--muted);
|
|
713
|
-
transition:
|
|
713
|
+
transition:
|
|
714
|
+
border-color 0.15s,
|
|
715
|
+
color 0.15s;
|
|
714
716
|
}
|
|
715
717
|
.search-page-bar:focus-within {
|
|
716
718
|
border-color: var(--accent-ink);
|
|
@@ -790,7 +792,9 @@ html[data-wallpaper-mode="fullscreen"]:not([data-wp-is-home]):not(
|
|
|
790
792
|
height: var(--banner-h, 60vh);
|
|
791
793
|
margin-top: calc(-1 * var(--header-h));
|
|
792
794
|
}
|
|
793
|
-
html[data-wallpaper-mode="fullscreen"]:not(
|
|
795
|
+
html[data-wallpaper-mode="fullscreen"]:not(
|
|
796
|
+
[data-wp-is-home]
|
|
797
|
+
)[data-wp-fs-layout="hero"]
|
|
794
798
|
.wp-stage {
|
|
795
799
|
position: fixed;
|
|
796
800
|
inset: 0;
|
|
@@ -886,7 +890,9 @@ html[data-wallpaper-mode="fullscreen"][data-wp-is-home] .wp-img {
|
|
|
886
890
|
/* overlay 形态的图片模糊/放大补偿(服务端算好 scale 防边缘露底),与 body 级旧口径一致;
|
|
887
891
|
非首页 fullscreen-hero 降级为 overlay,复用同一套模糊参数 */
|
|
888
892
|
html[data-wallpaper-mode="overlay"] .wp-img,
|
|
889
|
-
html[data-wallpaper-mode="fullscreen"]:not(
|
|
893
|
+
html[data-wallpaper-mode="fullscreen"]:not(
|
|
894
|
+
[data-wp-is-home]
|
|
895
|
+
)[data-wp-fs-layout="hero"]
|
|
890
896
|
.wp-img {
|
|
891
897
|
filter: blur(var(--wp-overlay-blur, 0px));
|
|
892
898
|
transform: scale(var(--wp-overlay-scale, 1));
|
|
@@ -1119,7 +1125,8 @@ html[data-wallpaper-mode="fullscreen"][data-wp-is-home] .wp-scroll-hint {
|
|
|
1119
1125
|
}
|
|
1120
1126
|
/* hero 布局:stage fixed 且 pointer-events:none,箭头需单独恢复可点击;
|
|
1121
1127
|
滚动后随 .wp-hero 同步淡出 */
|
|
1122
|
-
html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"]
|
|
1128
|
+
html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"]
|
|
1129
|
+
.wp-scroll-hint {
|
|
1123
1130
|
pointer-events: auto;
|
|
1124
1131
|
}
|
|
1125
1132
|
html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"].wp-scrolled
|
|
@@ -1169,7 +1176,7 @@ html[data-wallpaper-mode="overlay"] .card {
|
|
|
1169
1176
|
classic 滚下后正文区纯色背景(旧行为,无需额外规则)。
|
|
1170
1177
|
首屏两种布局一致:壁纸占满一屏 + 中央 hero 文案。 */
|
|
1171
1178
|
html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"][data-wp-is-home]
|
|
1172
|
-
|
|
1179
|
+
.wp-stage {
|
|
1173
1180
|
position: fixed;
|
|
1174
1181
|
inset: 0;
|
|
1175
1182
|
height: auto;
|
|
@@ -1180,15 +1187,14 @@ html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"][data-wp-is-home
|
|
|
1180
1187
|
/* 首屏让给 hero:正文流向下推一屏,滚上后覆盖 fixed 壁纸;
|
|
1181
1188
|
stage 隐藏(homeOnly 非首页 / JS 切走)时取消下推,避免顶部整屏空白 */
|
|
1182
1189
|
html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"][data-wp-is-home]:not(
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
.site-main {
|
|
1190
|
+
[data-wp-stage-hidden]
|
|
1191
|
+
) .site-main {
|
|
1186
1192
|
margin-top: 100vh;
|
|
1187
1193
|
}
|
|
1188
1194
|
/* hero 交互:fixed 后 pointer-events 被关,hero 文案纯展示无需点击(打字机亦然) */
|
|
1189
1195
|
/* hero 文字滚动淡出:滚离首屏后标题/副标题渐隐,不盖在正文区模糊壁纸上 */
|
|
1190
1196
|
html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"][data-wp-is-home]
|
|
1191
|
-
|
|
1197
|
+
.wp-hero {
|
|
1192
1198
|
transition: opacity 0.5s ease;
|
|
1193
1199
|
}
|
|
1194
1200
|
html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"][data-wp-is-home].wp-scrolled
|
|
@@ -1198,7 +1204,7 @@ html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"][data-wp-is-home
|
|
|
1198
1204
|
/* 滚动模糊过渡:滚过 40px(html.wp-scrolled,app.ts updateWallpaperScroll)后
|
|
1199
1205
|
壁纸加模糊 + 放大补偿 + 底部渐隐蒙层加深,保证模糊渐变在 blur=0 时也可见 */
|
|
1200
1206
|
html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"][data-wp-is-home]
|
|
1201
|
-
|
|
1207
|
+
.wp-img {
|
|
1202
1208
|
transition:
|
|
1203
1209
|
opacity 0.8s ease,
|
|
1204
1210
|
transform 0.8s ease,
|
|
@@ -1228,16 +1234,22 @@ html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"][data-wp-is-home
|
|
|
1228
1234
|
文章列表 .post-card 与分页 .page-item 不走 .card 类,需一并覆盖;
|
|
1229
1235
|
非首页 fullscreen-hero 降级为 overlay,同样需要半透明卡片 */
|
|
1230
1236
|
html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"][data-wp-is-home]
|
|
1231
|
-
|
|
1237
|
+
.card,
|
|
1232
1238
|
html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"][data-wp-is-home]
|
|
1233
1239
|
.post-card,
|
|
1234
1240
|
html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"][data-wp-is-home]
|
|
1235
1241
|
.page-item,
|
|
1236
|
-
html[data-wallpaper-mode="fullscreen"]:not(
|
|
1242
|
+
html[data-wallpaper-mode="fullscreen"]:not(
|
|
1243
|
+
[data-wp-is-home]
|
|
1244
|
+
)[data-wp-fs-layout="hero"]
|
|
1237
1245
|
.card,
|
|
1238
|
-
html[data-wallpaper-mode="fullscreen"]:not(
|
|
1246
|
+
html[data-wallpaper-mode="fullscreen"]:not(
|
|
1247
|
+
[data-wp-is-home]
|
|
1248
|
+
)[data-wp-fs-layout="hero"]
|
|
1239
1249
|
.post-card,
|
|
1240
|
-
html[data-wallpaper-mode="fullscreen"]:not(
|
|
1250
|
+
html[data-wallpaper-mode="fullscreen"]:not(
|
|
1251
|
+
[data-wp-is-home]
|
|
1252
|
+
)[data-wp-fs-layout="hero"]
|
|
1241
1253
|
.page-item {
|
|
1242
1254
|
background: color-mix(
|
|
1243
1255
|
in srgb,
|
|
@@ -1258,7 +1270,9 @@ html[data-wallpaper-mode="fullscreen"]:not([data-wp-is-home])[data-wp-fs-layout=
|
|
|
1258
1270
|
文字职责交还内容区标题(.page-title/.post-title-lg 等,见下方规则) */
|
|
1259
1271
|
html[data-wallpaper-mode="overlay"] .wp-hero,
|
|
1260
1272
|
html[data-wallpaper-mode="none"] .wp-hero,
|
|
1261
|
-
html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"]:not(
|
|
1273
|
+
html[data-wallpaper-mode="fullscreen"][data-wp-fs-layout="hero"]:not(
|
|
1274
|
+
[data-wp-is-home]
|
|
1275
|
+
)
|
|
1262
1276
|
.wp-hero {
|
|
1263
1277
|
display: none;
|
|
1264
1278
|
}
|
|
@@ -1272,10 +1286,22 @@ html[data-wp-hero="visible"][data-wallpaper-mode="banner"] .page-title,
|
|
|
1272
1286
|
html[data-wp-hero="visible"][data-wallpaper-mode="banner"] .page-sub,
|
|
1273
1287
|
html[data-wp-hero="visible"][data-wallpaper-mode="banner"] .post-title-lg,
|
|
1274
1288
|
html[data-wp-hero="visible"][data-wallpaper-mode="banner"] .archive-head,
|
|
1275
|
-
html[data-wp-hero="visible"][data-wallpaper-mode="fullscreen"]:not(
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1289
|
+
html[data-wp-hero="visible"][data-wallpaper-mode="fullscreen"]:not(
|
|
1290
|
+
[data-wp-fs-layout="hero"]
|
|
1291
|
+
)
|
|
1292
|
+
.page-title,
|
|
1293
|
+
html[data-wp-hero="visible"][data-wallpaper-mode="fullscreen"]:not(
|
|
1294
|
+
[data-wp-fs-layout="hero"]
|
|
1295
|
+
)
|
|
1296
|
+
.page-sub,
|
|
1297
|
+
html[data-wp-hero="visible"][data-wallpaper-mode="fullscreen"]:not(
|
|
1298
|
+
[data-wp-fs-layout="hero"]
|
|
1299
|
+
)
|
|
1300
|
+
.post-title-lg,
|
|
1301
|
+
html[data-wp-hero="visible"][data-wallpaper-mode="fullscreen"]:not(
|
|
1302
|
+
[data-wp-fs-layout="hero"]
|
|
1303
|
+
)
|
|
1304
|
+
.archive-head {
|
|
1279
1305
|
display: none;
|
|
1280
1306
|
}
|
|
1281
1307
|
html[data-wp-waves="off"] .wp-waves {
|
package/src/utils/image.ts
CHANGED
|
@@ -6,19 +6,21 @@
|
|
|
6
6
|
* 构建期解析,且经 sharp image service 重编码(默认 webp + 压缩,
|
|
7
7
|
* §4.106 升级),返回优化产物 URL(唯一被优化的格式)。
|
|
8
8
|
*
|
|
9
|
-
* 解析机制:import.meta.glob
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* 解析机制:import.meta.glob 匹配站点根下 src 全目录的媒体扩展名,`/` 前缀
|
|
10
|
+
* 相对 Vite 项目根,主题包组件运行在站点构建管线中,因此命中站点项目的 src/
|
|
11
|
+
* (§4.108:不写死 assets/ 前缀,src 下任意目录的图片/视频均可解析;
|
|
12
|
+
* ⚠️ 本段注释不可出现 glob 的双星斜杠字面量——含块注释终止符子串会提前闭合);
|
|
13
|
+
* 非 eager 按需 await,仅实际用到的媒体进入构建产物。
|
|
12
14
|
* svg(矢量)与 gif(动图)不做 sharp 编码——转码会破坏矢量/动画,原样输出。
|
|
13
15
|
* ico 不参与 glob(favicon 走 public / 远程,无优化需求)。
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import { getImage, type ImageMetadata } from "astro:assets";
|
|
17
19
|
|
|
18
|
-
/** glob
|
|
19
|
-
type AssetMeta = ImageMetadata;
|
|
20
|
+
/** glob 命中的资源元数据:图片为 ImageMetadata,视频等为产物 URL 字符串 */
|
|
21
|
+
type AssetMeta = ImageMetadata | string;
|
|
20
22
|
|
|
21
|
-
/** 三格式判定:true = 相对 src/
|
|
23
|
+
/** 三格式判定:true = 相对 src/ 的本地媒体资源(需构建期解析) */
|
|
22
24
|
export function isLocalImagePath(src: string): boolean {
|
|
23
25
|
if (!src) return false;
|
|
24
26
|
return !(
|
|
@@ -30,21 +32,26 @@ export function isLocalImagePath(src: string): boolean {
|
|
|
30
32
|
);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* 相对路径 → glob key:不写死 assets/ 前缀(§4.108),相对路径不加 `/` 即
|
|
37
|
+
* 相对站点 src/ 目录——"assets/images/avatar.png" → /src/assets/images/avatar.png,
|
|
38
|
+
* "styles/x.svg" → /src/styles/x.svg,src 下任意目录均可解析。
|
|
39
|
+
*/
|
|
34
40
|
export function toAssetKey(src: string): string {
|
|
35
|
-
return `/src
|
|
41
|
+
return `/src/${src.replace(/^\.?\//, "")}`;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
type AssetModules = Record<string, () => Promise<AssetMeta>>;
|
|
39
45
|
|
|
40
46
|
/**
|
|
41
|
-
*
|
|
47
|
+
* 解析单张站点静态媒体(图片/视频)为可直接引用的 URL。
|
|
42
48
|
* public / 外链原样返回;相对路径查 glob 表,命中则 await 产物元数据;
|
|
43
49
|
* 未命中(文件不存在)时 console.warn 并原样返回,页面不断构建。
|
|
44
50
|
*
|
|
45
51
|
* §4.106:位图(png/jpg/webp/avif)额外经 getImage() 走 sharp image service
|
|
46
52
|
* 构建期重编码(默认转 webp + 质量压缩,Astro 同图同参有编码缓存);
|
|
47
53
|
* svg/gif 保持原始产物 URL 不转码。失败时降级原样返回,页面不断构建。
|
|
54
|
+
* §4.108:视频(mp4/webm/mov)glob 导出为产物 URL 字符串,直接返回。
|
|
48
55
|
*
|
|
49
56
|
* 注意:import.meta.glob 第一参数必须是字符串字面量(vite 编译期静态分析,
|
|
50
57
|
* 变量/常量在 dev 下直接报 Invalid glob import syntax),模式不能提取为常量。
|
|
@@ -52,23 +59,27 @@ type AssetModules = Record<string, () => Promise<AssetMeta>>;
|
|
|
52
59
|
export async function resolveSiteImage(src: string): Promise<string> {
|
|
53
60
|
if (!isLocalImagePath(src)) return src;
|
|
54
61
|
const modules = import.meta.glob(
|
|
55
|
-
"/src
|
|
62
|
+
"/src/**/*.{png,jpg,jpeg,webp,avif,gif,svg,mp4,webm,mov}",
|
|
56
63
|
{ import: "default" },
|
|
57
64
|
) as unknown as AssetModules;
|
|
58
65
|
const loader = modules[toAssetKey(src)];
|
|
59
66
|
if (!loader) {
|
|
60
67
|
console.warn(
|
|
61
|
-
`[orchid-theme]
|
|
68
|
+
`[orchid-theme] 媒体文件未找到: ${toAssetKey(src)}(相对路径不带 / 时相对站点 src/ 目录解析)`,
|
|
62
69
|
);
|
|
63
70
|
return src;
|
|
64
71
|
}
|
|
65
72
|
const meta = await loader();
|
|
73
|
+
if (typeof meta === "string") return meta;
|
|
66
74
|
if (meta.format === "svg" || meta.format === "gif") return meta.src;
|
|
67
75
|
try {
|
|
68
76
|
const encoded = await getImage({ src: meta });
|
|
69
77
|
return encoded.src;
|
|
70
78
|
} catch (err) {
|
|
71
|
-
console.warn(
|
|
79
|
+
console.warn(
|
|
80
|
+
`[orchid-theme] 图片优化失败,回退原图: ${toAssetKey(src)}`,
|
|
81
|
+
err,
|
|
82
|
+
);
|
|
72
83
|
return meta.src;
|
|
73
84
|
}
|
|
74
85
|
}
|
package/src/utils/thumb.ts
CHANGED
|
@@ -20,7 +20,12 @@ export interface ThumbInfo {
|
|
|
20
20
|
export function getThumbInfo(src: string): ThumbInfo {
|
|
21
21
|
const hit = thumbs[`public:${src.replace(/^\//, "")}`];
|
|
22
22
|
if (hit?.thumb && hit.width > 0 && hit.height > 0) {
|
|
23
|
-
return {
|
|
23
|
+
return {
|
|
24
|
+
full: src,
|
|
25
|
+
src: `/${hit.thumb}`,
|
|
26
|
+
width: hit.width,
|
|
27
|
+
height: hit.height,
|
|
28
|
+
};
|
|
24
29
|
}
|
|
25
30
|
return { full: src, src };
|
|
26
31
|
}
|