Compare commits

...

4 Commits

Author SHA1 Message Date
Julian Freeman
6f023006dc support export and import 2026-03-11 14:11:37 -04:00
Julian Freeman
c11ef4a567 fix ui 2026-03-11 14:05:48 -04:00
Julian Freeman
22c511daf8 support edit 2026-03-11 14:01:47 -04:00
Julian Freeman
527046339a optimize 2026-03-11 13:51:10 -04:00
4 changed files with 182 additions and 80 deletions

View File

@@ -24,41 +24,51 @@ function doPost(e) {
sheet = ss.getActiveSheet(); sheet = ss.getActiveSheet();
} }
var headers = []; var existingHeaders = [];
var lastCol = sheet.getLastColumn();
// 2. Determine headers var lastRow = sheet.getLastRow();
if (sheet.getLastRow() === 0) {
// Use provided headers if available // 2. Get incoming headers (from data.headers or keys)
if (data.headers && Array.isArray(data.headers)) { var incomingHeaders = data.headers || Object.keys(data).filter(function(k) {
headers = data.headers; return !["sheetName", "headers"].includes(k);
} else { });
// Fallback: Use keys but exclude internal control keys
headers = Object.keys(data).filter(function(key) { if (lastRow === 0) {
return !["sheetName", "headers"].includes(key); // New sheet: Write all incoming headers as the first row
}); existingHeaders = incomingHeaders;
} sheet.appendRow(existingHeaders);
sheet.appendRow(headers);
} else { } else {
// Get existing headers from the first row // Existing sheet: Read current headers from row 1
var lastCol = sheet.getLastColumn(); existingHeaders = sheet.getRange(1, 1, 1, lastCol).getValues()[0];
if (lastCol > 0) {
headers = sheet.getRange(1, 1, 1, lastCol).getValues()[0]; // Find headers that are in 'incoming' but not in 'existing'
var missingHeaders = incomingHeaders.filter(function(h) {
return existingHeaders.indexOf(h) === -1;
});
if (missingHeaders.length > 0) {
// Append missing headers to the end of row 1
sheet.getRange(1, lastCol + 1, 1, missingHeaders.length).setValues([missingHeaders]);
// Update local existingHeaders list to include new ones
existingHeaders = existingHeaders.concat(missingHeaders);
} }
} }
// 3. Map data to headers // 3. Map data to the (potentially updated) existingHeaders
var row = headers.map(function(header) { var row = existingHeaders.map(function(header) {
var val = data[header]; var val = data[header];
if (val === undefined || val === null) return "N/A"; if (val === undefined || val === null) return "N/A";
// If value is an object or array, stringify it to avoid [Ljava.lang.Object]
if (typeof val === 'object') return JSON.stringify(val); if (typeof val === 'object') return JSON.stringify(val);
return val; return val;
}); });
sheet.appendRow(row); sheet.appendRow(row);
return ContentService.createTextOutput(JSON.stringify({ status: "success", sheet: sheet.getName() })) return ContentService.createTextOutput(JSON.stringify({
.setMimeType(ContentService.MimeType.JSON); status: "success",
sheet: sheet.getName(),
addedColumns: missingHeaders ? missingHeaders.length : 0
})).setMimeType(ContentService.MimeType.JSON);
} catch (error) { } catch (error) {
return ContentService.createTextOutput(JSON.stringify({ status: "error", message: error.toString() })) return ContentService.createTextOutput(JSON.stringify({ status: "error", message: error.toString() }))

View File

@@ -1,8 +1,8 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "AI 硬件采购助手", "name": "AI 硬件抓取助手",
"version": "1.0", "version": "1.0",
"description": "Extract structured product data using Gemini AI.", "description": "使用 Gemini AI 从网站抓取产品数据。",
"permissions": [ "permissions": [
"sidePanel", "sidePanel",
"scripting", "scripting",
@@ -20,7 +20,7 @@
"default_path": "sidepanel.html" "default_path": "sidepanel.html"
}, },
"action": { "action": {
"default_title": "AI 硬件采购助手", "default_title": "AI 硬件抓取助手",
"default_icon": { "default_icon": {
"16": "icons/icon16.png", "16": "icons/icon16.png",
"32": "icons/icon32.png", "32": "icons/icon32.png",

View File

@@ -3,16 +3,15 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 硬件采购助手</title> <title>AI 硬件抓取助手</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
</head> </head>
<body class="bg-gray-50 p-4"> <body class="bg-gray-50 p-4">
<div class="max-w-md mx-auto"> <div class="max-w-md mx-auto">
<!-- Header --> <!-- Header -->
<header class="mb-6"> <!-- <header class="mb-6">
<h1 class="text-xl font-bold text-indigo-700">AI 硬件采购助手</h1> <h1 class="text-xl font-bold text-indigo-700">AI 硬件抓取助手</h1>
<p class="text-gray-500 text-sm">从任何网站提取产品数据。</p> </header> -->
</header>
<!-- API Config --> <!-- API Config -->
<section class="bg-white rounded-lg shadow-sm mb-4 border border-gray-200"> <section class="bg-white rounded-lg shadow-sm mb-4 border border-gray-200">
@@ -36,6 +35,7 @@
<input type="text" id="modelInput" class="w-full mt-0.5 border border-gray-300 rounded px-2 py-1 text-sm focus:ring-1 focus:ring-indigo-500 outline-none" placeholder="例如gemini-1.5-flash"> <input type="text" id="modelInput" class="w-full mt-0.5 border border-gray-300 rounded px-2 py-1 text-sm focus:ring-1 focus:ring-indigo-500 outline-none" placeholder="例如gemini-1.5-flash">
</div> </div>
</div> </div>
<button id="saveConfig" class="w-full mt-2 mb-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm py-2 rounded transition-colors font-medium">保存设置</button>
<!-- Spacious Preset Management --> <!-- Spacious Preset Management -->
<div class="pt-4 border-t border-gray-100 space-y-3"> <div class="pt-4 border-t border-gray-100 space-y-3">
@@ -44,14 +44,23 @@
<input type="text" id="newPresetName" class="w-full border border-gray-300 rounded px-2 py-1.5 text-sm outline-none" placeholder="预设名称 (如:提取笔记本)"> <input type="text" id="newPresetName" class="w-full border border-gray-300 rounded px-2 py-1.5 text-sm outline-none" placeholder="预设名称 (如:提取笔记本)">
<input type="text" id="newPresetSheet" class="w-full border border-gray-300 rounded px-2 py-1.5 text-sm outline-none" placeholder="目标工作表名称 (如:笔记本清单)"> <input type="text" id="newPresetSheet" class="w-full border border-gray-300 rounded px-2 py-1.5 text-sm outline-none" placeholder="目标工作表名称 (如:笔记本清单)">
<textarea id="newPresetFields" class="w-full border border-gray-300 rounded px-2 py-1.5 text-sm outline-none h-16" placeholder="提取字段 (用逗号分隔,如:品牌, 型号, 价格)"></textarea> <textarea id="newPresetFields" class="w-full border border-gray-300 rounded px-2 py-1.5 text-sm outline-none h-16" placeholder="提取字段 (用逗号分隔,如:品牌, 型号, 价格)"></textarea>
<button id="addPreset" class="w-full bg-indigo-50 text-indigo-700 text-xs py-2 rounded hover:bg-indigo-100 transition-colors font-semibold border border-indigo-200">添加新预设</button> <div class="flex gap-2">
<button id="addPreset" class="flex-1 bg-indigo-50 text-indigo-700 text-xs py-2 rounded hover:bg-indigo-100 transition-colors font-semibold border border-indigo-200">添加新预设</button>
<button id="cancelEdit" class="hidden px-3 bg-gray-100 text-gray-600 text-xs py-2 rounded hover:bg-gray-200 transition-colors font-semibold border border-gray-200">取消</button>
</div>
</div> </div>
<div id="presetList" class="mt-4 space-y-2 max-h-40 overflow-y-auto pr-1"> <div id="presetList" class="mt-4 space-y-2 max-h-40 overflow-y-auto pr-1">
<!-- Saved presets --> <!-- Saved presets -->
</div> </div>
<!-- Import/Export Presets -->
<div class="flex gap-2 pt-2 border-t border-gray-100">
<button id="exportPresets" class="flex-1 bg-white text-gray-600 text-[10px] py-1 rounded border border-gray-300 hover:bg-gray-50 transition-colors font-medium">导出预设</button>
<button id="importPresets" class="flex-1 bg-white text-gray-600 text-[10px] py-1 rounded border border-gray-300 hover:bg-gray-50 transition-colors font-medium">导入预设</button>
<input type="file" id="importInput" class="hidden" accept=".json">
</div>
</div> </div>
<button id="saveConfig" class="w-full mt-4 bg-indigo-600 hover:bg-indigo-700 text-white text-sm py-2 rounded transition-colors font-medium">保存所有设置</button>
</div> </div>
</section> </section>
@@ -66,15 +75,6 @@
</div> </div>
</section> </section>
<!-- Chat / Custom Instructions -->
<section class="bg-white p-4 rounded-lg shadow-sm border border-gray-200 mb-4">
<h2 class="text-sm font-semibold mb-2">自定义指令</h2>
<div class="flex gap-2">
<input type="text" id="customInput" class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm focus:ring-1 focus:ring-indigo-500 outline-none" placeholder="例如:查找电池容量...">
<button id="sendCustom" class="bg-indigo-100 text-indigo-700 px-3 py-1 rounded text-sm font-medium hover:bg-indigo-200 transition-colors">执行</button>
</div>
</section>
<!-- Logs / Status --> <!-- Logs / Status -->
<section class="bg-gray-100 rounded-lg p-3 text-xs"> <section class="bg-gray-100 rounded-lg p-3 text-xs">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">

View File

@@ -3,8 +3,6 @@ const apiKeyInput = document.getElementById('apiKey');
const scriptUrlInput = document.getElementById('scriptUrl'); const scriptUrlInput = document.getElementById('scriptUrl');
const modelInput = document.getElementById('modelInput'); const modelInput = document.getElementById('modelInput');
const saveConfigBtn = document.getElementById('saveConfig'); const saveConfigBtn = document.getElementById('saveConfig');
const customInput = document.getElementById('customInput');
const sendCustomBtn = document.getElementById('sendCustom');
const statusBadge = document.getElementById('statusBadge'); const statusBadge = document.getElementById('statusBadge');
const resultsArea = document.getElementById('results'); const resultsArea = document.getElementById('results');
@@ -13,10 +11,16 @@ const newPresetName = document.getElementById('newPresetName');
const newPresetSheet = document.getElementById('newPresetSheet'); const newPresetSheet = document.getElementById('newPresetSheet');
const newPresetFields = document.getElementById('newPresetFields'); const newPresetFields = document.getElementById('newPresetFields');
const addPresetBtn = document.getElementById('addPreset'); const addPresetBtn = document.getElementById('addPreset');
const cancelEditBtn = document.getElementById('cancelEdit');
const presetList = document.getElementById('presetList'); const presetList = document.getElementById('presetList');
const presetSelect = document.getElementById('presetSelect'); const presetSelect = document.getElementById('presetSelect');
const runPresetBtn = document.getElementById('runPreset'); const runPresetBtn = document.getElementById('runPreset');
// Import/Export Elements
const exportBtn = document.getElementById('exportPresets');
const importBtn = document.getElementById('importPresets');
const importInput = document.getElementById('importInput');
// Collapsible logic // Collapsible logic
const toggleConfig = document.getElementById('toggleConfig'); const toggleConfig = document.getElementById('toggleConfig');
const configContent = document.getElementById('configContent'); const configContent = document.getElementById('configContent');
@@ -28,6 +32,7 @@ const DEFAULT_PRESETS = [
]; ];
let currentPresets = []; let currentPresets = [];
let editingPresetId = null;
toggleConfig.addEventListener('click', () => { toggleConfig.addEventListener('click', () => {
const isHidden = configContent.classList.toggle('hidden'); const isHidden = configContent.classList.toggle('hidden');
@@ -51,7 +56,7 @@ chrome.storage.local.get(['geminiApiKey', 'googleScriptUrl', 'geminiModel', 'use
renderPresets(currentPresets); renderPresets(currentPresets);
}); });
// Save settings // Save global settings
saveConfigBtn.addEventListener('click', () => { saveConfigBtn.addEventListener('click', () => {
const apiKey = apiKeyInput.value.trim(); const apiKey = apiKeyInput.value.trim();
const scriptUrl = scriptUrlInput.value.trim(); const scriptUrl = scriptUrlInput.value.trim();
@@ -70,21 +75,121 @@ saveConfigBtn.addEventListener('click', () => {
addPresetBtn.addEventListener('click', async () => { addPresetBtn.addEventListener('click', async () => {
const name = newPresetName.value.trim(); const name = newPresetName.value.trim();
const sheetName = newPresetSheet.value.trim(); const sheetName = newPresetSheet.value.trim();
const fields = newPresetFields.value.trim(); const rawFields = newPresetFields.value.trim();
if (!name || !fields) return alert('请输入名称和字段');
if (!name || !rawFields) return alert('请输入名称和字段');
const normalizedFields = rawFields
.split(/[,]/)
.map(f => f.trim())
.filter(f => f !== "")
.join(', ');
const { userPresets = DEFAULT_PRESETS } = await chrome.storage.local.get('userPresets'); const { userPresets = DEFAULT_PRESETS } = await chrome.storage.local.get('userPresets');
const newPreset = { id: Date.now().toString(), name, sheetName, fields };
currentPresets = [...userPresets, newPreset]; if (editingPresetId) {
// Update existing
currentPresets = userPresets.map(p =>
p.id === editingPresetId ? { ...p, name, sheetName, fields: normalizedFields } : p
);
stopEditing();
} else {
// Add new
const newPreset = { id: Date.now().toString(), name, sheetName, fields: normalizedFields };
currentPresets = [...userPresets, newPreset];
}
await chrome.storage.local.set({ userPresets: currentPresets }); await chrome.storage.local.set({ userPresets: currentPresets });
newPresetName.value = ''; clearPresetInputs();
newPresetSheet.value = '';
newPresetFields.value = '';
renderPresets(currentPresets); renderPresets(currentPresets);
}); });
cancelEditBtn.addEventListener('click', stopEditing);
// Export/Import Logic
exportBtn.addEventListener('click', async () => {
const { userPresets = DEFAULT_PRESETS } = await chrome.storage.local.get('userPresets');
const blob = new Blob([JSON.stringify(userPresets, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `hardware-presets-${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
});
importBtn.addEventListener('click', () => importInput.click());
importInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (event) => {
try {
const importedPresets = JSON.parse(event.target.result);
if (!Array.isArray(importedPresets)) throw new Error('无效的预设文件格式');
const { userPresets = DEFAULT_PRESETS } = await chrome.storage.local.get('userPresets');
const choice = confirm('导入成功!\n点击 [确定] 将导入的预设合并到当前列表。\n点击 [取消] 将替换整个预设列表。');
let finalPresets;
if (choice) {
// Merge: Append with new IDs to prevent conflicts
const newItems = importedPresets.map(p => ({
...p,
id: Date.now().toString() + Math.random().toString(36).substr(2, 5)
}));
finalPresets = [...userPresets, ...newItems];
} else {
finalPresets = importedPresets;
}
await chrome.storage.local.set({ userPresets: finalPresets });
currentPresets = finalPresets;
renderPresets(currentPresets);
alert('预设导入完成!');
} catch (err) {
alert('导入失败: ' + err.message);
}
importInput.value = ''; // Reset input
};
reader.readAsText(file);
});
function startEditing(preset) {
editingPresetId = preset.id;
newPresetName.value = preset.name;
newPresetSheet.value = preset.sheetName || '';
newPresetFields.value = preset.fields;
addPresetBtn.textContent = '更新预设';
addPresetBtn.classList.replace('bg-indigo-50', 'bg-indigo-600');
addPresetBtn.classList.replace('text-indigo-700', 'text-white');
cancelEditBtn.classList.remove('hidden');
configContent.classList.remove('hidden');
configChevron.classList.add('rotate-180');
}
function stopEditing() {
editingPresetId = null;
clearPresetInputs();
addPresetBtn.textContent = '添加新预设';
addPresetBtn.classList.replace('bg-indigo-600', 'bg-indigo-50');
addPresetBtn.classList.replace('text-white', 'text-indigo-700');
cancelEditBtn.classList.add('hidden');
}
function clearPresetInputs() {
newPresetName.value = '';
newPresetSheet.value = '';
newPresetFields.value = '';
}
async function deletePreset(id) { async function deletePreset(id) {
if (editingPresetId === id) stopEditing();
const { userPresets = DEFAULT_PRESETS } = await chrome.storage.local.get('userPresets'); const { userPresets = DEFAULT_PRESETS } = await chrome.storage.local.get('userPresets');
currentPresets = userPresets.filter(p => p.id !== id); currentPresets = userPresets.filter(p => p.id !== id);
await chrome.storage.local.set({ userPresets: currentPresets }); await chrome.storage.local.set({ userPresets: currentPresets });
@@ -92,33 +197,32 @@ async function deletePreset(id) {
} }
function renderPresets(presets) { function renderPresets(presets) {
// Clear containers
presetList.innerHTML = ''; presetList.innerHTML = '';
presetSelect.innerHTML = ''; presetSelect.innerHTML = '';
presets.forEach(preset => { presets.forEach(preset => {
// 1. Render in Settings List // 1. Settings List Item
const item = document.createElement('div'); const item = document.createElement('div');
item.className = 'flex items-center justify-between bg-white p-3 mb-2 rounded-lg border border-gray-200 shadow-sm transition-all hover:border-indigo-200'; item.className = 'flex items-center justify-between bg-white p-3 mb-2 rounded-lg border border-gray-200 shadow-sm transition-all hover:border-indigo-200';
item.innerHTML = ` item.innerHTML = `
<div class="flex-1 overflow-hidden mr-3"> <div class="flex-1 overflow-hidden mr-2">
<div class="text-xs font-bold text-gray-800 truncate">${preset.name} ${preset.sheetName ? `<span class="text-indigo-500 ml-1">#${preset.sheetName}</span>` : ''}</div> <div class="text-xs font-bold text-gray-800 truncate">${preset.name} ${preset.sheetName ? `<span class="text-indigo-500 ml-1">#${preset.sheetName}</span>` : ''}</div>
<div class="text-[11px] text-gray-400 truncate mt-1 leading-tight">${preset.fields}</div> <div class="text-[11px] text-gray-400 truncate mt-1 leading-tight">${preset.fields}</div>
</div> </div>
<button class="flex items-center justify-center w-7 h-7 rounded-full text-gray-300 hover:text-red-500 hover:bg-red-50 transition-all focus:outline-none border-none bg-transparent cursor-pointer" title="删除预设"> <div class="flex gap-1">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <button class="edit-btn flex items-center justify-center w-7 h-7 rounded-full text-gray-300 hover:text-indigo-600 hover:bg-indigo-50 transition-all cursor-pointer border-none bg-transparent" title="编辑预设">
<line x1="18" y1="6" x2="6" y2="18"></line> <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
<line x1="6" y1="6" x2="18" y2="18"></line> </button>
</svg> <button class="delete-btn flex items-center justify-center w-7 h-7 rounded-full text-gray-300 hover:text-red-500 hover:bg-red-50 transition-all cursor-pointer border-none bg-transparent" title="删除预设">
</button> <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
`; `;
item.querySelector('button').onclick = (e) => { item.querySelector('.edit-btn').onclick = () => startEditing(preset);
e.stopPropagation(); item.querySelector('.delete-btn').onclick = () => deletePreset(preset.id);
deletePreset(preset.id);
};
presetList.appendChild(item); presetList.appendChild(item);
// 2. Render as Dropdown Option // 2. Dropdown Option
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = preset.id; opt.value = preset.id;
opt.textContent = preset.name; opt.textContent = preset.name;
@@ -126,15 +230,13 @@ function renderPresets(presets) {
}); });
} }
// Action Handlers // Extraction Handler
runPresetBtn.addEventListener('click', () => { runPresetBtn.addEventListener('click', () => {
const selectedId = presetSelect.value; const selectedId = presetSelect.value;
const preset = currentPresets.find(p => p.id === selectedId); const preset = currentPresets.find(p => p.id === selectedId);
if (preset) handleExtraction('preset', preset); if (preset) handleExtraction('preset', preset);
}); });
sendCustomBtn.addEventListener('click', () => handleExtraction('custom', { fields: customInput.value, sheetName: '' }));
async function handleExtraction(type, presetObj) { async function handleExtraction(type, presetObj) {
const { geminiApiKey, googleScriptUrl, geminiModel } = await chrome.storage.local.get(['geminiApiKey', 'googleScriptUrl', 'geminiModel']); const { geminiApiKey, googleScriptUrl, geminiModel } = await chrome.storage.local.get(['geminiApiKey', 'googleScriptUrl', 'geminiModel']);
@@ -145,7 +247,7 @@ async function handleExtraction(type, presetObj) {
const selectedModel = geminiModel || 'gemini-1.5-flash'; const selectedModel = geminiModel || 'gemini-1.5-flash';
updateStatus('提取中...', 'bg-blue-500 text-white'); updateStatus('提取中...', 'bg-blue-500 text-white');
resultsArea.textContent = `正在使用 ${selectedModel} 提取 ${presetObj.name || '自定义项'}...`; resultsArea.textContent = `正在使用 ${selectedModel} 提取 ${presetObj.name}...`;
try { try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
@@ -172,7 +274,6 @@ Title: ${pageData.title}`;
const geminiResult = await callGemini(geminiApiKey, selectedModel, systemPrompt, pageData.text, userPrompt); const geminiResult = await callGemini(geminiApiKey, selectedModel, systemPrompt, pageData.text, userPrompt);
let cleanedJson = parseGeminiJson(geminiResult); let cleanedJson = parseGeminiJson(geminiResult);
// 核心修复:处理 AI 返回数组的情况
if (Array.isArray(cleanedJson)) { if (Array.isArray(cleanedJson)) {
cleanedJson = cleanedJson[0] || {}; cleanedJson = cleanedJson[0] || {};
} }
@@ -181,28 +282,19 @@ Title: ${pageData.title}`;
if (googleScriptUrl) { if (googleScriptUrl) {
updateStatus('保存至表格...', 'bg-purple-500 text-white'); updateStatus('保存至表格...', 'bg-purple-500 text-white');
// 1. 获取用户预设的业务表头
const businessHeaders = presetObj.fields.split(',').map(f => f.trim()).filter(f => f !== ""); const businessHeaders = presetObj.fields.split(',').map(f => f.trim()).filter(f => f !== "");
// 2. 定义元数据表头(可选,方便您追踪来源)
const metaHeaders = ["来源链接", "提取时间"]; const metaHeaders = ["来源链接", "提取时间"];
// 3. 合并总表头顺序
const allHeaders = [...businessHeaders, ...metaHeaders]; const allHeaders = [...businessHeaders, ...metaHeaders];
// 4. 构建发送给后端的最终数据
const payload = { const payload = {
sheetName: presetObj.sheetName || '', sheetName: presetObj.sheetName || '',
headers: allHeaders headers: allHeaders
}; };
// 将提取的数据填入 payload
businessHeaders.forEach(h => { businessHeaders.forEach(h => {
payload[h] = cleanedJson[h] || "N/A"; payload[h] = cleanedJson[h] || "N/A";
}); });
// 填入元数据
payload["来源链接"] = pageData.url; payload["来源链接"] = pageData.url;
payload["提取时间"] = new Date().toLocaleString(); payload["提取时间"] = new Date().toLocaleString();