@rxdrag/website-lib 0.0.176 → 1.0.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/components/Background.astro +140 -132
- package/components/Document.astro +256 -256
- package/components/Video.astro +68 -60
- package/package.json +8 -9
|
@@ -1,132 +1,140 @@
|
|
|
1
|
-
---
|
|
2
|
-
import type { BackgroundConfig } from "@rxdrag/website-lib-core";
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
: defaultFill
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
{
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<
|
|
120
|
-
className={className}
|
|
121
|
-
src={videoUrl}
|
|
122
|
-
poster={posterUrl}
|
|
123
|
-
client:load
|
|
124
|
-
/>
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
1
|
+
---
|
|
2
|
+
import type { BackgroundConfig, Locals } from "@rxdrag/website-lib-core";
|
|
3
|
+
import {
|
|
4
|
+
BackgroundVideoPlayer,
|
|
5
|
+
BackgroundHlsVideoPlayer,
|
|
6
|
+
Entify,
|
|
7
|
+
} from "@rxdrag/website-lib-core";
|
|
8
|
+
import Image from "./Image.astro";
|
|
9
|
+
import GlassBorder from "./GlassBorder.astro";
|
|
10
|
+
import GradientBorder from "./GradientBorder.astro";
|
|
11
|
+
import GlassRefraction from "./GlassRefraction.astro";
|
|
12
|
+
|
|
13
|
+
export interface Props {
|
|
14
|
+
background: BackgroundConfig;
|
|
15
|
+
index?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { env, imageSizes } = Astro.locals as Locals;
|
|
19
|
+
|
|
20
|
+
const { background } = Astro.props;
|
|
21
|
+
|
|
22
|
+
const baseFill = "absolute inset-0 w-full h-full";
|
|
23
|
+
const defaultFill = `${baseFill} object-cover`;
|
|
24
|
+
const rawClass = (background.class || "").trim();
|
|
25
|
+
const hasLayoutOverride = /(?:^|\s)(?:inset(?:-[xy])?-|top-|bottom-|left-|right-|w-|h-)/.test(
|
|
26
|
+
rawClass,
|
|
27
|
+
);
|
|
28
|
+
const className = rawClass
|
|
29
|
+
? `${hasLayoutOverride ? "absolute" : defaultFill} ${rawClass}`
|
|
30
|
+
: defaultFill;
|
|
31
|
+
|
|
32
|
+
// 安全地提取 mediaRef 和 posterRef(只有部分背景类型有这些属性)
|
|
33
|
+
const mediaRef = background.type === "video" ? background.mediaRef : undefined;
|
|
34
|
+
|
|
35
|
+
const rx = Entify.getInstance(env, imageSizes);
|
|
36
|
+
|
|
37
|
+
// 获取媒体数据
|
|
38
|
+
const media = rx ? await rx.getMedia(mediaRef) : undefined;
|
|
39
|
+
|
|
40
|
+
// 预计算视频相关数据
|
|
41
|
+
const videoUrl =
|
|
42
|
+
background.type === "video" ? media?.file?.original : undefined;
|
|
43
|
+
|
|
44
|
+
// 处理视频封面
|
|
45
|
+
let posterUrl: string | undefined;
|
|
46
|
+
if (background.type === "video" && background.poster) {
|
|
47
|
+
if (typeof background.poster === "string") {
|
|
48
|
+
posterUrl = background.poster;
|
|
49
|
+
} else if (typeof background.poster === "object") {
|
|
50
|
+
const poster = background.poster as Partial<ImageMetadata>;
|
|
51
|
+
if (
|
|
52
|
+
typeof poster.src === "string" &&
|
|
53
|
+
typeof poster.width === "number" &&
|
|
54
|
+
typeof poster.height === "number"
|
|
55
|
+
) {
|
|
56
|
+
posterUrl = poster.src;
|
|
57
|
+
} else {
|
|
58
|
+
console.log(
|
|
59
|
+
"[Background] invalid video poster, expect ImageMetadata:",
|
|
60
|
+
background.poster,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isHls =
|
|
67
|
+
background.type === "video" &&
|
|
68
|
+
(media?.storageType === "cloudflare_stream" ||
|
|
69
|
+
videoUrl?.endsWith(".m3u8") ||
|
|
70
|
+
videoUrl?.includes("cloudflarestream.com"));
|
|
71
|
+
|
|
72
|
+
// 预计算 Spline URL
|
|
73
|
+
const splineUrl = background.type === "spline" ? background.url : undefined;
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
{background.type === "color" && <div class={className} />}
|
|
78
|
+
|
|
79
|
+
{background.type === "blur" && <div class={className} />}
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
background.type === "glass" && (
|
|
83
|
+
<GlassRefraction class={className} />
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
background.type === "glass-border" && (
|
|
89
|
+
<GlassBorder {...background} class={className} />
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
background.type === "gradient-border" && (
|
|
95
|
+
<GradientBorder {...background} class={className} />
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
background.type === "image" && background.src && (
|
|
101
|
+
<Image
|
|
102
|
+
class={className}
|
|
103
|
+
src={background.src}
|
|
104
|
+
layout={background.layout || "full-width"}
|
|
105
|
+
alt=""
|
|
106
|
+
/>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
background.type === "svg" && background.code && (
|
|
112
|
+
<div class={className} set:html={background.code} />
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
background.type === "video" &&
|
|
118
|
+
(isHls ? (
|
|
119
|
+
<BackgroundHlsVideoPlayer
|
|
120
|
+
className={className}
|
|
121
|
+
src={videoUrl}
|
|
122
|
+
poster={posterUrl}
|
|
123
|
+
client:load
|
|
124
|
+
/>
|
|
125
|
+
) : (
|
|
126
|
+
<BackgroundVideoPlayer
|
|
127
|
+
className={className}
|
|
128
|
+
src={videoUrl}
|
|
129
|
+
poster={posterUrl}
|
|
130
|
+
client:load
|
|
131
|
+
/>
|
|
132
|
+
))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
{
|
|
136
|
+
background.type === "spline" && (
|
|
137
|
+
<iframe class={className} src={splineUrl} title="Spline 3D Scene" />
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
@@ -1,256 +1,256 @@
|
|
|
1
|
-
---
|
|
2
|
-
import "aos/dist/aos.css";
|
|
3
|
-
import type { PageMeta } from "@rxdrag/rxcms-models";
|
|
4
|
-
import type { Locals } from "@rxdrag/website-lib-core
|
|
5
|
-
import Meta from "
|
|
6
|
-
import { Entify } from "@rxdrag/website-lib-core
|
|
7
|
-
import { ClientRouter } from "astro:transitions";
|
|
8
|
-
|
|
9
|
-
const { env, imageSizes, currentHtmlLang, langs } = Astro.locals as Locals;
|
|
10
|
-
|
|
11
|
-
// 获取 Entify 实例
|
|
12
|
-
const rx = Entify.getInstance(env, imageSizes);
|
|
13
|
-
|
|
14
|
-
// 使用中间件下发的语言数据(如果中间件失败,提供回退逻辑)
|
|
15
|
-
const defaultLang = currentHtmlLang || "en";
|
|
16
|
-
|
|
17
|
-
// 获取当前语言的 dir 属性(用于 RTL 语言如阿拉伯语、希伯来语等)
|
|
18
|
-
const currentLangAbbr = (Astro.locals as any).currentLangAbbr;
|
|
19
|
-
const currentLangObj = langs?.find((l) => l.abbr === currentLangAbbr) || langs?.[0];
|
|
20
|
-
const defaultDir = currentLangObj?.dir || "ltr";
|
|
21
|
-
|
|
22
|
-
// 注意:langAbbr 已由中间件设置到 Entify 实例,无需再次调用 setLangAbbr
|
|
23
|
-
|
|
24
|
-
// 计算支持的语言列表(用于路径解析)
|
|
25
|
-
const supportedLangs = langs?.map((l) => l.htmlLang) || [];
|
|
26
|
-
const firstSegment = Astro.url.pathname.split("/")[1];
|
|
27
|
-
|
|
28
|
-
// 文档壳:只负责 <html>/<head>/<body> 结构,不包含 Header/Footer 等站点布局。
|
|
29
|
-
interface Props {
|
|
30
|
-
// html 的 lang 属性(可选,传入则覆盖默认动态获取的语言)
|
|
31
|
-
lang?: string;
|
|
32
|
-
// SEO Meta(由上层 Layout/页面传入)
|
|
33
|
-
meta?: PageMeta;
|
|
34
|
-
// 页面标题(由 Meta 组件消费)
|
|
35
|
-
title?: string;
|
|
36
|
-
// JSON-LD 结构化数据,支持单个或数组
|
|
37
|
-
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
|
38
|
-
// 兼容 class / className 两种写法,最终合并到 body 上
|
|
39
|
-
class?: string;
|
|
40
|
-
className?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const {
|
|
44
|
-
lang,
|
|
45
|
-
meta,
|
|
46
|
-
title,
|
|
47
|
-
jsonLd,
|
|
48
|
-
class: className = "scroll-smooth",
|
|
49
|
-
className: className2,
|
|
50
|
-
// 其余 props 透传到 body(例如 data-* 属性等)
|
|
51
|
-
...rest
|
|
52
|
-
} = Astro.props;
|
|
53
|
-
|
|
54
|
-
const jsonLdItems = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
|
|
55
|
-
|
|
56
|
-
const bodyAttrs = {
|
|
57
|
-
"data-aos-easing": "ease-out-cubic",
|
|
58
|
-
"data-aos-duration": "800",
|
|
59
|
-
"data-aos-delay": "0",
|
|
60
|
-
"data-aos-offset": "50",
|
|
61
|
-
"data-aos-once": "true",
|
|
62
|
-
...rest,
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
let resolvedMeta: PageMeta | undefined = meta;
|
|
66
|
-
let resolvedTitle: string | undefined = title;
|
|
67
|
-
|
|
68
|
-
// 仅在未传入 meta/title 时,针对动态详情页兜底拉取
|
|
69
|
-
if (!resolvedMeta || !resolvedTitle) {
|
|
70
|
-
try {
|
|
71
|
-
const pathname = Astro.url?.pathname || "";
|
|
72
|
-
const slug =
|
|
73
|
-
typeof Astro.params?.slug === "string" ? Astro.params.slug : undefined;
|
|
74
|
-
const id =
|
|
75
|
-
typeof Astro.params?.id === "string" ? Astro.params.id : undefined;
|
|
76
|
-
|
|
77
|
-
// 去掉语言前缀,支持多语言路径
|
|
78
|
-
// 例如: /en/posts/slug -> /posts/slug
|
|
79
|
-
const pathWithoutLang = supportedLangs.includes(firstSegment)
|
|
80
|
-
? pathname.slice(firstSegment.length + 1) // +1 去掉开头的 /
|
|
81
|
-
: pathname;
|
|
82
|
-
|
|
83
|
-
if (slug && pathWithoutLang.startsWith("/posts/categories/")) {
|
|
84
|
-
const category = await rx.getPostCategoryBySlug(slug);
|
|
85
|
-
resolvedTitle = resolvedTitle ?? category?.name ?? "Blog";
|
|
86
|
-
} else if (slug && pathWithoutLang.startsWith("/products/categories/")) {
|
|
87
|
-
const category = await rx.getProductCategoryBySlug(slug);
|
|
88
|
-
resolvedTitle = resolvedTitle ?? category?.name ?? "Products";
|
|
89
|
-
} else if (id && pathWithoutLang.startsWith("/profiles/")) {
|
|
90
|
-
const user = await rx.getOneUser(id);
|
|
91
|
-
resolvedTitle = resolvedTitle ?? user?.name ?? "Profile";
|
|
92
|
-
} else if (slug && pathWithoutLang.startsWith("/posts/")) {
|
|
93
|
-
const post = await rx.getPostBySlug(slug, { width: 1200, height: 600 });
|
|
94
|
-
resolvedTitle = resolvedTitle ?? post?.title;
|
|
95
|
-
resolvedMeta = resolvedMeta ?? post?.meta;
|
|
96
|
-
} else if (slug && pathWithoutLang.startsWith("/products/")) {
|
|
97
|
-
const product = await rx.getProductBySlug(slug, {
|
|
98
|
-
width: 800,
|
|
99
|
-
height: 800,
|
|
100
|
-
});
|
|
101
|
-
resolvedTitle = resolvedTitle ?? product?.title;
|
|
102
|
-
resolvedMeta = resolvedMeta ?? product?.meta;
|
|
103
|
-
}
|
|
104
|
-
} catch (e) {
|
|
105
|
-
console.log("[Document] resolve meta failed:", e);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
<!doctype html>
|
|
111
|
-
<html lang={lang || defaultLang} dir={defaultDir} class="aos-preload">
|
|
112
|
-
<head>
|
|
113
|
-
<meta charset="UTF-8" />
|
|
114
|
-
<meta
|
|
115
|
-
name="viewport"
|
|
116
|
-
content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=0.5, user-scalable=yes"
|
|
117
|
-
/>
|
|
118
|
-
|
|
119
|
-
<meta name="generator" content={Astro.generator} />
|
|
120
|
-
<link rel="sitemap" href="/sitemap-index.xml" />
|
|
121
|
-
<!-- 给上层 Layout/页面注入自定义 head 内容(预加载、脚本、样式等) -->
|
|
122
|
-
<!-- 预连接 -->
|
|
123
|
-
<link
|
|
124
|
-
rel="preconnect"
|
|
125
|
-
href="https://customer-amj1dt4tnge8w6ji.cloudflarestream.com"
|
|
126
|
-
crossorigin
|
|
127
|
-
/>
|
|
128
|
-
<script>
|
|
129
|
-
const w = window as any;
|
|
130
|
-
const state =
|
|
131
|
-
w.__aos_state__ ||
|
|
132
|
-
(w.__aos_state__ = { inited: false, loading: null, aos: null });
|
|
133
|
-
|
|
134
|
-
const el = document.documentElement;
|
|
135
|
-
const raf = () =>
|
|
136
|
-
new Promise((resolve) =>
|
|
137
|
-
requestAnimationFrame(() => resolve(undefined)),
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
const ensureAos = async () => {
|
|
141
|
-
if (state.aos) return state.aos;
|
|
142
|
-
if (!state.loading) {
|
|
143
|
-
state.loading = import("aos").then((m) => {
|
|
144
|
-
state.aos = m.default;
|
|
145
|
-
return state.aos;
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
return state.loading;
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const bootOrRefresh = async () => {
|
|
152
|
-
if (
|
|
153
|
-
window.matchMedia &&
|
|
154
|
-
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
155
|
-
) {
|
|
156
|
-
el.classList.add("aos-ready");
|
|
157
|
-
el.classList.remove("aos-preload");
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (!document.querySelector("[data-aos]")) {
|
|
162
|
-
el.classList.add("aos-ready");
|
|
163
|
-
el.classList.remove("aos-preload");
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
el.classList.add("aos-preload");
|
|
168
|
-
el.classList.remove("aos-ready");
|
|
169
|
-
|
|
170
|
-
const AOS = await ensureAos();
|
|
171
|
-
if (!state.inited) {
|
|
172
|
-
state.inited = true;
|
|
173
|
-
AOS.init({
|
|
174
|
-
duration: 800,
|
|
175
|
-
easing: "ease-out-cubic",
|
|
176
|
-
once: true,
|
|
177
|
-
offset: 50,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
await raf();
|
|
182
|
-
await raf();
|
|
183
|
-
AOS.refresh();
|
|
184
|
-
|
|
185
|
-
el.classList.add("aos-ready");
|
|
186
|
-
el.classList.remove("aos-preload");
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const boot = () => {
|
|
190
|
-
bootOrRefresh();
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
if (document.readyState === "loading") {
|
|
194
|
-
document.addEventListener("DOMContentLoaded", boot, { once: true });
|
|
195
|
-
} else {
|
|
196
|
-
boot();
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
document.addEventListener("astro:page-load", boot);
|
|
200
|
-
document.addEventListener("astro:after-swap", boot);
|
|
201
|
-
</script>
|
|
202
|
-
<ClientRouter />
|
|
203
|
-
<!-- Consent default (Google) -->
|
|
204
|
-
<script is:inline>
|
|
205
|
-
window.dataLayer = window.dataLayer || [];
|
|
206
|
-
function gtag() {
|
|
207
|
-
dataLayer.push(arguments);
|
|
208
|
-
}
|
|
209
|
-
gtag("js", new Date());
|
|
210
|
-
gtag("consent", "default", {
|
|
211
|
-
ad_storage: "denied",
|
|
212
|
-
analytics_storage: "denied",
|
|
213
|
-
});
|
|
214
|
-
</script>
|
|
215
|
-
<slot name="head" />
|
|
216
|
-
<Meta title={resolvedTitle} content={resolvedMeta} />
|
|
217
|
-
{
|
|
218
|
-
jsonLdItems.map((item) => (
|
|
219
|
-
<script
|
|
220
|
-
is:inline
|
|
221
|
-
type="application/ld+json"
|
|
222
|
-
set:html={JSON.stringify(item)}
|
|
223
|
-
/>
|
|
224
|
-
))
|
|
225
|
-
}
|
|
226
|
-
<style>
|
|
227
|
-
html.aos-preload [data-aos] {
|
|
228
|
-
opacity: 1 !important;
|
|
229
|
-
transform: none !important;
|
|
230
|
-
transition: none !important;
|
|
231
|
-
}
|
|
232
|
-
</style>
|
|
233
|
-
</head>
|
|
234
|
-
<!-- rest 会被展开到 body 上,方便上层控制 data-* 等属性 -->
|
|
235
|
-
<body class:list={[className, className2]} {...bodyAttrs}>
|
|
236
|
-
<slot />
|
|
237
|
-
</body>
|
|
238
|
-
</html>
|
|
239
|
-
|
|
240
|
-
<style>
|
|
241
|
-
html,
|
|
242
|
-
body {
|
|
243
|
-
margin: 0;
|
|
244
|
-
width: 100%;
|
|
245
|
-
height: 100%;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
html {
|
|
249
|
-
scroll-behavior: smooth;
|
|
250
|
-
scrollbar-gutter: stable;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
body {
|
|
254
|
-
scrollbar-gutter: stable;
|
|
255
|
-
}
|
|
256
|
-
</style>
|
|
1
|
+
---
|
|
2
|
+
import "aos/dist/aos.css";
|
|
3
|
+
import type { PageMeta } from "@rxdrag/rxcms-models";
|
|
4
|
+
import type { Locals } from "@rxdrag/website-lib-core";
|
|
5
|
+
import Meta from "@rxdrag/website-lib/components/Meta.astro";
|
|
6
|
+
import { Entify } from "@rxdrag/website-lib-core";
|
|
7
|
+
import { ClientRouter } from "astro:transitions";
|
|
8
|
+
|
|
9
|
+
const { env, imageSizes, currentHtmlLang, langs } = Astro.locals as Locals;
|
|
10
|
+
|
|
11
|
+
// 获取 Entify 实例
|
|
12
|
+
const rx = Entify.getInstance(env, imageSizes);
|
|
13
|
+
|
|
14
|
+
// 使用中间件下发的语言数据(如果中间件失败,提供回退逻辑)
|
|
15
|
+
const defaultLang = currentHtmlLang || "en";
|
|
16
|
+
|
|
17
|
+
// 获取当前语言的 dir 属性(用于 RTL 语言如阿拉伯语、希伯来语等)
|
|
18
|
+
const currentLangAbbr = (Astro.locals as any).currentLangAbbr;
|
|
19
|
+
const currentLangObj = langs?.find((l) => l.abbr === currentLangAbbr) || langs?.[0];
|
|
20
|
+
const defaultDir = currentLangObj?.dir || "ltr";
|
|
21
|
+
|
|
22
|
+
// 注意:langAbbr 已由中间件设置到 Entify 实例,无需再次调用 setLangAbbr
|
|
23
|
+
|
|
24
|
+
// 计算支持的语言列表(用于路径解析)
|
|
25
|
+
const supportedLangs = langs?.map((l) => l.htmlLang) || [];
|
|
26
|
+
const firstSegment = Astro.url.pathname.split("/")[1];
|
|
27
|
+
|
|
28
|
+
// 文档壳:只负责 <html>/<head>/<body> 结构,不包含 Header/Footer 等站点布局。
|
|
29
|
+
interface Props {
|
|
30
|
+
// html 的 lang 属性(可选,传入则覆盖默认动态获取的语言)
|
|
31
|
+
lang?: string;
|
|
32
|
+
// SEO Meta(由上层 Layout/页面传入)
|
|
33
|
+
meta?: PageMeta;
|
|
34
|
+
// 页面标题(由 Meta 组件消费)
|
|
35
|
+
title?: string;
|
|
36
|
+
// JSON-LD 结构化数据,支持单个或数组
|
|
37
|
+
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
|
38
|
+
// 兼容 class / className 两种写法,最终合并到 body 上
|
|
39
|
+
class?: string;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const {
|
|
44
|
+
lang,
|
|
45
|
+
meta,
|
|
46
|
+
title,
|
|
47
|
+
jsonLd,
|
|
48
|
+
class: className = "scroll-smooth",
|
|
49
|
+
className: className2,
|
|
50
|
+
// 其余 props 透传到 body(例如 data-* 属性等)
|
|
51
|
+
...rest
|
|
52
|
+
} = Astro.props;
|
|
53
|
+
|
|
54
|
+
const jsonLdItems = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
|
|
55
|
+
|
|
56
|
+
const bodyAttrs = {
|
|
57
|
+
"data-aos-easing": "ease-out-cubic",
|
|
58
|
+
"data-aos-duration": "800",
|
|
59
|
+
"data-aos-delay": "0",
|
|
60
|
+
"data-aos-offset": "50",
|
|
61
|
+
"data-aos-once": "true",
|
|
62
|
+
...rest,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
let resolvedMeta: PageMeta | undefined = meta;
|
|
66
|
+
let resolvedTitle: string | undefined = title;
|
|
67
|
+
|
|
68
|
+
// 仅在未传入 meta/title 时,针对动态详情页兜底拉取
|
|
69
|
+
if (!resolvedMeta || !resolvedTitle) {
|
|
70
|
+
try {
|
|
71
|
+
const pathname = Astro.url?.pathname || "";
|
|
72
|
+
const slug =
|
|
73
|
+
typeof Astro.params?.slug === "string" ? Astro.params.slug : undefined;
|
|
74
|
+
const id =
|
|
75
|
+
typeof Astro.params?.id === "string" ? Astro.params.id : undefined;
|
|
76
|
+
|
|
77
|
+
// 去掉语言前缀,支持多语言路径
|
|
78
|
+
// 例如: /en/posts/slug -> /posts/slug
|
|
79
|
+
const pathWithoutLang = supportedLangs.includes(firstSegment)
|
|
80
|
+
? pathname.slice(firstSegment.length + 1) // +1 去掉开头的 /
|
|
81
|
+
: pathname;
|
|
82
|
+
|
|
83
|
+
if (slug && pathWithoutLang.startsWith("/posts/categories/")) {
|
|
84
|
+
const category = await rx.getPostCategoryBySlug(slug);
|
|
85
|
+
resolvedTitle = resolvedTitle ?? category?.name ?? "Blog";
|
|
86
|
+
} else if (slug && pathWithoutLang.startsWith("/products/categories/")) {
|
|
87
|
+
const category = await rx.getProductCategoryBySlug(slug);
|
|
88
|
+
resolvedTitle = resolvedTitle ?? category?.name ?? "Products";
|
|
89
|
+
} else if (id && pathWithoutLang.startsWith("/profiles/")) {
|
|
90
|
+
const user = await rx.getOneUser(id);
|
|
91
|
+
resolvedTitle = resolvedTitle ?? user?.name ?? "Profile";
|
|
92
|
+
} else if (slug && pathWithoutLang.startsWith("/posts/")) {
|
|
93
|
+
const post = await rx.getPostBySlug(slug, { width: 1200, height: 600 });
|
|
94
|
+
resolvedTitle = resolvedTitle ?? post?.title;
|
|
95
|
+
resolvedMeta = resolvedMeta ?? post?.meta;
|
|
96
|
+
} else if (slug && pathWithoutLang.startsWith("/products/")) {
|
|
97
|
+
const product = await rx.getProductBySlug(slug, {
|
|
98
|
+
width: 800,
|
|
99
|
+
height: 800,
|
|
100
|
+
});
|
|
101
|
+
resolvedTitle = resolvedTitle ?? product?.title;
|
|
102
|
+
resolvedMeta = resolvedMeta ?? product?.meta;
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.log("[Document] resolve meta failed:", e);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
<!doctype html>
|
|
111
|
+
<html lang={lang || defaultLang} dir={defaultDir} class="aos-preload">
|
|
112
|
+
<head>
|
|
113
|
+
<meta charset="UTF-8" />
|
|
114
|
+
<meta
|
|
115
|
+
name="viewport"
|
|
116
|
+
content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=0.5, user-scalable=yes"
|
|
117
|
+
/>
|
|
118
|
+
|
|
119
|
+
<meta name="generator" content={Astro.generator} />
|
|
120
|
+
<link rel="sitemap" href="/sitemap-index.xml" />
|
|
121
|
+
<!-- 给上层 Layout/页面注入自定义 head 内容(预加载、脚本、样式等) -->
|
|
122
|
+
<!-- 预连接 -->
|
|
123
|
+
<link
|
|
124
|
+
rel="preconnect"
|
|
125
|
+
href="https://customer-amj1dt4tnge8w6ji.cloudflarestream.com"
|
|
126
|
+
crossorigin
|
|
127
|
+
/>
|
|
128
|
+
<script>
|
|
129
|
+
const w = window as any;
|
|
130
|
+
const state =
|
|
131
|
+
w.__aos_state__ ||
|
|
132
|
+
(w.__aos_state__ = { inited: false, loading: null, aos: null });
|
|
133
|
+
|
|
134
|
+
const el = document.documentElement;
|
|
135
|
+
const raf = () =>
|
|
136
|
+
new Promise((resolve) =>
|
|
137
|
+
requestAnimationFrame(() => resolve(undefined)),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const ensureAos = async () => {
|
|
141
|
+
if (state.aos) return state.aos;
|
|
142
|
+
if (!state.loading) {
|
|
143
|
+
state.loading = import("aos").then((m) => {
|
|
144
|
+
state.aos = m.default;
|
|
145
|
+
return state.aos;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return state.loading;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const bootOrRefresh = async () => {
|
|
152
|
+
if (
|
|
153
|
+
window.matchMedia &&
|
|
154
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
155
|
+
) {
|
|
156
|
+
el.classList.add("aos-ready");
|
|
157
|
+
el.classList.remove("aos-preload");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!document.querySelector("[data-aos]")) {
|
|
162
|
+
el.classList.add("aos-ready");
|
|
163
|
+
el.classList.remove("aos-preload");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
el.classList.add("aos-preload");
|
|
168
|
+
el.classList.remove("aos-ready");
|
|
169
|
+
|
|
170
|
+
const AOS = await ensureAos();
|
|
171
|
+
if (!state.inited) {
|
|
172
|
+
state.inited = true;
|
|
173
|
+
AOS.init({
|
|
174
|
+
duration: 800,
|
|
175
|
+
easing: "ease-out-cubic",
|
|
176
|
+
once: true,
|
|
177
|
+
offset: 50,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await raf();
|
|
182
|
+
await raf();
|
|
183
|
+
AOS.refresh();
|
|
184
|
+
|
|
185
|
+
el.classList.add("aos-ready");
|
|
186
|
+
el.classList.remove("aos-preload");
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const boot = () => {
|
|
190
|
+
bootOrRefresh();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (document.readyState === "loading") {
|
|
194
|
+
document.addEventListener("DOMContentLoaded", boot, { once: true });
|
|
195
|
+
} else {
|
|
196
|
+
boot();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
document.addEventListener("astro:page-load", boot);
|
|
200
|
+
document.addEventListener("astro:after-swap", boot);
|
|
201
|
+
</script>
|
|
202
|
+
<ClientRouter />
|
|
203
|
+
<!-- Consent default (Google) -->
|
|
204
|
+
<script is:inline>
|
|
205
|
+
window.dataLayer = window.dataLayer || [];
|
|
206
|
+
function gtag() {
|
|
207
|
+
dataLayer.push(arguments);
|
|
208
|
+
}
|
|
209
|
+
gtag("js", new Date());
|
|
210
|
+
gtag("consent", "default", {
|
|
211
|
+
ad_storage: "denied",
|
|
212
|
+
analytics_storage: "denied",
|
|
213
|
+
});
|
|
214
|
+
</script>
|
|
215
|
+
<slot name="head" />
|
|
216
|
+
<Meta title={resolvedTitle} content={resolvedMeta} />
|
|
217
|
+
{
|
|
218
|
+
jsonLdItems.map((item) => (
|
|
219
|
+
<script
|
|
220
|
+
is:inline
|
|
221
|
+
type="application/ld+json"
|
|
222
|
+
set:html={JSON.stringify(item)}
|
|
223
|
+
/>
|
|
224
|
+
))
|
|
225
|
+
}
|
|
226
|
+
<style>
|
|
227
|
+
html.aos-preload [data-aos] {
|
|
228
|
+
opacity: 1 !important;
|
|
229
|
+
transform: none !important;
|
|
230
|
+
transition: none !important;
|
|
231
|
+
}
|
|
232
|
+
</style>
|
|
233
|
+
</head>
|
|
234
|
+
<!-- rest 会被展开到 body 上,方便上层控制 data-* 等属性 -->
|
|
235
|
+
<body class:list={[className, className2]} {...bodyAttrs}>
|
|
236
|
+
<slot />
|
|
237
|
+
</body>
|
|
238
|
+
</html>
|
|
239
|
+
|
|
240
|
+
<style>
|
|
241
|
+
html,
|
|
242
|
+
body {
|
|
243
|
+
margin: 0;
|
|
244
|
+
width: 100%;
|
|
245
|
+
height: 100%;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
html {
|
|
249
|
+
scroll-behavior: smooth;
|
|
250
|
+
scrollbar-gutter: stable;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
body {
|
|
254
|
+
scrollbar-gutter: stable;
|
|
255
|
+
}
|
|
256
|
+
</style>
|
package/components/Video.astro
CHANGED
|
@@ -1,60 +1,68 @@
|
|
|
1
|
-
---
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
Entify,
|
|
4
|
+
ReactVideoPlayer,
|
|
5
|
+
type Locals,
|
|
6
|
+
} from "@rxdrag/website-lib-core";
|
|
7
|
+
|
|
8
|
+
const { env, imageSizes } = Astro.locals as Locals;
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
videoRef?: string;
|
|
12
|
+
poster?: string | ImageMetadata;
|
|
13
|
+
endTitle?: string;
|
|
14
|
+
eagerLoad?: boolean;
|
|
15
|
+
classNames?: {
|
|
16
|
+
container?: string;
|
|
17
|
+
aspect?: string;
|
|
18
|
+
video?: string;
|
|
19
|
+
playButton?: string;
|
|
20
|
+
playButtonOuter?: string;
|
|
21
|
+
playButtonInner?: string;
|
|
22
|
+
playIcon?: string;
|
|
23
|
+
overlay?: string;
|
|
24
|
+
overlayTitle?: string;
|
|
25
|
+
ctaButton?: string;
|
|
26
|
+
overlayReplayButton?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
videoRef,
|
|
32
|
+
poster,
|
|
33
|
+
eagerLoad,
|
|
34
|
+
endTitle = "Contact Us Now",
|
|
35
|
+
classNames = {},
|
|
36
|
+
} = Astro.props;
|
|
37
|
+
|
|
38
|
+
const rx = Entify.getInstance(env, imageSizes);
|
|
39
|
+
|
|
40
|
+
// 获取媒体数据(增加 rx 为空时的保护)
|
|
41
|
+
const media = rx ? await rx.getMedia(videoRef) : undefined;
|
|
42
|
+
|
|
43
|
+
// 处理封面图
|
|
44
|
+
let posterUrl: string | undefined;
|
|
45
|
+
if (poster) {
|
|
46
|
+
if (typeof poster === "string") {
|
|
47
|
+
posterUrl = poster;
|
|
48
|
+
} else {
|
|
49
|
+
//const posterImage = await getImage({ src: poster });
|
|
50
|
+
posterUrl = poster.src; //;posterImage.src;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
|
|
57
|
+
<ReactVideoPlayer
|
|
58
|
+
client:load
|
|
59
|
+
media={media}
|
|
60
|
+
endTitle={endTitle}
|
|
61
|
+
posterUrl={posterUrl}
|
|
62
|
+
eagerLoad={eagerLoad}
|
|
63
|
+
classNames={classNames}
|
|
64
|
+
>
|
|
65
|
+
<slot />
|
|
66
|
+
</ReactVideoPlayer>
|
|
67
|
+
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rxdrag/website-lib",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./index.ts",
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"eslint": "^9.39.2",
|
|
27
27
|
"gsap": "^3.12.7",
|
|
28
28
|
"typescript": "^5",
|
|
29
|
-
"@rxdrag/
|
|
30
|
-
"@rxdrag/
|
|
31
|
-
"@rxdrag/
|
|
32
|
-
"@rxdrag/
|
|
33
|
-
"@rxdrag/tsconfig": "0.
|
|
34
|
-
"@rxdrag/
|
|
29
|
+
"@rxdrag/entify-hooks": "0.3.0",
|
|
30
|
+
"@rxdrag/rxcms-models": "0.4.0",
|
|
31
|
+
"@rxdrag/eslint-config-custom": "0.3.0",
|
|
32
|
+
"@rxdrag/website-lib-core": "0.1.0",
|
|
33
|
+
"@rxdrag/tsconfig": "0.3.0",
|
|
34
|
+
"@rxdrag/tiptap-preview": "0.1.0"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"aos": "3.0.0-beta.6",
|
|
@@ -39,8 +39,7 @@
|
|
|
39
39
|
"react": "^19.1.0",
|
|
40
40
|
"react-dom": "^19.1.0",
|
|
41
41
|
"vanilla-cookieconsent": "3.1.0",
|
|
42
|
-
"@rxdrag/rxcms-models": "0.
|
|
43
|
-
"@rxdrag/website-lib-react": "0.0.8"
|
|
42
|
+
"@rxdrag/rxcms-models": "0.4.0"
|
|
44
43
|
},
|
|
45
44
|
"peerDependencies": {
|
|
46
45
|
"astro": "^6.1.1",
|