Compare commits
22 Commits
8e59702f0b
...
Editorial-
| Author | SHA1 | Date | |
|---|---|---|---|
| 48dc82326d | |||
| 6cb5dbccf0 | |||
| 26981f83c4 | |||
| a28d0d5079 | |||
| eff107aa15 | |||
| 563def58a5 | |||
| 222a00c1c7 | |||
| 1deb5bba86 | |||
| d613aa7d0d | |||
| 0a56b16fe4 | |||
| 7f725cad52 | |||
| 797ca258f8 | |||
| 9c44064f51 | |||
| 7527a6ef54 | |||
| c4b86be0d5 | |||
| 4fc78e1fe7 | |||
| 0d35b76c3a | |||
| 36f6c02376 | |||
| dd07a03d7b | |||
| 620a35f958 | |||
| 95b52b4d06 | |||
| 1f29fb5baf |
@@ -102,6 +102,22 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* POST x-www-form-urlencoded,数组字段序列化为 list[]=a&list[]=b(兼容 PHP 批量接口)
|
||||
*/
|
||||
postFormBracket(url, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
service
|
||||
.post(url, qs.stringify(params, { arrayFormat: 'brackets' }))
|
||||
.then((res) => {
|
||||
resolve(res.data)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* post方法,对应post请求
|
||||
* @param {String} url [请求的url地址]
|
||||
|
||||
169
src/common/js/citeTablePreview.js
Normal file
169
src/common/js/citeTablePreview.js
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 表格弹窗预览:将单元格内空 mycite 转为可见 [n],与 word.vue renderCiteLabels 一致(纯函数,无 Vue 依赖)
|
||||
*/
|
||||
|
||||
export function 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(', ');
|
||||
}
|
||||
|
||||
export function sortAutociteIdsByCiteNumber(ids, citeMap) {
|
||||
const map = citeMap || {};
|
||||
const uniq = [...new Set(ids.map(String))];
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} refs - chanFerForm,项含 p_refer_id
|
||||
* @param {Array<string|number>} bodyCiteIdOrder - 正文首次出现顺序;空则按列表行序 1..n
|
||||
*/
|
||||
export function buildCiteMapFromRefs(refs, bodyCiteIdOrder) {
|
||||
const refList = Array.isArray(refs) ? refs : [];
|
||||
const refIdSet = new Set(
|
||||
refList.map((r) => (r && r.p_refer_id != null ? String(r.p_refer_id) : '')).filter(Boolean)
|
||||
);
|
||||
|
||||
let orderedIds = [];
|
||||
if (Array.isArray(bodyCiteIdOrder) && bodyCiteIdOrder.length > 0) {
|
||||
orderedIds = bodyCiteIdOrder.map(String);
|
||||
}
|
||||
|
||||
if (orderedIds.length === 0) {
|
||||
const map = {};
|
||||
refList.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 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;
|
||||
refList.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;
|
||||
}
|
||||
|
||||
function escAttr(s) {
|
||||
return String(s || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @param {Array} refs - chanFerForm
|
||||
* @param {Object} citeMap - p_refer_id -> 显示序号
|
||||
*/
|
||||
export function renderCiteLabelsInHtml(html, refs, citeMap) {
|
||||
if (!html || typeof html !== 'string') return html || '';
|
||||
if (!Array.isArray(refs) || refs.length === 0) return html;
|
||||
|
||||
/** 与稿面一致:历史/表格 JSON 中可能为 <autocite> */
|
||||
let h = html.replace(/<\/autocite>/gi, '</mycite>').replace(/<autocite\b/gi, '<mycite');
|
||||
|
||||
const refList = refs;
|
||||
const refMap = refList.reduce((acc, item) => {
|
||||
const key = item && item.p_refer_id != null ? String(item.p_refer_id) : '';
|
||||
if (key) acc[key] = item;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
let normalized = h.replace(/<mycite\b[^>]*\/?>[\s\S]*?(?:<\/mycite>)?/gi, (fullTag) => {
|
||||
const attrMatch = fullTag.match(/\bdata-id\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i);
|
||||
const dataId = (attrMatch && (attrMatch[1] || attrMatch[2] || attrMatch[3])) || '';
|
||||
if (!dataId) return '';
|
||||
return `<mycite data-id="${escAttr(dataId)}"></mycite>`;
|
||||
});
|
||||
const citeGroupRe = /(?:<mycite\s+data-id="([^"]*)"\s*><\/mycite>\s*)+/gi;
|
||||
normalized = normalized.replace(citeGroupRe, (groupMatch) => {
|
||||
const ids = [];
|
||||
const innerRe = /<mycite\s+data-id="([^"]*)"\s*><\/mycite>/gi;
|
||||
let m;
|
||||
while ((m = innerRe.exec(groupMatch)) !== null) {
|
||||
m[1].split(',').forEach((part) => {
|
||||
const id = part.trim();
|
||||
if (id) ids.push(id);
|
||||
});
|
||||
}
|
||||
const sortedAll = sortAutociteIdsByCiteNumber(ids, citeMap);
|
||||
const validIds = sortedAll.filter((id) => refMap[id]);
|
||||
const sortedIds = validIds.length ? sortAutociteIdsByCiteNumber(validIds, citeMap) : [];
|
||||
const map = citeMap || {};
|
||||
|
||||
const parts = sortedIds.map((id) => {
|
||||
const ref = refMap[id];
|
||||
const no = ref ? map[id] : null;
|
||||
const num = no != null && no !== '' ? String(no) : null;
|
||||
return { id, ref, num };
|
||||
});
|
||||
|
||||
const numsForLabel = parts
|
||||
.map((p) => p.num)
|
||||
.filter((n) => n != null && n !== '')
|
||||
.map(Number);
|
||||
const label = numsForLabel.length > 0 ? formatCiteNumbers(numsForLabel) : '';
|
||||
if (!label) {
|
||||
return '';
|
||||
}
|
||||
const dataIdAttr = escAttr(sortedIds.join(','));
|
||||
return `<mycite data-id="${dataIdAttr}">[${label}]</mycite>`;
|
||||
});
|
||||
return normalized.replace(/<mycite[^>]*data-id=""[^>]*>[\s\S]*?<\/mycite>/gi, '');
|
||||
}
|
||||
|
||||
export function applyCiteLabelsToTableRows(rows, refs, citeMap) {
|
||||
if (!Array.isArray(rows) || !rows.length) return rows;
|
||||
if (!Array.isArray(refs) || refs.length === 0) return rows;
|
||||
|
||||
return rows.map((row) => {
|
||||
if (!Array.isArray(row)) return row;
|
||||
const nextRow = row.map((cell) => {
|
||||
if (!cell || typeof cell !== 'object') return cell;
|
||||
const t = cell.text;
|
||||
const lower = typeof t === 'string' ? t.toLowerCase() : '';
|
||||
if (!lower || (!lower.includes('mycite') && !lower.includes('autocite'))) return cell;
|
||||
const next = renderCiteLabelsInHtml(t, refs, citeMap);
|
||||
if (next === t) return cell;
|
||||
return { ...cell, text: next };
|
||||
});
|
||||
// TableUtils.addRowIdToData 会给数组行对象挂 rowId,渲染 oddColor 依赖它,不能在 map 后丢失
|
||||
if (row.rowId != null) {
|
||||
nextRow.rowId = row.rowId;
|
||||
}
|
||||
return nextRow;
|
||||
});
|
||||
}
|
||||
@@ -71,7 +71,51 @@ function findExtentElement(blipElement) {
|
||||
|
||||
export default {
|
||||
|
||||
|
||||
|
||||
|
||||
async searchTitleByDOI(doi) {
|
||||
if (!doi) {
|
||||
this.$message.warning('Please enter a DOI');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 开启全局 Loading,防止编辑重复点击
|
||||
|
||||
|
||||
try {
|
||||
// 1. 数据清洗
|
||||
const cleanDoi = doi.trim()
|
||||
.replace(/^doi:/i, '')
|
||||
.replace(/https?:\/\/doi\.org\//i, '');
|
||||
|
||||
// 2. 请求 Crossref 接口
|
||||
const response = await fetch(`https://api.crossref.org/works/${encodeURIComponent(cleanDoi)}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('DOI not found');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 3. 提取标题并跳转
|
||||
const title = data.message?data.message.title?data.message.title[0]:'':'';
|
||||
|
||||
if (title) {
|
||||
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(title)}`;
|
||||
window.open(searchUrl, '_blank');
|
||||
return title;
|
||||
} else {
|
||||
this.$message.error('Title not found in metadata.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("DOI Retrieval Error:", error);
|
||||
this.$message.error('Failed to fetch title. Please check the DOI.');
|
||||
} finally {
|
||||
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getJournalTypeName(value) {
|
||||
|
||||
var list = JSON.parse(localStorage.getItem('journalTypeDataAll'));
|
||||
@@ -226,8 +270,7 @@ export default {
|
||||
|
||||
|
||||
str = this.transformHtmlString(processedContent, 'table',{ keepBr: true })
|
||||
console.log("🚀 ~ extractContentWithoutOuterSpan888888 ~ str:", str);
|
||||
|
||||
|
||||
|
||||
// 创建一个临时的 DOM 元素来解析 HTML
|
||||
const div = document.createElement('div');
|
||||
@@ -917,9 +960,9 @@ str = str.replace(regex, function (match, content, offset, fullString) {
|
||||
});
|
||||
// 2. 删除所有不需要的标签 (除 `strong`, `em`, `sub`, `sup`, `b`, `i` 外的所有标签)
|
||||
if (type == 'table') {
|
||||
inputHtml = inputHtml.replace(/<(?!\/?(strong|em|sub|sup|b|i|blue|wmath|img|myfigure|mytable|myh3))[^>]+>/g, ''); // 删除不需要的标签
|
||||
inputHtml = inputHtml.replace(/<(?!\/?(strong|em|sub|sup|b|i|blue|wmath|img|myfigure|mytable|myh3|mycite))[^>]+>/g, ''); // 删除不需要的标签
|
||||
} else {
|
||||
inputHtml = inputHtml.replace(/<(?!\/?(strong|em|sub|sup|b|i|blue|wmath|myfigure|mytable|myh3))[^>]+>/g, ''); // 删除不需要的标签
|
||||
inputHtml = inputHtml.replace(/<(?!\/?(strong|em|sub|sup|b|i|blue|wmath|myfigure|mytable|myh3|mycite))[^>]+>/g, ''); // 删除不需要的标签
|
||||
}
|
||||
|
||||
|
||||
@@ -992,8 +1035,7 @@ str = str.replace(regex, function (match, content, offset, fullString) {
|
||||
const cells = row.querySelectorAll('th, td'); // 获取每个行中的单元格(包括 <th> 和 <td>)
|
||||
return await Promise.all(
|
||||
Array.from(cells).map(async (cell) => {
|
||||
console.log("🚀 ~ parseTableToArray777 ~ cell:", cell);
|
||||
|
||||
|
||||
const text = await this.extractMathJaxLatex(cell);
|
||||
return {
|
||||
text,
|
||||
|
||||
@@ -1336,6 +1336,9 @@ colTitle: 'Template title',
|
||||
deleteLogSuccess: 'Deleted',
|
||||
deleteLogFailed: 'Delete failed',
|
||||
noFailureReason: 'No failure reason',
|
||||
logDetailEditTitle: 'Edit delivery log',
|
||||
logDetailPreviewTitle: 'Preview delivery log',
|
||||
logLoadFailed: 'Failed to load logs',
|
||||
deletedSuccess: 'Deleted',
|
||||
mockPromotionSubject: 'Promotion for {journal}',
|
||||
mockPromotionContent: '<p>Dear {name},</p><p>Check out our latest journal updates...</p>'
|
||||
@@ -1347,6 +1350,32 @@ colTitle: 'Template title',
|
||||
previewWithVariablesHint: 'The expert data is an example, used for variable spelling check only.',
|
||||
close: 'Close',
|
||||
placeholder: 'Please enter email content'
|
||||
},
|
||||
wordCite: {
|
||||
noRefs: 'Reference list is not loaded yet. Please wait or refresh the page.',
|
||||
missingBoundRef: 'The bound reference no longer exists. Please cite again.',
|
||||
notFoundById: 'No reference found for citation ID {id}',
|
||||
selectRef: 'Select Reference',
|
||||
originalOrder: 'Original order',
|
||||
reference: 'Reference',
|
||||
uncited: 'Uncited',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
remove: 'Remove',
|
||||
selected: 'Selected',
|
||||
modifyRef: 'Edit citation',
|
||||
removeRefTag: 'Remove citation',
|
||||
citeUpdateFail: 'Could not update the citation in the text. Try again or use Edit.',
|
||||
matchBracketRefs: 'Auto-link References',
|
||||
matchBracketRefsDone: 'Converted {n} bracket citation(s) to autocite.',
|
||||
matchBracketRefsNone: "No active citation links were detected in the text. To enable automatic numbering, please use the 'Reference' tool to link your sources.",
|
||||
removeRefNeedClickCite: 'Click a citation in the text first, then choose Reference remove.',
|
||||
quickPickPlaceholder: 'Enter e.g. [5, 6, 10-15] to auto-select',
|
||||
quickPickApply: 'Link References',
|
||||
quickPickClear: 'Clear selection',
|
||||
currentCiteNo: 'Current',
|
||||
locateInBody: 'Click to scroll to citation in text',
|
||||
locateInRefHint: 'Highlight this entry in the list below'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1321,6 +1321,9 @@ const zh = {
|
||||
deleteLogSuccess: '删除成功',
|
||||
deleteLogFailed: '删除失败',
|
||||
noFailureReason: '暂无失败原因',
|
||||
logDetailEditTitle: '编辑发送记录',
|
||||
logDetailPreviewTitle: '预览发送记录',
|
||||
logLoadFailed: '加载日志失败',
|
||||
deletedSuccess: '已删除',
|
||||
mockPromotionSubject: '自动推广:{journal}',
|
||||
mockPromotionContent: '<p>亲爱的 {name},</p><p>请查看我们最新的期刊更新...</p>'
|
||||
@@ -1332,6 +1335,32 @@ const zh = {
|
||||
previewWithVariablesHint: '专家数据仅为示例,仅用于变量拼写检查。',
|
||||
close: '关闭',
|
||||
placeholder: '请输入邮件内容'
|
||||
},
|
||||
wordCite: {
|
||||
noRefs: '参考文献列表尚未加载,请稍候再试或刷新页面',
|
||||
missingBoundRef: '当前绑定的参考文献不存在,请重新引用',
|
||||
notFoundById: '未查询到编号{id}相关参考文献',
|
||||
selectRef: '选择参考文献',
|
||||
originalOrder: '原排序',
|
||||
reference: '参考文献',
|
||||
uncited: '未引用',
|
||||
cancel: '取消',
|
||||
confirm: '确认',
|
||||
remove: '移除',
|
||||
selected: '已选择',
|
||||
modifyRef: '修改引用',
|
||||
removeRefTag: '移除引用',
|
||||
citeUpdateFail: '未能更新正文中的引用标签,请重试或进入编辑修改',
|
||||
matchBracketRefs: '自动链接参考文献',
|
||||
matchBracketRefsDone: '已转换 {n} 处 [n] 为可点击角标',
|
||||
matchBracketRefsNone: '正文中未检测到可转换的纯文本引用 [n]。若要自动编号并关联参考文献,请使用工具栏中的「Reference」插入引用。',
|
||||
removeRefNeedClickCite: '请先在正文中点击要删除的引用角标,再点「移除参考文献」。',
|
||||
quickPickPlaceholder: '输入如 [5, 6, 10-15] 自动勾选对应参考文献',
|
||||
quickPickApply: '链接参考文献',
|
||||
quickPickClear: '清空勾选',
|
||||
currentCiteNo: '当前序号',
|
||||
locateInBody: '点击定位正文角标',
|
||||
locateInRefHint: '在下方列表中高亮该条'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -136,16 +136,7 @@
|
||||
<!-- <div v-else style="padding: 20px; box-sizing: border-box"></div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="content_box mt20 stepbox">
|
||||
<!-- 文章引用 -->
|
||||
<div class="con">
|
||||
<h4 class="con-title">{{ this.$t('PreAccept.step2') }}</h4>
|
||||
<p style="color: #505050; font-size: 14px; padding: 20px; box-sizing: border-box">
|
||||
<el-button @click="goGenerateCharts(thisArtcleId)" icon="el-icon-edit" type="text">Edit</el-button>
|
||||
</p>
|
||||
<!-- <div v-else style="padding: 20px; box-sizing: border-box"></div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content_box mt20 stepbox">
|
||||
<!-- 文章引用 -->
|
||||
<div class="con">
|
||||
@@ -170,7 +161,16 @@
|
||||
<!-- <div v-else style="padding: 20px; box-sizing: border-box"></div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content_box mt20 stepbox">
|
||||
<!-- 文章引用 -->
|
||||
<div class="con">
|
||||
<h4 class="con-title">{{ this.$t('PreAccept.step2') }}</h4>
|
||||
<p style="color: #505050; font-size: 14px; padding: 20px; box-sizing: border-box">
|
||||
<el-button @click="goGenerateCharts(thisArtcleId)" icon="el-icon-edit" type="text">Edit</el-button>
|
||||
</p>
|
||||
<!-- <div v-else style="padding: 20px; box-sizing: border-box"></div> -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- 答疑 -->
|
||||
<div class="mt20 helpcontent">
|
||||
<div class="flexbox">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -332,11 +332,11 @@ export default {
|
||||
// refName: 'setFiveRef',
|
||||
// rongCont: 'Modify the article body.'
|
||||
// },
|
||||
{
|
||||
name: 'Text Proofread',
|
||||
refName: 'setThreeRef',
|
||||
rongCont: 'HTML layout.'
|
||||
}
|
||||
// {
|
||||
// name: 'Text Proofread',
|
||||
// refName: 'setThreeRef',
|
||||
// rongCont: 'HTML layout.'
|
||||
// }
|
||||
// {
|
||||
// name: 'Create Build',
|
||||
// refName: 'setSevenRef',
|
||||
@@ -1337,7 +1337,7 @@ export default {
|
||||
|
||||
// 5----重新获取加载参考文献
|
||||
changeRefer(val) {
|
||||
console.log('重新获取参考文献');
|
||||
|
||||
this.$api
|
||||
.post('api/Production/getReferList', {
|
||||
p_article_id: this.p_article_id
|
||||
@@ -1348,7 +1348,7 @@ export default {
|
||||
for (var i = 0; i < this.chanFerForm.length; i++) {
|
||||
this.chanFerForm[i].edit_mark = 1;
|
||||
}
|
||||
console.log(this.chanFerForm);
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
@@ -1433,7 +1433,7 @@ export default {
|
||||
|
||||
// 5----重新获取加载参考文献
|
||||
changeRefer(val) {
|
||||
console.log('重新获取参考文献');
|
||||
|
||||
this.$api
|
||||
.post('api/Production/getReferList', {
|
||||
p_article_id: this.p_article_id
|
||||
@@ -1444,7 +1444,7 @@ export default {
|
||||
for (var i = 0; i < this.chanFerForm.length; i++) {
|
||||
this.chanFerForm[i].edit_mark = 1;
|
||||
}
|
||||
console.log(this.chanFerForm);
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,17 @@
|
||||
<template>
|
||||
<div class="tinymce-container editor-container">
|
||||
<textarea class="tinymce-textarea" :id="tinymceId"></textarea>
|
||||
<div class="tinymce-editor-surface">
|
||||
<textarea class="tinymce-textarea" :id="tinymceId"></textarea>
|
||||
</div>
|
||||
<div
|
||||
v-if="showRefButton && hasReferencesForAutoLink"
|
||||
ref="autoLinkFooterWrap"
|
||||
class="tinymce-autolink-footer"
|
||||
>
|
||||
<span @click="handleAutoLinkRefsClick" style="cursor: pointer;color: #409eff;font: 12px;font-weight: 700;">
|
||||
<i class="el-icon-link"></i> {{ $t('wordCite.matchBracketRefs') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@@ -63,6 +74,107 @@ export default {
|
||||
},
|
||||
articleId: {
|
||||
default: ''
|
||||
},
|
||||
chanFerForm: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/** 由父级根据全文(稿面 + Edit Content 草稿合并)扫描得到,与 word.vue citeMap 一致 */
|
||||
bodyCiteIdOrder: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/** mytable(data-id) -> 该表内已出现引用的最大全局序号(由父组件计算) */
|
||||
tableLinkCiteMaxMap: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 为 false 时不显示 Ref 工具栏按钮,也不自动在 LateX 后注入 insertRef(用于图表标题等) */
|
||||
showRefButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* true:仅「表格单元格编辑」场景,角标/自动匹配 [n] 按 old_index+1(或 order_index);
|
||||
* false:与稿面一致,始终用 bodyCiteIdOrder 的 citeMap(Edit Content 等虽 type=table 放宽标签,也必须 false)
|
||||
*/
|
||||
useTableLocalCitationIndex: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/** 是否启用表格局部序号(与全文 citeMap 互斥) */
|
||||
tableLocalCiteActive() {
|
||||
if (this.type !== 'table' || !this.useTableLocalCitationIndex) return false;
|
||||
return Object.keys(this.tableBracketNumToRefIdMap || {}).length > 0;
|
||||
},
|
||||
citeMap() {
|
||||
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
||||
const refIdSet = new Set(
|
||||
refs.map((r) => (r && r.p_refer_id != null ? String(r.p_refer_id) : '')).filter(Boolean)
|
||||
);
|
||||
|
||||
if (!Array.isArray(this.bodyCiteIdOrder) || this.bodyCiteIdOrder.length === 0) {
|
||||
const map = {};
|
||||
refs.forEach((row, idx) => {
|
||||
const key = row && row.p_refer_id != null ? String(row.p_refer_id) : '';
|
||||
if (key) map[key] = idx + 1;
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
const orderedIds = this.bodyCiteIdOrder.map(String);
|
||||
const filtered = [];
|
||||
orderedIds.forEach((id) => {
|
||||
if (refIdSet.has(id) && !filtered.includes(id)) filtered.push(id);
|
||||
});
|
||||
|
||||
const map = {};
|
||||
filtered.forEach((id, idx) => {
|
||||
map[id] = idx + 1;
|
||||
});
|
||||
|
||||
let next = filtered.length + 1;
|
||||
refs.forEach((r) => {
|
||||
const key = r && r.p_refer_id != null ? String(r.p_refer_id) : '';
|
||||
if (!key || map[key] != null) return;
|
||||
map[key] = next++;
|
||||
});
|
||||
return map;
|
||||
},
|
||||
/** 有参考文献列表时才显示「自动链接」:与 insertRef 一致,依赖 chanFerForm 中有效 p_refer_id */
|
||||
hasReferencesForAutoLink() {
|
||||
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
||||
return refs.some((r) => r && r.p_refer_id != null && String(r.p_refer_id).trim() !== '');
|
||||
},
|
||||
/**
|
||||
* 表格编辑器专用:单元格 [n] 中的 n → p_refer_id。
|
||||
* - 有 old_index(0 起)时:n = old_index + 1
|
||||
* - 仅有 order_index 时:按接口约定已为与 [n] 一致的序号,n = order_index(不再 +1)
|
||||
*/
|
||||
tableBracketNumToRefIdMap() {
|
||||
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
||||
const map = {};
|
||||
refs.forEach((r) => {
|
||||
if (!r || r.p_refer_id == null) return;
|
||||
const n = this._getTableBracketNoForRow(r);
|
||||
if (n == null) return;
|
||||
map[n] = String(r.p_refer_id);
|
||||
});
|
||||
return map;
|
||||
},
|
||||
/** p_refer_id → 表格角标数字 n(与 [n] 一致) */
|
||||
tableRefIdToBracketNum() {
|
||||
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
||||
const m = {};
|
||||
refs.forEach((r) => {
|
||||
if (!r || r.p_refer_id == null) return;
|
||||
const n = this._getTableBracketNoForRow(r);
|
||||
if (n == null) return;
|
||||
m[String(r.p_refer_id)] = n;
|
||||
});
|
||||
return m;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -124,22 +236,52 @@ export default {
|
||||
hasChange: false,
|
||||
hasInit: false,
|
||||
editorInstance: null,
|
||||
tinymceId: this.id || 'vue-tinymce-' + +new Date()
|
||||
tinymceId: this.id || 'vue-tinymce-' + +new Date(),
|
||||
/** 将底部栏移入 TinyMCE 容器前,记录原 DOM 位置以便销毁时移回 */
|
||||
_autoLinkFooterRestore: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
|
||||
handler(val) {
|
||||
|
||||
|
||||
if (!this.hasChange && this.hasInit) {
|
||||
if (!this.hasChange && this.hasInit) {
|
||||
this.$nextTick(() => {
|
||||
this.handleSetContent(val);
|
||||
});
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
chanFerForm: {
|
||||
handler() {
|
||||
this.$nextTick(() => {
|
||||
window.tinymce.get(this.tinymceId).setContent(val);
|
||||
if (this.editorInstance) {
|
||||
this.renderAutociteInEditor(this.editorInstance);
|
||||
}
|
||||
});
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
bodyCiteIdOrder: {
|
||||
handler() {
|
||||
this.$nextTick(() => {
|
||||
if (this.editorInstance) {
|
||||
this.renderAutociteInEditor(this.editorInstance);
|
||||
}
|
||||
});
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
hasReferencesForAutoLink(val) {
|
||||
if (!val) {
|
||||
this.restoreAutoLinkFooterDom();
|
||||
return;
|
||||
}
|
||||
if (this.showRefButton && this.editorInstance) {
|
||||
this.$nextTick(() => {
|
||||
this.mountAutoLinkFooterInsideEditor(this.editorInstance);
|
||||
});
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -163,10 +305,526 @@ export default {
|
||||
this.destroyTinymce();
|
||||
},
|
||||
methods: {
|
||||
parseAutociteDataIds(attr) {
|
||||
if (!attr || typeof attr !== 'string') return [];
|
||||
return attr
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
},
|
||||
/**
|
||||
* 与正文 [n] 一致的角标数字 n。old_index 为 0 起 → n=old_index+1;无 old_index 时用 order_index(已为 n).
|
||||
*/
|
||||
_getTableBracketNoForRow(r) {
|
||||
if (!r) return null;
|
||||
const pick = (v) => {
|
||||
if (v == null || v === '') return null;
|
||||
const x = Number(v);
|
||||
return Number.isNaN(x) ? null : x;
|
||||
};
|
||||
const oi =
|
||||
r.old_index != null && r.old_index !== ''
|
||||
? pick(r.old_index)
|
||||
: pick(r.oldIndex);
|
||||
if (oi != null) return oi + 1;
|
||||
const ord =
|
||||
r.order_index != null && r.order_index !== ''
|
||||
? pick(r.order_index)
|
||||
: pick(r.orderIndex);
|
||||
return ord;
|
||||
},
|
||||
/** 与 word.vue renderCiteLabels:合并角标 id 按 citeMap 序号排序 */
|
||||
sortAutociteIdsByCiteNumber(ids) {
|
||||
const uniq = [...new Set(ids.map(String))];
|
||||
const map = this.citeMap || {};
|
||||
return uniq.sort((a, b) => {
|
||||
const na = map[a];
|
||||
const nb = map[b];
|
||||
const ha = na != null && na !== '';
|
||||
const hb = nb != null && nb !== '';
|
||||
if (ha && hb) return Number(na) - Number(nb);
|
||||
if (ha) return -1;
|
||||
if (hb) return 1;
|
||||
return String(a).localeCompare(String(b));
|
||||
});
|
||||
},
|
||||
/** 同 word.vue:多 id 均在列表中时对全局序号做 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;
|
||||
},
|
||||
/** 自动匹配 [n]:表格内用 old_index+1 或 order_index(=n);mytable 之后段落仍用全局 citeMap */
|
||||
_getBracketNumToIdMaps() {
|
||||
const globalMap = this.buildNumToRefIdMap();
|
||||
if (this.type !== 'table' || !this.useTableLocalCitationIndex) {
|
||||
return { primary: globalMap, afterTable: globalMap };
|
||||
}
|
||||
const tm = this.tableBracketNumToRefIdMap || {};
|
||||
const has = Object.keys(tm).length > 0;
|
||||
return {
|
||||
primary: has ? tm : globalMap,
|
||||
afterTable: globalMap
|
||||
};
|
||||
},
|
||||
sortAutociteIdsByTableBracketNumber(ids) {
|
||||
const uniq = [...new Set(ids.map(String))];
|
||||
const numById = this.tableRefIdToBracketNum || {};
|
||||
return uniq.sort((a, b) => {
|
||||
const na = numById[a];
|
||||
const nb = numById[b];
|
||||
const ha = na != null && na !== '' && !Number.isNaN(Number(na));
|
||||
const hb = nb != null && nb !== '' && !Number.isNaN(Number(nb));
|
||||
if (ha && hb) return Number(na) - Number(nb);
|
||||
if (ha) return -1;
|
||||
if (hb) return 1;
|
||||
return String(a).localeCompare(String(b));
|
||||
});
|
||||
},
|
||||
_sortAutociteIdsForDisplay(ids) {
|
||||
if (this.tableLocalCiteActive) {
|
||||
return this.sortAutociteIdsByTableBracketNumber(ids);
|
||||
}
|
||||
return this.sortAutociteIdsByCiteNumber(ids);
|
||||
},
|
||||
_sortIdsAfterBracketMatch(ids, tableOffset) {
|
||||
if (this.tableLocalCiteActive && tableOffset === 0) {
|
||||
return this.sortAutociteIdsByTableBracketNumber(ids);
|
||||
}
|
||||
return this.sortAutociteIdsByCiteNumber(ids);
|
||||
},
|
||||
/** 解析 [1]、[1,2]、[1–4]、[1, 2–3, 4] 等括号内数字列表(不含方括号) */
|
||||
parseBracketInnerToNumbers(inner) {
|
||||
if (!inner || typeof inner !== 'string') return [];
|
||||
const t = inner.trim().replace(/,/g, ',');
|
||||
// 先按逗号拆段,再对每段解析单号或区间;避免 parseInt('41-42') 只得到 41
|
||||
if (/[,,]/.test(t)) {
|
||||
const parts = t.split(/[,,]/).map((s) => String(s).trim()).filter(Boolean);
|
||||
const out = [];
|
||||
parts.forEach((part) => {
|
||||
out.push(...this.parseBracketInnerToNumbers(part));
|
||||
});
|
||||
return out;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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 maps = this._getBracketNumToIdMaps();
|
||||
if (Object.keys(maps.primary).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(() => {
|
||||
// 先把被拆成多个 span 的 [13, 16, 21–23] 规整成单段文本,再做自动匹配
|
||||
this.normalizeSplitBracketCitesInTableCells(body);
|
||||
replaced = this._replaceBracketCitesInDocOrder(body, doc, maps, { tableOffset: 0 });
|
||||
});
|
||||
this.renderAutociteInEditor(ed);
|
||||
ed.fire('change');
|
||||
return { replaced };
|
||||
},
|
||||
/**
|
||||
* TinyMCE/粘贴可能把单元格里的 [1, 2–3] 拆成多个内联节点(span/text 混排),
|
||||
* 导致按文本节点匹配的自动链接无法命中。这里在匹配前把该类节点规整回纯文本。
|
||||
*/
|
||||
normalizeSplitBracketCitesInTableCells(root) {
|
||||
if (!root || !root.querySelectorAll) return;
|
||||
const cells = root.querySelectorAll('td, th');
|
||||
cells.forEach((cell) => {
|
||||
if (!cell || !cell.querySelectorAll) return;
|
||||
if (cell.querySelector('mycite, autocite, wmath')) return;
|
||||
if (!cell.querySelector('span')) return;
|
||||
const raw = (cell.textContent || '').replace(/\u200b/g, '');
|
||||
const compact = raw.replace(/\s+/g, ' ').trim();
|
||||
if (!/^\[[\d\s,,\-–—]+\]$/.test(compact)) return;
|
||||
cell.textContent = compact;
|
||||
});
|
||||
},
|
||||
/** 底部「自动链接参考文献」按钮,与原先工具栏 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 maps = this._getBracketNumToIdMaps();
|
||||
if (refs.length && Object.keys(maps.primary).length) {
|
||||
this.$message.info(this.$t('wordCite.matchBracketRefsNone'));
|
||||
}
|
||||
},
|
||||
/** 把「自动链接」栏挂到 TinyMCE 外壳 .tox-tinymce 内部底部,与工具栏/编辑区同一外框 */
|
||||
mountAutoLinkFooterInsideEditor(ed) {
|
||||
if (!this.showRefButton || !this.hasReferencesForAutoLink) return;
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs.autoLinkFooterWrap;
|
||||
if (!el || !ed) return;
|
||||
const container = ed.getContainer && ed.getContainer();
|
||||
if (!container) return;
|
||||
if (el.parentNode === container) return;
|
||||
this._autoLinkFooterRestore = {
|
||||
parent: el.parentNode,
|
||||
next: el.nextSibling
|
||||
};
|
||||
container.appendChild(el);
|
||||
});
|
||||
},
|
||||
/** 销毁编辑器前必须移回,否则节点会随 .tox-tinymce 一起被移除 */
|
||||
restoreAutoLinkFooterDom() {
|
||||
const el = this.$refs.autoLinkFooterWrap;
|
||||
const r = this._autoLinkFooterRestore;
|
||||
this._autoLinkFooterRestore = null;
|
||||
if (!el || !r || !r.parent) return;
|
||||
try {
|
||||
if (r.next && r.next.parentNode === r.parent) {
|
||||
r.parent.insertBefore(el, r.next);
|
||||
} else {
|
||||
r.parent.appendChild(el);
|
||||
}
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 按文档顺序遍历,使「表格链接」后任意后续段落里的 [n] 都能用上该表的最大全局序号偏移。
|
||||
* 旧实现只在同一父节点下、MYTABLE 与后续文本为兄弟时才生效,MYTABLE 在上一段、角标在下一段时会失效。
|
||||
*/
|
||||
_replaceBracketCitesInDocOrder(node, doc, maps, state) {
|
||||
if (node.nodeType === 3) {
|
||||
return this._processTextNodeForBracketCites(node, doc, maps, state.tableOffset);
|
||||
}
|
||||
if (node.nodeType !== 1) return 0;
|
||||
const name = node.nodeName;
|
||||
if (name === 'AUTOCITE' || name === 'MYCITE' || name === 'WMATH' || name === 'SCRIPT' || name === 'STYLE') {
|
||||
return 0;
|
||||
}
|
||||
const isMytable = name === 'MYTABLE' || (node.tagName && String(node.tagName).toLowerCase() === 'mytable');
|
||||
if (isMytable) {
|
||||
let total = 0;
|
||||
Array.from(node.childNodes).forEach((c) => {
|
||||
total += this._replaceBracketCitesInDocOrder(c, doc, maps, state);
|
||||
});
|
||||
const tid = (node.getAttribute && node.getAttribute('data-id')) || '';
|
||||
const map = this.tableLinkCiteMaxMap || {};
|
||||
const tableMax = Number(map[String(tid)]);
|
||||
if (!Number.isNaN(tableMax) && tableMax > 0) {
|
||||
state.tableOffset = tableMax;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
let total = 0;
|
||||
Array.from(node.childNodes).forEach((c) => {
|
||||
total += this._replaceBracketCitesInDocOrder(c, doc, maps, state);
|
||||
});
|
||||
return total;
|
||||
},
|
||||
_processTextNodeForBracketCites(textNode, doc, maps, tableOffset = 0) {
|
||||
const numToId = tableOffset > 0 ? maps.afterTable : maps.primary;
|
||||
const text = textNode.textContent;
|
||||
const re = /\[([\d\s,,\-–—]+)\]/g;
|
||||
let m;
|
||||
let lastIndex = 0;
|
||||
const pieces = [];
|
||||
let replaced = 0;
|
||||
let skippedSpecial = 0;
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
const rawNums = this.parseBracketInnerToNumbers(m[1]);
|
||||
// 仅 [0]、[-1]、[0, -1] 等「全是 0/-1」不转换;与正常号混写时去掉 0/-1 再匹配
|
||||
const onlyZeroOrNegOne =
|
||||
rawNums.length > 0 && rawNums.every((n) => n === 0 || n === -1);
|
||||
if (onlyZeroOrNegOne) {
|
||||
pieces.push({ type: 'text', s: text.slice(lastIndex, m.index) });
|
||||
pieces.push({ type: 'text', s: m[0] });
|
||||
lastIndex = m.index + m[0].length;
|
||||
skippedSpecial++;
|
||||
continue;
|
||||
}
|
||||
const nums = rawNums.filter((n) => n !== 0 && n !== -1);
|
||||
const mapNo = (n) => (n > 0 && tableOffset > 0 ? n + tableOffset - 1 : n);
|
||||
|
||||
if (!nums.length) {
|
||||
pieces.push({ type: 'text', s: text.slice(lastIndex, m.index) });
|
||||
pieces.push({ type: 'text', s: m[0] });
|
||||
lastIndex = m.index + m[0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
const validNums = nums.filter((n) => numToId[mapNo(n)]);
|
||||
const invalidNums = nums.filter((n) => !numToId[mapNo(n)]);
|
||||
|
||||
if (validNums.length === 0) {
|
||||
pieces.push({ type: 'text', s: text.slice(lastIndex, m.index) });
|
||||
pieces.push({ type: 'text', s: m[0] });
|
||||
lastIndex = m.index + m[0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
const ids = [];
|
||||
const seen = new Set();
|
||||
validNums.forEach((n) => {
|
||||
const mappedNo = mapNo(n);
|
||||
const id = numToId[mappedNo];
|
||||
if (id && !seen.has(id)) {
|
||||
seen.add(id);
|
||||
ids.push(id);
|
||||
}
|
||||
});
|
||||
pieces.push({ type: 'text', s: text.slice(lastIndex, m.index) });
|
||||
if (ids.length > 0) {
|
||||
const sorted = this._sortIdsAfterBracketMatch(ids, tableOffset);
|
||||
pieces.push({ type: 'cite', ids: sorted });
|
||||
if (invalidNums.length) {
|
||||
const invSorted = [...new Set(invalidNums)].sort((a, b) => a - b);
|
||||
const invLabel = this.formatCiteNumbers(invSorted);
|
||||
if (invLabel) {
|
||||
pieces.push({ type: 'text', s: ', [' + invLabel + ']' });
|
||||
}
|
||||
}
|
||||
replaced++;
|
||||
} else {
|
||||
pieces.push({ type: 'text', s: m[0] });
|
||||
}
|
||||
lastIndex = m.index + m[0].length;
|
||||
}
|
||||
if (replaced === 0) return 0;
|
||||
pieces.push({ type: 'text', s: text.slice(lastIndex) });
|
||||
const parent = textNode.parentNode;
|
||||
if (!parent) return 0;
|
||||
const frag = doc.createDocumentFragment();
|
||||
pieces.forEach((p) => {
|
||||
if (p.type === 'text') {
|
||||
frag.appendChild(doc.createTextNode(p.s));
|
||||
} else {
|
||||
const ac = doc.createElement('mycite');
|
||||
ac.setAttribute('data-id', p.ids.join(','));
|
||||
ac.setAttribute('contenteditable', 'false');
|
||||
ac.appendChild(doc.createTextNode('\u200b'));
|
||||
frag.appendChild(ac);
|
||||
}
|
||||
});
|
||||
parent.replaceChild(frag, textNode);
|
||||
if (skippedSpecial > 0) {
|
||||
// 提示:仅提示一次即可;这里在节点级别提示可能重复,放到宏任务末尾合并展示
|
||||
clearTimeout(this._autoLinkSkipToastTimer);
|
||||
this._autoLinkSkipToastTimer = setTimeout(() => {
|
||||
this.$message.info(`Skipped ${skippedSpecial} bracket cite(s) containing only 0 or -1.`);
|
||||
}, 0);
|
||||
}
|
||||
return replaced;
|
||||
},
|
||||
renderAutociteInEditor(ed) {
|
||||
const body = ed.getBody();
|
||||
if (!body) return;
|
||||
const allAutocites = Array.from(
|
||||
ed.dom && typeof ed.dom.select === 'function'
|
||||
? ed.dom.select('mycite,autocite', body)
|
||||
: body.querySelectorAll('mycite, autocite')
|
||||
);
|
||||
if (!allAutocites.length) return;
|
||||
|
||||
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
||||
const refMap = {};
|
||||
refs.forEach((item) => {
|
||||
const key = item && item.p_refer_id != null ? String(item.p_refer_id) : '';
|
||||
if (key) refMap[key] = item;
|
||||
});
|
||||
|
||||
const citeMap = this.citeMap || {};
|
||||
const tableNums = this.tableRefIdToBracketNum || {};
|
||||
const useTableBracketNums = this.tableLocalCiteActive;
|
||||
|
||||
allAutocites.forEach((el) => {
|
||||
ed.dom.setAttrib(el, 'contenteditable', 'false');
|
||||
el.style.display = '';
|
||||
const sortedAll = this._sortAutociteIdsForDisplay(this.parseAutociteDataIds(el.getAttribute('data-id')));
|
||||
const validIds = sortedAll.filter((id) => refMap[String(id)]);
|
||||
if (validIds.length < sortedAll.length) {
|
||||
ed.dom.setAttrib(el, 'data-id', validIds.length ? validIds.join(',') : '');
|
||||
}
|
||||
const sortedIds = validIds.length ? this._sortAutociteIdsForDisplay(validIds) : [];
|
||||
|
||||
const parts = sortedIds.map((id) => {
|
||||
const ref = refMap[String(id)];
|
||||
const noCite = ref ? citeMap[String(id)] : null;
|
||||
const noTable =
|
||||
useTableBracketNums && tableNums[String(id)] != null ? tableNums[String(id)] : null;
|
||||
// 单元格内 [n] 按 Original order(table 序号)匹配;角标展示与正文一致,优先用 citeMap 对应序号
|
||||
const num =
|
||||
noCite != null && noCite !== '' && !Number.isNaN(Number(noCite))
|
||||
? String(noCite)
|
||||
: noTable != null && noTable !== '' && !Number.isNaN(Number(noTable))
|
||||
? String(noTable)
|
||||
: null;
|
||||
return { id, ref, num };
|
||||
});
|
||||
|
||||
const numsForLabel = parts
|
||||
.map((p) => p.num)
|
||||
.filter((n) => n != null && n !== '')
|
||||
.map(Number);
|
||||
const label = numsForLabel.length > 0 ? this.formatCiteNumbers(numsForLabel) : '';
|
||||
|
||||
if (!label) {
|
||||
ed.dom.remove(el);
|
||||
return;
|
||||
}
|
||||
|
||||
el.textContent = `[${label}]`;
|
||||
el.style.display = '';
|
||||
if (el.removeAttribute) el.removeAttribute('title');
|
||||
ed.dom.setAttrib(el, 'data-cite-missing', null);
|
||||
if (String(el.tagName || '').toLowerCase() === 'autocite') {
|
||||
const doc = ed.getDoc();
|
||||
const nu = doc.createElement('mycite');
|
||||
nu.setAttribute('data-id', el.getAttribute('data-id') || '');
|
||||
nu.setAttribute('contenteditable', 'false');
|
||||
const st = el.getAttribute('style');
|
||||
if (st) nu.setAttribute('style', st);
|
||||
nu.textContent = el.textContent;
|
||||
el.parentNode.replaceChild(nu, el);
|
||||
}
|
||||
});
|
||||
this.padAutociteCaretPlaceholder(ed);
|
||||
},
|
||||
/** 段尾不可编辑节点后浏览器/TinyMCE 容易把后续输入新开 <p>,在引用后补零宽空格让光标留在同一段 */
|
||||
padAutociteCaretPlaceholder(ed) {
|
||||
const doc = ed.getDoc();
|
||||
const body = doc.body;
|
||||
if (!body) return;
|
||||
body.querySelectorAll('mycite, autocite').forEach((el) => {
|
||||
const next = el.nextSibling;
|
||||
if (next === null) {
|
||||
el.parentNode.appendChild(doc.createTextNode('\u200b'));
|
||||
return;
|
||||
}
|
||||
if (next.nodeType === 3) {
|
||||
if (next.textContent === '\u200b') return;
|
||||
if (next.textContent === '') {
|
||||
next.textContent = '\u200b';
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
insertAutocite(refIds) {
|
||||
const ed = this.editorInstance;
|
||||
if (!ed) return;
|
||||
const ids = (Array.isArray(refIds) ? refIds : [refIds]).map(String);
|
||||
const dataId = ids.join(',');
|
||||
const escaped = dataId.replace(/"/g, '"');
|
||||
|
||||
if (this._editingAutocite) {
|
||||
this._editingAutocite.setAttribute('data-id', dataId);
|
||||
this._editingAutocite = null;
|
||||
ed.fire('change');
|
||||
} else {
|
||||
if (this._refBookmark) {
|
||||
ed.selection.moveToBookmark(this._refBookmark);
|
||||
}
|
||||
ed.insertContent(`<mycite data-id="${escaped}" contenteditable="false"></mycite>​`);
|
||||
}
|
||||
this.renderAutociteInEditor(ed);
|
||||
},
|
||||
removeAutocite() {
|
||||
const ed = this.editorInstance;
|
||||
if (!ed || !this._editingAutocite) return;
|
||||
ed.dom.remove(this._editingAutocite);
|
||||
this._editingAutocite = null;
|
||||
ed.fire('change');
|
||||
},
|
||||
/**
|
||||
* 从当前编辑的 mycite 中去掉指定 p_refer_id;去掉后无 id 则删除整段标签。
|
||||
* ids 为空:删除整段(与 removeAutocite 一致)。
|
||||
*/
|
||||
stripAutociteIds(idsToRemove) {
|
||||
const ed = this.editorInstance;
|
||||
if (!ed || !this._editingAutocite) return;
|
||||
const remove = new Set((idsToRemove || []).map((id) => String(id)));
|
||||
const el = this._editingAutocite;
|
||||
this._editingAutocite = null;
|
||||
if (remove.size === 0) {
|
||||
ed.dom.remove(el);
|
||||
ed.fire('change');
|
||||
return;
|
||||
}
|
||||
const parts = this.parseAutociteDataIds(el.getAttribute('data-id') || '');
|
||||
const remaining = parts.filter((id) => !remove.has(String(id)));
|
||||
if (remaining.length === 0) {
|
||||
ed.dom.remove(el);
|
||||
} else {
|
||||
const sorted = this.sortAutociteIdsByCiteNumber(remaining);
|
||||
el.setAttribute('data-id', sorted.join(','));
|
||||
this.renderAutociteInEditor(ed);
|
||||
}
|
||||
ed.fire('change');
|
||||
},
|
||||
/** TinyMCE 会剔除「空」的行内标签;空 mycite 必须在入编辑器前占位,否则合并引用 [1–3] 等整段消失 */
|
||||
normalizeAutociteHtmlForEditor(html) {
|
||||
if (!html || typeof html !== 'string') return html;
|
||||
// 历史库/表格里可能为 <autocite>,与 mycite 统一,否则 renderAutociteInEditor 扫不到、无效 id 会残留
|
||||
let out = html.replace(/<\/autocite>/gi, '</mycite>').replace(/<autocite\b/gi, '<mycite');
|
||||
/** 角标显示一律由 renderAutociteInEditor 根据 data-id 生成,禁止保留库内遗留的 [1-4]、[1–4] 等旧文案 */
|
||||
out = out.replace(/<mycite([^>]*)>[\s\S]*?<\/mycite>/gi, '<mycite$1>​</mycite>');
|
||||
// 外侧:连续空格 / 合并为单个 ,避免「普通空格 + 」叠成大缝
|
||||
out = out.replace(/(?:\s| | )+(?=<mycite\b)/gi, ' ');
|
||||
out = out.replace(/(?<=<\/mycite>)(?:\s| | )+/gi, ' ');
|
||||
return out;
|
||||
},
|
||||
handleSetContent(val) {
|
||||
if (!this.editorInstance) return;
|
||||
|
||||
let finalContent = val || '';
|
||||
let finalContent = this.normalizeAutociteHtmlForEditor(val || '');
|
||||
// 你的业务逻辑:自动包裹 <p> 标签
|
||||
if (!finalContent.includes('wordTableHtml') && !finalContent.startsWith('<p>')) {
|
||||
finalContent = '<p>' + finalContent + '</p>';
|
||||
@@ -174,8 +832,11 @@ export default {
|
||||
|
||||
this.editorInstance.setContent(finalContent);
|
||||
|
||||
// 渲染数学公式
|
||||
// SetContent 回调里会 renderAutociteInEditor;再补一次 nextTick,避免时序下仍显示库内旧 [1-4]
|
||||
this.$nextTick(() => {
|
||||
if (this.editorInstance) {
|
||||
this.renderAutociteInEditor(this.editorInstance);
|
||||
}
|
||||
if (window.renderMathJax) {
|
||||
window.renderMathJax(this.tinymceId);
|
||||
}
|
||||
@@ -320,7 +981,10 @@ export default {
|
||||
},
|
||||
getDetail(val) {
|
||||
if (this.hasInit == true) {
|
||||
this.$nextTick(() => window.tinymce.get(this.tinymceId).setContent(val));
|
||||
this.$nextTick(() => {
|
||||
const ed = window.tinymce.get(this.tinymceId);
|
||||
if (ed) ed.setContent(this.normalizeAutociteHtmlForEditor(val || ''));
|
||||
});
|
||||
}
|
||||
},
|
||||
//将字符串添加到富文本编辑器中
|
||||
@@ -431,7 +1095,7 @@ export default {
|
||||
return new Blob([u8arr], { type: mime });
|
||||
},
|
||||
formatHtml(val) {
|
||||
const rawValue = val || ''; // 处理 null
|
||||
const rawValue = this.normalizeAutociteHtmlForEditor(val || ''); // 须先于 cleanEmptyTags,否则空 mycite 被删
|
||||
const cleanEmptyTags = /<([a-zA-Z1-6]+)\b[^>]*><\/\1>/g;
|
||||
const replaceSpaces = /\s+(?=<)|(?<=>)\s+/g;
|
||||
const removeBr = /<br\s*\/?>/gi; // 移除所有 br 标签
|
||||
@@ -456,7 +1120,7 @@ export default {
|
||||
}
|
||||
},
|
||||
getSafeContent(val) {
|
||||
const rawValue = val || '';
|
||||
const rawValue = this.normalizeAutociteHtmlForEditor(val || '');
|
||||
const cleanEmptyTags = /<([a-zA-Z1-6]+)\b[^>]*><\/\1>/g;
|
||||
const replaceSpaces = /\s+(?=<)|(?<=>)\s+/g;
|
||||
|
||||
@@ -497,9 +1161,10 @@ export default {
|
||||
window.tinymce.init({
|
||||
..._this.tinymceOtherInit,
|
||||
trim_span_elements: false, // 禁止修剪内联标签周围的空格
|
||||
extended_valid_elements: 'blue[*]',
|
||||
custom_elements: 'blue',
|
||||
valid_children: '+blue[#text|i|em|b|strong|span],+body[blue],+p[blue]',
|
||||
extended_valid_elements: 'blue[*],mycite[*]',
|
||||
/* ~ 前缀:按行内(类似 span)处理,否则默认当块级会拆段导致引用后强制换行 */
|
||||
custom_elements: 'blue,~mycite',
|
||||
valid_children: '+blue[#text|i|em|b|strong|span],+body[blue|mycite],+p[blue|mycite]',
|
||||
|
||||
inline: false, // 使用 iframe 模式
|
||||
selector: `#${this.tinymceId}`,
|
||||
@@ -509,7 +1174,7 @@ export default {
|
||||
valid_elements:
|
||||
this.type == 'table'
|
||||
? '*[*]'
|
||||
: `img[src|alt|width|height],strong,em,sub,sup,blue,table,b,i,myfigure,mytable,wmath${this.valid_elements}`, // 允许的标签和属性
|
||||
: `img[src|alt|width|height],strong,em,sub,sup,blue,table,b,i,myfigure,mytable,wmath,mycite[data-id|contenteditable|data-cite-missing|style]${this.valid_elements}`, // 允许的标签和属性(mycite 不使用 title 悬停)
|
||||
// valid_elements: '*[*]', // 允许所有 HTML 标签
|
||||
noneditable_editable_class: 'MathJax',
|
||||
height: this.height,
|
||||
@@ -541,6 +1206,34 @@ export default {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
/* inline 与 blue 引用一致,避免 inline-block 在行尾产生多余换行感 */
|
||||
mycite {
|
||||
display: inline;
|
||||
vertical-align: baseline;
|
||||
color: rgb(0, 130, 170);
|
||||
// font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 0 2px;
|
||||
border-radius: 3px;
|
||||
background-color: rgba(0, 130, 170, 0.08);
|
||||
user-select: all;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
mycite:hover {
|
||||
background-color: rgba(0, 130, 170, 0.2);
|
||||
text-shadow: 0 0 3px rgba(0, 130, 170, 0.3);
|
||||
}
|
||||
mycite[data-cite-missing] {
|
||||
background-color: rgba(0, 130, 170, 0.08) !important;
|
||||
color: rgb(0, 130, 170) !important;
|
||||
text-shadow: 0 0 3px rgba(0, 130, 170, 0.3);
|
||||
box-shadow: none !important;
|
||||
}
|
||||
mycite[data-cite-missing]:hover {
|
||||
background-color: rgba(0, 130, 170, 0.2) !important;
|
||||
}
|
||||
|
||||
@keyframes blueGlow {
|
||||
0%,
|
||||
100% {
|
||||
@@ -559,7 +1252,37 @@ export default {
|
||||
},
|
||||
body_class: 'panel-body ',
|
||||
object_resizing: false,
|
||||
toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
|
||||
toolbar: (function () {
|
||||
const tb = _this.toolbar;
|
||||
const hasCustom =
|
||||
(Array.isArray(tb) && tb.length > 0) ||
|
||||
(typeof tb === 'string' && tb.length > 0);
|
||||
const refInsert = _this.showRefButton ? ' insertRef' : '';
|
||||
/* 自动匹配 [n] 为角标:底部蓝色按钮,见模板 tinymce-autolink-footer */
|
||||
/* 与 content.vue 默认一致;含 insertRef 由 showRefButton 控制 */
|
||||
const defaultToolbar = [
|
||||
`bold italic |customBlue removeBlue|LateX${refInsert}| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace`
|
||||
];
|
||||
if (!hasCustom) return defaultToolbar;
|
||||
const rows = Array.isArray(tb) ? tb : [tb];
|
||||
return rows.map((row) => {
|
||||
if (typeof row !== 'string') return row;
|
||||
let out = row;
|
||||
if (!_this.showRefButton) {
|
||||
out = out
|
||||
.replace(/\binsertRef\b/g, '')
|
||||
.replace(/\bautoLinkRefs\b/g, '')
|
||||
.replace(/\|\s*\|/g, '|')
|
||||
.replace(/\|\s*$/g, '')
|
||||
.replace(/^\s*\|/g, '');
|
||||
} else if (out.indexOf('insertRef') === -1) {
|
||||
out = out
|
||||
.replace(/\|LateX\|/g, '|LateX insertRef|')
|
||||
.replace(/\|\s*LateX\s*\|/g, '| LateX insertRef |');
|
||||
}
|
||||
return out;
|
||||
});
|
||||
})(),
|
||||
menubar: false, // 启用菜单栏并保持必要的项目
|
||||
statusbar: false, // 关闭底部状态栏
|
||||
custom_colors: false,
|
||||
@@ -626,7 +1349,27 @@ export default {
|
||||
let currentPasteImages = [];
|
||||
_this.$commonJS.initEditorButton(_this, ed);
|
||||
var currentWmathElement = null;
|
||||
|
||||
ed.ui.registry.addButton('insertRef', {
|
||||
text: 'Reference',
|
||||
tooltip: 'Insert Reference',
|
||||
onAction: function () {
|
||||
_this._refBookmark = ed.selection.getBookmark(2);
|
||||
_this._editingAutocite = null;
|
||||
_this.$emit('openRefSelector', { currentRefIds: [] });
|
||||
}
|
||||
});
|
||||
|
||||
ed.on('click', function (e) {
|
||||
const autociteEl = e.target.closest('mycite') || e.target.closest('autocite');
|
||||
if (autociteEl) {
|
||||
const dataIds = _this.parseAutociteDataIds(autociteEl.getAttribute('data-id'));
|
||||
_this._refBookmark = ed.selection.getBookmark(2);
|
||||
_this._editingAutocite = autociteEl;
|
||||
_this.$emit('openRefSelector', { currentRefIds: dataIds });
|
||||
return;
|
||||
}
|
||||
|
||||
const wmathElement = e.target.closest('wmath');
|
||||
if (wmathElement) {
|
||||
currentWmathElement = wmathElement; // 保存当前点击的元素
|
||||
@@ -757,6 +1500,8 @@ export default {
|
||||
subtree: true,
|
||||
characterData: true
|
||||
});
|
||||
|
||||
_this.mountAutoLinkFooterInsideEditor(ed);
|
||||
});
|
||||
|
||||
// 定义自定义按钮
|
||||
@@ -791,14 +1536,20 @@ export default {
|
||||
const editorBody = ed.getBody();
|
||||
ed.dom.select('wmath', editorBody).forEach(function (wmathElement) {
|
||||
ed.dom.setAttrib(wmathElement, 'contenteditable', 'false');
|
||||
// ed.dom.addClass(wmathElement, 'non-editable-wmath');
|
||||
});
|
||||
_this.renderAutociteInEditor(ed);
|
||||
e.content = e.content.replace(/<strong>/g, '<b>').replace(/<\/strong>/g, '</b>');
|
||||
e.content = e.content.replace(/<em>/g, '<i>').replace(/<\/em>/g, '</i>');
|
||||
});
|
||||
ed.on('GetContent', function (e) {
|
||||
e.content = e.content.replace(/<b>/g, '<strong>').replace(/<\/b>/g, '</strong>');
|
||||
e.content = e.content.replace(/<i>/g, '<em>').replace(/<\/i>/g, '</em>');
|
||||
e.content = e.content.replace(/<i>/g, '<em>').replace(/<\/em>/g, '</em>');
|
||||
e.content = e.content.replace(/<\/mycite>\s*​/gi, '</mycite>');
|
||||
e.content = e.content.replace(/<\/mycite>\s*\u200b/g, '</mycite>');
|
||||
e.content = e.content.replace(/<mycite([^>]*)>[^<]*<\/mycite>/gi, function (match, attrs) {
|
||||
var clean = attrs.replace(/\s*style="[^"]*"/gi, '').replace(/\s*title="[^"]*"/gi, '').replace(/\s*contenteditable="[^"]*"/gi, '');
|
||||
return '<mycite' + clean + '></mycite>';
|
||||
});
|
||||
});
|
||||
},
|
||||
paste_preprocess: function (plugin, args) {
|
||||
@@ -958,13 +1709,15 @@ export default {
|
||||
//销毁富文本
|
||||
destroyTinymce() {
|
||||
this.onClear();
|
||||
this.restoreAutoLinkFooterDom();
|
||||
if (window.tinymce.get(this.tinymceId)) {
|
||||
window.tinymce.get(this.tinymceId).destroy();
|
||||
}
|
||||
},
|
||||
//设置内容
|
||||
setContent(value) {
|
||||
window.tinymce.get(this.tinymceId).setContent(value);
|
||||
const ed = window.tinymce.get(this.tinymceId);
|
||||
if (ed) ed.setContent(this.normalizeAutociteHtmlForEditor(value || ''));
|
||||
},
|
||||
//获取内容
|
||||
async getContent(type) {
|
||||
@@ -973,7 +1726,8 @@ export default {
|
||||
content = content.replace(/<span[^>]*>/g, '').replace(/<\/span>/g, ''); // 去除span标签
|
||||
content = content.replace(/<strong>/g, '<b>').replace(/<\/strong>/g, '</b>');
|
||||
content = content.replace(/<em>/g, '<i>').replace(/<\/em>/g, '</i>');
|
||||
content = content.replace(/ /g, ' '); // 将所有 替换为空格
|
||||
content = content.replace(/ /g, ' '); // 先统一为空格
|
||||
content = this.normalizeAutociteHtmlForEditor(content); // mycite 两侧水平空白以 存库(与入编辑器一致)
|
||||
|
||||
this.$emit('getContent', type, content);
|
||||
},
|
||||
@@ -1058,6 +1812,26 @@ export default {
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.tinymce-editor-surface {
|
||||
width: 100%;
|
||||
}
|
||||
.tinymce-autolink-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding-top: 8px;
|
||||
margin-top: 4px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
/* 移入 .tox-tinymce 内部后:贴编辑区下沿、与编辑器同宽同框 */
|
||||
::v-deep .tox-tinymce > .tinymce-autolink-footer {
|
||||
margin-top: 0;
|
||||
padding: 0px 10px;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid #dcdfe6;
|
||||
background: #fafafa;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
::v-deep .tox-tinymce-aux {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
@@ -58,9 +58,25 @@
|
||||
<script>
|
||||
import { TableUtils } from '@/common/js/TableUtils';
|
||||
import { mediaUrl } from '@/common/js/commonJS.js';
|
||||
import {
|
||||
buildCiteMapFromRefs,
|
||||
applyCiteLabelsToTableRows
|
||||
} from '@/common/js/citeTablePreview.js';
|
||||
|
||||
export default {
|
||||
name: 'TablePreviewer',
|
||||
props: {
|
||||
/** 与稿面 chanFerForm 一致,用于把单元格内 <mycite> 显示为 [n] */
|
||||
referenceList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/** 正文引用首次出现顺序(p_refer_id),与 GenerateCharts articleCiteIdOrder 一致 */
|
||||
bodyCiteIdOrder: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
@@ -70,23 +86,32 @@ export default {
|
||||
mediaUrl,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
previewCiteMap() {
|
||||
return buildCiteMapFromRefs(this.referenceList, this.bodyCiteIdOrder);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async open(type, item,isNoBg) {
|
||||
async open(type, item, isNoBg) {
|
||||
this.visible = true;
|
||||
this.type = type;
|
||||
const hideOddRowBg = isNoBg === true || isNoBg === 'true' || isNoBg === 1 || isNoBg === '1';
|
||||
|
||||
if (type === 'table') {
|
||||
this.loading = true;
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const processed = this.processTableData(item.table);
|
||||
const processed = this.processTableData(item.table, {
|
||||
refs: this.referenceList,
|
||||
citeMap: this.previewCiteMap
|
||||
});
|
||||
this.processedItem = Object.freeze({
|
||||
...item,
|
||||
table: {
|
||||
...item.table,
|
||||
tableHeader: processed.tableHeader,
|
||||
tableContent: processed.tableContent,
|
||||
oddRowIds: isNoBg ? [] : processed.oddRowIds
|
||||
oddRowIds: hideOddRowBg ? [] : processed.oddRowIds
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -97,6 +122,7 @@ export default {
|
||||
}, 50);
|
||||
} else {
|
||||
this.processedItem = item;
|
||||
|
||||
}
|
||||
|
||||
// 数学公式渲染
|
||||
@@ -105,12 +131,20 @@ export default {
|
||||
}, 500);
|
||||
},
|
||||
|
||||
processTableData(rawContent) {
|
||||
processTableData(rawContent, citeCtx) {
|
||||
try {
|
||||
const tableList = typeof rawContent === 'string' ? JSON.parse(rawContent) : rawContent;
|
||||
const { header, content } = TableUtils.splitTable(tableList);
|
||||
const { rowData, rowIds } = TableUtils.addRowIdToData(content);
|
||||
return { tableHeader: header, tableContent: rowData, oddRowIds: rowIds };
|
||||
const refs = citeCtx && citeCtx.refs;
|
||||
const citeMap = citeCtx && citeCtx.citeMap;
|
||||
let tableHeader = header;
|
||||
let tableContent = rowData;
|
||||
if (Array.isArray(refs) && refs.length > 0 && citeMap && typeof citeMap === 'object') {
|
||||
tableHeader = applyCiteLabelsToTableRows(header, refs, citeMap);
|
||||
tableContent = applyCiteLabelsToTableRows(rowData, refs, citeMap);
|
||||
}
|
||||
return { tableHeader, tableContent, oddRowIds: rowIds };
|
||||
} catch (e) {
|
||||
return { tableHeader: [], tableContent: [], oddRowIds: [] };
|
||||
}
|
||||
@@ -238,6 +272,16 @@ export default {
|
||||
background: rgb(250, 231, 232) !important;
|
||||
}
|
||||
|
||||
/* 与稿面 mycite 引用样式一致,弹窗内可见 [n] */
|
||||
.table_Box ::v-deep mycite {
|
||||
display: inline;
|
||||
vertical-align: baseline;
|
||||
color: rgb(0, 130, 170) !important;
|
||||
cursor: inherit;
|
||||
text-decoration: none;
|
||||
background-color: rgba(0, 130, 170, 0.08);
|
||||
}
|
||||
|
||||
.table-fade-enter-active, .table-fade-leave-active { transition: opacity 0.3s ease; }
|
||||
.table-fade-enter, .table-fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
@@ -10,13 +10,19 @@
|
||||
ref="tinymceChild1"
|
||||
:wordStyle="wordStyle"
|
||||
:isAutomaticUpdate="isAutomaticUpdate"
|
||||
:showRefButton="showRefButton"
|
||||
@getContent="getContent"
|
||||
@openLatexEditor="openLatexEditor"
|
||||
@openRefSelector="openRefSelector"
|
||||
@updateChange="updateChange"
|
||||
@input="onTinymceInput"
|
||||
:value="value"
|
||||
:chanFerForm="chanFerForm"
|
||||
:bodyCiteIdOrder="bodyCiteIdOrder"
|
||||
:tableLinkCiteMaxMap="tableLinkCiteMaxMap"
|
||||
:typesettingType="typesettingType"
|
||||
class="paste-area text-container"
|
||||
:toolbar="!isAutomaticUpdate?['bold italic |customBlue removeBlue|LateX| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace']:['bold italic |customBlue removeBlue| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace']"
|
||||
:toolbar="toolbarConfig"
|
||||
style="
|
||||
/* white-space: pre-line; */
|
||||
line-height: 12px;
|
||||
@@ -36,10 +42,40 @@
|
||||
<script>
|
||||
import Tinymce from '@/components/page/components/Tinymce';
|
||||
export default {
|
||||
props: ['value','isAutomaticUpdate','height','id'],
|
||||
props: {
|
||||
value: {},
|
||||
isAutomaticUpdate: {},
|
||||
height: {},
|
||||
id: {},
|
||||
chanFerForm: {},
|
||||
/** 全文 mycite 顺序,与稿面 word.vue 的 bodyCiteIdOrder 一致,用于弹窗内 [1][2] 与参考文献重排 */
|
||||
bodyCiteIdOrder: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/** mytable(data-id) 对应表格内已出现引用的最大全局序号,用于正文中 “See Table X. And see [2,3]” 的偏移映射 */
|
||||
tableLinkCiteMaxMap: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
/** false:标题等场景不显示 Ref */
|
||||
showRefButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Tinymce
|
||||
},
|
||||
computed: {
|
||||
toolbarConfig() {
|
||||
const refBtn = this.showRefButton ? ' insertRef' : '';
|
||||
if (!this.isAutomaticUpdate) {
|
||||
return [`bold italic |customBlue removeBlue|LateX${refBtn}| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace`];
|
||||
}
|
||||
return [`bold italic |customBlue removeBlue${refBtn}| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace`];
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
lineStyle() {}
|
||||
},
|
||||
@@ -85,8 +121,40 @@ export default {
|
||||
this.$refs.tinymceChild1.getContent(type);
|
||||
},
|
||||
getContent(type, content) {
|
||||
|
||||
this.$emit('getContent', type, content);
|
||||
},
|
||||
openRefSelector(data) {
|
||||
this.$emit('openRefSelector', data);
|
||||
},
|
||||
onTinymceInput(html) {
|
||||
this.$emit('editorInput', html);
|
||||
},
|
||||
insertAutocite(refId) {
|
||||
this.$refs.tinymceChild1.insertAutocite(refId);
|
||||
},
|
||||
removeAutocite() {
|
||||
this.$refs.tinymceChild1.removeAutocite();
|
||||
},
|
||||
/** 从当前 mycite 的 data-id 中移除指定 id;无剩余则删标签;未选 id 时删整段 */
|
||||
stripAutociteIds(ids) {
|
||||
if (this.$refs.tinymceChild1 && typeof this.$refs.tinymceChild1.stripAutociteIds === 'function') {
|
||||
this.$refs.tinymceChild1.stripAutociteIds(ids);
|
||||
}
|
||||
},
|
||||
/** 参考文献异步到达后刷新编辑器内 mycite 数字(供父组件在打开弹窗 / fetch 完成后调用) */
|
||||
refreshAutociteDisplay() {
|
||||
const t = this.$refs.tinymceChild1;
|
||||
if (t && typeof t.renderAutociteInEditor === 'function' && t.editorInstance) {
|
||||
t.renderAutociteInEditor(t.editorInstance);
|
||||
}
|
||||
},
|
||||
/** 将正文中的 [1]、[1–3] 等按全局序号匹配为 <mycite> */
|
||||
convertBracketRefsToAutocite() {
|
||||
const t = this.$refs.tinymceChild1;
|
||||
if (t && typeof t.convertPlainBracketCitesToAutocite === 'function') {
|
||||
return t.convertPlainBracketCitesToAutocite();
|
||||
}
|
||||
return { replaced: 0 };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,11 +2,17 @@
|
||||
<div>
|
||||
<tinymce
|
||||
type="table"
|
||||
:use-table-local-citation-index="true"
|
||||
:articleId="articleId"
|
||||
ref="tinymceChild1"
|
||||
:wordStyle="wordStyle"
|
||||
:show-ref-button="showRefButton"
|
||||
:chanFerForm="chanFerForm"
|
||||
:bodyCiteIdOrder="bodyCiteIdOrder"
|
||||
@getContent="getContent"
|
||||
@openLatexEditor="openLatexEditor"
|
||||
@openRefSelector="$emit('openRefSelector', $event)"
|
||||
@input="onTableEditorInput"
|
||||
:height="calcDynamicWidth()"
|
||||
:value="updatedHtml"
|
||||
:typesettingType="typesettingType"
|
||||
@@ -22,7 +28,24 @@
|
||||
<script>
|
||||
import Tinymce from '@/components/page/components/Tinymce';
|
||||
export default {
|
||||
props: ['lineStyle', 'articleId'],
|
||||
props: {
|
||||
lineStyle: {},
|
||||
articleId: {},
|
||||
/** 与稿面/段落编辑一致,供 Ref 插入;未传时 TinyMCE 内为空会提示「参考文献尚未加载」 */
|
||||
chanFerForm: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
bodyCiteIdOrder: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/** 与 content.vue 一致:为 true 时显示 Reference、Auto-link References(稿面表格编辑需文献角标时保持 true) */
|
||||
showRefButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Tinymce
|
||||
},
|
||||
@@ -91,6 +114,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onTableEditorInput(html) {
|
||||
this.$emit('editorInput', html);
|
||||
},
|
||||
openLatexEditor(data) {
|
||||
this.$emit('openLatexEditor', data);
|
||||
},
|
||||
@@ -121,6 +147,22 @@ export default {
|
||||
} else {
|
||||
this.$emit('getContent', type, { html_data: '', table: [] });
|
||||
}
|
||||
},
|
||||
insertAutocite(refIds) {
|
||||
if (this.$refs.tinymceChild1 && typeof this.$refs.tinymceChild1.insertAutocite === 'function') {
|
||||
this.$refs.tinymceChild1.insertAutocite(refIds);
|
||||
}
|
||||
},
|
||||
stripAutociteIds(ids) {
|
||||
if (this.$refs.tinymceChild1 && typeof this.$refs.tinymceChild1.stripAutociteIds === 'function') {
|
||||
this.$refs.tinymceChild1.stripAutociteIds(ids);
|
||||
}
|
||||
},
|
||||
refreshAutociteDisplay() {
|
||||
const t = this.$refs.tinymceChild1;
|
||||
if (t && typeof t.renderAutociteInEditor === 'function' && t.editorInstance) {
|
||||
t.renderAutociteInEditor(t.editorInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -252,10 +252,11 @@
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<DynamicTable
|
||||
ref="myTableModal"
|
||||
|
||||
/>
|
||||
<DynamicTable
|
||||
ref="myTableModal"
|
||||
:reference-list="chanFerForm || []"
|
||||
:body-cite-id-order="bodyCiteIdOrder || []"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -264,7 +265,18 @@
|
||||
import { mediaUrl } from '@/common/js/commonJS.js'; // 引入通用逻辑
|
||||
import DynamicTable from './DynamicTable.vue';
|
||||
export default {
|
||||
props: ['articleId', 'imgWidth', 'imgHeight', 'scale', 'isEdit', 'isShowEdit', 'urlList', 'content'],
|
||||
props: [
|
||||
'articleId',
|
||||
'imgWidth',
|
||||
'imgHeight',
|
||||
'scale',
|
||||
'isEdit',
|
||||
'isShowEdit',
|
||||
'urlList',
|
||||
'content',
|
||||
'chanFerForm',
|
||||
'bodyCiteIdOrder'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
identity: localStorage.getItem('U_role'),
|
||||
|
||||
@@ -464,10 +464,11 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<DynamicTable
|
||||
ref="myTableModal"
|
||||
|
||||
/>
|
||||
<DynamicTable
|
||||
ref="myTableModal"
|
||||
:reference-list="chanFerForm || []"
|
||||
:body-cite-id-order="bodyCiteIdOrder || []"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -476,7 +477,20 @@ import DynamicTable from './DynamicTable.vue';
|
||||
import { mediaUrl } from '@/common/js/commonJS.js'; // 引入通用逻辑
|
||||
|
||||
export default {
|
||||
props: ['articleId', 'imgWidth', 'imgHeight', 'scale', 'isEdit', 'isShowEdit', 'urlList', 'content'],
|
||||
props: [
|
||||
'articleId',
|
||||
'imgWidth',
|
||||
'imgHeight',
|
||||
'scale',
|
||||
'isEdit',
|
||||
'isShowEdit',
|
||||
'urlList',
|
||||
'content',
|
||||
/** 参考文献列表,供表格预览把 mycite 显示为 [n] */
|
||||
'chanFerForm',
|
||||
/** 正文引用顺序,与稿面 articleCiteIdOrder 一致 */
|
||||
'bodyCiteIdOrder'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
isShowComment: false,
|
||||
|
||||
@@ -239,18 +239,47 @@ export default {
|
||||
openAddDialog() { this.addDialogVisible = true; },
|
||||
resetAddForm() { this.addForm = { field: '', runNow: false }; this.addLoading = false; },
|
||||
async submitAddKeyword() {
|
||||
if (!this.addForm.field.trim()) return this.$message.warning(this.$t('crawlTask.enterKeyword'));
|
||||
const raw = String(this.addForm.field || '').trim();
|
||||
if (!raw) return this.$message.warning(this.$t('crawlTask.enterKeyword'));
|
||||
// 支持一次录入多个:按换行/逗号/分号拆分,去重
|
||||
const fields = raw
|
||||
.split(/[\r\n,,;;]+/g)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const uniqFields = Array.from(new Set(fields));
|
||||
if (uniqFields.length === 0) return this.$message.warning(this.$t('crawlTask.enterKeyword'));
|
||||
this.addLoading = true;
|
||||
try {
|
||||
const res = await this.$api.post('api/expert_manage/addFetchField', { field: this.addForm.field });
|
||||
if (res.code === 0) {
|
||||
this.$message.success(this.$t('crawlTask.addKeywordSuccess'));
|
||||
if (this.addForm.runNow) {
|
||||
await this.$api.post('api/expert_finder/fetchOneField', { field: this.addForm.field });
|
||||
const ok = [];
|
||||
const fail = [];
|
||||
for (const field of uniqFields) {
|
||||
try {
|
||||
const res = await this.$api.post('api/expert_manage/addFetchField', { field });
|
||||
if (res && res.code === 0) {
|
||||
ok.push(field);
|
||||
if (this.addForm.runNow) {
|
||||
// 不阻塞整体添加:run once 失败也记录,不影响已添加成功
|
||||
try {
|
||||
await this.$api.post('api/expert_finder/fetchOneField', { field });
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fail.push({ field, msg: (res && res.msg) || this.$t('crawlTask.operationFail') });
|
||||
}
|
||||
} catch (e) {
|
||||
fail.push({ field, msg: this.$t('crawlTask.operationFail') });
|
||||
}
|
||||
this.addDialogVisible = false;
|
||||
this.fetchList();
|
||||
}
|
||||
if (ok.length > 0) {
|
||||
this.$message.success(`${this.$t('crawlTask.addKeywordSuccess')} (${ok.length}/${uniqFields.length})`);
|
||||
}
|
||||
if (fail.length > 0) {
|
||||
this.$message.warning(`Failed: ${fail.map((x) => x.field).join(', ')}`);
|
||||
}
|
||||
this.addDialogVisible = false;
|
||||
this.fetchList();
|
||||
} finally { this.addLoading = false; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
Special note: If the number of authors is 6 or fewer, all authors should be listed.
|
||||
</div>
|
||||
<el-button
|
||||
v-if="canDelete"
|
||||
@click="StepBNext(1)"
|
||||
type="primary"
|
||||
plain
|
||||
@@ -110,7 +111,7 @@
|
||||
|
||||
<span @click="handleContainerClick" style="margin-left: 5px" v-html="getRepeatRefHtml()"> </span>
|
||||
</div>
|
||||
<div class="topBtnBox btns" v-if="chanFerForm.length > 0 && role == 'editor'">
|
||||
<div class="topBtnBox btns" v-if="chanFerForm.length > 0 && role == 'editor' && canDelete">
|
||||
<el-button type="primary" plain @click="selectAllRef">Select all</el-button>
|
||||
<el-button type="success" plain @click="toggleSelection">Select none</el-button>
|
||||
<el-button type="danger" plain @click="deleteSomeRefs" :disabled="multipleSelection.length > 0 ? false : true"
|
||||
@@ -154,11 +155,10 @@
|
||||
class="status ok"
|
||||
:class="scope.row.refer_type == 'journal' ? getJournalDateno(scope.row.dateno, 'status') : ''"
|
||||
v-if="
|
||||
(
|
||||
(scope.row.refer_type == 'journal' && scope.row.doilink != '' && scope.row.cs == 1) ||
|
||||
(scope.row.refer_type == 'book' && scope.row.isbn != '' && scope.row.cs == 1)
|
||||
) && scope.row.retract == 0
|
||||
"
|
||||
((scope.row.refer_type == 'journal' && scope.row.doilink != '' && scope.row.cs == 1) ||
|
||||
(scope.row.refer_type == 'book' && scope.row.isbn != '' && scope.row.cs == 1)) &&
|
||||
scope.row.retract == 0
|
||||
"
|
||||
>
|
||||
<i class="el-icon-circle-check"></i>
|
||||
</span>
|
||||
@@ -173,7 +173,9 @@
|
||||
<!-- journal 形式 -->
|
||||
<div style="text-align: left" v-if="scope.row.refer_type == 'journal'" class="reference-item">
|
||||
<p>
|
||||
{{ scope.row.author }} <span v-html="formatTitle(scope.row.title)"></span>. <em>{{ scope.row.joura }}</em
|
||||
{{ scope.row.author }} <span v-html="formatTitle(scope.row.title)"></span>. <em>{{
|
||||
scope.row.joura
|
||||
}}</em
|
||||
>. <span :class="getJournalDateno(scope.row.dateno, 'title')">{{ scope.row.dateno }}</span
|
||||
>.<br />
|
||||
</p>
|
||||
@@ -181,13 +183,16 @@
|
||||
</div>
|
||||
<!-- book 形式 -->
|
||||
<div style="text-align: left" v-if="scope.row.refer_type == 'book'" class="reference-item">
|
||||
<p>{{ scope.row.author }} <span v-html="formatTitle(scope.row.title)"></span>. {{ scope.row.dateno }}. <br /></p>
|
||||
<p>
|
||||
{{ scope.row.author }} <span v-html="formatTitle(scope.row.title)"></span>. {{
|
||||
scope.row.dateno
|
||||
}}. <br />
|
||||
</p>
|
||||
<a class="doiLink" :href="scope.row.isbn" target="_blank">{{ scope.row.isbn }}</a>
|
||||
</div>
|
||||
<!-- other 形式 -->
|
||||
<p class="wrongLine reference-item" style="text-align: left" v-if="scope.row.refer_type == 'other'">
|
||||
<span v-html="formatTitle(scope.row.refer_frag)"></span>
|
||||
|
||||
</p>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -242,7 +247,7 @@
|
||||
</div>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="bottomBtnBox btns" v-if="chanFerForm.length > 0 && role == 'editor'">
|
||||
<div class="bottomBtnBox btns" v-if="chanFerForm.length > 0 && role == 'editor'&& canDelete">
|
||||
<el-button type="primary" plain @click="selectAllRef">Select all</el-button>
|
||||
<el-button type="success" plain @click="toggleSelection">Select none</el-button>
|
||||
<el-button type="danger" plain @click="deleteSomeRefs" :disabled="multipleSelection.length > 0 ? false : true"
|
||||
@@ -546,6 +551,7 @@ export default {
|
||||
},
|
||||
addLoading: false,
|
||||
editboxVisible: false,
|
||||
canDelete: true,
|
||||
multipleSelection: [] // 多选
|
||||
};
|
||||
},
|
||||
@@ -572,18 +578,19 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
formatTitle(title) {
|
||||
if (!title) return '';
|
||||
// 使用正则匹配,'gi' 表示全局匹配且不区分大小写
|
||||
// \b 确保是完整单词匹配,防止误伤含有这些字母的其他单词
|
||||
const reg = /\b(Retracted|Retraction)\b/gi;
|
||||
|
||||
return title.replace(reg, (match) => {
|
||||
return `<span style="color: red; font-weight: bold;">${match}</span>`;
|
||||
});
|
||||
}
|
||||
,
|
||||
getCurrentRoute() {
|
||||
this.canDelete = !['/articleListEditor_B1'].includes(this.$route.path);
|
||||
},
|
||||
formatTitle(title) {
|
||||
if (!title) return '';
|
||||
// 使用正则匹配,'gi' 表示全局匹配且不区分大小写
|
||||
// \b 确保是完整单词匹配,防止误伤含有这些字母的其他单词
|
||||
const reg = /\b(Retracted|Retraction)\b/gi;
|
||||
|
||||
return title.replace(reg, (match) => {
|
||||
return `<span style="color: red; font-weight: bold;">${match}</span>`;
|
||||
});
|
||||
},
|
||||
getJournalDateno(dateno, type) {
|
||||
if (dateno && typeof dateno === 'string') {
|
||||
const hasInvalidColon = !dateno.includes(':') || (dateno.includes(':') && dateno.split(':').pop().trim() === '');
|
||||
@@ -682,7 +689,7 @@ export default {
|
||||
.then((res) => {
|
||||
if (res.status == 1) {
|
||||
return res.data;
|
||||
}
|
||||
}
|
||||
throw res.msg;
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -697,6 +704,7 @@ export default {
|
||||
return {};
|
||||
},
|
||||
init(e) {
|
||||
this.getCurrentRoute();
|
||||
this.chanFerForm = e;
|
||||
this.bijiao();
|
||||
////console.log('更新更新')
|
||||
|
||||
2126
src/components/page/editPublicRefTableOnly.vue
Normal file
2126
src/components/page/editPublicRefTableOnly.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -279,11 +279,14 @@
|
||||
journal_id: this.query.journal_id || (this.journalList[0] ? this.journalList[0].journal_id : null),
|
||||
account: '',
|
||||
password: '',
|
||||
// password: '123456qwe..%%%',
|
||||
smtp_from_name: '',
|
||||
smtp_host: 'mail.tmrjournals.co.nz',
|
||||
// smtp_host: 'smtp.mxhichina.com',
|
||||
smtp_port: '465',
|
||||
smtp_encryption: 'ssl',
|
||||
imap_host: 'mail.tmrjournals.co.nz',
|
||||
// imap_host: 'imap.qiye.aliyun.com',
|
||||
imap_port: '993',
|
||||
};
|
||||
this.dialogVisible = true;
|
||||
|
||||
@@ -96,13 +96,13 @@
|
||||
|
||||
<div class="body-editor-container">
|
||||
<div class="subject-label" style="margin-bottom: 10px;">{{ $t('mailboxMouldDetail.emailBody') }}:</div>
|
||||
<TmrEmailEditor
|
||||
<!-- <TmrEmailEditor
|
||||
v-model="form.body"
|
||||
:journalList="journalList"
|
||||
:journalId="form.journalId"
|
||||
:language="form.lang"
|
||||
placeholder=""
|
||||
/>
|
||||
/> -->
|
||||
<CkeditorMail v-model="form.body" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
@@ -124,7 +124,7 @@ export default {
|
||||
},
|
||||
// 5----重新获取加载参考文献
|
||||
changeRefer(val) {
|
||||
console.log('重新获取参考文献')
|
||||
|
||||
this.$api
|
||||
.post('api/Production/getReferList', {
|
||||
'p_article_id': this.p_article_id
|
||||
|
||||
22
src/utils/autociteHtml.js
Normal file
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;
|
||||
}
|
||||
161
src/utils/autociteMainListOrder.js
Normal file
161
src/utils/autociteMainListOrder.js
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Main_List 全文首次出现顺序(与 Edit Content / 稿面 / 底部参考文献列表共用)。
|
||||
* 正文中 <mytable> 占位处需先插入对应表格段在 Main_List 中的表题/表体/表注引用,再计其后的 mycite。
|
||||
*/
|
||||
import { extractAutociteIdsFromHtmlString } from '@/utils/autociteHtml.js';
|
||||
|
||||
export function collectCiteStringsDeep(source, out, depth = 0) {
|
||||
if (!source || depth > 6) return;
|
||||
if (typeof source === 'string') {
|
||||
const s = source.trim();
|
||||
if (!s) return;
|
||||
const maybeCite = /<(?:mycite|autocite)\b/i.test(s) || /\[[\d\s,,\-–—]+\]/.test(s);
|
||||
if (maybeCite) out.push(s);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(source)) {
|
||||
source.forEach((item) => collectCiteStringsDeep(item, out, depth + 1));
|
||||
return;
|
||||
}
|
||||
if (typeof source === 'object') {
|
||||
Object.keys(source).forEach((k) => {
|
||||
const v = source[k];
|
||||
collectCiteStringsDeep(v, out, depth + 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function collectCiteHtmlSourcesForMainListItem(p, draftAmId, draftHtml) {
|
||||
if (draftAmId != null && p && p.am_id == draftAmId && draftHtml != null) {
|
||||
return [draftHtml];
|
||||
}
|
||||
const out = [];
|
||||
const typ = p != null && p.type != null ? Number(p.type) : NaN;
|
||||
if (typ === 2 && p.table) {
|
||||
const t = p.table;
|
||||
if (typeof t.title === 'string' && t.title.trim()) out.push(t.title);
|
||||
if (typeof t.html_data === 'string' && t.html_data.trim()) {
|
||||
out.push(t.html_data);
|
||||
} else {
|
||||
const pushRows = (rows) => {
|
||||
if (!Array.isArray(rows)) return;
|
||||
rows.forEach((row) => {
|
||||
if (!Array.isArray(row)) return;
|
||||
row.forEach((cell) => {
|
||||
if (cell && typeof cell.text === 'string' && cell.text) out.push(cell.text);
|
||||
});
|
||||
});
|
||||
};
|
||||
pushRows(t.tableHeader);
|
||||
pushRows(t.tableContent);
|
||||
}
|
||||
if (typeof t.note === 'string' && t.note.trim()) out.push(t.note);
|
||||
collectCiteStringsDeep(t, out);
|
||||
return out;
|
||||
}
|
||||
if (typ === 1 && p.image) {
|
||||
const img = p.image;
|
||||
if (typeof img.title === 'string' && img.title.trim()) out.push(img.title);
|
||||
if (typeof img.note === 'string' && img.note.trim()) out.push(img.note);
|
||||
if (out.length === 0) {
|
||||
if (p && typeof p.content === 'string' && p.content.trim()) out.push(p.content);
|
||||
if (p && typeof p.text === 'string' && p.text.trim()) out.push(p.text);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (p && typeof p.text === 'string' && p.text.trim()) out.push(p.text);
|
||||
if (p && typeof p.content === 'string' && p.content.trim()) out.push(p.content);
|
||||
return out;
|
||||
}
|
||||
|
||||
export function findMainListTableByLinkId(mainList, rawId) {
|
||||
if (rawId == null || String(rawId).trim() === '') return null;
|
||||
const sid = String(rawId).trim();
|
||||
const list = Array.isArray(mainList) ? mainList : [];
|
||||
return (
|
||||
list.find((p) => {
|
||||
if (!p || Number(p.type) !== 2) return false;
|
||||
if (String(p.amt_id) === sid) return true;
|
||||
if (String(p.am_id) === sid) return true;
|
||||
if (p.p_main_table_id != null && String(p.p_main_table_id) === sid) return true;
|
||||
if (p.table && p.table.amt_id != null && String(p.table.amt_id) === sid) return true;
|
||||
if (p.table && p.table.am_id != null && String(p.table.am_id) === sid) return true;
|
||||
if (p.table && p.table.p_main_table_id != null && String(p.table.p_main_table_id) === sid)
|
||||
return true;
|
||||
if (p.table && p.table.table_id != null && String(p.table.table_id) === sid) return true;
|
||||
return false;
|
||||
}) || null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} [options]
|
||||
* @param {number|string|null} [options.tableDraftAmId] 正在编辑的表格段 am_id(Edit Table 抽屉)
|
||||
* @param {string|null} [options.tableDraftHtml] 与该表对应的 Title+表体+Note 合并稿;正文里 mytable 展开到该表时用此稿替代 Main_List 已存内容,使「全文 mytable 之前」的序号与抽屉内一致
|
||||
*/
|
||||
export function extractAutociteOrderFromDraftHtmlWithInlineTables(html, mainList, options = {}) {
|
||||
const order = [];
|
||||
const pushUnique = (id) => {
|
||||
if (id && !order.includes(id)) order.push(id);
|
||||
};
|
||||
const pushFromRaw = (raw) => {
|
||||
extractAutociteIdsFromHtmlString(raw).forEach((id) => pushUnique(id));
|
||||
};
|
||||
if (!html || typeof html !== 'string') return order;
|
||||
|
||||
const re = /<mytable\b[^>]*>[\s\S]*?<\/mytable>/gi;
|
||||
let last = 0;
|
||||
let m;
|
||||
while ((m = re.exec(html)) !== null) {
|
||||
const before = html.slice(last, m.index);
|
||||
if (before) pushFromRaw(before);
|
||||
|
||||
const openEnd = html.indexOf('>', m.index) + 1;
|
||||
const openTag = html.slice(m.index, openEnd);
|
||||
const dm = openTag.match(/\bdata-id\s*=\s*["']([^"']*)["']/i);
|
||||
const tid = dm ? String(dm[1]).trim() : '';
|
||||
const tableP = findMainListTableByLinkId(mainList, tid);
|
||||
if (tableP) {
|
||||
const useDraft =
|
||||
options.tableDraftAmId != null &&
|
||||
options.tableDraftHtml != null &&
|
||||
String(tableP.am_id) === String(options.tableDraftAmId);
|
||||
const sources = useDraft
|
||||
? [options.tableDraftHtml]
|
||||
: collectCiteHtmlSourcesForMainListItem(tableP, null, null);
|
||||
sources.forEach((raw) => pushFromRaw(raw));
|
||||
}
|
||||
|
||||
last = m.index + m[0].length;
|
||||
}
|
||||
const after = html.slice(last);
|
||||
if (after) pushFromRaw(after);
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
export function extractAutociteOrderFromMainList(mainList, draftAmId, draftHtml, options = {}) {
|
||||
const list = Array.isArray(mainList) ? mainList : [];
|
||||
const order = [];
|
||||
list.forEach((p) => {
|
||||
if (draftAmId != null && p && p.am_id == draftAmId && draftHtml != null) {
|
||||
extractAutociteOrderFromDraftHtmlWithInlineTables(draftHtml, list, options).forEach((id) => {
|
||||
if (!order.includes(id)) order.push(id);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const candidates = collectCiteHtmlSourcesForMainListItem(p, draftAmId, draftHtml);
|
||||
candidates.forEach((raw) => {
|
||||
if (typeof raw === 'string' && /<mytable\b/i.test(raw)) {
|
||||
extractAutociteOrderFromDraftHtmlWithInlineTables(raw, list, options).forEach((id) => {
|
||||
if (!order.includes(id)) order.push(id);
|
||||
});
|
||||
} else {
|
||||
extractAutociteIdsFromHtmlString(raw).forEach((id) => {
|
||||
if (!order.includes(id)) order.push(id);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return order;
|
||||
}
|
||||
Reference in New Issue
Block a user