添加期刊引用分析

This commit is contained in:
2026-06-17 09:35:15 +08:00
parent ec4a59eedb
commit ea5695f913
36 changed files with 6129 additions and 103 deletions

View 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(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/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
};