提交
This commit is contained in:
@@ -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' }
|
||||
];
|
||||
|
||||
// 遍历每个按钮并为每个按钮添加类
|
||||
|
||||
@@ -817,7 +817,10 @@ 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.'
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -798,6 +798,10 @@ const zh = {
|
||||
"tooLarge": "过大跳过(>1MB)",
|
||||
"error": "失败: {msg}",
|
||||
"imgLabel": "图"
|
||||
},
|
||||
mailTemplate: {
|
||||
// 如果已经有 mailTemplate,就只加这一行
|
||||
noTemplateTip: '当前期刊暂无可用模板,请重新选择期刊或联系管理员配置模板。'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
274
src/components/page/components/email/TemplateSelectorDialog.vue
Normal file
274
src/components/page/components/email/TemplateSelectorDialog.vue
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user