// js/ui/glossary-ui.js (multi-sets) (function() { function el(id) { return document.getElementById(id); } function escapeHtml(str) { return String(str || '').replace(/&/g,'&').replace(//g,'>').replace(/\"/g,'"'); } function normalizeTermForMatch(term) { return String(term || '').trim().toLowerCase(); } function buildSimpleTextFromEntries(entries) { if (!Array.isArray(entries) || entries.length === 0) return ''; return entries.map(item => { const cols = [item.term || '', item.translation || '']; const extras = [item.caseSensitive ? '1' : '0', item.wholeWord ? '1' : '0', item.enabled === false ? '0' : '1']; const defaults = ['0', '0', '1']; let len = extras.length; while (len > 0 && extras[len - 1] === defaults[len - 1]) len--; return cols.concat(extras.slice(0, len)).join('\t'); }).join('\n'); } function parseTMXFormat(xmlText) { try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlText, 'text/xml'); const parseError = xmlDoc.querySelector('parsererror'); if (parseError) throw new Error('XML 解析失败'); const tuNodes = xmlDoc.querySelectorAll('tu'); const entries = []; tuNodes.forEach(tu => { const tuvs = tu.querySelectorAll('tuv'); if (tuvs.length < 2) return; let sourceTerm = ''; let targetTerm = ''; tuvs.forEach(tuv => { const lang = tuv.getAttribute('xml:lang') || tuv.getAttribute('lang') || ''; const seg = tuv.querySelector('seg'); if (!seg) return; const text = seg.textContent.trim(); if (!text) return; if (lang.toLowerCase().startsWith('en') || lang.toLowerCase().startsWith('zh-hans') === false) { if (!sourceTerm) sourceTerm = text; } else { targetTerm = text; } }); if (sourceTerm && targetTerm) { entries.push({ id: generateUUID(), term: sourceTerm, translation: targetTerm, caseSensitive: false, wholeWord: false, enabled: true }); } }); return { success: true, entries }; } catch (err) { return { success: false, error: err.message }; } } function parseTBXFormat(xmlText) { try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlText, 'text/xml'); const parseError = xmlDoc.querySelector('parsererror'); if (parseError) throw new Error('XML 解析失败'); // TBX (TermBase eXchange) 格式 // 结构: -> -> -> -> -> const termEntries = xmlDoc.querySelectorAll('termEntry'); const entries = []; termEntries.forEach(termEntry => { const langSets = termEntry.querySelectorAll('langSet'); if (langSets.length < 2) return; let sourceTerm = ''; let targetTerm = ''; langSets.forEach(langSet => { const lang = langSet.getAttribute('xml:lang') || ''; const term = langSet.querySelector('term, tig term, ntig term'); if (!term) return; const text = term.textContent.trim(); if (!text) return; // 简单的语言判断 if (lang.toLowerCase().startsWith('en') || !sourceTerm) { sourceTerm = text; } else { targetTerm = text; } }); if (sourceTerm && targetTerm && sourceTerm !== targetTerm) { entries.push({ id: generateUUID(), term: sourceTerm, translation: targetTerm, caseSensitive: false, wholeWord: false, enabled: true }); } }); if (entries.length === 0) { return { success: false, error: 'TBX 文件中未找到有效的术语条目' }; } return { success: true, entries }; } catch (err) { return { success: false, error: `TBX 解析失败: ${err.message}` }; } } function parseCSVFormat(text) { // CSV 格式解析(兼容 SDLTB 导出的 CSV) const lines = text.trim().split(/\r?\n/); if (lines.length === 0) return { success: false, error: 'CSV 文件为空' }; const entries = []; let headerSkipped = false; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; // 简单的 CSV 解析(支持引号包裹的字段) const fields = []; let field = ''; let inQuotes = false; for (let j = 0; j < line.length; j++) { const char = line[j]; if (char === '"') { inQuotes = !inQuotes; } else if (char === ',' && !inQuotes) { fields.push(field.trim()); field = ''; } else { field += char; } } fields.push(field.trim()); // 移除引号 const cleanFields = fields.map(f => f.replace(/^"(.*)"$/, '$1')); // 检测是否为表头(包含 "source" "target" "term" 等关键字) if (!headerSkipped && cleanFields.some(f => /^(source|target|term|translation|原文|译文)$/i.test(f.toLowerCase()) )) { headerSkipped = true; continue; } // 至少需要两列 if (cleanFields.length < 2) continue; const term = cleanFields[0] || ''; const translation = cleanFields[1] || ''; if (term && translation) { entries.push({ id: generateUUID(), term, translation, caseSensitive: false, wholeWord: false, enabled: true }); } } if (entries.length === 0) { return { success: false, error: 'CSV 文件中未找到有效的术语条目' }; } return { success: true, entries }; } function parseSimpleGlossaryText(text) { const raw = String(text || ''); const trimmed = raw.trim(); const result = { rawLineCount: 0, entries: [], duplicates: [], invalidLines: [], errorMessage: '' }; if (!trimmed) return result; if (trimmed.startsWith(' { const key = normalizeTermForMatch(item.term); if (map.has(key)) duplicates.push(item.term); map.set(key, item); }); result.entries = Array.from(map.values()); result.rawLineCount = tmxResult.entries.length; result.duplicates = duplicates; return result; } if (trimmed.startsWith('{') || trimmed.startsWith('[')) { try { const parsedJson = JSON.parse(trimmed); let arr = []; if (Array.isArray(parsedJson)) arr = parsedJson; else if (parsedJson && Array.isArray(parsedJson.entries)) arr = parsedJson.entries; else throw new Error('JSON 中未找到 entries 数组'); const normalized = normalizeImportedEntries(arr); const map = new Map(); const duplicates = []; normalized.forEach(item => { const key = normalizeTermForMatch(item.term); if (map.has(key)) duplicates.push(item.term); map.set(key, { ...item }); }); result.entries = Array.from(map.values()); result.rawLineCount = normalized.length; result.duplicates = duplicates; } catch (err) { result.errorMessage = 'JSON 解析失败:' + (err && err.message ? err.message : String(err)); } return result; } // 尝试检测是否为 CSV 格式(包含逗号分隔) const firstLine = trimmed.split(/\r?\n/)[0]; const hasCommas = firstLine.includes(','); const hasTabs = firstLine.includes('\t'); if (hasCommas && !hasTabs) { // 很可能是 CSV 格式 const csvResult = parseCSVFormat(trimmed); if (csvResult.success) { const map = new Map(); const duplicates = []; csvResult.entries.forEach(item => { const key = normalizeTermForMatch(item.term); if (map.has(key)) duplicates.push(item.term); map.set(key, item); }); result.entries = Array.from(map.values()); result.rawLineCount = csvResult.entries.length; result.duplicates = duplicates; return result; } } const lines = raw.split(/\r?\n/); const trimmedLines = lines.map(line => line.trim()).filter(Boolean); const map = new Map(); const duplicates = []; const invalidLines = []; trimmedLines.forEach((line, idx) => { let cols = line.split('\t'); if (cols.length === 1) { if (line.includes('=>')) cols = line.split('=>'); else if (line.includes(',')) cols = line.split(','); } cols = cols.map(part => part.trim()); const term = cols[0] || ''; const translation = cols[1] || ''; if (!term || !translation) { invalidLines.push({ index: idx + 1, content: line }); return; } const caseSensitive = cols[2] ? ['1','true','yes','y'].includes(cols[2].toLowerCase()) : false; const wholeWord = cols[3] ? ['1','true','yes','y'].includes(cols[3].toLowerCase()) : false; const enabledVal = cols[4] ? ['1','true','yes','y'].includes(cols[4].toLowerCase()) : true; const entry = { id: generateUUID(), term, translation, caseSensitive, wholeWord, enabled: enabledVal }; const key = normalizeTermForMatch(term); if (map.has(key)) duplicates.push(term); map.set(key, entry); }); result.rawLineCount = trimmedLines.length; result.entries = Array.from(map.values()); result.duplicates = duplicates; result.invalidLines = invalidLines; return result; } function normalizeImportedEntries(arr) { return (Array.isArray(arr) ? arr : []).map(item => ({ id: generateUUID(), term: String(item.term || '').trim(), translation: String(item.translation || '').trim(), caseSensitive: !!item.caseSensitive, wholeWord: !!item.wholeWord, enabled: item.enabled === undefined ? true : !!item.enabled })).filter(it => it.term && it.translation); } function createGlossaryModal(title) { const overlay = document.createElement('div'); overlay.className = 'fixed inset-0 z-[10000] bg-slate-900/45 backdrop-blur-sm flex items-center justify-center p-4'; const card = document.createElement('div'); card.className = 'w-full max-w-4xl bg-white rounded-2xl shadow-2xl border border-slate-200 overflow-hidden flex flex-col'; const header = document.createElement('div'); header.className = 'flex items-center justify-between px-6 py-4 border-b border-slate-200 bg-slate-50'; const titleEl = document.createElement('h3'); titleEl.className = 'text-lg font-semibold text-slate-800'; titleEl.textContent = title; const closeBtn = document.createElement('button'); closeBtn.className = 'text-slate-500 hover:text-slate-800 transition-colors'; closeBtn.innerHTML = ''; header.appendChild(titleEl); header.appendChild(closeBtn); const body = document.createElement('div'); body.className = 'px-6 py-5 overflow-y-auto max-h-[70vh] space-y-4'; card.appendChild(header); card.appendChild(body); overlay.appendChild(card); const previousOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; document.body.appendChild(overlay); function close() { overlay.remove(); document.body.style.overflow = previousOverflow; } closeBtn.addEventListener('click', close); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); return { overlay, card, body, close }; } function openGlossaryExportModal(setId) { const sets = loadGlossarySets(); const target = sets[setId]; if (!target) { showNotification && showNotification('找不到对应术语库', 'error'); return; } const entries = Array.isArray(target.entries) ? target.entries : []; const simpleText = buildSimpleTextFromEntries(entries); const jsonText = JSON.stringify(entries, null, 2); const modal = createGlossaryModal('导出术语库'); const info = document.createElement('div'); info.className = 'text-sm text-slate-600 leading-relaxed'; info.innerHTML = `

