提交
This commit is contained in:
@@ -71,7 +71,51 @@ function findExtentElement(blipElement) {
|
|||||||
|
|
||||||
export default {
|
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) {
|
getJournalTypeName(value) {
|
||||||
|
|
||||||
var list = JSON.parse(localStorage.getItem('journalTypeDataAll'));
|
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` 外的所有标签)
|
// 2. 删除所有不需要的标签 (除 `strong`, `em`, `sub`, `sup`, `b`, `i` 外的所有标签)
|
||||||
if (type == 'table') {
|
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 {
|
} 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: {
|
wordCite: {
|
||||||
noRefs: 'Reference list is not loaded yet. Please wait or refresh the page.',
|
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}',
|
notFoundById: 'No reference found for citation ID {id}',
|
||||||
selectRef: 'Select Reference',
|
selectRef: 'Select Reference',
|
||||||
reference: 'Reference',
|
reference: 'Reference',
|
||||||
@@ -1168,8 +1169,15 @@ colTitle: 'Template title',
|
|||||||
remove: 'Remove',
|
remove: 'Remove',
|
||||||
selected: 'Selected',
|
selected: 'Selected',
|
||||||
modifyRef: 'Edit citation',
|
modifyRef: 'Edit citation',
|
||||||
removeRefTag: 'Remove citation',
|
removeRefTag: 'Reference remove',
|
||||||
citeUpdateFail: 'Could not update the citation in the text. Try again or use Edit.'
|
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: {
|
wordCite: {
|
||||||
noRefs: '参考文献列表尚未加载,请稍候再试或刷新页面',
|
noRefs: '参考文献列表尚未加载,请稍候再试或刷新页面',
|
||||||
|
missingBoundRef: '当前绑定的参考文献不存在,请重新引用',
|
||||||
notFoundById: '未查询到编号{id}相关参考文献',
|
notFoundById: '未查询到编号{id}相关参考文献',
|
||||||
selectRef: '选择参考文献',
|
selectRef: '选择参考文献',
|
||||||
reference: '参考文献',
|
reference: '参考文献',
|
||||||
@@ -1153,8 +1154,15 @@ const zh = {
|
|||||||
remove: '移除',
|
remove: '移除',
|
||||||
selected: '已选择',
|
selected: '已选择',
|
||||||
modifyRef: '修改引用',
|
modifyRef: '修改引用',
|
||||||
removeRefTag: '移除引用',
|
removeRefTag: '移除参考文献',
|
||||||
citeUpdateFail: '未能更新正文中的引用标签,请重试或进入编辑修改'
|
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>
|
<template>
|
||||||
<div class="tinymce-container editor-container">
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
@@ -67,17 +78,57 @@ export default {
|
|||||||
chanFerForm: {
|
chanFerForm: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
/** 由父级根据全文(稿面 + Edit Content 草稿合并)扫描得到,与 word.vue citeMap 一致 */
|
||||||
|
bodyCiteIdOrder: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
/** 为 false 时不显示 Ref 工具栏按钮,也不自动在 LateX 后注入 insertRef(用于图表标题等) */
|
||||||
|
showRefButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
citeMap() {
|
citeMap() {
|
||||||
const map = {};
|
|
||||||
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
||||||
refs.forEach((row, idx) => {
|
const refIdSet = new Set(
|
||||||
const key = row && row.p_refer_id != null ? String(row.p_refer_id) : '';
|
refs.map((r) => (r && r.p_refer_id != null ? String(r.p_refer_id) : '')).filter(Boolean)
|
||||||
if (key) map[key] = idx + 1;
|
);
|
||||||
|
|
||||||
|
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;
|
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() {
|
data() {
|
||||||
@@ -139,7 +190,9 @@ export default {
|
|||||||
hasChange: false,
|
hasChange: false,
|
||||||
hasInit: false,
|
hasInit: false,
|
||||||
editorInstance: null,
|
editorInstance: null,
|
||||||
tinymceId: this.id || 'vue-tinymce-' + +new Date()
|
tinymceId: this.id || 'vue-tinymce-' + +new Date(),
|
||||||
|
/** 将底部栏移入 TinyMCE 容器前,记录原 DOM 位置以便销毁时移回 */
|
||||||
|
_autoLinkFooterRestore: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -162,6 +215,27 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
deep: true
|
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() {
|
mounted() {
|
||||||
@@ -185,24 +259,6 @@ export default {
|
|||||||
this.destroyTinymce();
|
this.destroyTinymce();
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
parseAutociteDataIds(attr) {
|
||||||
if (!attr || typeof attr !== 'string') return [];
|
if (!attr || typeof attr !== 'string') return [];
|
||||||
return attr
|
return attr
|
||||||
@@ -225,10 +281,205 @@ export default {
|
|||||||
return String(a).localeCompare(String(b));
|
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) {
|
renderAutociteInEditor(ed) {
|
||||||
const body = ed.getBody();
|
const body = ed.getBody();
|
||||||
if (!body) return;
|
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;
|
if (!allAutocites.length) return;
|
||||||
|
|
||||||
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
||||||
@@ -238,25 +489,63 @@ export default {
|
|||||||
if (key) refMap[key] = item;
|
if (key) refMap[key] = item;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const citeMap = this.citeMap || {};
|
||||||
|
|
||||||
allAutocites.forEach((el) => {
|
allAutocites.forEach((el) => {
|
||||||
ed.dom.setAttrib(el, 'contenteditable', 'false');
|
ed.dom.setAttrib(el, 'contenteditable', 'false');
|
||||||
el.style.display = '';
|
el.style.display = '';
|
||||||
const ids = this.parseAutociteDataIds(el.getAttribute('data-id'));
|
const sortedAll = this.sortAutociteIdsByCiteNumber(
|
||||||
const sortedIds = this.sortAutociteIdsByCiteNumber(ids);
|
this.parseAutociteDataIds(el.getAttribute('data-id'))
|
||||||
const nums = sortedIds.map((id) => this.citeMap[String(id)]).filter((n) => n != null);
|
);
|
||||||
const label = nums.length > 0 ? this.formatCiteNumbers(nums) : '?';
|
/** 已删除的文献 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)];
|
const ref = refMap[String(id)];
|
||||||
if (!ref) return this.$t('wordCite.notFoundById', { id: no === '?' ? id : no });
|
const no = ref ? citeMap[String(id)] : null;
|
||||||
const content = ref.refer_frag || [ref.author, ref.title, ref.joura, ref.dateno].filter(Boolean).join(' ').trim() || '[?]';
|
const num = no != null && no !== '' ? String(no) : null;
|
||||||
const doi = ref.doilink || ref.isbn || ref.doi || '[?]';
|
return { id, ref, num };
|
||||||
return `[${no}] ${content}\nDOI: ${doi}`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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.textContent = `[${label}]`;
|
||||||
|
el.style.display = '';
|
||||||
ed.dom.setAttrib(el, 'title', lines.join('\n'));
|
ed.dom.setAttrib(el, 'title', lines.join('\n'));
|
||||||
|
/** 不再使用 data-cite-missing 红底提示,角标样式与普通引用一致 */
|
||||||
|
ed.dom.setAttrib(el, 'data-cite-missing', null);
|
||||||
});
|
});
|
||||||
this.padAutociteCaretPlaceholder(ed);
|
this.padAutociteCaretPlaceholder(ed);
|
||||||
},
|
},
|
||||||
@@ -265,7 +554,7 @@ export default {
|
|||||||
const doc = ed.getDoc();
|
const doc = ed.getDoc();
|
||||||
const body = doc.body;
|
const body = doc.body;
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
body.querySelectorAll('autocite').forEach((el) => {
|
body.querySelectorAll('mycite').forEach((el) => {
|
||||||
const next = el.nextSibling;
|
const next = el.nextSibling;
|
||||||
if (next === null) {
|
if (next === null) {
|
||||||
el.parentNode.appendChild(doc.createTextNode('\u200b'));
|
el.parentNode.appendChild(doc.createTextNode('\u200b'));
|
||||||
@@ -294,7 +583,7 @@ export default {
|
|||||||
if (this._refBookmark) {
|
if (this._refBookmark) {
|
||||||
ed.selection.moveToBookmark(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);
|
this.renderAutociteInEditor(ed);
|
||||||
},
|
},
|
||||||
@@ -306,7 +595,7 @@ export default {
|
|||||||
ed.fire('change');
|
ed.fire('change');
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 从当前编辑的 autocite 中去掉指定 p_refer_id;去掉后无 id 则删除整段标签。
|
* 从当前编辑的 mycite 中去掉指定 p_refer_id;去掉后无 id 则删除整段标签。
|
||||||
* ids 为空:删除整段(与 removeAutocite 一致)。
|
* ids 为空:删除整段(与 removeAutocite 一致)。
|
||||||
*/
|
*/
|
||||||
stripAutociteIds(idsToRemove) {
|
stripAutociteIds(idsToRemove) {
|
||||||
@@ -331,13 +620,14 @@ export default {
|
|||||||
}
|
}
|
||||||
ed.fire('change');
|
ed.fire('change');
|
||||||
},
|
},
|
||||||
/** TinyMCE 会剔除「空」的行内标签;空 autocite 必须在入编辑器前占位,否则合并引用 [1–3] 等整段消失 */
|
/** TinyMCE 会剔除「空」的行内标签;空 mycite 必须在入编辑器前占位,否则合并引用 [1–3] 等整段消失 */
|
||||||
normalizeAutociteHtmlForEditor(html) {
|
normalizeAutociteHtmlForEditor(html) {
|
||||||
if (!html || typeof html !== 'string') return html;
|
if (!html || typeof html !== 'string') return html;
|
||||||
let out = html.replace(/<autocite([^>]*)>\s*<\/autocite>/gi, '<autocite$1>​</autocite>');
|
/** 角标显示一律由 renderAutociteInEditor 根据 data-id 生成,禁止保留库内遗留的 [1-4]、[1–4] 等旧文案 */
|
||||||
// 与 getSafeContent 标签边界逻辑一致:autocite 左右水平空白改为 ,不换行符(避免吃掉段间 \n)
|
let out = html.replace(/<mycite([^>]*)>[\s\S]*?<\/mycite>/gi, '<mycite$1>​</mycite>');
|
||||||
out = out.replace(/[^\S\r\n]+(?=<autocite\b)/gi, ' ');
|
// 外侧:连续空格 / 合并为单个 ,避免「普通空格 + 」叠成大缝
|
||||||
out = out.replace(/(?<=<\/autocite>)[^\S\r\n]+/gi, ' ');
|
out = out.replace(/(?:\s| | )+(?=<mycite\b)/gi, ' ');
|
||||||
|
out = out.replace(/(?<=<\/mycite>)(?:\s| | )+/gi, ' ');
|
||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
handleSetContent(val) {
|
handleSetContent(val) {
|
||||||
@@ -351,8 +641,11 @@ export default {
|
|||||||
|
|
||||||
this.editorInstance.setContent(finalContent);
|
this.editorInstance.setContent(finalContent);
|
||||||
|
|
||||||
// 渲染数学公式
|
// SetContent 回调里会 renderAutociteInEditor;再补一次 nextTick,避免时序下仍显示库内旧 [1-4]
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
if (this.editorInstance) {
|
||||||
|
this.renderAutociteInEditor(this.editorInstance);
|
||||||
|
}
|
||||||
if (window.renderMathJax) {
|
if (window.renderMathJax) {
|
||||||
window.renderMathJax(this.tinymceId);
|
window.renderMathJax(this.tinymceId);
|
||||||
}
|
}
|
||||||
@@ -611,7 +904,7 @@ export default {
|
|||||||
return new Blob([u8arr], { type: mime });
|
return new Blob([u8arr], { type: mime });
|
||||||
},
|
},
|
||||||
formatHtml(val) {
|
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 cleanEmptyTags = /<([a-zA-Z1-6]+)\b[^>]*><\/\1>/g;
|
||||||
const replaceSpaces = /\s+(?=<)|(?<=>)\s+/g;
|
const replaceSpaces = /\s+(?=<)|(?<=>)\s+/g;
|
||||||
const removeBr = /<br\s*\/?>/gi; // 移除所有 br 标签
|
const removeBr = /<br\s*\/?>/gi; // 移除所有 br 标签
|
||||||
@@ -677,10 +970,10 @@ export default {
|
|||||||
window.tinymce.init({
|
window.tinymce.init({
|
||||||
..._this.tinymceOtherInit,
|
..._this.tinymceOtherInit,
|
||||||
trim_span_elements: false, // 禁止修剪内联标签周围的空格
|
trim_span_elements: false, // 禁止修剪内联标签周围的空格
|
||||||
extended_valid_elements: 'blue[*],autocite[*]',
|
extended_valid_elements: 'blue[*],mycite[*]',
|
||||||
/* ~ 前缀:按行内(类似 span)处理,否则默认当块级会拆段导致引用后强制换行 */
|
/* ~ 前缀:按行内(类似 span)处理,否则默认当块级会拆段导致引用后强制换行 */
|
||||||
custom_elements: 'blue,~autocite',
|
custom_elements: 'blue,~mycite',
|
||||||
valid_children: '+blue[#text|i|em|b|strong|span],+body[blue|autocite],+p[blue|autocite]',
|
valid_children: '+blue[#text|i|em|b|strong|span],+body[blue|mycite],+p[blue|mycite]',
|
||||||
|
|
||||||
inline: false, // 使用 iframe 模式
|
inline: false, // 使用 iframe 模式
|
||||||
selector: `#${this.tinymceId}`,
|
selector: `#${this.tinymceId}`,
|
||||||
@@ -690,7 +983,7 @@ export default {
|
|||||||
valid_elements:
|
valid_elements:
|
||||||
this.type == 'table'
|
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 标签
|
// valid_elements: '*[*]', // 允许所有 HTML 标签
|
||||||
noneditable_editable_class: 'MathJax',
|
noneditable_editable_class: 'MathJax',
|
||||||
height: this.height,
|
height: this.height,
|
||||||
@@ -723,7 +1016,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* inline 与 blue 引用一致,避免 inline-block 在行尾产生多余换行感 */
|
/* inline 与 blue 引用一致,避免 inline-block 在行尾产生多余换行感 */
|
||||||
autocite {
|
mycite {
|
||||||
display: inline;
|
display: inline;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
color: rgb(0, 130, 170);
|
color: rgb(0, 130, 170);
|
||||||
@@ -736,10 +1029,19 @@ export default {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
autocite:hover {
|
mycite:hover {
|
||||||
background-color: rgba(0, 130, 170, 0.2);
|
background-color: rgba(0, 130, 170, 0.2);
|
||||||
text-shadow: 0 0 3px rgba(0, 130, 170, 0.3);
|
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 {
|
@keyframes blueGlow {
|
||||||
0%,
|
0%,
|
||||||
@@ -764,17 +1066,31 @@ export default {
|
|||||||
const hasCustom =
|
const hasCustom =
|
||||||
(Array.isArray(tb) && tb.length > 0) ||
|
(Array.isArray(tb) && tb.length > 0) ||
|
||||||
(typeof tb === 'string' && 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 = [
|
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;
|
if (!hasCustom) return defaultToolbar;
|
||||||
const rows = Array.isArray(tb) ? tb : [tb];
|
const rows = Array.isArray(tb) ? tb : [tb];
|
||||||
return rows.map((row) =>
|
return rows.map((row) => {
|
||||||
typeof row === 'string' && row.indexOf('insertRef') === -1
|
if (typeof row !== 'string') return row;
|
||||||
? row.replace(/\|LateX\|/g, '|LateX insertRef|').replace(/\|\s*LateX\s*\|/g, '| LateX insertRef |')
|
let out = row;
|
||||||
: 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, // 启用菜单栏并保持必要的项目
|
menubar: false, // 启用菜单栏并保持必要的项目
|
||||||
statusbar: false, // 关闭底部状态栏
|
statusbar: false, // 关闭底部状态栏
|
||||||
@@ -844,7 +1160,7 @@ export default {
|
|||||||
var currentWmathElement = null;
|
var currentWmathElement = null;
|
||||||
|
|
||||||
ed.ui.registry.addButton('insertRef', {
|
ed.ui.registry.addButton('insertRef', {
|
||||||
text: 'Ref',
|
text: 'Reference',
|
||||||
tooltip: 'Insert Reference',
|
tooltip: 'Insert Reference',
|
||||||
onAction: function () {
|
onAction: function () {
|
||||||
const refs = Array.isArray(_this.chanFerForm) ? _this.chanFerForm : [];
|
const refs = Array.isArray(_this.chanFerForm) ? _this.chanFerForm : [];
|
||||||
@@ -859,7 +1175,7 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ed.on('click', function (e) {
|
ed.on('click', function (e) {
|
||||||
const autociteEl = e.target.closest('autocite');
|
const autociteEl = e.target.closest('mycite');
|
||||||
if (autociteEl) {
|
if (autociteEl) {
|
||||||
const refs = Array.isArray(_this.chanFerForm) ? _this.chanFerForm : [];
|
const refs = Array.isArray(_this.chanFerForm) ? _this.chanFerForm : [];
|
||||||
if (refs.length === 0) {
|
if (refs.length === 0) {
|
||||||
@@ -1003,6 +1319,8 @@ export default {
|
|||||||
subtree: true,
|
subtree: true,
|
||||||
characterData: true
|
characterData: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_this.mountAutoLinkFooterInsideEditor(ed);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 定义自定义按钮
|
// 定义自定义按钮
|
||||||
@@ -1045,11 +1363,11 @@ export default {
|
|||||||
ed.on('GetContent', function (e) {
|
ed.on('GetContent', function (e) {
|
||||||
e.content = e.content.replace(/<b>/g, '<strong>').replace(/<\/b>/g, '</strong>');
|
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(/<i>/g, '<em>').replace(/<\/em>/g, '</em>');
|
||||||
e.content = e.content.replace(/<\/autocite>\s*​/gi, '</autocite>');
|
e.content = e.content.replace(/<\/mycite>\s*​/gi, '</mycite>');
|
||||||
e.content = e.content.replace(/<\/autocite>\s*\u200b/g, '</autocite>');
|
e.content = e.content.replace(/<\/mycite>\s*\u200b/g, '</mycite>');
|
||||||
e.content = e.content.replace(/<autocite([^>]*)>[^<]*<\/autocite>/gi, function (match, attrs) {
|
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, '');
|
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() {
|
destroyTinymce() {
|
||||||
this.onClear();
|
this.onClear();
|
||||||
|
this.restoreAutoLinkFooterDom();
|
||||||
if (window.tinymce.get(this.tinymceId)) {
|
if (window.tinymce.get(this.tinymceId)) {
|
||||||
window.tinymce.get(this.tinymceId).destroy();
|
window.tinymce.get(this.tinymceId).destroy();
|
||||||
}
|
}
|
||||||
@@ -1226,7 +1545,8 @@ export default {
|
|||||||
content = content.replace(/<span[^>]*>/g, '').replace(/<\/span>/g, ''); // 去除span标签
|
content = content.replace(/<span[^>]*>/g, '').replace(/<\/span>/g, ''); // 去除span标签
|
||||||
content = content.replace(/<strong>/g, '<b>').replace(/<\/strong>/g, '</b>');
|
content = content.replace(/<strong>/g, '<b>').replace(/<\/strong>/g, '</b>');
|
||||||
content = content.replace(/<em>/g, '<i>').replace(/<\/em>/g, '</i>');
|
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);
|
this.$emit('getContent', type, content);
|
||||||
},
|
},
|
||||||
@@ -1311,6 +1631,26 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<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 {
|
::v-deep .tox-tinymce-aux {
|
||||||
z-index: 9999 !important;
|
z-index: 9999 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,15 @@
|
|||||||
ref="tinymceChild1"
|
ref="tinymceChild1"
|
||||||
:wordStyle="wordStyle"
|
:wordStyle="wordStyle"
|
||||||
:isAutomaticUpdate="isAutomaticUpdate"
|
:isAutomaticUpdate="isAutomaticUpdate"
|
||||||
|
:showRefButton="showRefButton"
|
||||||
@getContent="getContent"
|
@getContent="getContent"
|
||||||
@openLatexEditor="openLatexEditor"
|
@openLatexEditor="openLatexEditor"
|
||||||
@openRefSelector="openRefSelector"
|
@openRefSelector="openRefSelector"
|
||||||
@updateChange="updateChange"
|
@updateChange="updateChange"
|
||||||
|
@input="onTinymceInput"
|
||||||
:value="value"
|
:value="value"
|
||||||
:chanFerForm="chanFerForm"
|
:chanFerForm="chanFerForm"
|
||||||
|
:bodyCiteIdOrder="bodyCiteIdOrder"
|
||||||
:typesettingType="typesettingType"
|
:typesettingType="typesettingType"
|
||||||
class="paste-area text-container"
|
class="paste-area text-container"
|
||||||
:toolbar="toolbarConfig"
|
:toolbar="toolbarConfig"
|
||||||
@@ -38,14 +41,29 @@
|
|||||||
<script>
|
<script>
|
||||||
import Tinymce from '@/components/page/components/Tinymce';
|
import Tinymce from '@/components/page/components/Tinymce';
|
||||||
export default {
|
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: {
|
components: {
|
||||||
Tinymce
|
Tinymce
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
toolbarConfig() {
|
toolbarConfig() {
|
||||||
// Ref 必须始终出现在配置里:TinyMCE 只在 init 时读一次 toolbar,若首屏 chanFerForm 为空则按钮会永久缺失
|
const refBtn = this.showRefButton ? ' insertRef' : '';
|
||||||
const refBtn = ' insertRef';
|
|
||||||
if (!this.isAutomaticUpdate) {
|
if (!this.isAutomaticUpdate) {
|
||||||
return [`bold italic |customBlue removeBlue|LateX${refBtn}| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace`];
|
return [`bold italic |customBlue removeBlue|LateX${refBtn}| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace`];
|
||||||
}
|
}
|
||||||
@@ -102,24 +120,35 @@ export default {
|
|||||||
openRefSelector(data) {
|
openRefSelector(data) {
|
||||||
this.$emit('openRefSelector', data);
|
this.$emit('openRefSelector', data);
|
||||||
},
|
},
|
||||||
|
onTinymceInput(html) {
|
||||||
|
this.$emit('editorInput', html);
|
||||||
|
},
|
||||||
insertAutocite(refId) {
|
insertAutocite(refId) {
|
||||||
this.$refs.tinymceChild1.insertAutocite(refId);
|
this.$refs.tinymceChild1.insertAutocite(refId);
|
||||||
},
|
},
|
||||||
removeAutocite() {
|
removeAutocite() {
|
||||||
this.$refs.tinymceChild1.removeAutocite();
|
this.$refs.tinymceChild1.removeAutocite();
|
||||||
},
|
},
|
||||||
/** 从当前 autocite 的 data-id 中移除指定 id;无剩余则删标签;未选 id 时删整段 */
|
/** 从当前 mycite 的 data-id 中移除指定 id;无剩余则删标签;未选 id 时删整段 */
|
||||||
stripAutociteIds(ids) {
|
stripAutociteIds(ids) {
|
||||||
if (this.$refs.tinymceChild1 && typeof this.$refs.tinymceChild1.stripAutociteIds === 'function') {
|
if (this.$refs.tinymceChild1 && typeof this.$refs.tinymceChild1.stripAutociteIds === 'function') {
|
||||||
this.$refs.tinymceChild1.stripAutociteIds(ids);
|
this.$refs.tinymceChild1.stripAutociteIds(ids);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/** 参考文献异步到达后刷新编辑器内 autocite 数字(供父组件在打开弹窗 / fetch 完成后调用) */
|
/** 参考文献异步到达后刷新编辑器内 mycite 数字(供父组件在打开弹窗 / fetch 完成后调用) */
|
||||||
refreshAutociteDisplay() {
|
refreshAutociteDisplay() {
|
||||||
const t = this.$refs.tinymceChild1;
|
const t = this.$refs.tinymceChild1;
|
||||||
if (t && typeof t.renderAutociteInEditor === 'function' && t.editorInstance) {
|
if (t && typeof t.renderAutociteInEditor === 'function' && t.editorInstance) {
|
||||||
t.renderAutociteInEditor(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"
|
:articleId="articleId"
|
||||||
ref="tinymceChild1"
|
ref="tinymceChild1"
|
||||||
:wordStyle="wordStyle"
|
:wordStyle="wordStyle"
|
||||||
|
:show-ref-button="showRefButton"
|
||||||
|
:chanFerForm="chanFerForm"
|
||||||
|
:bodyCiteIdOrder="bodyCiteIdOrder"
|
||||||
@getContent="getContent"
|
@getContent="getContent"
|
||||||
@openLatexEditor="openLatexEditor"
|
@openLatexEditor="openLatexEditor"
|
||||||
|
@openRefSelector="$emit('openRefSelector', $event)"
|
||||||
|
@input="onTableEditorInput"
|
||||||
:height="calcDynamicWidth()"
|
:height="calcDynamicWidth()"
|
||||||
:value="updatedHtml"
|
:value="updatedHtml"
|
||||||
:typesettingType="typesettingType"
|
:typesettingType="typesettingType"
|
||||||
@@ -22,7 +27,24 @@
|
|||||||
<script>
|
<script>
|
||||||
import Tinymce from '@/components/page/components/Tinymce';
|
import Tinymce from '@/components/page/components/Tinymce';
|
||||||
export default {
|
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: {
|
components: {
|
||||||
Tinymce
|
Tinymce
|
||||||
},
|
},
|
||||||
@@ -91,6 +113,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onTableEditorInput(html) {
|
||||||
|
this.$emit('editorInput', html);
|
||||||
|
},
|
||||||
openLatexEditor(data) {
|
openLatexEditor(data) {
|
||||||
this.$emit('openLatexEditor', data);
|
this.$emit('openLatexEditor', data);
|
||||||
},
|
},
|
||||||
@@ -121,6 +146,22 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.$emit('getContent', type, { html_data: '', table: [] });
|
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="
|
v-if="
|
||||||
!isPreview &&
|
!isPreview &&
|
||||||
isEditComment &&
|
isEditComment &&
|
||||||
item.proof_read_num &&
|
isProofreadEnabled(item) &&
|
||||||
item.proof_read_num > 0 &&
|
resolvedProofreadNum(item) > 0 &&
|
||||||
item.is_proofread == 1 &&
|
|
||||||
item.type == 0 &&
|
item.type == 0 &&
|
||||||
item.content != ''
|
item.content != ''
|
||||||
"
|
"
|
||||||
class="proofreading-num"
|
class="proofreading-num"
|
||||||
>{{ item.proof_read_num }}</span
|
>{{ resolvedProofreadNum(item) }}</span
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@click="handleClickProofreadingList([item.am_id])"
|
@click="handleClickProofreadingList([item.am_id])"
|
||||||
v-if="
|
v-if="
|
||||||
!isPreview &&
|
!isPreview &&
|
||||||
isEditComment &&
|
isEditComment &&
|
||||||
item.proof_read_num == -1 &&
|
isProofreadEnabled(item) &&
|
||||||
item.is_proofread == 1 &&
|
resolvedProofreadNum(item) === -1 &&
|
||||||
item.type == 0 &&
|
item.type == 0 &&
|
||||||
item.content != ''
|
item.content != ''
|
||||||
"
|
"
|
||||||
@@ -131,8 +130,8 @@
|
|||||||
v-if="
|
v-if="
|
||||||
!isPreview &&
|
!isPreview &&
|
||||||
isEditComment &&
|
isEditComment &&
|
||||||
item.proof_read_num == 0 &&
|
isProofreadEnabled(item) &&
|
||||||
item.is_proofread == 1 &&
|
resolvedProofreadNum(item) === 0 &&
|
||||||
item.type == 0 &&
|
item.type == 0 &&
|
||||||
item.content != ''
|
item.content != ''
|
||||||
"
|
"
|
||||||
@@ -140,19 +139,11 @@
|
|||||||
style="color: #006699e0"
|
style="color: #006699e0"
|
||||||
><i class="el-icon-success"></i
|
><i class="el-icon-success"></i
|
||||||
></span>
|
></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
|
<span
|
||||||
@click="handleClickProofreadingList([item.am_id])"
|
@click="handleClickProofreadingList([item.am_id])"
|
||||||
class="isRemark"
|
class="isRemark"
|
||||||
:remark-main-id="item.am_id"
|
: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
|
<img
|
||||||
class="isRemark base-margin"
|
class="isRemark base-margin"
|
||||||
@@ -982,13 +973,22 @@
|
|||||||
<div v-if="isEditComment" class="menu-item comment-feat" @mousedown="cacheSelection" @click="menuAction('comment')">
|
<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>
|
<i class="el-icon-chat-line-square"></i><span>{{ $t('commonTable.Annotations') }}</span>
|
||||||
</div>
|
</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')">
|
<div class="menu-item menu-autocite-ref" @click.stop="menuAction('editRefCite')">
|
||||||
<i class="el-icon-edit-outline"></i><span>{{ $t('wordCite.modifyRef') }}</span>
|
<i class="el-icon-edit-outline"></i><span>{{ $t('wordCite.modifyRef') }}</span>
|
||||||
</div>
|
</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>
|
<i class="el-icon-remove-outline"></i><span>{{ $t('wordCite.removeRefTag') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1053,6 +1053,7 @@ const toolbar = 'addImageButton ';
|
|||||||
import { TableUtils } from '@/common/js/TableUtils';
|
import { TableUtils } from '@/common/js/TableUtils';
|
||||||
import { debounce, throttle } from '@/common/js/debounce';
|
import { debounce, throttle } from '@/common/js/debounce';
|
||||||
import { tableStyle, commonWordStyle } from '@/utils/tinymceStyles';
|
import { tableStyle, commonWordStyle } from '@/utils/tinymceStyles';
|
||||||
|
import { extractAutociteIdsFromHtmlString } from '@/utils/autociteHtml.js';
|
||||||
export default {
|
export default {
|
||||||
name: 'tinymce',
|
name: 'tinymce',
|
||||||
components: {},
|
components: {},
|
||||||
@@ -1097,6 +1098,11 @@ export default {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/** 父组件传入的正文 mycite 顺序(与 GenerateCharts 的 articleCiteIdOrder 一致);未传时仍用稿面扫描得到的 bodyCiteIdOrder */
|
||||||
|
citeOrderFromParent: {
|
||||||
|
type: Array,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
comments: {
|
comments: {
|
||||||
type: [Array, Object], // 允许数组或对象
|
type: [Array, Object], // 允许数组或对象
|
||||||
|
|
||||||
@@ -1180,8 +1186,11 @@ export default {
|
|||||||
editorsInitialized: {}, // 用于存储每个编辑器实例
|
editorsInitialized: {}, // 用于存储每个编辑器实例
|
||||||
mediaUrl: mediaUrl, //
|
mediaUrl: mediaUrl, //
|
||||||
lastTag: null,
|
lastTag: null,
|
||||||
/** 稿面点击 <autocite> 时记录,用于浮动条「修改引用 / 移除引用」 */
|
/** 稿面点击 <mycite> 时记录,用于浮动条「修改引用 / 移除引用」 */
|
||||||
manuscriptAutociteContext: null,
|
manuscriptAutociteContext: null,
|
||||||
|
/** 正文 DOM 中 mycite 首次出现顺序(唯一 p_refer_id),驱动 citeMap 与参考文献列表排序 */
|
||||||
|
bodyCiteIdOrder: [],
|
||||||
|
_syncRefOrderTimer: null,
|
||||||
isEditComment: false,
|
isEditComment: false,
|
||||||
isUserEditComment: false,
|
isUserEditComment: false,
|
||||||
typesettingType: 1,
|
typesettingType: 1,
|
||||||
@@ -1256,11 +1265,24 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
window.renderMathJax(); // 主动触发 MathJax 渲染
|
window.renderMathJax(); // 主动触发 MathJax 渲染
|
||||||
this.getCommentsData();
|
this.getCommentsData();
|
||||||
|
this.scheduleSyncRefOrder();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deep: true // 启用深度监听
|
deep: true // 启用深度监听
|
||||||
},
|
},
|
||||||
|
wordList: {
|
||||||
|
handler() {
|
||||||
|
this.scheduleSyncRefOrder();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
chanFerForm: {
|
||||||
|
handler() {
|
||||||
|
this.scheduleSyncRefOrder();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
totalNumbers: {
|
totalNumbers: {
|
||||||
handler(val) {
|
handler(val) {
|
||||||
if (val == 0) {
|
if (val == 0) {
|
||||||
@@ -1272,45 +1294,66 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
citeMap() {
|
citeMap() {
|
||||||
// 1) 优先使用参考文献列表顺序(与右侧 References 一致)
|
|
||||||
const mapFromRefs = {};
|
|
||||||
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
||||||
refs.forEach((row, idx) => {
|
const refIdSet = new Set(
|
||||||
const key = row && row.p_refer_id != null ? String(row.p_refer_id) : '';
|
refs.map((r) => (r && r.p_refer_id != null ? String(r.p_refer_id) : '')).filter(Boolean)
|
||||||
if (key) mapFromRefs[key] = idx + 1;
|
);
|
||||||
});
|
|
||||||
if (Object.keys(mapFromRefs).length > 0) return mapFromRefs;
|
|
||||||
|
|
||||||
// 2) 兜底:按正文首次出现顺序编号(支持 data-id 含多个逗号分隔 id)
|
let orderedIds = [];
|
||||||
const order = [];
|
if (Array.isArray(this.citeOrderFromParent) && this.citeOrderFromParent.length > 0) {
|
||||||
const re = /<autocite\s+data-id="([^"]+)"/gi;
|
orderedIds = this.citeOrderFromParent.map(String);
|
||||||
const paragraphs =
|
} else if (Array.isArray(this.bodyCiteIdOrder) && this.bodyCiteIdOrder.length > 0) {
|
||||||
Array.isArray(this.wordList) && this.wordList.length > 0
|
orderedIds = this.bodyCiteIdOrder.map(String);
|
||||||
? this.wordList
|
} else {
|
||||||
: Array.isArray(this.contentList)
|
const order = [];
|
||||||
? this.contentList
|
const paragraphs =
|
||||||
: [];
|
Array.isArray(this.wordList) && this.wordList.length > 0
|
||||||
|
? this.wordList
|
||||||
|
: Array.isArray(this.contentList)
|
||||||
|
? this.contentList
|
||||||
|
: [];
|
||||||
|
|
||||||
paragraphs.forEach((p) => {
|
paragraphs.forEach((p) => {
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
if (p && typeof p.text === 'string') candidates.push(p.text);
|
if (p && typeof p.text === 'string') candidates.push(p.text);
|
||||||
if (p && typeof p.content === 'string') candidates.push(p.content);
|
if (p && typeof p.content === 'string') candidates.push(p.content);
|
||||||
candidates.forEach((raw) => {
|
candidates.forEach((raw) => {
|
||||||
re.lastIndex = 0;
|
extractAutociteIdsFromHtmlString(raw).forEach((id) => {
|
||||||
let m;
|
if (!order.includes(id)) order.push(id);
|
||||||
while ((m = re.exec(raw)) !== null) {
|
|
||||||
m[1].split(',').forEach((part) => {
|
|
||||||
const id = part.trim();
|
|
||||||
if (id && !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) => {
|
const map = {};
|
||||||
acc[id] = idx + 1;
|
filtered.forEach((id, idx) => {
|
||||||
return acc;
|
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() {
|
sortedProofreadingList() {
|
||||||
const order = [2, 1, 3];
|
const order = [2, 1, 3];
|
||||||
@@ -1339,6 +1382,7 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
window.renderMathJax(); // 主动触发 MathJax 渲染
|
window.renderMathJax(); // 主动触发 MathJax 渲染
|
||||||
this.getCommentsData();
|
this.getCommentsData();
|
||||||
|
this.scheduleSyncRefOrder();
|
||||||
});
|
});
|
||||||
this.$refs.scrollDiv.addEventListener('scroll', this.divOnScroll, { passive: true });
|
this.$refs.scrollDiv.addEventListener('scroll', this.divOnScroll, { passive: true });
|
||||||
|
|
||||||
@@ -1397,6 +1441,7 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
window.renderMathJax(); // 主动触发 MathJax 渲染
|
window.renderMathJax(); // 主动触发 MathJax 渲染
|
||||||
this.getCommentsData();
|
this.getCommentsData();
|
||||||
|
this.scheduleSyncRefOrder();
|
||||||
});
|
});
|
||||||
this.$refs.scrollDiv.addEventListener('scroll', this.divOnScroll, { passive: true });
|
this.$refs.scrollDiv.addEventListener('scroll', this.divOnScroll, { passive: true });
|
||||||
},
|
},
|
||||||
@@ -1415,17 +1460,31 @@ export default {
|
|||||||
// window.removeEventListener('resize', this.calcMarkers);
|
// window.removeEventListener('resize', this.calcMarkers);
|
||||||
if (this.resizeObs) this.resizeObs.disconnect();
|
if (this.resizeObs) this.resizeObs.disconnect();
|
||||||
if (this.mutObs) this.mutObs.disconnect();
|
if (this.mutObs) this.mutObs.disconnect();
|
||||||
|
if (this._syncRefOrderTimer) clearTimeout(this._syncRefOrderTimer);
|
||||||
// 页面销毁前清理定时器和事件监听器
|
|
||||||
},
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
this.destroyTinymce();
|
this.destroyTinymce();
|
||||||
this.editors = {};
|
this.editors = {};
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
formatCiteNumbers(nums) {
|
||||||
if (!nums || !nums.length) return "";
|
if (!nums || !nums.length) return '';
|
||||||
const sorted = [...new Set(nums)].sort((a, b) => a - b);
|
const sorted = [...new Set(nums)].sort((a, b) => a - b);
|
||||||
const result = [];
|
const result = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -1442,28 +1501,17 @@ formatCiteNumbers(nums) {
|
|||||||
return result.join(', ');
|
return result.join(', ');
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 合并引用 tooltip / title 行按正文序号排列,与 [2–4, 6] 标签一致 */
|
// 2. 最终引用标签渲染器 (将 mycite 转换为 [n]);支持 data-id="a,b,c" 单标签 或 多个相邻单 id 标签(旧数据)
|
||||||
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 标签(旧数据)
|
|
||||||
renderCiteLabels(html) {
|
renderCiteLabels(html) {
|
||||||
const citeGroupRe = /(?:<autocite\s+data-id="([^"]+)"\s*><\/autocite>\s*)+/gi;
|
// 库内若曾写入 [1] 等,与外侧空格叠缝;先规范为空标签再按组合并渲染
|
||||||
return html.replace(citeGroupRe, (groupMatch) => {
|
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 ids = [];
|
||||||
const innerRe = /<autocite\s+data-id="([^"]+)"\s*><\/autocite>/gi;
|
const innerRe = /<mycite\s+data-id="([^"]*)"\s*><\/mycite>/gi;
|
||||||
let m;
|
let m;
|
||||||
while ((m = innerRe.exec(groupMatch)) !== null) {
|
while ((m = innerRe.exec(groupMatch)) !== null) {
|
||||||
m[1].split(',').forEach((part) => {
|
m[1].split(',').forEach((part) => {
|
||||||
@@ -1478,34 +1526,56 @@ renderCiteLabels(html) {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const sortedIds = this.sortAutociteIdsByCiteNumber(ids);
|
const sortedAll = this.sortAutociteIdsByCiteNumber(ids);
|
||||||
const nums = sortedIds.map((id) => this.citeMap && this.citeMap[id]).filter((n) => n != null);
|
const validIds = sortedAll.filter((id) => refMap[id]);
|
||||||
const label = nums.length > 0 ? this.formatCiteNumbers(nums) : '?';
|
const sortedIds = validIds.length ? this.sortAutociteIdsByCiteNumber(validIds) : [];
|
||||||
|
const citeMap = this.citeMap || {};
|
||||||
|
|
||||||
const lines = sortedIds.map((id) => {
|
/** 与 Tinymce:已删除的 id 不进入角标与 title,仅保留仍存在于列表中的文献 */
|
||||||
const no = this.citeMap && this.citeMap[id] != null ? this.citeMap[id] : '?';
|
const parts = sortedIds.map((id) => {
|
||||||
const ref = refMap[id];
|
const ref = refMap[id];
|
||||||
if (!ref) {
|
const no = ref ? citeMap[id] : null;
|
||||||
return this.$t('wordCite.notFoundById', { id: no === '?' ? id : no });
|
const num = no != null && no !== '' ? String(no) : null;
|
||||||
}
|
return { id, ref, num };
|
||||||
|
|
||||||
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 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) =>
|
const escAttr = (s) =>
|
||||||
String(s || '')
|
String(s || '')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/</g, '<');
|
.replace(/</g, '<');
|
||||||
|
if (!label) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
const dataIdAttr = escAttr(sortedIds.join(','));
|
const dataIdAttr = escAttr(sortedIds.join(','));
|
||||||
const titleAttr = escAttr(lines.join('\n'));
|
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) {
|
getInvolvedPMain(range) {
|
||||||
// 1. 找到起始节点所属的 .pMain
|
// 1. 找到起始节点所属的 .pMain
|
||||||
@@ -2110,11 +2180,25 @@ renderCiteLabels(html) {
|
|||||||
});
|
});
|
||||||
return parts.join(',');
|
return parts.join(',');
|
||||||
},
|
},
|
||||||
/** 按 data-id 替换第一段匹配的 <autocite>;兼容稿面已排序、正文存储未排序的情况 */
|
/** 按 data-id 替换第一段匹配的 <mycite>;兼容稿面已排序、正文存储未排序的情况 */
|
||||||
replaceFirstAutociteByDataId(html, dataId, replacement) {
|
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 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);
|
let out = html.replace(reExact, replacement);
|
||||||
if (out !== html) return out;
|
if (out !== html) return out;
|
||||||
|
|
||||||
@@ -2123,7 +2207,7 @@ renderCiteLabels(html) {
|
|||||||
|
|
||||||
const _this = this;
|
const _this = this;
|
||||||
let done = false;
|
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;
|
if (done) return full;
|
||||||
const dm = attrs.match(/\bdata-id\s*=\s*"([^"]*)"/i) || attrs.match(/\bdata-id\s*=\s*'([^']*)'/i);
|
const dm = attrs.match(/\bdata-id\s*=\s*"([^"]*)"/i) || attrs.match(/\bdata-id\s*=\s*'([^']*)'/i);
|
||||||
if (!dm) return full;
|
if (!dm) return full;
|
||||||
@@ -2146,14 +2230,177 @@ renderCiteLabels(html) {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
this.$emit('openRefSelector', { currentRefIds: parts, source: 'manuscript' });
|
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) {
|
applyManuscriptAutocite(refIds) {
|
||||||
const ctx = this.manuscriptAutociteContext;
|
const ctx = this.manuscriptAutociteContext;
|
||||||
if (!ctx || !ctx.am_id) return;
|
if (!ctx || !ctx.am_id) return;
|
||||||
const item = this.wordList.find((w) => w.am_id == ctx.am_id);
|
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 ids = (Array.isArray(refIds) ? refIds : [refIds]).map(String);
|
||||||
const dataId = ids.join(',');
|
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);
|
const newContent = this.replaceFirstAutociteByDataId(item.content, ctx.dataId, newTag);
|
||||||
if (newContent === item.content) {
|
if (newContent === item.content) {
|
||||||
this.$message.warning(this.$t('wordCite.citeUpdateFail'));
|
this.$message.warning(this.$t('wordCite.citeUpdateFail'));
|
||||||
@@ -2165,18 +2412,50 @@ renderCiteLabels(html) {
|
|||||||
this.$emit('saveContent', newContent, ctx.am_id);
|
this.$emit('saveContent', newContent, ctx.am_id);
|
||||||
},
|
},
|
||||||
removeManuscriptAutocite() {
|
removeManuscriptAutocite() {
|
||||||
|
if (!this.manuscriptAutociteContext || !this.manuscriptAutociteContext.am_id) {
|
||||||
|
this.$message.warning(this.$t('wordCite.removeRefNeedClickCite'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.stripManuscriptAutociteIds([]);
|
this.stripManuscriptAutociteIds([]);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 从稿面当前 autocite 的 data-id 中移除给定 id;移除后无 id 则删掉整段标签。
|
* 从稿面当前 mycite 的 data-id 中移除给定 id;移除后无 id 则删掉整段标签。
|
||||||
* ids 为空:删除整段引用(稿面「移除引用」浮动按钮)。
|
* ids 为空:删除整段引用(稿面「移除引用」浮动按钮)。
|
||||||
*/
|
*/
|
||||||
stripManuscriptAutociteIds(idsToRemove) {
|
stripManuscriptAutociteIds(idsToRemove) {
|
||||||
const ctx = this.manuscriptAutociteContext;
|
const ctx = this.manuscriptAutociteContext;
|
||||||
if (!ctx || !ctx.am_id) return;
|
if (!ctx || !ctx.am_id) return;
|
||||||
const item = this.wordList.find((w) => w.am_id == ctx.am_id);
|
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)));
|
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) {
|
if (removeSet.size === 0) {
|
||||||
const newContent = this.replaceFirstAutociteByDataId(item.content, ctx.dataId, '');
|
const newContent = this.replaceFirstAutociteByDataId(item.content, ctx.dataId, '');
|
||||||
if (newContent === item.content) {
|
if (newContent === item.content) {
|
||||||
@@ -2193,7 +2472,7 @@ renderCiteLabels(html) {
|
|||||||
const keyWant = this.normalizeAutociteDataIdKey(ctx.dataId);
|
const keyWant = this.normalizeAutociteDataIdKey(ctx.dataId);
|
||||||
const _this = this;
|
const _this = this;
|
||||||
let done = false;
|
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;
|
if (done) return full;
|
||||||
const dm = attrs.match(/\bdata-id\s*=\s*"([^"]*)"/i) || attrs.match(/\bdata-id\s*=\s*'([^']*)'/i);
|
const dm = attrs.match(/\bdata-id\s*=\s*"([^"]*)"/i) || attrs.match(/\bdata-id\s*=\s*'([^']*)'/i);
|
||||||
if (!dm) return full;
|
if (!dm) return full;
|
||||||
@@ -2207,7 +2486,7 @@ renderCiteLabels(html) {
|
|||||||
if (remaining.length === 0) return '';
|
if (remaining.length === 0) return '';
|
||||||
const sorted = _this.sortAutociteIdsByCiteNumber(remaining);
|
const sorted = _this.sortAutociteIdsByCiteNumber(remaining);
|
||||||
const newDataId = sorted.join(',');
|
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) {
|
if (!done || newHtml === item.content) {
|
||||||
@@ -2219,6 +2498,65 @@ renderCiteLabels(html) {
|
|||||||
this.manuscriptAutociteContext = null;
|
this.manuscriptAutociteContext = null;
|
||||||
this.$emit('saveContent', newHtml, ctx.am_id);
|
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() {
|
handleAIProofreading() {
|
||||||
if (this.currentId) {
|
if (this.currentId) {
|
||||||
this.$api
|
this.$api
|
||||||
@@ -2346,7 +2684,7 @@ renderCiteLabels(html) {
|
|||||||
let selectedText = tempDiv.innerText.trim().replace(/\s+/g, ' ');
|
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) {
|
function preserveTags(node) {
|
||||||
if (node.nodeType === 3) return node.nodeValue; // 文本节点
|
if (node.nodeType === 3) return node.nodeValue; // 文本节点
|
||||||
if (node.nodeType === 1 && allowedTags.includes(node.nodeName.toLowerCase())) {
|
if (node.nodeType === 1 && allowedTags.includes(node.nodeName.toLowerCase())) {
|
||||||
@@ -3091,12 +3429,13 @@ renderCiteLabels(html) {
|
|||||||
this.currentTag = '';
|
this.currentTag = '';
|
||||||
this.currentTagData = null; // 必须重置,防止带入旧数据
|
this.currentTagData = null; // 必须重置,防止带入旧数据
|
||||||
this.manuscriptAutociteContext = null;
|
this.manuscriptAutociteContext = null;
|
||||||
const clickedAutocite = event.target.closest('autocite');
|
const clickedAutocite = event.target.closest('mycite');
|
||||||
if (clickedAutocite && !this.isPreview) {
|
if (clickedAutocite && !this.isPreview) {
|
||||||
const dataId = clickedAutocite.getAttribute('data-id') || '';
|
const dataId = clickedAutocite.getAttribute('data-id');
|
||||||
if (dataId) {
|
this.manuscriptAutociteContext = {
|
||||||
this.manuscriptAutociteContext = { am_id: id, dataId };
|
am_id: id,
|
||||||
}
|
dataId: dataId != null && dataId !== '' ? String(dataId) : ''
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const clickedTag = event.target.closest('myfigure, mytable, myh3');
|
const clickedTag = event.target.closest('myfigure, mytable, myh3');
|
||||||
if (clickedTag) {
|
if (clickedTag) {
|
||||||
@@ -3307,15 +3646,19 @@ renderCiteLabels(html) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
highlightText(item, annotations, type) {
|
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.isPreview) {
|
||||||
if (this.isShowPiZhu) {
|
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) {
|
} 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 {
|
} 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, '<');
|
// 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 || '');
|
const src = String(text || '');
|
||||||
|
|
||||||
// 1) 用一个全局、区分大小写不敏感的 wmath 提取正则
|
// 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片段, ...]
|
// 2) 把原文切成: [非wmath片段, wmath片段, 非wmath片段, wmath片段, ...]
|
||||||
const parts = [];
|
const parts = [];
|
||||||
@@ -3443,7 +3786,7 @@ renderCiteLabels(html) {
|
|||||||
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
|
||||||
// 1) 切分出 wmath 与非 wmath 片段
|
// 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 = [];
|
const parts = [];
|
||||||
let lastIdx = 0,
|
let lastIdx = 0,
|
||||||
m;
|
m;
|
||||||
@@ -3526,7 +3869,7 @@ renderCiteLabels(html) {
|
|||||||
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
|
||||||
// 1) 切分出 wmath 与非 wmath 片段
|
// 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 = [];
|
const parts = [];
|
||||||
let lastIdx = 0,
|
let lastIdx = 0,
|
||||||
m;
|
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() {
|
isShowEditComment() {
|
||||||
if (localStorage.getItem('U_role')) {
|
if (localStorage.getItem('U_role')) {
|
||||||
var identity = localStorage.getItem('U_role');
|
var identity = localStorage.getItem('U_role');
|
||||||
@@ -4557,7 +4923,7 @@ renderCiteLabels(html) {
|
|||||||
}
|
}
|
||||||
.proofreading-num {
|
.proofreading-num {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
background: #e61a12;
|
background: #0082aa;
|
||||||
color: rgb(255, 255, 255);
|
color: rgb(255, 255, 255);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 0px 6px;
|
padding: 0px 6px;
|
||||||
@@ -4582,6 +4948,7 @@ renderCiteLabels(html) {
|
|||||||
.table_Box {
|
.table_Box {
|
||||||
padding: 8px 15px;
|
padding: 8px 15px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
.table-box tr:first-child td {
|
.table-box tr:first-child td {
|
||||||
border-top: 1px solid #000 !important;
|
border-top: 1px solid #000 !important;
|
||||||
@@ -4703,7 +5070,7 @@ wmath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 正文预览:引用直接用语义标签 autocite(原 blue 包装已移除) */
|
/* 正文预览:引用直接用语义标签 autocite(原 blue 包装已移除) */
|
||||||
::v-deep autocite {
|
::v-deep mycite {
|
||||||
display: inline;
|
display: inline;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
color: rgb(0, 130, 170) !important;
|
color: rgb(0, 130, 170) !important;
|
||||||
@@ -4714,6 +5081,15 @@ wmath {
|
|||||||
background-color: rgba(0, 130, 170, 0.08);
|
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 myfigure *,
|
||||||
::v-deep mytable * {
|
::v-deep mytable * {
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
|
|||||||
@@ -11,10 +11,11 @@
|
|||||||
<div
|
<div
|
||||||
:data-id="`ref-${item.p_refer_id}`"
|
:data-id="`ref-${item.p_refer_id}`"
|
||||||
v-for="(item, index) in chanFerForm"
|
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="ref-item-row"
|
||||||
:class="{ 'has-change': item.is_change == 1, 'is-repeat': item.is_repeat == 1 }"
|
:class="{ 'has-change': item.is_change == 1, 'is-repeat': item.is_repeat == 1 }"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="ref-content-area">
|
<div class="ref-content-area">
|
||||||
<div v-if="item.refer_type == 'journal'" class="reference-item">
|
<div v-if="item.refer_type == 'journal'" class="reference-item">
|
||||||
<b class="ref-number-prefix">{{ index + 1 }}.</b>
|
<b class="ref-number-prefix">{{ index + 1 }}.</b>
|
||||||
@@ -42,12 +43,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</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-edit action-icon primary" @click="change(item, 'Edit')"></i>
|
||||||
|
|
||||||
<i class="el-icon-circle-plus-outline action-icon success" @click="addLine(item, 'Add')"></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
|
<i
|
||||||
class="el-icon-caret-top action-icon"
|
class="el-icon-caret-top action-icon"
|
||||||
:class="{ disabled: index === 0 }"
|
:class="{ disabled: index === 0 }"
|
||||||
@@ -61,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<i class="el-icon-delete action-icon danger" @click="deleteLine(item)"></i>
|
<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" />
|
<img v-if="role == 'editor' && item.is_ai_check == 1" src="@/assets/img/ai.png" class="ai-mini-tag" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,6 +313,10 @@ export default {
|
|||||||
type: null,
|
type: null,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
isPreview: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
chanFerFormRepeatList: {
|
chanFerFormRepeatList: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
@@ -327,9 +332,30 @@ export default {
|
|||||||
role: {
|
role: {
|
||||||
type: String,
|
type: String,
|
||||||
default: () => 'editor'
|
default: () => 'editor'
|
||||||
|
},
|
||||||
|
/** 为 true 时顺序由正文引用决定,隐藏手动上下移(与 sortRefer 冲突) */
|
||||||
|
refOrderFollowsBody: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
formatTitle(title) {
|
||||||
if (!title) return '';
|
if (!title) return '';
|
||||||
const reg = /\b(Retracted|Retraction)\b/gi;
|
const reg = /\b(Retracted|Retraction)\b/gi;
|
||||||
@@ -369,11 +395,11 @@ export default {
|
|||||||
|
|
||||||
// 2. 借鉴 EndNote:对处理完批注的 HTML 进行引用联动渲染
|
// 2. 借鉴 EndNote:对处理完批注的 HTML 进行引用联动渲染
|
||||||
// 支持 data-id="a,b,c" 单标签 或 多个相邻单 id 标签
|
// 支持 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) => {
|
return html.replace(citeGroupRe, (groupMatch) => {
|
||||||
const ids = [];
|
const ids = [];
|
||||||
const innerRe = /<autocite\s+data-id="([^"]+)"\s*><\/autocite>/gi;
|
const innerRe = /<mycite\s+data-id="([^"]+)"\s*><\/mycite>/gi;
|
||||||
let m;
|
let m;
|
||||||
while ((m = innerRe.exec(groupMatch)) !== null) {
|
while ((m = innerRe.exec(groupMatch)) !== null) {
|
||||||
m[1].split(',').forEach((part) => {
|
m[1].split(',').forEach((part) => {
|
||||||
@@ -1018,6 +1044,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
chanFerForm: {
|
||||||
|
handler() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.syncReferenceRowIndices();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
SourceType: {
|
SourceType: {
|
||||||
handler(newVal, oldVal) {
|
handler(newVal, oldVal) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -1990,9 +2025,12 @@ export default {
|
|||||||
.action-icon.success:hover {
|
.action-icon.success:hover {
|
||||||
color: #67c23a;
|
color: #67c23a;
|
||||||
}
|
}
|
||||||
.action-icon.danger:hover {
|
.action-icon.danger {
|
||||||
color: #f56c6c;
|
color: #f56c6c;
|
||||||
}
|
}
|
||||||
|
.action-icon.danger:hover {
|
||||||
|
color: #e85a5a;
|
||||||
|
}
|
||||||
.action-icon.disabled {
|
.action-icon.disabled {
|
||||||
color: #e4e7ed;
|
color: #e4e7ed;
|
||||||
cursor: not-allowed;
|
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