100 lines
4.4 KiB
React
100 lines
4.4 KiB
React
import React, { useEffect, useState } from 'react';
|
||
import { useSearchParams } from 'react-router-dom';
|
||
import { useTheme } from '../theme.jsx';
|
||
import { getToken } from '../api/client.js';
|
||
|
||
/**
|
||
* 面板嵌入页(迁移自 embed.html):
|
||
* 入口参数 ?url=&title=&proxy=1 —— iframe 展示目标站点;
|
||
* proxy=1 或 HTTPS 页内嵌 HTTP 目标时走 /api/proxy/fetch?url=(代理剥 X-Frame-Options/CSP)。
|
||
* 工具栏:关闭(有来源则 history.back)、标题、原站(新标签)、主题切换。
|
||
*/
|
||
export default function Embed() {
|
||
const [searchParams] = useSearchParams();
|
||
const url = searchParams.get('url');
|
||
const title = searchParams.get('title') || url || '嵌入';
|
||
const useProxy = searchParams.get('proxy') === '1';
|
||
const { theme, toggleTheme } = useTheme();
|
||
const [showHint, setShowHint] = useState(false);
|
||
|
||
const isHttpsPage = window.location.protocol === 'https:';
|
||
const isHttpTarget = url ? url.startsWith('http:') : false;
|
||
const needsProxy = useProxy || (isHttpsPage && isHttpTarget);
|
||
// proxy 模式:iframe 无法携带 Authorization header,改用 ?token= 认证(后端 queryTokenAuth 转写)
|
||
const iframeSrc = url
|
||
? (needsProxy
|
||
? '/api/proxy/fetch?url=' + encodeURIComponent(url) + '&token=' + encodeURIComponent(getToken() || '')
|
||
: url)
|
||
: '';
|
||
|
||
// 非代理嵌入时 2s 后提示"无法嵌入?安装扩展"(照 v1)
|
||
useEffect(() => {
|
||
if (!url || needsProxy) return;
|
||
const t = setTimeout(() => setShowHint(true), 2000);
|
||
return () => clearTimeout(t);
|
||
}, [url, needsProxy]);
|
||
|
||
const handleClose = (e) => {
|
||
// 有来源页时返回上一页,否则跳首页
|
||
if (document.referrer && document.referrer !== window.location.href) {
|
||
e.preventDefault();
|
||
window.history.back();
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="embed-page" style={{ height: 'calc(100vh - 56px)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||
<h1 className="sr-only">嵌入页面</h1>
|
||
<div
|
||
className="embed-toolbar"
|
||
style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', height: 48, background: 'var(--md-ref-surface-container)', borderBottom: '1px solid var(--md-ref-outline-variant)', flexShrink: 0 }}
|
||
>
|
||
<a href="/" className="btn-icon" onClick={handleClose} title="关闭"><span className="material-icons">close</span></a>
|
||
<span className="embed-title" style={{ flex: 1, fontWeight: 500, fontSize: 15, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{title}</span>
|
||
{url && (
|
||
<a href={url} target="_blank" rel="noopener" className="btn btn-text btn-sm" title="在新标签页中打开">
|
||
<span className="material-icons">open_in_new</span> 原站
|
||
</a>
|
||
)}
|
||
<button className="btn-icon" onClick={toggleTheme} title="切换主题">
|
||
<span className="material-icons">{theme === 'dark' ? 'light_mode' : 'dark_mode'}</span>
|
||
</button>
|
||
</div>
|
||
|
||
{showHint && (
|
||
<div style={{ padding: '8px 16px', background: 'var(--md-ref-primary-container)', color: 'var(--md-ref-on-primary-container)', fontSize: 13, textAlign: 'center', flexShrink: 0 }}>
|
||
无法嵌入?试试安装{' '}
|
||
<a href="https://chromewebstore.google.com/search/ignore%20x-frame" target="_blank" rel="noopener" style={{ color: 'inherit', fontWeight: 600, textDecoration: 'underline' }}>
|
||
Ignore X-Frame-Headers
|
||
</a>{' '}
|
||
扩展,或点击右上角「原站」在新标签页打开
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowHint(false)}
|
||
aria-label="关闭提示"
|
||
style={{ cursor: 'pointer', marginLeft: 8, fontWeight: 600, height: 'auto', padding: '2px 8px', fontSize: 'inherit', fontFamily: 'inherit', background: 'transparent', border: 'none', color: 'inherit' }}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{iframeSrc ? (
|
||
<iframe
|
||
className="embed-container"
|
||
title={title}
|
||
style={{ flex: 1, width: '100%', border: 'none' }}
|
||
sandbox="allow-scripts allow-forms allow-same-origin allow-popups"
|
||
loading="lazy"
|
||
src={iframeSrc}
|
||
/>
|
||
) : (
|
||
<div className="empty-state" style={{ flex: 1 }}>
|
||
<div className="empty-icon">🔗</div>
|
||
<p>缺少 url 参数,无法加载嵌入内容</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|