12 Commits

Author SHA1 Message Date
7f725cad52 Merge branch 'master' of https://git.nuttyreading.com/wangjinlei/tougao_web into Editorial-Board 2026-04-10 09:13:17 +08:00
797ca258f8 提交 2026-04-10 09:12:40 +08:00
9c44064f51 参考文献 2026-04-09 11:52:33 +08:00
7527a6ef54 参考文献的暂存 2026-04-09 09:39:58 +08:00
c4b86be0d5 提交 2026-04-08 17:25:19 +08:00
4fc78e1fe7 提交 2026-04-08 13:09:02 +08:00
0d35b76c3a 提交 2026-04-08 13:03:54 +08:00
36f6c02376 提交 2026-04-03 09:58:39 +08:00
dd07a03d7b Merge branch 'master' of https://git.nuttyreading.com/wangjinlei/tougao_web into Editorial-Board 2026-04-03 09:52:11 +08:00
620a35f958 提交 2026-04-03 09:05:56 +08:00
95b52b4d06 提交 2026-04-02 10:56:37 +08:00
1f29fb5baf 根据标签autoCite显示参考文献 2026-04-01 17:30:40 +08:00
19 changed files with 7499 additions and 482 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'));
@@ -226,8 +270,7 @@ export default {
str = this.transformHtmlString(processedContent, 'table',{ keepBr: true })
console.log("🚀 ~ extractContentWithoutOuterSpan888888 ~ str:", str);
// 创建一个临时的 DOM 元素来解析 HTML
const div = document.createElement('div');
@@ -917,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))[^>]+>/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))[^>]+>/g, ''); // 删除不需要的标签
inputHtml = inputHtml.replace(/<(?!\/?(strong|em|sub|sup|b|i|blue|wmath|myfigure|mytable|myh3|mycite))[^>]+>/g, ''); // 删除不需要的标签
}

View File

@@ -1157,6 +1157,28 @@ colTitle: 'Template title',
tmrEmailEditor: {
preview: 'Preview',
placeholder: 'Please enter email content'
},
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',
uncited: 'Uncited',
cancel: 'Cancel',
confirm: 'Confirm',
remove: 'Remove',
selected: 'Selected',
modifyRef: 'Edit citation',
removeRefTag: 'Remove citation',
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

