Commit b0197048 by Sweet Zhang

先不做签名管理,不通过后端,由前端拼接

parent f7789154
...@@ -11,14 +11,17 @@ ...@@ -11,14 +11,17 @@
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@fortawesome/fontawesome-free": "^7.0.1", "@fortawesome/fontawesome-free": "^7.0.1",
"axios": "^1.12.2", "axios": "^1.12.2",
"date-fns": "^4.1.0",
"element-plus": "^2.11.3", "element-plus": "^2.11.3",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-router": "^4.5.1" "vue-router": "^4.5.1",
"wangeditor": "^4.7.15"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.54.1", "@playwright/test": "^1.54.1",
"@tsconfig/node22": "^22.0.2", "@tsconfig/node22": "^22.0.2",
"@types/date-fns": "^2.5.3",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/node": "^22.16.5", "@types/node": "^22.16.5",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
...@@ -490,6 +493,27 @@ ...@@ -490,6 +493,27 @@
"@babel/core": "^7.0.0-0" "@babel/core": "^7.0.0-0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/runtime-corejs3": {
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz",
"integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==",
"license": "MIT",
"dependencies": {
"core-js-pure": "^3.43.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.27.2", "version": "7.27.2",
"resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz", "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz",
...@@ -1896,6 +1920,13 @@ ...@@ -1896,6 +1920,13 @@
"@types/deep-eql": "*" "@types/deep-eql": "*"
} }
}, },
"node_modules/@types/date-fns": {
"version": "2.5.3",
"resolved": "https://registry.npmmirror.com/@types/date-fns/-/date-fns-2.5.3.tgz",
"integrity": "sha512-4KVPD3g5RjSgZtdOjvI/TDFkLNUHhdoWxmierdQbDeEg17Rov0hbBYtIzNaQA67ORpteOhvR9YEMTb6xeDCang==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/deep-eql": { "node_modules/@types/deep-eql": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz", "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz",
...@@ -3471,6 +3502,17 @@ ...@@ -3471,6 +3502,17 @@
"url": "https://github.com/sponsors/mesqueeb" "url": "https://github.com/sponsors/mesqueeb"
} }
}, },
"node_modules/core-js-pure": {
"version": "3.47.0",
"resolved": "https://registry.npmmirror.com/core-js-pure/-/core-js-pure-3.47.0.tgz",
"integrity": "sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
...@@ -3533,6 +3575,16 @@ ...@@ -3533,6 +3575,16 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/dayjs": { "node_modules/dayjs": {
"version": "1.11.18", "version": "1.11.18",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz", "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz",
...@@ -7059,6 +7111,12 @@ ...@@ -7059,6 +7111,12 @@
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
...@@ -7742,6 +7800,17 @@ ...@@ -7742,6 +7800,17 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/wangeditor": {
"version": "4.7.15",
"resolved": "https://registry.npmmirror.com/wangeditor/-/wangeditor-4.7.15.tgz",
"integrity": "sha512-aPTdREd8BxXVyJ5MI+LU83FQ7u1EPd341iXIorRNYSOvoimNoZ4nPg+yn3FGbB93/owEa6buLw8wdhYnMCJQLg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.11.2",
"@babel/runtime-corejs3": "^7.11.2",
"tslib": "^2.1.0"
}
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
......
...@@ -21,14 +21,17 @@ ...@@ -21,14 +21,17 @@
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@fortawesome/fontawesome-free": "^7.0.1", "@fortawesome/fontawesome-free": "^7.0.1",
"axios": "^1.12.2", "axios": "^1.12.2",
"date-fns": "^4.1.0",
"element-plus": "^2.11.3", "element-plus": "^2.11.3",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-router": "^4.5.1" "vue-router": "^4.5.1",
"wangeditor": "^4.7.15"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.54.1", "@playwright/test": "^1.54.1",
"@tsconfig/node22": "^22.0.2", "@tsconfig/node22": "^22.0.2",
"@types/date-fns": "^2.5.3",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/node": "^22.16.5", "@types/node": "^22.16.5",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
......
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
:multiple="uploadConfig.multiple" :multiple="uploadConfig.multiple"
@change="handleFileInputChange" @change="handleFileInputChange"
/> />
<el-icon class="text-5xl text-gray-400 mb-4"> <el-icon class="text-5xl text-gray-400 mb-4">
<UploadFilled /> <UploadFilled />
</el-icon> </el-icon>
......
<template>
<div class="rich-text-editor">
<div ref="editorRef" class="editor-container"></div>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits, watch, onMounted, onUnmounted } from 'vue'
import WangEditor from 'wangeditor'
import axios from 'axios'
import { env } from '@/utils/env' // 之前创建的环境配置文件
// Props 定义(保持和原组件一致,无缝替换)
const props = defineProps<{
modelValue: string
config?: {
height?: number
uploadUrl?: string
placeholder?: string
}
}>()
// Emits 定义
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'change', value: string): void
}>()
// 默认配置
const defaultConfig = {
height: 300,
uploadUrl: `${import.meta.env.VITE_REMOTE_API_BASE_URL}/oss/api/oss/upload`,
placeholder: '请输入内容...',
}
const config = { ...defaultConfig, ...props.config }
// 编辑器实例
const editorRef = ref<HTMLDivElement | null>(null)
let editor: WangEditor | null = null
const content = ref(props.modelValue)
// 初始化编辑器
onMounted(() => {
if (!editorRef.value) return
// 创建编辑器
editor = new WangEditor(editorRef.value)
// 配置
editor.config.height = config.height
editor.config.placeholder = config.placeholder
editor.config.showLinkImg = false // 隐藏网络图片功能
editor.config.uploadImgServer = config.uploadUrl // 图片上传接口
editor.config.uploadImgHeaders = {
Authorization: `Bearer ${localStorage.getItem('token')}`,
}
editor.config.uploadImgMaxSize = 2 * 1024 * 1024 // 2MB
editor.config.uploadImgMaxLength = 1 // 单次上传1张(LOGO)
editor.config.onchange = (html: string) => {
content.value = html
emit('update:modelValue', html)
emit('change', html)
}
// 图片上传成功处理
editor.config.uploadImgHooks = {
success: (xhr: any, editor: any, result: any) => {
if (result.code === 200) {
return result.data.url
}
},
fail: (xhr: any, editor: any, result: any) => {
console.error('图片上传失败:', result)
},
}
// 创建编辑器
editor.create()
// 设置初始内容
if (props.modelValue) {
editor.txt.html(props.modelValue)
}
})
// 监听外部内容变化
watch(
() => props.modelValue,
(newVal) => {
if (editor && editor.txt.html() !== newVal) {
editor.txt.html(newVal)
content.value = newVal
}
},
{ immediate: true },
)
// 销毁编辑器
onUnmounted(() => {
if (editor) {
editor.destroy()
editor = null
}
})
</script>
<style scoped>
.rich-text-editor {
border: 1px solid #e5e7eb;
border-radius: 4px;
width: 100%;
}
.editor-container {
min-height: 300px;
padding: 10px;
}
/* 适配 wangEditor 样式 */
:deep(.w-e-toolbar) {
border-bottom: 1px solid #e5e7eb;
flex-wrap: wrap;
}
:deep(.w-e-text-container) {
min-height: 250px;
}
</style>
...@@ -46,6 +46,11 @@ const router = createRouter({ ...@@ -46,6 +46,11 @@ const router = createRouter({
component: () => import('../views/EmailManagement.vue'), component: () => import('../views/EmailManagement.vue'),
}, },
{ {
path: '/signature-management',
name: 'signature-management',
component: () => import('../views/SignatureManagement.vue'),
},
{
path: '/login', path: '/login',
name: 'login', name: 'login',
component: () => import('../views/LoginPage.vue'), component: () => import('../views/LoginPage.vue'),
......
...@@ -108,6 +108,8 @@ export interface EmailForm { ...@@ -108,6 +108,8 @@ export interface EmailForm {
ccEmails?: string ccEmails?: string
scheduleSend?: boolean scheduleSend?: boolean
scheduleTime?: string scheduleTime?: string
signatureId?: string
customContent?: string
} }
//邮件服务商类型 //邮件服务商类型
export interface EmailProvider extends Pagination<EmailProvider> { export interface EmailProvider extends Pagination<EmailProvider> {
......
// 签名基础类型
export interface Signature {
id: string | number
name: string // 签名名称
type: 'template' | 'custom' // 类型:模板型/自定义型
isDefault: boolean // 是否默认签名
config: {
// 模板型签名配置
companyName: string // 公司名称
logoUrl: string // LOGO地址
name: string // 姓名
alias: string // 别名/职位
phone: string // 电话
email: string // 邮箱
address: string // 地址
// 字段显示配置
showCompanyName: boolean
showLogo: boolean
showName: boolean
showAlias: boolean
showPhone: boolean
showEmail: boolean
showAddress: boolean
}
customContent: string // 自定义富文本内容
createTime: string
updateTime: string
}
// 签名字段配置项
export interface SignatureField {
key: keyof Signature['config']
label: string
type: 'text' | 'image' | 'boolean'
placeholder?: string
}
...@@ -33,6 +33,12 @@ export const menuConfig: MenuItem[] = [ ...@@ -33,6 +33,12 @@ export const menuConfig: MenuItem[] = [
title: '变量管理', title: '变量管理',
}, },
{ {
name: 'signatures',
path: '/signature-management',
icon: 'fas fa-file-excel',
title: '签名管理',
},
{
name: 'emails', name: 'emails',
path: '/emails', path: '/emails',
icon: 'fas fa-history', icon: 'fas fa-history',
...@@ -47,4 +53,5 @@ export const pageTitles: Record<string, string> = { ...@@ -47,4 +53,5 @@ export const pageTitles: Record<string, string> = {
senders: '发件人管理', senders: '发件人管理',
variables: '变量管理', variables: '变量管理',
emails: '邮件记录', emails: '邮件记录',
signatures: '签名管理',
} }
...@@ -137,6 +137,24 @@ ...@@ -137,6 +137,24 @@
placeholder="邮件主题" placeholder="邮件主题"
/> />
</div> </div>
<!-- <div class="mb-4">
<label class="block text-gray-700 mb-2 font-medium">签名</label>
<el-select
v-model="emailForm.signatureId"
placeholder="选择签名"
class="flex-auto"
size="large"
@change="handleSignatureChange"
>
<el-option
v-for="signature in signatureList"
:key="signature.id"
:label="signature.name"
:value="signature.id"
/>
</el-select>
<el-button type="text" @click="goToSignatureManagement" class="ml-2"> 管理签名 </el-button>
</div> -->
<div class="mb-4"> <div class="mb-4">
<label class="block text-gray-700 mb-2 font-medium">正文</label> <label class="block text-gray-700 mb-2 font-medium">正文</label>
<div class="border border-gray-300 rounded-md overflow-hidden"> <div class="border border-gray-300 rounded-md overflow-hidden">
...@@ -159,7 +177,17 @@ ...@@ -159,7 +177,17 @@
<div class="mb-4"> <div class="mb-4">
<label class="block text-gray-700 mb-2 font-medium">附件</label> <label class="block text-gray-700 mb-2 font-medium">附件</label>
<div class="mb-10 p-6 bg-white rounded-lg shadow-md"> <div class="mb-10 p-6 bg-white rounded-lg shadow-md">
<FileUploadComponent v-model="uploadedFiles" @success="handleDocumentUploadSuccess" /> <FileUploadComponent
v-model="uploadedFiles"
:config="uploadConfig"
@success="handleDocumentUploadSuccess"
/>
</div>
</div>
<div class="mb-4">
<label class="block text-gray-700 mb-2 font-medium">签名</label>
<div class="mb-10 p-6 bg-white rounded-lg shadow-md">
<RichTextEditor v-model="emailForm.customContent" />
</div> </div>
</div> </div>
...@@ -242,9 +270,11 @@ ...@@ -242,9 +270,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted } from 'vue' import { ref, watch, onMounted, computed } from 'vue'
import { ElMessageBox } from 'element-plus' import { ElMessageBox } from 'element-plus'
import { Signature } from '@/types/signature'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import RichTextEditor from '@/components/RichTextEditor.vue'
const router = useRouter() const router = useRouter()
...@@ -266,6 +296,76 @@ import { ...@@ -266,6 +296,76 @@ import {
importContactApi, importContactApi,
} from '../api/api' } from '../api/api'
const dialogVisible = ref(false) const dialogVisible = ref(false)
const uploadConfig = {
multiple: true,
}
// 签名列表
const signatureList = ref<Signature[]>([])
// 获取签名列表
const fetchSignatureList = async () => {
try {
// 实际项目中替换为API请求
// const res = await api.get('/email/signatures');
// signatureList.value = res.data;
// 模拟数据
signatureList.value = [
{
id: 1,
name: '默认签名',
type: 'template',
isDefault: true,
config: {
companyName: 'Yindun Insurance Brokers Co..Ltd',
logoUrl: 'https://m.zuihuibi.cn/ydLife/assets/images/ydinsurance_logo.png',
name: '',
alias: '',
phone: '',
email: '',
address:
'Room 3063, Tower No.8 Shuang Xing , No 1 Weigongcun Ave, Haidian District, Beijing. P.R. China',
showCompanyName: true,
showLogo: true,
showName: true,
showAlias: true,
showPhone: true,
showEmail: true,
showAddress: true,
},
customContent: '',
createTime: '',
updateTime: '',
},
]
// 默认选中默认签名
const defaultSignature = signatureList.value.find((item) => item.isDefault)
if (defaultSignature) {
emailForm.value.signatureId = defaultSignature.id
}
} catch (error) {
ElMessage.error('获取签名列表失败')
console.error(error)
}
}
// 当前选中的签名
const selectedSignature = computed(() => {
return signatureList.value.find((item) => item.id === emailForm.value.signatureId) || null
})
// 切换签名
const handleSignatureChange = () => {
if (selectedSignature.value) {
ElMessage.success(`已切换为:${selectedSignature.value.name}`)
}
}
// 跳转到签名管理页面
const goToSignatureManagement = () => {
router.push('/signature-management')
}
// 远程搜索方法 // 远程搜索方法
const remoteSearch = async (query: string, type: string) => { const remoteSearch = async (query: string, type: string) => {
...@@ -362,6 +462,8 @@ const emailForm = ref<EmailForm>({ ...@@ -362,6 +462,8 @@ const emailForm = ref<EmailForm>({
attachmentPath: '', attachmentPath: '',
sessionId: '', sessionId: '',
ccEmails: '', ccEmails: '',
signatureId: '',
customContent: '',
}) })
const selectedVariableTemplate = ref<VariableTemplate | null>(null) const selectedVariableTemplate = ref<VariableTemplate | null>(null)
...@@ -392,6 +494,7 @@ onMounted(() => { ...@@ -392,6 +494,7 @@ onMounted(() => {
getContacts() getContacts()
getGroups() getGroups()
applyVariableTemplate() applyVariableTemplate()
fetchSignatureList()
}) })
const loading = ref(false) const loading = ref(false)
const variablePrefix = '{{' const variablePrefix = '{{'
...@@ -525,8 +628,14 @@ const handleImportContacts = (results) => { ...@@ -525,8 +628,14 @@ const handleImportContacts = (results) => {
// 发送邮件 // 发送邮件
const sendEmail = () => { const sendEmail = () => {
// 构建完整的邮件内容(正文 + 签名)
const signatureContent = selectedSignature.value
? getSignaturePreview(selectedSignature.value)
: ''
const fullContent = `${emailForm.value.content}\n\n${signatureContent}`
const params = { const params = {
...emailForm.value, ...emailForm.value,
content: fullContent,
variableGroupBizId: selectedVariableTemplate.value?.variableGroupBizId || '', variableGroupBizId: selectedVariableTemplate.value?.variableGroupBizId || '',
senderBizId: currentSender.value?.senderBizId, senderBizId: currentSender.value?.senderBizId,
sendEmail: currentSender.value?.email || '', sendEmail: currentSender.value?.email || '',
...@@ -611,11 +720,56 @@ const handleDocumentUploadSuccess = (results: UploadResult[]) => { ...@@ -611,11 +720,56 @@ const handleDocumentUploadSuccess = (results: UploadResult[]) => {
ElMessage.success(`成功上传 ${successCount} 个文档`) ElMessage.success(`成功上传 ${successCount} 个文档`)
} }
// 获取签名预览HTML
const getSignaturePreview = (signature: Signature): string => {
if (signature.type === 'template') {
const config = signature.config
let html = '<div style="font-family: Arial, sans-serif; line-height: 1.8; color: #333;">'
if (config.showName) {
html += `<p style="margin: 0; font-weight: bold; font-size: 14px;">${config.name}`
if (config.showAlias && config.alias) {
html += ` | ${config.alias}`
}
html += '</p>'
}
if (config.showLogo && config.logoUrl) {
html += `<img src="${config.logoUrl}" alt="公司LOGO" style="width: 120px; height: auto; margin: 8px 0;" />`
}
if (config.showCompanyName && config.companyName) {
html += `<p style="margin: 0; font-size: 13px;">${config.companyName}</p>`
}
if (config.showPhone && config.phone) {
html += `<p style="margin: 0; font-size: 12px;">电话:${config.phone}</p>`
}
if (config.showEmail && config.email) {
html += `<p style="margin: 0; font-size: 12px;">邮箱:<a href="mailto:${config.email}" style="color: #409eff; text-decoration: none;">${config.email}</a></p>`
}
if (config.showAddress && config.address) {
html += `<p style="margin: 0; font-size: 12px;">地址:${config.address}</p>`
}
html += '</div>'
return html
} else {
return signature.customContent
}
}
// 测试发送邮件 // 测试发送邮件
const sendSelfEmail = () => { const sendSelfEmail = () => {
// 构建完整的邮件内容(正文 + 签名)
const signatureContent = selectedSignature.value
? getSignaturePreview(selectedSignature.value)
: ''
const fullContent = `${emailForm.value.content}\n\n${signatureContent}`
const params = { const params = {
...emailForm.value, ...emailForm.value,
content: fullContent,
variableGroupBizId: selectedVariableTemplate.value?.variableGroupBizId || '', variableGroupBizId: selectedVariableTemplate.value?.variableGroupBizId || '',
senderBizId: currentSender.value?.senderBizId, senderBizId: currentSender.value?.senderBizId,
sendEmail: currentSender.value?.email || '', sendEmail: currentSender.value?.email || '',
...@@ -629,8 +783,25 @@ const sendSelfEmail = () => { ...@@ -629,8 +783,25 @@ const sendSelfEmail = () => {
type: 'success', type: 'success',
}) })
} else { } else {
ElMessage.error(`测试邮件发送失败: ${res.data.msg || '未知错误'}`) ElMessage.error(`测试邮件发送失败: ${res.msg || '未知错误'}`)
} }
}) })
} }
</script> </script>
<style scoped>
.email-compose-container {
@apply bg-gray-50 min-h-[800px];
}
.email-content-editor {
@apply font-sans;
}
.signature-preview {
@apply bg-gray-50 p-3 rounded;
}
.signature-content {
line-height: 1.8;
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment