相关性代码调整
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
WidthType
|
||||
} from 'docx';
|
||||
import { saveAs } from 'file-saver';
|
||||
import JSZip from 'jszip';
|
||||
import { TableUtils } from '@/common/js/TableUtils';
|
||||
import { isMathFormulaTableRecord } from '@/utils/mathFormulaModule';
|
||||
|
||||
@@ -28,6 +29,10 @@ const FONT_NAME = 'Charis SIL';
|
||||
/** 六号 = 7.5pt */
|
||||
const TABLE_FONT_SIZE = 15;
|
||||
const TITLE_FONT_SIZE = TABLE_FONT_SIZE;
|
||||
/** Word「为字体调整字间距」:1 磅或更大(w:kern 以半磅为单位) */
|
||||
const FONT_KERN_MIN_1PT = 2;
|
||||
/** Word「为字体调整字间距」:小二(18pt)或更大 */
|
||||
const FONT_KERN_MIN_XIAO_ER = 36;
|
||||
|
||||
function cmToTwips(cm) {
|
||||
return Math.round((cm * 1440) / 2.54);
|
||||
@@ -73,7 +78,8 @@ const WORD_DOCUMENT_STYLES = {
|
||||
},
|
||||
run: {
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE
|
||||
size: TABLE_FONT_SIZE,
|
||||
kern: FONT_KERN_MIN_1PT
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -85,7 +91,8 @@ const WORD_DOCUMENT_STYLES = {
|
||||
spacing: WORD_PARAGRAPH_SPACING,
|
||||
run: {
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE
|
||||
size: TABLE_FONT_SIZE,
|
||||
kern: FONT_KERN_MIN_1PT
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -99,7 +106,8 @@ const WORD_DOCUMENT_STYLES = {
|
||||
font: FONT_NAME,
|
||||
size: TITLE_FONT_SIZE,
|
||||
bold: true,
|
||||
color: TITLE_COLOR
|
||||
color: TITLE_COLOR,
|
||||
kern: FONT_KERN_MIN_1PT
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -126,8 +134,7 @@ function createWordParagraph(children, options) {
|
||||
|
||||
if (opts.title) {
|
||||
paragraphOptions.alignment = AlignmentType.CENTER;
|
||||
paragraphOptions.style = 'Heading1';
|
||||
paragraphOptions.outlineLevel = 0;
|
||||
paragraphOptions.style = 'Normal';
|
||||
} else {
|
||||
paragraphOptions.alignment = alignment;
|
||||
paragraphOptions.style = 'Normal';
|
||||
@@ -143,9 +150,134 @@ function appendTableBlockEmptyLines(children) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格标题段落:居中,大纲 1 级,固定行距 10pt,段前段后 0,无缩进 */
|
||||
/** 表格标题段落:居中,固定行距 10pt,段前段后 0,无缩进(非大纲一级) */
|
||||
function createTitleParagraph(children) {
|
||||
return createWordParagraph(children, { title: true });
|
||||
return createWordParagraph(children, { alignment: AlignmentType.CENTER });
|
||||
}
|
||||
|
||||
function decodeTableCaptionXmlText(text) {
|
||||
return String(text || '')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&/g, '&');
|
||||
}
|
||||
|
||||
function extractTableCaptionParagraphPlainText(pInner) {
|
||||
const parts = [];
|
||||
String(pInner || '').replace(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/g, function (_match, value) {
|
||||
parts.push(decodeTableCaptionXmlText(value));
|
||||
});
|
||||
return parts.join('').replace(/[\u200B-\u200D\uFEFF]/g, '').trim();
|
||||
}
|
||||
|
||||
function isBodyTableCaptionPlainText(text) {
|
||||
return /^Table\s+\d+/i.test(String(text || '').trim());
|
||||
}
|
||||
|
||||
function applyTableCaptionCenterParagraphPr(pInner) {
|
||||
if (/<w:pPr>/.test(pInner)) {
|
||||
let inner = pInner
|
||||
.replace(/<w:jc\b[^>]*\/>/g, '')
|
||||
.replace(/<w:jc\b[^>]*>[\s\S]*?<\/w:jc>/g, '');
|
||||
return inner.replace(/<w:pPr>/, '<w:pPr><w:jc w:val="center"/>');
|
||||
}
|
||||
return '<w:pPr><w:jc w:val="center"/></w:pPr>' + pInner;
|
||||
}
|
||||
|
||||
/** 正文表格标题(Table N…)紧挨 w:tbl 前的段落强制居中,不含首页元数据表格 */
|
||||
export function patchBodyTableCaptionCenterToXml(xml) {
|
||||
return xml.replace(/((?:<w:p\b[^>]*>[\s\S]*?<\/w:p>\s*)+)(\s*<w:tbl\b)/gi, function (match, paragraphGroup, tblOpen) {
|
||||
const blocks = paragraphGroup.match(/<w:p\b[^>]*>[\s\S]*?<\/w:p>/gi) || [];
|
||||
if (!blocks.length) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const firstText = extractTableCaptionParagraphPlainText(
|
||||
blocks[0].replace(/^<w:p\b[^>]*>/, '').replace(/<\/w:p>$/, '')
|
||||
);
|
||||
if (!isBodyTableCaptionPlainText(firstText)) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const centered = blocks.map(function (block) {
|
||||
return block.replace(/(<w:p\b[^>]*>)([\s\S]*?)(<\/w:p>)/, function (_full, open, inner, close) {
|
||||
return open + applyTableCaptionCenterParagraphPr(inner) + close;
|
||||
});
|
||||
});
|
||||
return centered.join('') + tblOpen;
|
||||
});
|
||||
}
|
||||
|
||||
/** 导出后处理:除首页表格外,正文表格标题居中 */
|
||||
async function patchBodyTableCaptionCenter(blob) {
|
||||
if (!blob) {
|
||||
return blob;
|
||||
}
|
||||
|
||||
const zip = await JSZip.loadAsync(await blob.arrayBuffer());
|
||||
const documentFile = zip.file('word/document.xml');
|
||||
if (!documentFile) {
|
||||
return blob;
|
||||
}
|
||||
|
||||
let xml = await documentFile.async('string');
|
||||
if (!/\bTable\s+\d+/i.test(xml)) {
|
||||
return blob;
|
||||
}
|
||||
|
||||
xml = patchBodyTableCaptionCenterToXml(xml);
|
||||
|
||||
zip.file('word/document.xml', xml);
|
||||
return zip.generateAsync({
|
||||
type: 'blob',
|
||||
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
});
|
||||
}
|
||||
|
||||
/** 图片标题:与表格标题相同字号/颜色/段落,但两端对齐;末尾无句点则补 . */
|
||||
function ensureFigureTitleTrailingPeriod(text) {
|
||||
const trimmed = String(text || '').trim();
|
||||
if (!trimmed) {
|
||||
return '';
|
||||
}
|
||||
if (trimmed.endsWith('.')) {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed + '.';
|
||||
}
|
||||
|
||||
function createFigureTitleParagraph(children) {
|
||||
return createWordParagraph(children, { alignment: AlignmentType.JUSTIFIED });
|
||||
}
|
||||
|
||||
function appendFigureTitleParagraphs(children, titleHtml) {
|
||||
const segments = splitHtmlSegments(titleHtml)
|
||||
.map(function (segment) {
|
||||
return htmlToPlainText(segment).trim();
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (!segments.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
segments.forEach(function (segment, index) {
|
||||
const text =
|
||||
index === segments.length - 1 ? ensureFigureTitleTrailingPeriod(segment) : segment;
|
||||
children.push(
|
||||
createFigureTitleParagraph([
|
||||
new TextRun({
|
||||
text: text,
|
||||
bold: true,
|
||||
color: TITLE_COLOR,
|
||||
font: FONT_NAME,
|
||||
size: TITLE_FONT_SIZE,
|
||||
kern: FONT_KERN_MIN_1PT
|
||||
})
|
||||
])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function createTableParagraph(children, alignment) {
|
||||
@@ -201,6 +333,42 @@ function isBlueElement(node) {
|
||||
return !!(node.classList && node.classList.contains('color-highlight'));
|
||||
}
|
||||
|
||||
/** 参考文献标号 [1] / [1, 2]:逗号后补空格(与 HTML 排版规则一致,重复 3 次) */
|
||||
function normalizeCitationBracketCommaSpacing(text) {
|
||||
return String(text || '').replace(/\[([^\]]*)\]/g, function (match, inner) {
|
||||
let fixed = inner;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
fixed = fixed.replace(/(\d),(\d)/g, '$1, $2');
|
||||
}
|
||||
return '[' + fixed + ']';
|
||||
});
|
||||
}
|
||||
|
||||
/** 匹配正文参考文献标号:\[[0-9, -\-\]{1,}\] */
|
||||
const CITATION_BRACKET_PATTERN = /\[[0-9, \-]+\]/g;
|
||||
|
||||
function appendRunsForCitationText(text, style, appendRun) {
|
||||
const normalized = normalizeCitationBracketCommaSpacing(text);
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
CITATION_BRACKET_PATTERN.lastIndex = 0;
|
||||
while ((match = CITATION_BRACKET_PATTERN.exec(normalized)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
appendRun(normalized.slice(lastIndex, match.index), style);
|
||||
}
|
||||
appendRun(match[0], Object.assign({}, style, { blue: true }));
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
if (lastIndex < normalized.length) {
|
||||
appendRun(normalized.slice(lastIndex), style);
|
||||
}
|
||||
}
|
||||
|
||||
function htmlToTextRuns(html, options) {
|
||||
const { bold = false } = options || {};
|
||||
const runs = [];
|
||||
@@ -212,18 +380,42 @@ function htmlToTextRuns(html, options) {
|
||||
text: '',
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE,
|
||||
bold
|
||||
bold,
|
||||
kern: FONT_KERN_MIN_1PT
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
const plainRuns = [];
|
||||
appendRunsForCitationText(htmlToPlainText(raw), { bold, italics: false, sup: false, sub: false, blue: false }, function (
|
||||
text,
|
||||
style
|
||||
) {
|
||||
plainRuns.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,
|
||||
kern: FONT_KERN_MIN_1PT
|
||||
})
|
||||
);
|
||||
});
|
||||
if (plainRuns.length) {
|
||||
return plainRuns;
|
||||
}
|
||||
return [
|
||||
new TextRun({
|
||||
text: htmlToPlainText(raw),
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE,
|
||||
bold
|
||||
bold,
|
||||
kern: FONT_KERN_MIN_1PT
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -244,14 +436,19 @@ function htmlToTextRuns(html, options) {
|
||||
italics: style.italic,
|
||||
superScript: style.sup,
|
||||
subScript: style.sub,
|
||||
color: style.blue ? BLUE_COLOR : undefined
|
||||
color: style.blue ? BLUE_COLOR : undefined,
|
||||
kern: FONT_KERN_MIN_1PT
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function walk(node, style) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
appendRun(decodeHtmlEntities(node.textContent).replace(/\u00a0/g, ' '), style);
|
||||
appendRunsForCitationText(
|
||||
decodeHtmlEntities(node.textContent).replace(/\u00a0/g, ' '),
|
||||
style,
|
||||
appendRun
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||||
@@ -304,7 +501,8 @@ function htmlToTextRuns(html, options) {
|
||||
text: htmlToPlainText(raw),
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE,
|
||||
bold
|
||||
bold,
|
||||
kern: FONT_KERN_MIN_1PT
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -360,7 +558,8 @@ function htmlToHeaderTextRuns(html) {
|
||||
text: formatHeaderWordSpacing(html),
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE,
|
||||
bold: true
|
||||
bold: true,
|
||||
kern: FONT_KERN_MIN_1PT
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -525,7 +724,8 @@ function buildTableWordChildren(processedItem, options) {
|
||||
bold: true,
|
||||
color: TITLE_COLOR,
|
||||
font: FONT_NAME,
|
||||
size: TITLE_FONT_SIZE
|
||||
size: TITLE_FONT_SIZE,
|
||||
kern: FONT_KERN_MIN_1PT
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -601,7 +801,8 @@ function sanitizeFileName(name) {
|
||||
|
||||
export async function downloadTableWord(processedItem, fileName) {
|
||||
const doc = buildTableWordDocument(processedItem);
|
||||
const blob = await Packer.toBlob(doc);
|
||||
let blob = await Packer.toBlob(doc);
|
||||
blob = await patchBodyTableCaptionCenter(blob);
|
||||
saveAs(blob, `${sanitizeFileName(fileName || processedItem.title)}.docx`);
|
||||
}
|
||||
|
||||
@@ -620,19 +821,26 @@ export async function downloadAllTablesWord(items, fileName) {
|
||||
}
|
||||
|
||||
const doc = buildAllTablesWordDocument(exportItems);
|
||||
const blob = await Packer.toBlob(doc);
|
||||
let blob = await Packer.toBlob(doc);
|
||||
blob = await patchBodyTableCaptionCenter(blob);
|
||||
saveAs(blob, `${sanitizeFileName(fileName || 'tables')}.docx`);
|
||||
}
|
||||
|
||||
export {
|
||||
appendFigureTitleParagraphs,
|
||||
appendTableBlockEmptyLines,
|
||||
buildTableWordChildren,
|
||||
createFigureTitleParagraph,
|
||||
createTableParagraph,
|
||||
createWordDocument,
|
||||
ensureFigureTitleTrailingPeriod,
|
||||
FONT_KERN_MIN_1PT,
|
||||
FONT_KERN_MIN_XIAO_ER,
|
||||
FONT_NAME,
|
||||
htmlToPlainText,
|
||||
htmlToTextRuns,
|
||||
PAGE_MARGINS,
|
||||
patchBodyTableCaptionCenter,
|
||||
splitHtmlSegments,
|
||||
TABLE_FONT_SIZE,
|
||||
TITLE_COLOR,
|
||||
|
||||
Reference in New Issue
Block a user