fix: 术语表导入丢失中文列(BOM) + 导出改用数据库实际内容

- handleGlossaryFiles: 加 transformHeader 剥离BOM+规范化表头,
  修复Excel/PapaParse读取带BOM的CSV时第一列名变成 zh 导致中文列被丢弃
- exportGlossaryCsv: 改为从 /service/glossary 拉取数据库实际内容再导出,
  避免导出未保存的本地脏数据;导出文件加UTF-8 BOM保证Excel中文不乱码
- 导入成功后提示已导入条数并提醒保存

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 19:35:01 +08:00
parent 51e51efd2a
commit dd3c1cfe05
2 changed files with 22 additions and 7 deletions

View File

@@ -185,7 +185,8 @@
"glossaryExportCsv": "导出CSV", "glossaryExportCsv": "导出CSV",
"glossaryUnsaved": "未保存", "glossaryUnsaved": "未保存",
"glossarySaved": "术语表已保存,对所有用户生效。", "glossarySaved": "术语表已保存,对所有用户生效。",
"glossarySaveFailed": "保存失败" "glossarySaveFailed": "保存失败",
"glossaryImported": "已导入 {n} 条术语,请点击【保存】使其对所有用户生效。"
}, },
"en": { "en": {
"pageTitle": "DocuTranslate - Interactive Document Translation", "pageTitle": "DocuTranslate - Interactive Document Translation",
@@ -373,7 +374,8 @@
"glossaryExportCsv": "Export CSV", "glossaryExportCsv": "Export CSV",
"glossaryUnsaved": "Unsaved", "glossaryUnsaved": "Unsaved",
"glossarySaved": "Glossary saved and applied to all users.", "glossarySaved": "Glossary saved and applied to all users.",
"glossarySaveFailed": "Save failed" "glossarySaveFailed": "Save failed",
"glossaryImported": "Imported {n} terms. Click [Save] to apply to all users."
}, },
"id": { "id": {
"pageTitle": "DocuTranslate - Terjemahan Dokumen Interaktif", "pageTitle": "DocuTranslate - Terjemahan Dokumen Interaktif",
@@ -561,6 +563,7 @@
"glossaryExportCsv": "Ekspor CSV", "glossaryExportCsv": "Ekspor CSV",
"glossaryUnsaved": "Belum disimpan", "glossaryUnsaved": "Belum disimpan",
"glossarySaved": "Glosarium disimpan, berlaku untuk semua pengguna.", "glossarySaved": "Glosarium disimpan, berlaku untuk semua pengguna.",
"glossarySaveFailed": "Gagal menyimpan" "glossarySaveFailed": "Gagal menyimpan",
"glossaryImported": "Mengimpor {n} istilah. Klik [Simpan] untuk berlaku ke semua pengguna."
} }
} }

View File

@@ -1427,15 +1427,22 @@
glossarySaving.value = false; glossarySaving.value = false;
} }
}; };
const exportGlossaryCsv = () => { const exportGlossaryCsv = async () => {
const rows = glossaryRows.value; // 从服务端拉取数据库实际内容再导出(避免导出未保存的本地脏数据)
let rows = glossaryRows.value;
try {
const res = await fetch('/service/glossary');
const data = await res.json();
if (data && Array.isArray(data.rows)) rows = data.rows;
} catch (e) { console.error('export fetch failed, using local', e); }
const esc = (s) => { const esc = (s) => {
s = String(s||''); s = String(s||'');
return /[",\n]/.test(s) ? '"' + s.replace(/"/g,'""') + '"' : s; return /[",\n]/.test(s) ? '"' + s.replace(/"/g,'""') + '"' : s;
}; };
let csv = 'zh,en,id\n'; let csv = 'zh,en,id\n';
rows.forEach(r => { csv += [esc(r.zh), esc(r.en), esc(r.id)].join(',') + '\n'; }); rows.forEach(r => { csv += [esc(r.zh), esc(r.en), esc(r.id)].join(',') + '\n'; });
const blob = new Blob([csv], {type: 'text/csv;charset=utf-8'}); // 加 UTF-8 BOM保证 Excel 正确识别中文
const blob = new Blob(['' + csv], {type: 'text/csv;charset=utf-8'});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.download = 'glossary.csv'; a.click(); a.href = url; a.download = 'glossary.csv'; a.click();
@@ -1448,8 +1455,11 @@
Array.from(files).forEach(f => { Array.from(files).forEach(f => {
Papa.parse(f, { Papa.parse(f, {
header: true, skipEmptyLines: true, header: true, skipEmptyLines: true,
// 剥离 BOM + 规范化表头(小写、去空白),兼容 Excel 另存的 CSV
transformHeader: (h) => String(h||'').replace(/^/, '').trim().toLowerCase(),
complete: (res) => { complete: (res) => {
if (!res.data) return; if (!res.data) return;
let added = 0;
res.data.forEach(r => { res.data.forEach(r => {
const row = {zh:'', en:'', id:''}; const row = {zh:'', en:'', id:''};
if ('zh' in r || 'en' in r || 'id' in r) { if ('zh' in r || 'en' in r || 'id' in r) {
@@ -1461,8 +1471,10 @@
if (row.zh || row.en || row.id) { if (row.zh || row.en || row.id) {
glossaryRows.value.push(row); glossaryRows.value.push(row);
glossaryDirty.value = true; glossaryDirty.value = true;
added++;
} }
}); });
if (added) alert(t('glossaryImported').replace('{n}', added));
} }
}); });
}); });