审稿人证书生成

This commit is contained in:
2026-07-10 09:12:09 +08:00
parent 262e634f85
commit 3448ffe961
29 changed files with 7825 additions and 492 deletions

View File

@@ -0,0 +1,485 @@
import { htmlToPlainText } from '@/utils/manuscriptCitationRelevance';
import { parseReferencesBulkContent } from '@/utils/manuscriptReferenceHtml';
import { extractReferencesSectionFromWordHtml, fetchWordReferencesSection } from '@/utils/manuscriptWordReferences';
import { importWordDocumentWithMath } from '@/utils/wordMathImport';
import { applyDealContentToFields, parseReferenceFieldsFromHtml, normalizeImportedReferenceFields, normalizeImportedReferenceText, finalizeReferenceFields } from '@/utils/parseReferenceFields';
function mapUploadedItemsToRows(items) {
return (items || []).map(function (item, index) {
const referType = String(item.refer_type || 'journal').toLowerCase();
const fields = parseReferenceFieldsFromHtml(item.html, referType);
return {
index: index + 1,
html: item.html || '',
refer_type: fields ? fields.refer_type : referType,
fields: fields
};
});
}
/** 解析本地 Word 文件中的 References 条目 */
export async function parseLocalWordReferencesFile(file) {
if (!file) {
return { found: false, rows: [], count: 0 };
}
const html = await importWordDocumentWithMath(file, { keepReferences: true });
const extracted = extractReferencesSectionFromWordHtml(html);
if (!extracted.found) {
return { found: false, rows: [], count: 0 };
}
const items = parseReferencesBulkContent(extracted.html);
const rows = mapUploadedItemsToRows(items);
return {
found: rows.length > 0,
rows: rows,
count: rows.length
};
}
/** 从 gridData 稿件路径下载并解析 References */
export async function parseManuscriptWordReferences(apiClient, options) {
const extracted = await fetchWordReferencesSection(apiClient, options || {});
if (!extracted.found) {
return { found: false, rows: [], count: 0 };
}
const items = parseReferencesBulkContent(extracted.html);
const rows = mapUploadedItemsToRows(items);
return {
found: rows.length > 0,
rows: rows,
count: rows.length
};
}
function previewExistingReference(row) {
if (!row) return '';
if (row.refer_frag) {
return htmlToPlainText(row.refer_frag).replace(/\s+/g, ' ').trim();
}
const parts = [];
if (row.author) parts.push(row.author);
if (row.title) parts.push(row.title);
if (row.joura) parts.push(row.joura);
if (row.dateno) parts.push(row.dateno);
if (row.doilink) parts.push('Available at: ' + row.doilink);
return parts.join(' ').replace(/\s+/g, ' ').trim();
}
function normalizeWordRefBatchRow(row) {
if (!row) return row;
let normalized = normalizeImportedReferenceFields(row);
if (normalized.doilink) {
normalized.doilink = normalizeDoilinkFull(normalized.doilink);
normalized.doi = normalizeDoiValue(normalized.doilink);
}
if (normalized.dateno) {
normalized.dateno = normalized.dateno.replace(/^(\d{4}):/, '$1;');
}
normalized = finalizeReferenceFields(normalized, row.wordHtml || normalized.content || '');
return Object.assign({}, row, normalized);
}
/** 按序号将 Word 条目与现有参考文献一一对应 */
export function buildWordRefBatchRows(existingRows, uploadedRows) {
const existing = existingRows || [];
const uploaded = uploadedRows || [];
const maxLen = Math.max(existing.length, uploaded.length);
const rows = [];
for (let i = 0; i < maxLen; i++) {
const existingRow = existing[i] || null;
const uploadedRow = uploaded[i] || null;
const fields = (uploadedRow && uploadedRow.fields) || null;
const prevExistingRow = i > 0 ? existing[i - 1] : null;
rows.push(
normalizeWordRefBatchRow({
index: i + 1,
apply: !!(uploadedRow && fields),
p_refer_id: existingRow ? existingRow.p_refer_id : null,
pre_p_refer_id: existingRow
? existingRow.pre_p_refer_id
: prevExistingRow
? prevExistingRow.p_refer_id
: null,
existingType: existingRow ? existingRow.refer_type : '',
existingPreview: previewExistingReference(existingRow),
refer_type: fields ? fields.refer_type : uploadedRow ? uploadedRow.refer_type : 'journal',
author: fields ? fields.author : '',
title: fields ? fields.title : '',
joura: fields ? fields.joura : '',
dateno: fields ? fields.dateno : '',
doilink: fields ? fields.doilink : '',
doi: fields ? fields.doi || fields.doilink : '',
isbn: fields ? fields.isbn : '',
content: fields ? fields.content : '',
wordHtml: uploadedRow ? uploadedRow.html : '',
unmatched: !existingRow || !uploadedRow,
missingExisting: !existingRow,
missingUploaded: !uploadedRow
})
);
}
return rows;
}
/** 调用 dealContent 精修字段拆解 */
export async function enrichWordRefBatchRowsWithDealContent(apiClient, rows) {
const list = rows || [];
const updated = [];
for (let i = 0; i < list.length; i++) {
const row = list[i];
if (!row || !row.wordHtml) {
updated.push(normalizeWordRefBatchRow(row));
continue;
}
try {
const content = normalizeImportedReferenceText(htmlToPlainText(row.wordHtml));
const res = await apiClient.post('api/References/dealContent', { content: content });
const data = res && res.data ? res.data : null;
if (data) {
const merged = applyDealContentToFields(
{
refer_type: row.refer_type,
author: row.author,
title: row.title,
joura: row.joura,
dateno: row.dateno,
doilink: row.doilink,
doi: row.doi || row.doilink,
isbn: row.isbn,
content: row.content
},
data
);
const nextRow = normalizeWordRefBatchRow(
Object.assign({}, row, merged, {
refer_type: merged.refer_type || row.refer_type,
doi: merged.doi || merged.doilink || row.doi || row.doilink
})
);
updated.push(nextRow);
continue;
}
} catch (err) {
// 保留本地拆解结果
}
updated.push(normalizeWordRefBatchRow(row));
}
return updated;
}
function normalizeDoiValue(doilink) {
let raw = String(doilink || '').trim();
if (!raw) return '';
raw = raw.replace(/^https?:\/\/doi\.org\//i, '');
raw = raw.replace(/^doi:/i, '');
return raw.toLowerCase();
}
function normalizeDoilinkFull(doilink) {
const raw = String(doilink || '').trim();
if (!raw) return '';
if (/^https?:\/\//i.test(raw)) return raw;
const doi = normalizeDoiValue(raw);
return doi ? 'https://doi.org/' + doi : raw;
}
function appendReferPart(content, part) {
const piece = String(part || '').trim();
if (!piece) return content;
let out = String(content || '').trim();
if (!out) return piece;
if (out.endsWith('.')) {
return out + piece;
}
return out + '.' + piece;
}
/** journal 的 content 字段,与 editRefer 接口一致 */
export function buildJournalReferContent(fields) {
const author = String((fields && fields.author) || '').trim();
const title = String((fields && fields.title) || '').trim();
const joura = String((fields && fields.joura) || '').trim();
const dateno = String((fields && fields.dateno) || '').trim();
const doilink = normalizeDoilinkFull(fields && fields.doilink);
let content = author;
if (title) {
if (!content) {
content = title;
} else if (content.endsWith('.')) {
content = content + ' ' + title;
} else {
content = content + '. ' + title;
}
}
if (joura) {
content = content + ' ' + joura;
if (!/\.\s*$/.test(joura)) {
content = content + '.';
}
}
if (dateno) {
const datenoForContent = dateno.replace(/^(\d{4});/, '$1:');
content = appendReferPart(content, datenoForContent);
}
if (doilink) {
content = content + '.Available at: ' + doilink;
}
return content;
}
/** book 的 content 字段 */
export function buildBookReferContent(fields) {
const author = String((fields && fields.author) || '').trim();
const title = String((fields && fields.title) || '').trim();
const dateno = String((fields && fields.dateno) || '').trim();
const isbn = String((fields && fields.isbn) || '').trim();
let content = author;
if (title) {
content = appendReferPart(content, title);
}
if (dateno) {
content = appendReferPart(content, dateno);
}
if (isbn) {
content = content + '. Available at: ISBN: ' + isbn;
}
return content;
}
export function buildEditReferPayload(row, pArticleId) {
const normalized = normalizeWordRefBatchRow(row || {});
const referType = String(normalized.refer_type || 'journal').toLowerCase();
const base = {
p_article_id: pArticleId != null && pArticleId !== '' ? pArticleId : '',
p_refer_id: normalized.p_refer_id,
pre_p_refer_id: normalized.pre_p_refer_id != null && normalized.pre_p_refer_id !== '' ? normalized.pre_p_refer_id : ''
};
if (referType === 'book') {
return Object.assign(base, {
doi: '',
refer_type: 'book',
author: normalized.author || '',
title: normalized.title || '',
dateno: normalized.dateno || '',
isbn: normalized.isbn || '',
joura: '',
doilink: '',
content: buildBookReferContent(normalized)
});
}
if (referType === 'other') {
return Object.assign(base, {
doi: normalizeDoiValue(normalized.doilink || normalized.doi),
refer_type: 'other',
author: '',
title: '',
dateno: '',
joura: '',
doilink: normalizeDoilinkFull(normalized.doilink) || '',
isbn: '',
content: normalized.content || ''
});
}
const doilink = normalizeDoilinkFull(normalized.doilink);
const doi = normalizeDoiValue(normalized.doilink || normalized.doi);
return Object.assign(base, {
doi: doi,
refer_type: 'journal',
author: normalized.author || '',
title: normalized.title || '',
joura: normalized.joura || '',
dateno: normalized.dateno || '',
doilink: doilink,
isbn: '',
content: buildJournalReferContent(normalized)
});
}
export function extractAddedReferId(data) {
if (!data || typeof data !== 'object') {
return '';
}
if (data.p_refer_id != null && data.p_refer_id !== '') {
return String(data.p_refer_id);
}
return '';
}
/** 新增参考文献 payloadp_refer_id 留空pre_p_refer_id 指向前一条 */
export function buildAddReferPayload(row, pArticleId, preReferId) {
const payload = buildEditReferPayload(row, pArticleId);
delete payload.p_refer_id;
payload.pre_p_refer_id = preReferId != null && preReferId !== '' ? String(preReferId) : '';
return payload;
}
export function canWordRefBatchRowSave(row) {
if (!row || row.missingUploaded) {
return false;
}
if (row.p_refer_id) {
return true;
}
return !!row.missingExisting;
}
const WORD_REF_BATCH_FIELD_DEFS = {
journal: ['author', 'title', 'joura', 'dateno', 'doilink'],
book: ['author', 'title', 'dateno', 'isbn'],
other: ['content']
};
const WORD_REF_BATCH_REQUIRED_FIELDS = {
journal: ['title', 'joura', 'dateno'],
book: ['author', 'title', 'dateno'],
other: ['content']
};
export function getWordRefBatchFieldKeys(referType) {
const type = String(referType || 'journal').toLowerCase();
return (WORD_REF_BATCH_FIELD_DEFS[type] || WORD_REF_BATCH_FIELD_DEFS.journal).slice();
}
export function isWordRefBatchRowParseIncomplete(row) {
if (!row || row.missingUploaded) {
return false;
}
const type = String(row.refer_type || 'journal').toLowerCase();
const required = WORD_REF_BATCH_REQUIRED_FIELDS[type] || WORD_REF_BATCH_REQUIRED_FIELDS.journal;
return required.some(function (key) {
return !String(row[key] || '').trim();
});
}
export function stripWordRefBatchHtmlPreview(html) {
return htmlToPlainText(String(html || ''))
.replace(/\s+/g, ' ')
.trim();
}
/** 切换为 other 时:用 Word 原文(或已拆字段拼接)填入 content */
export function buildWordRefBatchContentFromRow(row) {
if (!row) {
return '';
}
const fromWord = stripWordRefBatchHtmlPreview(row.wordHtml);
if (fromWord) {
return normalizeImportedReferenceText(fromWord);
}
const parts = [];
if (row.author) {
parts.push(String(row.author).trim());
}
if (row.title) {
parts.push(String(row.title).trim());
}
if (row.joura) {
parts.push(String(row.joura).trim());
}
if (row.dateno) {
parts.push(String(row.dateno).trim());
}
let content = parts
.filter(Boolean)
.join(' ')
.replace(/\s+\./g, '.')
.replace(/\s+/g, ' ')
.trim();
const doilink = String(row.doilink || '').trim();
if (doilink && !/Available at:/i.test(content)) {
content = content + '. Available at: ' + doilink;
}
return normalizeImportedReferenceText(content || row.content || '');
}
export function applyWordRefBatchOtherType(row) {
if (!row) {
return row;
}
row.refer_type = 'other';
row.content = buildWordRefBatchContentFromRow(row);
const plain = String(row.content || '');
const urlMatch = plain.match(/Available at:\s*(https?:\/\/\S+)/i);
if (urlMatch) {
row.doilink = urlMatch[1].replace(/[.,;)\]]+$/, '');
}
return row;
}
/**
* 按序号批量保存:已有条目走 editReferWord 多出的条目走 addReferByParticleid
* 每条成功后用返回/已有的 p_refer_id 作为下一条的 pre_p_refer_id。
*/
export async function saveWordRefBatchRows(apiClient, options) {
const opts = options || {};
const rows = (opts.rows || [])
.filter(function (row) {
return row && row.apply && canWordRefBatchRowSave(row);
})
.sort(function (a, b) {
return (a.index || 0) - (b.index || 0);
});
const pArticleId = opts.pArticleId;
const useUserAdd = opts.role === 'user';
const addEndpoint = useUserAdd ? 'api/Preaccept/addRefer' : 'api/Preaccept/addReferByParticleid';
let lastReferId = null;
let ok = 0;
let fail = 0;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
try {
if (row.p_refer_id) {
const payload = buildEditReferPayload(row, pArticleId);
if (lastReferId != null && lastReferId !== '') {
payload.pre_p_refer_id = lastReferId;
}
const res = await apiClient.post('api/Preaccept/editRefer', payload);
if (res && res.code == 0) {
ok += 1;
lastReferId = String(row.p_refer_id);
} else {
fail += 1;
}
continue;
}
if (row.missingExisting) {
const payload = buildAddReferPayload(row, pArticleId, lastReferId);
if (useUserAdd && opts.articleId != null && opts.articleId !== '') {
payload.article_id = opts.articleId;
}
const res = await apiClient.post(addEndpoint, payload);
if (res && res.code == 0) {
ok += 1;
const newId = extractAddedReferId(res.data);
if (newId) {
lastReferId = newId;
}
} else {
fail += 1;
}
continue;
}
fail += 1;
} catch (err) {
fail += 1;
}
}
return { ok: ok, fail: fail };
}