support edit
This commit is contained in:
@@ -24,41 +24,51 @@ function doPost(e) {
|
|||||||
sheet = ss.getActiveSheet();
|
sheet = ss.getActiveSheet();
|
||||||
}
|
}
|
||||||
|
|
||||||
var headers = [];
|
var existingHeaders = [];
|
||||||
|
|
||||||
// 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);
|
|
||||||
} else {
|
|
||||||
// Get existing headers from the first row
|
|
||||||
var lastCol = sheet.getLastColumn();
|
var lastCol = sheet.getLastColumn();
|
||||||
if (lastCol > 0) {
|
var lastRow = sheet.getLastRow();
|
||||||
headers = sheet.getRange(1, 1, 1, lastCol).getValues()[0];
|
|
||||||
|
// 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 {
|
||||||
|
// 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
|
// 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() }))
|
||||||
|
|||||||
@@ -44,7 +44,10 @@
|
|||||||
<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 -->
|
||||||
|
|||||||
90
sidepanel.js
90
sidepanel.js
@@ -11,6 +11,7 @@ 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');
|
||||||
@@ -26,6 +27,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');
|
||||||
@@ -49,7 +51,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();
|
||||||
@@ -72,25 +74,66 @@ addPresetBtn.addEventListener('click', async () => {
|
|||||||
|
|
||||||
if (!name || !rawFields) return alert('请输入名称和字段');
|
if (!name || !rawFields) return alert('请输入名称和字段');
|
||||||
|
|
||||||
// 核心逻辑:支持中英文逗号,并清理多余空格
|
|
||||||
const normalizedFields = rawFields
|
const normalizedFields = rawFields
|
||||||
.split(/[,,]/) // 使用正则匹配中英文逗号
|
.split(/[,,]/)
|
||||||
.map(f => f.trim()) // 清理每一项前后的空格
|
.map(f => f.trim())
|
||||||
.filter(f => f !== "") // 过滤掉空项
|
.filter(f => f !== "")
|
||||||
.join(', '); // 统一用英文逗号+空格连接
|
.join(', ');
|
||||||
|
|
||||||
const { userPresets = DEFAULT_PRESETS } = await chrome.storage.local.get('userPresets');
|
const { userPresets = DEFAULT_PRESETS } = await chrome.storage.local.get('userPresets');
|
||||||
|
|
||||||
|
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 };
|
const newPreset = { id: Date.now().toString(), name, sheetName, fields: normalizedFields };
|
||||||
currentPresets = [...userPresets, newPreset];
|
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);
|
||||||
|
|
||||||
|
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) {
|
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 });
|
||||||
@@ -98,33 +141,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>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
|
<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="删除预设">
|
||||||
|
<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;
|
||||||
@@ -132,7 +174,7 @@ 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);
|
||||||
@@ -149,7 +191,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 });
|
||||||
|
|||||||
Reference in New Issue
Block a user