This commit is contained in:
2026-04-08 13:03:54 +08:00
parent 36f6c02376
commit 0d35b76c3a
11 changed files with 3866 additions and 589 deletions

View File

@@ -71,7 +71,51 @@ function findExtentElement(blipElement) {
export default {
async searchTitleByDOI(doi) {
if (!doi) {
this.$message.warning('Please enter a DOI');
return null;
}
// 开启全局 Loading防止编辑重复点击
try {
// 1. 数据清洗
const cleanDoi = doi.trim()
.replace(/^doi:/i, '')
.replace(/https?:\/\/doi\.org\//i, '');
// 2. 请求 Crossref 接口
const response = await fetch(`https://api.crossref.org/works/${encodeURIComponent(cleanDoi)}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (!response.ok) throw new Error('DOI not found');
const data = await response.json();
// 3. 提取标题并跳转
const title = data.message?data.message.title?data.message.title[0]:'':'';
if (title) {
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(title)}`;
window.open(searchUrl, '_blank');
return title;
} else {
this.$message.error('Title not found in metadata.');
}
} catch (error) {
console.error("DOI Retrieval Error:", error);
this.$message.error('Failed to fetch title. Please check the DOI.');
} finally {
}
return null;
},
getJournalTypeName(value) {
var list = JSON.parse(localStorage.getItem('journalTypeDataAll'));
@@ -916,9 +960,9 @@ str = str.replace(regex, function (match, content, offset, fullString) {
});
// 2. 删除所有不需要的标签 (除 `strong`, `em`, `sub`, `sup`, `b`, `i` 外的所有标签)
if (type == 'table') {
inputHtml = inputHtml.replace(/<(?!\/?(strong|em|sub|sup|b|i|blue|wmath|img|myfigure|mytable|myh3|autocite))[^>]+>/g, ''); // 删除不需要的标签
inputHtml = inputHtml.replace(/<(?!\/?(strong|em|sub|sup|b|i|blue|wmath|img|myfigure|mytable|myh3|mycite))[^>]+>/g, ''); // 删除不需要的标签
} else {
inputHtml = inputHtml.replace(/<(?!\/?(strong|em|sub|sup|b|i|blue|wmath|myfigure|mytable|myh3|autocite))[^>]+>/g, ''); // 删除不需要的标签
inputHtml = inputHtml.replace(/<(?!\/?(strong|em|sub|sup|b|i|blue|wmath|myfigure|mytable|myh3|mycite))[^>]+>/g, ''); // 删除不需要的标签
}

View File

@@ -1160,6 +1160,7 @@ colTitle: 'Template title',
},
wordCite: {
noRefs: 'Reference list is not loaded yet. Please wait or refresh the page.',
missingBoundRef: 'The bound reference no longer exists. Please cite again.',
notFoundById: 'No reference found for citation ID {id}',
selectRef: 'Select Reference',
reference: 'Reference',
@@ -1168,8 +1169,15 @@ colTitle: 'Template title',
remove: 'Remove',
selected: 'Selected',
modifyRef: 'Edit citation',
removeRefTag: 'Remove citation',
citeUpdateFail: 'Could not update the citation in the text. Try again or use Edit.'
removeRefTag: 'Reference remove',
citeUpdateFail: 'Could not update the citation in the text. Try again or use Edit.',
matchBracketRefs: 'Auto-link References',
matchBracketRefsDone: 'Converted {n} bracket citation(s) to autocite.',
matchBracketRefsNone: "No active citation links were detected in the text. To enable automatic numbering, please use the 'Reference' tool to link your sources.",
removeRefNeedClickCite: 'Click a citation in the text first, then choose Reference remove.',
quickPickPlaceholder: 'Enter e.g. [5, 6, 10-15] to auto-select',
quickPickApply: 'Link References',
quickPickClear: 'Clear selection'
}

View File

@@ -1145,6 +1145,7 @@ const zh = {
},
wordCite: {
noRefs: '参考文献列表尚未加载,请稍候再试或刷新页面',
missingBoundRef: '当前绑定的参考文献不存在,请重新引用',
notFoundById: '未查询到编号{id}相关参考文献',
selectRef: '选择参考文献',
reference: '参考文献',
@@ -1153,8 +1154,15 @@ const zh = {
remove: '移除',
selected: '已选择',
modifyRef: '修改引用',
removeRefTag: '移除引用',
citeUpdateFail: '未能更新正文中的引用标签,请重试或进入编辑修改'
removeRefTag: '移除参考文献',
citeUpdateFail: '未能更新正文中的引用标签,请重试或进入编辑修改',
matchBracketRefs: '自动链接参考文献',
matchBracketRefsDone: '已转换 {n} 处 [n] 为可点击角标',
matchBracketRefsNone: '正文中未检测到可转换的纯文本引用 [n]。若要自动编号并关联参考文献请使用工具栏中的「Reference」插入引用。',
removeRefNeedClickCite: '请先在正文中点击要删除的引用角标,再点「移除参考文献」。',
quickPickPlaceholder: '输入如 [5, 6, 10-15] 自动勾选对应参考文献',
quickPickApply: '链接参考文献',
quickPickClear: '清空勾选'
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,17 @@
<template>
<div class="tinymce-container editor-container">
<textarea class="tinymce-textarea" :id="tinymceId"></textarea>
<div class="tinymce-editor-surface">
<textarea class="tinymce-textarea" :id="tinymceId"></textarea>
</div>
<div
v-if="showRefButton && hasReferencesForAutoLink"
ref="autoLinkFooterWrap"
class="tinymce-autolink-footer"
>
<span @click="handleAutoLinkRefsClick" style="cursor: pointer;color: #409eff;font: 12px;font-weight: 700;">
<i class="el-icon-link"></i> {{ $t('wordCite.matchBracketRefs') }}
</span>
</div>
</div>
</template>
<script>
@@ -67,17 +78,57 @@ export default {
chanFerForm: {
type: Array,
default: () => []
},
/** 由父级根据全文(稿面 + Edit Content 草稿合并)扫描得到,与 word.vue citeMap 一致 */
bodyCiteIdOrder: {
type: Array,
default: () => []
},
/** 为 false 时不显示 Ref 工具栏按钮,也不自动在 LateX 后注入 insertRef用于图表标题等 */
showRefButton: {
type: Boolean,
default: true
}
},
computed: {
citeMap() {
const map = {};
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
refs.forEach((row, idx) => {
const key = row && row.p_refer_id != null ? String(row.p_refer_id) : '';
if (key) map[key] = idx + 1;
const refIdSet = new Set(
refs.map((r) => (r && r.p_refer_id != null ? String(r.p_refer_id) : '')).filter(Boolean)
);
if (!Array.isArray(this.bodyCiteIdOrder) || this.bodyCiteIdOrder.length === 0) {
const map = {};
refs.forEach((row, idx) => {
const key = row && row.p_refer_id != null ? String(row.p_refer_id) : '';
if (key) map[key] = idx + 1;
});
return map;
}
const orderedIds = this.bodyCiteIdOrder.map(String);
const filtered = [];
orderedIds.forEach((id) => {
if (refIdSet.has(id) && !filtered.includes(id)) filtered.push(id);
});
const map = {};
filtered.forEach((id, idx) => {
map[id] = idx + 1;
});
let next = filtered.length + 1;
refs.forEach((r) => {
const key = r && r.p_refer_id != null ? String(r.p_refer_id) : '';
if (!key || map[key] != null) return;
map[key] = next++;
});
return map;
},
/** 有参考文献列表时才显示「自动链接」:与 insertRef 一致,依赖 chanFerForm 中有效 p_refer_id */
hasReferencesForAutoLink() {
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
return refs.some((r) => r && r.p_refer_id != null && String(r.p_refer_id).trim() !== '');
}
},
data() {
@@ -139,7 +190,9 @@ export default {
hasChange: false,
hasInit: false,
editorInstance: null,
tinymceId: this.id || 'vue-tinymce-' + +new Date()
tinymceId: this.id || 'vue-tinymce-' + +new Date(),
/** 将底部栏移入 TinyMCE 容器前,记录原 DOM 位置以便销毁时移回 */
_autoLinkFooterRestore: null
};
},
watch: {
@@ -162,6 +215,27 @@ export default {
});
},
deep: true
},
bodyCiteIdOrder: {
handler() {
this.$nextTick(() => {
if (this.editorInstance) {
this.renderAutociteInEditor(this.editorInstance);
}
});
},
deep: true
},
hasReferencesForAutoLink(val) {
if (!val) {
this.restoreAutoLinkFooterDom();
return;
}
if (this.showRefButton && this.editorInstance) {
this.$nextTick(() => {
this.mountAutoLinkFooterInsideEditor(this.editorInstance);
});
}
}
},
mounted() {
@@ -185,24 +259,6 @@ export default {
this.destroyTinymce();
},
methods: {
/** 与正文一致:两项连续写作 1, 2三项及以上连续写作 1-3 */
formatCiteNumbers(nums) {
if (!nums || !nums.length) return '';
const sorted = [...new Set(nums)].sort((a, b) => a - b);
const result = [];
let i = 0;
while (i < sorted.length) {
let j = i;
while (j < sorted.length - 1 && sorted[j + 1] === sorted[j] + 1) j++;
if (j - i >= 2) {
result.push(`${sorted[i]}${sorted[j]}`);
} else {
for (let k = i; k <= j; k++) result.push(sorted[k]);
}
i = j + 1;
}
return result.join(', ');
},
parseAutociteDataIds(attr) {
if (!attr || typeof attr !== 'string') return [];
return attr
@@ -225,10 +281,205 @@ export default {
return String(a).localeCompare(String(b));
});
},
/** 同 word.vue多 id 均在列表中时对全局序号做 13 区间合并 */
formatCiteNumbers(nums) {
if (!nums || !nums.length) return '';
const sorted = [...new Set(nums)].sort((a, b) => a - b);
const result = [];
let i = 0;
while (i < sorted.length) {
let j = i;
while (j < sorted.length - 1 && sorted[j + 1] === sorted[j] + 1) j++;
if (j - i >= 2) {
result.push(`${sorted[i]}${sorted[j]}`);
} else {
for (let k = i; k <= j; k++) result.push(sorted[k]);
}
i = j + 1;
}
return result.join(', ');
},
/** 全局角标序号 n → p_refer_id用于将正文中的 [n] 转为 mycite */
buildNumToRefIdMap() {
const citeMap = this.citeMap || {};
const map = {};
Object.keys(citeMap).forEach((id) => {
const n = citeMap[id];
if (n != null && n !== '' && !Number.isNaN(Number(n))) {
map[Number(n)] = String(id);
}
});
return map;
},
/** 解析 [1]、[1,2]、[14] 等括号内数字列表(不含方括号) */
parseBracketInnerToNumbers(inner) {
if (!inner || typeof inner !== 'string') return [];
const t = inner.trim().replace(//g, ',');
const range = t.match(/^(\d+)\s*[-–—]\s*(\d+)$/);
if (range) {
const a = Number(range[1]);
const b = Number(range[2]);
if (Number.isNaN(a) || Number.isNaN(b)) return [];
const lo = Math.min(a, b);
const hi = Math.max(a, b);
const out = [];
for (let i = lo; i <= hi; i++) out.push(i);
return out;
}
if (/[,]/.test(t)) {
return t
.split(/[,]/)
.map((x) => parseInt(String(x).trim(), 10))
.filter((n) => !Number.isNaN(n));
}
const n = parseInt(t, 10);
return Number.isNaN(n) ? [] : [n];
},
/**
* 将编辑器内纯文本形式的 [1]、[13]、[1, 2] 按当前 citeMap 转为 <mycite>(跳过已有 mycite/wmath 内文字)
* @returns {{ replaced: number }}
*/
convertPlainBracketCitesToAutocite() {
const ed = this.editorInstance;
if (!ed) return { replaced: 0 };
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
if (!refs.length) {
this.$message.warning(this.$t('wordCite.noRefs'));
return { replaced: 0 };
}
const numToId = this.buildNumToRefIdMap();
if (Object.keys(numToId).length === 0) {
this.$message.warning(this.$t('wordCite.noRefs'));
return { replaced: 0 };
}
const doc = ed.getDoc();
const body = ed.getBody();
if (!body) return { replaced: 0 };
let replaced = 0;
ed.undoManager.transact(() => {
replaced = this._replaceBracketCitesInNode(body, doc, numToId);
});
this.renderAutociteInEditor(ed);
ed.fire('change');
return { replaced };
},
/** 底部「自动链接参考文献」按钮,与原先工具栏 autoLinkRefs 行为一致 */
handleAutoLinkRefsClick() {
const r = this.convertPlainBracketCitesToAutocite();
if (!r) return;
if (r.replaced > 0) {
this.$message.success(this.$t('wordCite.matchBracketRefsDone', { n: r.replaced }));
return;
}
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
const numToId = this.buildNumToRefIdMap();
if (refs.length && Object.keys(numToId).length) {
this.$message.info(this.$t('wordCite.matchBracketRefsNone'));
}
},
/** 把「自动链接」栏挂到 TinyMCE 外壳 .tox-tinymce 内部底部,与工具栏/编辑区同一外框 */
mountAutoLinkFooterInsideEditor(ed) {
if (!this.showRefButton || !this.hasReferencesForAutoLink) return;
this.$nextTick(() => {
const el = this.$refs.autoLinkFooterWrap;
if (!el || !ed) return;
const container = ed.getContainer && ed.getContainer();
if (!container) return;
if (el.parentNode === container) return;
this._autoLinkFooterRestore = {
parent: el.parentNode,
next: el.nextSibling
};
container.appendChild(el);
});
},
/** 销毁编辑器前必须移回,否则节点会随 .tox-tinymce 一起被移除 */
restoreAutoLinkFooterDom() {
const el = this.$refs.autoLinkFooterWrap;
const r = this._autoLinkFooterRestore;
this._autoLinkFooterRestore = null;
if (!el || !r || !r.parent) return;
try {
if (r.next && r.next.parentNode === r.parent) {
r.parent.insertBefore(el, r.next);
} else {
r.parent.appendChild(el);
}
} catch (e) {
/* ignore */
}
},
_replaceBracketCitesInNode(node, doc, numToId) {
if (node.nodeType === 3) {
return this._processTextNodeForBracketCites(node, doc, numToId);
}
if (node.nodeType !== 1) return 0;
const name = node.nodeName;
if (name === 'AUTOCITE' || name === 'WMATH' || name === 'SCRIPT' || name === 'STYLE') {
return 0;
}
let total = 0;
const children = Array.from(node.childNodes);
for (let i = 0; i < children.length; i++) {
total += this._replaceBracketCitesInNode(children[i], doc, numToId);
}
return total;
},
_processTextNodeForBracketCites(textNode, doc, numToId) {
const text = textNode.textContent;
const re = /\[([\d\s,\-–—]+)\]/g;
let m;
let lastIndex = 0;
const pieces = [];
let replaced = 0;
while ((m = re.exec(text)) !== null) {
const nums = this.parseBracketInnerToNumbers(m[1]);
const ids = [];
const seen = new Set();
nums.forEach((n) => {
const id = numToId[n];
if (id && !seen.has(id)) {
seen.add(id);
ids.push(id);
}
});
pieces.push({ type: 'text', s: text.slice(lastIndex, m.index) });
if (ids.length > 0) {
const sorted = this.sortAutociteIdsByCiteNumber(ids);
pieces.push({ type: 'cite', ids: sorted });
replaced++;
} else {
pieces.push({ type: 'text', s: m[0] });
}
lastIndex = m.index + m[0].length;
}
if (replaced === 0) return 0;
pieces.push({ type: 'text', s: text.slice(lastIndex) });
const parent = textNode.parentNode;
if (!parent) return 0;
const frag = doc.createDocumentFragment();
pieces.forEach((p) => {
if (p.type === 'text') {
frag.appendChild(doc.createTextNode(p.s));
} else {
const ac = doc.createElement('mycite');
ac.setAttribute('data-id', p.ids.join(','));
ac.setAttribute('contenteditable', 'false');
ac.appendChild(doc.createTextNode('\u200b'));
frag.appendChild(ac);
}
});
parent.replaceChild(frag, textNode);
return replaced;
},
renderAutociteInEditor(ed) {
const body = ed.getBody();
if (!body) return;
const allAutocites = Array.from(body.querySelectorAll('autocite'));
const allAutocites = Array.from(
ed.dom && typeof ed.dom.select === 'function'
? ed.dom.select('mycite', body)
: body.querySelectorAll('mycite')
);
if (!allAutocites.length) return;
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
@@ -238,25 +489,63 @@ export default {
if (key) refMap[key] = item;
});
const citeMap = this.citeMap || {};
allAutocites.forEach((el) => {
ed.dom.setAttrib(el, 'contenteditable', 'false');
el.style.display = '';
const ids = this.parseAutociteDataIds(el.getAttribute('data-id'));
const sortedIds = this.sortAutociteIdsByCiteNumber(ids);
const nums = sortedIds.map((id) => this.citeMap[String(id)]).filter((n) => n != null);
const label = nums.length > 0 ? this.formatCiteNumbers(nums) : '?';
const sortedAll = this.sortAutociteIdsByCiteNumber(
this.parseAutociteDataIds(el.getAttribute('data-id'))
);
/** 已删除的文献 id 从角标中忽略,并写回 data-id仅保留仍存在于列表中的 id */
const validIds = sortedAll.filter((id) => refMap[String(id)]);
if (validIds.length < sortedAll.length) {
ed.dom.setAttrib(el, 'data-id', validIds.length ? validIds.join(',') : '');
}
const sortedIds = validIds.length ? this.sortAutociteIdsByCiteNumber(validIds) : [];
const lines = sortedIds.map((id) => {
const no = this.citeMap[String(id)] != null ? this.citeMap[String(id)] : '?';
/** 角标只展示仍绑定且已有全局序号的文献 */
const parts = sortedIds.map((id) => {
const ref = refMap[String(id)];
if (!ref) return this.$t('wordCite.notFoundById', { id: no === '?' ? id : no });
const content = ref.refer_frag || [ref.author, ref.title, ref.joura, ref.dateno].filter(Boolean).join(' ').trim() || '[?]';
const doi = ref.doilink || ref.isbn || ref.doi || '[?]';
return `[${no}] ${content}\nDOI: ${doi}`;
const no = ref ? citeMap[String(id)] : null;
const num = no != null && no !== '' ? String(no) : null;
return { id, ref, num };
});
const numsForLabel = parts
.map((p) => p.num)
.filter((n) => n != null && n !== '')
.map(Number);
const label =
numsForLabel.length > 0 ? this.formatCiteNumbers(numsForLabel) : '';
const lines = parts
.filter((p) => p.ref)
.map((p) => {
const ref = p.ref;
const content =
ref.refer_frag ||
[ref.author, ref.title, ref.joura, ref.dateno].filter(Boolean).join(' ').trim() ||
'';
const doi = ref.doilink || ref.isbn || ref.doi || '';
const numLabel = p.num;
if (numLabel) {
return `[${numLabel}] ${content}\nDOI: ${doi}`;
}
return `${content}\nDOI: ${doi}`;
});
/** 无可展示序号或全部 id 已失效时,直接移除节点,不保留空标签 */
if (!label) {
ed.dom.remove(el);
return;
}
el.textContent = `[${label}]`;
el.style.display = '';
ed.dom.setAttrib(el, 'title', lines.join('\n'));
/** 不再使用 data-cite-missing 红底提示,角标样式与普通引用一致 */
ed.dom.setAttrib(el, 'data-cite-missing', null);
});
this.padAutociteCaretPlaceholder(ed);
},
@@ -265,7 +554,7 @@ export default {
const doc = ed.getDoc();
const body = doc.body;
if (!body) return;
body.querySelectorAll('autocite').forEach((el) => {
body.querySelectorAll('mycite').forEach((el) => {
const next = el.nextSibling;
if (next === null) {
el.parentNode.appendChild(doc.createTextNode('\u200b'));
@@ -294,7 +583,7 @@ export default {
if (this._refBookmark) {
ed.selection.moveToBookmark(this._refBookmark);
}
ed.insertContent(`<autocite data-id="${escaped}" contenteditable="false"></autocite>&#8203;`);
ed.insertContent(`<mycite data-id="${escaped}" contenteditable="false"></mycite>&#8203;`);
}
this.renderAutociteInEditor(ed);
},
@@ -306,7 +595,7 @@ export default {
ed.fire('change');
},
/**
* 从当前编辑的 autocite 中去掉指定 p_refer_id去掉后无 id 则删除整段标签。
* 从当前编辑的 mycite 中去掉指定 p_refer_id去掉后无 id 则删除整段标签。
* ids 为空:删除整段(与 removeAutocite 一致)。
*/
stripAutociteIds(idsToRemove) {
@@ -331,13 +620,14 @@ export default {
}
ed.fire('change');
},
/** TinyMCE 会剔除「空」的行内标签;空 autocite 必须在入编辑器前占位,否则合并引用 [13] 等整段消失 */
/** TinyMCE 会剔除「空」的行内标签;空 mycite 必须在入编辑器前占位,否则合并引用 [13] 等整段消失 */
normalizeAutociteHtmlForEditor(html) {
if (!html || typeof html !== 'string') return html;
let out = html.replace(/<autocite([^>]*)>\s*<\/autocite>/gi, '<autocite$1>&#8203;</autocite>');
// 与 getSafeContent 标签边界逻辑一致autocite 左右水平空白改为 &nbsp;,不换行符(避免吃掉段间 \n
out = out.replace(/[^\S\r\n]+(?=<autocite\b)/gi, '&nbsp;');
out = out.replace(/(?<=<\/autocite>)[^\S\r\n]+/gi, '&nbsp;');
/** 角标显示一律由 renderAutociteInEditor 根据 data-id 生成,禁止保留库内遗留的 [1-4]、[14] 等旧文案 */
let out = html.replace(/<mycite([^>]*)>[\s\S]*?<\/mycite>/gi, '<mycite$1>&#8203;</mycite>');
// 外侧:连续空格 / &nbsp; 合并为单个 &nbsp;,避免「普通空格 + &nbsp;」叠成大缝
out = out.replace(/(?:\s|&nbsp;|&#160;)+(?=<mycite\b)/gi, '&nbsp;');
out = out.replace(/(?<=<\/mycite>)(?:\s|&nbsp;|&#160;)+/gi, '&nbsp;');
return out;
},
handleSetContent(val) {
@@ -351,8 +641,11 @@ export default {
this.editorInstance.setContent(finalContent);
// 渲染数学公式
// SetContent 回调里会 renderAutociteInEditor再补一次 nextTick避免时序下仍显示库内旧 [1-4]
this.$nextTick(() => {
if (this.editorInstance) {
this.renderAutociteInEditor(this.editorInstance);
}
if (window.renderMathJax) {
window.renderMathJax(this.tinymceId);
}
@@ -611,7 +904,7 @@ export default {
return new Blob([u8arr], { type: mime });
},
formatHtml(val) {
const rawValue = this.normalizeAutociteHtmlForEditor(val || ''); // 须先于 cleanEmptyTags否则空 autocite 被删
const rawValue = this.normalizeAutociteHtmlForEditor(val || ''); // 须先于 cleanEmptyTags否则空 mycite 被删
const cleanEmptyTags = /<([a-zA-Z1-6]+)\b[^>]*><\/\1>/g;
const replaceSpaces = /\s+(?=<)|(?<=>)\s+/g;
const removeBr = /<br\s*\/?>/gi; // 移除所有 br 标签
@@ -677,10 +970,10 @@ export default {
window.tinymce.init({
..._this.tinymceOtherInit,
trim_span_elements: false, // 禁止修剪内联标签周围的空格
extended_valid_elements: 'blue[*],autocite[*]',
extended_valid_elements: 'blue[*],mycite[*]',
/* ~ 前缀:按行内(类似 span处理否则默认当块级会拆段导致引用后强制换行 */
custom_elements: 'blue,~autocite',
valid_children: '+blue[#text|i|em|b|strong|span],+body[blue|autocite],+p[blue|autocite]',
custom_elements: 'blue,~mycite',
valid_children: '+blue[#text|i|em|b|strong|span],+body[blue|mycite],+p[blue|mycite]',
inline: false, // 使用 iframe 模式
selector: `#${this.tinymceId}`,
@@ -690,7 +983,7 @@ export default {
valid_elements:
this.type == 'table'
? '*[*]'
: `img[src|alt|width|height],strong,em,sub,sup,blue,table,b,i,myfigure,mytable,wmath,autocite[data-id|contenteditable|title]${this.valid_elements}`, // 允许的标签和属性
: `img[src|alt|width|height],strong,em,sub,sup,blue,table,b,i,myfigure,mytable,wmath,mycite[data-id|contenteditable|title|data-cite-missing|style]${this.valid_elements}`, // 允许的标签和属性
// valid_elements: '*[*]', // 允许所有 HTML 标签
noneditable_editable_class: 'MathJax',
height: this.height,
@@ -723,7 +1016,7 @@ export default {
}
/* inline 与 blue 引用一致,避免 inline-block 在行尾产生多余换行感 */
autocite {
mycite {
display: inline;
vertical-align: baseline;
color: rgb(0, 130, 170);
@@ -736,10 +1029,19 @@ export default {
font-size: 12px;
white-space: nowrap;
}
autocite:hover {
mycite:hover {
background-color: rgba(0, 130, 170, 0.2);
text-shadow: 0 0 3px rgba(0, 130, 170, 0.3);
}
mycite[data-cite-missing] {
background-color: rgba(0, 130, 170, 0.08) !important;
color: rgb(0, 130, 170) !important;
text-shadow: 0 0 3px rgba(0, 130, 170, 0.3);
box-shadow: none !important;
}
mycite[data-cite-missing]:hover {
background-color: rgba(0, 130, 170, 0.2) !important;
}
@keyframes blueGlow {
0%,
@@ -764,17 +1066,31 @@ export default {
const hasCustom =
(Array.isArray(tb) && tb.length > 0) ||
(typeof tb === 'string' && tb.length > 0);
/* 与 content.vue 默认一致,且含 insertRef避免未传 toolbar 时引用到未定义的变量 */
const refInsert = _this.showRefButton ? ' insertRef' : '';
/* 自动匹配 [n] 为角标:底部蓝色按钮,见模板 tinymce-autolink-footer */
/* 与 content.vue 默认一致;含 insertRef 由 showRefButton 控制 */
const defaultToolbar = [
'bold italic |customBlue removeBlue|LateX insertRef| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace'
`bold italic |customBlue removeBlue|LateX${refInsert}| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace`
];
if (!hasCustom) return defaultToolbar;
const rows = Array.isArray(tb) ? tb : [tb];
return rows.map((row) =>
typeof row === 'string' && row.indexOf('insertRef') === -1
? row.replace(/\|LateX\|/g, '|LateX insertRef|').replace(/\|\s*LateX\s*\|/g, '| LateX insertRef |')
: row
);
return rows.map((row) => {
if (typeof row !== 'string') return row;
let out = row;
if (!_this.showRefButton) {
out = out
.replace(/\binsertRef\b/g, '')
.replace(/\bautoLinkRefs\b/g, '')
.replace(/\|\s*\|/g, '|')
.replace(/\|\s*$/g, '')
.replace(/^\s*\|/g, '');
} else if (out.indexOf('insertRef') === -1) {
out = out
.replace(/\|LateX\|/g, '|LateX insertRef|')
.replace(/\|\s*LateX\s*\|/g, '| LateX insertRef |');
}
return out;
});
})(),
menubar: false, // 启用菜单栏并保持必要的项目
statusbar: false, // 关闭底部状态栏
@@ -844,7 +1160,7 @@ export default {
var currentWmathElement = null;
ed.ui.registry.addButton('insertRef', {
text: 'Ref',
text: 'Reference',
tooltip: 'Insert Reference',
onAction: function () {
const refs = Array.isArray(_this.chanFerForm) ? _this.chanFerForm : [];
@@ -859,7 +1175,7 @@ export default {
});
ed.on('click', function (e) {
const autociteEl = e.target.closest('autocite');
const autociteEl = e.target.closest('mycite');
if (autociteEl) {
const refs = Array.isArray(_this.chanFerForm) ? _this.chanFerForm : [];
if (refs.length === 0) {
@@ -1003,6 +1319,8 @@ export default {
subtree: true,
characterData: true
});
_this.mountAutoLinkFooterInsideEditor(ed);
});
// 定义自定义按钮
@@ -1045,11 +1363,11 @@ export default {
ed.on('GetContent', function (e) {
e.content = e.content.replace(/<b>/g, '<strong>').replace(/<\/b>/g, '</strong>');
e.content = e.content.replace(/<i>/g, '<em>').replace(/<\/em>/g, '</em>');
e.content = e.content.replace(/<\/autocite>\s*&#8203;/gi, '</autocite>');
e.content = e.content.replace(/<\/autocite>\s*\u200b/g, '</autocite>');
e.content = e.content.replace(/<autocite([^>]*)>[^<]*<\/autocite>/gi, function (match, attrs) {
e.content = e.content.replace(/<\/mycite>\s*&#8203;/gi, '</mycite>');
e.content = e.content.replace(/<\/mycite>\s*\u200b/g, '</mycite>');
e.content = e.content.replace(/<mycite([^>]*)>[^<]*<\/mycite>/gi, function (match, attrs) {
var clean = attrs.replace(/\s*style="[^"]*"/gi, '').replace(/\s*title="[^"]*"/gi, '').replace(/\s*contenteditable="[^"]*"/gi, '');
return '<autocite' + clean + '></autocite>';
return '<mycite' + clean + '></mycite>';
});
});
},
@@ -1210,6 +1528,7 @@ export default {
//销毁富文本
destroyTinymce() {
this.onClear();
this.restoreAutoLinkFooterDom();
if (window.tinymce.get(this.tinymceId)) {
window.tinymce.get(this.tinymceId).destroy();
}
@@ -1226,7 +1545,8 @@ export default {
content = content.replace(/<span[^>]*>/g, '').replace(/<\/span>/g, ''); // 去除span标签
content = content.replace(/<strong>/g, '<b>').replace(/<\/strong>/g, '</b>');
content = content.replace(/<em>/g, '<i>').replace(/<\/em>/g, '</i>');
content = content.replace(/&nbsp;/g, ' '); // 将所有 &nbsp; 替换为空格
content = content.replace(/&nbsp;/g, ' '); // 先统一为空格
content = this.normalizeAutociteHtmlForEditor(content); // mycite 两侧水平空白以 &nbsp; 存库(与入编辑器一致)
this.$emit('getContent', type, content);
},
@@ -1311,6 +1631,26 @@ export default {
};
</script>
<style scoped>
.tinymce-editor-surface {
width: 100%;
}
.tinymce-autolink-footer {
display: flex;
justify-content: flex-end;
align-items: center;
padding-top: 8px;
margin-top: 4px;
border-top: 1px solid #ebeef5;
}
/* 移入 .tox-tinymce 内部后:贴编辑区下沿、与编辑器同宽同框 */
::v-deep .tox-tinymce > .tinymce-autolink-footer {
margin-top: 0;
padding: 0px 10px;
box-sizing: border-box;
border-top: 1px solid #dcdfe6;
background: #fafafa;
flex-shrink: 0;
}
::v-deep .tox-tinymce-aux {
z-index: 9999 !important;
}

View File

@@ -10,12 +10,15 @@
ref="tinymceChild1"
:wordStyle="wordStyle"
:isAutomaticUpdate="isAutomaticUpdate"
:showRefButton="showRefButton"
@getContent="getContent"
@openLatexEditor="openLatexEditor"
@openRefSelector="openRefSelector"
@updateChange="updateChange"
@input="onTinymceInput"
:value="value"
:chanFerForm="chanFerForm"
:bodyCiteIdOrder="bodyCiteIdOrder"
:typesettingType="typesettingType"
class="paste-area text-container"
:toolbar="toolbarConfig"
@@ -38,14 +41,29 @@
<script>
import Tinymce from '@/components/page/components/Tinymce';
export default {
props: ['value', 'isAutomaticUpdate', 'height', 'id', 'chanFerForm'],
props: {
value: {},
isAutomaticUpdate: {},
height: {},
id: {},
chanFerForm: {},
/** 全文 mycite 顺序,与稿面 word.vue 的 bodyCiteIdOrder 一致,用于弹窗内 [1][2] 与参考文献重排 */
bodyCiteIdOrder: {
type: Array,
default: () => []
},
/** false标题等场景不显示 Ref */
showRefButton: {
type: Boolean,
default: true
}
},
components: {
Tinymce
},
computed: {
toolbarConfig() {
// Ref 必须始终出现在配置里TinyMCE 只在 init 时读一次 toolbar若首屏 chanFerForm 为空则按钮会永久缺失
const refBtn = ' insertRef';
const refBtn = this.showRefButton ? ' insertRef' : '';
if (!this.isAutomaticUpdate) {
return [`bold italic |customBlue removeBlue|LateX${refBtn}| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace`];
}
@@ -102,24 +120,35 @@ export default {
openRefSelector(data) {
this.$emit('openRefSelector', data);
},
onTinymceInput(html) {
this.$emit('editorInput', html);
},
insertAutocite(refId) {
this.$refs.tinymceChild1.insertAutocite(refId);
},
removeAutocite() {
this.$refs.tinymceChild1.removeAutocite();
},
/** 从当前 autocite 的 data-id 中移除指定 id无剩余则删标签未选 id 时删整段 */
/** 从当前 mycite 的 data-id 中移除指定 id无剩余则删标签未选 id 时删整段 */
stripAutociteIds(ids) {
if (this.$refs.tinymceChild1 && typeof this.$refs.tinymceChild1.stripAutociteIds === 'function') {
this.$refs.tinymceChild1.stripAutociteIds(ids);
}
},
/** 参考文献异步到达后刷新编辑器内 autocite 数字(供父组件在打开弹窗 / fetch 完成后调用) */
/** 参考文献异步到达后刷新编辑器内 mycite 数字(供父组件在打开弹窗 / fetch 完成后调用) */
refreshAutociteDisplay() {
const t = this.$refs.tinymceChild1;
if (t && typeof t.renderAutociteInEditor === 'function' && t.editorInstance) {
t.renderAutociteInEditor(t.editorInstance);
}
},
/** 将正文中的 [1]、[13] 等按全局序号匹配为 <mycite> */
convertBracketRefsToAutocite() {
const t = this.$refs.tinymceChild1;
if (t && typeof t.convertPlainBracketCitesToAutocite === 'function') {
return t.convertPlainBracketCitesToAutocite();
}
return { replaced: 0 };
}
}
};

View File

@@ -5,8 +5,13 @@
:articleId="articleId"
ref="tinymceChild1"
:wordStyle="wordStyle"
:show-ref-button="showRefButton"
:chanFerForm="chanFerForm"
:bodyCiteIdOrder="bodyCiteIdOrder"
@getContent="getContent"
@openLatexEditor="openLatexEditor"
@openRefSelector="$emit('openRefSelector', $event)"
@input="onTableEditorInput"
:height="calcDynamicWidth()"
:value="updatedHtml"
:typesettingType="typesettingType"
@@ -22,7 +27,24 @@
<script>
import Tinymce from '@/components/page/components/Tinymce';
export default {
props: ['lineStyle', 'articleId'],
props: {
lineStyle: {},
articleId: {},
/** 与稿面/段落编辑一致,供 Ref 插入;未传时 TinyMCE 内为空会提示「参考文献尚未加载」 */
chanFerForm: {
type: Array,
default: () => []
},
bodyCiteIdOrder: {
type: Array,
default: () => []
},
/** 与 content.vue 一致:为 true 时显示 Reference、Auto-link References稿面表格编辑需文献角标时保持 true */
showRefButton: {
type: Boolean,
default: true
}
},
components: {
Tinymce
},
@@ -91,6 +113,9 @@ export default {
}
},
methods: {
onTableEditorInput(html) {
this.$emit('editorInput', html);
},
openLatexEditor(data) {
this.$emit('openLatexEditor', data);
},
@@ -121,6 +146,22 @@ export default {
} else {
this.$emit('getContent', type, { html_data: '', table: [] });
}
},
insertAutocite(refIds) {
if (this.$refs.tinymceChild1 && typeof this.$refs.tinymceChild1.insertAutocite === 'function') {
this.$refs.tinymceChild1.insertAutocite(refIds);
}
},
stripAutociteIds(ids) {
if (this.$refs.tinymceChild1 && typeof this.$refs.tinymceChild1.stripAutociteIds === 'function') {
this.$refs.tinymceChild1.stripAutociteIds(ids);
}
},
refreshAutociteDisplay() {
const t = this.$refs.tinymceChild1;
if (t && typeof t.renderAutociteInEditor === 'function' && t.editorInstance) {
t.renderAutociteInEditor(t.editorInstance);
}
}
}
};

View File

@@ -103,22 +103,21 @@
v-if="
!isPreview &&
isEditComment &&
item.proof_read_num &&
item.proof_read_num > 0 &&
item.is_proofread == 1 &&
isProofreadEnabled(item) &&
resolvedProofreadNum(item) > 0 &&
item.type == 0 &&
item.content != ''
"
class="proofreading-num"
>{{ item.proof_read_num }}</span
>{{ resolvedProofreadNum(item) }}</span
>
<span
@click="handleClickProofreadingList([item.am_id])"
v-if="
!isPreview &&
isEditComment &&
item.proof_read_num == -1 &&
item.is_proofread == 1 &&
isProofreadEnabled(item) &&
resolvedProofreadNum(item) === -1 &&
item.type == 0 &&
item.content != ''
"
@@ -131,8 +130,8 @@
v-if="
!isPreview &&
isEditComment &&
item.proof_read_num == 0 &&
item.is_proofread == 1 &&
isProofreadEnabled(item) &&
resolvedProofreadNum(item) === 0 &&
item.type == 0 &&
item.content != ''
"
@@ -140,19 +139,11 @@
style="color: #006699e0"
><i class="el-icon-success"></i
></span>
<span
@click="handleClickProofreadingList([item.am_id])"
v-if="!isPreview && isEditComment && item.is_proofread == 2 && item.type == 0 && item.content != ''"
class="Proofreadingstatus"
style="color: #e6a23c"
><i class="el-icon-warning"></i
></span>
<span
@click="handleClickProofreadingList([item.am_id])"
class="isRemark"
:remark-main-id="item.am_id"
v-if="(item.proof_read_num > 0 || item.proof_read_num == 0) && !isPreview && !isShowPiZhu"
v-if="(resolvedProofreadNum(item) > 0 || resolvedProofreadNum(item) === 0) && !isPreview && !isShowPiZhu"
>
<img
class="isRemark base-margin"
@@ -982,13 +973,22 @@
<div v-if="isEditComment" class="menu-item comment-feat" @mousedown="cacheSelection" @click="menuAction('comment')">
<i class="el-icon-chat-line-square"></i><span>{{ $t('commonTable.Annotations') }}</span>
</div>
<div class="row-divider" v-if="currentData.type == 0 && (isEditComment || manuscriptAutociteContext)"></div>
<div
class="row-divider"
v-if="(currentData.type == 0 || currentData.type == 1) && (isEditComment || manuscriptAutociteContext)"
></div>
<template v-if="manuscriptAutociteContext && currentData && currentData.type == 0">
<template
v-if="
manuscriptAutociteContext &&
currentData &&
(currentData.type == 0 || currentData.type == 1 || currentData.type == 2)
"
>
<div class="menu-item menu-autocite-ref" @click.stop="menuAction('editRefCite')">
<i class="el-icon-edit-outline"></i><span>{{ $t('wordCite.modifyRef') }}</span>
</div>
<div class="menu-item menu-autocite-ref" @click.stop="menuAction('removeRefCite')">
<div class="menu-item danger" @click.stop="menuAction('removeRefCite')">
<i class="el-icon-remove-outline"></i><span>{{ $t('wordCite.removeRefTag') }}</span>
</div>
</template>
@@ -1053,6 +1053,7 @@ const toolbar = 'addImageButton ';
import { TableUtils } from '@/common/js/TableUtils';
import { debounce, throttle } from '@/common/js/debounce';
import { tableStyle, commonWordStyle } from '@/utils/tinymceStyles';
import { extractAutociteIdsFromHtmlString } from '@/utils/autociteHtml.js';
export default {
name: 'tinymce',
components: {},
@@ -1097,6 +1098,11 @@ export default {
return [];
}
},
/** 父组件传入的正文 mycite 顺序(与 GenerateCharts 的 articleCiteIdOrder 一致);未传时仍用稿面扫描得到的 bodyCiteIdOrder */
citeOrderFromParent: {
type: Array,
default: null
},
comments: {
type: [Array, Object], // 允许数组或对象
@@ -1180,8 +1186,11 @@ export default {
editorsInitialized: {}, // 用于存储每个编辑器实例
mediaUrl: mediaUrl, //
lastTag: null,
/** 稿面点击 <autocite> 时记录,用于浮动条「修改引用 / 移除引用」 */
/** 稿面点击 <mycite> 时记录,用于浮动条「修改引用 / 移除引用」 */
manuscriptAutociteContext: null,
/** 正文 DOM 中 mycite 首次出现顺序(唯一 p_refer_id驱动 citeMap 与参考文献列表排序 */
bodyCiteIdOrder: [],
_syncRefOrderTimer: null,
isEditComment: false,
isUserEditComment: false,
typesettingType: 1,
@@ -1256,11 +1265,24 @@ export default {
this.$nextTick(() => {
window.renderMathJax(); // 主动触发 MathJax 渲染
this.getCommentsData();
this.scheduleSyncRefOrder();
});
}
},
deep: true // 启用深度监听
},
wordList: {
handler() {
this.scheduleSyncRefOrder();
},
deep: true
},
chanFerForm: {
handler() {
this.scheduleSyncRefOrder();
},
deep: true
},
totalNumbers: {
handler(val) {
if (val == 0) {
@@ -1272,45 +1294,66 @@ export default {
},
computed: {
citeMap() {
// 1) 优先使用参考文献列表顺序(与右侧 References 一致)
const mapFromRefs = {};
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
refs.forEach((row, idx) => {
const key = row && row.p_refer_id != null ? String(row.p_refer_id) : '';
if (key) mapFromRefs[key] = idx + 1;
});
if (Object.keys(mapFromRefs).length > 0) return mapFromRefs;
const refIdSet = new Set(
refs.map((r) => (r && r.p_refer_id != null ? String(r.p_refer_id) : '')).filter(Boolean)
);
// 2) 兜底:按正文首次出现顺序编号(支持 data-id 含多个逗号分隔 id
const order = [];
const re = /<autocite\s+data-id="([^"]+)"/gi;
const paragraphs =
Array.isArray(this.wordList) && this.wordList.length > 0
? this.wordList
: Array.isArray(this.contentList)
? this.contentList
: [];
let orderedIds = [];
if (Array.isArray(this.citeOrderFromParent) && this.citeOrderFromParent.length > 0) {
orderedIds = this.citeOrderFromParent.map(String);
} else if (Array.isArray(this.bodyCiteIdOrder) && this.bodyCiteIdOrder.length > 0) {
orderedIds = this.bodyCiteIdOrder.map(String);
} else {
const order = [];
const paragraphs =
Array.isArray(this.wordList) && this.wordList.length > 0
? this.wordList
: Array.isArray(this.contentList)
? this.contentList
: [];
paragraphs.forEach((p) => {
const candidates = [];
if (p && typeof p.text === 'string') candidates.push(p.text);
if (p && typeof p.content === 'string') candidates.push(p.content);
candidates.forEach((raw) => {
re.lastIndex = 0;
let m;
while ((m = re.exec(raw)) !== null) {
m[1].split(',').forEach((part) => {
const id = part.trim();
if (id && !order.includes(id)) order.push(id);
paragraphs.forEach((p) => {
const candidates = [];
if (p && typeof p.text === 'string') candidates.push(p.text);
if (p && typeof p.content === 'string') candidates.push(p.content);
candidates.forEach((raw) => {
extractAutociteIdsFromHtmlString(raw).forEach((id) => {
if (!order.includes(id)) order.push(id);
});
}
});
});
orderedIds = order.map(String);
}
// 无正文顺序时:按参考文献表当前行序编号(与列表 # 列一致)
if (orderedIds.length === 0) {
const map = {};
refs.forEach((row, idx) => {
const key = row && row.p_refer_id != null ? String(row.p_refer_id) : '';
if (key) map[key] = idx + 1;
});
return map;
}
/** 仅对「当前列表里真实存在」的 id 依正文首次出现顺序编号;正文曾出现但列表已删的 id 不占号 */
const filtered = [];
orderedIds.forEach((id) => {
if (refIdSet.has(id) && !filtered.includes(id)) filtered.push(id);
});
return order.reduce((acc, id, idx) => {
acc[id] = idx + 1;
return acc;
}, {});
const map = {};
filtered.forEach((id, idx) => {
map[id] = idx + 1;
});
let next = filtered.length + 1;
refs.forEach((r) => {
const key = r && r.p_refer_id != null ? String(r.p_refer_id) : '';
if (!key || map[key] != null) return;
map[key] = next++;
});
return map;
},
sortedProofreadingList() {
const order = [2, 1, 3];
@@ -1339,6 +1382,7 @@ export default {
this.$nextTick(() => {
window.renderMathJax(); // 主动触发 MathJax 渲染
this.getCommentsData();
this.scheduleSyncRefOrder();
});
this.$refs.scrollDiv.addEventListener('scroll', this.divOnScroll, { passive: true });
@@ -1397,6 +1441,7 @@ export default {
this.$nextTick(() => {
window.renderMathJax(); // 主动触发 MathJax 渲染
this.getCommentsData();
this.scheduleSyncRefOrder();
});
this.$refs.scrollDiv.addEventListener('scroll', this.divOnScroll, { passive: true });
},
@@ -1415,17 +1460,31 @@ export default {
// window.removeEventListener('resize', this.calcMarkers);
if (this.resizeObs) this.resizeObs.disconnect();
if (this.mutObs) this.mutObs.disconnect();
// 页面销毁前清理定时器和事件监听器
if (this._syncRefOrderTimer) clearTimeout(this._syncRefOrderTimer);
},
destroy() {
this.destroyTinymce();
this.editors = {};
},
methods: {
// 1. 引用序号合并:两项连续 1, 2三项及以上连续 1-3与富文本 autocite data-id 多 id 一致)
/** 合并引用 tooltip / title 行按 citeMap 排序 */
sortAutociteIdsByCiteNumber(ids) {
const uniq = [...new Set(ids)];
const map = this.citeMap || {};
return uniq.sort((a, b) => {
const na = map[a];
const nb = map[b];
const ha = na != null && na !== '';
const hb = nb != null && nb !== '';
if (ha && hb) return Number(na) - Number(nb);
if (ha) return -1;
if (hb) return 1;
return String(a).localeCompare(String(b));
});
},
/** 多项连续时合并为 13仅用于「同组多 id 且均在列表中」的全局序号展示 */
formatCiteNumbers(nums) {
if (!nums || !nums.length) return "";
if (!nums || !nums.length) return '';
const sorted = [...new Set(nums)].sort((a, b) => a - b);
const result = [];
let i = 0;
@@ -1442,28 +1501,17 @@ formatCiteNumbers(nums) {
return result.join(', ');
},
/** 合并引用 tooltip / title 行按正文序号排列,与 [24, 6] 标签一致 */
sortAutociteIdsByCiteNumber(ids) {
const uniq = [...new Set(ids)];
const map = this.citeMap || {};
return uniq.sort((a, b) => {
const na = map[a];
const nb = map[b];
const ha = na != null && na !== '';
const hb = nb != null && nb !== '';
if (ha && hb) return Number(na) - Number(nb);
if (ha) return -1;
if (hb) return 1;
return String(a).localeCompare(String(b));
});
},
// 2. 最终引用标签渲染器 (将 autocite 转换为 [n]);支持 data-id="a,b,c" 单标签 或 多个相邻单 id 标签(旧数据)
// 2. 最终引用标签渲染器 (将 mycite 转换为 [n]);支持 data-id="a,b,c" 单标签 或 多个相邻单 id 标签(旧数据)
renderCiteLabels(html) {
const citeGroupRe = /(?:<autocite\s+data-id="([^"]+)"\s*><\/autocite>\s*)+/gi;
return html.replace(citeGroupRe, (groupMatch) => {
// 库内若曾写入 &nbsp;[1]&nbsp; 等,与外侧空格叠缝;先规范为空标签再按组合并渲染
let normalized = String(html || '').replace(
/<mycite\s+data-id="([^"]*)"[^>]*>[\s\S]*?<\/mycite>/gi,
'<mycite data-id="$1"></mycite>'
);
const citeGroupRe = /(?:<mycite\s+data-id="([^"]*)"\s*><\/mycite>\s*)+/gi;
return normalized.replace(citeGroupRe, (groupMatch) => {
const ids = [];
const innerRe = /<autocite\s+data-id="([^"]+)"\s*><\/autocite>/gi;
const innerRe = /<mycite\s+data-id="([^"]*)"\s*><\/mycite>/gi;
let m;
while ((m = innerRe.exec(groupMatch)) !== null) {
m[1].split(',').forEach((part) => {
@@ -1478,34 +1526,56 @@ renderCiteLabels(html) {
return acc;
}, {});
const sortedIds = this.sortAutociteIdsByCiteNumber(ids);
const nums = sortedIds.map((id) => this.citeMap && this.citeMap[id]).filter((n) => n != null);
const label = nums.length > 0 ? this.formatCiteNumbers(nums) : '?';
const sortedAll = this.sortAutociteIdsByCiteNumber(ids);
const validIds = sortedAll.filter((id) => refMap[id]);
const sortedIds = validIds.length ? this.sortAutociteIdsByCiteNumber(validIds) : [];
const citeMap = this.citeMap || {};
const lines = sortedIds.map((id) => {
const no = this.citeMap && this.citeMap[id] != null ? this.citeMap[id] : '?';
/** 与 Tinymce已删除的 id 不进入角标与 title仅保留仍存在于列表中的文献 */
const parts = sortedIds.map((id) => {
const ref = refMap[id];
if (!ref) {
return this.$t('wordCite.notFoundById', { id: no === '?' ? id : no });
}
const content =
ref.refer_frag ||
[ref.author, ref.title, ref.joura, ref.dateno].filter(Boolean).join(' ').trim() ||
'[?]';
const doi = ref.doilink || ref.isbn || ref.doi || '[?]';
return `[${no}] ${content}&#10;DOI: ${doi}`;
const no = ref ? citeMap[id] : null;
const num = no != null && no !== '' ? String(no) : null;
return { id, ref, num };
});
const numsForLabel = parts
.map((p) => p.num)
.filter((n) => n != null && n !== '')
.map(Number);
const label =
numsForLabel.length > 0 ? this.formatCiteNumbers(numsForLabel) : '';
const lines = parts
.filter((p) => p.ref)
.map((p) => {
const ref = p.ref;
const content =
ref.refer_frag ||
[ref.author, ref.title, ref.joura, ref.dateno].filter(Boolean).join(' ').trim() ||
'';
const doi = ref.doilink || ref.isbn || ref.doi || '';
const numLabel = p.num;
if (numLabel) {
return `[${numLabel}] ${content}\nDOI: ${doi}`;
}
return `${content}\nDOI: ${doi}`;
});
const escAttr = (s) =>
String(s || '')
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;');
if (!label) {
return '';
}
const dataIdAttr = escAttr(sortedIds.join(','));
const titleAttr = escAttr(lines.join('\n'));
return `<autocite data-id="${dataIdAttr}" title="${titleAttr}">[${label}]</autocite>`;
});
return `<mycite data-id="${dataIdAttr}" title="${titleAttr}">[${label}]</mycite>`;
})
/** 库内遗留:空 data-id 或仅零宽字符的 mycite 整段去掉 */
.replace(/<mycite[^>]*data-id=""[^>]*>[\s\S]*?<\/mycite>/gi, '');
},
getInvolvedPMain(range) {
// 1. 找到起始节点所属的 .pMain
@@ -2110,11 +2180,25 @@ renderCiteLabels(html) {
});
return parts.join(',');
},
/** 按 data-id 替换第一段匹配的 <autocite>;兼容稿面已排序、正文存储未排序的情况 */
/** 按 data-id 替换第一段匹配的 <mycite>;兼容稿面已排序、正文存储未排序的情况 */
replaceFirstAutociteByDataId(html, dataId, replacement) {
if (!html || typeof html !== 'string' || dataId == null || dataId === '') return html;
if (!html || typeof html !== 'string') return html;
/** 删除整段且 data-id 为空:移除第一个 data-id 为空的 <mycite>(与浮动条「移除引用」一致) */
if (replacement === '' && (dataId == null || dataId === '')) {
let done = false;
return html.replace(/<mycite\s([^>]*)>([\s\S]*?)<\/mycite>/gi, function (full, attrs) {
if (done) return full;
const dm =
attrs.match(/\bdata-id\s*=\s*"([^"]*)"/i) || attrs.match(/\bdata-id\s*=\s*'([^']*)'/i);
const idVal = dm ? String(dm[1]).trim() : '';
if (idVal !== '') return full;
done = true;
return '';
});
}
if (dataId == null || dataId === '') return html;
const esc = String(dataId).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const reExact = new RegExp(`<autocite\\s[^>]*\\bdata-id="${esc}"[^>]*>[\\s\\S]*?<\\/autocite>`, 'i');
const reExact = new RegExp(`<mycite\\s[^>]*\\bdata-id="${esc}"[^>]*>[\\s\\S]*?<\\/mycite>`, 'i');
let out = html.replace(reExact, replacement);
if (out !== html) return out;
@@ -2123,7 +2207,7 @@ renderCiteLabels(html) {
const _this = this;
let done = false;
return html.replace(/<autocite\s([^>]*)>([\s\S]*?)<\/autocite>/gi, function (full, attrs) {
return html.replace(/<mycite\s([^>]*)>([\s\S]*?)<\/mycite>/gi, function (full, attrs) {
if (done) return full;
const dm = attrs.match(/\bdata-id\s*=\s*"([^"]*)"/i) || attrs.match(/\bdata-id\s*=\s*'([^']*)'/i);
if (!dm) return full;
@@ -2146,14 +2230,177 @@ renderCiteLabels(html) {
.filter(Boolean);
this.$emit('openRefSelector', { currentRefIds: parts, source: 'manuscript' });
},
/** 表格局部:按表头→表体、从左到右遍历单元格 */
forEachTableCellInOrder(item, fn) {
const t = item.table;
if (!t) return;
const walk = (rows) => {
if (!Array.isArray(rows)) return;
for (let r = 0; r < rows.length; r++) {
const row = rows[r];
if (!Array.isArray(row)) continue;
for (let c = 0; c < row.length; c++) {
const cell = row[c];
if (cell && typeof cell.text === 'string') {
if (fn(cell) === true) return true;
}
}
}
return false;
};
if (walk(t.tableHeader)) return;
walk(t.tableContent);
},
mergeTableDataForSave(item) {
const t = item.table;
if (!t) return [];
const cloneRow = (row) => {
if (!Array.isArray(row)) return row;
return row.map((cell) => {
if (!cell || typeof cell !== 'object') return cell;
const { rowId, cellId, ...rest } = cell;
return { ...rest };
});
};
return [...(t.tableHeader || []).map(cloneRow), ...(t.tableContent || []).map(cloneRow)];
},
applyFirstAutociteReplaceInTable(item, ctx, newTag) {
let changed = false;
this.forEachTableCellInOrder(item, (cell) => {
const n = this.replaceFirstAutociteByDataId(cell.text, ctx.dataId, newTag);
if (n !== cell.text) {
cell.text = n;
changed = true;
return true;
}
});
if (changed) return true;
const t = item.table;
if (!t) return false;
for (const field of ['title', 'note']) {
if (typeof t[field] !== 'string' || !t[field]) continue;
const n = this.replaceFirstAutociteByDataId(t[field], ctx.dataId, newTag);
if (n !== t[field]) {
this.$set(t, field, n);
return true;
}
}
return false;
},
stripAutociteInHtmlString(html, ctx, removeSet) {
if (!html || typeof html !== 'string') return html;
if (removeSet.size === 0) {
return this.replaceFirstAutociteByDataId(html, ctx.dataId, '');
}
const keyWant = this.normalizeAutociteDataIdKey(ctx.dataId);
const _this = this;
let done = false;
return html.replace(/<mycite\s([^>]*)>([\s\S]*?)<\/mycite>/gi, function (full, attrs) {
if (done) return full;
const dm = attrs.match(/\bdata-id\s*=\s*"([^"]*)"/i) || attrs.match(/\bdata-id\s*=\s*'([^']*)'/i);
if (!dm) return full;
if (_this.normalizeAutociteDataIdKey(dm[1]) !== keyWant) return full;
done = true;
const parts = dm[1]
.split(',')
.map((s) => s.trim())
.filter(Boolean);
const remaining = parts.filter((id) => !removeSet.has(String(id)));
if (remaining.length === 0) return '';
const sorted = _this.sortAutociteIdsByCiteNumber(remaining);
const newDataId = sorted.join(',');
return `<mycite data-id="${_this.escapeHtmlAttr(newDataId)}"></mycite>`;
});
},
applyStripToFirstMatchingTableCell(item, ctx, removeSet) {
let changed = false;
this.forEachTableCellInOrder(item, (cell) => {
const nh = this.stripAutociteInHtmlString(cell.text, ctx, removeSet);
if (nh !== cell.text) {
cell.text = nh;
changed = true;
return true;
}
});
if (changed) return true;
const t = item.table;
if (!t) return false;
for (const field of ['title', 'note']) {
if (typeof t[field] !== 'string' || !t[field]) continue;
const nh = this.stripAutociteInHtmlString(t[field], ctx, removeSet);
if (nh !== t[field]) {
this.$set(t, field, nh);
return true;
}
}
return false;
},
/** 图片说明/标题中的 autocite与表格 note 同理 */
applyStripToFirstMatchingImageField(item, ctx, removeSet) {
const img = item.image;
if (!img) return false;
for (const field of ['note', 'title']) {
if (typeof img[field] !== 'string' || !img[field]) continue;
const nh = this.stripAutociteInHtmlString(img[field], ctx, removeSet);
if (nh !== img[field]) {
this.$set(img, field, nh);
return true;
}
}
return false;
},
emitSaveTableManuscript(item) {
this.manuscriptAutociteContext = null;
this.$emit('saveTableManuscript', {
amt_id: item.amt_id,
am_id: item.am_id,
table_data: JSON.stringify(this.mergeTableDataForSave(item)),
title: (item.table && item.table.title) || '',
note: (item.table && item.table.note) || '',
html_data: (item.table && item.table.html_data) || ''
});
},
applyManuscriptAutocite(refIds) {
const ctx = this.manuscriptAutociteContext;
if (!ctx || !ctx.am_id) return;
const item = this.wordList.find((w) => w.am_id == ctx.am_id);
if (!item || typeof item.content !== 'string') return;
if (!item) return;
const ids = (Array.isArray(refIds) ? refIds : [refIds]).map(String);
const dataId = ids.join(',');
const newTag = `<autocite data-id="${this.escapeHtmlAttr(dataId)}"></autocite>`;
const newTag = `<mycite data-id="${this.escapeHtmlAttr(dataId)}"></mycite>`;
if (item.type === 2 && item.table) {
if (!this.applyFirstAutociteReplaceInTable(item, ctx, newTag)) {
this.$message.warning(this.$t('wordCite.citeUpdateFail'));
return;
}
this.emitSaveTableManuscript(item);
return;
}
if (item.type === 1 && item.image) {
const img = item.image;
for (const field of ['note', 'title']) {
if (typeof img[field] !== 'string' || !img[field]) continue;
const n = this.replaceFirstAutociteByDataId(img[field], ctx.dataId, newTag);
if (n !== img[field]) {
this.$set(img, field, n);
this.manuscriptAutociteContext = null;
this.$emit('saveImageManuscript', {
ami_id: item.ami_id,
am_id: item.am_id,
note: img.note || '',
title: img.title || '',
url: img.url || ''
});
return;
}
}
this.$message.warning(this.$t('wordCite.citeUpdateFail'));
return;
}
if (typeof item.content !== 'string') return;
const newContent = this.replaceFirstAutociteByDataId(item.content, ctx.dataId, newTag);
if (newContent === item.content) {
this.$message.warning(this.$t('wordCite.citeUpdateFail'));
@@ -2165,18 +2412,50 @@ renderCiteLabels(html) {
this.$emit('saveContent', newContent, ctx.am_id);
},
removeManuscriptAutocite() {
if (!this.manuscriptAutociteContext || !this.manuscriptAutociteContext.am_id) {
this.$message.warning(this.$t('wordCite.removeRefNeedClickCite'));
return;
}
this.stripManuscriptAutociteIds([]);
},
/**
* 从稿面当前 autocite 的 data-id 中移除给定 id移除后无 id 则删掉整段标签。
* 从稿面当前 mycite 的 data-id 中移除给定 id移除后无 id 则删掉整段标签。
* ids 为空:删除整段引用(稿面「移除引用」浮动按钮)。
*/
stripManuscriptAutociteIds(idsToRemove) {
const ctx = this.manuscriptAutociteContext;
if (!ctx || !ctx.am_id) return;
const item = this.wordList.find((w) => w.am_id == ctx.am_id);
if (!item || typeof item.content !== 'string') return;
if (!item) return;
const removeSet = new Set((idsToRemove || []).map((id) => String(id)));
if (item.type === 2 && item.table) {
if (!this.applyStripToFirstMatchingTableCell(item, ctx, removeSet)) {
this.$message.warning(this.$t('wordCite.citeUpdateFail'));
return;
}
this.emitSaveTableManuscript(item);
return;
}
if (item.type === 1 && item.image) {
if (!this.applyStripToFirstMatchingImageField(item, ctx, removeSet)) {
this.$message.warning(this.$t('wordCite.citeUpdateFail'));
return;
}
this.manuscriptAutociteContext = null;
const img = item.image;
this.$emit('saveImageManuscript', {
ami_id: item.ami_id,
am_id: item.am_id,
note: img.note || '',
title: img.title || '',
url: img.url || ''
});
return;
}
if (typeof item.content !== 'string') return;
if (removeSet.size === 0) {
const newContent = this.replaceFirstAutociteByDataId(item.content, ctx.dataId, '');
if (newContent === item.content) {
@@ -2193,7 +2472,7 @@ renderCiteLabels(html) {
const keyWant = this.normalizeAutociteDataIdKey(ctx.dataId);
const _this = this;
let done = false;
const newHtml = item.content.replace(/<autocite\s([^>]*)>([\s\S]*?)<\/autocite>/gi, function (full, attrs) {
const newHtml = item.content.replace(/<mycite\s([^>]*)>([\s\S]*?)<\/mycite>/gi, function (full, attrs) {
if (done) return full;
const dm = attrs.match(/\bdata-id\s*=\s*"([^"]*)"/i) || attrs.match(/\bdata-id\s*=\s*'([^']*)'/i);
if (!dm) return full;
@@ -2207,7 +2486,7 @@ renderCiteLabels(html) {
if (remaining.length === 0) return '';
const sorted = _this.sortAutociteIdsByCiteNumber(remaining);
const newDataId = sorted.join(',');
return `<autocite data-id="${_this.escapeHtmlAttr(newDataId)}"></autocite>`;
return `<mycite data-id="${_this.escapeHtmlAttr(newDataId)}"></mycite>`;
});
if (!done || newHtml === item.content) {
@@ -2219,6 +2498,65 @@ renderCiteLabels(html) {
this.manuscriptAutociteContext = null;
this.$emit('saveContent', newHtml, ctx.am_id);
},
/** 正文变化后防抖扫描 autocite更新 bodyCiteIdOrder 并通知父组件重排参考文献 */
scheduleSyncRefOrder() {
if (this._syncRefOrderTimer) clearTimeout(this._syncRefOrderTimer);
this._syncRefOrderTimer = setTimeout(() => {
this._syncRefOrderTimer = null;
this.$nextTick(() => {
this.syncRefOrder();
});
}, 0);
},
syncRefOrder() {
const root = this.$refs.scrollDiv;
if (!root) return;
const nodes = root.querySelectorAll('mycite');
const seen = new Set();
const order = [];
nodes.forEach((el) => {
const raw = el.getAttribute('data-id') || '';
raw.split(',').forEach((part) => {
const id = part.trim();
if (!id) return;
if (!seen.has(id)) {
seen.add(id);
order.push(id);
}
});
});
const prev = this.bodyCiteIdOrder || [];
const sameBody =
order.length === prev.length && order.every((id, i) => String(id) === String(prev[i]));
const listOk = order.length === 0 || this.refListMatchesBodyOrder(order);
if (sameBody && listOk) return;
this.bodyCiteIdOrder = order.slice();
if (order.length > 0) {
this.$emit('reorderReferencesByBody', order.slice());
}
},
/** 参考文献数组顺序是否与正文 mycite 首次出现顺序一致(未在正文出现的条目须在末尾) */
refListMatchesBodyOrder(order) {
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
if (refs.length === 0) return true;
const idSet = new Set(order.map(String));
const expectedHead = order.filter((id) => refs.some((r) => r && String(r.p_refer_id) === String(id)));
let hi = 0;
let seenTail = false;
for (const r of refs) {
if (!r || r.p_refer_id == null) continue;
const id = String(r.p_refer_id);
const cited = idSet.has(id);
if (cited) {
if (seenTail) return false;
if (hi >= expectedHead.length || expectedHead[hi] !== id) return false;
hi++;
} else {
seenTail = true;
}
}
return hi === expectedHead.length;
},
handleAIProofreading() {
if (this.currentId) {
this.$api
@@ -2346,7 +2684,7 @@ renderCiteLabels(html) {
let selectedText = tempDiv.innerText.trim().replace(/\s+/g, ' ');
const allowedTags = ['sup', 'sub', 'strong', 'em', 'b', 'i', 'blue', 'autocite', 'tr', 'td'];
const allowedTags = ['sup', 'sub', 'strong', 'em', 'b', 'i', 'blue', 'mycite', 'tr', 'td'];
function preserveTags(node) {
if (node.nodeType === 3) return node.nodeValue; // 文本节点
if (node.nodeType === 1 && allowedTags.includes(node.nodeName.toLowerCase())) {
@@ -3091,12 +3429,13 @@ renderCiteLabels(html) {
this.currentTag = '';
this.currentTagData = null; // 必须重置,防止带入旧数据
this.manuscriptAutociteContext = null;
const clickedAutocite = event.target.closest('autocite');
const clickedAutocite = event.target.closest('mycite');
if (clickedAutocite && !this.isPreview) {
const dataId = clickedAutocite.getAttribute('data-id') || '';
if (dataId) {
this.manuscriptAutociteContext = { am_id: id, dataId };
}
const dataId = clickedAutocite.getAttribute('data-id');
this.manuscriptAutociteContext = {
am_id: id,
dataId: dataId != null && dataId !== '' ? String(dataId) : ''
};
}
const clickedTag = event.target.closest('myfigure, mytable, myh3');
if (clickedTag) {
@@ -3307,15 +3646,19 @@ renderCiteLabels(html) {
},
highlightText(item, annotations, type) {
var text = item.text;
const raw = item && (item.text != null ? item.text : item.content);
var text = raw;
if (!this.isPreview) {
if (this.isShowPiZhu) {
text = this.highlightText1(item.text, annotations, type, item.am_id);
text = this.highlightText1(raw, annotations, type, item.am_id);
} else if (!this.isShowPiZhu && this.proofreadingList.length > 0 && this.currentSelectProofreadingId == item.am_id) {
text = this.highlightText2(item.text, this.proofreadingList[0].data, type, item.am_id);
text = this.highlightText2(raw, this.proofreadingList[0].data, type, item.am_id);
} else {
text = this.highlightText3(item.text, [], type, item.am_id);
text = this.highlightText3(raw, [], type, item.am_id);
}
} else {
// 预览与编辑「显示批注」同源:先切 wmath/autocite、再 renderCiteLabels避免仅跑片段导致角标/公式不一致
text = this.highlightText1(String(raw || ''), annotations || [], type);
}
// const finalHtml = text.replace(/<(?!(\/?(span|p|div|table|tr|td|th|b|i|strong|em|ul|ol|li|br|img|myh3|myfigure|mytable|blue|wmath)))/gi, '&lt;');
@@ -3337,7 +3680,7 @@ renderCiteLabels(html) {
const src = String(text || '');
// 1) 用一个全局、区分大小写不敏感的 wmath 提取正则
const wmathRe = /<(wmath|autocite)\b[^>]*>[\s\S]*?<\/(wmath|autocite)\s*>/gi;
const wmathRe = /<(wmath|mycite)\b[^>]*>[\s\S]*?<\/(wmath|mycite)\s*>/gi;
// 2) 把原文切成: [非wmath片段, wmath片段, 非wmath片段, wmath片段, ...]
const parts = [];
@@ -3443,7 +3786,7 @@ renderCiteLabels(html) {
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// 1) 切分出 wmath 与非 wmath 片段
const wmathRe = /<(wmath|autocite)\b[^>]*>[\s\S]*?<\/(wmath|autocite)\s*>/gi;
const wmathRe = /<(wmath|mycite)\b[^>]*>[\s\S]*?<\/(wmath|mycite)\s*>/gi;
const parts = [];
let lastIdx = 0,
m;
@@ -3526,7 +3869,7 @@ renderCiteLabels(html) {
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// 1) 切分出 wmath 与非 wmath 片段
const wmathRe = /<(wmath|autocite)\b[^>]*>[\s\S]*?<\/(wmath|autocite)\s*>/gi;
const wmathRe = /<(wmath|mycite)\b[^>]*>[\s\S]*?<\/(wmath|mycite)\s*>/gi;
const parts = [];
let lastIdx = 0,
m;
@@ -3684,6 +4027,29 @@ renderCiteLabels(html) {
}
},
/**
* 是否开启 AI 校对条目标记(与接口 is_proofread 对齐,兼容字符串 "1")。
* 未下发 is_proofread 时:编辑端仍显示每段状态,避免本地/测试数据与线上一致性断裂。
*/
isProofreadEnabled(item) {
if (!item) return false;
const v = item.is_proofread;
if (v === 0 || v === false || v === '0') return false;
if (v === 1 || v === true || v === '1') return true;
if (v === undefined || v === null || v === '') return !!this.isEditComment;
return false;
},
/**
* 本地/测试接口常漏掉 proof_read_numundefined/null 时 JS 里 item.proof_read_num==0 为 false导致右侧 Proofreadingstatus 整段不渲染。
* 缺省按 0 处理(与「无待处理项」档一致),保证每段在开启校对时都能显示状态。
*/
resolvedProofreadNum(item) {
if (!item) return 0;
const raw = item.proof_read_num;
if (raw === undefined || raw === null || raw === '') return 0;
const n = Number(raw);
return Number.isNaN(n) ? 0 : n;
},
isShowEditComment() {
if (localStorage.getItem('U_role')) {
var identity = localStorage.getItem('U_role');
@@ -4557,7 +4923,7 @@ renderCiteLabels(html) {
}
.proofreading-num {
z-index: 2;
background: #e61a12;
background: #0082aa;
color: rgb(255, 255, 255);
border-radius: 20px;
padding: 0px 6px;
@@ -4582,6 +4948,7 @@ renderCiteLabels(html) {
.table_Box {
padding: 8px 15px;
box-sizing: border-box;
overflow-x: auto;
}
.table-box tr:first-child td {
border-top: 1px solid #000 !important;
@@ -4703,7 +5070,7 @@ wmath {
}
/* 正文预览:引用直接用语义标签 autocite原 blue 包装已移除) */
::v-deep autocite {
::v-deep mycite {
display: inline;
vertical-align: baseline;
color: rgb(0, 130, 170) !important;
@@ -4714,6 +5081,15 @@ wmath {
background-color: rgba(0, 130, 170, 0.08);
}
/* 历史 HTML 可能仍带 data-cite-missing与正文引用同色不再使用红底 */
::v-deep mycite[data-cite-missing] {
background-color: rgba(0, 130, 170, 0.08) !important;
color: rgb(0, 130, 170) !important;
box-shadow: none !important;
padding: 0 2px;
border-radius: 3px;
}
::v-deep myfigure *,
::v-deep mytable * {
color: inherit !important;

View File

@@ -11,10 +11,11 @@
<div
:data-id="`ref-${item.p_refer_id}`"
v-for="(item, index) in chanFerForm"
:key="index"
:key="item.p_refer_id != null ? item.p_refer_id : `ref-row-${index}`"
class="ref-item-row"
:class="{ 'has-change': item.is_change == 1, 'is-repeat': item.is_repeat == 1 }"
>
<div class="ref-content-area">
<div v-if="item.refer_type == 'journal'" class="reference-item">
<b class="ref-number-prefix">{{ index + 1 }}.</b>
@@ -42,12 +43,12 @@
</div>
</div>
<div class="ref-actions-area">
<div class="ref-actions-area" v-if="!isPreview">
<i class="el-icon-edit action-icon primary" @click="change(item, 'Edit')"></i>
<i class="el-icon-circle-plus-outline action-icon success" @click="addLine(item, 'Add')"></i>
<div class="order-icons">
<div v-if="!refOrderFollowsBody" class="order-icons">
<i
class="el-icon-caret-top action-icon"
:class="{ disabled: index === 0 }"
@@ -61,7 +62,7 @@
</div>
<i class="el-icon-delete action-icon danger" @click="deleteLine(item)"></i>
<img v-if="role == 'editor' && item.is_ai_check == 1" src="@/assets/img/ai.png" class="ai-mini-tag" />
</div>
</div>
@@ -312,6 +313,10 @@ export default {
type: null,
required: true
},
isPreview: {
type: Boolean,
default: false
},
chanFerFormRepeatList: {
type: Array,
default: () => []
@@ -327,9 +332,30 @@ export default {
role: {
type: String,
default: () => 'editor'
},
/** 为 true 时顺序由正文引用决定,隐藏手动上下移(与 sortRefer 冲突) */
refOrderFollowsBody: {
type: Boolean,
default: false
}
},
methods: {
/** 与当前 chanFerForm 数组顺序一致:行内 index 置为 1-based 序号(排序/正文重排后同步) */
syncReferenceRowIndices() {
const list = this.chanFerForm;
if (!Array.isArray(list)) return;
list.forEach((row, i) => {
if (row && typeof row === 'object') {
const n = i;
if (row.index !== n) {
this.$set(row, 'index', n);
}
}
});
},
searchTitleByDOI(doi) {
this.$commonJS.searchTitleByDOI(doi);
},
formatTitle(title) {
if (!title) return '';
const reg = /\b(Retracted|Retraction)\b/gi;
@@ -369,11 +395,11 @@ export default {
// 2. 借鉴 EndNote对处理完批注的 HTML 进行引用联动渲染
// 支持 data-id="a,b,c" 单标签 或 多个相邻单 id 标签
const citeGroupRe = /(?:<autocite\s+data-id="([^"]+)"\s*><\/autocite>\s*)+/gi;
const citeGroupRe = /(?:<mycite\s+data-id="([^"]+)"\s*><\/mycite>\s*)+/gi;
return html.replace(citeGroupRe, (groupMatch) => {
const ids = [];
const innerRe = /<autocite\s+data-id="([^"]+)"\s*><\/autocite>/gi;
const innerRe = /<mycite\s+data-id="([^"]+)"\s*><\/mycite>/gi;
let m;
while ((m = innerRe.exec(groupMatch)) !== null) {
m[1].split(',').forEach((part) => {
@@ -1018,6 +1044,15 @@ export default {
}
},
watch: {
chanFerForm: {
handler() {
this.$nextTick(() => {
this.syncReferenceRowIndices();
});
},
immediate: true,
deep: true
},
SourceType: {
handler(newVal, oldVal) {
this.$nextTick(() => {
@@ -1990,9 +2025,12 @@ export default {
.action-icon.success:hover {
color: #67c23a;
}
.action-icon.danger:hover {
.action-icon.danger {
color: #f56c6c;
}
.action-icon.danger:hover {
color: #e85a5a;
}
.action-icon.disabled {
color: #e4e7ed;
cursor: not-allowed;

22
src/utils/autociteHtml.js Normal file
View File

@@ -0,0 +1,22 @@
/**
* 从 HTML 字符串中按出现顺序收集引用标签的 data-idmycite / autocite 与稿面、TinyMCE、历史库一致
* 支持逗号多 id、属性任意顺序、单/双引号
*/
export function extractAutociteIdsFromHtmlString(raw) {
if (!raw || typeof raw !== 'string') return [];
const ids = [];
const tagRe = /<(mycite|autocite)\b[^>]*>/gi;
let m;
while ((m = tagRe.exec(raw)) !== null) {
const tag = m[0];
const dbl = tag.match(/\bdata-id\s*=\s*"([^"]*)"/i);
const sgl = tag.match(/\bdata-id\s*=\s*'([^']*)'/i);
const val = dbl ? dbl[1] : sgl ? sgl[1] : '';
if (!val) continue;
val.split(',').forEach((part) => {
const id = part.trim();
if (id && !ids.includes(id)) ids.push(id);
});
}
return ids;
}