@windcoder/astro-theme-orchid 0.0.0-stage → 0.2.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 +118 -2
- package/package.json +70 -5
- package/src/components/AlbumCard.astro +73 -0
- package/src/components/Analytics.astro +22 -0
- package/src/components/BaseLayout.astro +188 -0
- package/src/components/Calendar.astro +379 -0
- package/src/components/CategoryBar.astro +74 -0
- package/src/components/Comment.astro +48 -0
- package/src/components/EncryptedContent.astro +42 -0
- package/src/components/Footer.astro +31 -0
- package/src/components/Header.astro +153 -0
- package/src/components/License.astro +29 -0
- package/src/components/Live2DWidget.astro +262 -0
- package/src/components/MusicPlayer.astro +940 -0
- package/src/components/OutdatedCard.astro +53 -0
- package/src/components/Pagination.astro +94 -0
- package/src/components/PhotoCard.astro +22 -0
- package/src/components/PostCard.astro +89 -0
- package/src/components/PostNav.astro +40 -0
- package/src/components/RecommendedPost.astro +166 -0
- package/src/components/SakuraEffect.astro +152 -0
- package/src/components/Sidebar.astro +334 -0
- package/src/components/Toc.astro +27 -0
- package/src/components/analytics/GoogleAnalytics.astro +22 -0
- package/src/components/analytics/La51Analytics.astro +52 -0
- package/src/components/analytics/MicrosoftClarity.astro +18 -0
- package/src/components/analytics/UmamiAnalytics.astro +86 -0
- package/src/components/comments/Artalk.astro +51 -0
- package/src/components/comments/Disqus.astro +43 -0
- package/src/components/comments/Giscus.astro +50 -0
- package/src/components/comments/Twikoo.astro +65 -0
- package/src/components/comments/Waline.astro +36 -0
- package/src/i18n/en.ts +164 -0
- package/src/i18n/index.ts +18 -0
- package/src/i18n/resolve.ts +49 -0
- package/src/i18n/zh-CN.ts +184 -0
- package/src/icons.ts +71 -0
- package/src/index.ts +411 -0
- package/src/plugins/plantuml-encoder.ts +95 -0
- package/src/plugins/rehype-diagram.ts +218 -0
- package/src/plugins/rehype-ec-vt-rerun.ts +80 -0
- package/src/plugins/rehype-email.ts +75 -0
- package/src/plugins/rehype-github-card.ts +114 -0
- package/src/plugins/rehype.ts +284 -0
- package/src/plugins/remark-diagram.ts +78 -0
- package/src/plugins/remark-directives.ts +100 -0
- package/src/plugins/remark-excerpt.ts +29 -0
- package/src/plugins/remark-image-grid.ts +97 -0
- package/src/plugins/remark-reading-time.ts +38 -0
- package/src/plugins/scripts/diagram-panzoom.js +388 -0
- package/src/plugins/scripts/plantuml-theme-switch.js +90 -0
- package/src/routes/404.astro +14 -0
- package/src/routes/_hydrate.ts +34 -0
- package/src/routes/about.astro +25 -0
- package/src/routes/api/allPostMeta.json.ts +26 -0
- package/src/routes/archive.astro +81 -0
- package/src/routes/categories/[category].astro +37 -0
- package/src/routes/categories/index.astro +29 -0
- package/src/routes/friends.astro +83 -0
- package/src/routes/gallery/[album].astro +97 -0
- package/src/routes/gallery/index.astro +100 -0
- package/src/routes/guestbook.astro +37 -0
- package/src/routes/index.astro +47 -0
- package/src/routes/og/[...slug].ts +281 -0
- package/src/routes/page/[page].astro +53 -0
- package/src/routes/posts/[...slug].astro +350 -0
- package/src/routes/robots.txt.ts +18 -0
- package/src/routes/rss.xml.ts +37 -0
- package/src/routes/series/[name].astro +66 -0
- package/src/routes/series/index.astro +64 -0
- package/src/routes/sponsor.astro +84 -0
- package/src/routes/tags/[tag].astro +40 -0
- package/src/routes/tags/index.astro +29 -0
- package/src/schema.ts +1218 -0
- package/src/scripts/app.ts +1041 -0
- package/src/scripts/share-poster.ts +468 -0
- package/src/styles/global.css +4864 -0
- package/src/utils/crypto.ts +46 -0
- package/src/utils/gallery.ts +76 -0
- package/src/utils/lqip.ts +18 -0
- package/src/utils/related.ts +101 -0
- package/src/utils.ts +233 -0
- package/src/vite.ts +105 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/* 分享海报(b9.6,P1-13):文章页「生成分享海报」按钮 → 浏览器 canvas 绘制
|
|
2
|
+
850px 宽海报(封面 + 日期角标 + 标题/摘要 + 作者/站点 + 二维码)→ 弹窗预览,
|
|
3
|
+
支持复制链接与保存 PNG。移植自原版 misc/SharePoster.svelte,主题中立化改造:
|
|
4
|
+
- 去头像(纯文字作者)、去 weserv.nl 第三方图片代理(封面加载失败降级 accent 色块)
|
|
5
|
+
- 字体改为中文栈(canvas 不走 CSS --font-display)
|
|
6
|
+
- qrcode 动态 import 分包(点开才加载,构建零开销)
|
|
7
|
+
- 自读 #qy-ui-labels,避免与 app.ts 循环依赖 */
|
|
8
|
+
|
|
9
|
+
const doc = document;
|
|
10
|
+
|
|
11
|
+
/* ---------- UI 文案:读 BaseLayout 的 #qy-ui-labels JSON(兜底防 JSON 缺失/解析失败) ---------- */
|
|
12
|
+
const FALLBACK: Record<string, string> = {
|
|
13
|
+
sharePoster: "生成分享海报",
|
|
14
|
+
generatingPoster: "海报生成中…",
|
|
15
|
+
copyLink: "复制链接",
|
|
16
|
+
savePoster: "保存海报",
|
|
17
|
+
scanToRead: "扫码阅读全文",
|
|
18
|
+
author: "作者",
|
|
19
|
+
close: "关闭",
|
|
20
|
+
copied: "已复制",
|
|
21
|
+
};
|
|
22
|
+
let cachedLabels: Record<string, string> | null = null;
|
|
23
|
+
|
|
24
|
+
function label(key: string): string {
|
|
25
|
+
if (!cachedLabels) {
|
|
26
|
+
try {
|
|
27
|
+
const el = doc.getElementById("qy-ui-labels");
|
|
28
|
+
cachedLabels = el
|
|
29
|
+
? (JSON.parse(el.textContent || "{}") as Record<string, string>)
|
|
30
|
+
: {};
|
|
31
|
+
} catch {
|
|
32
|
+
cachedLabels = {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return cachedLabels[key] || FALLBACK[key] || "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* ---------- 读取当前 accent 主题色(原版 temp div 技巧,canvas 不认 CSS 变量) ---------- */
|
|
39
|
+
function readAccent(): string {
|
|
40
|
+
const tmp = doc.createElement("div");
|
|
41
|
+
tmp.style.display = "none";
|
|
42
|
+
tmp.style.color = "var(--accent)";
|
|
43
|
+
doc.body.appendChild(tmp);
|
|
44
|
+
const color = getComputedStyle(tmp).color;
|
|
45
|
+
tmp.remove();
|
|
46
|
+
return color || "#8b7ec8";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ---------- 图片加载:同源相对路径直接加载,失败 resolve(null) 由调用方降级 ---------- */
|
|
50
|
+
function loadImage(src: string): Promise<HTMLImageElement | null> {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const img = new Image();
|
|
53
|
+
img.crossOrigin = "anonymous";
|
|
54
|
+
img.onload = () => resolve(img);
|
|
55
|
+
img.onerror = () => resolve(null);
|
|
56
|
+
img.src = src;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ---------- 逐字符换行(中文无空格分词,原版同款实现) ---------- */
|
|
61
|
+
function getLines(
|
|
62
|
+
ctx: CanvasRenderingContext2D,
|
|
63
|
+
text: string,
|
|
64
|
+
maxWidth: number,
|
|
65
|
+
): string[] {
|
|
66
|
+
const chars = text.split("");
|
|
67
|
+
const lines: string[] = [];
|
|
68
|
+
let current = "";
|
|
69
|
+
for (const char of chars) {
|
|
70
|
+
if (ctx.measureText(current + char).width < maxWidth) {
|
|
71
|
+
current += char;
|
|
72
|
+
} else {
|
|
73
|
+
lines.push(current);
|
|
74
|
+
current = char;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (current) lines.push(current);
|
|
78
|
+
return lines;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function drawRoundedRect(
|
|
82
|
+
ctx: CanvasRenderingContext2D,
|
|
83
|
+
x: number,
|
|
84
|
+
y: number,
|
|
85
|
+
width: number,
|
|
86
|
+
height: number,
|
|
87
|
+
radius: number,
|
|
88
|
+
) {
|
|
89
|
+
ctx.beginPath();
|
|
90
|
+
ctx.moveTo(x + radius, y);
|
|
91
|
+
ctx.lineTo(x + width - radius, y);
|
|
92
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
93
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
94
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
95
|
+
ctx.lineTo(x + radius, y + height);
|
|
96
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
97
|
+
ctx.closePath();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ---------- 海报绘制:布局参数与原版一致(scale=2,宽 425*2) ---------- */
|
|
101
|
+
const FONT_STACK =
|
|
102
|
+
"'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif";
|
|
103
|
+
|
|
104
|
+
interface PosterData {
|
|
105
|
+
title: string;
|
|
106
|
+
author: string;
|
|
107
|
+
desc: string;
|
|
108
|
+
date: string;
|
|
109
|
+
cover: string;
|
|
110
|
+
url: string;
|
|
111
|
+
site: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function drawPoster(data: PosterData): Promise<string> {
|
|
115
|
+
const QRCode = (await import("qrcode")).default;
|
|
116
|
+
const scale = 2;
|
|
117
|
+
const width = 425 * scale;
|
|
118
|
+
const padding = 24 * scale;
|
|
119
|
+
const accent = readAccent();
|
|
120
|
+
|
|
121
|
+
/* 1. 资源准备:二维码 data URL + 封面(失败降级 accent 色块) */
|
|
122
|
+
const qrDataUrl = await QRCode.toDataURL(data.url, {
|
|
123
|
+
margin: 1,
|
|
124
|
+
width: 100 * scale,
|
|
125
|
+
color: { dark: "#000000", light: "#ffffff" },
|
|
126
|
+
});
|
|
127
|
+
const [qrImg, coverImg] = await Promise.all([
|
|
128
|
+
loadImage(qrDataUrl),
|
|
129
|
+
data.cover ? loadImage(data.cover) : Promise.resolve(null),
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
/* 2. 测量画布(高度先估后调) */
|
|
133
|
+
const canvas = doc.createElement("canvas");
|
|
134
|
+
const ctx = canvas.getContext("2d");
|
|
135
|
+
if (!ctx) throw new Error("Canvas context not available");
|
|
136
|
+
canvas.width = width;
|
|
137
|
+
canvas.height = 1000 * scale;
|
|
138
|
+
|
|
139
|
+
/* 3. 布局计算 */
|
|
140
|
+
const contentWidth = width - padding * 2;
|
|
141
|
+
let currentY = 0;
|
|
142
|
+
|
|
143
|
+
const coverHeight = (coverImg ? 200 : 120) * scale;
|
|
144
|
+
currentY += coverHeight;
|
|
145
|
+
currentY += padding;
|
|
146
|
+
|
|
147
|
+
ctx.font = `700 ${24 * scale}px ${FONT_STACK}`;
|
|
148
|
+
const titleLines = getLines(ctx, data.title, contentWidth).slice(
|
|
149
|
+
0,
|
|
150
|
+
3,
|
|
151
|
+
); /* clamp 3 行 */
|
|
152
|
+
const titleLineHeight = 30 * scale;
|
|
153
|
+
currentY += titleLines.length * titleLineHeight;
|
|
154
|
+
currentY += 16 * scale;
|
|
155
|
+
|
|
156
|
+
let descHeight = 0;
|
|
157
|
+
if (data.desc) {
|
|
158
|
+
ctx.font = `${14 * scale}px ${FONT_STACK}`;
|
|
159
|
+
const descLines = getLines(ctx, data.desc, contentWidth - 16 * scale).slice(
|
|
160
|
+
0,
|
|
161
|
+
6,
|
|
162
|
+
);
|
|
163
|
+
descHeight = descLines.length * 25 * scale;
|
|
164
|
+
currentY += descHeight;
|
|
165
|
+
} else {
|
|
166
|
+
currentY += 8 * scale;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
currentY += 24 * scale; /* 分隔线前距 */
|
|
170
|
+
const footerHeight = 64 * scale;
|
|
171
|
+
currentY += footerHeight;
|
|
172
|
+
currentY += padding; /* 底部留白 */
|
|
173
|
+
canvas.height = currentY;
|
|
174
|
+
|
|
175
|
+
/* 4. 绘制 */
|
|
176
|
+
ctx.fillStyle = "#ffffff";
|
|
177
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
178
|
+
|
|
179
|
+
/* 装饰圆(accent 10% 透明度) */
|
|
180
|
+
ctx.save();
|
|
181
|
+
ctx.globalAlpha = 0.1;
|
|
182
|
+
ctx.fillStyle = accent;
|
|
183
|
+
ctx.beginPath();
|
|
184
|
+
ctx.arc(width - 25 * scale, 25 * scale, 75 * scale, 0, Math.PI * 2);
|
|
185
|
+
ctx.fill();
|
|
186
|
+
ctx.beginPath();
|
|
187
|
+
ctx.arc(10 * scale, canvas.height - 10 * scale, 50 * scale, 0, Math.PI * 2);
|
|
188
|
+
ctx.fill();
|
|
189
|
+
ctx.restore();
|
|
190
|
+
|
|
191
|
+
/* 日期角标数据 */
|
|
192
|
+
let dateObj: { day: string; month: string; year: string } | null = null;
|
|
193
|
+
const parsed = new Date(data.date);
|
|
194
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
195
|
+
dateObj = {
|
|
196
|
+
day: parsed.getDate().toString().padStart(2, "0"),
|
|
197
|
+
month: (parsed.getMonth() + 1).toString().padStart(2, "0"),
|
|
198
|
+
year: parsed.getFullYear().toString(),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* 封面(object-fit: cover)或降级 accent 色块 */
|
|
203
|
+
if (coverImg) {
|
|
204
|
+
const imgRatio = coverImg.width / coverImg.height;
|
|
205
|
+
const targetRatio = width / coverHeight;
|
|
206
|
+
let sx: number;
|
|
207
|
+
let sy: number;
|
|
208
|
+
let sWidth: number;
|
|
209
|
+
let sHeight: number;
|
|
210
|
+
if (imgRatio > targetRatio) {
|
|
211
|
+
sHeight = coverImg.height;
|
|
212
|
+
sWidth = sHeight * targetRatio;
|
|
213
|
+
sx = (coverImg.width - sWidth) / 2;
|
|
214
|
+
sy = 0;
|
|
215
|
+
} else {
|
|
216
|
+
sWidth = coverImg.width;
|
|
217
|
+
sHeight = sWidth / targetRatio;
|
|
218
|
+
sx = 0;
|
|
219
|
+
sy = (coverImg.height - sHeight) / 2;
|
|
220
|
+
}
|
|
221
|
+
ctx.drawImage(coverImg, sx, sy, sWidth, sHeight, 0, 0, width, coverHeight);
|
|
222
|
+
} else {
|
|
223
|
+
ctx.save();
|
|
224
|
+
ctx.fillStyle = accent;
|
|
225
|
+
ctx.globalAlpha = 0.2;
|
|
226
|
+
ctx.fillRect(0, 0, width, coverHeight);
|
|
227
|
+
ctx.restore();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* 日期角标(封面左下角) */
|
|
231
|
+
if (dateObj) {
|
|
232
|
+
const boxW = 60 * scale;
|
|
233
|
+
const boxH = 60 * scale;
|
|
234
|
+
const boxX = padding;
|
|
235
|
+
const boxY = coverHeight - boxH;
|
|
236
|
+
|
|
237
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
|
|
238
|
+
drawRoundedRect(ctx, boxX, boxY, boxW, boxH, 4 * scale);
|
|
239
|
+
ctx.fill();
|
|
240
|
+
|
|
241
|
+
ctx.fillStyle = "#ffffff";
|
|
242
|
+
ctx.textAlign = "center";
|
|
243
|
+
ctx.textBaseline = "middle";
|
|
244
|
+
ctx.font = `700 ${30 * scale}px ${FONT_STACK}`;
|
|
245
|
+
ctx.fillText(dateObj.day, boxX + boxW / 2, boxY + 24 * scale);
|
|
246
|
+
|
|
247
|
+
ctx.beginPath();
|
|
248
|
+
ctx.strokeStyle = "rgba(255, 255, 255, 0.6)";
|
|
249
|
+
ctx.lineWidth = 1 * scale;
|
|
250
|
+
ctx.moveTo(boxX + 10 * scale, boxY + 42 * scale);
|
|
251
|
+
ctx.lineTo(boxX + boxW - 10 * scale, boxY + 42 * scale);
|
|
252
|
+
ctx.stroke();
|
|
253
|
+
|
|
254
|
+
ctx.font = `${10 * scale}px ${FONT_STACK}`;
|
|
255
|
+
ctx.fillText(
|
|
256
|
+
`${dateObj.year} ${dateObj.month}`,
|
|
257
|
+
boxX + boxW / 2,
|
|
258
|
+
boxY + 51 * scale,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* 标题 */
|
|
263
|
+
let drawY = coverHeight + padding;
|
|
264
|
+
ctx.textBaseline = "top";
|
|
265
|
+
ctx.textAlign = "left";
|
|
266
|
+
ctx.font = `700 ${24 * scale}px ${FONT_STACK}`;
|
|
267
|
+
ctx.fillStyle = "#111827";
|
|
268
|
+
titleLines.forEach((line) => {
|
|
269
|
+
ctx.fillText(line, padding, drawY);
|
|
270
|
+
drawY += titleLineHeight;
|
|
271
|
+
});
|
|
272
|
+
drawY += 16 * scale - (titleLineHeight - 24 * scale);
|
|
273
|
+
|
|
274
|
+
/* 摘要(左侧竖线 + 最多 6 行) */
|
|
275
|
+
if (data.desc) {
|
|
276
|
+
ctx.fillStyle = "#e5e7eb";
|
|
277
|
+
drawRoundedRect(
|
|
278
|
+
ctx,
|
|
279
|
+
padding,
|
|
280
|
+
drawY - 8 * scale,
|
|
281
|
+
4 * scale,
|
|
282
|
+
descHeight + 8 * scale,
|
|
283
|
+
2 * scale,
|
|
284
|
+
);
|
|
285
|
+
ctx.fill();
|
|
286
|
+
|
|
287
|
+
ctx.font = `${14 * scale}px ${FONT_STACK}`;
|
|
288
|
+
ctx.fillStyle = "#4b5563";
|
|
289
|
+
getLines(ctx, data.desc, contentWidth - 16 * scale)
|
|
290
|
+
.slice(0, 6)
|
|
291
|
+
.forEach((line) => {
|
|
292
|
+
ctx.fillText(line, padding + 16 * scale, drawY);
|
|
293
|
+
drawY += 25 * scale;
|
|
294
|
+
});
|
|
295
|
+
} else {
|
|
296
|
+
drawY += 8 * scale;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* 分隔线 */
|
|
300
|
+
drawY += 24 * scale;
|
|
301
|
+
ctx.beginPath();
|
|
302
|
+
ctx.strokeStyle = "#f3f4f6";
|
|
303
|
+
ctx.lineWidth = 1 * scale;
|
|
304
|
+
ctx.moveTo(padding, drawY);
|
|
305
|
+
ctx.lineTo(width - padding, drawY);
|
|
306
|
+
ctx.stroke();
|
|
307
|
+
drawY += 24 * scale;
|
|
308
|
+
|
|
309
|
+
/* 页脚:左作者文字(主题中立化:去头像),右二维码 + 站点信息 */
|
|
310
|
+
const footerY = drawY;
|
|
311
|
+
const textCenterY = footerY + 32 * scale;
|
|
312
|
+
|
|
313
|
+
ctx.textAlign = "left";
|
|
314
|
+
ctx.fillStyle = "#9ca3af";
|
|
315
|
+
ctx.font = `${12 * scale}px ${FONT_STACK}`;
|
|
316
|
+
ctx.fillText(label("author"), padding, textCenterY - 20 * scale);
|
|
317
|
+
|
|
318
|
+
ctx.fillStyle = "#1f2937";
|
|
319
|
+
ctx.font = `700 ${20 * scale}px ${FONT_STACK}`;
|
|
320
|
+
ctx.fillText(data.author, padding, textCenterY + 4 * scale);
|
|
321
|
+
|
|
322
|
+
const qrSize = 64 * scale;
|
|
323
|
+
const qrX = width - padding - qrSize;
|
|
324
|
+
ctx.fillStyle = "#ffffff";
|
|
325
|
+
ctx.shadowColor = "rgba(0, 0, 0, 0.05)";
|
|
326
|
+
ctx.shadowBlur = 4 * scale;
|
|
327
|
+
ctx.shadowOffsetY = 2 * scale;
|
|
328
|
+
drawRoundedRect(ctx, qrX, footerY, qrSize, qrSize, 4 * scale);
|
|
329
|
+
ctx.fill();
|
|
330
|
+
ctx.shadowColor = "transparent";
|
|
331
|
+
|
|
332
|
+
const qrInner = 56 * scale;
|
|
333
|
+
const qrPad = (qrSize - qrInner) / 2;
|
|
334
|
+
if (qrImg) {
|
|
335
|
+
ctx.drawImage(qrImg, qrX + qrPad, footerY + qrPad, qrInner, qrInner);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const siteInfoX = qrX - 16 * scale;
|
|
339
|
+
ctx.textAlign = "right";
|
|
340
|
+
ctx.fillStyle = "#9ca3af";
|
|
341
|
+
ctx.font = `${12 * scale}px ${FONT_STACK}`;
|
|
342
|
+
ctx.fillText(label("scanToRead"), siteInfoX, textCenterY - 20 * scale);
|
|
343
|
+
|
|
344
|
+
ctx.fillStyle = "#1f2937";
|
|
345
|
+
ctx.font = `700 ${20 * scale}px ${FONT_STACK}`;
|
|
346
|
+
ctx.fillText(data.site, siteInfoX, textCenterY + 4 * scale);
|
|
347
|
+
|
|
348
|
+
return canvas.toDataURL("image/png");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* ---------- 弹窗:懒建 DOM,生成中 spinner,完成后展示海报 + 操作区 ---------- */
|
|
352
|
+
export async function openSharePoster(btn: HTMLElement) {
|
|
353
|
+
if (doc.getElementById("qyPosterModal")) return; /* 防重复打开 */
|
|
354
|
+
|
|
355
|
+
const data: PosterData = {
|
|
356
|
+
title: btn.dataset.title || "",
|
|
357
|
+
author: btn.dataset.author || "",
|
|
358
|
+
desc: btn.dataset.desc || "",
|
|
359
|
+
date: btn.dataset.date || "",
|
|
360
|
+
cover: btn.dataset.cover || "",
|
|
361
|
+
url: btn.dataset.url || "",
|
|
362
|
+
site: btn.dataset.site || "",
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const prevOverflow = doc.body.style.overflow;
|
|
366
|
+
doc.body.style.overflow = "hidden";
|
|
367
|
+
|
|
368
|
+
const modal = doc.createElement("div");
|
|
369
|
+
modal.id = "qyPosterModal";
|
|
370
|
+
modal.className = "poster-modal";
|
|
371
|
+
modal.setAttribute("role", "dialog");
|
|
372
|
+
modal.setAttribute("aria-modal", "true");
|
|
373
|
+
/* 骨架仅含静态结构,动态文本/alt 随后用 DOM API 填值(文章标题可能含引号等字符) */
|
|
374
|
+
modal.innerHTML = `
|
|
375
|
+
<div class="poster-card">
|
|
376
|
+
<header class="poster-head">
|
|
377
|
+
<strong class="poster-title"></strong>
|
|
378
|
+
<button type="button" class="poster-close">
|
|
379
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
|
380
|
+
</button>
|
|
381
|
+
</header>
|
|
382
|
+
<div class="poster-stage">
|
|
383
|
+
<div class="poster-loading"><span class="poster-spinner" aria-hidden="true"></span><span class="poster-loading-text"></span></div>
|
|
384
|
+
<img class="poster-img" hidden />
|
|
385
|
+
</div>
|
|
386
|
+
<footer class="poster-foot">
|
|
387
|
+
<button type="button" class="poster-btn poster-copy"></button>
|
|
388
|
+
<button type="button" class="poster-btn poster-save" disabled></button>
|
|
389
|
+
</footer>
|
|
390
|
+
</div>`;
|
|
391
|
+
modal.querySelector<HTMLElement>(".poster-title")!.textContent =
|
|
392
|
+
label("sharePoster");
|
|
393
|
+
modal
|
|
394
|
+
.querySelector(".poster-close")!
|
|
395
|
+
.setAttribute("aria-label", label("close"));
|
|
396
|
+
modal.querySelector<HTMLElement>(".poster-loading-text")!.textContent =
|
|
397
|
+
label("generatingPoster");
|
|
398
|
+
modal.querySelector<HTMLImageElement>(".poster-img")!.alt = data.title;
|
|
399
|
+
modal.querySelector<HTMLButtonElement>(".poster-copy")!.textContent =
|
|
400
|
+
label("copyLink");
|
|
401
|
+
modal.querySelector<HTMLButtonElement>(".poster-save")!.textContent =
|
|
402
|
+
label("savePoster");
|
|
403
|
+
doc.body.appendChild(modal);
|
|
404
|
+
|
|
405
|
+
const close = () => {
|
|
406
|
+
doc.removeEventListener("keydown", onKey);
|
|
407
|
+
modal.remove();
|
|
408
|
+
doc.body.style.overflow = prevOverflow;
|
|
409
|
+
};
|
|
410
|
+
const onKey = (e: KeyboardEvent) => {
|
|
411
|
+
if (e.key === "Escape") close();
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
modal.addEventListener("click", (e) => {
|
|
415
|
+
if (e.target === modal) close();
|
|
416
|
+
});
|
|
417
|
+
modal.querySelector(".poster-close")?.addEventListener("click", close);
|
|
418
|
+
doc.addEventListener("keydown", onKey);
|
|
419
|
+
|
|
420
|
+
/* 复制链接(clipboard 优先 + execCommand 兜底),2s 恢复文案 */
|
|
421
|
+
modal.querySelector(".poster-copy")?.addEventListener("click", (e) => {
|
|
422
|
+
const button = e.currentTarget as HTMLButtonElement;
|
|
423
|
+
const done = () => {
|
|
424
|
+
button.textContent = label("copied");
|
|
425
|
+
setTimeout(() => {
|
|
426
|
+
button.textContent = label("copyLink");
|
|
427
|
+
}, 2000);
|
|
428
|
+
};
|
|
429
|
+
if (navigator.clipboard?.writeText) {
|
|
430
|
+
navigator.clipboard
|
|
431
|
+
.writeText(data.url)
|
|
432
|
+
.then(done)
|
|
433
|
+
.catch(() => {});
|
|
434
|
+
} else {
|
|
435
|
+
const ta = doc.createElement("textarea");
|
|
436
|
+
ta.value = data.url;
|
|
437
|
+
doc.body.appendChild(ta);
|
|
438
|
+
ta.select();
|
|
439
|
+
doc.execCommand("copy");
|
|
440
|
+
ta.remove();
|
|
441
|
+
done();
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
/* 生成完成:展示海报 + 解锁保存按钮 */
|
|
446
|
+
const img = modal.querySelector<HTMLImageElement>(".poster-img");
|
|
447
|
+
const saveBtn = modal.querySelector<HTMLButtonElement>(".poster-save");
|
|
448
|
+
try {
|
|
449
|
+
const poster = await drawPoster(data);
|
|
450
|
+
if (img) {
|
|
451
|
+
img.src = poster;
|
|
452
|
+
img.hidden = false;
|
|
453
|
+
}
|
|
454
|
+
if (saveBtn) {
|
|
455
|
+
saveBtn.disabled = false;
|
|
456
|
+
saveBtn.addEventListener("click", () => {
|
|
457
|
+
const a = doc.createElement("a");
|
|
458
|
+
a.href = poster;
|
|
459
|
+
a.download = `poster-${data.title.replace(/\s+/g, "-")}.png`;
|
|
460
|
+
a.click();
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
} catch (err) {
|
|
464
|
+
/* 生成失败(qrcode 加载失败/canvas 不可用):关弹窗 + 控制台留痕,不留空壳 */
|
|
465
|
+
console.error("[share-poster] generate failed:", err);
|
|
466
|
+
close();
|
|
467
|
+
}
|
|
468
|
+
}
|