提交
This commit is contained in:
@@ -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, ''); // 删除不需要的标签
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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 均在列表中时对全局序号做 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(', ');
|
||||
},
|
||||
/** 全局角标序号 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]、[1–4] 等括号内数字列表(不含方括号) */
|
||||
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]、[1–3]、[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>​`);
|
||||
ed.insertContent(`<mycite data-id="${escaped}" contenteditable="false"></mycite>​`);
|
||||
}
|
||||
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 必须在入编辑器前占位,否则合并引用 [1–3] 等整段消失 */
|
||||
/** TinyMCE 会剔除「空」的行内标签;空 mycite 必须在入编辑器前占位,否则合并引用 [1–3] 等整段消失 */
|
||||
normalizeAutociteHtmlForEditor(html) {
|
||||
if (!html || typeof html !== 'string') return html;
|
||||
let out = html.replace(/<autocite([^>]*)>\s*<\/autocite>/gi, '<autocite$1>​</autocite>');
|
||||
// 与 getSafeContent 标签边界逻辑一致:autocite 左右水平空白改为 ,不换行符(避免吃掉段间 \n)
|
||||
out = out.replace(/[^\S\r\n]+(?=<autocite\b)/gi, ' ');
|
||||
out = out.replace(/(?<=<\/autocite>)[^\S\r\n]+/gi, ' ');
|
||||
/** 角标显示一律由 renderAutociteInEditor 根据 data-id 生成,禁止保留库内遗留的 [1-4]、[1–4] 等旧文案 */
|
||||
let out = html.replace(/<mycite([^>]*)>[\s\S]*?<\/mycite>/gi, '<mycite$1>​</mycite>');
|
||||
// 外侧:连续空格 / 合并为单个 ,避免「普通空格 + 」叠成大缝
|
||||
out = out.replace(/(?:\s| | )+(?=<mycite\b)/gi, ' ');
|
||||
out = out.replace(/(?<=<\/mycite>)(?:\s| | )+/gi, ' ');
|
||||
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*​/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*​/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(/ /g, ' '); // 将所有 替换为空格
|
||||
content = content.replace(/ /g, ' '); // 先统一为空格
|
||||
content = this.normalizeAutociteHtmlForEditor(content); // mycite 两侧水平空白以 存库(与入编辑器一致)
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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]、[1–3] 等按全局序号匹配为 <mycite> */
|
||||
convertBracketRefsToAutocite() {
|
||||
const t = this.$refs.tinymceChild1;
|
||||
if (t && typeof t.convertPlainBracketCitesToAutocite === 'function') {
|
||||
return t.convertPlainBracketCitesToAutocite();
|
||||
}
|
||||
return { replaced: 0 };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
},
|
||||
/** 多项连续时合并为 1–3;仅用于「同组多 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 行按正文序号排列,与 [2–4, 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) => {
|
||||
// 库内若曾写入 [1] 等,与外侧空格叠缝;先规范为空标签再按组合并渲染
|
||||
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} 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, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<');
|
||||
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, '<');
|
||||
|
||||
@@ -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_num:undefined/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;
|
||||
|
||||
@@ -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
22
src/utils/autociteHtml.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 从 HTML 字符串中按出现顺序收集引用标签的 data-id(mycite / 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;
|
||||
}
|
||||
Reference in New Issue
Block a user