|
|
|
|
@ -51,9 +51,9 @@
|
|
|
|
|
<div class="flex-grow-1">
|
|
|
|
|
<div class="template-title">{{ template.name }}</div>
|
|
|
|
|
<div class="template-meta">
|
|
|
|
|
<span><i class="el-icon-date"></i> {{ template.createTime }}</span>
|
|
|
|
|
<span><i class="el-icon-date"></i> {{ template.createTime || template.updatedTime }}</span>
|
|
|
|
|
<span><i class="el-icon-data-line"></i> 使用{{ template.useCount }}次</span>
|
|
|
|
|
<span><i class="el-icon-price-tag"></i> {{ template.variables.length }}个变量</span>
|
|
|
|
|
<span><i class="el-icon-price-tag"></i> {{ template.variables ? template.variables.length : 0 }}个变量</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-dropdown @command="handleTemplateAction" trigger="click" @click.stop>
|
|
|
|
|
@ -82,7 +82,7 @@
|
|
|
|
|
<div class="template-variables">
|
|
|
|
|
<strong>可用变量:</strong>
|
|
|
|
|
<span v-for="variable in template.variables" :key="variable" class="variable-tag">
|
|
|
|
|
{{ '{' + '{' + variable + '}' + '}' }}
|
|
|
|
|
{{ '{' + variable + '}' }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@ -130,18 +130,28 @@
|
|
|
|
|
|
|
|
|
|
<!-- 用户列表选择区域 -->
|
|
|
|
|
<div class="recipient-table">
|
|
|
|
|
<div class="table-header">
|
|
|
|
|
<el-checkbox v-model="selectAll" @change="toggleSelectAll">全选用户</el-checkbox>
|
|
|
|
|
<div class="table-header" style="font-weight:600;font-size:16px;padding:12px 0 12px 16px;">收件人列表</div>
|
|
|
|
|
<div v-if="recipients.length === 0" style="padding: 60px 0; color: #909399; text-align: center;">
|
|
|
|
|
<i class="el-icon-user" style="font-size: 60px; margin-bottom: 16px;"></i>
|
|
|
|
|
<div style="font-size: 18px; margin-top: 8px;">暂无收件人数据</div>
|
|
|
|
|
<div style="font-size: 14px; color: #bfbfbf; margin-top: 4px;">请先上传Excel文件</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-table
|
|
|
|
|
v-else
|
|
|
|
|
ref="recipientTable"
|
|
|
|
|
:data="recipients"
|
|
|
|
|
@selection-change="handleSelectionChange"
|
|
|
|
|
style="width: 100%">
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
:max-height="400">
|
|
|
|
|
<el-table-column type="selection" width="55"></el-table-column>
|
|
|
|
|
<el-table-column prop="name" label="姓名"></el-table-column>
|
|
|
|
|
<el-table-column prop="email" label="邮箱"></el-table-column>
|
|
|
|
|
<el-table-column prop="company" label="公司"></el-table-column>
|
|
|
|
|
<el-table-column prop="course" label="课程名称"></el-table-column>
|
|
|
|
|
<el-table-column
|
|
|
|
|
v-for="field in recipientFields"
|
|
|
|
|
:key="field"
|
|
|
|
|
:prop="field"
|
|
|
|
|
:label="field"
|
|
|
|
|
:min-width="120"
|
|
|
|
|
show-overflow-tooltip>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@ -179,14 +189,18 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="selectedRecipients.length > 0" class="selected-preview">
|
|
|
|
|
<el-table :data="selectedRecipients" size="small">
|
|
|
|
|
<el-table-column prop="name" label="姓名"></el-table-column>
|
|
|
|
|
<el-table-column prop="email" label="邮箱"></el-table-column>
|
|
|
|
|
<el-table-column prop="company" label="公司"></el-table-column>
|
|
|
|
|
<el-table-column prop="course" label="课程"></el-table-column>
|
|
|
|
|
<el-table :data="selectedRecipients" size="small" style="width: 100%;" :max-height="300">
|
|
|
|
|
<el-table-column
|
|
|
|
|
v-for="field in recipientFields"
|
|
|
|
|
:key="field"
|
|
|
|
|
:prop="field"
|
|
|
|
|
:label="field"
|
|
|
|
|
:min-width="120"
|
|
|
|
|
show-overflow-tooltip>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="操作" width="80">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<el-button size="mini" type="danger" @click="removeFromSelection(scope.row.id)">
|
|
|
|
|
<el-button size="mini" type="danger" @click="removeFromSelectionByIndex(scope.$index)">
|
|
|
|
|
<i class="el-icon-close"></i>
|
|
|
|
|
</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
@ -227,11 +241,15 @@
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<h5>收件人预览(前3条)</h5>
|
|
|
|
|
<div class="preview-table">
|
|
|
|
|
<el-table :data="previewRecipients" size="small">
|
|
|
|
|
<el-table-column prop="name" label="姓名"></el-table-column>
|
|
|
|
|
<el-table-column prop="email" label="邮箱"></el-table-column>
|
|
|
|
|
<el-table-column prop="company" label="公司"></el-table-column>
|
|
|
|
|
<el-table-column prop="course" label="课程"></el-table-column>
|
|
|
|
|
<el-table :data="previewRecipients" size="small" :max-height="200">
|
|
|
|
|
<el-table-column
|
|
|
|
|
v-for="field in recipientFields"
|
|
|
|
|
:key="field"
|
|
|
|
|
:prop="field"
|
|
|
|
|
:label="field"
|
|
|
|
|
:min-width="120"
|
|
|
|
|
show-overflow-tooltip>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
<div v-if="selectedRecipients.length > 3" class="more-info">
|
|
|
|
|
还有 {{ selectedRecipients.length - 3 }} 位收件人未显示
|
|
|
|
|
@ -243,7 +261,7 @@
|
|
|
|
|
<el-form-item label="邮件主题" required>
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="sendForm.subject"
|
|
|
|
|
:placeholder="`支持变量,如:{{姓名}},欢迎参加{{课程名称}}`"
|
|
|
|
|
:placeholder="`请输入邮件主题`"
|
|
|
|
|
@input="updateEmailPreview">
|
|
|
|
|
</el-input>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
@ -274,9 +292,17 @@
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<h5>邮件内容预览</h5>
|
|
|
|
|
<div class="email-preview-card">
|
|
|
|
|
<div class="preview-title">邮件预览</div>
|
|
|
|
|
<div class="preview-meta">以第一位收件人为示例</div>
|
|
|
|
|
<div class="preview-content" v-html="emailPreviewContent"></div>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<strong>主题:</strong>{{ subjectPreview }}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<strong>收件人:</strong>{{ sampleUser.email }}
|
|
|
|
|
</div>
|
|
|
|
|
<hr>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<strong>邮件内容预览:</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="template-preview-container" v-html="contentPreview"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
@ -351,9 +377,9 @@
|
|
|
|
|
</el-tabs>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 创建模板弹窗 -->
|
|
|
|
|
<!-- 创建/编辑模板弹窗 -->
|
|
|
|
|
<el-dialog
|
|
|
|
|
title="创建邮件模板"
|
|
|
|
|
:title="templateForm.id ? '编辑邮件模板' : '创建邮件模板'"
|
|
|
|
|
:visible.sync="showTemplateModal"
|
|
|
|
|
width="80%"
|
|
|
|
|
:before-close="handleCloseTemplateModal">
|
|
|
|
|
@ -372,20 +398,18 @@
|
|
|
|
|
v-for="variable in availableVariables"
|
|
|
|
|
:key="variable"
|
|
|
|
|
class="variable-tag clickable"
|
|
|
|
|
@click="insertVariable(`{{${variable}}}`)">
|
|
|
|
|
{{ '{' + '{' + variable + '}' + '}' }}
|
|
|
|
|
@click="insertVariable(`{${variable}}`)">
|
|
|
|
|
{{ '{' + variable + '}' }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="邮件内容" required>
|
|
|
|
|
<el-input
|
|
|
|
|
ref="templateContentRef"
|
|
|
|
|
<my-tinymce
|
|
|
|
|
v-model="templateForm.content"
|
|
|
|
|
type="textarea"
|
|
|
|
|
:rows="15"
|
|
|
|
|
placeholder="请输入邮件内容,可以使用上方的变量..."
|
|
|
|
|
@input="updateTemplatePreview">
|
|
|
|
|
</el-input>
|
|
|
|
|
:height="300"
|
|
|
|
|
:toolbar="'undo redo | bold italic underline strikethrough | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist | outdent indent | removeformat | fullscreen'"
|
|
|
|
|
:plugins="'lists fullscreen textcolor colorpicker'"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
@ -397,7 +421,7 @@
|
|
|
|
|
</el-form>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<span class="dialog-footer">
|
|
|
|
|
<el-button @click="showTemplateModal = false">取消</el-button>
|
|
|
|
|
<el-button @click="handleCloseTemplateModal">取消</el-button>
|
|
|
|
|
<el-button type="primary" @click="saveTemplate">保存模板</el-button>
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
@ -406,6 +430,11 @@
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
import { saveEmailTemplate, getEmailTemplateList, deleteEmailTemplate } from '@/api/email/index'
|
|
|
|
|
import { getToken } from '@/utils/auth'
|
|
|
|
|
import { uploadEmailRecord } from '@/api/email/index'
|
|
|
|
|
import { saveEmailRecord, getEmailRecordList } from '@/api/email/index'
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
name: 'EmailManagement',
|
|
|
|
|
data() {
|
|
|
|
|
@ -420,6 +449,7 @@ export default {
|
|
|
|
|
|
|
|
|
|
// 表单数据
|
|
|
|
|
templateForm: {
|
|
|
|
|
id: null,
|
|
|
|
|
name: '',
|
|
|
|
|
description: '',
|
|
|
|
|
content: ''
|
|
|
|
|
@ -433,82 +463,35 @@ export default {
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 邮件模板数据
|
|
|
|
|
emailTemplates: [
|
|
|
|
|
{
|
|
|
|
|
id: 1,
|
|
|
|
|
name: '课程通知模板',
|
|
|
|
|
description: '用于课程开始前的通知邮件',
|
|
|
|
|
content: `亲爱的{{姓名}},您好!
|
|
|
|
|
|
|
|
|
|
感谢您报名参加我们的课程。以下是课程详细信息:
|
|
|
|
|
|
|
|
|
|
课程名称:{{课程名称}}
|
|
|
|
|
开始时间:{{开始时间}}
|
|
|
|
|
上课地点:{{地点}}
|
|
|
|
|
|
|
|
|
|
请您准时参加,如有任何问题,请随时联系我们。
|
|
|
|
|
|
|
|
|
|
祝好!
|
|
|
|
|
苏州科技商学院`,
|
|
|
|
|
variables: ['姓名', '课程名称', '开始时间', '地点'],
|
|
|
|
|
createTime: '2025-06-01 10:00',
|
|
|
|
|
useCount: 15
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 2,
|
|
|
|
|
name: '课程反馈邀请',
|
|
|
|
|
description: '课程结束后邀请填写反馈问卷',
|
|
|
|
|
content: `{{姓名}}同学,您好!
|
|
|
|
|
|
|
|
|
|
感谢您参加{{课程名称}}课程。为了不断改进我们的教学质量,诚邀您填写课程反馈问卷。
|
|
|
|
|
|
|
|
|
|
您的宝贵意见对我们非常重要,问卷填写仅需3-5分钟。
|
|
|
|
|
|
|
|
|
|
点击链接填写:[问卷链接]
|
|
|
|
|
|
|
|
|
|
再次感谢您的参与!
|
|
|
|
|
|
|
|
|
|
{{公司}}
|
|
|
|
|
苏州科技商学院`,
|
|
|
|
|
variables: ['姓名', '课程名称', '公司'],
|
|
|
|
|
createTime: '2025-06-02 14:30',
|
|
|
|
|
useCount: 8
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
emailTemplates: [],
|
|
|
|
|
|
|
|
|
|
// 收件人数据
|
|
|
|
|
recipients: [],
|
|
|
|
|
recipientFields: [],
|
|
|
|
|
selectedRecipients: [],
|
|
|
|
|
|
|
|
|
|
// 发送记录
|
|
|
|
|
sendHistory: [
|
|
|
|
|
{
|
|
|
|
|
id: 1,
|
|
|
|
|
templateName: '课程通知模板',
|
|
|
|
|
recipientCount: 25,
|
|
|
|
|
successCount: 23,
|
|
|
|
|
failCount: 2,
|
|
|
|
|
status: 'completed',
|
|
|
|
|
sendTime: '2025-06-01 15:30',
|
|
|
|
|
details: '发送完成,2封邮件因邮箱地址无效发送失败'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 2,
|
|
|
|
|
templateName: '课程反馈邀请',
|
|
|
|
|
recipientCount: 15,
|
|
|
|
|
successCount: 15,
|
|
|
|
|
failCount: 0,
|
|
|
|
|
status: 'completed',
|
|
|
|
|
sendTime: '2025-06-02 18:00',
|
|
|
|
|
details: '全部发送成功'
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
sendStatusList: [],
|
|
|
|
|
availableVariables: ['姓名', '邮箱', '公司', '课程名称', '开始时间', '地点']
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
created() {
|
|
|
|
|
this.loadTemplates()
|
|
|
|
|
// 如果初始tab就是history,自动加载
|
|
|
|
|
if (this.activeTab === 'history') {
|
|
|
|
|
this.loadSendHistory()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
watch: {
|
|
|
|
|
activeTab(val) {
|
|
|
|
|
if (val === 'history') {
|
|
|
|
|
this.loadSendHistory()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
selectedTemplate() {
|
|
|
|
|
return this.emailTemplates.find(t => t.id === this.selectedTemplateId)
|
|
|
|
|
@ -522,25 +505,16 @@ export default {
|
|
|
|
|
return this.selectedRecipients.slice(0, 3)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
emailPreviewContent() {
|
|
|
|
|
if (!this.selectedTemplate || this.selectedRecipients.length === 0 || !this.sendForm.subject) {
|
|
|
|
|
return '请先完善发送设置...'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sampleUser = this.selectedRecipients[0]
|
|
|
|
|
const subjectPreview = this.replaceVariables(this.sendForm.subject, sampleUser)
|
|
|
|
|
const contentPreview = this.replaceVariables(this.selectedTemplate.content, sampleUser)
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<strong>主题:</strong>${subjectPreview}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<strong>收件人:</strong>${sampleUser.name} (${sampleUser.email})
|
|
|
|
|
</div>
|
|
|
|
|
<hr>
|
|
|
|
|
<div>${contentPreview.replace(/\n/g, '<br>')}</div>
|
|
|
|
|
`
|
|
|
|
|
sampleUser() {
|
|
|
|
|
return this.selectedRecipients[0] || {}
|
|
|
|
|
},
|
|
|
|
|
subjectPreview() {
|
|
|
|
|
if (!this.selectedTemplate || !this.sendForm.subject) return ''
|
|
|
|
|
return this.replaceVariables(this.sendForm.subject, this.sampleUser)
|
|
|
|
|
},
|
|
|
|
|
contentPreview() {
|
|
|
|
|
if (!this.selectedTemplate) return ''
|
|
|
|
|
return this.replaceVariables(this.selectedTemplate.content, this.sampleUser)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
templatePreviewContent() {
|
|
|
|
|
@ -565,6 +539,24 @@ export default {
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
methods: {
|
|
|
|
|
loadTemplates() {
|
|
|
|
|
getEmailTemplateList().then(res => {
|
|
|
|
|
// 处理API返回的数据结构
|
|
|
|
|
this.emailTemplates = (res.data || []).map(t => ({
|
|
|
|
|
id: t.id,
|
|
|
|
|
name: t.title, // API返回的是title字段
|
|
|
|
|
description: t.description,
|
|
|
|
|
content: t.content,
|
|
|
|
|
variables: this.parseVariables(t.var), // 解析var字段为变量数组
|
|
|
|
|
createTime: t.created_at,
|
|
|
|
|
updatedTime: t.updated_at,
|
|
|
|
|
useCount: t.email_records_count || 0 // 使用email_records_count字段
|
|
|
|
|
}))
|
|
|
|
|
}).catch(error => {
|
|
|
|
|
console.error('加载模板失败:', error)
|
|
|
|
|
this.$message.error('加载模板失败')
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
// 模板相关方法
|
|
|
|
|
selectTemplate(templateId) {
|
|
|
|
|
this.selectedTemplateId = templateId
|
|
|
|
|
@ -587,7 +579,12 @@ export default {
|
|
|
|
|
editTemplate(templateId) {
|
|
|
|
|
const template = this.emailTemplates.find(t => t.id === templateId)
|
|
|
|
|
if (template) {
|
|
|
|
|
this.templateForm = { ...template }
|
|
|
|
|
this.templateForm = {
|
|
|
|
|
id: template.id, // 设置模板ID,用于区分编辑和创建
|
|
|
|
|
name: template.name,
|
|
|
|
|
description: template.description,
|
|
|
|
|
content: template.content
|
|
|
|
|
}
|
|
|
|
|
this.showTemplateModal = true
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
@ -596,14 +593,17 @@ export default {
|
|
|
|
|
const template = this.emailTemplates.find(t => t.id === templateId)
|
|
|
|
|
if (template) {
|
|
|
|
|
const newTemplate = {
|
|
|
|
|
...template,
|
|
|
|
|
id: Date.now(),
|
|
|
|
|
name: template.name + ' (副本)',
|
|
|
|
|
createTime: new Date().toLocaleString('zh-CN'),
|
|
|
|
|
useCount: 0
|
|
|
|
|
title: template.name + ' (副本)',
|
|
|
|
|
description: template.description,
|
|
|
|
|
content: template.content
|
|
|
|
|
}
|
|
|
|
|
this.emailTemplates.push(newTemplate)
|
|
|
|
|
this.$message.success('模板复制成功!')
|
|
|
|
|
saveEmailTemplate(newTemplate).then(() => {
|
|
|
|
|
this.$message.success('模板复制成功!')
|
|
|
|
|
this.loadTemplates()
|
|
|
|
|
}).catch(error => {
|
|
|
|
|
console.error('复制模板失败:', error)
|
|
|
|
|
this.$message.error('复制模板失败')
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
@ -613,11 +613,10 @@ export default {
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
type: 'warning'
|
|
|
|
|
}).then(() => {
|
|
|
|
|
this.emailTemplates = this.emailTemplates.filter(t => t.id !== templateId)
|
|
|
|
|
if (this.selectedTemplateId === templateId) {
|
|
|
|
|
this.selectedTemplateId = null
|
|
|
|
|
}
|
|
|
|
|
this.$message.success('删除成功!')
|
|
|
|
|
deleteEmailTemplate(templateId).then(() => {
|
|
|
|
|
this.$message.success('删除成功!')
|
|
|
|
|
this.loadTemplates()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
@ -677,25 +676,41 @@ export default {
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
parseExcelFile(file) {
|
|
|
|
|
// 模拟Excel解析
|
|
|
|
|
// 上传Excel文件到后端
|
|
|
|
|
const loading = this.$loading({
|
|
|
|
|
lock: true,
|
|
|
|
|
text: '正在解析Excel文件...',
|
|
|
|
|
text: '正在上传并解析Excel文件...',
|
|
|
|
|
spinner: 'el-icon-loading',
|
|
|
|
|
background: 'rgba(0, 0, 0, 0.7)'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const newUsers = [
|
|
|
|
|
{ id: Date.now() + 1, name: '新用户1', email: 'newuser1@example.com', company: '导入公司1', course: '导入课程1' },
|
|
|
|
|
{ id: Date.now() + 2, name: '新用户2', email: 'newuser2@example.com', company: '导入公司2', course: '导入课程2' },
|
|
|
|
|
{ id: Date.now() + 3, name: '新用户3', email: 'newuser3@example.com', company: '导入公司3', course: '导入课程3' }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
this.recipients = [...this.recipients, ...newUsers]
|
|
|
|
|
loading.close()
|
|
|
|
|
this.$message.success(`成功导入 ${newUsers.length} 个用户数据!`)
|
|
|
|
|
}, 2000)
|
|
|
|
|
const formData = new FormData()
|
|
|
|
|
formData.append('file', file)
|
|
|
|
|
uploadEmailRecord(formData)
|
|
|
|
|
.then(res => {
|
|
|
|
|
// 直接用res作为数组
|
|
|
|
|
const newUsers = (res || []).map(item => ({
|
|
|
|
|
email: item.email,
|
|
|
|
|
...item.var_data
|
|
|
|
|
}))
|
|
|
|
|
// 自动收集所有字段,跳过email字段
|
|
|
|
|
const allFields = new Set(this.recipientFields)
|
|
|
|
|
newUsers.forEach(user => {
|
|
|
|
|
Object.keys(user).forEach(key => {
|
|
|
|
|
if (key !== 'email') { // 跳过email字段
|
|
|
|
|
allFields.add(key)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
this.recipientFields = Array.from(allFields)
|
|
|
|
|
this.recipients = [...this.recipients, ...newUsers]
|
|
|
|
|
this.$message.success(`成功导入 ${newUsers.length} 个用户数据!`)
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
this.$message.error('Excel导入失败,请检查文件格式')
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
loading.close()
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 收件人选择相关方法
|
|
|
|
|
@ -704,16 +719,22 @@ export default {
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
toggleSelectAll() {
|
|
|
|
|
this.$refs.recipientTable.toggleAllSelection()
|
|
|
|
|
// 安全检查:确保表格ref存在再调用方法
|
|
|
|
|
if (this.$refs.recipientTable && this.$refs.recipientTable.toggleAllSelection) {
|
|
|
|
|
this.$refs.recipientTable.toggleAllSelection()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
removeFromSelection(userId) {
|
|
|
|
|
this.selectedRecipients = this.selectedRecipients.filter(user => user.id !== userId)
|
|
|
|
|
removeFromSelectionByIndex(index) {
|
|
|
|
|
this.selectedRecipients.splice(index, 1)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
clearSelection() {
|
|
|
|
|
this.selectedRecipients = []
|
|
|
|
|
this.$refs.recipientTable.clearSelection()
|
|
|
|
|
// 安全检查:确保表格ref存在再调用方法
|
|
|
|
|
if (this.$refs.recipientTable && this.$refs.recipientTable.clearSelection) {
|
|
|
|
|
this.$refs.recipientTable.clearSelection()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
exportSelected() {
|
|
|
|
|
@ -722,15 +743,12 @@ export default {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建CSV内容
|
|
|
|
|
// 创建CSV内容 - 动态使用所有字段
|
|
|
|
|
const csvData = [
|
|
|
|
|
['姓名', '邮箱', '公司', '课程名称'],
|
|
|
|
|
...this.selectedRecipients.map(user => [
|
|
|
|
|
user.name,
|
|
|
|
|
user.email,
|
|
|
|
|
user.company,
|
|
|
|
|
user.course || ''
|
|
|
|
|
])
|
|
|
|
|
this.recipientFields, // 使用动态字段作为表头
|
|
|
|
|
...this.selectedRecipients.map(user =>
|
|
|
|
|
this.recipientFields.map(field => user[field] || '') // 按字段顺序获取值
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const csvContent = csvData.map(row => row.join(',')).join('\n')
|
|
|
|
|
@ -752,6 +770,7 @@ export default {
|
|
|
|
|
// 模板编辑相关方法
|
|
|
|
|
handleCloseTemplateModal() {
|
|
|
|
|
this.templateForm = {
|
|
|
|
|
id: null,
|
|
|
|
|
name: '',
|
|
|
|
|
description: '',
|
|
|
|
|
content: ''
|
|
|
|
|
@ -760,17 +779,15 @@ export default {
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
insertVariable(variable) {
|
|
|
|
|
const textarea = this.$refs.templateContentRef.$el.querySelector('textarea')
|
|
|
|
|
const start = textarea.selectionStart
|
|
|
|
|
const end = textarea.selectionEnd
|
|
|
|
|
const text = this.templateForm.content
|
|
|
|
|
|
|
|
|
|
this.templateForm.content = text.substring(0, start) + variable + text.substring(end)
|
|
|
|
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
textarea.focus()
|
|
|
|
|
textarea.setSelectionRange(start + variable.length, start + variable.length)
|
|
|
|
|
})
|
|
|
|
|
// 针对Tinymce富文本编辑器插入变量
|
|
|
|
|
const editor = window.tinymce && window.tinymce.activeEditor
|
|
|
|
|
if (editor) {
|
|
|
|
|
editor.insertContent(variable)
|
|
|
|
|
editor.focus()
|
|
|
|
|
} else {
|
|
|
|
|
// 兜底:直接拼接到内容末尾
|
|
|
|
|
this.templateForm.content += variable
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
updateTemplatePreview() {
|
|
|
|
|
@ -783,31 +800,39 @@ export default {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 提取变量
|
|
|
|
|
const variables = [...new Set(this.templateForm.content.match(/\{\{([^}]+)\}\}/g) || [])].map(v => v.slice(2, -2))
|
|
|
|
|
// 提取模板内容中的变量
|
|
|
|
|
const variables = this.extractVariables(this.templateForm.content)
|
|
|
|
|
const varString = variables.map(v => `{${v}}`).join(',')
|
|
|
|
|
|
|
|
|
|
const template = {
|
|
|
|
|
id: this.templateForm.id || Date.now(),
|
|
|
|
|
name: this.templateForm.name,
|
|
|
|
|
id: this.templateForm.id || undefined, // 新建时不传id
|
|
|
|
|
title: this.templateForm.name,
|
|
|
|
|
description: this.templateForm.description,
|
|
|
|
|
content: this.templateForm.content,
|
|
|
|
|
variables: variables,
|
|
|
|
|
createTime: new Date().toLocaleString('zh-CN'),
|
|
|
|
|
useCount: this.templateForm.useCount || 0
|
|
|
|
|
var: varString
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.templateForm.id) {
|
|
|
|
|
// 编辑现有模板
|
|
|
|
|
const index = this.emailTemplates.findIndex(t => t.id === this.templateForm.id)
|
|
|
|
|
this.emailTemplates.splice(index, 1, template)
|
|
|
|
|
this.$message.success('模板更新成功!')
|
|
|
|
|
saveEmailTemplate(template).then(() => {
|
|
|
|
|
this.$message.success('模板更新成功!')
|
|
|
|
|
this.loadTemplates() // 重新加载模板列表
|
|
|
|
|
this.handleCloseTemplateModal()
|
|
|
|
|
}).catch(error => {
|
|
|
|
|
console.error('更新模板失败:', error)
|
|
|
|
|
this.$message.error('更新模板失败')
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
// 创建新模板
|
|
|
|
|
this.emailTemplates.push(template)
|
|
|
|
|
this.$message.success('模板创建成功!')
|
|
|
|
|
saveEmailTemplate(template).then(res => {
|
|
|
|
|
this.$message.success('模板创建成功!')
|
|
|
|
|
this.loadTemplates() // 重新加载模板列表
|
|
|
|
|
this.handleCloseTemplateModal()
|
|
|
|
|
}).catch(error => {
|
|
|
|
|
console.error('创建模板失败:', error)
|
|
|
|
|
this.$message.error('创建模板失败')
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.handleCloseTemplateModal()
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 发送相关方法
|
|
|
|
|
@ -830,50 +855,38 @@ export default {
|
|
|
|
|
type: 'warning'
|
|
|
|
|
}
|
|
|
|
|
).then(() => {
|
|
|
|
|
this.simulateSending()
|
|
|
|
|
this.sendEmailRecord()
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
simulateSending() {
|
|
|
|
|
sendEmailRecord() {
|
|
|
|
|
if (!this.selectedTemplate || this.selectedRecipients.length === 0) {
|
|
|
|
|
this.$message.error('请先选择模板和收件人')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const params = {
|
|
|
|
|
subject: this.sendForm.subject,
|
|
|
|
|
email_template_id: this.selectedTemplate.id,
|
|
|
|
|
email_record_users: this.selectedRecipients.map(user => ({
|
|
|
|
|
email: user.email,
|
|
|
|
|
var_data: { ...user }
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
if (this.sendForm.sendMode === 'schedule' && this.sendForm.scheduleTime) {
|
|
|
|
|
params.time = this.sendForm.scheduleTime
|
|
|
|
|
}
|
|
|
|
|
this.showSendProgress = true
|
|
|
|
|
this.sendProgress = 0
|
|
|
|
|
this.sendStatusList = this.selectedRecipients.map(user => ({
|
|
|
|
|
...user,
|
|
|
|
|
status: 'pending'
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
let sent = 0
|
|
|
|
|
const total = this.selectedRecipients.length
|
|
|
|
|
|
|
|
|
|
const sendInterval = setInterval(() => {
|
|
|
|
|
if (sent < total) {
|
|
|
|
|
// 模拟发送成功/失败
|
|
|
|
|
const success = Math.random() > 0.1 // 90%成功率
|
|
|
|
|
this.sendStatusList[sent].status = success ? 'sent' : 'failed'
|
|
|
|
|
|
|
|
|
|
sent++
|
|
|
|
|
this.sendProgress = Math.round((sent / total) * 100)
|
|
|
|
|
|
|
|
|
|
if (sent === total) {
|
|
|
|
|
clearInterval(sendInterval)
|
|
|
|
|
this.$message.success('邮件发送完成!')
|
|
|
|
|
|
|
|
|
|
// 添加到发送记录
|
|
|
|
|
const newRecord = {
|
|
|
|
|
id: Date.now(),
|
|
|
|
|
templateName: this.selectedTemplate.name,
|
|
|
|
|
recipientCount: total,
|
|
|
|
|
successCount: this.sendStatusList.filter(s => s.status === 'sent').length,
|
|
|
|
|
failCount: this.sendStatusList.filter(s => s.status === 'failed').length,
|
|
|
|
|
status: 'completed',
|
|
|
|
|
sendTime: new Date().toLocaleString('zh-CN'),
|
|
|
|
|
details: '发送完成'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.sendHistory.unshift(newRecord)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, 1000)
|
|
|
|
|
saveEmailRecord(params)
|
|
|
|
|
.then(res => {
|
|
|
|
|
this.$message.success('邮件发送请求已提交!')
|
|
|
|
|
this.showSendProgress = false
|
|
|
|
|
// 可选:刷新历史、重置表单等
|
|
|
|
|
})
|
|
|
|
|
.catch(err => {
|
|
|
|
|
this.$message.error('邮件发送失败')
|
|
|
|
|
this.showSendProgress = false
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 发送记录相关方法
|
|
|
|
|
@ -883,19 +896,52 @@ export default {
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 工具方法
|
|
|
|
|
replaceVariables(text, userData, isPreview = false) {
|
|
|
|
|
let result = text
|
|
|
|
|
.replace(/\{\{姓名\}\}/g, userData.name || '示例姓名')
|
|
|
|
|
.replace(/\{\{邮箱\}\}/g, userData.email || 'example@email.com')
|
|
|
|
|
.replace(/\{\{公司\}\}/g, userData.company || '示例公司')
|
|
|
|
|
.replace(/\{\{课程名称\}\}/g, userData.course || '示例课程')
|
|
|
|
|
.replace(/\{\{开始时间\}\}/g, '2025-06-15 09:00')
|
|
|
|
|
.replace(/\{\{地点\}\}/g, '苏州科技大学')
|
|
|
|
|
extractVariables(content) {
|
|
|
|
|
if (!content) return []
|
|
|
|
|
// 从模板内容中提取变量,匹配 {变量名} 格式
|
|
|
|
|
const variableRegex = /\{([^}]+)\}/g
|
|
|
|
|
const variables = []
|
|
|
|
|
let match
|
|
|
|
|
|
|
|
|
|
while ((match = variableRegex.exec(content)) !== null) {
|
|
|
|
|
const variable = match[1].trim()
|
|
|
|
|
if (variable && !variables.includes(variable)) {
|
|
|
|
|
variables.push(variable)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return variables
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
if (isPreview) {
|
|
|
|
|
result = result.replace(/\{\{([^}]+)\}\}/g, '<span class="variable-tag">{{$1}}</span>')
|
|
|
|
|
parseVariables(varString) {
|
|
|
|
|
if (!varString) return []
|
|
|
|
|
// 解析API返回的变量字符串,格式如:"{姓名},{学校}"
|
|
|
|
|
try {
|
|
|
|
|
return varString.split(',').map(v => v.trim().replace(/[{}]/g, ''))
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('解析变量失败:', error)
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
replaceVariables(text, userData, isPreview = false) {
|
|
|
|
|
// 替换变量 - 使用表格列的Label来匹配
|
|
|
|
|
let result = text.replace(/\{([^}]+)\}/g, (match, varName) => {
|
|
|
|
|
varName = varName.trim()
|
|
|
|
|
|
|
|
|
|
// 直接从userData中查找对应的字段值
|
|
|
|
|
if (userData[varName] !== undefined) {
|
|
|
|
|
return userData[varName] || ''
|
|
|
|
|
} else {
|
|
|
|
|
// 实时预览时,非匹配变量保持原样
|
|
|
|
|
return isPreview ? match : ''
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 实时预览时高亮所有变量
|
|
|
|
|
if (isPreview) {
|
|
|
|
|
result = result.replace(/\{([^}]+)\}/g, '<span class="variable-tag">{$1}</span>')
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
@ -917,6 +963,14 @@ export default {
|
|
|
|
|
failed: '发送失败'
|
|
|
|
|
}
|
|
|
|
|
return statusMap[status] || '未知状态'
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
loadSendHistory() {
|
|
|
|
|
getEmailRecordList().then(res => {
|
|
|
|
|
this.sendHistory = res.data || []
|
|
|
|
|
}).catch(error => {
|
|
|
|
|
this.$message.error('获取发送记录失败')
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -1096,6 +1150,14 @@ export default {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recipient-table .el-table {
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recipient-table .el-table__body-wrapper {
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-header {
|
|
|
|
|
background: #409EFF;
|
|
|
|
|
color: white;
|
|
|
|
|
@ -1153,6 +1215,14 @@ export default {
|
|
|
|
|
margin-top: 15px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.selected-preview .el-table {
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.selected-preview .el-table__body-wrapper {
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-selection {
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: #6c757d;
|
|
|
|
|
@ -1178,6 +1248,14 @@ export default {
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-table .el-table {
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-table .el-table__body-wrapper {
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.more-info {
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: #6c757d;
|
|
|
|
|
@ -1245,6 +1323,7 @@ export default {
|
|
|
|
|
max-height: 400px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.variable-tags {
|
|
|
|
|
|