375 lines
13 KiB
JavaScript
375 lines
13 KiB
JavaScript
import { resolvePublicRequestOrigin } from '../mindspace-public-page-context.mjs';
|
||
import { extractCoverSignals } from '../mindspace-thumbnails.mjs';
|
||
|
||
function escapePublicHtml(value) {
|
||
return String(value ?? '')
|
||
.replaceAll('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll("'", ''');
|
||
}
|
||
export function extractOgImageUrl(html) {
|
||
return (
|
||
String(html ?? '').match(/<meta[^>]+property=["']og:image["'][^>]+content=["']([^"']+)["']/i)?.[1] ||
|
||
String(html ?? '').match(/<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:image["']/i)?.[1] ||
|
||
''
|
||
);
|
||
}
|
||
export function extractShareMetaFromPageHtml(html) {
|
||
const signals = extractCoverSignals(String(html ?? ''));
|
||
return {
|
||
title: detectPublishedPageTitle(html),
|
||
subtitle: signals.subtitle,
|
||
};
|
||
}
|
||
|
||
export function appendQueryParam(url, key, value) {
|
||
const separator = url.includes('?') ? '&' : '?';
|
||
return `${url}${separator}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||
}
|
||
|
||
export function removeQueryParam(url, key) {
|
||
if (!url || !url.includes('?')) return url;
|
||
const [base, queryAndHash] = url.split('?', 2);
|
||
const [query, hash = ''] = queryAndHash.split('#', 2);
|
||
const params = new URLSearchParams(query);
|
||
params.delete(key);
|
||
const nextQuery = params.toString();
|
||
return `${base}${nextQuery ? `?${nextQuery}` : ''}${hash ? `#${hash}` : ''}`;
|
||
}
|
||
|
||
export function resolveRequestOrigin(req) {
|
||
return resolvePublicRequestOrigin({
|
||
hostHeader: req.headers['x-forwarded-host'] || req.headers.host || '',
|
||
forwardedProto: req.headers['x-forwarded-proto'] || '',
|
||
protocol: req.protocol,
|
||
});
|
||
}
|
||
|
||
export function detectPublishedPageTitle(html) {
|
||
const match = String(html ?? '').match(/<title[^>]*>([^<]+)<\/title>/i);
|
||
return match?.[1]?.replace(/\s+/g, ' ').trim() || 'MindSpace 页面';
|
||
}
|
||
|
||
export function publishedPageShellHtml({ iframeUrl, shareUrl, title, longImageUrl }) {
|
||
const iframeSrc = escapePublicHtml(iframeUrl);
|
||
const safeShareUrl = escapePublicHtml(shareUrl);
|
||
const safeTitle = escapePublicHtml(title);
|
||
const serializedShareUrl = JSON.stringify(shareUrl).replace(/</g, '\\u003c');
|
||
const serializedTitle = JSON.stringify(title).replace(/</g, '\\u003c');
|
||
const serializedLongImageUrl = JSON.stringify(longImageUrl).replace(/</g, '\\u003c');
|
||
return `<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'self'; script-src 'unsafe-inline'; frame-src 'self'; base-uri 'none'; form-action 'none'">
|
||
<title>${safeTitle}</title>
|
||
<style>
|
||
* { box-sizing: border-box; }
|
||
html, body { margin: 0; min-height: 100%; background: #f6f1e7; }
|
||
body { font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif; color: #18211d; }
|
||
.publication-shell { position: relative; min-height: 100vh; }
|
||
.publication-frame { display: block; width: 100%; min-height: 100vh; border: 0; background: #fff; }
|
||
.publication-share-fab {
|
||
position: fixed;
|
||
right: 18px;
|
||
bottom: calc(18px + env(safe-area-inset-bottom, 0px));
|
||
z-index: 50;
|
||
border: 0;
|
||
border-radius: 999px;
|
||
padding: 14px 18px;
|
||
background: linear-gradient(135deg, #2f6f57, #1f4c3c);
|
||
color: #fffdf7;
|
||
font: 700 14px/1 ui-sans-serif, sans-serif;
|
||
letter-spacing: .04em;
|
||
box-shadow: 0 20px 48px rgba(24, 33, 29, .24);
|
||
cursor: pointer;
|
||
}
|
||
.publication-share-sheet[hidden] { display: none; }
|
||
.publication-share-sheet {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 60;
|
||
display: grid;
|
||
place-items: end center;
|
||
padding: 16px;
|
||
background: rgba(7, 12, 10, 0.56);
|
||
}
|
||
.publication-share-panel {
|
||
width: min(480px, 100%);
|
||
padding: 18px 18px 22px;
|
||
border-radius: 24px 24px 20px 20px;
|
||
background: #fffaf2;
|
||
box-shadow: 0 24px 80px rgba(24, 33, 29, 0.24);
|
||
}
|
||
.publication-share-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.publication-share-header p {
|
||
margin: 0;
|
||
color: #9b6518;
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
letter-spacing: .16em;
|
||
text-transform: uppercase;
|
||
}
|
||
.publication-share-header h2 {
|
||
margin: 6px 0 0;
|
||
font: 500 28px/1.1 Georgia, 'Times New Roman', serif;
|
||
color: #18211d;
|
||
}
|
||
.publication-share-header strong {
|
||
display: block;
|
||
margin-top: 6px;
|
||
color: #68716c;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
}
|
||
.publication-share-close {
|
||
width: 36px;
|
||
height: 36px;
|
||
border: 0;
|
||
border-radius: 999px;
|
||
background: rgba(24, 33, 29, 0.08);
|
||
color: #18211d;
|
||
font-size: 24px;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
}
|
||
.publication-share-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 12px;
|
||
}
|
||
.publication-share-option {
|
||
display: grid;
|
||
gap: 8px;
|
||
justify-items: center;
|
||
padding: 14px 10px;
|
||
border: 1px solid rgba(24, 33, 29, 0.08);
|
||
border-radius: 18px;
|
||
background: rgba(255, 255, 255, 0.72);
|
||
color: #18211d;
|
||
font: inherit;
|
||
cursor: pointer;
|
||
}
|
||
.publication-share-option:disabled {
|
||
opacity: .6;
|
||
cursor: wait;
|
||
}
|
||
.publication-share-option span {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 14px;
|
||
color: #fff;
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
}
|
||
.publication-share-option[data-action="wechat"] span { background: linear-gradient(145deg, #07c160, #06ad56); }
|
||
.publication-share-option[data-action="copy"] span { background: linear-gradient(145deg, #2f6f57, #245845); }
|
||
.publication-share-option[data-action="capture"] span { background: linear-gradient(145deg, #8f6b2f, #6f5121); }
|
||
.publication-share-link, .publication-share-message {
|
||
margin-top: 16px;
|
||
padding: 12px 14px;
|
||
border-radius: 14px;
|
||
font-size: 13px;
|
||
}
|
||
.publication-share-link {
|
||
background: rgba(24, 33, 29, 0.05);
|
||
}
|
||
.publication-share-link code {
|
||
display: block;
|
||
margin-top: 6px;
|
||
word-break: break-all;
|
||
color: #445049;
|
||
font-size: 12px;
|
||
}
|
||
.publication-share-message {
|
||
display: none;
|
||
color: #245845;
|
||
background: rgba(47, 111, 87, 0.12);
|
||
}
|
||
.publication-share-message.is-visible {
|
||
display: block;
|
||
}
|
||
@media (max-width: 480px) {
|
||
.publication-share-sheet {
|
||
padding: 0;
|
||
place-items: end stretch;
|
||
}
|
||
.publication-share-panel {
|
||
width: 100%;
|
||
border-radius: 24px 24px 0 0;
|
||
}
|
||
.publication-share-fab {
|
||
right: 14px;
|
||
bottom: calc(14px + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="publication-shell">
|
||
<!-- Game-like publications need script execution, but not same-origin sandbox escape. -->
|
||
<iframe class="publication-frame" title="${safeTitle}" src="${iframeSrc}" sandbox="allow-scripts allow-downloads"></iframe>
|
||
</div>
|
||
<button type="button" class="publication-share-fab" id="publication-share-fab">分享</button>
|
||
<div class="publication-share-sheet" id="publication-share-sheet" hidden>
|
||
<section class="publication-share-panel" role="dialog" aria-modal="true" aria-labelledby="publication-share-title">
|
||
<header class="publication-share-header">
|
||
<div>
|
||
<p>Share</p>
|
||
<h2 id="publication-share-title">分享页面</h2>
|
||
<strong>${safeTitle}</strong>
|
||
</div>
|
||
<button type="button" class="publication-share-close" id="publication-share-close" aria-label="关闭">×</button>
|
||
</header>
|
||
<div class="publication-share-grid">
|
||
<button type="button" class="publication-share-option" data-action="wechat"><span>微</span>微信</button>
|
||
<button type="button" class="publication-share-option" data-action="copy"><span>链</span>复制链接</button>
|
||
<button type="button" class="publication-share-option" data-action="capture"><span>图</span>保存长图</button>
|
||
</div>
|
||
<div class="publication-share-link">
|
||
分享链接
|
||
<code>${safeShareUrl}</code>
|
||
</div>
|
||
<div class="publication-share-message" id="publication-share-message" role="status"></div>
|
||
</section>
|
||
</div>
|
||
<script>
|
||
(function () {
|
||
var shareUrl = ${serializedShareUrl};
|
||
var shareTitle = ${serializedTitle};
|
||
var longImageUrl = ${serializedLongImageUrl};
|
||
var fab = document.getElementById('publication-share-fab');
|
||
var sheet = document.getElementById('publication-share-sheet');
|
||
var close = document.getElementById('publication-share-close');
|
||
var message = document.getElementById('publication-share-message');
|
||
var frame = document.querySelector('.publication-frame');
|
||
var actionButtons = sheet ? Array.prototype.slice.call(sheet.querySelectorAll('.publication-share-option')) : [];
|
||
function setBusy(busy) {
|
||
actionButtons.forEach(function (button) {
|
||
button.disabled = !!busy;
|
||
button.setAttribute('aria-busy', busy ? 'true' : 'false');
|
||
});
|
||
}
|
||
function setMessage(text, isError) {
|
||
if (!message) return;
|
||
message.textContent = text || '';
|
||
message.classList.toggle('is-visible', Boolean(text));
|
||
message.style.color = isError ? '#8b2d20' : '#245845';
|
||
message.style.background = isError ? 'rgba(139, 45, 32, 0.12)' : 'rgba(47, 111, 87, 0.12)';
|
||
}
|
||
function openSheet() {
|
||
if (sheet) sheet.hidden = false;
|
||
setMessage('');
|
||
}
|
||
function closeSheet() {
|
||
if (sheet) sheet.hidden = true;
|
||
}
|
||
async function copyText(text) {
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
return;
|
||
} catch {
|
||
// Fall back to execCommand for browsers where clipboard API exists but is blocked.
|
||
}
|
||
}
|
||
var textarea = document.createElement('textarea');
|
||
textarea.value = text;
|
||
textarea.style.position = 'fixed';
|
||
textarea.style.opacity = '0';
|
||
textarea.style.left = '-9999px';
|
||
document.body.appendChild(textarea);
|
||
textarea.focus();
|
||
textarea.select();
|
||
var copied = document.execCommand('copy');
|
||
textarea.remove();
|
||
if (!copied) throw new Error('复制失败,请手动复制链接');
|
||
}
|
||
async function saveLongImage() {
|
||
var link = document.createElement('a');
|
||
link.href = longImageUrl;
|
||
link.download = '';
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
link.remove();
|
||
}
|
||
if (fab) fab.addEventListener('click', openSheet);
|
||
if (close) close.addEventListener('click', closeSheet);
|
||
if (sheet) {
|
||
sheet.addEventListener('click', function (event) {
|
||
if (event.target === sheet) closeSheet();
|
||
});
|
||
actionButtons.forEach(function (button) {
|
||
button.addEventListener('click', async function () {
|
||
var action = button.getAttribute('data-action');
|
||
try {
|
||
setBusy(true);
|
||
setMessage('');
|
||
if (action === 'wechat') {
|
||
await copyText([shareTitle, shareUrl].filter(Boolean).join('\\n'));
|
||
setMessage('文案已复制,请打开微信粘贴分享');
|
||
return;
|
||
}
|
||
if (action === 'copy') {
|
||
await copyText(shareUrl);
|
||
setMessage('链接已复制');
|
||
return;
|
||
}
|
||
if (action === 'capture') {
|
||
setMessage('正在生成长图,请稍候');
|
||
await saveLongImage();
|
||
setMessage('长图已保存');
|
||
}
|
||
} catch (error) {
|
||
setMessage(error && error.message ? error.message : '操作失败,请稍后再试', true);
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
function syncFrameHeight() {
|
||
if (!frame) return;
|
||
var doc = frame.contentDocument;
|
||
var body = doc && doc.body;
|
||
var root = doc && doc.documentElement;
|
||
var height = Math.max(
|
||
body ? body.scrollHeight : 0,
|
||
body ? body.offsetHeight : 0,
|
||
root ? root.scrollHeight : 0,
|
||
root ? root.offsetHeight : 0,
|
||
window.innerHeight,
|
||
320
|
||
);
|
||
frame.style.height = height + 'px';
|
||
}
|
||
if (frame) {
|
||
frame.addEventListener('load', function () {
|
||
syncFrameHeight();
|
||
window.setTimeout(syncFrameHeight, 300);
|
||
});
|
||
}
|
||
window.addEventListener('message', function (event) {
|
||
if (!event || !event.data || event.data.type !== 'plaza:embed-section' || !frame) return;
|
||
if (event.data.height) {
|
||
frame.style.height = Math.max(Number(event.data.height) || 0, window.innerHeight, 320) + 'px';
|
||
}
|
||
});
|
||
window.addEventListener('resize', syncFrameHeight);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>`;
|
||
}
|