当前术语库 ${escapeHtml(target.name || '')}${entries.length} 条词条。可复制下方简易文本或下载 JSON。

`; const textareaWrap = document.createElement('div'); const textareaLabel = document.createElement('label'); textareaLabel.className = 'block text-sm font-medium text-slate-600 mb-2'; textareaLabel.textContent = '简易文本(术语[TAB]译文 ...)'; const textarea = document.createElement('textarea'); textarea.className = 'w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm leading-relaxed focus:border-blue-300 focus:outline-none focus:ring-2 focus:ring-blue-200 transition'; textarea.rows = 10; textarea.value = simpleText; textareaWrap.appendChild(textareaLabel); textareaWrap.appendChild(textarea); const details = document.createElement('details'); details.className = 'border border-slate-200 rounded-xl bg-slate-50/60'; const summary = document.createElement('summary'); summary.className = 'cursor-pointer px-4 py-2 text-sm font-medium text-slate-700'; summary.textContent = '查看 JSON 格式'; const pre = document.createElement('pre'); pre.className = 'px-4 py-3 text-xs text-slate-600 overflow-x-auto whitespace-pre-wrap'; pre.textContent = jsonText; details.appendChild(summary); details.appendChild(pre); const btnRow = document.createElement('div'); btnRow.className = 'flex flex-wrap items-center justify-between gap-3 pt-2'; const leftHint = document.createElement('div'); leftHint.className = 'text-xs text-slate-500'; leftHint.textContent = '提示:简易文本可直接粘贴到导入面板,JSON 适合备份。'; const btnGroup = document.createElement('div'); btnGroup.className = 'flex flex-wrap gap-2'; const copyBtn = document.createElement('button'); copyBtn.type = 'button'; copyBtn.className = 'inline-flex items-center gap-1 rounded-lg border border-slate-200 px-3 py-1.5 bg-white text-sm hover:border-blue-300 hover:text-blue-600 transition'; copyBtn.innerHTML = '复制文本'; copyBtn.addEventListener('click', async () => { try { if (navigator.clipboard) await navigator.clipboard.writeText(textarea.value || ''); else { textarea.select(); document.execCommand('copy'); } showNotification && showNotification('已复制到剪贴板', 'success'); } catch (err) { showNotification && showNotification('复制失败,请手动复制', 'warning'); } }); const downloadTextBtn = document.createElement('button'); downloadTextBtn.type = 'button'; downloadTextBtn.className = 'inline-flex items-center gap-1 rounded-lg border border-slate-200 px-3 py-1.5 bg-white text-sm hover:border-blue-300 hover:text-blue-600 transition'; downloadTextBtn.innerHTML = '下载 TXT'; downloadTextBtn.addEventListener('click', () => { const blob = new Blob([textarea.value || ''], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'glossary.txt'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); }); const downloadJsonBtn = document.createElement('button'); downloadJsonBtn.type = 'button'; downloadJsonBtn.className = 'inline-flex items-center gap-1 rounded-lg border border-blue-500 px-3 py-1.5 bg-blue-50 text-sm text-blue-600 hover:border-blue-600 hover:bg-blue-100 transition'; downloadJsonBtn.innerHTML = '下载 JSON'; downloadJsonBtn.addEventListener('click', () => { const blob = new Blob([jsonText], { type: 'application/json;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'glossary-entries.json'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); }); const closeBtn = document.createElement('button'); closeBtn.type = 'button'; closeBtn.className = 'inline-flex items-center gap-1 rounded-lg border border-slate-200 px-3 py-1.5 bg-white text-sm hover:border-slate-300 transition'; closeBtn.innerHTML = '关闭'; closeBtn.addEventListener('click', () => modal.close()); btnGroup.appendChild(copyBtn); btnGroup.appendChild(downloadTextBtn); btnGroup.appendChild(downloadJsonBtn); btnGroup.appendChild(closeBtn); btnRow.appendChild(leftHint); btnRow.appendChild(btnGroup); modal.body.appendChild(info); modal.body.appendChild(textareaWrap); modal.body.appendChild(details); modal.body.appendChild(btnRow); } function openGlossaryImportModal(setId) { const sets = loadGlossarySets(); const target = sets[setId]; if (!target) { showNotification && showNotification('找不到对应术语库', 'error'); return; } const existingEntries = Array.isArray(target.entries) ? target.entries : []; const modal = createGlossaryModal('导入术语库'); const intro = document.createElement('div'); intro.className = 'text-sm text-slate-600 leading-relaxed space-y-2'; intro.innerHTML = `

