diff --git a/google-script.js b/google-script.js index 0ea3242..1806176 100644 --- a/google-script.js +++ b/google-script.js @@ -24,41 +24,51 @@ function doPost(e) { sheet = ss.getActiveSheet(); } - var headers = []; - - // 2. Determine headers - if (sheet.getLastRow() === 0) { - // Use provided headers if available - if (data.headers && Array.isArray(data.headers)) { - headers = data.headers; - } else { - // Fallback: Use keys but exclude internal control keys - headers = Object.keys(data).filter(function(key) { - return !["sheetName", "headers"].includes(key); - }); - } - sheet.appendRow(headers); + var existingHeaders = []; + var lastCol = sheet.getLastColumn(); + var lastRow = sheet.getLastRow(); + + // 2. Get incoming headers (from data.headers or keys) + var incomingHeaders = data.headers || Object.keys(data).filter(function(k) { + return !["sheetName", "headers"].includes(k); + }); + + if (lastRow === 0) { + // New sheet: Write all incoming headers as the first row + existingHeaders = incomingHeaders; + sheet.appendRow(existingHeaders); } else { - // Get existing headers from the first row - var lastCol = sheet.getLastColumn(); - if (lastCol > 0) { - headers = sheet.getRange(1, 1, 1, lastCol).getValues()[0]; + // Existing sheet: Read current headers from row 1 + existingHeaders = 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 - var row = headers.map(function(header) { + // 3. Map data to the (potentially updated) existingHeaders + var row = existingHeaders.map(function(header) { var val = data[header]; 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); return val; }); sheet.appendRow(row); - return ContentService.createTextOutput(JSON.stringify({ status: "success", sheet: sheet.getName() })) - .setMimeType(ContentService.MimeType.JSON); + return ContentService.createTextOutput(JSON.stringify({ + status: "success", + sheet: sheet.getName(), + addedColumns: missingHeaders ? missingHeaders.length : 0 + })).setMimeType(ContentService.MimeType.JSON); } catch (error) { return ContentService.createTextOutput(JSON.stringify({ status: "error", message: error.toString() })) diff --git a/sidepanel.html b/sidepanel.html index 60c9ba9..f251242 100644 --- a/sidepanel.html +++ b/sidepanel.html @@ -44,7 +44,10 @@ - +
+ + +
diff --git a/sidepanel.js b/sidepanel.js index 03eb423..e953507 100644 --- a/sidepanel.js +++ b/sidepanel.js @@ -11,6 +11,7 @@ const newPresetName = document.getElementById('newPresetName'); const newPresetSheet = document.getElementById('newPresetSheet'); const newPresetFields = document.getElementById('newPresetFields'); const addPresetBtn = document.getElementById('addPreset'); +const cancelEditBtn = document.getElementById('cancelEdit'); const presetList = document.getElementById('presetList'); const presetSelect = document.getElementById('presetSelect'); const runPresetBtn = document.getElementById('runPreset'); @@ -26,6 +27,7 @@ const DEFAULT_PRESETS = [ ]; let currentPresets = []; +let editingPresetId = null; toggleConfig.addEventListener('click', () => { const isHidden = configContent.classList.toggle('hidden'); @@ -49,7 +51,7 @@ chrome.storage.local.get(['geminiApiKey', 'googleScriptUrl', 'geminiModel', 'use renderPresets(currentPresets); }); -// Save settings +// Save global settings saveConfigBtn.addEventListener('click', () => { const apiKey = apiKeyInput.value.trim(); const scriptUrl = scriptUrlInput.value.trim(); @@ -72,25 +74,66 @@ addPresetBtn.addEventListener('click', async () => { if (!name || !rawFields) return alert('请输入名称和字段'); - // 核心逻辑:支持中英文逗号,并清理多余空格 const normalizedFields = rawFields - .split(/[,,]/) // 使用正则匹配中英文逗号 - .map(f => f.trim()) // 清理每一项前后的空格 - .filter(f => f !== "") // 过滤掉空项 - .join(', '); // 统一用英文逗号+空格连接 + .split(/[,,]/) + .map(f => f.trim()) + .filter(f => f !== "") + .join(', '); const { userPresets = DEFAULT_PRESETS } = await chrome.storage.local.get('userPresets'); - const newPreset = { id: Date.now().toString(), name, sheetName, fields: normalizedFields }; - 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 }); - newPresetName.value = ''; - newPresetSheet.value = ''; - newPresetFields.value = ''; + clearPresetInputs(); renderPresets(currentPresets); }); +cancelEditBtn.addEventListener('click', stopEditing); + +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'); + + // Ensure config is expanded so user can see inputs + 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) { + if (editingPresetId === id) stopEditing(); const { userPresets = DEFAULT_PRESETS } = await chrome.storage.local.get('userPresets'); currentPresets = userPresets.filter(p => p.id !== id); await chrome.storage.local.set({ userPresets: currentPresets }); @@ -98,33 +141,32 @@ async function deletePreset(id) { } function renderPresets(presets) { - // Clear containers presetList.innerHTML = ''; presetSelect.innerHTML = ''; presets.forEach(preset => { - // 1. Render in Settings List + // 1. Settings List Item 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.innerHTML = ` -
+
${preset.name} ${preset.sheetName ? `#${preset.sheetName}` : ''}
${preset.fields}
- +
+ + +
`; - item.querySelector('button').onclick = (e) => { - e.stopPropagation(); - deletePreset(preset.id); - }; + item.querySelector('.edit-btn').onclick = () => startEditing(preset); + item.querySelector('.delete-btn').onclick = () => deletePreset(preset.id); presetList.appendChild(item); - // 2. Render as Dropdown Option + // 2. Dropdown Option const opt = document.createElement('option'); opt.value = preset.id; opt.textContent = preset.name; @@ -132,7 +174,7 @@ function renderPresets(presets) { }); } -// Action Handlers +// Extraction Handler runPresetBtn.addEventListener('click', () => { const selectedId = presetSelect.value; const preset = currentPresets.find(p => p.id === selectedId); @@ -149,7 +191,7 @@ async function handleExtraction(type, presetObj) { const selectedModel = geminiModel || 'gemini-1.5-flash'; updateStatus('提取中...', 'bg-blue-500 text-white'); - resultsArea.textContent = `正在使用 ${selectedModel} 提取 ${presetObj.name || '自定义项'}...`; + resultsArea.textContent = `正在使用 ${selectedModel} 提取 ${presetObj.name}...`; try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });