tijiao
This commit is contained in:
@@ -19,8 +19,8 @@ const service = axios.create({
|
|||||||
// baseURL: 'https://submission.tmrjournals.com/', //正式 记得切换
|
// baseURL: 'https://submission.tmrjournals.com/', //正式 记得切换
|
||||||
// baseURL: 'http://www.tougao.com/', //测试本地 记得切换
|
// baseURL: 'http://www.tougao.com/', //测试本地 记得切换
|
||||||
// baseURL: 'http://192.168.110.110/tougao/public/index.php/',
|
// baseURL: 'http://192.168.110.110/tougao/public/index.php/',
|
||||||
// baseURL: '/api', //本地
|
baseURL: '/api', //本地
|
||||||
baseURL: '/', //正式
|
// baseURL: '/', //正式
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -874,6 +874,30 @@ const en = {
|
|||||||
taskStoppedMsg: 'Crawl has been paused.',
|
taskStoppedMsg: 'Crawl has been paused.',
|
||||||
runOnceQueued: 'A one-off crawl has been queued.',
|
runOnceQueued: 'A one-off crawl has been queued.',
|
||||||
},
|
},
|
||||||
|
articleDetailEmail: {
|
||||||
|
pageTitle: 'Manuscript email detail',
|
||||||
|
sender: 'Sender :',
|
||||||
|
content: 'Content :',
|
||||||
|
attachment: 'Attachment :',
|
||||||
|
uploadTip: 'Only word file can be uploaded(.docx)',
|
||||||
|
clickUpload: 'Click Upload',
|
||||||
|
sendMail: 'Send mail',
|
||||||
|
templateSelection: 'Template selection',
|
||||||
|
letterLang: 'Email language',
|
||||||
|
langZh: '中文',
|
||||||
|
langEn: 'English',
|
||||||
|
greeting: 'Dear Dr. {name},',
|
||||||
|
signOff: 'Yours Sincerely',
|
||||||
|
officeLine: '{journal} | Editorial Office | New Zealand',
|
||||||
|
telephone: 'Telephone: +64 02108293806',
|
||||||
|
emailLine: 'Email: {email}',
|
||||||
|
subscribeLine: 'Subscribe to receive Latest Research and News from {journal}',
|
||||||
|
contentRequired: 'Please enter the message content!',
|
||||||
|
sendSuccess: 'Sent successfully!',
|
||||||
|
uploadError: 'upload error',
|
||||||
|
serviceError: 'service error: {msg}',
|
||||||
|
uploadLimit: 'The maximum number of uploaded files has been exceeded!'
|
||||||
|
},
|
||||||
mailboxSend: {
|
mailboxSend: {
|
||||||
title: 'Write mail',
|
title: 'Write mail',
|
||||||
to: 'To:',
|
to: 'To:',
|
||||||
@@ -1502,7 +1526,55 @@ const en = {
|
|||||||
previewWithVariablesHint: 'The expert data is an example, used for variable spelling check only.',
|
previewWithVariablesHint: 'The expert data is an example, used for variable spelling check only.',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
placeholder: 'Please enter email content'
|
placeholder: 'Please enter email content'
|
||||||
}
|
},
|
||||||
|
refRelevance: {
|
||||||
|
filterAll: 'All ({count})',
|
||||||
|
filterModify: 'Needs revision ({count})',
|
||||||
|
columnTitle: 'AI citation relevance review',
|
||||||
|
uncitedTag: 'Not cited in text',
|
||||||
|
uncitedDesc: 'This reference appears in the list but no matching in-text citation was found.',
|
||||||
|
uncitedTip: 'Remove it from the reference list or add an in-text citation in the manuscript.',
|
||||||
|
citationN: 'Cite {n}',
|
||||||
|
relevancePct: 'Relevance {score}%',
|
||||||
|
relevancePctShort: '{score}%',
|
||||||
|
viewAiAnalysis: 'View AI analysis →',
|
||||||
|
viewAiAnalysisShort: 'AI →',
|
||||||
|
dialogTitle: 'Citation relevance details',
|
||||||
|
prevRef: 'Previous reference',
|
||||||
|
nextRef: 'Next reference',
|
||||||
|
manuscript: 'Manuscript',
|
||||||
|
excerpt: '(excerpt)',
|
||||||
|
citeListTitle: 'This reference · all in-text citations',
|
||||||
|
citeListTotal: '{n} citation(s)',
|
||||||
|
current: 'Current',
|
||||||
|
close: 'Close',
|
||||||
|
emptyModify: 'No references need revision',
|
||||||
|
locatorMark: 'Highlighted in-text marker {mark}',
|
||||||
|
locatorMarkRef: 'Highlighted marker {mark} (reference [{ref}])',
|
||||||
|
locatorMarkOnly: 'Highlighted citation marker {mark}',
|
||||||
|
summaryModify: '{total} cite(s) · {modify} revise',
|
||||||
|
summaryOk: '{total} cite(s) · OK',
|
||||||
|
insightTitle: 'Context review',
|
||||||
|
insightTitleRef: 'Context review [{ref}]',
|
||||||
|
recommendAction: 'Editorial note',
|
||||||
|
recommendRevise: 'Suggested revision',
|
||||||
|
recommendKeep: 'Keep as is',
|
||||||
|
recommendTextRevise: '{brief} Review the passage at left before revising or removing this citation.',
|
||||||
|
recommendTextReviseDefault: 'Review the passage at left before revising or removing this citation.',
|
||||||
|
recommendTextKeep: 'This citation strongly supports the argument; no change recommended.',
|
||||||
|
revisionLabel: 'Revision',
|
||||||
|
citeTag: 'Citation {idx}',
|
||||||
|
citeTagTotal: 'Citation {idx}/{total}',
|
||||||
|
briefSuggestRevise: 'Revise',
|
||||||
|
briefOk: 'OK',
|
||||||
|
briefAppropriate: 'OK',
|
||||||
|
briefKeep: 'Keep',
|
||||||
|
briefTrim: 'Revise',
|
||||||
|
briefWeakEvidence: 'Peripheral',
|
||||||
|
briefNeedDiff: 'Clarify',
|
||||||
|
briefDelete: 'Drop',
|
||||||
|
briefYearMismatch: 'Year?'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -619,6 +619,30 @@ const zh = {
|
|||||||
saveFail: '保存失败',
|
saveFail: '保存失败',
|
||||||
saveSuccessMock: '模板已保存(模拟)',
|
saveSuccessMock: '模板已保存(模拟)',
|
||||||
},
|
},
|
||||||
|
articleDetailEmail: {
|
||||||
|
pageTitle: '稿件邮件详情',
|
||||||
|
sender: '发件人:',
|
||||||
|
content: '正文:',
|
||||||
|
attachment: '附件:',
|
||||||
|
uploadTip: '仅支持上传 Word 文件(.docx)',
|
||||||
|
clickUpload: '点击上传',
|
||||||
|
sendMail: '发送邮件',
|
||||||
|
templateSelection: '模板选择',
|
||||||
|
letterLang: '邮件语言',
|
||||||
|
langZh: '中文',
|
||||||
|
langEn: 'English',
|
||||||
|
greeting: '尊敬的 Dr. {name}:',
|
||||||
|
signOff: '此致敬礼',
|
||||||
|
officeLine: '{journal} | 编辑部 | 新西兰',
|
||||||
|
telephone: '电话:+64 02108293806',
|
||||||
|
emailLine: '邮箱:{email}',
|
||||||
|
subscribeLine: '订阅 {journal} 最新研究与资讯',
|
||||||
|
contentRequired: '请输入邮件正文内容!',
|
||||||
|
sendSuccess: '发送成功!',
|
||||||
|
uploadError: '上传失败',
|
||||||
|
serviceError: '服务错误:{msg}',
|
||||||
|
uploadLimit: '已超过可上传文件数量上限!'
|
||||||
|
},
|
||||||
mailboxCollect: {
|
mailboxCollect: {
|
||||||
inboxTab: '收件箱',
|
inboxTab: '收件箱',
|
||||||
outboxTab: '发件箱',
|
outboxTab: '发件箱',
|
||||||
@@ -1483,7 +1507,55 @@ const zh = {
|
|||||||
previewWithVariablesHint: '专家数据仅为示例,仅用于变量拼写检查。',
|
previewWithVariablesHint: '专家数据仅为示例,仅用于变量拼写检查。',
|
||||||
close: '关闭',
|
close: '关闭',
|
||||||
placeholder: '请输入邮件内容'
|
placeholder: '请输入邮件内容'
|
||||||
}
|
},
|
||||||
|
refRelevance: {
|
||||||
|
filterAll: '全部({count})',
|
||||||
|
filterModify: '需修改({count})',
|
||||||
|
columnTitle: 'AI 智能评定相关性建议',
|
||||||
|
uncitedTag: '正文未引用',
|
||||||
|
uncitedDesc: '该文献已列入参考文献列表,但正文中未找到对应的引用上标。',
|
||||||
|
uncitedTip: '建议:从参考文献列表中删除,或在正文中补充引用。',
|
||||||
|
citationN: '第{n}处',
|
||||||
|
relevancePct: '相关性 {score}%',
|
||||||
|
relevancePctShort: '{score}%',
|
||||||
|
viewAiAnalysis: '查看 AI 分析 →',
|
||||||
|
viewAiAnalysisShort: 'AI分析→',
|
||||||
|
dialogTitle: '相关性检测详情',
|
||||||
|
prevRef: '上一篇参考文献',
|
||||||
|
nextRef: '下一篇参考文献',
|
||||||
|
manuscript: '稿件',
|
||||||
|
excerpt: '(摘录)',
|
||||||
|
citeListTitle: '本参考文献 · 全部引用处',
|
||||||
|
citeListTotal: '共 {n} 处',
|
||||||
|
current: '当前',
|
||||||
|
close: '关闭',
|
||||||
|
emptyModify: '暂无需要修改的参考文献',
|
||||||
|
locatorMark: '高亮段落中的引用标记 {mark}',
|
||||||
|
locatorMarkRef: '高亮标记 {mark}(对应参考文献 [{ref}])',
|
||||||
|
locatorMarkOnly: '高亮引用标记 {mark}',
|
||||||
|
summaryModify: '{total}处引用·{modify}处待改',
|
||||||
|
summaryOk: '{total}处引用·均可保留',
|
||||||
|
insightTitle: '语境深度批核',
|
||||||
|
insightTitleRef: '语境深度批核 「[{ref}]」',
|
||||||
|
recommendAction: '操作建议',
|
||||||
|
recommendRevise: '建议修订引用',
|
||||||
|
recommendKeep: '建议完美保留',
|
||||||
|
recommendTextRevise: '{brief}。请根据左侧正文核对后决定是否删改或改写该处引用。',
|
||||||
|
recommendTextReviseDefault: '请根据左侧正文核对后决定是否删改或改写该处引用。',
|
||||||
|
recommendTextKeep: '与段落论述高度一致,有力支撑核心论点,建议不作修改保留该引用。',
|
||||||
|
revisionLabel: '修订建议',
|
||||||
|
citeTag: '引用 {idx}',
|
||||||
|
citeTagTotal: '引用 {idx}/{total}',
|
||||||
|
briefSuggestRevise: '待改',
|
||||||
|
briefOk: '恰当',
|
||||||
|
briefAppropriate: '恰当',
|
||||||
|
briefKeep: '保留',
|
||||||
|
briefTrim: '宜改写',
|
||||||
|
briefWeakEvidence: '非主依据',
|
||||||
|
briefNeedDiff: '需说明差异',
|
||||||
|
briefDelete: '删除',
|
||||||
|
briefYearMismatch: '年份不符'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -999,6 +999,10 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="plagiarism-sim-date">{{ formatPlagiarismDate(row) }}</span>
|
<span class="plagiarism-sim-date">{{ formatPlagiarismDate(row) }}</span>
|
||||||
<span class="plagiarism-sim-state" :class="getPlagiarismStateClass(row)">
|
<span class="plagiarism-sim-state" :class="getPlagiarismStateClass(row)">
|
||||||
|
<i
|
||||||
|
v-if="isPlagiarismStateLoading(row)"
|
||||||
|
class="el-icon-loading plagiarism-state-loading-icon"
|
||||||
|
></i>
|
||||||
{{ formatPlagiarismStateLabel(row) }}
|
{{ formatPlagiarismStateLabel(row) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="plagiarism-sim-report">
|
<span class="plagiarism-sim-report">
|
||||||
@@ -1012,7 +1016,11 @@
|
|||||||
{{ $t('articleListEditor.plagiarismPreviewPdf') }}
|
{{ $t('articleListEditor.plagiarismPreviewPdf') }}
|
||||||
<i class="el-icon-link"></i>
|
<i class="el-icon-link"></i>
|
||||||
</a>
|
</a>
|
||||||
<span v-else class="plagiarism-check-no-pdf">{{ $t('articleListEditor.plagiarismNoPdfLink') }}</span>
|
<span
|
||||||
|
v-else-if="!isPlagiarismStateLoading(row)"
|
||||||
|
class="plagiarism-check-no-pdf"
|
||||||
|
>{{ $t('articleListEditor.plagiarismNoPdfLink') }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2973,6 +2981,15 @@ export default {
|
|||||||
closeResubmit() {
|
closeResubmit() {
|
||||||
(this.resubmitVisible = false), this.$refs['resubmitJournal'].resetFields();
|
(this.resubmitVisible = false), this.$refs['resubmitJournal'].resetFields();
|
||||||
},
|
},
|
||||||
|
/** 查重列表静默轮询间隔(毫秒) */
|
||||||
|
getPlagiarismPollIntervalMs() {
|
||||||
|
return 60 * 1000;
|
||||||
|
},
|
||||||
|
/** 无「进行中」记录时视为已结束,停止轮询(state 1=进行中,2/3=完成,4/5=失败) */
|
||||||
|
shouldStopPlagiarismPolling(list) {
|
||||||
|
if (!Array.isArray(list) || !list.length) return false;
|
||||||
|
return !list.some((row) => Number(row && row.state) === 1);
|
||||||
|
},
|
||||||
startPlagiarismPolling(resetList) {
|
startPlagiarismPolling(resetList) {
|
||||||
this.stopPlagiarismPolling();
|
this.stopPlagiarismPolling();
|
||||||
if (resetList !== false) {
|
if (resetList !== false) {
|
||||||
@@ -2982,7 +2999,7 @@ export default {
|
|||||||
this.fetchPlagiarismList(false);
|
this.fetchPlagiarismList(false);
|
||||||
this.plagiarismPollTimer = setInterval(() => {
|
this.plagiarismPollTimer = setInterval(() => {
|
||||||
this.fetchPlagiarismList(false);
|
this.fetchPlagiarismList(false);
|
||||||
}, 3 * 60 * 1000);
|
}, this.getPlagiarismPollIntervalMs());
|
||||||
},
|
},
|
||||||
stopPlagiarismPolling() {
|
stopPlagiarismPolling() {
|
||||||
if (this.plagiarismPollTimer) {
|
if (this.plagiarismPollTimer) {
|
||||||
@@ -2998,10 +3015,11 @@ export default {
|
|||||||
}
|
}
|
||||||
this.plagiarismSubmitLoading = true;
|
this.plagiarismSubmitLoading = true;
|
||||||
try {
|
try {
|
||||||
const res = await this.$api.post('api/Plagiarism/submit', { article_id: articleId });
|
const res = await this.$api.post('api/Plagiarism/submit', { article_id: articleId ,type:'body_only'});
|
||||||
if (res && Number(res.code) === 0) {
|
if (res && Number(res.code) === 0) {
|
||||||
this.$message.success((res && res.msg) || this.$t('articleListEditor.plagiarismChecking'));
|
this.$message.success((res && res.msg) || this.$t('articleListEditor.plagiarismChecking'));
|
||||||
await this.fetchPlagiarismList(true);
|
await this.fetchPlagiarismList(true);
|
||||||
|
this.startPlagiarismPolling(false);
|
||||||
} else {
|
} else {
|
||||||
this.$message.error((res && res.msg) || this.$t('articleListEditor.plagiarismCheckFailed'));
|
this.$message.error((res && res.msg) || this.$t('articleListEditor.plagiarismCheckFailed'));
|
||||||
}
|
}
|
||||||
@@ -3026,6 +3044,9 @@ export default {
|
|||||||
const payload = res.data || {};
|
const payload = res.data || {};
|
||||||
const list = Array.isArray(payload.list) ? payload.list : Array.isArray(payload) ? payload : [];
|
const list = Array.isArray(payload.list) ? payload.list : Array.isArray(payload) ? payload : [];
|
||||||
this.plagiarismList = list;
|
this.plagiarismList = list;
|
||||||
|
if (!manual && this.shouldStopPlagiarismPolling(list)) {
|
||||||
|
this.stopPlagiarismPolling();
|
||||||
|
}
|
||||||
} else if (manual) {
|
} else if (manual) {
|
||||||
this.$message.error((res && res.msg) || this.$t('articleListEditor.plagiarismStatusFailed'));
|
this.$message.error((res && res.msg) || this.$t('articleListEditor.plagiarismStatusFailed'));
|
||||||
}
|
}
|
||||||
@@ -3058,6 +3079,13 @@ export default {
|
|||||||
if (s === 4 || s === 5) return 'state-fail';
|
if (s === 4 || s === 5) return 'state-fail';
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
/** 上传中 / 比对中等进行中状态显示加载图标 */
|
||||||
|
isPlagiarismStateLoading(row) {
|
||||||
|
if (!row || typeof row !== 'object') return false;
|
||||||
|
if (Number(row.state) === 1) return true;
|
||||||
|
const label = String(row.state_label || row.stateLabel || '').trim();
|
||||||
|
return /上传中|比对中/i.test(label);
|
||||||
|
},
|
||||||
getPlagiarismSimilarityScore(row) {
|
getPlagiarismSimilarityScore(row) {
|
||||||
if (!row || typeof row !== 'object') return null;
|
if (!row || typeof row !== 'object') return null;
|
||||||
const raw = row.similarity_score != null ? row.similarity_score : row.similarity;
|
const raw = row.similarity_score != null ? row.similarity_score : row.similarity;
|
||||||
@@ -3338,13 +3366,24 @@ export default {
|
|||||||
min-width: 48px;
|
min-width: 48px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.plagiarism-state-loading-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
.plagiarism-sim-state.state-uploading {
|
.plagiarism-sim-state.state-uploading {
|
||||||
color: #e6a23c;
|
color: #e6a23c;
|
||||||
}
|
}
|
||||||
|
.plagiarism-sim-state.state-uploading .plagiarism-state-loading-icon {
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
.plagiarism-sim-state.state-done {
|
.plagiarism-sim-state.state-done {
|
||||||
color: #67c23a;
|
color: #67c23a;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="crumbs">
|
<div class="crumbs">
|
||||||
<el-breadcrumb separator="/">
|
<el-breadcrumb separator="/">
|
||||||
<el-breadcrumb-item>
|
<el-breadcrumb-item>
|
||||||
<i class="el-icon-lx-calendar"></i> Manuscript email detail
|
<i class="el-icon-lx-calendar"></i> {{ $t('articleDetailEmail.pageTitle') }}
|
||||||
</el-breadcrumb-item>
|
</el-breadcrumb-item>
|
||||||
</el-breadcrumb>
|
</el-breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
@@ -11,39 +11,46 @@
|
|||||||
<el-row :gutter="30">
|
<el-row :gutter="30">
|
||||||
<el-col :span="16">
|
<el-col :span="16">
|
||||||
<el-form :model="EmailData" label-width="110px" class="Email_Data">
|
<el-form :model="EmailData" label-width="110px" class="Email_Data">
|
||||||
<el-form-item label="Sender :">
|
<el-form-item :label="$t('articleDetailEmail.sender')">
|
||||||
<el-input v-model="AuthorMes.email"></el-input>
|
<el-input v-model="AuthorMes.email"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Content :">
|
<el-form-item :label="$t('articleDetailEmail.content')">
|
||||||
|
<div class="email-letter-lang">
|
||||||
|
<span class="email-letter-lang-label">{{ $t('articleDetailEmail.letterLang') }}</span>
|
||||||
|
<el-radio-group v-model="emailLetterLang" size="small" @change="applyEmailLetterBlocks">
|
||||||
|
<el-radio-button label="zh">{{ $t('articleDetailEmail.langZh') }}</el-radio-button>
|
||||||
|
<el-radio-button label="en">{{ $t('articleDetailEmail.langEn') }}</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
<p v-html="EmailData.topmail"></p>
|
<p v-html="EmailData.topmail"></p>
|
||||||
<p v-html="EmailData.articleInfor"></p>
|
<p v-html="EmailData.articleInfor"></p>
|
||||||
<el-input type="textarea" rows="9" v-model="EmailData.substance" @input="btn_ft=false">
|
<el-input type="textarea" rows="9" v-model="EmailData.substance" @input="btn_ft=false">
|
||||||
</el-input>
|
</el-input>
|
||||||
<p v-html="EmailData.bottomail"></p>
|
<p v-html="EmailData.bottomail"></p>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Attachment :">
|
<el-form-item :label="$t('articleDetailEmail.attachment')">
|
||||||
<el-upload style="display: inline-block;" class="upload-demo" :action="upload_enclosure"
|
<el-upload style="display: inline-block;" class="upload-demo" :action="upload_enclosure"
|
||||||
accept=".docx," name="enclosure" :before-upload="beforeupload_enclosure"
|
accept=".docx," name="enclosure" :before-upload="beforeupload_enclosure"
|
||||||
:on-error="uperr_enclosure" :on-success="upSuccess_enclosure" :limit="1"
|
:on-error="uperr_enclosure" :on-success="upSuccess_enclosure" :limit="1"
|
||||||
:on-exceed="alertlimit" :on-remove="removefilenclosure">
|
:on-exceed="alertlimit" :on-remove="removefilenclosure">
|
||||||
<div class="el-upload__text" style="padding:0 5px;">
|
<div class="el-upload__text" style="padding:0 5px;">
|
||||||
<em>Click Upload</em>
|
<em>{{ $t('articleDetailEmail.clickUpload') }}</em>
|
||||||
</div>
|
</div>
|
||||||
<div class="el-upload__tip" slot="tip">Only word file can be uploaded(.docx)</div>
|
<div class="el-upload__tip" slot="tip">{{ $t('articleDetailEmail.uploadTip') }}</div>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-button type="primary" @click="sendData" :disabled="btn_ft"
|
<el-button type="primary" @click="sendData" :disabled="btn_ft"
|
||||||
style="float: right;margin-top: -20px;">Send mail
|
style="float: right;margin-top: -20px;">{{ $t('articleDetailEmail.sendMail') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<div class="muban_list">
|
<div class="muban_list">
|
||||||
<p>Template selection</p>
|
<p>{{ $t('articleDetailEmail.templateSelection') }}</p>
|
||||||
<el-collapse v-model="activeNames" default-expand-all>
|
<el-collapse v-model="activeNames" default-expand-all>
|
||||||
<el-collapse-item v-for="(itemC, indexC) in mubanList" :title="itemC.etitle"
|
<el-collapse-item v-for="(itemC, indexC) in mubanList" :title="itemC.etitle"
|
||||||
:name="itemC.eid">
|
:name="itemC.eid" :key="itemC.eid || indexC">
|
||||||
<div class="sel_muban" v-for="(item, index) in itemC.children">
|
<div class="sel_muban" v-for="(item, index) in itemC.children" :key="index">
|
||||||
<i class="header-icon el-icon-circle-plus" @click="add_muban(item)"></i>
|
<i class="header-icon el-icon-circle-plus" @click="add_muban(item)"></i>
|
||||||
{{item.econtent}}
|
{{item.econtent}}
|
||||||
<span style="color: #999;margin-left: 10px;">({{item.num}})</span>
|
<span style="color: #999;margin-left: 10px;">({{item.num}})</span>
|
||||||
@@ -62,11 +69,20 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
const savedLang = localStorage.getItem('langs');
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
baseUrl: this.Common.baseUrl,
|
baseUrl: this.Common.baseUrl,
|
||||||
articleId: this.$route.query.id,
|
articleId: this.$route.query.id,
|
||||||
AuthorMes: {},
|
AuthorMes: {},
|
||||||
|
emailLetterLang: savedLang === 'zh' ? 'zh' : 'en',
|
||||||
|
authorDisplayName: '',
|
||||||
|
journalMeta: {
|
||||||
|
title: '',
|
||||||
|
email: '',
|
||||||
|
website: '',
|
||||||
|
issn: ''
|
||||||
|
},
|
||||||
EmailData: {
|
EmailData: {
|
||||||
email: '',
|
email: '',
|
||||||
attachment: '',
|
attachment: '',
|
||||||
@@ -83,12 +99,52 @@
|
|||||||
created: function() {
|
created: function() {
|
||||||
this.getData();
|
this.getData();
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
'$i18n.locale'(val) {
|
||||||
|
if (val === 'zh' || val === 'en') {
|
||||||
|
this.emailLetterLang = val;
|
||||||
|
this.applyEmailLetterBlocks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
upload_enclosure: function() {
|
upload_enclosure: function() {
|
||||||
return this.baseUrl + 'api/Email/up_enclosure_file';
|
return this.baseUrl + 'api/Email/up_enclosure_file';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
letterT(key, params) {
|
||||||
|
const loc = this.emailLetterLang === 'zh' ? 'zh' : 'en';
|
||||||
|
return params ? this.$t('articleDetailEmail.' + key, loc, params) : this.$t('articleDetailEmail.' + key, loc);
|
||||||
|
},
|
||||||
|
applyEmailLetterBlocks() {
|
||||||
|
const name = this.authorDisplayName || '';
|
||||||
|
this.EmailData.topmail = this.letterT('greeting', { name }) + '<br/>';
|
||||||
|
const j = this.journalMeta;
|
||||||
|
if (!j.title) return;
|
||||||
|
const subscribeUrl = 'https://www.tmrjournals.com/draw_up.html?issn=' + (j.issn || '');
|
||||||
|
this.EmailData.bottomail =
|
||||||
|
'<br/>' +
|
||||||
|
this.letterT('signOff') +
|
||||||
|
'<br/>' +
|
||||||
|
this.letterT('officeLine', { journal: j.title }) +
|
||||||
|
'<br/>' +
|
||||||
|
this.letterT('telephone') +
|
||||||
|
'<br/>' +
|
||||||
|
this.letterT('emailLine', { email: j.email || '' }) +
|
||||||
|
'<br/>' +
|
||||||
|
(j.website || '') +
|
||||||
|
'<br/>' +
|
||||||
|
this.letterT('subscribeLine', { journal: j.title }) +
|
||||||
|
'<br/>' +
|
||||||
|
subscribeUrl;
|
||||||
|
},
|
||||||
|
setAuthorFromResponse(user) {
|
||||||
|
if (!user) return;
|
||||||
|
this.AuthorMes = user;
|
||||||
|
this.authorDisplayName = user.realname || user.name || '';
|
||||||
|
this.applyEmailLetterBlocks();
|
||||||
|
},
|
||||||
//获取数据
|
//获取数据
|
||||||
getData() {
|
getData() {
|
||||||
this.$api
|
this.$api
|
||||||
@@ -96,9 +152,6 @@
|
|||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
this.mubanList = res.data.templates;
|
this.mubanList = res.data.templates;
|
||||||
// for (let i in this.mubanList) {
|
|
||||||
// this.activeNames.push(this.mubanList[i].eid)
|
|
||||||
// }
|
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(res.msg);
|
this.$message.error(res.msg);
|
||||||
}
|
}
|
||||||
@@ -117,15 +170,13 @@ if(this.$route.query.user_id){
|
|||||||
.then(res => {
|
.then(res => {
|
||||||
if(this.$route.query.user_id){
|
if(this.$route.query.user_id){
|
||||||
if (res.status == 1) {
|
if (res.status == 1) {
|
||||||
this.AuthorMes = res.data.user;
|
this.setAuthorFromResponse(res.data.user);
|
||||||
this.EmailData.topmail = 'Dear Dr. ' + this.AuthorMes.realname + ',<br/>';
|
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(res.msg);
|
this.$message.error(res.msg);
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
this.AuthorMes = res.data.userDetail;
|
this.setAuthorFromResponse(res.data.userDetail);
|
||||||
this.EmailData.topmail = 'Dear Dr. ' + this.AuthorMes.realname + ',<br/>';
|
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(res.msg);
|
this.$message.error(res.msg);
|
||||||
}
|
}
|
||||||
@@ -156,13 +207,13 @@ if(this.$route.query.user_id){
|
|||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
let arry = res.data.journal;
|
let arry = res.data.journal;
|
||||||
this.EmailData.bottomail = '<br/>Yours Sincerely' +
|
this.journalMeta = {
|
||||||
'<br/>' + arry.title + ' | Editorial Office | New Zealand' +
|
title: arry.title || '',
|
||||||
'<br/>Telephone: +64 02108293806' +
|
email: arry.email || '',
|
||||||
'<br/>Email: ' + arry.email +
|
website: arry.website || '',
|
||||||
'<br/>' + arry.website +
|
issn: arry.issn || ''
|
||||||
'<br/>Subscribe to receive Latest Research and News from ' + arry.title +
|
};
|
||||||
'<br/>https://www.tmrjournals.com/draw_up.html?issn=' + arry.issn
|
this.applyEmailLetterBlocks();
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(res.msg);
|
this.$message.error(res.msg);
|
||||||
}
|
}
|
||||||
@@ -176,7 +227,7 @@ if(this.$route.query.user_id){
|
|||||||
// 发送邮件
|
// 发送邮件
|
||||||
sendData() {
|
sendData() {
|
||||||
if (this.EmailData.substance == '') {
|
if (this.EmailData.substance == '') {
|
||||||
this.$message.error('Please enter the message content!');
|
this.$message.error(this.$t('articleDetailEmail.contentRequired'));
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -191,7 +242,7 @@ if(this.$route.query.user_id){
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.btn_ft = true
|
this.btn_ft = true
|
||||||
this.$message.success('Sent successfully!');
|
this.$message.success(this.$t('articleDetailEmail.sendSuccess'));
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: 'articleDetailEmailist',
|
path: 'articleDetailEmailist',
|
||||||
query: {
|
query: {
|
||||||
@@ -229,20 +280,20 @@ if(this.$route.query.user_id){
|
|||||||
// 附件上传
|
// 附件上传
|
||||||
beforeupload_enclosure() {},
|
beforeupload_enclosure() {},
|
||||||
uperr_enclosure(err) {
|
uperr_enclosure(err) {
|
||||||
this.$message.error('upload error');
|
this.$message.error(this.$t('articleDetailEmail.uploadError'));
|
||||||
},
|
},
|
||||||
upSuccess_enclosure(res, file) {
|
upSuccess_enclosure(res, file) {
|
||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
this.EmailData.attachment = 'enclosure/' + res.upurl;
|
this.EmailData.attachment = 'enclosure/' + res.upurl;
|
||||||
} else {
|
} else {
|
||||||
this.$message.error('service error:' + res.msg);
|
this.$message.error(this.$t('articleDetailEmail.serviceError', { msg: res.msg }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removefilenclosure(file, fileList) {
|
removefilenclosure(file, fileList) {
|
||||||
this.EmailData.attachment = '';
|
this.EmailData.attachment = '';
|
||||||
},
|
},
|
||||||
alertlimit() {
|
alertlimit() {
|
||||||
this.$message.error('The maximum number of uploaded files has been exceeded!');
|
this.$message.error(this.$t('articleDetailEmail.uploadLimit'));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -254,6 +305,20 @@ if(this.$route.query.user_id){
|
|||||||
margin: 30px 0 0 0;
|
margin: 30px 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.email-letter-lang {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-letter-lang-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.Email_Data .el-textarea__inner {
|
.Email_Data .el-textarea__inner {
|
||||||
line-height: 26px;
|
line-height: 26px;
|
||||||
margin: 10px 0 -20px 0;
|
margin: 10px 0 -20px 0;
|
||||||
|
|||||||
@@ -59,22 +59,38 @@
|
|||||||
mailData.to_list.length
|
mailData.to_list.length
|
||||||
}}人
|
}}人
|
||||||
</div>
|
</div>
|
||||||
<div v-if="mailData.attachments && mailData.attachments.length" class="attachment-brief-bar">
|
|
||||||
<span class="brief-info">
|
|
||||||
{{ $t('mailboxCollect.totalAttachments', { count: mailData.attachments.length }) }}(
|
|
||||||
<i class="el-icon-document" style="color: #409EFF;"></i>
|
|
||||||
<span class="first-file-name">{{ mailData.attachments[0].name }}{{ mailData.attachments.length > 1 ? $t('mailboxCollect.etcSuffix') : '' }}</span>
|
|
||||||
)
|
|
||||||
|
|
||||||
</span>
|
|
||||||
<el-link type="primary" :underline="false" @click="scrollToAttachments" class="jump-link">
|
|
||||||
{{ $t('mailboxCollect.viewAttachments') }}
|
|
||||||
</el-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mail-body-content" v-html="mailBodyHtml"></div>
|
<div
|
||||||
|
v-if="mailData.attachments && mailData.attachments.length"
|
||||||
|
class="attachment-brief-bar"
|
||||||
|
>
|
||||||
|
<span class="brief-info">
|
||||||
|
{{ $t('mailboxCollect.totalAttachments', { count: mailData.attachments.length }) }}(
|
||||||
|
<i class="el-icon-document" style="color: #409EFF;"></i>
|
||||||
|
<span class="first-file-name">{{ mailData.attachments[0].name }}{{ mailData.attachments.length > 1 ? $t('mailboxCollect.etcSuffix') : '' }}</span>
|
||||||
|
)
|
||||||
|
</span>
|
||||||
|
<el-link type="primary" :underline="false" @click="scrollToAttachments" class="jump-link">
|
||||||
|
{{ $t('mailboxCollect.viewAttachments') }}
|
||||||
|
</el-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mail-body-wrap">
|
||||||
|
<iframe
|
||||||
|
v-if="mailIframeSrcdoc"
|
||||||
|
ref="mailHtmlFrame"
|
||||||
|
class="mail-html-iframe"
|
||||||
|
:srcdoc="mailIframeSrcdoc"
|
||||||
|
sandbox="allow-scripts allow-same-origin"
|
||||||
|
frameborder="0"
|
||||||
|
scrolling="no"
|
||||||
|
:style="mailIframeStyle"
|
||||||
|
@load="onMailIframeLoad"
|
||||||
|
></iframe>
|
||||||
|
<div v-else class="mail-body-content" v-html="mailBodyHtml"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="mailData.attachments && mailData.attachments.length" class="attachment-section">
|
<div v-if="mailData.attachments && mailData.attachments.length" class="attachment-section">
|
||||||
<div class="attachment-header">
|
<div class="attachment-header">
|
||||||
@@ -123,7 +139,16 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Common from '@/components/common/common';
|
import Common from '@/components/common/common';
|
||||||
import { normalizeEmailHtmlForInlineDisplay } from '@/utils/emailHtmlView';
|
import {
|
||||||
|
normalizeEmailHtmlForInlineDisplay,
|
||||||
|
buildEmailPreviewDocument,
|
||||||
|
isRichHtmlEmail,
|
||||||
|
unescapeHtmlEntities,
|
||||||
|
resizeIframeToContent,
|
||||||
|
measureHtmlContentHeight,
|
||||||
|
sanitizeMailPreviewHeight,
|
||||||
|
MAIL_PREVIEW_HEIGHT_MSG
|
||||||
|
} from '@/utils/emailHtmlView';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import FilePreviewDialog from './FilePreviewDialog.vue';
|
import FilePreviewDialog from './FilePreviewDialog.vue';
|
||||||
@@ -152,19 +177,61 @@ export default {
|
|||||||
mediaUrl: Common.mediaUrl,
|
mediaUrl: Common.mediaUrl,
|
||||||
isDetailExpanded: false,
|
isDetailExpanded: false,
|
||||||
downloadingIndex: -1,
|
downloadingIndex: -1,
|
||||||
packingAll: false
|
packingAll: false,
|
||||||
|
mailIframeHeight: 120,
|
||||||
|
_iframeResizeObserver: null,
|
||||||
|
_iframeResizeTimers: [],
|
||||||
|
_mailPreviewMessageHandler: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this._mailPreviewMessageHandler = (e) => this.handleMailPreviewMessage(e);
|
||||||
|
window.addEventListener('message', this._mailPreviewMessageHandler);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this._mailPreviewMessageHandler) {
|
||||||
|
window.removeEventListener('message', this._mailPreviewMessageHandler);
|
||||||
|
this._mailPreviewMessageHandler = null;
|
||||||
|
}
|
||||||
|
this.teardownIframeResize();
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
totalAttachmentSize() {
|
totalAttachmentSize() {
|
||||||
if (!this.mailData.attachments || !this.mailData.attachments.length) return '0B';
|
if (!this.mailData.attachments || !this.mailData.attachments.length) return '0B';
|
||||||
const total = this.mailData.attachments.reduce((sum, f) => sum + (Number(f.size) || 0), 0);
|
const total = this.mailData.attachments.reduce((sum, f) => sum + (Number(f.size) || 0), 0);
|
||||||
return this.formatFileSize(total);
|
return this.formatFileSize(total);
|
||||||
},
|
},
|
||||||
|
mailRawHtml() {
|
||||||
|
const m = this.mailData || {};
|
||||||
|
const html =
|
||||||
|
m.content_html ||
|
||||||
|
m.body_html ||
|
||||||
|
m.html ||
|
||||||
|
m.body ||
|
||||||
|
m.content ||
|
||||||
|
'';
|
||||||
|
if (html && String(html).trim()) return html;
|
||||||
|
const text = m.content_text || '';
|
||||||
|
if (isRichHtmlEmail(text)) return text;
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
mailIframeSrcdoc() {
|
||||||
|
if (!isRichHtmlEmail(this.mailRawHtml)) return '';
|
||||||
|
return buildEmailPreviewDocument(this.mailRawHtml) || '';
|
||||||
|
},
|
||||||
|
mailIframeStyle() {
|
||||||
|
const h = Math.max(Number(this.mailIframeHeight) || 0, 80);
|
||||||
|
return {
|
||||||
|
height: h + 'px',
|
||||||
|
minHeight: '80px',
|
||||||
|
maxHeight: 'none'
|
||||||
|
};
|
||||||
|
},
|
||||||
/** 正文:兼容 content_html / body / html,纯文本时包一层 pre */
|
/** 正文:兼容 content_html / body / html,纯文本时包一层 pre */
|
||||||
mailBodyHtml() {
|
mailBodyHtml() {
|
||||||
|
if (this.mailIframeSrcdoc) return '';
|
||||||
const m = this.mailData || {};
|
const m = this.mailData || {};
|
||||||
let raw = m.content_html || m.body_html || m.html || m.body || m.content || '';
|
let raw = unescapeHtmlEntities(this.mailRawHtml);
|
||||||
raw = normalizeEmailHtmlForInlineDisplay(raw);
|
raw = normalizeEmailHtmlForInlineDisplay(raw);
|
||||||
if (raw) return raw;
|
if (raw) return raw;
|
||||||
const text = m.content_text;
|
const text = m.content_text;
|
||||||
@@ -174,7 +241,109 @@ export default {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
mailIframeSrcdoc() {
|
||||||
|
this.teardownIframeResize();
|
||||||
|
this.mailIframeHeight = 120;
|
||||||
|
this.$nextTick(() => this.onMailIframeLoad());
|
||||||
|
},
|
||||||
|
mailRawHtml() {
|
||||||
|
this.mailIframeHeight = 120;
|
||||||
|
this.teardownIframeResize();
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleMailPreviewMessage(e) {
|
||||||
|
if (!e || !e.data || e.data.type !== MAIL_PREVIEW_HEIGHT_MSG) return;
|
||||||
|
const frame = this.$refs.mailHtmlFrame;
|
||||||
|
if (
|
||||||
|
frame &&
|
||||||
|
frame.contentWindow &&
|
||||||
|
e.source &&
|
||||||
|
e.source !== frame.contentWindow
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const h = sanitizeMailPreviewHeight(e.data.height);
|
||||||
|
if (h > 0) {
|
||||||
|
this.mailIframeHeight = h;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
measureMailIframeOffscreen() {
|
||||||
|
const panel = this.$el && this.$el.querySelector('.detail-scroll-content');
|
||||||
|
const w = panel ? Math.max(panel.clientWidth - 48, 280) : 640;
|
||||||
|
const h = sanitizeMailPreviewHeight(measureHtmlContentHeight(this.mailRawHtml, w));
|
||||||
|
if (h > 0) {
|
||||||
|
this.mailIframeHeight = Math.max(this.mailIframeHeight, h);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
teardownIframeResize() {
|
||||||
|
if (this._iframeResizeObserver) {
|
||||||
|
this._iframeResizeObserver.disconnect();
|
||||||
|
this._iframeResizeObserver = null;
|
||||||
|
}
|
||||||
|
if (this._iframeResizeTimers && this._iframeResizeTimers.length) {
|
||||||
|
this._iframeResizeTimers.forEach((id) => clearTimeout(id));
|
||||||
|
this._iframeResizeTimers = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resizeMailIframe() {
|
||||||
|
const frame = this.$refs.mailHtmlFrame;
|
||||||
|
if (!frame) return;
|
||||||
|
try {
|
||||||
|
const h = sanitizeMailPreviewHeight(resizeIframeToContent(frame));
|
||||||
|
if (h > 0) {
|
||||||
|
this.mailIframeHeight = h;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* 无法访问 iframe 文档时保持当前高度 */
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setupIframeResizeObserver() {
|
||||||
|
if (this._iframeResizeObserver) {
|
||||||
|
this._iframeResizeObserver.disconnect();
|
||||||
|
this._iframeResizeObserver = null;
|
||||||
|
}
|
||||||
|
const frame = this.$refs.mailHtmlFrame;
|
||||||
|
if (!frame || typeof ResizeObserver === 'undefined') return;
|
||||||
|
try {
|
||||||
|
const doc = frame.contentDocument;
|
||||||
|
if (!doc || !doc.body) return;
|
||||||
|
const ro = new ResizeObserver(() => this.resizeMailIframe());
|
||||||
|
ro.observe(doc.body);
|
||||||
|
if (doc.documentElement) ro.observe(doc.documentElement);
|
||||||
|
this._iframeResizeObserver = ro;
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bindIframeImageLoad() {
|
||||||
|
const frame = this.$refs.mailHtmlFrame;
|
||||||
|
if (!frame) return;
|
||||||
|
try {
|
||||||
|
const doc = frame.contentDocument;
|
||||||
|
if (!doc) return;
|
||||||
|
doc.querySelectorAll('img').forEach((img) => {
|
||||||
|
if (img.complete) return;
|
||||||
|
img.addEventListener('load', () => this.resizeMailIframe());
|
||||||
|
img.addEventListener('error', () => this.resizeMailIframe());
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMailIframeLoad() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.resizeMailIframe();
|
||||||
|
this.measureMailIframeOffscreen();
|
||||||
|
this.setupIframeResizeObserver();
|
||||||
|
this.bindIframeImageLoad();
|
||||||
|
const delays = [100, 400, 1000, 2000];
|
||||||
|
this._iframeResizeTimers = delays.map((ms) =>
|
||||||
|
setTimeout(() => this.resizeMailIframe(), ms)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
escapeHtml(text) {
|
escapeHtml(text) {
|
||||||
if (text == null) return '';
|
if (text == null) return '';
|
||||||
return String(text)
|
return String(text)
|
||||||
@@ -453,6 +622,17 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
|||||||
color: #909399;
|
color: #909399;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 完整 HTML 邮件 iframe:高度由 JS 设为固定 px,禁止 height:auto */
|
||||||
|
.mail-html-iframe {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 120px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
border: 0;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/* 正文 */
|
/* 正文 */
|
||||||
.mail-body-content {
|
.mail-body-content {
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
@@ -460,6 +640,30 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
|||||||
color: #303133;
|
color: #303133;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
margin-bottom: 50px;
|
margin-bottom: 50px;
|
||||||
|
overflow-x: auto;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
/* 营销邮件在窄容器内常被 @media 规则隐藏 desktop-only,预览强制展示 */
|
||||||
|
.mail-body-content >>> .desktop-only,
|
||||||
|
.mail-body-content >>> .mobile-only {
|
||||||
|
display: block !important;
|
||||||
|
max-height: none !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
.mail-body-content >>> .imgDesk {
|
||||||
|
display: block !important;
|
||||||
|
max-height: none !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
.mail-body-content >>> .imgMobile {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.mail-body-content >>> table {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
.mail-body-content >>> img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* v-html 注入的正文无 scoped 属性,用 deep 命中内部的 pre */
|
/* v-html 注入的正文无 scoped 属性,用 deep 命中内部的 pre */
|
||||||
@@ -664,26 +868,37 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
|||||||
color: #0052d9;
|
color: #0052d9;
|
||||||
}
|
}
|
||||||
.attachment-brief-bar {
|
.attachment-brief-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 0;
|
justify-content: space-between;
|
||||||
font-size: 13px;
|
width: 100%;
|
||||||
color: #606266;
|
box-sizing: border-box;
|
||||||
border-top: 1px solid #ebeef5;
|
padding: 10px 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
}
|
}
|
||||||
.attachment-brief-bar .brief-info {
|
.attachment-brief-bar .brief-info {
|
||||||
|
flex: 1;
|
||||||
}
|
min-width: 0;
|
||||||
.attachment-brief-bar .first-file-name {
|
overflow: hidden;
|
||||||
color: #909399;
|
text-overflow: ellipsis;
|
||||||
margin-left: 4px;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.jump-link {
|
.attachment-brief-bar .first-file-name {
|
||||||
margin-left: 15px;
|
color: #909399;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
.attachment-brief-bar .jump-link {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 16px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
.mail-body-wrap {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
.attachment-section {
|
.attachment-section {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -172,7 +172,11 @@ const API = {
|
|||||||
getAllJournal: 'api/Article/getJournal'
|
getAllJournal: 'api/Article/getJournal'
|
||||||
};
|
};
|
||||||
import MailDetail from '../../components/page/components/email/MailDetail.vue';
|
import MailDetail from '../../components/page/components/email/MailDetail.vue';
|
||||||
import { normalizeEmailHtmlForInlineDisplay } from '@/utils/emailHtmlView';
|
import {
|
||||||
|
normalizeEmailHtmlForInlineDisplay,
|
||||||
|
isRichHtmlEmail,
|
||||||
|
unescapeHtmlEntities
|
||||||
|
} from '@/utils/emailHtmlView';
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -551,20 +555,31 @@ fetchLatestSingleMail(jEmailId, journalId) {
|
|||||||
has_attachment: (item && item.has_attachment) || 0
|
has_attachment: (item && item.has_attachment) || 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const html = d.content_html || d.body_html || d.html || d.body || '';
|
let html = d.content_html || d.body_html || d.html || d.body || '';
|
||||||
const text = d.content_text || '';
|
const text = d.content_text || '';
|
||||||
|
if (!html && isRichHtmlEmail(text)) {
|
||||||
|
html = text;
|
||||||
|
}
|
||||||
let content_html =
|
let content_html =
|
||||||
html ||
|
html ||
|
||||||
(text ? `<pre class="mail-plain-pre">${this.escapeHtml(text)}</pre>` : '') ||
|
(text ? `<pre class="mail-plain-pre">${this.escapeHtml(text)}</pre>` : '') ||
|
||||||
listSnippet;
|
listSnippet;
|
||||||
if (content_html && typeof content_html === 'string' && /^<!DOCTYPE|^<\s*html[\s>]/i.test(content_html.trim())) {
|
content_html = unescapeHtmlEntities(content_html);
|
||||||
const normalized = normalizeEmailHtmlForInlineDisplay(content_html);
|
if (isRichHtmlEmail(content_html) || isRichHtmlEmail(html)) {
|
||||||
if (normalized && normalized.trim()) content_html = normalized;
|
const normalized = normalizeEmailHtmlForInlineDisplay(content_html || html);
|
||||||
|
if (normalized && normalized.trim()) {
|
||||||
|
content_html = normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!content_html || !String(content_html).trim()) {
|
||||||
|
content_html =
|
||||||
|
(text ? `<pre class="mail-plain-pre">${this.escapeHtml(text)}</pre>` : '') || listSnippet;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
...d,
|
...d,
|
||||||
content_html,
|
content_html,
|
||||||
|
content_text: text || d.content_text || '',
|
||||||
inbox_id: inboxKey,
|
inbox_id: inboxKey,
|
||||||
attachments: []
|
attachments: []
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- publish 引用编辑页面 -->
|
<!-- publish 引用编辑页面 -->
|
||||||
<editPublicRefRdit ref="editPublicRefRdit" :chanFerFormRepeatList="chanFerFormRepeatList" :chanFerForm = "chanFerForm" :gridData = "gridData" :p_article_id='p_article_id' @ChanFerMashUp="ChanFerMashUp" @refrashComp="refrashComp" @changeRefer="changeRefer"></editPublicRefRdit>
|
<editPublicRefRdit ref="editPublicRefRdit" :chanFerFormRepeatList="chanFerFormRepeatList" :chanFerForm = "chanFerForm" :gridData = "gridData" :p_article_id='p_article_id' :article_id="article_id" @ChanFerMashUp="ChanFerMashUp" @refrashComp="refrashComp" @changeRefer="changeRefer"></editPublicRefRdit>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
export default {
|
export default {
|
||||||
data(){
|
data(){
|
||||||
return{
|
return{
|
||||||
article_id: this.$route.query.id,
|
article_id: null,
|
||||||
chanFerForm: [],
|
chanFerForm: [],
|
||||||
chanFerFormRepeatList: [],
|
chanFerFormRepeatList: [],
|
||||||
gridData: '',
|
gridData: '',
|
||||||
@@ -21,9 +21,27 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created(){
|
created(){
|
||||||
this.getData()
|
this.initPage()
|
||||||
},
|
},
|
||||||
methods:{
|
methods:{
|
||||||
|
/** 正文接口 getArticleMainsNew 需用 production.article_id,非路由 id */
|
||||||
|
fetchProductionArticleId() {
|
||||||
|
if (!this.p_article_id) return Promise.resolve();
|
||||||
|
return this.$api
|
||||||
|
.post('api/Production/getProductionDetail', { p_article_id: this.p_article_id })
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code == 0 && res.data && res.data.production) {
|
||||||
|
this.article_id = res.data.production.article_id;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn('getProductionDetail failed', err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async initPage() {
|
||||||
|
await this.fetchProductionArticleId();
|
||||||
|
this.getData();
|
||||||
|
},
|
||||||
// 获取p_article_id的值
|
// 获取p_article_id的值
|
||||||
getArtcleDetails(){
|
getArtcleDetails(){
|
||||||
// 获得文章详情
|
// 获得文章详情
|
||||||
|
|||||||
@@ -1,26 +1,655 @@
|
|||||||
/**
|
/**
|
||||||
* 将完整 HTML 邮件转为适合 div[v-html] 的片段:
|
|
||||||
* 取 body.innerHTML,并前置 head 内 <style>,避免版式与改版前不一致。
|
* HTML 邮件预览:ResearchGate 等营销邮件在 @media 窄屏规则下会隐藏 .desktop-only,
|
||||||
|
|
||||||
|
* 导致预览区看似空白。通过剥离 @media、注入覆盖样式,并用 iframe 渲染完整文档。
|
||||||
|
|
||||||
*/
|
*/
|
||||||
export function normalizeEmailHtmlForInlineDisplay(html) {
|
|
||||||
if (!html || typeof html !== 'string') return '';
|
|
||||||
const t = html.trim();
|
|
||||||
if (!/^<!DOCTYPE|^<\s*html[\s>]/i.test(t)) return html;
|
const MAIL_PREVIEW_FIX_CSS = `
|
||||||
try {
|
html, body {
|
||||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
overflow: visible !important;
|
||||||
const body = doc && doc.body;
|
height: auto !important;
|
||||||
if (!body) return html;
|
min-height: 0 !important;
|
||||||
const inner = body.innerHTML;
|
max-height: none !important;
|
||||||
if (!inner || !inner.trim()) return html;
|
margin: 0;
|
||||||
let headInject = '';
|
padding: 0;
|
||||||
const head = doc.head;
|
|
||||||
if (head) {
|
|
||||||
head.querySelectorAll('style').forEach((node) => {
|
|
||||||
headInject += node.outerHTML;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return headInject + inner;
|
|
||||||
} catch (e) {
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.desktop-only,
|
||||||
|
|
||||||
|
.mobile-only {
|
||||||
|
|
||||||
|
display: block !important;
|
||||||
|
|
||||||
|
max-height: none !important;
|
||||||
|
|
||||||
|
overflow: visible !important;
|
||||||
|
|
||||||
|
visibility: visible !important;
|
||||||
|
|
||||||
|
font-size: inherit !important;
|
||||||
|
|
||||||
|
mso-hide: initial !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.imgDesk {
|
||||||
|
|
||||||
|
display: block !important;
|
||||||
|
|
||||||
|
max-height: none !important;
|
||||||
|
|
||||||
|
overflow: visible !important;
|
||||||
|
|
||||||
|
font-size: inherit !important;
|
||||||
|
|
||||||
|
mso-hide: initial !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.imgMobile {
|
||||||
|
|
||||||
|
display: none !important;
|
||||||
|
|
||||||
|
max-height: 0 !important;
|
||||||
|
|
||||||
|
overflow: hidden !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-sm {
|
||||||
|
|
||||||
|
display: block !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
td.hidden-sm {
|
||||||
|
|
||||||
|
display: table-cell !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
table.container,
|
||||||
|
|
||||||
|
table.content,
|
||||||
|
|
||||||
|
table.content-outer {
|
||||||
|
|
||||||
|
width: 100% !important;
|
||||||
|
|
||||||
|
max-width: 100% !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
|
||||||
|
max-width: 100% !important;
|
||||||
|
|
||||||
|
height: auto !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
body table,
|
||||||
|
body div,
|
||||||
|
body center,
|
||||||
|
body td {
|
||||||
|
overflow: visible !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > table,
|
||||||
|
body > div {
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
body > table,
|
||||||
|
body > div,
|
||||||
|
body > center,
|
||||||
|
body table {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body table[width] {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body td[width] {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 550px) {
|
||||||
|
|
||||||
|
.desktop-only,
|
||||||
|
|
||||||
|
.mobile-only {
|
||||||
|
|
||||||
|
display: block !important;
|
||||||
|
|
||||||
|
max-height: none !important;
|
||||||
|
|
||||||
|
overflow: visible !important;
|
||||||
|
|
||||||
|
visibility: visible !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.imgDesk {
|
||||||
|
|
||||||
|
display: block !important;
|
||||||
|
|
||||||
|
max-height: none !important;
|
||||||
|
|
||||||
|
overflow: visible !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.imgMobile {
|
||||||
|
|
||||||
|
display: none !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-device-width: 736px) {
|
||||||
|
|
||||||
|
.hidden-sm {
|
||||||
|
|
||||||
|
display: block !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
td.hidden-sm {
|
||||||
|
|
||||||
|
display: table-cell !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-device-width: 550px) {
|
||||||
|
|
||||||
|
.imgDesk {
|
||||||
|
|
||||||
|
display: block !important;
|
||||||
|
|
||||||
|
max-height: none !important;
|
||||||
|
|
||||||
|
overflow: visible !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.imgMobile {
|
||||||
|
|
||||||
|
display: none !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const MAIL_PREVIEW_FIX_STYLE = `<style data-mail-preview-fix>${MAIL_PREVIEW_FIX_CSS}</style>`;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** 去掉邮件内 @media,避免窄视口把 desktop-only 整块藏掉 */
|
||||||
|
|
||||||
|
function stripMediaQueries(css) {
|
||||||
|
|
||||||
|
if (!css || typeof css !== 'string') return '';
|
||||||
|
|
||||||
|
let out = '';
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
const s = css;
|
||||||
|
|
||||||
|
while (i < s.length) {
|
||||||
|
|
||||||
|
const idx = s.indexOf('@media', i);
|
||||||
|
|
||||||
|
if (idx === -1) {
|
||||||
|
|
||||||
|
out += s.slice(i);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
out += s.slice(i, idx);
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
|
||||||
|
let j = idx;
|
||||||
|
|
||||||
|
let started = false;
|
||||||
|
|
||||||
|
for (; j < s.length; j++) {
|
||||||
|
|
||||||
|
const ch = s[j];
|
||||||
|
|
||||||
|
if (ch === '{') {
|
||||||
|
|
||||||
|
depth++;
|
||||||
|
|
||||||
|
started = true;
|
||||||
|
|
||||||
|
} else if (ch === '}') {
|
||||||
|
|
||||||
|
depth--;
|
||||||
|
|
||||||
|
if (started && depth === 0) {
|
||||||
|
|
||||||
|
j++;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
i = j;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function extractBodyHtmlFallback(html) {
|
||||||
|
|
||||||
|
const m = String(html).match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||||||
|
|
||||||
|
return m && m[1] ? m[1].trim() : '';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** API 有时返回 HTML 实体编码 */
|
||||||
|
|
||||||
|
export function unescapeHtmlEntities(html) {
|
||||||
|
|
||||||
|
if (!html || typeof html !== 'string') return html || '';
|
||||||
|
|
||||||
|
const t = html.trim();
|
||||||
|
|
||||||
|
if (!/<(?:!DOCTYPE|html|body|table)/i.test(t) && !/^</i.test(t)) return html;
|
||||||
|
|
||||||
|
if (typeof document === 'undefined') return html;
|
||||||
|
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
|
||||||
|
ta.innerHTML = t;
|
||||||
|
|
||||||
|
return ta.value;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function isRichHtmlEmail(html) {
|
||||||
|
|
||||||
|
if (!html || typeof html !== 'string') return false;
|
||||||
|
|
||||||
|
const t = unescapeHtmlEntities(html).trim();
|
||||||
|
|
||||||
|
if (!t) return false;
|
||||||
|
|
||||||
|
if (/^<!DOCTYPE|^<\s*html[\s>]/i.test(t)) return true;
|
||||||
|
|
||||||
|
return t.length > 80 && /<(?:table|div|td|tbody|style|img|p|span)[\s>/]/i.test(t);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function collectHeadStyles(doc) {
|
||||||
|
|
||||||
|
let headInject = '';
|
||||||
|
|
||||||
|
const head = doc && doc.head;
|
||||||
|
|
||||||
|
if (!head) return headInject;
|
||||||
|
|
||||||
|
head.querySelectorAll('style').forEach((node) => {
|
||||||
|
|
||||||
|
if (node.getAttribute('data-mail-preview-fix') != null) return;
|
||||||
|
|
||||||
|
const el = node.cloneNode(true);
|
||||||
|
|
||||||
|
el.textContent = stripMediaQueries(node.textContent || '');
|
||||||
|
|
||||||
|
headInject += el.outerHTML;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return headInject;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** iframe 内脚本:向父页面报告真实内容高度 */
|
||||||
|
function injectHeightReporterScript(doc) {
|
||||||
|
if (!doc || !doc.body) return;
|
||||||
|
if (doc.querySelector('script[data-mail-preview-height]')) return;
|
||||||
|
const script = doc.createElement('script');
|
||||||
|
script.setAttribute('data-mail-preview-height', '');
|
||||||
|
script.textContent = `(function(){
|
||||||
|
var SUSP=8000,EXP=12000;
|
||||||
|
function report(){
|
||||||
|
var b=document.body,de=document.documentElement;
|
||||||
|
if(!b)return;
|
||||||
|
b.style.overflow='visible';b.style.height='auto';b.style.maxHeight='none';
|
||||||
|
if(de){de.style.overflow='visible';de.style.height='auto';de.style.maxHeight='none';}
|
||||||
|
var h=0,i,r,top=b.getBoundingClientRect().top,maxB=0;
|
||||||
|
var nodes=b.querySelectorAll('*');
|
||||||
|
for(i=0;i<nodes.length;i++){
|
||||||
|
r=nodes[i].getBoundingClientRect();
|
||||||
|
if(r.height>0&&r.bottom>maxB)maxB=r.bottom;
|
||||||
|
}
|
||||||
|
r=b.getBoundingClientRect();
|
||||||
|
if(r.height>0&&r.bottom>maxB)maxB=r.bottom;
|
||||||
|
h=maxB>top?maxB-top:r.height||0;
|
||||||
|
h=Math.ceil(h+24);
|
||||||
|
if(h<80||(h>=SUSP&&h>=EXP*0.6))return;
|
||||||
|
try{window.parent.postMessage({type:'mail-preview-height',height:h},'*');}catch(e){}
|
||||||
|
}
|
||||||
|
function boot(){
|
||||||
|
report();
|
||||||
|
if(typeof ResizeObserver!=='undefined'){try{new ResizeObserver(report).observe(document.body);}catch(e){}}
|
||||||
|
Array.prototype.forEach.call(document.images||[],function(img){
|
||||||
|
if(!img.complete)img.addEventListener('load',report);
|
||||||
|
img.addEventListener('error',report);
|
||||||
|
});
|
||||||
|
[0,80,200,500,1000,2000,4000,8000].forEach(function(ms){setTimeout(report,ms);});
|
||||||
|
}
|
||||||
|
if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',boot);}else{boot();}
|
||||||
|
window.addEventListener('load',boot);
|
||||||
|
})();`;
|
||||||
|
doc.body.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectPreviewFixIntoDocument(doc) {
|
||||||
|
|
||||||
|
if (!doc || !doc.head) return;
|
||||||
|
|
||||||
|
doc.head.querySelectorAll('style').forEach((node) => {
|
||||||
|
|
||||||
|
if (node.getAttribute('data-mail-preview-fix') != null) return;
|
||||||
|
|
||||||
|
node.textContent = stripMediaQueries(node.textContent || '');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!doc.head.querySelector('style[data-mail-preview-fix]')) {
|
||||||
|
|
||||||
|
const fix = doc.createElement('style');
|
||||||
|
|
||||||
|
fix.setAttribute('data-mail-preview-fix', '');
|
||||||
|
|
||||||
|
fix.textContent = MAIL_PREVIEW_FIX_CSS;
|
||||||
|
|
||||||
|
doc.head.appendChild(fix);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc.head.querySelector('meta[charset]')) {
|
||||||
|
|
||||||
|
const meta = doc.createElement('meta');
|
||||||
|
|
||||||
|
meta.setAttribute('charset', 'utf-8');
|
||||||
|
|
||||||
|
doc.head.insertBefore(meta, doc.head.firstChild);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
injectHeightReporterScript(doc);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
|
* 将完整 HTML 邮件转为适合 div[v-html] 的片段(非 iframe 场景)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function normalizeEmailHtmlForInlineDisplay(html) {
|
||||||
|
|
||||||
|
if (!html || typeof html !== 'string') return '';
|
||||||
|
|
||||||
|
const decoded = unescapeHtmlEntities(html);
|
||||||
|
|
||||||
|
const t = decoded.trim();
|
||||||
|
|
||||||
|
if (!isRichHtmlEmail(decoded)) return decoded;
|
||||||
|
|
||||||
|
if (!/^<!DOCTYPE|^<\s*html[\s>]/i.test(t)) {
|
||||||
|
|
||||||
|
return MAIL_PREVIEW_FIX_STYLE + decoded;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const doc = new DOMParser().parseFromString(decoded, 'text/html');
|
||||||
|
|
||||||
|
const body = doc && doc.body;
|
||||||
|
|
||||||
|
let inner = body && body.innerHTML ? body.innerHTML.trim() : '';
|
||||||
|
|
||||||
|
if (!inner) {
|
||||||
|
|
||||||
|
inner = extractBodyHtmlFallback(t);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inner) return decoded;
|
||||||
|
|
||||||
|
const headInject = collectHeadStyles(doc);
|
||||||
|
|
||||||
|
return headInject + MAIL_PREVIEW_FIX_STYLE + inner;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
const inner = extractBodyHtmlFallback(t);
|
||||||
|
|
||||||
|
if (inner) return MAIL_PREVIEW_FIX_STYLE + inner;
|
||||||
|
|
||||||
|
return decoded;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** 测量 iframe 被临时撑到 16000px 时,scrollHeight 会虚高,只按内容占位计算 */
|
||||||
|
const IFRAME_MEASURE_EXPAND_PX = 12000;
|
||||||
|
const SUSPICIOUS_HEIGHT_PX = 8000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按子元素 getBoundingClientRect 计算真实内容高度(不用 scrollHeight)
|
||||||
|
*/
|
||||||
|
export function measureEmailDocumentHeight(doc) {
|
||||||
|
if (!doc || !doc.body) return 200;
|
||||||
|
const body = doc.body;
|
||||||
|
const docEl = doc.documentElement;
|
||||||
|
if (docEl) {
|
||||||
|
docEl.style.overflow = 'visible';
|
||||||
|
docEl.style.height = 'auto';
|
||||||
|
docEl.style.minHeight = '0';
|
||||||
|
docEl.style.maxHeight = 'none';
|
||||||
|
}
|
||||||
|
body.style.overflow = 'visible';
|
||||||
|
body.style.height = 'auto';
|
||||||
|
body.style.minHeight = '0';
|
||||||
|
body.style.maxHeight = 'none';
|
||||||
|
|
||||||
|
let maxBottom = 0;
|
||||||
|
const bodyTop = body.getBoundingClientRect().top;
|
||||||
|
const nodes = body.querySelectorAll('*');
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const r = nodes[i].getBoundingClientRect();
|
||||||
|
if (r.height > 0 && r.bottom > maxBottom) maxBottom = r.bottom;
|
||||||
|
}
|
||||||
|
const bodyRect = body.getBoundingClientRect();
|
||||||
|
if (bodyRect.height > 0 && bodyRect.bottom > maxBottom) maxBottom = bodyRect.bottom;
|
||||||
|
const fromRect = maxBottom > bodyTop ? maxBottom - bodyTop : bodyRect.height || 0;
|
||||||
|
|
||||||
|
return Math.ceil(Math.max(fromRect, 80) + 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 过滤测量异常值(如误用撑开后的 scrollHeight 得到 16000) */
|
||||||
|
export function sanitizeMailPreviewHeight(h) {
|
||||||
|
const n = Math.ceil(Number(h) || 0);
|
||||||
|
if (n < 80) return 0;
|
||||||
|
if (n >= SUSPICIOUS_HEIGHT_PX && n >= IFRAME_MEASURE_EXPAND_PX * 0.6) return 0;
|
||||||
|
return Math.min(n, 20000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 iframe 高度设为与内部文档一致
|
||||||
|
*/
|
||||||
|
export function resizeIframeToContent(frame) {
|
||||||
|
if (!frame) return 200;
|
||||||
|
let doc;
|
||||||
|
try {
|
||||||
|
doc = frame.contentDocument;
|
||||||
|
} catch (e) {
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
if (!doc || !doc.body) return 200;
|
||||||
|
|
||||||
|
const prevOverflow = frame.style.overflow;
|
||||||
|
frame.style.height = IFRAME_MEASURE_EXPAND_PX + 'px';
|
||||||
|
frame.style.minHeight = '0';
|
||||||
|
frame.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
let h = measureEmailDocumentHeight(doc);
|
||||||
|
h = sanitizeMailPreviewHeight(h) || measureEmailDocumentHeight(doc);
|
||||||
|
|
||||||
|
if (typeof frame.contentWindow !== 'undefined' && frame.contentWindow) {
|
||||||
|
try {
|
||||||
|
frame.contentWindow.scrollTo(0, 0);
|
||||||
|
} catch (e2) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const h2 = sanitizeMailPreviewHeight(measureEmailDocumentHeight(doc));
|
||||||
|
if (h2 > h) h = h2;
|
||||||
|
|
||||||
|
const finalH = sanitizeMailPreviewHeight(h) || 400;
|
||||||
|
frame.style.height = finalH + 'px';
|
||||||
|
frame.style.minHeight = finalH + 'px';
|
||||||
|
frame.style.overflow = prevOverflow || 'hidden';
|
||||||
|
return finalH;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
|
* 生成 iframe srcdoc 用的完整 HTML 文档(样式在 head 内,渲染最稳定)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function buildEmailPreviewDocument(html) {
|
||||||
|
|
||||||
|
if (!html || typeof html !== 'string') return '';
|
||||||
|
|
||||||
|
if (typeof DOMParser === 'undefined') return '';
|
||||||
|
|
||||||
|
const decoded = unescapeHtmlEntities(html);
|
||||||
|
|
||||||
|
const t = decoded.trim();
|
||||||
|
|
||||||
|
if (!t || !isRichHtmlEmail(decoded)) return '';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let doc;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (/^<!DOCTYPE|^<\s*html[\s>]/i.test(t)) {
|
||||||
|
|
||||||
|
doc = new DOMParser().parseFromString(decoded, 'text/html');
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
doc = new DOMParser().parseFromString(
|
||||||
|
|
||||||
|
'<!DOCTYPE html><html><head></head><body></body></html>',
|
||||||
|
|
||||||
|
'text/html'
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
doc.body.innerHTML = decoded;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
return '';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (!doc || !doc.documentElement) return '';
|
||||||
|
|
||||||
|
injectPreviewFixIntoDocument(doc);
|
||||||
|
|
||||||
|
return '<!DOCTYPE html>\n' + doc.documentElement.outerHTML;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 离屏 div 测量 HTML 高度(与 iframe 同宽,作高度兜底) */
|
||||||
|
export function measureHtmlContentHeight(html, containerWidth) {
|
||||||
|
if (typeof document === 'undefined' || !html) return 0;
|
||||||
|
const decoded = unescapeHtmlEntities(html);
|
||||||
|
if (!isRichHtmlEmail(decoded)) return 0;
|
||||||
|
const content = normalizeEmailHtmlForInlineDisplay(decoded);
|
||||||
|
if (!content) return 0;
|
||||||
|
const w = Math.max(Number(containerWidth) || 640, 280);
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.setAttribute('data-mail-measure', '');
|
||||||
|
el.style.cssText =
|
||||||
|
'position:fixed;left:-30000px;top:0;width:' +
|
||||||
|
w +
|
||||||
|
'px;visibility:hidden;pointer-events:none;overflow:visible;z-index:-1;box-sizing:border-box;';
|
||||||
|
el.innerHTML = content;
|
||||||
|
document.body.appendChild(el);
|
||||||
|
let maxBottom = 0;
|
||||||
|
const top = el.getBoundingClientRect().top;
|
||||||
|
el.querySelectorAll('*').forEach((node) => {
|
||||||
|
const r = node.getBoundingClientRect();
|
||||||
|
if (r.height > 0 && r.bottom > maxBottom) maxBottom = r.bottom;
|
||||||
|
});
|
||||||
|
const er = el.getBoundingClientRect();
|
||||||
|
if (er.height > 0 && er.bottom > maxBottom) maxBottom = er.bottom;
|
||||||
|
const fromRect = maxBottom > top ? maxBottom - top : er.height || 0;
|
||||||
|
const h = sanitizeMailPreviewHeight(Math.ceil(Math.max(fromRect, 80) + 24));
|
||||||
|
document.body.removeChild(el);
|
||||||
|
return h || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MAIL_PREVIEW_HEIGHT_MSG = 'mail-preview-height';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user