183 lines
5.9 KiB
TypeScript
183 lines
5.9 KiB
TypeScript
// Panel Content Script —— 商品页右下角悬浮按钮 + 页内悬浮面板
|
||
//
|
||
// 面板 = iframe 加载插件内置 sidepanel.html(扩展页面在 iframe 里仍有 chrome.* 权限,
|
||
// 采集/生成/导出逻辑零改动);按钮与面板容器渲染在独立 Shadow DOM 中,
|
||
// 不受商品页全局 CSS 影响,面板悬浮覆盖页面、不挤压原页面布局。
|
||
import { matchProfile } from '../src/profiles/index';
|
||
|
||
/** 面板内 App.tsx → 宿主页的收起消息 */
|
||
const PANEL_CLOSE_MSG = 'sc-panel-close';
|
||
|
||
const STYLES = `
|
||
:host { all: initial; }
|
||
|
||
.fab {
|
||
position: fixed;
|
||
right: 24px;
|
||
bottom: 24px;
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 50%;
|
||
border: none;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font: 700 20px/1 -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||
color: #fff;
|
||
background: linear-gradient(135deg, #8b5cf6, #6d28d9);
|
||
box-shadow: 0 4px 16px rgba(109, 40, 217, 0.45);
|
||
z-index: 3;
|
||
transition: transform 0.15s ease;
|
||
}
|
||
.fab:hover { transform: scale(1.08); }
|
||
.fab.hidden { display: none; }
|
||
|
||
/* 收起按钮:骑在面板左上边缘(一半在面板外)。必须在 iframe 外渲染——
|
||
iframe 裁剪内容,iframe 内的元素永远溢不出面板边界 */
|
||
.panel-close {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: min(880px, 100vw); /* 面板左边缘 = 视口右沿 - 面板宽度 */
|
||
transform: translateX(50%);
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 50%;
|
||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||
background: #fff;
|
||
color: #555;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
|
||
z-index: 4;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.25s ease, transform 0.15s ease;
|
||
}
|
||
.panel-close.visible { opacity: 1; pointer-events: auto; }
|
||
.panel-close:hover { color: #6d28d9; border-color: #6d28d9; transform: translateX(50%) scale(1.08); }
|
||
|
||
.panel {
|
||
position: fixed;
|
||
top: 0;
|
||
bottom: 0;
|
||
right: 0;
|
||
width: min(880px, 100vw);
|
||
border-radius: 14px 0 0 14px;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(0, 0, 0, 0.06);
|
||
transform: translateX(100%);
|
||
transition: transform 0.25s ease;
|
||
z-index: 2;
|
||
pointer-events: none;
|
||
}
|
||
.panel.open { transform: translateX(0); pointer-events: auto; }
|
||
|
||
.panel iframe {
|
||
width: 100%;
|
||
height: 100%;
|
||
border: 0;
|
||
display: block;
|
||
background: #fff;
|
||
}
|
||
`;
|
||
|
||
export default defineContentScript({
|
||
matches: [
|
||
// Ozon
|
||
'https://*.ozon.ru/*',
|
||
'https://*.ozon.kz/*',
|
||
'https://*.ozon.by/*',
|
||
// 1688
|
||
'https://detail.1688.com/*',
|
||
// 淘宝 / 天猫
|
||
'https://item.taobao.com/*',
|
||
'https://detail.tmall.com/*',
|
||
],
|
||
async main(ctx) {
|
||
const ui = await createShadowRootUi(ctx, {
|
||
name: 'suite-studio-panel',
|
||
position: 'overlay',
|
||
anchor: 'body',
|
||
alignment: 'bottom-right',
|
||
zIndex: 2147483646,
|
||
css: STYLES,
|
||
isolateEvents: true,
|
||
onMount(container) {
|
||
const fab = document.createElement('button');
|
||
fab.className = 'fab hidden';
|
||
fab.title = '电商套图工作台';
|
||
fab.textContent = '套';
|
||
|
||
const panel = document.createElement('div');
|
||
panel.className = 'panel';
|
||
// iframe 懒加载:首次展开才设 src,避免每个商品页都加载整个面板应用
|
||
const iframe = document.createElement('iframe');
|
||
iframe.title = '电商套图工作台';
|
||
panel.append(iframe);
|
||
|
||
// 外置收起按钮(骑在面板左上边缘,一半在面板外)
|
||
const closeBtn = document.createElement('button');
|
||
closeBtn.className = 'panel-close';
|
||
closeBtn.title = '收起面板(Esc)';
|
||
closeBtn.innerHTML =
|
||
'<svg width="12" height="12" viewBox="0 0 12 12" fill="none">' +
|
||
'<path d="M2 2l8 8M10 2l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
|
||
container.append(fab, panel, closeBtn);
|
||
|
||
const open = () => {
|
||
if (!iframe.src) iframe.src = chrome.runtime.getURL('/sidepanel.html');
|
||
panel.classList.add('open');
|
||
closeBtn.classList.add('visible');
|
||
fab.classList.add('hidden');
|
||
// 聚焦进面板,键盘操作(Esc 关闭 / 预览翻页)直接可用
|
||
iframe.focus();
|
||
};
|
||
const close = () => {
|
||
panel.classList.remove('open');
|
||
closeBtn.classList.remove('visible');
|
||
if (isProductPage()) fab.classList.remove('hidden');
|
||
};
|
||
|
||
fab.addEventListener('click', open);
|
||
closeBtn.addEventListener('click', close);
|
||
|
||
// 面板内 App(Esc)→ 收起;✕ 按钮已外置到宿主层(closeBtn)
|
||
window.addEventListener('message', (e) => {
|
||
if (e.source === iframe.contentWindow && (e.data as any)?.type === PANEL_CLOSE_MSG) close();
|
||
});
|
||
|
||
// 工具栏图标点击 → 开关面板
|
||
chrome.runtime.onMessage.addListener((msg: any) => {
|
||
if (msg?.action === 'toggle-suite-panel') {
|
||
panel.classList.contains('open') ? close() : open();
|
||
}
|
||
});
|
||
|
||
// 仅商品详情页显示按钮;站内软导航后重判(WXT 内置事件,自动拦截 pushState/replaceState/popState)
|
||
const refresh = () => {
|
||
if (isProductPage()) {
|
||
if (!panel.classList.contains('open')) fab.classList.remove('hidden');
|
||
} else {
|
||
fab.classList.add('hidden');
|
||
close();
|
||
}
|
||
};
|
||
ctx.addEventListener(window, 'wxt:locationchange', refresh);
|
||
refresh();
|
||
|
||
return { open, close };
|
||
},
|
||
});
|
||
ui.mount();
|
||
},
|
||
});
|
||
|
||
/** 是否为四站点支持的商品详情页(与采集 profile 一致) */
|
||
function isProductPage(): boolean {
|
||
return matchProfile(location.href) !== null;
|
||
}
|