@@ -1142,6 +1142,28 @@ const zh = {
tmrEmailEditor: {
preview: '预览效果',
placeholder: '请输入邮件内容'
},
wordCite: {
noRefs: '参考文献列表尚未加载,请稍候再试或刷新页面',
missingBoundRef: '当前绑定的参考文献不存在,请重新引用',
notFoundById: '未查询到编号{id}相关参考文献',
selectRef: '选择参考文献',
reference: '参考文献',
uncited: '未引用',
cancel: '取消',
confirm: '确认',
remove: '移除',
selected: '已选择',
modifyRef: '修改引用',
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

View File

@@ -1337,7 +1337,7 @@ export default {
// 5----重新获取加载参考文献
changeRefer(val) {
console.log('重新获取参考文献');
this.$api
.post('api/Production/getReferList', {
p_article_id: this.p_article_id
@@ -1348,7 +1348,7 @@ export default {
for (var i = 0; i < this.chanFerForm.length; i++) {
this.chanFerForm[i].edit_mark = 1;
}
console.log(this.chanFerForm);
})
.catch((err) => {
console.log(err);

View File

@@ -1433,7 +1433,7 @@ export default {
// 5----重新获取加载参考文献
changeRefer(val) {
console.log('重新获取参考文献');
this.$api
.post('api/Production/getReferList', {
p_article_id: this.p_article_id
@@ -1444,7 +1444,7 @@ export default {
for (var i = 0; i < this.chanFerForm.length; i++) {
this.chanFerForm[i].edit_mark = 1;
}
console.log(this.chanFerForm);
})
.catch((err) => {
console.log(err);

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>
@@ -63,6 +74,66 @@ export default {
},
articleId: {
default: ''
},
chanFerForm: {
type: Array,
default: () => []
},
/** 由父级根据全文(稿面 + Edit Content 草稿合并)扫描得到,与 word.vue citeMap 一致 */
bodyCiteIdOrder: {
type: Array,
default: () => []
},
/** mytable(data-id) -> 该表内已出现引用的最大全局序号(由父组件计算) */
tableLinkCiteMaxMap: {
type: Object,
default: () => ({})
},
/** 为 false 时不显示 Ref 工具栏按钮,也不自动在 LateX 后注入 insertRef用于图表标题等 */
showRefButton: {
type: Boolean,
default: true
}
},
computed: {
citeMap() {
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
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() {
@@ -124,22 +195,52 @@ 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: {
value: {
handler(val) {
if (!this.hasChange && this.hasInit) {
if (!this.hasChange && this.hasInit) {
this.$nextTick(() => {
this.handleSetContent(val);
});
}
},
immediate: true
},
chanFerForm: {
handler() {
this.$nextTick(() => {
window.tinymce.get(this.tinymceId).setContent(val);
if (this.editorInstance) {
this.renderAutociteInEditor(this.editorInstance);
}
});
},
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);
});
}
},
immediate: true
}
},
mounted() {
@@ -163,10 +264,411 @@ export default {
this.destroyTinymce();
},
methods: {
parseAutociteDataIds(attr) {
if (!attr || typeof attr !== 'string') return [];
return attr
.split(',')
.map((s) => s.trim())
.filter(Boolean);
},
/** 与 word.vue renderCiteLabelstooltip 行按引用序号排序 */
sortAutociteIdsByCiteNumber(ids) {
const uniq = [...new Set(ids.map(String))];
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));
});
},
/** 同 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._replaceBracketCitesInDocOrder(body, doc, numToId, { tableOffset: 0 });
});
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 */
}
},
/**
* 按文档顺序遍历,使「表格链接」后任意后续段落里的 [n] 都能用上该表的最大全局序号偏移。
* 旧实现只在同一父节点下、MYTABLE 与后续文本为兄弟时才生效MYTABLE 在上一段、角标在下一段时会失效。
*/
_replaceBracketCitesInDocOrder(node, doc, numToId, state) {
if (node.nodeType === 3) {
return this._processTextNodeForBracketCites(node, doc, numToId, state.tableOffset);
}
if (node.nodeType !== 1) return 0;
const name = node.nodeName;
if (name === 'AUTOCITE' || name === 'MYCITE' || name === 'WMATH' || name === 'SCRIPT' || name === 'STYLE') {
return 0;
}
const isMytable = name === 'MYTABLE' || (node.tagName && String(node.tagName).toLowerCase() === 'mytable');
if (isMytable) {
let total = 0;
Array.from(node.childNodes).forEach((c) => {
total += this._replaceBracketCitesInDocOrder(c, doc, numToId, state);
});
const tid = (node.getAttribute && node.getAttribute('data-id')) || '';
const map = this.tableLinkCiteMaxMap || {};
const tableMax = Number(map[String(tid)]);
if (!Number.isNaN(tableMax) && tableMax > 0) {
state.tableOffset = tableMax;
}
return total;
}
let total = 0;
Array.from(node.childNodes).forEach((c) => {
total += this._replaceBracketCitesInDocOrder(c, doc, numToId, state);
});
return total;
},
_processTextNodeForBracketCites(textNode, doc, numToId, tableOffset = 0) {
const text = textNode.textContent;
const re = /\[([\d\s,\-–—]+)\]/g;
let m;
let lastIndex = 0;
const pieces = [];
let replaced = 0;
let skippedSpecial = 0;
while ((m = re.exec(text)) !== null) {
const nums = this.parseBracketInnerToNumbers(m[1]);
// 规则:只要括号里包含 0 或 -1这一段不做解析替换但不影响其它正常 [1][2]
if (nums.some((n) => n === 0 || n === -1)) {
pieces.push({ type: 'text', s: text.slice(lastIndex, m.index) });
pieces.push({ type: 'text', s: m[0] });
lastIndex = m.index + m[0].length;
skippedSpecial++;
continue;
}
const ids = [];
const seen = new Set();
nums.forEach((n) => {
// 表格链接后的局部序号从 tableMax+1 开始接续1->max+1, 2->max+2 ...
const mappedNo = n > 0 && tableOffset > 0 ? n + tableOffset - 1 : n;
const id = numToId[mappedNo];
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);
if (skippedSpecial > 0) {
// 提示:仅提示一次即可;这里在节点级别提示可能重复,放到宏任务末尾合并展示
clearTimeout(this._autoLinkSkipToastTimer);
this._autoLinkSkipToastTimer = setTimeout(() => {
this.$message.info(`Skipped ${skippedSpecial} bracket cite(s) containing 0/-1.`);
}, 0);
}
return replaced;
},
renderAutociteInEditor(ed) {
const body = ed.getBody();
if (!body) return;
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 : [];
const refMap = {};
refs.forEach((item) => {
const key = item && item.p_refer_id != null ? String(item.p_refer_id) : '';
if (key) refMap[key] = item;
});
const citeMap = this.citeMap || {};
allAutocites.forEach((el) => {
ed.dom.setAttrib(el, 'contenteditable', 'false');
el.style.display = '';
const sortedAll = this.sortAutociteIdsByCiteNumber(
this.parseAutociteDataIds(el.getAttribute('data-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 parts = sortedIds.map((id) => {
const ref = refMap[String(id)];
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}`;
});
if (!label) {
ed.dom.remove(el);
return;
}
el.textContent = `[${label}]`;
el.style.display = '';
ed.dom.setAttrib(el, 'title', lines.join('\n'));
ed.dom.setAttrib(el, 'data-cite-missing', null);
});
this.padAutociteCaretPlaceholder(ed);
},
/** 段尾不可编辑节点后浏览器/TinyMCE 容易把后续输入新开 <p>,在引用后补零宽空格让光标留在同一段 */
padAutociteCaretPlaceholder(ed) {
const doc = ed.getDoc();
const body = doc.body;
if (!body) return;
body.querySelectorAll('mycite').forEach((el) => {
const next = el.nextSibling;
if (next === null) {
el.parentNode.appendChild(doc.createTextNode('\u200b'));
return;
}
if (next.nodeType === 3) {
if (next.textContent === '\u200b') return;
if (next.textContent === '') {
next.textContent = '\u200b';
}
}
});
},
insertAutocite(refIds) {
const ed = this.editorInstance;
if (!ed) return;
const ids = (Array.isArray(refIds) ? refIds : [refIds]).map(String);
const dataId = ids.join(',');
const escaped = dataId.replace(/"/g, '&quot;');
if (this._editingAutocite) {
this._editingAutocite.setAttribute('data-id', dataId);
this._editingAutocite = null;
ed.fire('change');
} else {
if (this._refBookmark) {
ed.selection.moveToBookmark(this._refBookmark);
}
ed.insertContent(`<mycite data-id="${escaped}" contenteditable="false"></mycite>&#8203;`);
}
this.renderAutociteInEditor(ed);
},
removeAutocite() {
const ed = this.editorInstance;
if (!ed || !this._editingAutocite) return;
ed.dom.remove(this._editingAutocite);
this._editingAutocite = null;
ed.fire('change');
},
/**
* 从当前编辑的 mycite 中去掉指定 p_refer_id去掉后无 id 则删除整段标签。
* ids 为空:删除整段(与 removeAutocite 一致)。
*/
stripAutociteIds(idsToRemove) {
const ed = this.editorInstance;
if (!ed || !this._editingAutocite) return;
const remove = new Set((idsToRemove || []).map((id) => String(id)));
const el = this._editingAutocite;
this._editingAutocite = null;
if (remove.size === 0) {
ed.dom.remove(el);
ed.fire('change');
return;
}
const parts = this.parseAutociteDataIds(el.getAttribute('data-id') || '');
const remaining = parts.filter((id) => !remove.has(String(id)));
if (remaining.length === 0) {
ed.dom.remove(el);
} else {
const sorted = this.sortAutociteIdsByCiteNumber(remaining);
el.setAttribute('data-id', sorted.join(','));
this.renderAutociteInEditor(ed);
}
ed.fire('change');
},
/** TinyMCE 会剔除「空」的行内标签;空 mycite 必须在入编辑器前占位,否则合并引用 [13] 等整段消失 */
normalizeAutociteHtmlForEditor(html) {
if (!html || typeof html !== 'string') return html;
/** 角标显示一律由 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) {
if (!this.editorInstance) return;
let finalContent = val || '';
let finalContent = this.normalizeAutociteHtmlForEditor(val || '');
// 你的业务逻辑:自动包裹 <p> 标签
if (!finalContent.includes('wordTableHtml') && !finalContent.startsWith('<p>')) {
finalContent = '<p>' + finalContent + '</p>';
@@ -174,8 +676,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);
}
@@ -320,7 +825,10 @@ export default {
},
getDetail(val) {
if (this.hasInit == true) {
this.$nextTick(() => window.tinymce.get(this.tinymceId).setContent(val));
this.$nextTick(() => {
const ed = window.tinymce.get(this.tinymceId);
if (ed) ed.setContent(this.normalizeAutociteHtmlForEditor(val || ''));
});
}
},
//将字符串添加到富文本编辑器中
@@ -431,7 +939,7 @@ export default {
return new Blob([u8arr], { type: mime });
},
formatHtml(val) {
const rawValue = val || ''; // 处理 null
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 标签
@@ -456,7 +964,7 @@ export default {
}
},
getSafeContent(val) {
const rawValue = val || '';
const rawValue = this.normalizeAutociteHtmlForEditor(val || '');
const cleanEmptyTags = /<([a-zA-Z1-6]+)\b[^>]*><\/\1>/g;
const replaceSpaces = /\s+(?=<)|(?<=>)\s+/g;
@@ -497,9 +1005,10 @@ export default {
window.tinymce.init({
..._this.tinymceOtherInit,
trim_span_elements: false, // 禁止修剪内联标签周围的空格
extended_valid_elements: 'blue[*]',
custom_elements: 'blue',
valid_children: '+blue[#text|i|em|b|strong|span],+body[blue],+p[blue]',
extended_valid_elements: 'blue[*],mycite[*]',
/* ~ 前缀:按行内(类似 span处理否则默认当块级会拆段导致引用后强制换行 */
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}`,
@@ -509,7 +1018,7 @@ export default {
valid_elements:
this.type == 'table'
? '*[*]'
: `img[src|alt|width|height],strong,em,sub,sup,blue,table,b,i,myfigure,mytable,wmath${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,
@@ -541,6 +1050,34 @@ export default {
font-weight: bold !important;
}
/* inline 与 blue 引用一致,避免 inline-block 在行尾产生多余换行感 */
mycite {
display: inline;
vertical-align: baseline;
color: rgb(0, 130, 170);
// font-weight: bold;
cursor: pointer;
padding: 0 2px;
border-radius: 3px;
background-color: rgba(0, 130, 170, 0.08);
user-select: all;
font-size: 12px;
white-space: nowrap;
}
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%,
100% {
@@ -559,7 +1096,37 @@ export default {
},
body_class: 'panel-body ',
object_resizing: false,
toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
toolbar: (function () {
const tb = _this.toolbar;
const hasCustom =
(Array.isArray(tb) && tb.length > 0) ||
(typeof tb === 'string' && tb.length > 0);
const refInsert = _this.showRefButton ? ' insertRef' : '';
/* 自动匹配 [n] 为角标:底部蓝色按钮,见模板 tinymce-autolink-footer */
/* 与 content.vue 默认一致;含 insertRef 由 showRefButton 控制 */
const defaultToolbar = [
`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) => {
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, // 关闭底部状态栏
custom_colors: false,
@@ -626,7 +1193,27 @@ export default {
let currentPasteImages = [];
_this.$commonJS.initEditorButton(_this, ed);
var currentWmathElement = null;
ed.ui.registry.addButton('insertRef', {
text: 'Reference',
tooltip: 'Insert Reference',
onAction: function () {
_this._refBookmark = ed.selection.getBookmark(2);
_this._editingAutocite = null;
_this.$emit('openRefSelector', { currentRefIds: [] });
}
});
ed.on('click', function (e) {
const autociteEl = e.target.closest('mycite');
if (autociteEl) {
const dataIds = _this.parseAutociteDataIds(autociteEl.getAttribute('data-id'));
_this._refBookmark = ed.selection.getBookmark(2);
_this._editingAutocite = autociteEl;
_this.$emit('openRefSelector', { currentRefIds: dataIds });
return;
}
const wmathElement = e.target.closest('wmath');
if (wmathElement) {
currentWmathElement = wmathElement; // 保存当前点击的元素
@@ -757,6 +1344,8 @@ export default {
subtree: true,
characterData: true
});
_this.mountAutoLinkFooterInsideEditor(ed);
});
// 定义自定义按钮
@@ -791,14 +1380,20 @@ export default {
const editorBody = ed.getBody();
ed.dom.select('wmath', editorBody).forEach(function (wmathElement) {
ed.dom.setAttrib(wmathElement, 'contenteditable', 'false');
// ed.dom.addClass(wmathElement, 'non-editable-wmath');
});
_this.renderAutociteInEditor(ed);
e.content = e.content.replace(/<strong>/g, '<b>').replace(/<\/strong>/g, '</b>');
e.content = e.content.replace(/<em>/g, '<i>').replace(/<\/em>/g, '</i>');
});
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(/<\/i>/g, '</em>');
e.content = e.content.replace(/<i>/g, '<em>').replace(/<\/em>/g, '</em>');
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 '<mycite' + clean + '></mycite>';
});
});
},
paste_preprocess: function (plugin, args) {
@@ -958,13 +1553,15 @@ export default {
//销毁富文本
destroyTinymce() {
this.onClear();
this.restoreAutoLinkFooterDom();
if (window.tinymce.get(this.tinymceId)) {
window.tinymce.get(this.tinymceId).destroy();
}
},
//设置内容
setContent(value) {
window.tinymce.get(this.tinymceId).setContent(value);
const ed = window.tinymce.get(this.tinymceId);
if (ed) ed.setContent(this.normalizeAutociteHtmlForEditor(value || ''));
},
//获取内容
async getContent(type) {
@@ -973,7 +1570,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);
},
@@ -1058,6 +1656,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

@@ -191,10 +191,23 @@ export default {
}
},
sortedFilteredFields() {
const kw = (this.fieldSearchText || '').trim().toLowerCase();
const kwRaw = String(this.fieldSearchText || '');
const normalize = (s) =>
String(s || '')
.trim()
.replace(/\s+/g, ' ')
.toLowerCase();
const tokens = kwRaw
? kwRaw
.split(/[\r\n,;]+/g)
.map((s) => normalize(s))
.filter(Boolean)
: [];
const list = (this.availableFields || []).filter((item) => {
if (!kw) return true;
return String(item.label || '').toLowerCase().includes(kw);
if (!tokens.length) return true;
const label = normalize(item.label || '');
// 严格匹配:必须与字段名完全一致(忽略大小写与空白差异)
return tokens.some((t) => t === label);
});
return list.slice().sort((a, b) => String(a.label || '').localeCompare(String(b.label || '')));
},

View File

@@ -10,13 +10,19 @@
ref="tinymceChild1"
:wordStyle="wordStyle"
:isAutomaticUpdate="isAutomaticUpdate"
:showRefButton="showRefButton"
@getContent="getContent"
@openLatexEditor="openLatexEditor"
@openRefSelector="openRefSelector"
@updateChange="updateChange"
@input="onTinymceInput"
:value="value"
:chanFerForm="chanFerForm"
:bodyCiteIdOrder="bodyCiteIdOrder"
:tableLinkCiteMaxMap="tableLinkCiteMaxMap"
:typesettingType="typesettingType"
class="paste-area text-container"
:toolbar="!isAutomaticUpdate?['bold italic |customBlue removeBlue|LateX| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace']:['bold italic |customBlue removeBlue| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace']"
:toolbar="toolbarConfig"
style="
/* white-space: pre-line; */
line-height: 12px;
@@ -36,10 +42,40 @@
<script>
import Tinymce from '@/components/page/components/Tinymce';
export default {
props: ['value','isAutomaticUpdate','height','id'],
props: {
value: {},
isAutomaticUpdate: {},
height: {},
id: {},
chanFerForm: {},
/** 全文 mycite 顺序,与稿面 word.vue 的 bodyCiteIdOrder 一致,用于弹窗内 [1][2] 与参考文献重排 */
bodyCiteIdOrder: {
type: Array,
default: () => []
},
/** mytable(data-id) 对应表格内已出现引用的最大全局序号,用于正文中 “See Table X. And see [2,3]” 的偏移映射 */
tableLinkCiteMaxMap: {
type: Object,
default: () => ({})
},
/** false标题等场景不显示 Ref */
showRefButton: {
type: Boolean,
default: true
}
},
components: {
Tinymce
},
computed: {
toolbarConfig() {
const refBtn = this.showRefButton ? ' insertRef' : '';
if (!this.isAutomaticUpdate) {
return [`bold italic |customBlue removeBlue|LateX${refBtn}| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace`];
}
return [`bold italic |customBlue removeBlue${refBtn}| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace`];
}
},
watch: {
lineStyle() {}
},
@@ -85,8 +121,40 @@ export default {
this.$refs.tinymceChild1.getContent(type);
},
getContent(type, content) {
this.$emit('getContent', type, content);
},
openRefSelector(data) {
this.$emit('openRefSelector', data);
},
onTinymceInput(html) {
this.$emit('editorInput', html);
},
insertAutocite(refId) {
this.$refs.tinymceChild1.insertAutocite(refId);
},
removeAutocite() {
this.$refs.tinymceChild1.removeAutocite();
},
/** 从当前 mycite 的 data-id 中移除指定 id无剩余则删标签未选 id 时删整段 */
stripAutociteIds(ids) {
if (this.$refs.tinymceChild1 && typeof this.$refs.tinymceChild1.stripAutociteIds === 'function') {
this.$refs.tinymceChild1.stripAutociteIds(ids);
}
},
/** 参考文献异步到达后刷新编辑器内 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);
}
}
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -239,18 +239,47 @@ export default {
openAddDialog() { this.addDialogVisible = true; },
resetAddForm() { this.addForm = { field: '', runNow: false }; this.addLoading = false; },
async submitAddKeyword() {
if (!this.addForm.field.trim()) return this.$message.warning(this.$t('crawlTask.enterKeyword'));
const raw = String(this.addForm.field || '').trim();
if (!raw) return this.$message.warning(this.$t('crawlTask.enterKeyword'));
// 支持一次录入多个:按换行/逗号/分号拆分,去重
const fields = raw
.split(/[\r\n,;]+/g)
.map((s) => s.trim())
.filter(Boolean);
const uniqFields = Array.from(new Set(fields));
if (uniqFields.length === 0) return this.$message.warning(this.$t('crawlTask.enterKeyword'));
this.addLoading = true;
try {
const res = await this.$api.post('api/expert_manage/addFetchField', { field: this.addForm.field });
if (res.code === 0) {
this.$message.success(this.$t('crawlTask.addKeywordSuccess'));
if (this.addForm.runNow) {
await this.$api.post('api/expert_finder/fetchOneField', { field: this.addForm.field });
const ok = [];
const fail = [];
for (const field of uniqFields) {
try {
const res = await this.$api.post('api/expert_manage/addFetchField', { field });
if (res && res.code === 0) {
ok.push(field);
if (this.addForm.runNow) {
// 不阻塞整体添加run once 失败也记录,不影响已添加成功
try {
await this.$api.post('api/expert_finder/fetchOneField', { field });
} catch (e) {
/* ignore */
}
}
} else {
fail.push({ field, msg: (res && res.msg) || this.$t('crawlTask.operationFail') });
}
} catch (e) {
fail.push({ field, msg: this.$t('crawlTask.operationFail') });
}
this.addDialogVisible = false;
this.fetchList();
}
if (ok.length > 0) {
this.$message.success(`${this.$t('crawlTask.addKeywordSuccess')} (${ok.length}/${uniqFields.length})`);
}
if (fail.length > 0) {
this.$message.warning(`Failed: ${fail.map((x) => x.field).join(', ')}`);
}
this.addDialogVisible = false;
this.fetchList();
} finally { this.addLoading = false; }
}
}

View File

@@ -71,6 +71,7 @@
Special note: If the number of authors is 6 or fewer, all authors should be listed.
</div>
<el-button
v-if="canDelete"
@click="StepBNext(1)"
type="primary"
plain
@@ -110,7 +111,7 @@
<span @click="handleContainerClick" style="margin-left: 5px" v-html="getRepeatRefHtml()"> </span>
</div>
<div class="topBtnBox btns" v-if="chanFerForm.length > 0 && role == 'editor'">
<div class="topBtnBox btns" v-if="chanFerForm.length > 0 && role == 'editor' && canDelete">
<el-button type="primary" plain @click="selectAllRef">Select all</el-button>
<el-button type="success" plain @click="toggleSelection">Select none</el-button>
<el-button type="danger" plain @click="deleteSomeRefs" :disabled="multipleSelection.length > 0 ? false : true"
@@ -154,11 +155,10 @@
class="status ok"
:class="scope.row.refer_type == 'journal' ? getJournalDateno(scope.row.dateno, 'status') : ''"
v-if="
(
(scope.row.refer_type == 'journal' && scope.row.doilink != '' && scope.row.cs == 1) ||
(scope.row.refer_type == 'book' && scope.row.isbn != '' && scope.row.cs == 1)
) && scope.row.retract == 0
"
((scope.row.refer_type == 'journal' && scope.row.doilink != '' && scope.row.cs == 1) ||
(scope.row.refer_type == 'book' && scope.row.isbn != '' && scope.row.cs == 1)) &&
scope.row.retract == 0
"
>
<i class="el-icon-circle-check"></i>
</span>
@@ -173,7 +173,9 @@
<!-- journal 形式 -->
<div style="text-align: left" v-if="scope.row.refer_type == 'journal'" class="reference-item">
<p>
{{ scope.row.author }}&nbsp;<span v-html="formatTitle(scope.row.title)"></span>. &nbsp;<em>{{ scope.row.joura }}</em
{{ scope.row.author }}&nbsp;<span v-html="formatTitle(scope.row.title)"></span>. &nbsp;<em>{{
scope.row.joura
}}</em
>.&nbsp;<span :class="getJournalDateno(scope.row.dateno, 'title')">{{ scope.row.dateno }}</span
>.<br />
</p>
@@ -181,13 +183,16 @@
</div>
<!-- book 形式 -->
<div style="text-align: left" v-if="scope.row.refer_type == 'book'" class="reference-item">
<p>{{ scope.row.author }}&nbsp;<span v-html="formatTitle(scope.row.title)"></span>.&nbsp;{{ scope.row.dateno }}.&nbsp;<br /></p>
<p>
{{ scope.row.author }}&nbsp;<span v-html="formatTitle(scope.row.title)"></span>.&nbsp;{{
scope.row.dateno
}}.&nbsp;<br />
</p>
<a class="doiLink" :href="scope.row.isbn" target="_blank">{{ scope.row.isbn }}</a>
</div>
<!-- other 形式 -->
<p class="wrongLine reference-item" style="text-align: left" v-if="scope.row.refer_type == 'other'">
<span v-html="formatTitle(scope.row.refer_frag)"></span>
</p>
</template>
</el-table-column>
@@ -242,7 +247,7 @@
</div>
</el-table-column>
</el-table>
<div class="bottomBtnBox btns" v-if="chanFerForm.length > 0 && role == 'editor'">
<div class="bottomBtnBox btns" v-if="chanFerForm.length > 0 && role == 'editor'&& canDelete">
<el-button type="primary" plain @click="selectAllRef">Select all</el-button>
<el-button type="success" plain @click="toggleSelection">Select none</el-button>
<el-button type="danger" plain @click="deleteSomeRefs" :disabled="multipleSelection.length > 0 ? false : true"
@@ -546,6 +551,7 @@ export default {
},
addLoading: false,
editboxVisible: false,
canDelete: true,
multipleSelection: [] // 多选
};
},
@@ -572,18 +578,19 @@ export default {
}
},
methods: {
formatTitle(title) {
if (!title) return '';
// 使用正则匹配,'gi' 表示全局匹配且不区分大小写
// \b 确保是完整单词匹配,防止误伤含有这些字母的其他单词
const reg = /\b(Retracted|Retraction)\b/gi;
return title.replace(reg, (match) => {
return `<span style="color: red; font-weight: bold;">${match}</span>`;
});
}
,
getCurrentRoute() {
this.canDelete = !['/articleListEditor_B1'].includes(this.$route.path);
},
formatTitle(title) {
if (!title) return '';
// 使用正则匹配,'gi' 表示全局匹配且不区分大小写
// \b 确保是完整单词匹配,防止误伤含有这些字母的其他单词
const reg = /\b(Retracted|Retraction)\b/gi;
return title.replace(reg, (match) => {
return `<span style="color: red; font-weight: bold;">${match}</span>`;
});
},
getJournalDateno(dateno, type) {
if (dateno && typeof dateno === 'string') {
const hasInvalidColon = !dateno.includes(':') || (dateno.includes(':') && dateno.split(':').pop().trim() === '');
@@ -682,7 +689,7 @@ export default {
.then((res) => {
if (res.status == 1) {
return res.data;
}
}
throw res.msg;
})
.catch((err) => {
@@ -697,6 +704,7 @@ export default {
return {};
},
init(e) {
this.getCurrentRoute();
this.chanFerForm = e;
this.bijiao();
////console.log('更新更新')

File diff suppressed because it is too large Load Diff

View File

@@ -279,11 +279,14 @@
journal_id: this.query.journal_id || (this.journalList[0] ? this.journalList[0].journal_id : null),
account: '',
password: '',
// password: '123456qwe..%%%',
smtp_from_name: '',
smtp_host: 'mail.tmrjournals.co.nz',
// smtp_host: 'smtp.mxhichina.com',
smtp_port: '465',
smtp_encryption: 'ssl',
imap_host: 'mail.tmrjournals.co.nz',
// imap_host: 'imap.qiye.aliyun.com',
imap_port: '993',
};
this.dialogVisible = true;

View File

@@ -124,7 +124,7 @@ export default {
},
// 5----重新获取加载参考文献
changeRefer(val) {
console.log('重新获取参考文献')
this.$api
.post('api/Production/getReferList', {
'p_article_id': this.p_article_id

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;
}

View File

@@ -0,0 +1,161 @@
/**
* Main_List 全文首次出现顺序(与 Edit Content / 稿面 / 底部参考文献列表共用)。
* 正文中 <mytable> 占位处需先插入对应表格段在 Main_List 中的表题/表体/表注引用,再计其后的 mycite。
*/
import { extractAutociteIdsFromHtmlString } from '@/utils/autociteHtml.js';
export function collectCiteStringsDeep(source, out, depth = 0) {
if (!source || depth > 6) return;
if (typeof source === 'string') {
const s = source.trim();
if (!s) return;
const maybeCite = /<(?:mycite|autocite)\b/i.test(s) || /\[[\d\s,\-–—]+\]/.test(s);
if (maybeCite) out.push(s);
return;
}
if (Array.isArray(source)) {
source.forEach((item) => collectCiteStringsDeep(item, out, depth + 1));
return;
}
if (typeof source === 'object') {
Object.keys(source).forEach((k) => {
const v = source[k];
collectCiteStringsDeep(v, out, depth + 1);
});
}
}
export function collectCiteHtmlSourcesForMainListItem(p, draftAmId, draftHtml) {
if (draftAmId != null && p && p.am_id == draftAmId && draftHtml != null) {
return [draftHtml];
}
const out = [];
const typ = p != null && p.type != null ? Number(p.type) : NaN;
if (typ === 2 && p.table) {
const t = p.table;
if (typeof t.title === 'string' && t.title.trim()) out.push(t.title);
if (typeof t.html_data === 'string' && t.html_data.trim()) {
out.push(t.html_data);
} else {
const pushRows = (rows) => {
if (!Array.isArray(rows)) return;
rows.forEach((row) => {
if (!Array.isArray(row)) return;
row.forEach((cell) => {
if (cell && typeof cell.text === 'string' && cell.text) out.push(cell.text);
});
});
};
pushRows(t.tableHeader);
pushRows(t.tableContent);
}
if (typeof t.note === 'string' && t.note.trim()) out.push(t.note);
collectCiteStringsDeep(t, out);
return out;
}
if (typ === 1 && p.image) {
const img = p.image;
if (typeof img.title === 'string' && img.title.trim()) out.push(img.title);
if (typeof img.note === 'string' && img.note.trim()) out.push(img.note);
if (out.length === 0) {
if (p && typeof p.content === 'string' && p.content.trim()) out.push(p.content);
if (p && typeof p.text === 'string' && p.text.trim()) out.push(p.text);
}
return out;
}
if (p && typeof p.text === 'string' && p.text.trim()) out.push(p.text);
if (p && typeof p.content === 'string' && p.content.trim()) out.push(p.content);
return out;
}
export function findMainListTableByLinkId(mainList, rawId) {
if (rawId == null || String(rawId).trim() === '') return null;
const sid = String(rawId).trim();
const list = Array.isArray(mainList) ? mainList : [];
return (
list.find((p) => {
if (!p || Number(p.type) !== 2) return false;
if (String(p.amt_id) === sid) return true;
if (String(p.am_id) === sid) return true;
if (p.p_main_table_id != null && String(p.p_main_table_id) === sid) return true;
if (p.table && p.table.amt_id != null && String(p.table.amt_id) === sid) return true;
if (p.table && p.table.am_id != null && String(p.table.am_id) === sid) return true;
if (p.table && p.table.p_main_table_id != null && String(p.table.p_main_table_id) === sid)
return true;
if (p.table && p.table.table_id != null && String(p.table.table_id) === sid) return true;
return false;
}) || null
);
}
/**
* @param {object} [options]
* @param {number|string|null} [options.tableDraftAmId] 正在编辑的表格段 am_idEdit Table 抽屉)
* @param {string|null} [options.tableDraftHtml] 与该表对应的 Title+表体+Note 合并稿;正文里 mytable 展开到该表时用此稿替代 Main_List 已存内容,使「全文 mytable 之前」的序号与抽屉内一致
*/
export function extractAutociteOrderFromDraftHtmlWithInlineTables(html, mainList, options = {}) {
const order = [];
const pushUnique = (id) => {
if (id && !order.includes(id)) order.push(id);
};
const pushFromRaw = (raw) => {
extractAutociteIdsFromHtmlString(raw).forEach((id) => pushUnique(id));
};
if (!html || typeof html !== 'string') return order;
const re = /<mytable\b[^>]*>[\s\S]*?<\/mytable>/gi;
let last = 0;
let m;
while ((m = re.exec(html)) !== null) {
const before = html.slice(last, m.index);
if (before) pushFromRaw(before);
const openEnd = html.indexOf('>', m.index) + 1;
const openTag = html.slice(m.index, openEnd);
const dm = openTag.match(/\bdata-id\s*=\s*["']([^"']*)["']/i);
const tid = dm ? String(dm[1]).trim() : '';
const tableP = findMainListTableByLinkId(mainList, tid);
if (tableP) {
const useDraft =
options.tableDraftAmId != null &&
options.tableDraftHtml != null &&
String(tableP.am_id) === String(options.tableDraftAmId);
const sources = useDraft
? [options.tableDraftHtml]
: collectCiteHtmlSourcesForMainListItem(tableP, null, null);
sources.forEach((raw) => pushFromRaw(raw));
}
last = m.index + m[0].length;
}
const after = html.slice(last);
if (after) pushFromRaw(after);
return order;
}
export function extractAutociteOrderFromMainList(mainList, draftAmId, draftHtml, options = {}) {
const list = Array.isArray(mainList) ? mainList : [];
const order = [];
list.forEach((p) => {
if (draftAmId != null && p && p.am_id == draftAmId && draftHtml != null) {
extractAutociteOrderFromDraftHtmlWithInlineTables(draftHtml, list, options).forEach((id) => {
if (!order.includes(id)) order.push(id);
});
return;
}
const candidates = collectCiteHtmlSourcesForMainListItem(p, draftAmId, draftHtml);
candidates.forEach((raw) => {
if (typeof raw === 'string' && /<mytable\b/i.test(raw)) {
extractAutociteOrderFromDraftHtmlWithInlineTables(raw, list, options).forEach((id) => {
if (!order.includes(id)) order.push(id);
});
} else {
extractAutociteIdsFromHtmlString(raw).forEach((id) => {
if (!order.includes(id)) order.push(id);
});
}
});
});
return order;
}