feat: 采集插件整合进OSK
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { generateSuite, getSuite, planSuite } from '../src/api/client';
|
||||
import { reportProduct } from '../src/api/report';
|
||||
|
||||
// Background Service Worker —— 唯一出网口(生成 / 规划 / 轮询任务,绕 CORS)
|
||||
export default defineBackground(() => {
|
||||
console.log('[电商套图工作台] background started');
|
||||
|
||||
// 点击扩展图标 → 开关当前商品页的悬浮面板(页面无 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') {
|
||||
generateSuite(msg.baseUrl, msg.token, msg.payload)
|
||||
.then((data) => sendResponse({ ok: true, data }))
|
||||
.catch((err) => sendResponse({ ok: false, error: err instanceof Error ? err.message : String(err) }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg?.action === 'planSuite') {
|
||||
planSuite(msg.baseUrl, msg.token, msg.payload)
|
||||
.then((data) => sendResponse({ ok: true, data }))
|
||||
.catch((err) => sendResponse({ ok: false, error: err instanceof Error ? err.message : String(err) }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg?.action === 'getSuite') {
|
||||
getSuite(msg.baseUrl, msg.token, msg.suiteId)
|
||||
.then((data) => sendResponse({ ok: true, data }))
|
||||
.catch((err) => sendResponse({ ok: false, error: err instanceof Error ? err.message : String(err) }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// 商品上报:采集结果 POST 到 ozon-seller-kit 后台 /api/materials
|
||||
if (msg?.action === 'reportProduct') {
|
||||
reportProduct(msg.reportBaseUrl, msg.payload)
|
||||
.then((data) => sendResponse({ ok: true, data }))
|
||||
.catch((err) => sendResponse({ ok: false, error: err instanceof Error ? err.message : String(err) }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// 通用文本代理:采集引擎拉跨域资源(1688 详情 CDN / mtop API)
|
||||
if (msg?.action === 'fetchText') {
|
||||
const headers: Record<string, string> = {};
|
||||
if (msg.referer) headers['Referer'] = msg.referer;
|
||||
fetch(msg.url, {
|
||||
headers,
|
||||
credentials: msg.credentials ? 'include' : 'omit',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
sendResponse({ ok: true, text: await res.text() });
|
||||
})
|
||||
.catch((err) => sendResponse({ ok: false, error: err instanceof Error ? err.message : String(err) }));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
// MAIN world 桥 —— 跑在页面主世界,读取页面 JS 变量(isolated world 读不到)。
|
||||
// 协议(对齐竞品 inject.js 模式):
|
||||
// isolated → MAIN: {type:'sc-bridge-req', requestId, keys: [...]} keys 含 '*' 时返回诊断键列表
|
||||
// MAIN → isolated: {type:'sc-bridge-res', requestId, payload: {key: value}}
|
||||
// MAIN 侧零业务逻辑:只读白名单键、JSON 序列化过滤后回传,不注入任何页面行为。
|
||||
export default defineContentScript({
|
||||
matches: [
|
||||
'https://*.ozon.ru/*', 'https://*.ozon.kz/*', 'https://*.ozon.by/*',
|
||||
'https://detail.1688.com/*',
|
||||
'https://item.taobao.com/*', 'https://detail.tmall.com/*',
|
||||
],
|
||||
world: 'MAIN',
|
||||
main() {
|
||||
window.addEventListener('message', (ev: MessageEvent) => {
|
||||
if (ev.source !== window) return;
|
||||
const d = ev.data as { type?: string; requestId?: string; keys?: string[] } | null;
|
||||
if (!d || d.type !== 'sc-bridge-req' || !d.requestId || !Array.isArray(d.keys)) return;
|
||||
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (d.keys.includes('*')) {
|
||||
// 诊断模式:列出页面上可能有数据的全局键
|
||||
payload['__sc_window_keys__'] = Object.keys(window).filter(k =>
|
||||
/^(__|_)?[A-Za-z]/.test(k) && /(context|rawData|ICE|sku|item|g_config|DATA|state)/i.test(k)
|
||||
);
|
||||
}
|
||||
for (const k of d.keys) {
|
||||
if (k === '*') continue;
|
||||
try {
|
||||
const v = (window as unknown as Record<string, unknown>)[k];
|
||||
if (v !== undefined) payload[k] = JSON.parse(JSON.stringify(v)); // 过滤函数/循环引用
|
||||
} catch { /* 不可序列化的跳过 */ }
|
||||
}
|
||||
window.postMessage({ type: 'sc-bridge-res', requestId: d.requestId, payload }, '*');
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
// Content Script —— 注入四个平台的商品页,暴露采集入口
|
||||
import { scanCurrentPage } from '../../src/collector/scan';
|
||||
|
||||
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/*',
|
||||
],
|
||||
main() {
|
||||
console.log('[电商套图工作台] Content script loaded');
|
||||
|
||||
// 暴露采集入口到全局(供 side panel 调用 / console 调试)
|
||||
(window as any).__SuiteCollector = {
|
||||
scan: scanCurrentPage,
|
||||
};
|
||||
|
||||
console.log('[电商套图工作台] 就绪。Console 可测: await window.__SuiteCollector.scan()');
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
// 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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,334 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>电商套图工作台</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f5f5f5; /* 页面背景(中性灰) */
|
||||
--card: #ffffff;
|
||||
--card-soft: #fafafa;
|
||||
--border: #f0f0f0;
|
||||
--border-strong: #e0e0e0;
|
||||
--primary: #8b5cf6; /* 紫(ozon-seller-kit v2 主题色) */
|
||||
--primary-hover: #7c3aed;
|
||||
--primary-ring: rgba(139, 92, 246, 0.12);
|
||||
--primary-soft: #a78bfa; /* 主题色同色系偏淡(未勾选描边/✓) */
|
||||
--green: #52c41a;
|
||||
--red: #ff4d4f;
|
||||
--warn-bg: #fffbe6;
|
||||
--warn-border: #ffe58f;
|
||||
--warn-text: #8c6d1f;
|
||||
--text: #262626;
|
||||
--text-2: #8c8c8c;
|
||||
}
|
||||
html, body, #root {
|
||||
min-width: 860px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
/* ── 页面骨架 ── */
|
||||
.page { padding: 16px 18px 22px; }
|
||||
/* 采集区两列等高:左右卡片拉伸到同一高度 */
|
||||
.two-col { display: flex; gap: 14px; align-items: stretch; margin-bottom: 14px; }
|
||||
.two-col .section { flex: 1; min-width: 0; margin-bottom: 0; display: flex; flex-direction: column; }
|
||||
.two-col .section .section-head { flex-shrink: 0; }
|
||||
/* 图片列表占满 section 除标题外的剩余高度;min-height:0 是 flex 子项内滚动的关键 */
|
||||
.img-groups { flex: 1 1 auto; min-height: 0; overflow-y: auto; max-height: 78vh; }
|
||||
/* 采集图片区:section 自身去掉左右 padding,标题行自持 padding;
|
||||
图片区左侧对齐标题,右侧只留窄缝给滚动条(滚动条贴卡片内缘,图片与滚动条之间有小间距) */
|
||||
.section-images { padding: 14px 0 !important; }
|
||||
.section-images .section-head { padding: 0 16px; }
|
||||
.section-images .img-groups { padding: 2px 8px 0 16px; }
|
||||
.section-images .empty { margin: 0 16px; }
|
||||
.img-export-bar {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
margin: 10px 16px 2px; padding-top: 10px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.img-export-bar .hint { font-size: 12px; color: var(--text-2); }
|
||||
.divider { border-top: 1px solid var(--border); margin: 12px 0; }
|
||||
|
||||
/* ── 顶部 ── */
|
||||
.topbar {
|
||||
display: flex; align-items: center; gap: 11px;
|
||||
padding-bottom: 14px; margin-bottom: 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.logo {
|
||||
width: 38px; height: 38px; border-radius: 9px;
|
||||
background: var(--primary); color: #fff;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 19px; font-weight: 700;
|
||||
}
|
||||
.topbar h1 { font-size: 17px; margin: 0; font-weight: 700; }
|
||||
.topbar .sub { font-size: 12px; color: var(--text-2); margin-top: 1px; }
|
||||
.topbar .spacer { flex: 1; }
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
||||
padding: 8px 16px; border-radius: 8px; border: 1px solid var(--border-strong);
|
||||
font-size: 14px; cursor: pointer; user-select: none;
|
||||
background: #fff; color: var(--text);
|
||||
transition: all .15s;
|
||||
}
|
||||
.btn:hover { border-color: var(--primary); color: var(--primary); }
|
||||
.btn-primary {
|
||||
background: var(--primary); border-color: var(--primary); color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); color: #fff; }
|
||||
/* AI 智能规划:深靛紫渐变(智慧/深度感) */
|
||||
.btn-ai {
|
||||
background: linear-gradient(135deg, #4338ca 0%, #6d28d9 100%);
|
||||
border: none; color: #fff; font-weight: 600;
|
||||
box-shadow: 0 2px 10px rgba(88, 60, 210, 0.35);
|
||||
}
|
||||
.btn-ai:hover:not([disabled]) {
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
||||
color: #fff; box-shadow: 0 3px 14px rgba(88, 60, 210, 0.45);
|
||||
}
|
||||
/* 三个主操作按钮统一宽度 */
|
||||
.btn-main { width: 160px; }
|
||||
/* 「规划并生成」复选框 */
|
||||
.auto-chk {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
font-size: 12.5px; color: var(--text-2); cursor: pointer; user-select: none;
|
||||
}
|
||||
.auto-chk input { accent-color: var(--primary); width: 14px; height: 14px; cursor: pointer; }
|
||||
.auto-chk:hover { color: var(--text); }
|
||||
.btn[disabled] { opacity: .5; cursor: not-allowed; }
|
||||
.btn-sm { padding: 5px 11px; font-size: 12.5px; }
|
||||
.icon-btn {
|
||||
width: 34px; height: 34px; border-radius: 8px; border: 1px solid var(--border-strong);
|
||||
background: #fff; cursor: pointer; color: var(--text-2);
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.icon-btn:hover { color: var(--primary); border-color: var(--primary); }
|
||||
|
||||
/* ── 编号步骤卡片 ── */
|
||||
.section {
|
||||
background: var(--card); border: 1px solid var(--border);
|
||||
border-radius: 8px; padding: 14px 16px; margin-bottom: 14px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
.section-head { display: flex; align-items: baseline; gap: 9px; margin-bottom: 12px; }
|
||||
.section-no {
|
||||
font-size: 20px; font-weight: 800; color: var(--primary);
|
||||
font-variant-numeric: tabular-nums; line-height: 1;
|
||||
}
|
||||
.section-title { font-size: 15px; font-weight: 700; }
|
||||
.section-extra { margin-left: auto; font-size: 12px; color: var(--text-2); }
|
||||
|
||||
/* ── 字段 ── */
|
||||
.field { margin-bottom: 10px; }
|
||||
.field label { display: block; font-size: 12.5px; color: var(--text-2); margin-bottom: 4px; }
|
||||
.field input, .field textarea {
|
||||
width: 100%; padding: 8px 11px; border: 1px solid var(--border-strong);
|
||||
border-radius: 6px; font-size: 14px; font-family: inherit; line-height: 1.5;
|
||||
background: var(--card-soft); color: var(--text); outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
.field input:focus, .field textarea:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px var(--primary-ring);
|
||||
background: #fff;
|
||||
}
|
||||
.kv-table {
|
||||
width: 100%; border-collapse: collapse; font-size: 13px;
|
||||
background: var(--card-soft); border-radius: 6px; overflow: hidden;
|
||||
}
|
||||
.kv-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
||||
.kv-table tr:last-child td { border-bottom: none; }
|
||||
.kv-table td.k { color: var(--text-2); white-space: nowrap; width: 1%; padding-right: 16px; }
|
||||
|
||||
/* ── 药丸选择 ── */
|
||||
.pills { display: flex; flex-wrap: wrap; gap: 7px; }
|
||||
.pill {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
padding: 5px 13px; border-radius: 999px; border: 1px solid var(--border-strong);
|
||||
background: #fff; font-size: 13px; cursor: pointer; color: var(--text-2);
|
||||
user-select: none; transition: all .15s; line-height: 1.4;
|
||||
}
|
||||
.pill:hover { border-color: var(--primary); color: var(--primary); }
|
||||
.pill.on {
|
||||
background: var(--primary); border-color: var(--primary); color: #fff; font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── 图片网格 ── */
|
||||
.group-head { display: flex; align-items: center; gap: 8px; margin: 4px 0 9px; }
|
||||
.group-head .name { font-size: 13px; font-weight: 600; color: var(--text-2); }
|
||||
.group-head .count {
|
||||
font-size: 12px; color: var(--text-2); background: var(--card-soft);
|
||||
border: 1px solid var(--border); border-radius: 999px; padding: 0 8px;
|
||||
}
|
||||
.group-head .mini-check { margin-left: auto; font-size: 12px; color: var(--primary); cursor: pointer; user-select: none; }
|
||||
.group-head .mini-check:hover { text-decoration: underline; }
|
||||
.img-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; }
|
||||
.img-cell {
|
||||
position: relative; aspect-ratio: 1; border-radius: 6px; overflow: hidden;
|
||||
border: 2px solid transparent; cursor: zoom-in; background: var(--card-soft);
|
||||
}
|
||||
.img-cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.img-cell.on { border-color: var(--primary); }
|
||||
.img-cell .tick {
|
||||
position: absolute; top: 5px; left: 5px; width: 18px; height: 18px;
|
||||
border-radius: 50%; border: 1.5px solid var(--primary-soft);
|
||||
background: rgba(255, 255, 255, 0.55); display: flex; align-items: center; justify-content: center;
|
||||
color: var(--primary-soft); font-size: 11px; transition: all .15s; cursor: pointer;
|
||||
}
|
||||
.img-cell.on .tick { background: var(--primary); border-color: var(--primary); color: #fff; }
|
||||
.img-cell .variant {
|
||||
position: absolute; bottom: 0; left: 0; right: 0;
|
||||
background: rgba(0, 0, 0, 0.55); color: #fff; font-size: 11px;
|
||||
padding: 1px 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ── 生成结果(整行,6 列)── */
|
||||
.result-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
|
||||
.result-cell { position: relative; border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }
|
||||
.result-cell img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; }
|
||||
.result-cell .cap {
|
||||
font-size: 11.5px; text-align: center; padding: 3px 0;
|
||||
background: var(--card-soft); color: var(--text-2);
|
||||
border-top: 1px solid var(--border); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.result-cell.fail { opacity: .55; }
|
||||
.result-cell .fail-tag {
|
||||
position: absolute; top: 5px; right: 5px; font-size: 11px;
|
||||
background: var(--red); color: #fff; border-radius: 4px; padding: 0 5px;
|
||||
}
|
||||
|
||||
/* ── 目标平台切换条 ── */
|
||||
.platform-bar {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
background: var(--card); border: 1px solid var(--border);
|
||||
border-radius: 8px; padding: 10px 16px; margin-bottom: 14px;
|
||||
}
|
||||
.platform-label { font-size: 13px; font-weight: 700; }
|
||||
.platform-spec { margin-left: auto; font-size: 12.5px; color: var(--text-2); }
|
||||
|
||||
/* 平台切换:Button.Group 形式,选中态用低饱和灰绿(不抢主题色) */
|
||||
.seg-group { display: inline-flex; }
|
||||
.seg-btn {
|
||||
padding: 7px 20px; font-size: 13.5px; font-family: inherit;
|
||||
min-width: 120px; text-align: center; /* 选中加粗会让文字变宽,固定宽度消除跳动 */
|
||||
background: #fff; border: 1px solid var(--border-strong); border-left-width: 0;
|
||||
color: var(--text-2); cursor: pointer; user-select: none; transition: all .15s;
|
||||
}
|
||||
.seg-group .seg-btn:first-child { border-left-width: 1px; border-radius: 8px 0 0 8px; }
|
||||
.seg-group .seg-btn:last-child { border-radius: 0 8px 8px 0; }
|
||||
.seg-btn:hover { color: var(--text); background: var(--card-soft); }
|
||||
.seg-btn.on {
|
||||
background: #eef0eb; border-color: #c9cec6; color: #3f453c; font-weight: 700;
|
||||
}
|
||||
.seg-group .seg-btn.on + .seg-btn { border-left-color: #c9cec6; }
|
||||
/* 三个平台药丸等宽:选中态加粗会让文字变宽,用固定 min-width 消除抖动 */
|
||||
.platform-bar .pill { min-width: 108px; text-align: center; }
|
||||
|
||||
/* ── 图片放大预览(画廊)── */
|
||||
.lightbox {
|
||||
position: fixed; inset: 0; z-index: 1000;
|
||||
background: rgba(0, 0, 0, 0.82);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-direction: column; gap: 12px; cursor: zoom-out;
|
||||
}
|
||||
.lightbox img {
|
||||
max-width: 88%; max-height: 82%;
|
||||
border-radius: 8px; box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.lightbox-nav {
|
||||
position: absolute; top: 50%; transform: translateY(-50%);
|
||||
width: 40px; height: 64px; border: none; border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.12); color: #fff;
|
||||
font-size: 30px; line-height: 1; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: background .15s; user-select: none;
|
||||
}
|
||||
.lightbox-nav:hover { background: rgba(255, 255, 255, 0.28); }
|
||||
.lightbox-nav.prev { left: 14px; }
|
||||
.lightbox-nav.next { right: 14px; }
|
||||
.lightbox-counter {
|
||||
position: absolute; top: 14px; right: 16px;
|
||||
background: rgba(0, 0, 0, 0.5); color: #fff;
|
||||
font-size: 13px; padding: 3px 10px; border-radius: 999px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.lightbox-tip { color: rgba(255, 255, 255, 0.75); font-size: 12.5px; }
|
||||
|
||||
/* ── 出图方案 ── */
|
||||
.plan-list { display: flex; flex-direction: column; gap: 4px; }
|
||||
.plan-row {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 7px 10px; border: 1px solid var(--border); border-radius: 6px;
|
||||
background: var(--card-soft); cursor: pointer;
|
||||
}
|
||||
.plan-row:hover { border-color: var(--primary); }
|
||||
.plan-row.off { opacity: .45; }
|
||||
.plan-all-toggle {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
margin: 8px 2px 2px; padding-top: 6px;
|
||||
border-top: 1px dashed var(--border);
|
||||
}
|
||||
.plan-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; }
|
||||
.plan-title { font-size: 13.5px; font-weight: 600; white-space: nowrap; }
|
||||
.variant-chip {
|
||||
flex-shrink: 0; font-size: 11.5px; padding: 0 8px; line-height: 1.8;
|
||||
border-radius: 999px; background: #f3efff; border: 1px solid #ddd3fa; color: #6d28d9;
|
||||
}
|
||||
.plan-detail {
|
||||
font-size: 12px; color: var(--text-2);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
/* 构图提示(prompt_hint):生图要求的落地处,用主题蓝区分 */
|
||||
.plan-detail.plan-hint { color: #4f6bed; }
|
||||
.stepper { display: inline-flex; align-items: center; gap: 0; flex-shrink: 0; }
|
||||
.step-btn {
|
||||
width: 24px; height: 24px; border: 1px solid var(--border-strong); background: #fff;
|
||||
border-radius: 5px; cursor: pointer; font-size: 14px; line-height: 1; color: var(--text);
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.step-btn:hover:not([disabled]) { border-color: var(--primary); color: var(--primary); }
|
||||
.step-btn[disabled] { opacity: .35; cursor: not-allowed; }
|
||||
.step-num {
|
||||
min-width: 28px; text-align: center; font-size: 13.5px; font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.ai-tag {
|
||||
background: var(--primary); color: #fff; font-size: 11px;
|
||||
border-radius: 4px; padding: 1px 6px; margin-right: 4px;
|
||||
}
|
||||
|
||||
.hint { font-size: 12.5px; color: var(--text-2); line-height: 1.6; }
|
||||
|
||||
/* ── 模型下拉选项 ── */
|
||||
.model-opt-name { font-size: 13.5px; font-weight: 600; color: var(--text); }
|
||||
.model-opt-desc { font-size: 12px; color: var(--text-2); margin-top: 2px; }
|
||||
.ok-chip {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
background: #f6ffed; border: 1px solid #b7eb8f; color: #389e0d;
|
||||
border-radius: 6px; padding: 4px 9px; font-size: 12.5px;
|
||||
}
|
||||
.warn-box {
|
||||
background: var(--warn-bg); border: 1px solid var(--warn-border); color: var(--warn-text);
|
||||
border-radius: 6px; padding: 7px 10px; font-size: 12.5px; margin-top: 6px; line-height: 1.6;
|
||||
}
|
||||
.empty {
|
||||
text-align: center; color: var(--text-2); font-size: 13px;
|
||||
padding: 22px 0; background: var(--card-soft); border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./App.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user