This commit is contained in:
2026-04-10 09:12:40 +08:00
parent 9c44064f51
commit 797ca258f8
9 changed files with 285 additions and 25 deletions

View File

@@ -1164,6 +1164,7 @@ colTitle: 'Template title',
notFoundById: 'No reference found for citation ID {id}', notFoundById: 'No reference found for citation ID {id}',
selectRef: 'Select Reference', selectRef: 'Select Reference',
reference: 'Reference', reference: 'Reference',
uncited: 'Uncited',
cancel: 'Cancel', cancel: 'Cancel',
confirm: 'Confirm', confirm: 'Confirm',
remove: 'Remove', remove: 'Remove',

View File

@@ -1149,6 +1149,7 @@ const zh = {
notFoundById: '未查询到编号{id}相关参考文献', notFoundById: '未查询到编号{id}相关参考文献',
selectRef: '选择参考文献', selectRef: '选择参考文献',
reference: '参考文献', reference: '参考文献',
uncited: '未引用',
cancel: '取消', cancel: '取消',
confirm: '确认', confirm: '确认',
remove: '移除', remove: '移除',

View File

@@ -138,6 +138,7 @@
ref="editPublicRefTableOnly" ref="editPublicRefTableOnly"
:chanFerForm="chanFerForm" :chanFerForm="chanFerForm"
:chanFerFormRepeatList="chanFerFormRepeatList" :chanFerFormRepeatList="chanFerFormRepeatList"
:citeOrderFromParent="articleCiteIdOrder"
:ref-order-follows-body="true" :ref-order-follows-body="true"
:p_article_id="p_article_id" :p_article_id="p_article_id"
@ChanFerMashUp="ChanFerMashUp" @ChanFerMashUp="ChanFerMashUp"
@@ -558,6 +559,16 @@
</el-table-column> </el-table-column>
<el-table-column :label="$t('wordCite.reference')"> <el-table-column :label="$t('wordCite.reference')">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tag
v-if="isUncitedReference(scope.row)"
class="uncited-tag"
size="mini"
type="info"
effect="plain"
style="margin: 0 8px 6px 0"
>
{{ $t('wordCite.uncited') }}
</el-tag>
<!-- journal 形式 --> <!-- journal 形式 -->
<div style="text-align: left" v-if="scope.row.refer_type == 'journal'" class="reference-item"> <div style="text-align: left" v-if="scope.row.refer_type == 'journal'" class="reference-item">
<p> <p>
@@ -1163,6 +1174,13 @@ export default {
}, },
methods: { methods: {
isUncitedReference(row) {
const id = row && row.p_refer_id != null ? String(row.p_refer_id) : '';
if (!id) return false;
const order = Array.isArray(this.articleCiteIdOrder) ? this.articleCiteIdOrder.map(String) : [];
if (!order.length) return false;
return !order.includes(id);
},
/** 参考文献:刷新(供编辑/新增/删除后更新选择器列表) */ /** 参考文献:刷新(供编辑/新增/删除后更新选择器列表) */
changeRefer() { changeRefer() {
return this.fetchReferList(); return this.fetchReferList();
@@ -3992,6 +4010,12 @@ export default {
cursor: pointer; cursor: pointer;
} }
::v-deep .uncited-tag.el-tag {
background-color: #f56c6c !important;
border-color: #f56c6c !important;
color: #fff !important;
}
.type_CHar { .type_CHar {
position: relative; position: relative;
border-left: 4px solid rgba(0 102 153 / 20%); border-left: 4px solid rgba(0 102 153 / 20%);

View File

@@ -1198,11 +1198,6 @@ export default {
text: 'Reference', text: 'Reference',
tooltip: 'Insert Reference', tooltip: 'Insert Reference',
onAction: function () { onAction: function () {
const refs = Array.isArray(_this.chanFerForm) ? _this.chanFerForm : [];
if (refs.length === 0) {
_this.$message.warning(_this.$t('wordCite.noRefs'));
return;
}
_this._refBookmark = ed.selection.getBookmark(2); _this._refBookmark = ed.selection.getBookmark(2);
_this._editingAutocite = null; _this._editingAutocite = null;
_this.$emit('openRefSelector', { currentRefIds: [] }); _this.$emit('openRefSelector', { currentRefIds: [] });
@@ -1212,11 +1207,6 @@ export default {
ed.on('click', function (e) { ed.on('click', function (e) {
const autociteEl = e.target.closest('mycite'); const autociteEl = e.target.closest('mycite');
if (autociteEl) { if (autociteEl) {
const refs = Array.isArray(_this.chanFerForm) ? _this.chanFerForm : [];
if (refs.length === 0) {
_this.$message.warning(_this.$t('wordCite.noRefs'));
return;
}
const dataIds = _this.parseAutociteDataIds(autociteEl.getAttribute('data-id')); const dataIds = _this.parseAutociteDataIds(autociteEl.getAttribute('data-id'));
_this._refBookmark = ed.selection.getBookmark(2); _this._refBookmark = ed.selection.getBookmark(2);
_this._editingAutocite = autociteEl; _this._editingAutocite = autociteEl;

View File

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

View File

@@ -1056,6 +1056,28 @@
</div> </div>
</div> </div>
</div> </div>
<div
v-if="citePreviewVisible && citePreviewItems.length > 0"
class="cite-preview-pop"
:style="citePreviewStyle"
@click.stop
>
<div class="cite-preview-head">
<span>{{ $t('wordCite.reference') }}</span>
<i class="el-icon-close" @click="hideCitePreview"></i>
</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>
<div class="cite-preview-meta">
{{ it.meta }}
</div>
<a v-if="it.doi" class="cite-preview-doi" :href="it.doi" target="_blank">{{ it.doi }}</a>
</div>
</div>
</div>
<common-media-link-dialog <common-media-link-dialog
ref="mediaLinkRef" ref="mediaLinkRef"
@confirm="onLinkConfirm" @confirm="onLinkConfirm"
@@ -1211,6 +1233,14 @@ export default {
lastTag: null, lastTag: null,
/** 稿面点击 <mycite> 时记录,用于浮动条「修改引用 / 移除引用」 */ /** 稿面点击 <mycite> 时记录,用于浮动条「修改引用 / 移除引用」 */
manuscriptAutociteContext: null, manuscriptAutociteContext: null,
citePreviewVisible: false,
citePreviewItems: [],
citePreviewStyle: {
position: 'fixed',
left: '0px',
top: '0px',
zIndex: 10001
},
/** 正文 DOM 中 mycite 首次出现顺序(唯一 p_refer_id驱动 citeMap 与参考文献列表排序 */ /** 正文 DOM 中 mycite 首次出现顺序(唯一 p_refer_id驱动 citeMap 与参考文献列表排序 */
bodyCiteIdOrder: [], bodyCiteIdOrder: [],
_syncRefOrderTimer: null, _syncRefOrderTimer: null,
@@ -1725,6 +1755,7 @@ renderCiteLabels(html) {
// 如果点在气泡内,不关 // 如果点在气泡内,不关
if (e.target.closest('.bubble-container')) return; if (e.target.closest('.bubble-container')) return;
if (e.target.closest('.selection-bubble')) return; if (e.target.closest('.selection-bubble')) return;
if (e.target.closest('.cite-preview-pop')) return;
const selection = window.getSelection(); const selection = window.getSelection();
const hasSelection = selection && selection.toString().trim() !== ''; const hasSelection = selection && selection.toString().trim() !== '';
@@ -1738,6 +1769,7 @@ renderCiteLabels(html) {
this.currentData = {}; this.currentData = {};
this.currentId = ''; this.currentId = '';
this.manuscriptAutociteContext = null; this.manuscriptAutociteContext = null;
this.hideCitePreview();
this.clearActiveGlow(); // 清除高亮 this.clearActiveGlow(); // 清除高亮
} }
@@ -2185,6 +2217,52 @@ renderCiteLabels(html) {
this.bubbleVisible = false; this.bubbleVisible = false;
this.isMenuVisible = false; this.isMenuVisible = false;
this.hideCitePreview();
},
hideCitePreview() {
this.citePreviewVisible = false;
this.citePreviewItems = [];
},
showCitePreviewByAutocite(el, dataId) {
const ids = String(dataId || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
if (!ids.length) {
this.hideCitePreview();
return;
}
const refs = Array.isArray(this.chanFerForm) ? this.chanFerForm : [];
const citeMap = this.citeMap || {};
const refMap = {};
refs.forEach((r) => {
if (r && r.p_refer_id != null) refMap[String(r.p_refer_id)] = r;
});
const items = ids
.map((id) => {
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 doi = (ref.doilink || ref.doi || ref.isbn || '').toString().trim();
return { id: String(id), no, title, meta, 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;
}, },
escapeHtmlAttr(val) { escapeHtmlAttr(val) {
return String(val == null ? '' : val).replace(/&/g, '&amp;').replace(/"/g, '&quot;'); return String(val == null ? '' : val).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
@@ -3439,6 +3517,7 @@ renderCiteLabels(html) {
this.currentTag = ''; this.currentTag = '';
this.currentTagData = null; // 必须重置,防止带入旧数据 this.currentTagData = null; // 必须重置,防止带入旧数据
this.manuscriptAutociteContext = null; this.manuscriptAutociteContext = null;
this.hideCitePreview();
const clickedAutocite = event.target.closest('mycite'); const clickedAutocite = event.target.closest('mycite');
if (clickedAutocite && !this.isPreview) { if (clickedAutocite && !this.isPreview) {
const dataId = clickedAutocite.getAttribute('data-id'); const dataId = clickedAutocite.getAttribute('data-id');
@@ -3446,6 +3525,7 @@ renderCiteLabels(html) {
am_id: id, am_id: id,
dataId: dataId != null && dataId !== '' ? String(dataId) : '' dataId: dataId != null && dataId !== '' ? String(dataId) : ''
}; };
this.showCitePreviewByAutocite(clickedAutocite, dataId);
} }
const clickedTag = event.target.closest('myfigure, mytable, myh3'); const clickedTag = event.target.closest('myfigure, mytable, myh3');
if (clickedTag) { if (clickedTag) {
@@ -5122,6 +5202,58 @@ font-weight: bold !important;
::v-deep myh3 *{ ::v-deep myh3 *{
font-weight: bold !important; font-weight: bold !important;
} }
.cite-preview-pop {
width: 440px;
max-height: 220px;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 6px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.16);
overflow: hidden;
}
.cite-preview-head {
height: 32px;
padding: 0 10px;
border-bottom: 1px solid #ebeef5;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #606266;
background: #f8f9fb;
}
.cite-preview-head i {
cursor: pointer;
}
.cite-preview-body {
max-height: 188px;
overflow: auto;
padding: 8px 10px;
}
.cite-preview-item {
font-size: 12px;
line-height: 18px;
color: #303133;
margin-bottom: 8px;
}
.cite-preview-item:last-child {
margin-bottom: 0;
}
.cite-preview-title {
font-weight: 600;
word-break: break-word;
}
.cite-preview-meta {
color: #606266;
word-break: break-word;
}
.cite-preview-doi {
color: #006699;
text-decoration: none;
word-break: break-all;
}
@keyframes blueGlow { @keyframes blueGlow {
0%, 0%,
100% { 100% {

View File

@@ -239,18 +239,47 @@ export default {
openAddDialog() { this.addDialogVisible = true; }, openAddDialog() { this.addDialogVisible = true; },
resetAddForm() { this.addForm = { field: '', runNow: false }; this.addLoading = false; }, resetAddForm() { this.addForm = { field: '', runNow: false }; this.addLoading = false; },
async submitAddKeyword() { 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; this.addLoading = true;
try { try {
const res = await this.$api.post('api/expert_manage/addFetchField', { field: this.addForm.field }); const ok = [];
if (res.code === 0) { const fail = [];
this.$message.success(this.$t('crawlTask.addKeywordSuccess')); for (const field of uniqFields) {
if (this.addForm.runNow) { try {
await this.$api.post('api/expert_finder/fetchOneField', { field: this.addForm.field }); 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; } } finally { this.addLoading = false; }
} }
} }

View File

@@ -13,12 +13,24 @@
v-for="(item, index) in chanFerForm" v-for="(item, index) in chanFerForm"
:key="item.p_refer_id != null ? item.p_refer_id : `ref-row-${index}`" :key="item.p_refer_id != null ? item.p_refer_id : `ref-row-${index}`"
class="ref-item-row" class="ref-item-row"
:class="{ 'has-change': item.is_change == 1, 'is-repeat': item.is_repeat == 1 }" :class="{ 'has-change': item.is_change == 1, 'is-repeat': item.is_repeat == 1, 'uncited-row': isUncitedReference(item) }"
> >
<div class="ref-content-area"> <div class="ref-content-area">
<div v-if="item.refer_type == 'journal'" class="reference-item"> <div v-if="item.refer_type == 'journal'" class="reference-item">
<b class="ref-number-prefix">{{ index + 1 }}.</b> <template v-if="!isUncitedReference(item)">
<b class="ref-number-prefix">{{ index + 1 }}.</b>
</template>
<el-tag
v-else
class="uncited-tag"
size="mini"
type="info"
effect="plain"
style="margin-right: 8px"
>
{{ $t('wordCite.uncited') }}
</el-tag>
<span class="author-text">{{ item.author }} </span> <span class="author-text">{{ item.author }} </span>
<span v-html="formatTitle(item.title)"></span>. <em class="journal-name">{{ item.joura }}</em <span v-html="formatTitle(item.title)"></span>. <em class="journal-name">{{ item.joura }}</em
@@ -31,14 +43,38 @@
</div> </div>
<div v-if="item.refer_type == 'book'" class="reference-item"> <div v-if="item.refer_type == 'book'" class="reference-item">
<b class="ref-number-prefix">{{ index + 1 }}.</b> <template v-if="!isUncitedReference(item)">
<b class="ref-number-prefix">{{ index + 1 }}.</b>
</template>
<el-tag
v-else
class="uncited-tag"
size="mini"
type="info"
effect="plain"
style="margin-right: 8px"
>
{{ $t('wordCite.uncited') }}
</el-tag>
{{ item.author }} <span v-html="formatTitle(item.title)"></span>. {{ item.dateno }}. {{ item.author }} <span v-html="formatTitle(item.title)"></span>. {{ item.dateno }}.
<br /> <br />
<a class="doiLink" :href="item.isbn" target="_blank">{{ item.isbn }}</a> <a class="doiLink" :href="item.isbn" target="_blank">{{ item.isbn }}</a>
</div> </div>
<div v-if="item.refer_type == 'other'" class="reference-item"> <div v-if="item.refer_type == 'other'" class="reference-item">
<b class="ref-number-prefix">{{ index + 1 }}.</b> <template v-if="!isUncitedReference(item)">
<b class="ref-number-prefix">{{ index + 1 }}.</b>
</template>
<el-tag
v-else
class="uncited-tag"
size="mini"
type="info"
effect="plain"
style="margin-right: 8px"
>
{{ $t('wordCite.uncited') }}
</el-tag>
<span v-html="formatTitle(item.refer_frag)"></span> <span v-html="formatTitle(item.refer_frag)"></span>
</div> </div>
</div> </div>
@@ -325,6 +361,11 @@ export default {
type: Array, type: Array,
required: true required: true
}, },
/** 父组件传入的正文首次出现顺序(被引用的 p_refer_id 列表);不在其中的视为“未引用” */
citeOrderFromParent: {
type: Array,
default: () => null
},
gridData: { gridData: {
type: String, type: String,
default: '' default: ''
@@ -340,6 +381,13 @@ export default {
} }
}, },
methods: { methods: {
isUncitedReference(row) {
const id = row && row.p_refer_id != null ? String(row.p_refer_id) : '';
if (!id) return false;
const order = Array.isArray(this.citeOrderFromParent) ? this.citeOrderFromParent.map(String) : [];
if (!order.length) return false;
return !order.includes(id);
},
/** 与当前 chanFerForm 数组顺序一致:行内 index 置为 1-based 序号(排序/正文重排后同步) */ /** 与当前 chanFerForm 数组顺序一致:行内 index 置为 1-based 序号(排序/正文重排后同步) */
syncReferenceRowIndices() { syncReferenceRowIndices() {
const list = this.chanFerForm; const list = this.chanFerForm;
@@ -1961,6 +2009,25 @@ export default {
background-color: #f9fbff; background-color: #f9fbff;
} }
.uncited-row .ref-content-area,
.uncited-row .reference-item,
.uncited-row .author-text,
.uncited-row em,
.uncited-row span {
color: #909399 !important;
}
.uncited-row .doiLink,
.uncited-row .doi-link-text {
color: #909399 !important;
}
::v-deep .uncited-tag.el-tag {
background-color: #f56c6c !important;
border-color: #f56c6c !important;
color: #fff !important;
}
/* 序号与状态 */ /* 序号与状态 */
.ref-index-area { .ref-index-area {
width: 50px; width: 50px;

View File

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