This commit is contained in:
2026-03-17 09:19:35 +08:00
parent d66225dc5d
commit 94d45dfddf
8 changed files with 339 additions and 46 deletions

View File

@@ -2301,7 +2301,8 @@ str = str.replace(regex, function (match, content, offset, fullString) {
{ selector: '.tox-tbtn[data-mce-name="edit"]', className: 'tinymce-custom-button-edit' },
{ selector: '.tox-tbtn[data-mce-name="save"]', className: 'tinymce-custom-button-save' },
{ selector: '.tox-tbtn[data-mce-name="customblue"]', className: 'tinymce-custom-button-blue' },
{ selector: '.tox-tbtn[data-mce-name="removeblue"]', className: 'tinymce-custom-button-removeblue' }
{ selector: '.tox-tbtn[data-mce-name="removeblue"]', className: 'tinymce-custom-button-removeblue' },
{ selector: '.tox-tbtn[data-mce-name="selecttemplate"]', className: 'tinymce-custom-button-selecttemplate' }
];
// 遍历每个按钮并为每个按钮添加类

View File

@@ -817,6 +817,9 @@ const en = {
"tooLarge": "Too Large (>1MB)",
"error": "Error: {msg}",
"imgLabel": "Img"
},
mailTemplate: {
noTemplateTip: 'No templates are available for the current journal. Please select another journal or contact the administrator to configure templates.'
}

View File

@@ -798,6 +798,10 @@ const zh = {
"tooLarge": "过大跳过(>1MB)",
"error": "失败: {msg}",
"imgLabel": "图"
},
mailTemplate: {
// 如果已经有 mailTemplate就只加这一行
noTemplateTip: '当前期刊暂无可用模板,请重新选择期刊或联系管理员配置模板。'
}

View File

@@ -5,11 +5,14 @@
</template>
<script>
import CommonJS from '@/common/js/commonJS'
export default {
name: 'TinyEditor',
props: {
value: { type: String, default: '' },
id: { type: String, default: () => 'tiny-' + +new Date() }
id: { type: String, default: () => 'tiny-' + +new Date() },
showSelectTemplateButton: { type: Boolean, default: false }
},
data() {
return {
@@ -114,21 +117,26 @@ autoInlineStyles(htmlContent) {
return "<!DOCTYPE html>\n" + doc.documentElement.outerHTML;
},
initTiny() {
const vueInstance = this;
const hasTemplateBtn = this.showSelectTemplateButton;
window.tinymce.init({
selector: `#${this.tinymceId}`,
language: 'zh_CN', // 记得下载语言包,没有就删掉这行
height: 500,
// 核心配置:允许所有 HTML 标签和样式,这对比邮件模板至关重要
valid_children: '+body[style],+p[style],+div[style]',
valid_children: '+body[tr],+body[thead],+body[tbody],+body[table],+body[style],+p[style],+div[style]',
extended_valid_elements: 'style,meta,title',
custom_elements: 'style,meta,title',
verify_html: false, // 关闭 HTML 校验,防止自动删掉你的邮件结构
forced_root_block: '', // 停止自动包裹 <p> 标签
// 插件需要包含这些,否则工具栏按钮不会显示
plugins: 'code link image table lists fullscreen directionality codesample preview charmap autolink nonbreaking',
plugins: 'code link table lists fullscreen directionality codesample preview charmap autolink nonbreaking',
// 按照图片布局精准排序:
toolbar: 'undo redo | code preview | fontselect fontsizeselect | formatselect | bold italic underline strikethrough | forecolor backcolor | bullist numlist | lineheight outdent indent | alignleft aligncenter alignright alignjustify | image table link | fullscreen',
toolbar: hasTemplateBtn
? 'undo redo | selectTemplate | code preview | fontselect fontsizeselect | formatselect | bold italic underline strikethrough | forecolor backcolor | bullist numlist | lineheight outdent indent | alignleft aligncenter alignright alignjustify | table link | fullscreen'
: 'undo redo | code preview | fontselect fontsizeselect | formatselect | bold italic underline strikethrough | forecolor backcolor | bullist numlist | lineheight outdent indent | alignleft aligncenter alignright alignjustify | table link | fullscreen',
// 补充:让字体和字号选择器更有序
fontsize_formats: '12px 14px 16px 18px 24px 36px 48px',
@@ -137,14 +145,29 @@ autoInlineStyles(htmlContent) {
promotion: false,
// 邮件编辑优化:允许在源码中看到完整的 html 结构
fullpage_enabled: true,
setup(editor) {
// 自定义 Select Template 按钮(触发 Vue 事件给父组件,按需显示)
if (hasTemplateBtn) {
editor.ui.registry.addButton('selectTemplate', {
text: 'Select Template ▾',
onAction() {
vueInstance.$emit('onSelectTemplate');
}
});
}
// 统一给自定义按钮加样式(无按钮时不会产生影响)
CommonJS.inTinymceButtonClass();
},
init_instance_callback: (editor) => {
this.editor = editor;
if (this.value) {
editor.setContent(this.value);
}
// 监听内容变化
editor.on('NodeChange Change KeyUp SetContent', () => {
this.$emit('input', editor.getContent());
vueInstance.$emit('input', editor.getContent());
});
}
});

View File

@@ -0,0 +1,274 @@
<template>
<el-dialog
title="Template Selection"
:visible.sync="visible"
:close-on-click-modal="false"
width="90%"
top="5vh"
destroy-on-close
:before-close="handleClose"
custom-class="template-modal"
>
<div class="template-wrapper">
<div class="selection-panel">
<el-tabs v-model="activeStep">
<el-tab-pane label="1.Choose Style" name="style">
<div class="card-grid">
<div
v-for="(item,index) in headerStyles" :key="item.id"
:class="['card-item', { active: selectedHeaderId === item.id }]"
@click="selectedHeaderId = item.id"
>
<div class="card-img" @click.stop="selectedHeaderId = item.id">
<div v-html="item.htmlHeader+item.htmlFooter" style="zoom: 0.15;pointer-events: none; user-select: none;"></div>
</div>
<p class="card-title"><span>{{ index+1 }}. </span>{{ item.name }}</p>
<p
class="card-desc"
:class="{ expanded: expandedHeaderId === item.id }"
@click.stop="toggleDesc(item.id)"
>
{{ item.description }}
</p>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="2.Choose Template" name="content">
<div style="margin-bottom: 15px; padding: 0 5px;">
<p style="font-size: 12px; color: #999; margin-bottom: 5px;">Journal:</p>
<el-select
v-model="selectedJournalId"
placeholder="All Journals"
clearable
size="small"
style="width: 100%"
@change="handleJournalChange"
>
<el-option
v-for="item in journalOptions"
:key="item.id"
:label="item.label"
:value="item.id"
/>
</el-select>
</div>
<div
class="card-grid"
v-loading="contentLoading"
>
<div
v-for="(item,index) in contentTemplates" :key="item.id"
:class="['card-item', { active: selectedContentId === item.id }]"
@click="selectedContentId = item.id"
>
<p class="card-title" style="text-align: left;margin-top: 0;"><span>{{ index+1 }}. </span>{{ item.name }}<br><span style="color:#888 ;">{{ item.subject }}</span></p>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<div class="preview-panel">
<div class="preview-label">LIVE PREVIEW</div>
<div class="preview-container">
<div class="mail-render-box" v-html="combinedHtml"></div>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<div class="footer-info">
Selected: <strong>{{ currentSelectionText }}</strong>
</div>
<div>
<el-button @click="handleClose" style="">Cancel</el-button>
<el-button type="primary" icon="el-icon-check" @click="submit">Apply Template</el-button>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
props: {
visible: Boolean
},
data() {
return {
activeStep: 'style',
// 期刊过滤
journalOptions: [],
selectedJournalId: null,
selectedHeaderId: null,
selectedContentId: null,
expandedHeaderId: null,
// 头部样式列表(通过接口获取)
headerStyles: [],
contentTemplates: [],
contentLoading: false
};
},
created() {
this.fetchJournals();
this.fetchHeaderStyles();
},
computed: {
// 【关键】拼接 HTML
combinedHtml() {
const header = this.headerStyles.find(h => h.id === this.selectedHeaderId);
const content = this.contentTemplates.find(c => c.id === this.selectedContentId);
// 没有可用模板时,右侧显示提示文案(走国际化)
if (!header || !content) {
const msg = this.$t('mailTemplate.noTemplateTip');
return `<div style="padding: 40px; text-align: center; color: #999; font-size: 14px;">
${msg}
</div>`;
}
// 返回完整的拼接代码,使用内联样式以兼容邮件
return `${header.htmlHeader}${content.bodyHtml}${header.htmlFooter}`;
},
currentSelectionText() {
const h = this.headerStyles.find(h => h.id === this.selectedHeaderId);
const c = this.contentTemplates.find(c => c.id === this.selectedContentId);
return `${h ? h.name : ''} + ${c ? c.name : ''}`;
}
},
methods: {
// 获取期刊列表用于过滤模板
async fetchJournals() {
try {
const res = await this.$api.post('api/Journal/getAllJournal', {});
if (!res || res.code !== 0) {
return;
}
this.journalOptions = res.data.journals.map(j => ({
id: j.journal_id,
label:j.title,
}));
if (this.journalOptions.length && !this.selectedJournalId) {
this.selectedJournalId = this.journalOptions[0].id;
this.fetchContentTemplates();
}
} catch (e) {
// 静默失败
}
},
toggleDesc(id) {
this.expandedHeaderId = this.expandedHeaderId === id ? null : id;
},
async handleJournalChange() {
// 切换期刊时重新拉取样式和模板(按需可改为带 journal_id 过滤)
this.contentTemplates=[]
await this.fetchContentTemplates();
if (this.contentTemplates.length) {
this.selectedContentId = this.contentTemplates[0].id;
}
},
// 调用接口获取头部样式列表
async fetchHeaderStyles() {
try {
const res = await this.$api.post('api/mail_template/listStyles', {
journal_id: this.selectedJournalId
});
if (!res || res.code !== 0 ) {
return;
}
// 后端返回字段style_id, name, header_html, footer_html
this.headerStyles = res.data.list.map(item => ({
id: item.style_id,
name: item.name,
description:item.description || '',
htmlHeader: item.header_html || '',
htmlFooter: item.footer_html || ''
}));
console.log("🚀 ~ headerStyles:", this.headerStyles);
// 默认选中第一条
if (this.headerStyles.length && !this.selectedHeaderId) {
this.selectedHeaderId = this.headerStyles[0].id;
}
} catch (e) {
// 静默失败,避免打断用户流程
}
},
// 调用接口获取内容模板列表
async fetchContentTemplates() {
try {
this.contentLoading = true;
const res = await this.$api.post('api/mail_template/listTemplates', {
journal_id: this.selectedJournalId
});
if (!res || res.code !== 0 ) {
return;
}
// 后端返回字段template_id, title, body_html 等
this.contentTemplates = res.data.list.map(item => ({
id: item.template_id,
name: item.title,
subject: item.subject,
bodyHtml: item.body_html || ''
}));
// 默认选中第一条
if (this.contentTemplates.length && !this.selectedContentId) {
this.selectedContentId = this.contentTemplates[0].id;
}
} catch (e) {
// 静默失败
} finally {
this.contentLoading = false;
}
},
handleClose() {
this.$emit('update:visible', false);
},
submit() {
// 将拼接好的 HTML 抛给父组件
this.$emit('confirm', this.combinedHtml);
this.handleClose();
}
}
};
</script>
<style scoped>
.template-wrapper { display: flex; height: 70vh; gap: 20px; }
.selection-panel { width: 260px; border-right: 1px solid #eee; padding-right: 10px; overflow-y: auto; }
.journal-filter-form { padding-right: 8px; }
.preview-panel { flex: 1; background: #f4f6f8; padding: 20px; display: flex; flex-direction: column; }
.preview-label { font-weight: bold; color: #999; font-size: 12px; margin-bottom: 10px; }
.preview-container { background: #fff; flex: 1; border-radius: 4px; overflow-y: auto; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
.card-grid { display: flex; flex-direction: column; gap: 12px; margin-top: 15px; }
.card-item { border: 1px solid #ddd; border-radius: 8px; padding: 8px; cursor: pointer; transition: 0.3s; }
.card-item.active { border-color: #6366f1; background: #f5f7ff; outline: 1px solid #6366f1; }
.card-img { width: 100%; height: auto; border-radius: 4px; }
.card-title { font-size: 13px; margin: 8px 0 0; text-align: center; color: #666; }
.card-desc {
font-size: 12px;
color: #888;
margin-top: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.card-desc.expanded {
white-space: normal;
overflow: visible;
}
.dialog-footer { display: flex; justify-content: space-between; align-items: center; width: 100%; }
.footer-info { color: #666; font-size: 14px; }
</style>

View File

@@ -40,7 +40,7 @@
</div>
<div class="mail-list-panel" :style="{ width: listWidth + 'px' }" v-if="selectedAccount">
<div class="panel-header">
<!-- <div class="panel-header">
<el-input
v-model="searchKeyword"
prefix-icon="el-icon-search"
@@ -48,8 +48,8 @@
clearable
@change="handleSearch"
></el-input>
<!-- <el-button icon="el-icon-refresh" circle :loading="syncLoading" @click="handleSyncInbox" style="margin-left: 10px;"></el-button> -->
</div>
<el-button icon="el-icon-refresh" circle :loading="syncLoading" @click="handleSyncInbox" style="margin-left: 10px;"></el-button>
</div> -->
<div ref="listScrollArea" class="list-scroll-area" @scroll="onListScroll">
<template v-if="displayList.length > 0">

View File

@@ -106,6 +106,8 @@
:source-content.sync="sourceContent"
:source-rows="16"
:source-placeholder="$t('mailboxSend.sourcePlaceholder')"
:show-select-template-button="true"
@onSelectTemplate="showTemplateDialog = true"
/>
</div>
<div class="mail-footer-bar" :style="{ left: footerBarLeft }">
@@ -153,31 +155,16 @@
</div>
</el-dialog>
<!-- 选择模板 -->
<el-dialog :title="$t('mailboxSend.selectTemplate')" :visible.sync="Templatebox" width="620px" :close-on-click-modal="false">
<el-form ref="Tempform" :model="TempForm" label-width="225px">
<el-form-item :label="$t('mailboxSend.chooseTemplate')">
<el-select v-model="TempForm.board" :placeholder="$t('mailboxSend.chooseTemplatePlaceholder')" @change="select_tem($event)" style="width: 220px;">
<el-option :key="0" :label="$t('mailboxSend.none') " :value="0"></el-option>
<el-option v-for="item in fol_low" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('mailboxSend.previewTemplate')">
<img src="../../assets/img/img.jpg" alt="" style="width: 250px;">
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="Templatebox = false">{{ $t('mailboxSend.cancel') }}</el-button>
<el-button type="primary" @click="saveTemplate">{{ $t('mailboxSend.save') }}</el-button>
</span>
</el-dialog>
<template-selector-dialog v-if="showTemplateDialog"
:visible.sync="showTemplateDialog"
@confirm="handleTemplateApply"
/>
</div>
</template>
<script>
import emailCkeditor from '@/components/page/components/email/CkeditorMail.vue'
import TemplateSelectorDialog from '@/components/page/components/email/TemplateSelectorDialog.vue'
import 'multi-items-input'
import 'multi-items-input/dist/multi-items-input.css'
import bus from '../common/bus'
@@ -211,7 +198,7 @@ import emailCkeditor from '@/components/page/components/email/CkeditorMail.vue'
},
LibrForm: {},
LibrarySelection: [],
Templatebox: false,
showTemplateDialog: false,
Librarybox: false,
link_TotalLibry: 0,
isSourceMode: false,
@@ -232,6 +219,7 @@ import emailCkeditor from '@/components/page/components/email/CkeditorMail.vue'
},
components: {
emailCkeditor,
TemplateSelectorDialog,
},
computed: {
footerBarLeft() {
@@ -259,6 +247,16 @@ import emailCkeditor from '@/components/page/components/email/CkeditorMail.vue'
bus.$off('collapse-content');
},
methods: {
handleTemplateApply(htmlContent) {
// 假设你使用的是 TinyMCE
if (window.tinymce && window.tinymce.activeEditor) {
// 建议:如果你想保留已有内容,用 insertContent
// 如果想彻底更换模板,用 setContent。
window.tinymce.activeEditor.setContent(htmlContent);
this.$message.success('Template applied successfully!');
}
},
// 切换富文本 / 源代码编辑模式(源码用 sourceContent 保留完整 HTML可自由来回切换
toggleSourceMode() {
if (this.isSourceMode) {
@@ -573,19 +571,7 @@ import emailCkeditor from '@/components/page/components/email/CkeditorMail.vue'
this.getLibary();
},
// 模板选择-弹出框
handleSetMoudle() {
this.Templatebox = true
},
// 保存模板
saveTemplate() {
},
// 下拉换模板预览
select_tem(e) {
},

View File

@@ -29,11 +29,13 @@
</el-form-item>
<el-form-item prop="header_html" :label="$t('mailboxStyleDetail.headerHtml')">
<CkeditorMail v-model="form.header_html" />
<el-input type="textarea" v-model="form.header_html" rows="10" :placeholder="$t('mailboxStyleDetail.headerHtmlPlaceholder')"></el-input>
<!-- <CkeditorMail v-model="form.header_html" /> -->
</el-form-item>
<el-form-item prop="footer_html" :label="$t('mailboxStyleDetail.footerHtml')">
<CkeditorMail v-model="form.footer_html" />
<!-- <CkeditorMail v-model="form.footer_html" /> -->
<el-input type="textarea" v-model="form.footer_html" rows="10" :placeholder="$t('mailboxStyleDetail.footerHtmlPlaceholder')"></el-input>
</el-form-item>
</el-form>
</section>