相关性代码调整

This commit is contained in:
2026-06-24 16:52:24 +08:00
parent 5a1bbb0894
commit 5260ca8ea5
29 changed files with 6330 additions and 568 deletions

View File

@@ -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(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&amp;/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,