tijiao
This commit is contained in:
@@ -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地址]
|
||||
|
||||
@@ -1164,6 +1164,7 @@ colTitle: 'Template title',
|
||||
notFoundById: 'No reference found for citation ID {id}',
|
||||
selectRef: 'Select Reference',
|
||||
reference: 'Reference',
|
||||
originalOrder: 'Original order',
|
||||
uncited: 'Uncited',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
@@ -1178,7 +1179,10 @@ colTitle: 'Template title',
|
||||
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'
|
||||
quickPickClear: 'Clear selection',
|
||||
currentCiteNo: 'Current',
|
||||
locateInBody: 'Click to scroll to citation in text',
|
||||
locateInRefHint: 'Highlight this entry in the list below'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1149,6 +1149,7 @@ const zh = {
|
||||
notFoundById: '未查询到编号{id}相关参考文献',
|
||||
selectRef: '选择参考文献',
|
||||
reference: '参考文献',
|
||||
originalOrder: '原排序',
|
||||
uncited: '未引用',
|
||||
cancel: '取消',
|
||||
confirm: '确认',
|
||||
@@ -1163,7 +1164,10 @@ const zh = {
|
||||
removeRefNeedClickCite: '请先在正文中点击要删除的引用角标,再点「移除参考文献」。',
|
||||
quickPickPlaceholder: '输入如 [5, 6, 10-15] 自动勾选对应参考文献',
|
||||
quickPickApply: '链接参考文献',
|
||||
quickPickClear: '清空勾选'
|
||||
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">
|
||||
|
||||
@@ -595,6 +595,17 @@
|
||||
</p>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('wordCite.originalOrder')" width="120" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{
|
||||
scope.row.old_index != null && scope.row.old_index !== ''
|
||||
? scope.row.old_index+1
|
||||
: scope.row.old_index != null && scope.row.old_index !== ''
|
||||
? scope.row.old_index+1
|
||||
: '—'
|
||||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" :width="'200'">
|
||||
<div slot-scope="scope">
|
||||
<div class="operation" style="">
|
||||
@@ -890,6 +901,8 @@ export default {
|
||||
refSelectorSource: 'commonContent',
|
||||
/** 表格抽屉内合并稿防抖:按全文首次出现顺序更新 articleCiteIdOrder,避免角标停留在列表序号 [38] */
|
||||
_tableReorderTimer: null,
|
||||
/** 最近一次 getReferList 返回的 p_refer_id 顺序;与当前 chanFerForm 不一致时回写 batchUpdateRefer */
|
||||
_lastReferListApiOrder: null,
|
||||
/** 打开选择器时若带 currentRefIds(编辑已有引用),排序用「传值计算的顺序」;关闭后清空 */
|
||||
refSelectorContextIds: [],
|
||||
/** 选择参考文献弹窗:按 # 序号快速勾选,如 [5, 6, 10-15] */
|
||||
@@ -1496,6 +1509,7 @@ export default {
|
||||
p_article_id: this.p_article_id
|
||||
})
|
||||
.then((res) => {
|
||||
this._lastReferListApiOrder = this.referIdOrderSnapshot(res.data && res.data.refers).slice();
|
||||
this.chanFerForm = res.data.refers;
|
||||
this.chanFerFormRepeatList = Object.values(res.data.repeat || {});
|
||||
for (let i = 0; i < this.chanFerForm.length; i++) {
|
||||
@@ -1522,6 +1536,8 @@ export default {
|
||||
}
|
||||
/** getReferList 会整表覆盖 chanFerForm,需再按正文首次出现顺序对齐,否则下方列表与 [n] 脱节 */
|
||||
this.applyRefOrderAfterFetchReferList();
|
||||
/** 正文序与接口序不一致时回写(Main_List 未就绪时可能无变化,getDate 后会再比一次) */
|
||||
this.scheduleTryBatchSyncReferOrderIfOutOfSync();
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -1774,6 +1790,69 @@ export default {
|
||||
if (sig(next) === sig(refs)) return;
|
||||
this.chanFerForm = next;
|
||||
},
|
||||
/**
|
||||
* 将当前 chanFerForm 全文同步到后端(顺序 + 各字段内容与 getReferList 行结构一致)。
|
||||
* 接口:api/References/batchUpdateRefer:p_article_id + list(JSON 字符串,文献对象数组,顺序即保存序)
|
||||
*/
|
||||
syncBatchReferenceOrderToServer() {
|
||||
const pid = this.p_article_id;
|
||||
if (pid == null || pid === '') return Promise.resolve();
|
||||
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
||||
if (refs.length === 0) return Promise.resolve();
|
||||
let listJson;
|
||||
try {
|
||||
listJson = JSON.stringify(JSON.parse(JSON.stringify(refs)));
|
||||
} catch (e) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.$api
|
||||
.post('api/References/batchUpdateRefer', {
|
||||
p_article_id: String(pid),
|
||||
list: listJson
|
||||
})
|
||||
.then((res) => {
|
||||
const ok = res && (res.code === 0 || res.code === 1 || res.status === 1);
|
||||
if (ok) {
|
||||
this._lastReferListApiOrder = this.referIdOrderSnapshot(this.chanFerForm).slice();
|
||||
}
|
||||
if (!ok && res && (res.msg || res.message)) {
|
||||
console.warn('[batchUpdateRefer]', res.msg || res.message);
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn('[batchUpdateRefer] request failed', err);
|
||||
});
|
||||
},
|
||||
/** 从 getReferList 原始 refers 提取 p_refer_id 顺序(字符串,便于比较) */
|
||||
referIdOrderSnapshot(refs) {
|
||||
return (Array.isArray(refs) ? refs : [])
|
||||
.map((r) => (r && r.p_refer_id != null ? String(r.p_refer_id) : ''))
|
||||
.filter(Boolean);
|
||||
},
|
||||
/** 当前 chanFerForm 顺序与 _lastReferListApiOrder 不一致则调用 batchUpdateRefer */
|
||||
tryBatchSyncReferOrderIfOutOfSync() {
|
||||
if (this._lastReferListApiOrder == null) return;
|
||||
const track = this._lastReferListApiOrder.map(String);
|
||||
const cur = this.referIdOrderSnapshot(this.chanFerForm);
|
||||
if (cur.length === 0) return;
|
||||
const same = track.length === cur.length && track.every((id, i) => id === cur[i]);
|
||||
if (!same) {
|
||||
this.syncBatchReferenceOrderToServer();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 等 applyRefOrder / syncRefOrder / 表格抽屉内 nextTick 跑完后再比对顺序。
|
||||
*/
|
||||
scheduleTryBatchSyncReferOrderIfOutOfSync() {
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(() => {
|
||||
this.tryBatchSyncReferOrderIfOutOfSync();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
extractBracketCiteNumbersFromText(raw) {
|
||||
const out = [];
|
||||
if (!raw || typeof raw !== 'string') return out;
|
||||
@@ -2278,6 +2357,9 @@ export default {
|
||||
/** 以接口回写后的 Main_List 再对齐一次;稿面 word 会由 contentList 监听触发 syncRefOrder */
|
||||
this.$nextTick(() => {
|
||||
this.reorderReferencesFromMainListBody(null, null);
|
||||
this.$nextTick(() => {
|
||||
this.syncBatchReferenceOrderToServer();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
loading.close();
|
||||
@@ -3294,6 +3376,8 @@ export default {
|
||||
/** 正文加载后按 Main_List 扫出全文首次出现顺序,写入 articleCiteIdOrder,弹窗角标与稿面一致 */
|
||||
this.$nextTick(() => {
|
||||
this.reorderReferencesFromMainListBody(null, null);
|
||||
/** getReferList 早于 Main_List 时在此才完成正文序重排,与接口序不一致则回写 */
|
||||
this.scheduleTryBatchSyncReferOrderIfOutOfSync();
|
||||
});
|
||||
loading.close();
|
||||
});
|
||||
@@ -3519,6 +3603,9 @@ export default {
|
||||
if (w && typeof w.syncRefOrder === 'function') {
|
||||
w.syncRefOrder();
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.syncBatchReferenceOrderToServer();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
@@ -3571,6 +3658,9 @@ export default {
|
||||
if (w && typeof w.syncRefOrder === 'function') {
|
||||
w.syncRefOrder();
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.syncBatchReferenceOrderToServer();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -218,6 +218,8 @@ export default {
|
||||
refSelectorIsEdit: false,
|
||||
refSelectedRows: [],
|
||||
refSelectorSource: 'commonContent',
|
||||
/** 最近一次 getReferList 返回的 p_refer_id 顺序;与当前 chanFerForm 不一致时回写 batchUpdateRefer */
|
||||
_lastReferListApiOrder: null,
|
||||
/** 打开选择器时若带 currentRefIds(编辑已有引用),排序用「传值计算的顺序」;关闭后清空 */
|
||||
refSelectorContextIds: [],
|
||||
/** 选择参考文献弹窗:按 # 序号快速勾选,如 [5, 6, 10-15] */
|
||||
@@ -448,6 +450,7 @@ export default {
|
||||
p_article_id: this.p_article_id
|
||||
})
|
||||
.then((res) => {
|
||||
this._lastReferListApiOrder = this.referIdOrderSnapshot(res.data && res.data.refers).slice();
|
||||
this.chanFerForm = res.data.refers;
|
||||
this.chanFerFormRepeatList = Object.values(res.data.repeat || {});
|
||||
for (let i = 0; i < this.chanFerForm.length; i++) {
|
||||
@@ -474,6 +477,7 @@ export default {
|
||||
}
|
||||
/** getReferList 会整表覆盖 chanFerForm,需再按正文首次出现顺序对齐,否则下方列表与 [n] 脱节 */
|
||||
this.applyRefOrderAfterFetchReferList();
|
||||
this.scheduleTryBatchSyncReferOrderIfOutOfSync();
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -647,6 +651,64 @@ export default {
|
||||
if (sig(next) === sig(refs)) return;
|
||||
this.chanFerForm = next;
|
||||
},
|
||||
/**
|
||||
* 将当前 chanFerForm 全文同步到后端(顺序 + 各字段)。
|
||||
* 接口:api/References/batchUpdateRefer:p_article_id + list(JSON 字符串,文献对象数组)
|
||||
*/
|
||||
syncBatchReferenceOrderToServer() {
|
||||
const pid = this.p_article_id;
|
||||
if (pid == null || pid === '') return Promise.resolve();
|
||||
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
||||
if (refs.length === 0) return Promise.resolve();
|
||||
let listJson;
|
||||
try {
|
||||
listJson = JSON.stringify(JSON.parse(JSON.stringify(refs)));
|
||||
} catch (e) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.$api
|
||||
.post('api/References/batchUpdateRefer', {
|
||||
p_article_id: String(pid),
|
||||
list: listJson
|
||||
})
|
||||
.then((res) => {
|
||||
const ok = res && (res.code === 0 || res.code === 1 || res.status === 1);
|
||||
if (ok) {
|
||||
this._lastReferListApiOrder = this.referIdOrderSnapshot(this.chanFerForm).slice();
|
||||
}
|
||||
if (!ok && res && (res.msg || res.message)) {
|
||||
console.warn('[batchUpdateRefer]', res.msg || res.message);
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn('[batchUpdateRefer] request failed', err);
|
||||
});
|
||||
},
|
||||
referIdOrderSnapshot(refs) {
|
||||
return (Array.isArray(refs) ? refs : [])
|
||||
.map((r) => (r && r.p_refer_id != null ? String(r.p_refer_id) : ''))
|
||||
.filter(Boolean);
|
||||
},
|
||||
tryBatchSyncReferOrderIfOutOfSync() {
|
||||
if (this._lastReferListApiOrder == null) return;
|
||||
const track = this._lastReferListApiOrder.map(String);
|
||||
const cur = this.referIdOrderSnapshot(this.chanFerForm);
|
||||
if (cur.length === 0) return;
|
||||
const same = track.length === cur.length && track.every((id, i) => id === cur[i]);
|
||||
if (!same) {
|
||||
this.syncBatchReferenceOrderToServer();
|
||||
}
|
||||
},
|
||||
scheduleTryBatchSyncReferOrderIfOutOfSync() {
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(() => {
|
||||
this.tryBatchSyncReferOrderIfOutOfSync();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
/** 编辑弹窗内正文一变立即按全文合并稿重排参考文献(不再防抖,选中/输入后列表马上跟正文一致) */
|
||||
scheduleReorderFromEditModal(html) {
|
||||
if (!this.editVisible || !this.currentContent || this.currentContent.am_id == null) return;
|
||||
@@ -1066,6 +1128,9 @@ export default {
|
||||
/** 以接口回写后的 Main_List 再对齐一次;稿面 word 会由 contentList 监听触发 syncRefOrder */
|
||||
this.$nextTick(() => {
|
||||
this.reorderReferencesFromMainListBody(null, null);
|
||||
this.$nextTick(() => {
|
||||
this.syncBatchReferenceOrderToServer();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
loading.close();
|
||||
@@ -2078,6 +2143,10 @@ export default {
|
||||
if (this.$refs.catalogue) {
|
||||
await this.$refs.catalogue.getCatalogueList();
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.reorderReferencesFromMainListBody(null, null);
|
||||
this.scheduleTryBatchSyncReferOrderIfOutOfSync();
|
||||
});
|
||||
loading.close();
|
||||
});
|
||||
// }, 1000);
|
||||
@@ -2304,6 +2373,9 @@ export default {
|
||||
if (w && typeof w.syncRefOrder === 'function') {
|
||||
w.syncRefOrder();
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.syncBatchReferenceOrderToServer();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
@@ -2356,6 +2428,9 @@ export default {
|
||||
if (w && typeof w.syncRefOrder === 'function') {
|
||||
w.syncRefOrder();
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.syncBatchReferenceOrderToServer();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
|
||||
@@ -134,6 +134,37 @@ export default {
|
||||
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() !== '');
|
||||
},
|
||||
/**
|
||||
* 表格编辑器专用:接口文献行的 old_index(0 起)+1 = 单元格里 [n] 对应的全局显示序号,再映射到 p_refer_id。
|
||||
* 无 old_index 时返回空对象,自动匹配回退为正文 citeMap。
|
||||
*/
|
||||
tableBracketNumToRefIdMap() {
|
||||
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
||||
const map = {};
|
||||
refs.forEach((r) => {
|
||||
if (!r || r.p_refer_id == null) return;
|
||||
const oi = r.old_index != null && r.old_index !== '' ? r.old_index : r.oldIndex;
|
||||
if (oi == null || oi === '') return;
|
||||
const n = Number(oi);
|
||||
if (Number.isNaN(n)) return;
|
||||
map[n + 1] = String(r.p_refer_id);
|
||||
});
|
||||
return map;
|
||||
},
|
||||
/** p_refer_id → 表格角标序号(old_index+1),用于表格内渲染 [n] 与排序 */
|
||||
tableRefIdToBracketNum() {
|
||||
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
||||
const m = {};
|
||||
refs.forEach((r) => {
|
||||
if (!r || r.p_refer_id == null) return;
|
||||
const oi = r.old_index != null && r.old_index !== '' ? r.old_index : r.oldIndex;
|
||||
if (oi == null || oi === '') return;
|
||||
const num = Number(oi);
|
||||
if (Number.isNaN(num)) return;
|
||||
m[String(r.p_refer_id)] = num + 1;
|
||||
});
|
||||
return m;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -271,7 +302,7 @@ export default {
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
},
|
||||
/** 与 word.vue renderCiteLabels:tooltip 行按引用序号排序 */
|
||||
/** 与 word.vue renderCiteLabels:合并角标 id 按 citeMap 序号排序 */
|
||||
sortAutociteIdsByCiteNumber(ids) {
|
||||
const uniq = [...new Set(ids.map(String))];
|
||||
const map = this.citeMap || {};
|
||||
@@ -316,10 +347,62 @@ export default {
|
||||
});
|
||||
return map;
|
||||
},
|
||||
/** 解析 [1]、[1,2]、[1–4] 等括号内数字列表(不含方括号) */
|
||||
/** 自动匹配 [n]:表格内用 old_index+1 映射;mytable 之后段落仍用全局 citeMap */
|
||||
_getBracketNumToIdMaps() {
|
||||
const globalMap = this.buildNumToRefIdMap();
|
||||
if (this.type !== 'table') {
|
||||
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.type === 'table' && Object.keys(this.tableBracketNumToRefIdMap || {}).length > 0) {
|
||||
return this.sortAutociteIdsByTableBracketNumber(ids);
|
||||
}
|
||||
return this.sortAutociteIdsByCiteNumber(ids);
|
||||
},
|
||||
_sortIdsAfterBracketMatch(ids, tableOffset) {
|
||||
if (
|
||||
this.type === 'table' &&
|
||||
tableOffset === 0 &&
|
||||
Object.keys(this.tableBracketNumToRefIdMap || {}).length > 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]);
|
||||
@@ -331,12 +414,6 @@ export default {
|
||||
for (let i = lo; i <= hi; i++) out.push(i);
|
||||
return out;
|
||||
}
|
||||
if (/[,,]/.test(t)) {
|
||||
return t
|
||||
.split(/[,,]/)
|
||||
.map((x) => parseInt(String(x).trim(), 10))
|
||||
.filter((n) => !Number.isNaN(n));
|
||||
}
|
||||
const n = parseInt(t, 10);
|
||||
return Number.isNaN(n) ? [] : [n];
|
||||
},
|
||||
@@ -352,8 +429,8 @@ export default {
|
||||
this.$message.warning(this.$t('wordCite.noRefs'));
|
||||
return { replaced: 0 };
|
||||
}
|
||||
const numToId = this.buildNumToRefIdMap();
|
||||
if (Object.keys(numToId).length === 0) {
|
||||
const maps = this._getBracketNumToIdMaps();
|
||||
if (Object.keys(maps.primary).length === 0) {
|
||||
this.$message.warning(this.$t('wordCite.noRefs'));
|
||||
return { replaced: 0 };
|
||||
}
|
||||
@@ -362,7 +439,7 @@ export default {
|
||||
if (!body) return { replaced: 0 };
|
||||
let replaced = 0;
|
||||
ed.undoManager.transact(() => {
|
||||
replaced = this._replaceBracketCitesInDocOrder(body, doc, numToId, { tableOffset: 0 });
|
||||
replaced = this._replaceBracketCitesInDocOrder(body, doc, maps, { tableOffset: 0 });
|
||||
});
|
||||
this.renderAutociteInEditor(ed);
|
||||
ed.fire('change');
|
||||
@@ -377,8 +454,8 @@ export default {
|
||||
return;
|
||||
}
|
||||
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
|
||||
const numToId = this.buildNumToRefIdMap();
|
||||
if (refs.length && Object.keys(numToId).length) {
|
||||
const maps = this._getBracketNumToIdMaps();
|
||||
if (refs.length && Object.keys(maps.primary).length) {
|
||||
this.$message.info(this.$t('wordCite.matchBracketRefsNone'));
|
||||
}
|
||||
},
|
||||
@@ -418,9 +495,9 @@ export default {
|
||||
* 按文档顺序遍历,使「表格链接」后任意后续段落里的 [n] 都能用上该表的最大全局序号偏移。
|
||||
* 旧实现只在同一父节点下、MYTABLE 与后续文本为兄弟时才生效,MYTABLE 在上一段、角标在下一段时会失效。
|
||||
*/
|
||||
_replaceBracketCitesInDocOrder(node, doc, numToId, state) {
|
||||
_replaceBracketCitesInDocOrder(node, doc, maps, state) {
|
||||
if (node.nodeType === 3) {
|
||||
return this._processTextNodeForBracketCites(node, doc, numToId, state.tableOffset);
|
||||
return this._processTextNodeForBracketCites(node, doc, maps, state.tableOffset);
|
||||
}
|
||||
if (node.nodeType !== 1) return 0;
|
||||
const name = node.nodeName;
|
||||
@@ -431,7 +508,7 @@ export default {
|
||||
if (isMytable) {
|
||||
let total = 0;
|
||||
Array.from(node.childNodes).forEach((c) => {
|
||||
total += this._replaceBracketCitesInDocOrder(c, doc, numToId, state);
|
||||
total += this._replaceBracketCitesInDocOrder(c, doc, maps, state);
|
||||
});
|
||||
const tid = (node.getAttribute && node.getAttribute('data-id')) || '';
|
||||
const map = this.tableLinkCiteMaxMap || {};
|
||||
@@ -443,11 +520,12 @@ export default {
|
||||
}
|
||||
let total = 0;
|
||||
Array.from(node.childNodes).forEach((c) => {
|
||||
total += this._replaceBracketCitesInDocOrder(c, doc, numToId, state);
|
||||
total += this._replaceBracketCitesInDocOrder(c, doc, maps, state);
|
||||
});
|
||||
return total;
|
||||
},
|
||||
_processTextNodeForBracketCites(textNode, doc, numToId, tableOffset = 0) {
|
||||
_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;
|
||||
@@ -465,11 +543,24 @@ export default {
|
||||
skippedSpecial++;
|
||||
continue;
|
||||
}
|
||||
// 任一序号在参考文献中无对应(含超出列表、tableOffset 后仍无效)则整段不转换,避免部分匹配
|
||||
const mapNo = (n) => (n > 0 && tableOffset > 0 ? n + tableOffset - 1 : n);
|
||||
if (
|
||||
!nums.length ||
|
||||
nums.some((n) => {
|
||||
const mappedNo = mapNo(n);
|
||||
return !numToId[mappedNo];
|
||||
})
|
||||
) {
|
||||
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();
|
||||
nums.forEach((n) => {
|
||||
// 表格链接后的局部序号从 tableMax+1 开始接续:1->max+1, 2->max+2 ...
|
||||
const mappedNo = n > 0 && tableOffset > 0 ? n + tableOffset - 1 : n;
|
||||
const mappedNo = mapNo(n);
|
||||
const id = numToId[mappedNo];
|
||||
if (id && !seen.has(id)) {
|
||||
seen.add(id);
|
||||
@@ -478,7 +569,7 @@ export default {
|
||||
});
|
||||
pieces.push({ type: 'text', s: text.slice(lastIndex, m.index) });
|
||||
if (ids.length > 0) {
|
||||
const sorted = this.sortAutociteIdsByCiteNumber(ids);
|
||||
const sorted = this._sortIdsAfterBracketMatch(ids, tableOffset);
|
||||
pieces.push({ type: 'cite', ids: sorted });
|
||||
replaced++;
|
||||
} else {
|
||||
@@ -517,8 +608,8 @@ export default {
|
||||
if (!body) return;
|
||||
const allAutocites = Array.from(
|
||||
ed.dom && typeof ed.dom.select === 'function'
|
||||
? ed.dom.select('mycite', body)
|
||||
: body.querySelectorAll('mycite')
|
||||
? ed.dom.select('mycite,autocite', body)
|
||||
: body.querySelectorAll('mycite, autocite')
|
||||
);
|
||||
if (!allAutocites.length) return;
|
||||
|
||||
@@ -530,23 +621,30 @@ export default {
|
||||
});
|
||||
|
||||
const citeMap = this.citeMap || {};
|
||||
const tableNums = this.tableRefIdToBracketNum || {};
|
||||
const useTableBracketNums = this.type === 'table' && Object.keys(this.tableBracketNumToRefIdMap || {}).length > 0;
|
||||
|
||||
allAutocites.forEach((el) => {
|
||||
ed.dom.setAttrib(el, 'contenteditable', 'false');
|
||||
el.style.display = '';
|
||||
const sortedAll = this.sortAutociteIdsByCiteNumber(
|
||||
this.parseAutociteDataIds(el.getAttribute('data-id'))
|
||||
);
|
||||
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.sortAutociteIdsByCiteNumber(validIds) : [];
|
||||
const sortedIds = validIds.length ? this._sortAutociteIdsForDisplay(validIds) : [];
|
||||
|
||||
const parts = sortedIds.map((id) => {
|
||||
const ref = refMap[String(id)];
|
||||
const no = ref ? citeMap[String(id)] : null;
|
||||
const num = no != null && no !== '' ? String(no) : null;
|
||||
const noCite = ref ? citeMap[String(id)] : null;
|
||||
const noTable =
|
||||
useTableBracketNums && tableNums[String(id)] != null ? tableNums[String(id)] : null;
|
||||
const num =
|
||||
noTable != null && noTable !== '' && !Number.isNaN(Number(noTable))
|
||||
? String(noTable)
|
||||
: noCite != null && noCite !== ''
|
||||
? String(noCite)
|
||||
: null;
|
||||
return { id, ref, num };
|
||||
});
|
||||
|
||||
@@ -556,22 +654,6 @@ export default {
|
||||
.map(Number);
|
||||
const label = numsForLabel.length > 0 ? this.formatCiteNumbers(numsForLabel) : '';
|
||||
|
||||
const lines = parts
|
||||
.filter((p) => p.ref)
|
||||
.map((p) => {
|
||||
const ref = p.ref;
|
||||
const content =
|
||||
ref.refer_frag ||
|
||||
[ref.author, ref.title, ref.joura, ref.dateno].filter(Boolean).join(' ').trim() ||
|
||||
'';
|
||||
const doi = ref.doilink || ref.isbn || ref.doi || '';
|
||||
const numLabel = p.num;
|
||||
if (numLabel) {
|
||||
return `[${numLabel}] ${content}\nDOI: ${doi}`;
|
||||
}
|
||||
return `${content}\nDOI: ${doi}`;
|
||||
});
|
||||
|
||||
if (!label) {
|
||||
ed.dom.remove(el);
|
||||
return;
|
||||
@@ -579,8 +661,18 @@ export default {
|
||||
|
||||
el.textContent = `[${label}]`;
|
||||
el.style.display = '';
|
||||
ed.dom.setAttrib(el, 'title', lines.join('\n'));
|
||||
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);
|
||||
},
|
||||
@@ -589,7 +681,7 @@ export default {
|
||||
const doc = ed.getDoc();
|
||||
const body = doc.body;
|
||||
if (!body) return;
|
||||
body.querySelectorAll('mycite').forEach((el) => {
|
||||
body.querySelectorAll('mycite, autocite').forEach((el) => {
|
||||
const next = el.nextSibling;
|
||||
if (next === null) {
|
||||
el.parentNode.appendChild(doc.createTextNode('\u200b'));
|
||||
@@ -658,8 +750,10 @@ export default {
|
||||
/** 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] 等旧文案 */
|
||||
let out = html.replace(/<mycite([^>]*)>[\s\S]*?<\/mycite>/gi, '<mycite$1>​</mycite>');
|
||||
out = out.replace(/<mycite([^>]*)>[\s\S]*?<\/mycite>/gi, '<mycite$1>​</mycite>');
|
||||
// 外侧:连续空格 / 合并为单个 ,避免「普通空格 + 」叠成大缝
|
||||
out = out.replace(/(?:\s| | )+(?=<mycite\b)/gi, ' ');
|
||||
out = out.replace(/(?<=<\/mycite>)(?:\s| | )+/gi, ' ');
|
||||
@@ -1018,7 +1112,7 @@ export default {
|
||||
valid_elements:
|
||||
this.type == 'table'
|
||||
? '*[*]'
|
||||
: `img[src|alt|width|height],strong,em,sub,sup,blue,table,b,i,myfigure,mytable,wmath,mycite[data-id|contenteditable|title|data-cite-missing|style]${this.valid_elements}`, // 允许的标签和属性
|
||||
: `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,
|
||||
@@ -1205,7 +1299,7 @@ export default {
|
||||
});
|
||||
|
||||
ed.on('click', function (e) {
|
||||
const autociteEl = e.target.closest('mycite');
|
||||
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);
|
||||
|
||||
@@ -997,23 +997,9 @@
|
||||
</div>
|
||||
<div
|
||||
class="row-divider"
|
||||
v-if="(currentData.type == 0 || currentData.type == 1) && (isEditComment || manuscriptAutociteContext)"
|
||||
v-if="(currentData.type == 0 || currentData.type == 1) && isEditComment"
|
||||
></div>
|
||||
|
||||
<template
|
||||
v-if="
|
||||
manuscriptAutociteContext &&
|
||||
currentData &&
|
||||
(currentData.type == 0 || currentData.type == 1 || currentData.type == 2)
|
||||
"
|
||||
>
|
||||
<div class="menu-item menu-autocite-ref" @click.stop="menuAction('editRefCite')">
|
||||
<i class="el-icon-edit-outline"></i><span>{{ $t('wordCite.modifyRef') }}</span>
|
||||
</div>
|
||||
<div class="menu-item danger" @click.stop="menuAction('removeRefCite')">
|
||||
<i class="el-icon-remove-outline"></i><span>{{ $t('wordCite.removeRefTag') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
class="menu-item menu-link"
|
||||
v-if="currentData.type == 0 && !['figure', 'table'].includes(currentTag) && !manuscriptAutociteContext"
|
||||
@@ -1064,15 +1050,72 @@
|
||||
>
|
||||
<div class="cite-preview-head">
|
||||
<span>{{ $t('wordCite.reference') }}</span>
|
||||
<i class="el-icon-close" @click="hideCitePreview"></i>
|
||||
<div>
|
||||
<span
|
||||
class="cite-preview-action-btn"
|
||||
:title="$t('wordCite.modifyRef')"
|
||||
@click.stop="menuAction('editRefCite')"
|
||||
>
|
||||
<i class="el-icon-edit-outline"></i>Edit citation
|
||||
</span>
|
||||
<span style="margin-left: 20px"
|
||||
class="cite-preview-action-btn cite-preview-action-danger"
|
||||
:title="$t('wordCite.removeRefTag')"
|
||||
@click.stop="menuAction('removeRefCite')"
|
||||
>
|
||||
<i class="el-icon-remove-outline"></i>Remove citation
|
||||
</span>
|
||||
|
||||
<i class="el-icon-close" @click="hideCitePreview" style="margin-left: 20px"></i>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="cite-preview-body">
|
||||
<div v-for="(it, idx) in citePreviewItems" :key="`${it.id}-${idx}`" class="cite-preview-item">
|
||||
<div class="cite-preview-title">
|
||||
[{{ it.no }}] {{ it.title }}
|
||||
<div v-if="citePreviewSortedItems.length" class="cite-preview-toolbar">
|
||||
<div class="cite-preview-nums-wrap">
|
||||
<span
|
||||
v-for="(it, idx) in citePreviewSortedItems"
|
||||
:key="`chip-${it.id}-${idx}`"
|
||||
:class="['cite-num-chip', { active: idx === citePreviewActiveIndex }]"
|
||||
:title="$t('wordCite.locateInRefHint')"
|
||||
@click.stop="onCitePreviewChipClick(idx)"
|
||||
>{{ it.no }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="citePreviewSortedItems.length > 1" class="cite-preview-pager">
|
||||
<span class="cite-preview-pager-text">{{ citePreviewActiveIndex + 1 }} / {{ citePreviewSortedItems.length }}</span>
|
||||
<i
|
||||
class="el-icon-arrow-left cite-preview-pager-btn"
|
||||
:class="{ disabled: citePreviewActiveIndex <= 0 }"
|
||||
@click.stop="citePreviewStep(-1)"
|
||||
></i>
|
||||
<i
|
||||
class="el-icon-arrow-right cite-preview-pager-btn"
|
||||
:class="{ disabled: citePreviewActiveIndex >= citePreviewSortedItems.length - 1 }"
|
||||
@click.stop="citePreviewStep(1)"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="citePreviewBody" class="cite-preview-body">
|
||||
<div
|
||||
v-for="(it, idx) in citePreviewSortedItems"
|
||||
:key="`body-${it.id}-${idx}`"
|
||||
:class="['cite-preview-item', { 'is-active': idx === citePreviewActiveIndex }]"
|
||||
>
|
||||
<div class="cite-preview-line cite-preview-num-auth">
|
||||
<span class="cite-preview-num">{{ it.no }}.</span>
|
||||
<span v-if="it.author" class="cite-preview-auth">{{ it.author }}</span>
|
||||
</div>
|
||||
<div class="cite-preview-meta">
|
||||
{{ it.meta }}
|
||||
<div v-if="it.title && it.title !== '-'" class="cite-preview-line cite-preview-article-title">
|
||||
{{ it.title }}
|
||||
</div>
|
||||
<div
|
||||
v-if="it.joura || it.dateno"
|
||||
class="cite-preview-line cite-preview-journal-row"
|
||||
>
|
||||
<em v-if="it.joura" class="cite-preview-journal">{{ it.joura }}</em>
|
||||
<span v-if="it.joura && it.dateno"> </span>
|
||||
<span v-if="it.dateno" class="cite-preview-dateno">{{ it.dateno }}</span>
|
||||
</div>
|
||||
<a v-if="it.doi" class="cite-preview-doi" :href="it.doi" target="_blank">{{ it.doi }}</a>
|
||||
</div>
|
||||
@@ -1235,10 +1278,15 @@ export default {
|
||||
manuscriptAutociteContext: null,
|
||||
citePreviewVisible: false,
|
||||
citePreviewItems: [],
|
||||
/** 合并引用多文献时,顶部序号条当前高亮项(与分页箭头同步) */
|
||||
citePreviewActiveIndex: 0,
|
||||
citePreviewStyle: {
|
||||
position: 'fixed',
|
||||
left: '0px',
|
||||
top: '0px',
|
||||
bottom: 'auto',
|
||||
width: '',
|
||||
maxHeight: '',
|
||||
zIndex: 10001
|
||||
},
|
||||
/** 正文 DOM 中 mycite 首次出现顺序(唯一 p_refer_id),驱动 citeMap 与参考文献列表排序 */
|
||||
@@ -1408,6 +1456,24 @@ export default {
|
||||
});
|
||||
return map;
|
||||
},
|
||||
/** 弹窗内文献按全局序号排序,与稿面角标顺序一致 */
|
||||
citePreviewSortedItems() {
|
||||
const items = this.citePreviewItems || [];
|
||||
return items.slice().sort((a, b) => {
|
||||
const na = Number(a.no);
|
||||
const nb = Number(b.no);
|
||||
if (!Number.isNaN(na) && !Number.isNaN(nb)) return na - nb;
|
||||
if (!Number.isNaN(na)) return -1;
|
||||
if (!Number.isNaN(nb)) return 1;
|
||||
return String(a.no || '').localeCompare(String(b.no || ''));
|
||||
});
|
||||
},
|
||||
citePreviewActiveItem() {
|
||||
const arr = this.citePreviewSortedItems;
|
||||
if (!arr.length) return null;
|
||||
const i = Math.max(0, Math.min(this.citePreviewActiveIndex, arr.length - 1));
|
||||
return arr[i];
|
||||
},
|
||||
sortedProofreadingList() {
|
||||
const order = [2, 1, 3];
|
||||
const rank = { 2: 0, 1: 1, 3: 2 };
|
||||
@@ -1599,22 +1665,6 @@ renderCiteLabels(html) {
|
||||
const label =
|
||||
numsForLabel.length > 0 ? this.formatCiteNumbers(numsForLabel) : '';
|
||||
|
||||
const lines = parts
|
||||
.filter((p) => p.ref)
|
||||
.map((p) => {
|
||||
const ref = p.ref;
|
||||
const content =
|
||||
ref.refer_frag ||
|
||||
[ref.author, ref.title, ref.joura, ref.dateno].filter(Boolean).join(' ').trim() ||
|
||||
'';
|
||||
const doi = ref.doilink || ref.isbn || ref.doi || '';
|
||||
const numLabel = p.num;
|
||||
if (numLabel) {
|
||||
return `[${numLabel}] ${content}\nDOI: ${doi}`;
|
||||
}
|
||||
return `${content}\nDOI: ${doi}`;
|
||||
});
|
||||
|
||||
const escAttr = (s) =>
|
||||
String(s || '')
|
||||
.replace(/&/g, '&')
|
||||
@@ -1624,8 +1674,7 @@ renderCiteLabels(html) {
|
||||
return '';
|
||||
}
|
||||
const dataIdAttr = escAttr(sortedIds.join(','));
|
||||
const titleAttr = escAttr(lines.join('\n'));
|
||||
return `<mycite data-id="${dataIdAttr}" title="${titleAttr}">[${label}]</mycite>`;
|
||||
return `<mycite data-id="${dataIdAttr}">[${label}]</mycite>`;
|
||||
})
|
||||
/** 库内遗留:空 data-id 或仅零宽字符的 mycite 整段去掉 */
|
||||
.replace(/<mycite[^>]*data-id=""[^>]*>[\s\S]*?<\/mycite>/gi, '');
|
||||
@@ -1765,6 +1814,7 @@ renderCiteLabels(html) {
|
||||
if (!hasSelection && !isClickOnParagraph) {
|
||||
this.bubbleVisible = false;
|
||||
this.isMenuVisible = false;
|
||||
this.citePreviewVisible = false;
|
||||
|
||||
this.currentData = {};
|
||||
this.currentId = '';
|
||||
@@ -2222,8 +2272,67 @@ renderCiteLabels(html) {
|
||||
hideCitePreview() {
|
||||
this.citePreviewVisible = false;
|
||||
this.citePreviewItems = [];
|
||||
this.citePreviewActiveIndex = 0;
|
||||
this._citePreviewMyciteEl = null;
|
||||
this.citePreviewStyle = {
|
||||
position: 'fixed',
|
||||
left: '0px',
|
||||
top: '0px',
|
||||
bottom: 'auto',
|
||||
width: '',
|
||||
maxHeight: '',
|
||||
zIndex: 10001
|
||||
};
|
||||
},
|
||||
/** 按视口与角标位置计算弹窗 maxHeight、贴下或贴上,避免靠页底时裁切 */
|
||||
placeCitePreviewNear(triggerEl) {
|
||||
if (!triggerEl || typeof triggerEl.getBoundingClientRect !== 'function') return;
|
||||
const rect = triggerEl.getBoundingClientRect();
|
||||
const pad = 12;
|
||||
const gap = 8;
|
||||
const popW = 640;
|
||||
const widthPx = Math.min(popW, Math.max(280, window.innerWidth - pad * 2));
|
||||
/** 略限制首选高度,避免弹窗占满屏;列表区靠更紧凑的样式展示更多条 */
|
||||
const maxPref = Math.min(440, window.innerHeight - pad * 2);
|
||||
const minReadable = 120;
|
||||
|
||||
const spaceBelow = window.innerHeight - rect.bottom - pad;
|
||||
const spaceAbove = rect.top - pad;
|
||||
|
||||
let useAbove = false;
|
||||
let maxH;
|
||||
if (spaceBelow >= minReadable) {
|
||||
maxH = Math.min(maxPref, spaceBelow - gap);
|
||||
} else if (spaceAbove >= minReadable) {
|
||||
useAbove = true;
|
||||
maxH = Math.min(maxPref, spaceAbove - gap);
|
||||
} else {
|
||||
useAbove = spaceAbove > spaceBelow;
|
||||
maxH = Math.min(maxPref, (useAbove ? spaceAbove : spaceBelow) - gap);
|
||||
}
|
||||
|
||||
maxH = Math.floor(Math.max(72, maxH));
|
||||
const left = Math.max(pad, Math.min(rect.left, window.innerWidth - widthPx - pad));
|
||||
|
||||
const next = {
|
||||
position: 'fixed',
|
||||
left: `${left}px`,
|
||||
zIndex: 10001,
|
||||
width: `${widthPx}px`,
|
||||
maxHeight: `${maxH}px`
|
||||
};
|
||||
if (useAbove) {
|
||||
next.bottom = `${window.innerHeight - rect.top + gap}px`;
|
||||
next.top = 'auto';
|
||||
} else {
|
||||
next.top = `${rect.bottom + gap}px`;
|
||||
next.bottom = 'auto';
|
||||
}
|
||||
this.citePreviewStyle = next;
|
||||
},
|
||||
showCitePreviewByAutocite(el, dataId) {
|
||||
this._citePreviewMyciteEl = el || null;
|
||||
this.citePreviewActiveIndex = 0;
|
||||
const ids = String(dataId || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
@@ -2243,26 +2352,77 @@ renderCiteLabels(html) {
|
||||
const ref = refMap[String(id)];
|
||||
if (!ref) return null;
|
||||
const no = citeMap[String(id)] || '?';
|
||||
// 标题优先走 Crossref 解析出的 title;兜底 refer_frag
|
||||
const title = (ref.title || ref.refer_frag || '').toString().trim() || '-';
|
||||
const meta = [ref.author, ref.joura, ref.dateno].filter(Boolean).join(' ').trim();
|
||||
const author = (ref.author || '').toString().trim();
|
||||
const title =
|
||||
(ref.title || '').toString().trim() ||
|
||||
(ref.refer_frag || '').toString().trim() ||
|
||||
'-';
|
||||
const joura = (ref.joura || '').toString().trim();
|
||||
const dateno = (ref.dateno || '').toString().trim();
|
||||
const doi = (ref.doilink || ref.doi || ref.isbn || '').toString().trim();
|
||||
return { id: String(id), no, title, meta, doi };
|
||||
return { id: String(id), no, author, title, joura, dateno, doi };
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (!items.length) {
|
||||
this.hideCitePreview();
|
||||
return;
|
||||
}
|
||||
const rect = el.getBoundingClientRect();
|
||||
this.citePreviewStyle = {
|
||||
position: 'fixed',
|
||||
left: `${Math.max(12, Math.min(rect.left, window.innerWidth - 460))}px`,
|
||||
top: `${Math.min(rect.bottom + 8, window.innerHeight - 220)}px`,
|
||||
zIndex: 10001
|
||||
};
|
||||
this.citePreviewItems = items;
|
||||
this.citePreviewVisible = true;
|
||||
this.$nextTick(() => {
|
||||
this.placeCitePreviewNear(el);
|
||||
});
|
||||
},
|
||||
scrollCitePreviewToBody() {
|
||||
const el = this._citePreviewMyciteEl;
|
||||
const sc = this.$refs.scrollDiv;
|
||||
if (!el || !sc) return;
|
||||
const top = this.topInScroll(el, sc) - 80;
|
||||
sc.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
|
||||
if (el._citeFlashTimer) {
|
||||
clearTimeout(el._citeFlashTimer);
|
||||
el._citeFlashTimer = null;
|
||||
}
|
||||
el.classList.remove('cite-mycite-flash');
|
||||
void el.offsetWidth;
|
||||
el.classList.add('cite-mycite-flash');
|
||||
el._citeFlashTimer = setTimeout(() => {
|
||||
el.classList.remove('cite-mycite-flash');
|
||||
el._citeFlashTimer = null;
|
||||
}, 1000);
|
||||
},
|
||||
scrollCitePreviewToRefRow(pReferId) {
|
||||
if (pReferId == null || pReferId === '') return;
|
||||
const target = document.querySelector(`[data-id="ref-${pReferId}"]`);
|
||||
if (!target) return;
|
||||
const sc = this.$refs.scrollDiv;
|
||||
if (!sc) {
|
||||
this.scrollContainerTo(target, 80);
|
||||
return;
|
||||
}
|
||||
const top = this.topInScroll(target, sc) - 80;
|
||||
sc.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
|
||||
},
|
||||
onCitePreviewChipClick(idx) {
|
||||
this.citePreviewActiveIndex = idx;
|
||||
this.$nextTick(() => this.scrollCitePreviewItemIntoView());
|
||||
},
|
||||
citePreviewStep(delta) {
|
||||
const list = this.citePreviewSortedItems;
|
||||
if (!list.length) return;
|
||||
let i = this.citePreviewActiveIndex + delta;
|
||||
i = Math.max(0, Math.min(list.length - 1, i));
|
||||
if (i === this.citePreviewActiveIndex) return;
|
||||
this.citePreviewActiveIndex = i;
|
||||
this.$nextTick(() => this.scrollCitePreviewItemIntoView());
|
||||
},
|
||||
scrollCitePreviewItemIntoView() {
|
||||
const body = this.$refs.citePreviewBody;
|
||||
if (!body) return;
|
||||
const row = body.querySelector('.cite-preview-item.is-active');
|
||||
if (row && typeof row.scrollIntoView === 'function') {
|
||||
row.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
},
|
||||
escapeHtmlAttr(val) {
|
||||
return String(val == null ? '' : val).replace(/&/g, '&').replace(/"/g, '"');
|
||||
@@ -3628,6 +3788,11 @@ renderCiteLabels(html) {
|
||||
if (Math.abs(newScrollTop - (this.lastScrollTop || 0)) < 10) return;
|
||||
this.lastScrollTop = newScrollTop;
|
||||
|
||||
// 与段落工具栏一致:主稿滚动时关闭 Reference 预览浮层
|
||||
if (this.citePreviewVisible) {
|
||||
this.hideCitePreview();
|
||||
}
|
||||
|
||||
this.clearHighlight();
|
||||
|
||||
if (!this.isPreview) {
|
||||
@@ -5204,56 +5369,265 @@ font-weight: bold !important;
|
||||
}
|
||||
|
||||
.cite-preview-pop {
|
||||
width: 440px;
|
||||
max-height: 220px;
|
||||
width: 640px;
|
||||
max-width: calc(100vw - 24px);
|
||||
/* max-height 由 placeCitePreviewNear 按视口与角标位置内联计算,避免贴底裁切 */
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.16);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 10px;
|
||||
}
|
||||
.cite-preview-head {
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
flex-shrink: 0;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
background: #f8f9fb;
|
||||
}
|
||||
.cite-preview-head i {
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
padding: 2px;
|
||||
}
|
||||
.cite-preview-toolbar {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
background: #fafbfc;
|
||||
}
|
||||
.cite-preview-nums-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 3px 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-height: 52px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 1px 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.cite-preview-nums-wrap::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
.cite-preview-nums-wrap::-webkit-scrollbar-thumb {
|
||||
background: #c0c4cc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.cite-num-chip {
|
||||
display: inline-block;
|
||||
padding: 2px 5px;
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
color: #303133;
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.cite-num-chip:hover {
|
||||
border-color: #409eff;
|
||||
color: #409eff;
|
||||
}
|
||||
.cite-num-chip.active {
|
||||
background: #ecf5ff;
|
||||
border-color: #409eff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cite-preview-ref-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
padding-left: 6px;
|
||||
margin-left: 2px;
|
||||
border-left: 1px solid #e4e7ed;
|
||||
align-self: center;
|
||||
}
|
||||
.cite-preview-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: auto;
|
||||
height: 22px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
font-size: 11px;background: #ecf5ff;
|
||||
color: #006699;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.cite-preview-action-btn.cite-preview-action-danger {
|
||||
background: #fef0f0;
|
||||
color: red;
|
||||
}
|
||||
.cite-preview-pager {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
}
|
||||
.cite-preview-pager-text {
|
||||
font-size: 10px;
|
||||
color: #606266;
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
.cite-preview-pager-btn {
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
padding: 1px;
|
||||
}
|
||||
.cite-preview-pager-btn.disabled {
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.cite-preview-current {
|
||||
flex-shrink: 0;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
.cite-preview-current:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
.cite-preview-current-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
.cite-preview-current-no {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.cite-preview-locate-tip {
|
||||
font-size: 12px;
|
||||
color: #409eff;
|
||||
}
|
||||
.cite-preview-body {
|
||||
max-height: 188px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 8px 10px;
|
||||
padding: 4px 6px 6px;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.cite-preview-body::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.cite-preview-body::-webkit-scrollbar-thumb {
|
||||
background: #c0c4cc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.cite-preview-item {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 2px;
|
||||
padding: 3px 5px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.cite-preview-item.is-active {
|
||||
background: #e8f4ff;
|
||||
border-color: #a0cfff;
|
||||
}
|
||||
.cite-preview-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.cite-preview-title {
|
||||
font-weight: 600;
|
||||
/* 与稿末参考文献列表一致:序号(蓝) + 作者 → 题名 → 斜体期刊 + 年卷期 → DOI 独行 */
|
||||
.cite-preview-line {
|
||||
word-break: break-word;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.cite-preview-meta {
|
||||
color: #606266;
|
||||
word-break: break-word;
|
||||
.cite-preview-line:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.cite-preview-num-auth {
|
||||
line-height: 1.4;
|
||||
}
|
||||
.cite-preview-num {
|
||||
font-weight: 700;
|
||||
color: #006699;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.cite-preview-auth {
|
||||
color: #303133;
|
||||
font-weight: 400;
|
||||
}
|
||||
.cite-preview-article-title {
|
||||
color: #303133;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.cite-preview-journal-row {
|
||||
color: #303133;
|
||||
font-size: 10px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.cite-preview-journal {
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
.cite-preview-dateno {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
.cite-preview-doi {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
color: #006699;
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
::v-deep mycite.cite-mycite-flash {
|
||||
outline: 2px solid #409eff;
|
||||
outline-offset: 2px;
|
||||
animation: citeMyciteFlash 0.95s ease;
|
||||
}
|
||||
@keyframes citeMyciteFlash {
|
||||
0%,
|
||||
100% {
|
||||
outline-color: #409eff;
|
||||
}
|
||||
50% {
|
||||
outline-color: #67c23a;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blueGlow {
|
||||
0%,
|
||||
100% {
|
||||
|
||||
@@ -96,11 +96,11 @@
|
||||
|
||||
<div class="body-editor-container">
|
||||
<div class="subject-label" style="margin-bottom: 10px;">{{ $t('mailboxMouldDetail.emailBody') }}:</div>
|
||||
<TmrEmailEditor
|
||||
<!-- <TmrEmailEditor
|
||||
v-model="form.body"
|
||||
placeholder=""
|
||||
/>
|
||||
<!-- <CkeditorMail v-model="form.body" /> -->
|
||||
/> -->
|
||||
<CkeditorMail v-model="form.body" />
|
||||
</div>
|
||||
</el-card>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user