296 lines
7.2 KiB
TypeScript
296 lines
7.2 KiB
TypeScript
// ==========================================
|
|
// 本地导出实现方案 (基于1688插件方式)
|
|
// ==========================================
|
|
|
|
/**
|
|
* 导出配置
|
|
*/
|
|
interface ExportConfig {
|
|
downloadType: '1' | '2'; // 1=平铺, 2=分组到子文件夹
|
|
includeJson: boolean; // 是否导出product.json
|
|
}
|
|
|
|
/**
|
|
* 导出素材到本地
|
|
* 在 background.ts 中实现
|
|
*/
|
|
async function exportToLocal(
|
|
folderName: string,
|
|
materials: {
|
|
texts: TextMaterial[];
|
|
images: ImageMaterial[];
|
|
},
|
|
config: ExportConfig
|
|
) {
|
|
const downloadTasks: Promise<void>[] = [];
|
|
|
|
// 1. 导出图片
|
|
for (const img of materials.images) {
|
|
const groupFolder = config.downloadType === '2' ? img.groupName : '';
|
|
|
|
// 文件名: 分组key-索引-规格名(可选).扩展名
|
|
const ext = img.url.split('.').pop()?.split('?')[0] || 'jpg';
|
|
let filename = `${img.groupKey}-${String(img.index).padStart(3, '0')}`;
|
|
if (img.variantName) {
|
|
filename += `-${img.variantName}`;
|
|
}
|
|
filename += `.${ext}`;
|
|
|
|
// 构建完整路径: 商品名/分组/文件名
|
|
const path = [folderName, groupFolder, filename]
|
|
.filter(Boolean)
|
|
.join('/');
|
|
|
|
downloadTasks.push(
|
|
chrome.downloads.download({
|
|
url: img.url,
|
|
filename: path,
|
|
conflictAction: 'uniquify',
|
|
saveAs: false
|
|
}).then(() => {
|
|
console.log(`Downloaded: ${path}`);
|
|
})
|
|
);
|
|
}
|
|
|
|
// 2. 导出product.json (Ozon API格式)
|
|
if (config.includeJson) {
|
|
const productData = buildOzonProductJson(materials);
|
|
const jsonBlob = new Blob(
|
|
[JSON.stringify(productData, null, 2)],
|
|
{ type: 'application/json' }
|
|
);
|
|
const jsonUrl = URL.createObjectURL(jsonBlob);
|
|
|
|
downloadTasks.push(
|
|
chrome.downloads.download({
|
|
url: jsonUrl,
|
|
filename: `${folderName}/product.json`,
|
|
conflictAction: 'overwrite',
|
|
saveAs: false
|
|
}).then(() => {
|
|
URL.revokeObjectURL(jsonUrl);
|
|
})
|
|
);
|
|
}
|
|
|
|
// 等待所有下载完成
|
|
await Promise.allSettled(downloadTasks);
|
|
|
|
return {
|
|
total: downloadTasks.length,
|
|
folder: folderName
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 构建Ozon API格式的JSON
|
|
* 参考: https://docs.ozon.ru/api/seller/zh/#operation/ProductAPI_ImportProductsV3
|
|
*/
|
|
function buildOzonProductJson(materials: {
|
|
texts: TextMaterial[];
|
|
images: ImageMaterial[];
|
|
}): OzonProductImport {
|
|
const title = materials.texts.find(t => t.kind === 'title')?.content || '';
|
|
const desc = materials.texts.find(t => t.kind === 'desc')?.content || '';
|
|
const params = materials.texts.find(t => t.kind === 'params');
|
|
|
|
// 图片URL按分组整理
|
|
const mainImages = materials.images
|
|
.filter(img => img.groupKey === 'main')
|
|
.map(img => img.url);
|
|
|
|
const skuImages = materials.images
|
|
.filter(img => img.groupKey === 'sku')
|
|
.reduce((acc, img) => {
|
|
if (img.variantName) {
|
|
acc[img.variantName] = img.url;
|
|
}
|
|
return acc;
|
|
}, {} as Record<string, string>);
|
|
|
|
return {
|
|
items: [{
|
|
// 基础信息
|
|
name: title,
|
|
description: desc,
|
|
offer_id: '', // 需要用户填写
|
|
|
|
// 图片
|
|
images: mainImages,
|
|
color_image: skuImages[Object.keys(skuImages)[0]] || '',
|
|
|
|
// 参数 (简化版,实际需要映射到Ozon类目属性)
|
|
attributes: params?.pairs?.map(p => ({
|
|
complex_id: 0,
|
|
id: 0, // 需要查询Ozon类目属性字典
|
|
values: [{
|
|
value: p.value
|
|
}]
|
|
})) || [],
|
|
|
|
// 尺寸重量 (需要从参数中提取或用户填写)
|
|
height: 0,
|
|
width: 0,
|
|
depth: 0,
|
|
dimension_unit: 'cm',
|
|
weight: 0,
|
|
weight_unit: 'g'
|
|
}]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 消息处理: 导出命令
|
|
*/
|
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
if (msg.name === 'export-to-local') {
|
|
exportToLocal(
|
|
msg.payload.folderName,
|
|
msg.payload.materials,
|
|
msg.payload.config
|
|
)
|
|
.then(result => sendResponse({ ok: true, data: result }))
|
|
.catch(error => sendResponse({ ok: false, error: error.message }));
|
|
|
|
return true; // 保持异步通道
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// Manifest配置
|
|
// ==========================================
|
|
/*
|
|
{
|
|
"optional_permissions": [
|
|
"downloads" // 放在optional中,首次导出时才申请
|
|
],
|
|
|
|
"host_permissions": [
|
|
"https://www.ozon.ru/*",
|
|
"https://cdn*.ozon.ru/*" // 图片CDN
|
|
]
|
|
}
|
|
*/
|
|
|
|
// ==========================================
|
|
// Side Panel UI - 导出操作
|
|
// ==========================================
|
|
/*
|
|
<div class="export-section">
|
|
<h3>导出选项</h3>
|
|
|
|
<label>
|
|
<input type="radio" name="exportType" value="2" checked>
|
|
分组到子文件夹 (推荐)
|
|
</label>
|
|
<label>
|
|
<input type="radio" name="exportType" value="1">
|
|
全部平铺
|
|
</label>
|
|
|
|
<label>
|
|
<input type="checkbox" name="includeJson" checked>
|
|
同时导出product.json (Ozon格式)
|
|
</label>
|
|
|
|
<button onclick="handleExport()">
|
|
导出到本地 (Downloads文件夹)
|
|
</button>
|
|
|
|
<p class="hint">
|
|
文件将保存到: ~/Downloads/[商品名]/
|
|
</p>
|
|
</div>
|
|
*/
|
|
|
|
async function handleExport() {
|
|
// 1. 首次使用时请求downloads权限
|
|
const hasPermission = await chrome.permissions.contains({
|
|
permissions: ['downloads']
|
|
});
|
|
|
|
if (!hasPermission) {
|
|
const granted = await chrome.permissions.request({
|
|
permissions: ['downloads']
|
|
});
|
|
if (!granted) {
|
|
alert('需要下载权限才能导出文件');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 2. 获取当前文件夹数据
|
|
const materials = await getCurrentFolderMaterials();
|
|
const folderName = cleanFilename(materials.title || '未命名商品');
|
|
|
|
// 3. 发送导出消息到background
|
|
const result = await chrome.runtime.sendMessage({
|
|
name: 'export-to-local',
|
|
payload: {
|
|
folderName,
|
|
materials,
|
|
config: {
|
|
downloadType: document.querySelector('input[name="exportType"]:checked').value,
|
|
includeJson: document.querySelector('input[name="includeJson"]').checked
|
|
}
|
|
}
|
|
});
|
|
|
|
if (result.ok) {
|
|
alert(`成功导出 ${result.data.total} 个文件到:\n~/Downloads/${result.data.folder}/`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 清理文件名中的非法字符
|
|
*/
|
|
function cleanFilename(name: string): string {
|
|
return name
|
|
.replace(/[<>:"/\\|?*]/g, '_') // Windows非法字符
|
|
.replace(/\s+/g, '_') // 空格替换为下划线
|
|
.substring(0, 100); // 限制长度
|
|
}
|
|
|
|
// ==========================================
|
|
// 类型定义
|
|
// ==========================================
|
|
|
|
interface TextMaterial {
|
|
kind: 'title' | 'params' | 'desc' | 'price';
|
|
content: string;
|
|
pairs?: Array<{ key: string; value: string }>;
|
|
}
|
|
|
|
interface ImageMaterial {
|
|
groupKey: 'main' | 'sku' | 'detail' | 'video';
|
|
groupName: string;
|
|
variantName?: string; // SKU规格名
|
|
url: string;
|
|
index: number;
|
|
}
|
|
|
|
interface OzonProductImport {
|
|
items: Array<{
|
|
name: string;
|
|
description: string;
|
|
offer_id: string;
|
|
images: string[];
|
|
color_image: string;
|
|
attributes: Array<{
|
|
complex_id: number;
|
|
id: number;
|
|
values: Array<{
|
|
dictionary_value_id?: number;
|
|
value?: string;
|
|
}>;
|
|
}>;
|
|
height: number;
|
|
width: number;
|
|
depth: number;
|
|
dimension_unit: string;
|
|
weight: number;
|
|
weight_unit: string;
|
|
}>;
|
|
}
|