feat: 插件改为浮窗模式

This commit is contained in:
R524809
2026-08-19 17:05:23 +08:00
parent 06b220ae8d
commit 82cb694837
11 changed files with 381 additions and 22 deletions
+9 -2
View File
@@ -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') {
+145
View File
@@ -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;
}
+24 -3
View File
@@ -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>
+10
View File
@@ -114,6 +114,16 @@ export const IMAGE_MODEL_OPTIONS = [
label: 'wan2.7-image-pro',
desc: '异步精修,质感与细节更强,适合高质量电商大片',
},
{
value: 'wan2.6-image',
label: 'wan2.6-image',
desc: '通义 2.6 图生图,支持参考图与多图融合,速度更快、稳定性好',
},
{
value: 'wan2.6-t2i',
label: 'wan2.6-t2i',
desc: '通义 2.6 纯文生图,不使用参考图(商品外观靠文案描述),速度最快',
},
{
value: 'gpt-image-2',
label: 'gpt-image-2',
+13 -1
View File
@@ -32,7 +32,19 @@ export default defineConfig({
],
action: {
default_title: '电商套图工作台'
}
},
// 页内悬浮面板用 iframe 加载 sidepanel.html,必须声明为 web accessible
web_accessible_resources: [{
resources: ['sidepanel.html'],
matches: [
'https://*.ozon.ru/*',
'https://*.ozon.kz/*',
'https://*.ozon.by/*',
'https://detail.1688.com/*',
'https://item.taobao.com/*',
'https://detail.tmall.com/*',
],
}]
},
modules: ['react']
});