feat: 插件改为浮窗模式
This commit is contained in:
@@ -4,8 +4,15 @@ import { generateSuite, getSuite, planSuite } from '../src/api/client';
|
||||
export default defineBackground(() => {
|
||||
console.log('[电商套图工作台] background started');
|
||||
|
||||
// 点击扩展图标 → 打开 Side Panel
|
||||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
|
||||
// 点击扩展图标 → 开关当前商品页的悬浮面板(页面无 content script 时忽略)
|
||||
chrome.action.onClicked.addListener(async (tab) => {
|
||||
if (tab.id == null) return;
|
||||
try {
|
||||
await chrome.tabs.sendMessage(tab.id, { action: 'toggle-suite-panel' });
|
||||
} catch {
|
||||
// 非四站点页面,未注入面板
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||
if (msg?.action === 'generateSuite') {
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// 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; }
|
||||
|
||||
.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);
|
||||
container.append(fab, panel);
|
||||
|
||||
const open = () => {
|
||||
if (!iframe.src) iframe.src = chrome.runtime.getURL('/sidepanel.html');
|
||||
panel.classList.add('open');
|
||||
fab.classList.add('hidden');
|
||||
// 聚焦进面板,键盘操作(Esc 关闭 / 预览翻页)直接可用
|
||||
iframe.focus();
|
||||
};
|
||||
const close = () => {
|
||||
panel.classList.remove('open');
|
||||
if (isProductPage()) fab.classList.remove('hidden');
|
||||
};
|
||||
|
||||
fab.addEventListener('click', open);
|
||||
|
||||
// 面板内 App(✕ / Esc)→ 收起
|
||||
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;
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App as AntApp, ConfigProvider, Popover, Progress, Select } from 'antd';
|
||||
import { SettingOutlined, DownloadOutlined, ThunderboltOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import { SettingOutlined, DownloadOutlined, ThunderboltOutlined, UploadOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import type { ScanResult, ImageMaterial } from '../../src/collector/scan';
|
||||
import {
|
||||
buildGeneratePayload, suiteZipUrl, uploadImage,
|
||||
@@ -40,6 +40,14 @@ const PLATFORM_LABELS: Record<string, string> = {
|
||||
/** 支持的站点(用于判断刷新是否有意义) */
|
||||
const SUPPORTED_URL_RE = /^https?:\/\/([a-z0-9-]+\.ozon\.(ru|kz|by)|detail\.1688\.com|item\.taobao\.com|detail\.tmall\.com)\//i;
|
||||
|
||||
/** 页内悬浮面板模式(作为 iframe 嵌在商品页里运行);独立 Side Panel 打开时为 false */
|
||||
const IN_PAGE = window.self !== window.top;
|
||||
|
||||
/** 通知宿主页收起悬浮面板 */
|
||||
function closePanel(): void {
|
||||
window.parent.postMessage({ type: 'sc-panel-close' }, '*');
|
||||
}
|
||||
|
||||
/** 在指定 tab 执行采集,返回结果或 null(未就绪/不支持) */
|
||||
async function tryScan(tabId: number): Promise<ScanResult | null> {
|
||||
const [res] = await chrome.scripting.executeScript({
|
||||
@@ -133,8 +141,8 @@ const App: React.FC = () => {
|
||||
// 出图方案
|
||||
const [platform, setPlatform] = useState<'ozon' | 'wb' | 'cn'>('ozon');
|
||||
const [styleSet, setStyleSet] = useState(1);
|
||||
/** 生图模型(通义 DashScope,默认 wan2.7-image-pro,与后端 .env 默认一致) */
|
||||
const [model, setModel] = useState<string>('wan2.7-image-pro');
|
||||
/** 生图模型(默认 gpt-image-2,走 RightAPI provider) */
|
||||
const [model, setModel] = useState<string>('gpt-image-2');
|
||||
/** 用户改写的风格提示词(按风格 id 存,切风格不丢) */
|
||||
const [stylePrompts, setStylePrompts] = useState<Record<number, string>>({});
|
||||
/** 生图要求(最高优先级,强制约束,覆盖其他设定) */
|
||||
@@ -179,6 +187,16 @@ const App: React.FC = () => {
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [preview]);
|
||||
|
||||
// 页内面板模式:Esc 收起面板(预览打开时优先关预览,由上面的监听处理)
|
||||
useEffect(() => {
|
||||
if (!IN_PAGE) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && !preview) closePanel();
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [preview]);
|
||||
|
||||
const brand = result?.texts.find(t => t.kind === 'brand')?.content ?? '';
|
||||
const sales = result?.texts.find(t => t.kind === 'sales')?.content ?? '';
|
||||
const shop = result?.texts.find(t => t.kind === 'shop')?.content ?? '';
|
||||
@@ -500,6 +518,9 @@ const App: React.FC = () => {
|
||||
<div className="page">
|
||||
{/* ── 顶栏 ── */}
|
||||
<div className="topbar">
|
||||
{IN_PAGE && (
|
||||
<button className="icon-btn" title="收起面板(Esc)" onClick={closePanel}><CloseOutlined /></button>
|
||||
)}
|
||||
<div className="logo">套</div>
|
||||
<div>
|
||||
<h1>电商套图工作台</h1>
|
||||
|
||||
Reference in New Issue
Block a user