参考文献的暂存

This commit is contained in:
2026-04-09 09:39:58 +08:00
parent c4b86be0d5
commit 7527a6ef54
4 changed files with 188 additions and 35 deletions

View File

@@ -412,6 +412,7 @@
@openRefSelector="handleOpenRefSelector"
:chanFerForm="chanFerForm"
:body-cite-id-order="editModalBodyCiteOrder"
:table-link-cite-max-map="tableLinkCiteMaxMap"
@editorInput="onEditModalEditorInput"
v-if="editVisible"
ref="commonContent"
@@ -1032,6 +1033,49 @@ export default {
const order = this.extractAutociteOrderFromMainList(this.Main_List, this.currentContent.am_id, draft);
return order.length > 0 ? order : this.articleCiteIdOrder;
},
/**
* mytable(data-id) -> 该表格中已出现引用的最大全局序号
* 用于正文链接表格后,后续 [2,3] 按“表内最大序号之后”继续映射(如 33,34
*/
tableLinkCiteMaxMap() {
const out = {};
const list = Array.isArray(this.Main_List) ? this.Main_List : [];
if (!list.length) return out;
const order = Array.isArray(this.editModalBodyCiteOrder) ? this.editModalBodyCiteOrder : this.articleCiteIdOrder;
const citeMap = this.buildDisplayCiteMap(order || []);
list.forEach((p) => {
if (!p || Number(p.type) !== 2) return;
const candidates = this.collectCiteHtmlSourcesForMainListItem(p);
const ids = [];
candidates.forEach((raw) => {
extractAutociteIdsFromHtmlString(raw).forEach((id) => {
if (!ids.includes(id)) ids.push(id);
});
});
const numsFromIds = ids
.map((id) => Number(citeMap[String(id)]))
.filter((n) => !Number.isNaN(n) && n > 0);
// 兜底:有些表格仍是纯文本 [34](未转 mycite这里直接取括号数字最大值参与 max
const numsFromPlain = [];
candidates.forEach((raw) => {
this.extractBracketCiteNumbersFromText(raw).forEach((n) => numsFromPlain.push(n));
});
const nums = [...numsFromIds, ...numsFromPlain];
const max = nums.length ? Math.max(...nums) : 0;
if (max <= 0) return;
if (p.am_id != null) out[String(p.am_id)] = max;
if (p.amt_id != null) out[String(p.amt_id)] = max;
if (p.p_main_table_id != null) out[String(p.p_main_table_id)] = max;
if (p.table) {
if (p.table.amt_id != null) out[String(p.table.amt_id)] = max;
if (p.table.am_id != null) out[String(p.table.am_id)] = max;
if (p.table.p_main_table_id != null) out[String(p.table.p_main_table_id)] = max;
if (p.table.table_id != null) out[String(p.table.table_id)] = max;
}
});
console.log('[tableLinkCiteMaxMap]', out);
return out;
},
/** 表格抽屉内角标:与 Edit Content 同源,按 Main_List + Title/表/Note 合并稿做全文首次出现排序,避免仍用 articleCiteIdOrder 列表下标 [38] */
tableModalBodyCiteOrder() {
if (!this.threeVisible || !this.lineStyle || this.lineStyle.am_id == null) {
@@ -1728,6 +1772,8 @@ export default {
pushRows(t.tableContent);
}
if (typeof t.note === 'string' && t.note.trim()) out.push(t.note);
// 兜底:不同接口版本的表体字段名不一致,深度扫描 table 对象里所有可能含引用的字符串字段。
this.collectCiteStringsDeep(t, out);
return out;
}
if (typ === 1 && p.image) {
@@ -1744,6 +1790,58 @@ export default {
if (p && typeof p.content === 'string' && p.content.trim()) out.push(p.content);
return out;
},
/**
* 递归收集对象中的字符串字段,覆盖 html/text/content 等异构字段名。
* 仅保留看起来可能包含引用信息的片段,避免噪音过大。
*/
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) => this.collectCiteStringsDeep(item, out, depth + 1));
return;
}
if (typeof source === 'object') {
Object.keys(source).forEach((k) => {
const v = source[k];
this.collectCiteStringsDeep(v, out, depth + 1);
});
}
},
extractBracketCiteNumbersFromText(raw) {
const out = [];
if (!raw || typeof raw !== 'string') return out;
const text = raw.replace(/<[^>]+>/g, ' ');
const re = /\[([\d\s,\-–—]+)\]/g;
let m;
while ((m = re.exec(text)) !== null) {
const inner = String(m[1] || '').trim().replace(//g, ',');
if (!inner) continue;
const range = inner.match(/^(\d+)\s*[-–—]\s*(\d+)$/);
if (range) {
const a = Number(range[1]);
const b = Number(range[2]);
if (!Number.isNaN(a) && !Number.isNaN(b)) {
const lo = Math.min(a, b);
const hi = Math.max(a, b);
for (let i = lo; i <= hi; i++) out.push(i);
}
continue;
}
inner
.split(',')
.map((x) => parseInt(String(x).trim(), 10))
.filter((n) => !Number.isNaN(n) && n > 0)
.forEach((n) => out.push(n));
}
return out;
},
/** 与 word.vue 一致:从各段 HTML 中按出现顺序收集 autocite含表格单元格、表题、表注、图题图注 */
extractAutociteOrderFromMainList(mainList, draftAmId, draftHtml) {
const list = Array.isArray(mainList) ? mainList : [];
@@ -2172,12 +2270,21 @@ export default {
this.saveContent(newContent, mainId);
},
handleConfirmLink(selectedMedia) {
const targetId = selectedMedia.select.amt_id || selectedMedia.select.ami_id;
const select = (selectedMedia && selectedMedia.select) || {};
// 不同列表来源字段不一致:表可能是 amt_id / am_id图可能是 ami_id / am_id
const targetId =
select.amt_id ||
select.ami_id ||
select.am_id ||
select.p_main_table_id ||
select.table_id ||
(select.table &&
(select.table.amt_id || select.table.am_id || select.table.p_main_table_id || select.table.table_id));
const type = selectedMedia.type;
const tagName = `my${type}`;
let originalContent = selectedMedia.linkData.content;
const label = selectedMedia.linkData.label;
if (!label || !originalContent) return;
if (!label || !originalContent || targetId == null || targetId === '') return;
const isAlreadyTagged = label.startsWith('<my') && label.endsWith('>');
let newContent = '';
if (isAlreadyTagged) {

View File

@@ -84,6 +84,11 @@ export default {
type: Array,
default: () => []
},
/** mytable(data-id) -> 该表内已出现引用的最大全局序号(由父组件计算) */
tableLinkCiteMaxMap: {
type: Object,
default: () => ({})
},
/** 为 false 时不显示 Ref 工具栏按钮,也不自动在 LateX 后注入 insertRef用于图表标题等 */
showRefButton: {
type: Boolean,
@@ -357,7 +362,7 @@ export default {
if (!body) return { replaced: 0 };
let replaced = 0;
ed.undoManager.transact(() => {
replaced = this._replaceBracketCitesInNode(body, doc, numToId);
replaced = this._replaceBracketCitesInDocOrder(body, doc, numToId, { tableOffset: 0 });
});
this.renderAutociteInEditor(ed);
ed.fire('change');
@@ -409,23 +414,41 @@ export default {
/* ignore */
}
},
_replaceBracketCitesInNode(node, doc, numToId) {
/**
* 按文档顺序遍历,使「表格链接」后任意后续段落里的 [n] 都能用上该表的最大全局序号偏移。
* 旧实现只在同一父节点下、MYTABLE 与后续文本为兄弟时才生效MYTABLE 在上一段、角标在下一段时会失效。
*/
_replaceBracketCitesInDocOrder(node, doc, numToId, state) {
if (node.nodeType === 3) {
return this._processTextNodeForBracketCites(node, doc, numToId);
return this._processTextNodeForBracketCites(node, doc, numToId, state.tableOffset);
}
if (node.nodeType !== 1) return 0;
const name = node.nodeName;
if (name === 'AUTOCITE' || name === 'WMATH' || name === 'SCRIPT' || name === 'STYLE') {
if (name === 'AUTOCITE' || name === 'MYCITE' || 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);
const isMytable = name === 'MYTABLE' || (node.tagName && String(node.tagName).toLowerCase() === 'mytable');
if (isMytable) {
let total = 0;
Array.from(node.childNodes).forEach((c) => {
total += this._replaceBracketCitesInDocOrder(c, doc, numToId, state);
});
const tid = (node.getAttribute && node.getAttribute('data-id')) || '';
const map = this.tableLinkCiteMaxMap || {};
const tableMax = Number(map[String(tid)]);
console.log('[autolink/mytable]', { tid, tableMax, map });
if (!Number.isNaN(tableMax) && tableMax > 0) {
state.tableOffset = tableMax;
}
return total;
}
let total = 0;
Array.from(node.childNodes).forEach((c) => {
total += this._replaceBracketCitesInDocOrder(c, doc, numToId, state);
});
return total;
},
_processTextNodeForBracketCites(textNode, doc, numToId) {
_processTextNodeForBracketCites(textNode, doc, numToId, tableOffset = 0) {
const text = textNode.textContent;
const re = /\[([\d\s,\-–—]+)\]/g;
let m;
@@ -446,7 +469,16 @@ export default {
const ids = [];
const seen = new Set();
nums.forEach((n) => {
const id = numToId[n];
// 表格链接后的局部序号从 tableMax+1 开始接续1->max+1, 2->max+2 ...
const mappedNo = n > 0 && tableOffset > 0 ? n + tableOffset - 1 : n;
const id = numToId[mappedNo];
console.log('[autolink/map]', {
rawBracket: m[0],
n,
tableOffset,
mappedNo,
id
});
if (id && !seen.has(id)) {
seen.add(id);
ids.push(id);

View File

@@ -19,6 +19,7 @@
:value="value"
:chanFerForm="chanFerForm"
:bodyCiteIdOrder="bodyCiteIdOrder"
:tableLinkCiteMaxMap="tableLinkCiteMaxMap"
:typesettingType="typesettingType"
class="paste-area text-container"
:toolbar="toolbarConfig"
@@ -52,6 +53,11 @@ export default {
type: Array,
default: () => []
},
/** mytable(data-id) 对应表格内已出现引用的最大全局序号,用于正文中 “See Table X. And see [2,3]” 的偏移映射 */
tableLinkCiteMaxMap: {
type: Object,
default: () => ({})
},
/** false标题等场景不显示 Ref */
showRefButton: {
type: Boolean,

View File

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