支持五种格式:CSV(逗号分隔)、简易文本(TAB 分隔)、JSONTMX(Translation Memory eXchange)、TBX(TermBase eXchange)。导入前可预览冲突并逐条决定保留现有或采用新词条。

提示:可直接粘贴文本/JSON/TMX/TBX/CSV,或上传文件

`; // 添加文件上传区域 const fileUploadArea = document.createElement('div'); fileUploadArea.className = 'rounded-xl border-2 border-dashed border-slate-200 bg-slate-50/50 p-4 text-center'; fileUploadArea.innerHTML = `
或拖拽文件到此处
支持: .csv, .tmx, .tbx, .txt, .json, .xml
`; const textarea = document.createElement('textarea'); textarea.className = 'w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm leading-relaxed focus:border-blue-300 focus:outline-none focus:ring-2 focus:ring-blue-200 transition'; textarea.rows = 8; textarea.placeholder = '或直接粘贴术语文本、JSON、TMX 内容...'; const actionRow = document.createElement('div'); actionRow.className = 'flex flex-wrap items-center justify-between gap-3'; const hint = document.createElement('div'); hint.className = 'text-xs text-slate-500'; hint.textContent = `当前术语库共有 ${existingEntries.length} 条,导入时可选择覆盖冲突条目。`; const btnGroup = document.createElement('div'); btnGroup.className = 'flex flex-wrap gap-2'; const previewBtn = document.createElement('button'); previewBtn.type = 'button'; previewBtn.className = 'inline-flex items-center gap-1 rounded-lg border border-blue-500 px-3 py-1.5 text-sm text-blue-600 bg-blue-50 hover:border-blue-600 hover:bg-blue-100 transition'; previewBtn.innerHTML = '解析文本'; const applyBtn = document.createElement('button'); applyBtn.type = 'button'; applyBtn.className = 'inline-flex items-center gap-1 rounded-lg border border-green-500 px-3 py-1.5 text-sm text-white bg-green-500 hover:bg-green-600 disabled:bg-slate-200 disabled:border-slate-200 disabled:text-slate-400 transition'; applyBtn.innerHTML = '确认导入'; applyBtn.disabled = true; const cancelBtn = document.createElement('button'); cancelBtn.type = 'button'; cancelBtn.className = 'inline-flex items-center gap-1 rounded-lg border border-slate-200 px-3 py-1.5 text-sm bg-white hover:border-slate-300 transition'; cancelBtn.innerHTML = '取消'; cancelBtn.addEventListener('click', () => modal.close()); btnGroup.appendChild(previewBtn); btnGroup.appendChild(applyBtn); btnGroup.appendChild(cancelBtn); actionRow.appendChild(hint); actionRow.appendChild(btnGroup); const previewArea = document.createElement('div'); previewArea.className = 'space-y-4'; modal.body.appendChild(intro); modal.body.appendChild(fileUploadArea); modal.body.appendChild(textarea); modal.body.appendChild(actionRow); modal.body.appendChild(previewArea); const state = { setId, parseResult: null, conflicts: [], conflictChoices: new Map(), nonConflictEntries: [], incomingCount: 0, originalEntries: existingEntries }; // 文件上传处理 const fileInput = modal.body.querySelector('#glossaryFileInput'); fileInput.addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; const fileName = file.name.toLowerCase(); try { if (fileName.endsWith('.tbx') || fileName.endsWith('.xml')) { // 处理 TBX/XML 文件 const text = await file.text(); const result = parseTBXFormat(text); if (!result.success) { // 如果 TBX 解析失败,尝试作为普通 TMX/文本解析 textarea.value = text; state.parseResult = parseSimpleGlossaryText(text); } else { state.parseResult = { rawLineCount: result.entries.length, entries: result.entries, duplicates: [], invalidLines: [], errorMessage: '' }; } } else if (fileName.endsWith('.csv')) { // 处理 CSV 文件 const text = await file.text(); const result = parseCSVFormat(text); if (!result.success) { showNotification && showNotification(result.error, 'error'); return; } state.parseResult = { rawLineCount: result.entries.length, entries: result.entries, duplicates: [], invalidLines: [], errorMessage: '' }; } else { // TMX/TXT/JSON 文本文件 const text = await file.text(); textarea.value = text; state.parseResult = parseSimpleGlossaryText(text); } // 自动触发预览 analyzeAndPreview(); } catch (err) { showNotification && showNotification('文件读取失败: ' + err.message, 'error'); } }); // 拖拽上传 fileUploadArea.addEventListener('dragover', (e) => { e.preventDefault(); fileUploadArea.classList.add('border-blue-400', 'bg-blue-50'); }); fileUploadArea.addEventListener('dragleave', () => { fileUploadArea.classList.remove('border-blue-400', 'bg-blue-50'); }); fileUploadArea.addEventListener('drop', async (e) => { e.preventDefault(); fileUploadArea.classList.remove('border-blue-400', 'bg-blue-50'); const file = e.dataTransfer.files[0]; if (file) { fileInput.files = e.dataTransfer.files; fileInput.dispatchEvent(new Event('change')); } }); function renderConflictPreview() { const pr = state.parseResult; previewArea.innerHTML = ''; if (!pr) return; if (pr.errorMessage) { const errorBox = document.createElement('div'); errorBox.className = 'rounded-xl border border-red-200 bg-red-50/80 text-red-600 text-sm px-4 py-3'; errorBox.innerHTML = `
解析失败
${escapeHtml(pr.errorMessage)}
`; previewArea.appendChild(errorBox); return; } if (pr.entries.length === 0) { const empty = document.createElement('div'); empty.className = 'rounded-xl border border-slate-200 bg-white px-4 py-4 text-sm text-slate-600 text-center'; empty.innerHTML = '未解析到有效术语,请检查文本格式。'; previewArea.appendChild(empty); applyBtn.disabled = true; applyBtn.innerHTML = '确认导入'; return; } if (pr.invalidLines.length > 0) { const warn = document.createElement('div'); warn.className = 'rounded-xl border border-amber-200 bg-amber-50/80 text-amber-700 text-sm px-4 py-3'; const list = pr.invalidLines.map(item => `
  • 第 ${item.index} 行:${escapeHtml(item.content)}
  • `).join(''); warn.innerHTML = `
    已忽略 ${pr.invalidLines.length} 行格式不正确的记录
      ${list}
    `; previewArea.appendChild(warn); } if (pr.duplicates.length > 0) { const dup = document.createElement('div'); dup.className = 'rounded-xl border border-indigo-200 bg-indigo-50/80 text-indigo-700 text-sm px-4 py-3'; const uniq = Array.from(new Set(pr.duplicates.map(t => t.trim()).filter(Boolean))); dup.innerHTML = `
    导入文本内存在重复术语,已保留最后一次出现
    ${uniq.map(t => escapeHtml(t)).join('、')}
    `; previewArea.appendChild(dup); } const summary = document.createElement('div'); summary.className = 'rounded-xl border border-slate-200 bg-white shadow-sm px-4 py-3 text-sm text-slate-600 flex flex-col gap-1'; summary.innerHTML = `
    有效条目:${state.incomingCount}
    新增:${state.nonConflictEntries.length} 条,冲突:${state.conflicts.length}
    `; previewArea.appendChild(summary); if (state.conflicts.length > 0) { const card = document.createElement('div'); card.className = 'rounded-2xl border-2 border-dashed border-amber-200 bg-amber-50/60 p-4 space-y-4'; const header = document.createElement('div'); header.className = 'flex flex-col md:flex-row md:items-center md:justify-between gap-3'; header.innerHTML = `
    检测到 ${state.conflicts.length} 条冲突项
    `; const actionBtns = document.createElement('div'); actionBtns.className = 'flex flex-wrap gap-2 text-xs'; const useNewBtn = document.createElement('button'); useNewBtn.type = 'button'; useNewBtn.className = 'inline-flex items-center gap-1 rounded-lg border border-emerald-400 px-3 py-1 bg-emerald-100 text-emerald-700 hover:bg-emerald-200 transition'; useNewBtn.innerHTML = '全部使用导入版本'; const useOldBtn = document.createElement('button'); useOldBtn.type = 'button'; useOldBtn.className = 'inline-flex items-center gap-1 rounded-lg border border-slate-300 px-3 py-1 bg-white text-slate-600 hover:border-slate-400 transition'; useOldBtn.innerHTML = '全部保留现有版本'; actionBtns.appendChild(useNewBtn); actionBtns.appendChild(useOldBtn); header.appendChild(actionBtns); card.appendChild(header); const list = document.createElement('div'); list.className = 'space-y-3'; state.conflicts.forEach((conflict, idx) => { const choice = state.conflictChoices.get(conflict.key) || 'new'; const item = document.createElement('div'); item.className = 'rounded-xl border border-amber-200 bg-white px-4 py-3 space-y-3 shadow-sm'; item.innerHTML = `
    ${escapeHtml(conflict.term)}
    现有条目
    译文:${escapeHtml(conflict.existing.translation)}
    大小写敏感:${conflict.existing.caseSensitive ? '是' : '否'} | 全词匹配:${conflict.existing.wholeWord ? '是' : '否'} | 启用:${conflict.existing.enabled === false ? '否' : '是'}
    导入条目
    译文:${escapeHtml(conflict.incoming.translation)}
    大小写敏感:${conflict.incoming.caseSensitive ? '是' : '否'} | 全词匹配:${conflict.incoming.wholeWord ? '是' : '否'} | 启用:${conflict.incoming.enabled === false ? '否' : '是'}
    `; list.appendChild(item); }); card.appendChild(list); previewArea.appendChild(card); actionBtns.querySelectorAll('button').forEach(btn => { btn.addEventListener('click', (evt) => { evt.preventDefault(); const mode = btn === useNewBtn ? 'new' : 'old'; state.conflicts.forEach(conflict => state.conflictChoices.set(conflict.key, mode)); renderConflictPreview(); }); }); list.querySelectorAll('input[type="radio"]').forEach(radio => { radio.addEventListener('change', () => { const key = radio.getAttribute('data-conflict-key'); const value = radio.value; state.conflictChoices.set(key, value); renderConflictPreview(); }); }); } } function analyzeAndPreview() { if (!state.parseResult) { const text = textarea.value; state.parseResult = parseSimpleGlossaryText(text); } const parsed = state.parseResult; state.incomingCount = parsed.entries.length; const currentMap = new Map(state.originalEntries.map(item => [normalizeTermForMatch(item.term), item])); const conflicts = []; const nonConflict = []; parsed.entries.forEach(entry => { const key = normalizeTermForMatch(entry.term); const existing = currentMap.get(key); if (existing) conflicts.push({ key, term: entry.term, existing, incoming: entry }); else nonConflict.push(entry); }); state.conflicts = conflicts; state.nonConflictEntries = nonConflict; state.conflictChoices = new Map(conflicts.map(c => [c.key, 'new'])); const canApply = parsed.entries.length > 0 && !parsed.errorMessage; applyBtn.disabled = !canApply; if (canApply) { applyBtn.innerHTML = `确认导入(${parsed.entries.length} 条)`; } else { applyBtn.innerHTML = '确认导入'; } renderConflictPreview(); } previewBtn.addEventListener('click', () => { state.parseResult = null; // 重置,强制重新解析 analyzeAndPreview(); }); applyBtn.addEventListener('click', async () => { if (!state.parseResult || state.parseResult.entries.length === 0 || state.parseResult.errorMessage) return; const entryCount = state.parseResult.entries.length; const setsLatest = loadGlossarySets(); const targetLatest = setsLatest[setId]; if (!targetLatest) { showNotification && showNotification('术语库已被删除', 'error'); modal.close(); return; } // 准备合并数据 const existing = Array.isArray(targetLatest.entries) ? targetLatest.entries : []; const result = []; const conflictMap = new Map(state.conflicts.map(conflict => [conflict.key, conflict])); const choiceMap = state.conflictChoices; existing.forEach(item => { const key = normalizeTermForMatch(item.term); if (conflictMap.has(key)) { const conflict = conflictMap.get(key); const choice = choiceMap.get(key) || 'new'; if (choice === 'new') { const newEntry = { ...conflict.incoming, id: item.id }; result.push(newEntry); } else { result.push(item); } } else { result.push(item); } }); state.nonConflictEntries.forEach(entry => { result.push({ ...entry }); }); // 对于大数据量显示进度条 if (entryCount > 1000 && typeof window.glossaryProgress !== 'undefined') { try { // 显示进度条 window.glossaryProgress.show(`正在导入 ${entryCount.toLocaleString()} 条术语到 "${escapeHtml(targetLatest.name)}"...`); modal.close(); // 关闭模态框,显示进度条 // 使用异步保存并更新进度 targetLatest.entries = result; await saveGlossarySetAsync(setId, targetLatest.name, targetLatest.enabled, result, (current, total) => { window.glossaryProgress.update(current, total, `正在保存术语...`); }); // 刷新增强编辑器(如果正在使用) if (typeof window.glossaryEditorEnhanced !== 'undefined' && window.glossaryEditorEnhanced._currentSetId === setId) { await window.glossaryEditorEnhanced.open(setId); } else { renderEntriesTable(setId); } renderGlossarySetsTable(); window.glossaryProgress.complete(`成功导入 ${entryCount.toLocaleString()} 条术语(新增 ${state.nonConflictEntries.length},冲突 ${state.conflicts.length})`, true); } catch (err) { window.glossaryProgress.complete(`导入失败: ${err.message}`, false); } } else { // 小数据量直接保存 targetLatest.entries = result; saveGlossarySets(setsLatest); // 刷新增强编辑器(如果正在使用) if (typeof window.glossaryEditorEnhanced !== 'undefined' && window.glossaryEditorEnhanced._currentSetId === setId) { window.glossaryEditorEnhanced.open(setId); } else { renderEntriesTable(setId); } renderGlossarySetsTable(); modal.close(); showNotification && showNotification(`已导入 ${entryCount.toLocaleString()} 条术语(新增 ${state.nonConflictEntries.length},冲突 ${state.conflicts.length})`, 'success'); } }); } function renderGlossarySetsTable() { const container = el('glossarySetsTable'); if (!container || typeof loadGlossarySets !== 'function') return; const sets = loadGlossarySets(); const ids = Object.keys(sets); const hint = el('glossarySetsCountHint'); const enabledCount = ids.reduce((acc, id) => acc + (sets[id] && sets[id].enabled ? 1 : 0), 0); if (hint) { if (ids.length === 0) { hint.textContent = '暂无术语库'; hint.classList.add('text-slate-400'); } else { hint.textContent = `启用 ${enabledCount} / ${ids.length}`; hint.classList.remove('text-slate-400'); } } if (ids.length === 0) { container.innerHTML = `

    暂无术语库,可点击上方按钮快速创建或导入现有 JSON 文件。

    `; return; } const cards = ids.map(id => { const s = sets[id]; const count = Array.isArray(s.entries) ? s.entries.length : 0; return `
    ${count} 条词条 ID: ${id.slice(0, 8)}
    `; }).join(''); container.innerHTML = `
    ${cards}
    `; container.querySelectorAll('[data-action="toggle-set"]').forEach(cb => { cb.addEventListener('change', (e) => { const id = e.target.getAttribute('data-id'); toggleGlossarySet(id, e.target.checked); renderGlossarySetsTable(); }); }); container.querySelectorAll('[data-action="rename-set"]').forEach(inp => { const handler = (e) => renameGlossarySet(e.target.getAttribute('data-id'), e.target.value); inp.addEventListener('change', handler); inp.addEventListener('blur', handler); }); container.querySelectorAll('[data-action="delete-set"]').forEach(btn => { btn.addEventListener('click', (e) => { const id = e.currentTarget.getAttribute('data-id'); if (!confirm('确认删除该术语库?此操作不可撤销。')) return; deleteGlossarySet(id); const panel = el('glossaryEditorPanel'); if (panel && panel.dataset.editingId === id) { panel.classList.add('hidden'); panel.dataset.editingId = ''; } renderGlossarySetsTable(); }); }); container.querySelectorAll('[data-action="export-set"]').forEach(btn => { btn.addEventListener('click', (e) => { const id = e.currentTarget.getAttribute('data-id'); const data = exportGlossarySet(id); const blob = new Blob([data], { type: 'application/json;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'glossary-set.json'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); }); }); container.querySelectorAll('[data-action="edit-set"]').forEach(btn => btn.addEventListener('click', (e) => openEditorForSet(e.currentTarget.getAttribute('data-id')))); } function openEditorForSet(setId) { // 优先使用增强版编辑器(支持搜索、分页、批量操作) if (typeof window.glossaryEditorEnhanced !== 'undefined') { window.glossaryEditorEnhanced.open(setId); return; } // 降级到旧版编辑器 const panel = el('glossaryEditorPanel'); if (!panel) return; panel.classList.remove('hidden'); panel.dataset.editingId = setId; renderEntriesTable(setId); } function renderEntriesTable(setId) { const wrap = el('glossaryEntriesTable'); if (!wrap) return; const sets = loadGlossarySets(); const s = sets[setId]; if (!s) { wrap.innerHTML = '
    术语库不存在,可能已被删除。
    '; return; } const entries = Array.isArray(s.entries) ? s.entries : []; const rows = entries.map(e => ` `).join(''); wrap.innerHTML = `
    当前术语库:${escapeHtml(s.name || '')}(${entries.length} 条)
    ${rows || ''}
    术语/词组 译文 大小写敏感 全词匹配 启用 操作
    暂无词条,请先新增或导入。
    `; wrap.querySelectorAll('input[data-row][data-col], textarea[data-row][data-col]').forEach(inp => { const handler = () => { const row = inp.getAttribute('data-row'); const col = inp.getAttribute('data-col'); const sets2 = loadGlossarySets(); const s2 = sets2[setId]; if (!s2) return; const idx = s2.entries.findIndex(x => x.id === row); if (idx === -1) return; let val; if (inp.type === 'checkbox') { val = inp.checked; } else { val = inp.value; } s2.entries[idx][col] = val; saveGlossarySets(sets2); }; inp.addEventListener('change', handler); if (inp.type !== 'checkbox') inp.addEventListener('input', handler); }); wrap.querySelectorAll('[data-action="del-entry"]').forEach(btn => { btn.addEventListener('click', () => { const row = btn.getAttribute('data-row'); const sets2 = loadGlossarySets(); const s2 = sets2[setId]; if (!s2) return; s2.entries = s2.entries.filter(x => x.id !== row); saveGlossarySets(sets2); renderEntriesTable(setId); }); }); const addBtn = el('addEntryBtn'); if (addBtn) addBtn.addEventListener('click', () => { const sets2 = loadGlossarySets(); const s2 = sets2[setId]; s2.entries.push({ id: generateUUID(), term: '', translation: '', caseSensitive: false, wholeWord: false, enabled: true }); saveGlossarySets(sets2); renderEntriesTable(setId); }); const importBtn = el('importEntriesBtn'); if (importBtn) importBtn.addEventListener('click', () => openGlossaryImportModal(setId)); const exportBtn = el('exportEntriesBtn'); if (exportBtn) exportBtn.addEventListener('click', () => openGlossaryExportModal(setId)); } function bindTopControls() { const addSetBtn = el('addGlossarySetBtn'); if (addSetBtn) addSetBtn.addEventListener('click', () => { const s = prompt('输入术语库名称', '新术语库'); if (s === null) return; createGlossarySet(s || '新术语库'); renderGlossarySetsTable(); }); const importSetBtn = el('importGlossarySetBtn'); const importSetFile = el('importGlossarySetFile'); if (importSetBtn && importSetFile) { importSetBtn.addEventListener('click', () => importSetFile.click()); importSetFile.addEventListener('change', async (e) => { const file = e.target.files && e.target.files[0]; if (!file) return; try { const text = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || '{}')); reader.onerror = () => reject(reader.error); reader.readAsText(file); }); // 检查数据大小以决定是否显示进度条 const data = JSON.parse(text); const entryCount = data.entries?.length || 0; // 对于大于 1000 条的导入显示进度条 if (entryCount > 1000 && typeof window.glossaryProgress !== 'undefined') { window.glossaryProgress.show(`正在导入 ${entryCount.toLocaleString()} 条术语...`); await importGlossarySetAsync(text, (current, total) => { window.glossaryProgress.update(current, total, `正在保存术语...`); }); window.glossaryProgress.complete(`成功导入 ${entryCount.toLocaleString()} 条术语`, true); renderGlossarySetsTable(); } else { // 小量数据直接导入,不显示进度条 importGlossarySet(text); renderGlossarySetsTable(); showNotification && showNotification('术语库已导入', 'success'); } } catch (err) { if (typeof window.glossaryProgress !== 'undefined') { window.glossaryProgress.complete(`导入失败: ${err.message}`, false); } showNotification && showNotification(`导入失败:${err.message}`, 'error'); } finally { importSetFile.value = ''; } }); } const exportAllBtn = el('exportAllGlossarySetsBtn'); if (exportAllBtn) exportAllBtn.addEventListener('click', () => { const data = exportAllGlossarySets(); const blob = new Blob([data], { type: 'application/json;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'glossary-sets.json'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); }); } window.glossaryUI = { renderGlossarySetsTable, openEditorForSet, renderEntriesTable }; // 暴露导入导出函数供增强编辑器使用 window.openGlossaryImportModal = openGlossaryImportModal; window.openGlossaryExportModal = openGlossaryExportModal; // 监听术语库加载完成事件 window.addEventListener('glossarySetsLoaded', function() { console.log('[GlossaryUI] Glossary sets loaded, rendering...'); renderGlossarySetsTable(); }); document.addEventListener('DOMContentLoaded', function() { console.log('[GlossaryUI] DOM loaded, initializing...'); // 绑定顶部控件 bindTopControls(); // 检查缓存是否已经准备好 if (window._glossarySetsCache && Object.keys(window._glossarySetsCache).length > 0) { console.log('[GlossaryUI] Cache already ready, rendering immediately'); renderGlossarySetsTable(); } else { console.log('[GlossaryUI] Waiting for glossary data to load...'); // 显示加载提示 const container = el('glossarySetsTable'); if (container) { container.innerHTML = `

    正在加载术语库...

    `; } } }); })();