添加期刊引用分析
This commit is contained in:
640
src/utils/exportTableWord.js
Normal file
640
src/utils/exportTableWord.js
Normal file
@@ -0,0 +1,640 @@
|
||||
import {
|
||||
AlignmentType,
|
||||
BorderStyle,
|
||||
Document,
|
||||
LineRuleType,
|
||||
Packer,
|
||||
Paragraph,
|
||||
ShadingType,
|
||||
Table,
|
||||
TableCell,
|
||||
TableLayoutType,
|
||||
TableRow,
|
||||
TextRun,
|
||||
VerticalAlign,
|
||||
WidthType
|
||||
} from 'docx';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { TableUtils } from '@/common/js/TableUtils';
|
||||
import { isMathFormulaTableRecord } from '@/utils/mathFormulaModule';
|
||||
|
||||
/** 斑马纹 rgb(250, 231, 232) */
|
||||
const ODD_ROW_FILL = 'FAE7E8';
|
||||
/** 标题 rgb(210, 90, 90) */
|
||||
const TITLE_COLOR = 'D25A5A';
|
||||
/** 引用 blue rgb(0, 130, 170) */
|
||||
const BLUE_COLOR = '0082AA';
|
||||
const FONT_NAME = 'Charis SIL';
|
||||
/** 六号 = 7.5pt */
|
||||
const TABLE_FONT_SIZE = 15;
|
||||
const TITLE_FONT_SIZE = TABLE_FONT_SIZE;
|
||||
|
||||
function cmToTwips(cm) {
|
||||
return Math.round((cm * 1440) / 2.54);
|
||||
}
|
||||
|
||||
/** 页面边距:上/下 2.54cm,左/右 1.91cm */
|
||||
const PAGE_MARGINS = {
|
||||
top: cmToTwips(2.54),
|
||||
bottom: cmToTwips(2.54),
|
||||
left: cmToTwips(1.91),
|
||||
right: cmToTwips(1.91)
|
||||
};
|
||||
|
||||
/** 表格属性:宽 98.3%,整体居中,无环绕 */
|
||||
const TABLE_WIDTH_PERCENT = 98.3;
|
||||
/** 表格选项(图二):单元格边距 上/下 0、左/右 0.19cm,自动适应内容 */
|
||||
const TABLE_CELL_MARGIN_TWIPS = cmToTwips(0.19);
|
||||
const TABLE_CELL_MARGINS = {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: TABLE_CELL_MARGIN_TWIPS,
|
||||
right: TABLE_CELL_MARGIN_TWIPS
|
||||
};
|
||||
/** 行属性:允许跨页断行,不指定行高 */
|
||||
const TABLE_ROW_OPTIONS = {
|
||||
cantSplit: false
|
||||
};
|
||||
/** 全篇段落:固定行距 10 磅(1pt = 20 twips → 10pt = 200) */
|
||||
const WORD_PARAGRAPH_SPACING = {
|
||||
before: 0,
|
||||
after: 0,
|
||||
line: 200,
|
||||
lineRule: LineRuleType.EXACT,
|
||||
beforeAutoSpacing: false,
|
||||
afterAutoSpacing: false
|
||||
};
|
||||
|
||||
const WORD_DOCUMENT_STYLES = {
|
||||
default: {
|
||||
document: {
|
||||
paragraph: {
|
||||
spacing: WORD_PARAGRAPH_SPACING
|
||||
},
|
||||
run: {
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE
|
||||
}
|
||||
}
|
||||
},
|
||||
paragraphStyles: [
|
||||
{
|
||||
id: 'Normal',
|
||||
name: 'Normal',
|
||||
quickFormat: true,
|
||||
spacing: WORD_PARAGRAPH_SPACING,
|
||||
run: {
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Heading1',
|
||||
name: 'Heading 1',
|
||||
basedOn: 'Normal',
|
||||
next: 'Normal',
|
||||
quickFormat: true,
|
||||
spacing: WORD_PARAGRAPH_SPACING,
|
||||
run: {
|
||||
font: FONT_NAME,
|
||||
size: TITLE_FONT_SIZE,
|
||||
bold: true,
|
||||
color: TITLE_COLOR
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/** 每个表格块末尾空行数 */
|
||||
const TABLE_BLOCK_EMPTY_LINE_COUNT = 4;
|
||||
|
||||
function createWordParagraph(children, options) {
|
||||
const opts = options || {};
|
||||
let alignment = AlignmentType.JUSTIFIED;
|
||||
if (opts.alignment !== undefined && opts.alignment !== null) {
|
||||
alignment = opts.alignment;
|
||||
}
|
||||
|
||||
const paragraphOptions = {
|
||||
spacing: WORD_PARAGRAPH_SPACING,
|
||||
indent: {
|
||||
left: 0,
|
||||
right: 0
|
||||
},
|
||||
children
|
||||
};
|
||||
|
||||
if (opts.title) {
|
||||
paragraphOptions.alignment = AlignmentType.CENTER;
|
||||
paragraphOptions.style = 'Heading1';
|
||||
paragraphOptions.outlineLevel = 0;
|
||||
} else {
|
||||
paragraphOptions.alignment = alignment;
|
||||
paragraphOptions.style = 'Normal';
|
||||
}
|
||||
|
||||
return new Paragraph(paragraphOptions);
|
||||
}
|
||||
|
||||
function appendTableBlockEmptyLines(children) {
|
||||
let i = 0;
|
||||
for (i = 0; i < TABLE_BLOCK_EMPTY_LINE_COUNT; i += 1) {
|
||||
children.push(createTableParagraph([new TextRun('')], AlignmentType.JUSTIFIED));
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格标题段落:居中,大纲 1 级,固定行距 10pt,段前段后 0,无缩进 */
|
||||
function createTitleParagraph(children) {
|
||||
return createWordParagraph(children, { title: true });
|
||||
}
|
||||
|
||||
function createTableParagraph(children, alignment) {
|
||||
return createWordParagraph(children, { alignment });
|
||||
}
|
||||
|
||||
function splitHtmlSegments(html) {
|
||||
const raw = String(html || '');
|
||||
if (!raw) {
|
||||
return [''];
|
||||
}
|
||||
const parts = raw.split(/<br\s*\/?>/gi);
|
||||
if (!parts.length) {
|
||||
return [''];
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
function decodeHtmlEntities(text) {
|
||||
return String(text || '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function htmlToPlainText(html) {
|
||||
const raw = String(html || '');
|
||||
if (typeof document !== 'undefined') {
|
||||
const node = document.createElement('div');
|
||||
node.innerHTML = raw;
|
||||
return (node.textContent || node.innerText || '').replace(/\u00a0/g, ' ').trim();
|
||||
}
|
||||
return decodeHtmlEntities(
|
||||
raw
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<[^>]*>/g, '')
|
||||
).trim();
|
||||
}
|
||||
|
||||
function isBlueElement(node) {
|
||||
if (!node || node.nodeType !== Node.ELEMENT_NODE) {
|
||||
return false;
|
||||
}
|
||||
const tag = node.tagName.toLowerCase();
|
||||
if (tag === 'blue') {
|
||||
return true;
|
||||
}
|
||||
if (tag === 'span' && node.classList && node.classList.contains('blue')) {
|
||||
return true;
|
||||
}
|
||||
return !!(node.classList && node.classList.contains('color-highlight'));
|
||||
}
|
||||
|
||||
function htmlToTextRuns(html, options) {
|
||||
const { bold = false } = options || {};
|
||||
const runs = [];
|
||||
const raw = String(html || '');
|
||||
|
||||
if (!raw) {
|
||||
return [
|
||||
new TextRun({
|
||||
text: '',
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE,
|
||||
bold
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
return [
|
||||
new TextRun({
|
||||
text: htmlToPlainText(raw),
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE,
|
||||
bold
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
const root = document.createElement('div');
|
||||
root.innerHTML = raw;
|
||||
|
||||
function appendRun(text, style) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
runs.push(
|
||||
new TextRun({
|
||||
text,
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE,
|
||||
bold: bold || style.bold,
|
||||
italics: style.italic,
|
||||
superScript: style.sup,
|
||||
subScript: style.sub,
|
||||
color: style.blue ? BLUE_COLOR : undefined
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function walk(node, style) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
appendRun(decodeHtmlEntities(node.textContent).replace(/\u00a0/g, ' '), style);
|
||||
return;
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = node.tagName.toLowerCase();
|
||||
if (tag === 'br') {
|
||||
appendRun('\n', style);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextStyle = {
|
||||
bold: style.bold,
|
||||
italic: style.italic,
|
||||
sup: style.sup,
|
||||
sub: style.sub,
|
||||
blue: style.blue
|
||||
};
|
||||
if (tag === 'b' || tag === 'strong') {
|
||||
nextStyle.bold = true;
|
||||
}
|
||||
if (tag === 'i' || tag === 'em') {
|
||||
nextStyle.italic = true;
|
||||
}
|
||||
if (tag === 'sup') {
|
||||
nextStyle.sup = true;
|
||||
}
|
||||
if (tag === 'sub') {
|
||||
nextStyle.sub = true;
|
||||
}
|
||||
if (isBlueElement(node)) {
|
||||
nextStyle.blue = true;
|
||||
}
|
||||
|
||||
Array.from(node.childNodes).forEach((child) => walk(child, nextStyle));
|
||||
}
|
||||
|
||||
walk(root, {
|
||||
bold: false,
|
||||
italic: false,
|
||||
sup: false,
|
||||
sub: false,
|
||||
blue: false
|
||||
});
|
||||
|
||||
if (!runs.length) {
|
||||
return [
|
||||
new TextRun({
|
||||
text: htmlToPlainText(raw),
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE,
|
||||
bold
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
return runs;
|
||||
}
|
||||
|
||||
/** 表头:相邻片段之间补空格,再规范为“一词一空” */
|
||||
function formatHeaderWordSpacing(html) {
|
||||
const raw = String(html || '');
|
||||
if (!raw) {
|
||||
return '';
|
||||
}
|
||||
if (typeof document !== 'undefined') {
|
||||
const root = document.createElement('div');
|
||||
root.innerHTML = raw;
|
||||
const parts = [];
|
||||
|
||||
function collect(node) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = decodeHtmlEntities(node.textContent).replace(/\u00a0/g, ' ').trim();
|
||||
if (text) {
|
||||
parts.push(text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||||
return;
|
||||
}
|
||||
if (node.tagName.toLowerCase() === 'br') {
|
||||
parts.push('\n');
|
||||
return;
|
||||
}
|
||||
Array.from(node.childNodes).forEach(collect);
|
||||
}
|
||||
|
||||
collect(root);
|
||||
return parts
|
||||
.join(' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
return htmlToPlainText(raw)
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function htmlToHeaderTextRuns(html) {
|
||||
return [
|
||||
new TextRun({
|
||||
text: formatHeaderWordSpacing(html),
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE,
|
||||
bold: true
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格边框:黑色 0.5 磅(OOXML sz 单位为 1/8 pt → 0.5pt = 4) */
|
||||
const TABLE_BORDER = {
|
||||
style: BorderStyle.SINGLE,
|
||||
size: 4,
|
||||
color: '000000'
|
||||
};
|
||||
const TABLE_BORDER_NONE = {
|
||||
style: BorderStyle.NONE,
|
||||
size: 0,
|
||||
color: 'FFFFFF'
|
||||
};
|
||||
|
||||
function createBorder(show) {
|
||||
return show ? TABLE_BORDER : TABLE_BORDER_NONE;
|
||||
}
|
||||
|
||||
function createTableCell(cell, options) {
|
||||
const { header = false, odd = false, lastRow = false } = options;
|
||||
if (!cell || cell.rowspan === 0 || cell.colspan === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cellOptions = {
|
||||
verticalAlign: VerticalAlign.CENTER,
|
||||
borders: {
|
||||
top: createBorder(header),
|
||||
bottom: createBorder(header || lastRow),
|
||||
left: createBorder(false),
|
||||
right: createBorder(false)
|
||||
},
|
||||
children: splitHtmlSegments(cell.text).map((segment) =>
|
||||
createTableParagraph(
|
||||
header ? htmlToHeaderTextRuns(segment) : htmlToTextRuns(segment, { bold: false }),
|
||||
AlignmentType.LEFT
|
||||
)
|
||||
)
|
||||
};
|
||||
|
||||
if (cell.colspan > 1) {
|
||||
cellOptions.columnSpan = cell.colspan;
|
||||
}
|
||||
if (cell.rowspan > 1) {
|
||||
cellOptions.rowSpan = cell.rowspan;
|
||||
}
|
||||
if (odd) {
|
||||
cellOptions.shading = {
|
||||
fill: ODD_ROW_FILL,
|
||||
type: ShadingType.CLEAR,
|
||||
color: 'auto'
|
||||
};
|
||||
}
|
||||
|
||||
return new TableCell(cellOptions);
|
||||
}
|
||||
|
||||
function createTableRow(cells) {
|
||||
return new TableRow({
|
||||
...TABLE_ROW_OPTIONS,
|
||||
children: cells
|
||||
});
|
||||
}
|
||||
|
||||
function buildTableRows(table) {
|
||||
const headers = (table && table.tableHeader) || [];
|
||||
const contents = (table && table.tableContent) || [];
|
||||
const oddRowIds = (table && table.oddRowIds) || [];
|
||||
const rows = [];
|
||||
|
||||
headers.forEach((row) => {
|
||||
const cells = (row || []).map((cell) => createTableCell(cell, { header: true })).filter(Boolean);
|
||||
if (cells.length) {
|
||||
rows.push(createTableRow(cells));
|
||||
}
|
||||
});
|
||||
|
||||
contents.forEach((row, rowIndex) => {
|
||||
const odd = oddRowIds.length ? oddRowIds.includes(row.rowId) : rowIndex % 2 === 0;
|
||||
const cells = (row || [])
|
||||
.map((cell) =>
|
||||
createTableCell(cell, {
|
||||
odd,
|
||||
lastRow: rowIndex === contents.length - 1
|
||||
})
|
||||
)
|
||||
.filter(Boolean);
|
||||
if (cells.length) {
|
||||
rows.push(createTableRow(cells));
|
||||
}
|
||||
});
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
function createWordDocument(children) {
|
||||
return new Document({
|
||||
styles: WORD_DOCUMENT_STYLES,
|
||||
sections: [
|
||||
{
|
||||
properties: {
|
||||
page: {
|
||||
margin: PAGE_MARGINS
|
||||
},
|
||||
grid: {
|
||||
linePitch: 200
|
||||
}
|
||||
},
|
||||
children
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
export function prepareTableExportItem(item) {
|
||||
if (!item || isMathFormulaTableRecord(item)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (item.table && item.table.tableHeader) {
|
||||
return {
|
||||
title: item.title || '',
|
||||
note: item.note || '',
|
||||
table: item.table
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const tableList = typeof item.table === 'string' ? JSON.parse(item.table) : item.table;
|
||||
const splitResult = TableUtils.splitTable(tableList);
|
||||
const rowResult = TableUtils.addRowIdToData(splitResult.content);
|
||||
return {
|
||||
title: item.title || '',
|
||||
note: item.note || '',
|
||||
table: {
|
||||
tableHeader: splitResult.header,
|
||||
tableContent: rowResult.rowData,
|
||||
oddRowIds: rowResult.rowIds
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildTableWordChildren(processedItem, options) {
|
||||
const opts = options || {};
|
||||
const appendTrailingEmptyLines = opts.appendTrailingEmptyLines !== false;
|
||||
const title = (processedItem && processedItem.title) || '';
|
||||
const note = (processedItem && processedItem.note) || '';
|
||||
const table = (processedItem && processedItem.table) || {};
|
||||
const children = [];
|
||||
|
||||
if (title) {
|
||||
splitHtmlSegments(title).forEach((segment) => {
|
||||
children.push(
|
||||
createTitleParagraph([
|
||||
new TextRun({
|
||||
text: htmlToPlainText(segment),
|
||||
bold: true,
|
||||
color: TITLE_COLOR,
|
||||
font: FONT_NAME,
|
||||
size: TITLE_FONT_SIZE
|
||||
})
|
||||
])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const tableRows = buildTableRows(table);
|
||||
if (tableRows.length) {
|
||||
children.push(
|
||||
new Table({
|
||||
width: {
|
||||
size: TABLE_WIDTH_PERCENT,
|
||||
type: WidthType.PERCENTAGE
|
||||
},
|
||||
alignment: AlignmentType.CENTER,
|
||||
layout: TableLayoutType.AUTOFIT,
|
||||
margins: TABLE_CELL_MARGINS,
|
||||
borders: {
|
||||
top: TABLE_BORDER_NONE,
|
||||
bottom: TABLE_BORDER_NONE,
|
||||
left: TABLE_BORDER_NONE,
|
||||
right: TABLE_BORDER_NONE,
|
||||
insideHorizontal: TABLE_BORDER_NONE,
|
||||
insideVertical: TABLE_BORDER_NONE
|
||||
},
|
||||
rows: tableRows
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (note) {
|
||||
splitHtmlSegments(note).forEach((segment) => {
|
||||
children.push(
|
||||
createTableParagraph(htmlToTextRuns(segment, { bold: false }), AlignmentType.JUSTIFIED)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (appendTrailingEmptyLines) {
|
||||
appendTableBlockEmptyLines(children);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
export function buildTableWordDocument(processedItem) {
|
||||
return createWordDocument(buildTableWordChildren(processedItem));
|
||||
}
|
||||
|
||||
export function buildAllTablesWordDocument(items) {
|
||||
const children = [];
|
||||
(items || []).forEach((item) => {
|
||||
const prepared = prepareTableExportItem(item);
|
||||
if (!prepared) {
|
||||
return;
|
||||
}
|
||||
const blockChildren = buildTableWordChildren(prepared);
|
||||
blockChildren.forEach((child) => {
|
||||
children.push(child);
|
||||
});
|
||||
});
|
||||
return createWordDocument(children);
|
||||
}
|
||||
|
||||
function sanitizeFileName(name) {
|
||||
const base = String(name || 'table')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/[\\/:*?"<>|]/g, '')
|
||||
.trim()
|
||||
.slice(0, 80);
|
||||
return base || 'table';
|
||||
}
|
||||
|
||||
export async function downloadTableWord(processedItem, fileName) {
|
||||
const doc = buildTableWordDocument(processedItem);
|
||||
const blob = await Packer.toBlob(doc);
|
||||
saveAs(blob, `${sanitizeFileName(fileName || processedItem.title)}.docx`);
|
||||
}
|
||||
|
||||
export async function downloadAllTablesWord(items, fileName) {
|
||||
const exportItems = [];
|
||||
(items || []).forEach((item) => {
|
||||
const prepared = prepareTableExportItem(item);
|
||||
if (prepared) {
|
||||
exportItems.push(prepared);
|
||||
}
|
||||
});
|
||||
|
||||
if (!exportItems.length) {
|
||||
const error = new Error('NO_TABLES');
|
||||
throw error;
|
||||
}
|
||||
|
||||
const doc = buildAllTablesWordDocument(exportItems);
|
||||
const blob = await Packer.toBlob(doc);
|
||||
saveAs(blob, `${sanitizeFileName(fileName || 'tables')}.docx`);
|
||||
}
|
||||
|
||||
export {
|
||||
appendTableBlockEmptyLines,
|
||||
buildTableWordChildren,
|
||||
createTableParagraph,
|
||||
createWordDocument,
|
||||
FONT_NAME,
|
||||
htmlToPlainText,
|
||||
htmlToTextRuns,
|
||||
PAGE_MARGINS,
|
||||
splitHtmlSegments,
|
||||
TABLE_FONT_SIZE,
|
||||
TITLE_COLOR,
|
||||
WORD_PARAGRAPH_SPACING
|
||||
};
|
||||
Reference in New Issue
Block a user