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>
<template>
<div class="signature-management-container bg-white rounded-lg shadow-md p-6 mb-6">
<!-- 顶部操作区 -->
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold text-gray-800">签名管理</h2>
<el-button type="primary" @click="openAddSignatureModal">
<el-icon><Plus /></el-icon>
添加签名
</el-button>
</div>
<!-- 签名列表 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<el-table
:data="signatureList"
border
stripe
empty-text="暂无签名,系统将自动创建默认签名模板"
:loading="loading"
>
<el-table-column prop="name" label="签名名称" min-width="150">
<template #default="scope">
<div class="flex items-center">
<span>{{ scope.row.name }}</span>
<el-tag v-if="scope.row.isDefault" size="small" type="success" class="ml-2"
>默认</el-tag
>
<el-tag v-if="scope.row.type === 'template'" size="small" type="info" class="ml-2">
公司
</el-tag>
<el-tag v-if="scope.row.type === 'custom'" size="small" type="warning" class="ml-2">
自定义
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="签名预览" min-width="300">
<template #default="scope">
<div class="signature-preview text-sm" v-html="getSignaturePreview(scope.row)"></div>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="300" fixed="right">
<template #default="scope">
<el-button
size="small"
type="primary"
text
@click="setDefaultSignature(scope.row.id)"
:disabled="scope.row.isDefault"
>
{{ scope.row.isDefault ? '已设为默认' : '设为默认' }}
</el-button>
<el-button size="small" type="warning" text @click="editSignature(scope.row)">
编辑
</el-button>
<el-button
size="small"
type="danger"
v-if="scope.row.type !== 'template'"
text
@click="deleteSignature(scope.row.id)"
:disabled="scope.row.isDefault"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 添加/编辑签名弹窗 -->
<el-dialog
v-model="signatureModalVisible"
:title="isEdit ? '编辑签名' : '添加签名'"
width="800px"
>
<el-form
ref="signatureFormRef"
:model="signatureForm"
:rules="signatureRules"
label-width="100px"
>
<!-- 基础信息 -->
<el-form-item label="基础设置" prop="name">
<el-row :gutter="10" class="name-default-row">
<!-- 签名名称输入框(占主要宽度) -->
<el-col :span="18">
<el-input
v-model="signatureForm.name"
placeholder="请输入签名名称(如:默认签名、商务签名)"
maxlength="50"
show-word-limit
style="width: 100%"
/>
</el-col>
<!-- 设为默认签名开关(靠右) -->
<el-col :span="6" class="default-switch-col">
<el-form-item label="设为默认签名" class="default-switch-item">
<el-switch v-model="signatureForm.isDefault" />
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<!-- 模板型签名配置 -->
<div v-if="signatureForm.type === 'template'" class="template-config-section mt-4">
<h4 class="font-medium mb-3 text-gray-700">字段设置</h4>
<!-- 基础信息配置 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="12">
<el-form-item label="姓名">
<el-input
v-model="signatureForm.config.name"
placeholder="请输入姓名"
maxlength="50"
show-word-limit
/>
</el-form-item>
<el-form-item label="职位">
<el-input
v-model="signatureForm.config.title"
placeholder="请输入职位"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-form-item label="公司名称">
<el-input
v-model="signatureForm.config.companyName"
placeholder="请输入公司名称"
maxlength="100"
show-word-limit
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="电话">
<el-input
v-model="signatureForm.config.phone"
placeholder="请输入联系电话"
maxlength="20"
show-word-limit
/>
</el-form-item>
<el-form-item label="邮箱">
<el-input
v-model="signatureForm.config.email"
placeholder="请输入邮箱地址"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-form-item label="公司地址">
<el-input
v-model="signatureForm.config.address"
placeholder="请输入公司地址"
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
<!-- 字段显示配置 -->
<div class="field-show-config mt-4">
<h4 class="font-medium mb-3 text-gray-700">字段显示配置</h4>
<el-row :gutter="10">
<el-col :span="6">
<el-form-item label="姓名">
<el-switch v-model="signatureForm.config.showName" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="职位">
<el-switch v-model="signatureForm.config.showTitle" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="电话">
<el-switch v-model="signatureForm.config.showPhone" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="邮箱">
<el-switch v-model="signatureForm.config.showEmail" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="公司名称">
<el-switch v-model="signatureForm.config.showCompanyName" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="LOGO">
<el-switch v-model="signatureForm.config.showLogo" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="地址">
<el-switch v-model="signatureForm.config.showAddress" />
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 模板预览 -->
<div class="template-preview mt-4 p-3 border rounded bg-gray-50">
<h4 class="font-medium mb-2 text-gray-700">模板预览</h4>
<div v-html="generateTemplatePreview()" class="preview-content text-sm"></div>
</div>
</div>
<!-- 自定义签名配置 -->
<div v-if="signatureForm.type === 'custom'" class="custom-config-section mt-4">
<el-form-item label="签名内容">
<RichTextEditor v-model="signatureForm.customContent" />
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button @click="signatureModalVisible = false">取消</el-button>
<el-button type="primary" @click="saveSignature" :loading="submitLoading"> 保存 </el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { format } from 'date-fns'
import RichTextEditor from '@/components/RichTextEditor.vue'
import { Signature } from '@/types/signature'
// 基础状态管理
const loading = ref(false)
const submitLoading = ref(false)
const signatureModalVisible = ref(false)
const isEdit = ref(false)
// 签名列表
const signatureList = ref<Signature[]>([])
// 签名表单
const signatureFormRef = ref()
const signatureForm = reactive<Signature>({
id: '',
name: '',
type: 'custom',
isDefault: false,
config: {
companyName: 'Yindun Insurance Brokers Co..Ltd',
logoUrl: '',
name: '',
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,
showPhone: true,
showEmail: true,
showAddress: true,
},
customContent: '',
createTime: '',
updateTime: '',
})
// 签名表单校验规则
const signatureRules = reactive({
name: [
{ required: true, message: '请输入签名名称', trigger: 'blur' },
{ min: 2, max: 50, message: '签名名称长度在 2 到 50 个字符', trigger: 'blur' },
],
type: [{ required: true, message: '请选择签名类型', trigger: 'change' }],
})
// 获取签名列表
const fetchSignatureList = async () => {
try {
loading.value = true
// 实际项目中替换为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: '张三',
title: '产品经理',
phone: '13800138000',
email: 'zhangsan@example.com',
address:
'Room 3063, Tower No.8 Shuang Xing , No 1 Weigongcun Ave, Haidian District, Beijing. P.R. China',
showCompanyName: true,
showLogo: true,
showName: true,
showTitle: true,
showPhone: true,
showEmail: true,
showAddress: true,
},
customContent: '',
createTime: '2025-01-10 00:00:00',
updateTime: '2025-01-10 00:00:00',
},
]
} catch (error) {
ElMessage.error('获取签名列表失败')
console.error(error)
} finally {
loading.value = false
}
}
// 生成模板型签名预览HTML
const generateTemplatePreview = (): string => {
const config = signatureForm.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.showTitle && config.title) {
html += ` | ${config.title}`
}
html += '</p>'
}
// LOGO
if (config.showLogo && config.logoUrl) {
html += `<img src="${config.logoUrl}" alt="公司LOGO" style="width: 120px; height: auto; margin: 4px 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;">M:+86 ${config.phone}</p>`
}
if (config.showEmail && config.email) {
html += `<p style="margin: 0; font-size: 12px;">E:${config.email}</p>`
}
if (config.showAddress && config.address) {
html += `<p style="margin: 0; font-size: 12px;">A:${config.address}</p>`
}
html += '</div>'
return html
}
// 获取签名预览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; max-height: 200px; overflow: hidden;">'
if (config.showName) {
html += `<p style="margin: 0; font-weight: bold; font-size: 14px;">${config.name}`
if (config.showTitle && config.title) {
html += ` | ${config.title}`
}
html += '</p>'
}
if (config.showLogo && config.logoUrl) {
html += `<img src="${config.logoUrl}" alt="公司LOGO" style="width: 100px; height: auto; margin: 4px 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;">M:+86 ${config.phone}</p>`
}
if (config.showEmail && config.email) {
html += `<p style="margin: 0; font-size: 12px;">E:${config.email}</p>`
}
if (config.showAddress && config.address) {
html += `<p style="margin: 0; font-size: 12px;">A:${config.address}</p>`
}
html += '</div>'
return html
} else {
// 自定义签名预览(限制高度)
return `<div style="max-height: 200px; overflow: hidden;">${signature.customContent}</div>`
}
}
// 重置表单
const resetForm = () => {
signatureForm.id = ''
signatureForm.name = ''
signatureForm.type = 'custom'
signatureForm.isDefault = false
signatureForm.config = {
companyName: 'Yindun Insurance Brokers Co..Ltd',
logoUrl: '',
name: '',
title: '',
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,
showTitle: true,
showPhone: true,
showEmail: true,
showAddress: true,
}
signatureForm.customContent = ''
signatureForm.createTime = ''
signatureForm.updateTime = ''
signatureFormRef.value?.resetFields()
}
// 保存签名
const saveSignature = async () => {
try {
await signatureFormRef.value?.validate()
submitLoading.value = true
// 确保只有一个默认签名
if (signatureForm.isDefault) {
signatureList.value.forEach((s) => {
s.isDefault = false
})
}
if (isEdit.value) {
// 编辑签名
// const res = await api.put(`/email/signatures/${signatureForm.id}`, signatureForm);
const index = signatureList.value.findIndex((item) => item.id === signatureForm.id)
if (index > -1) {
signatureList.value[index] = {
...signatureList.value[index],
...signatureForm,
updateTime: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
}
}
ElMessage.success('签名编辑成功')
} else {
// 添加新签名
// const res = await api.post('/email/signatures', signatureForm);
const newSignature: Signature = {
...signatureForm,
id: Date.now(),
createTime: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updateTime: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
}
signatureList.value.unshift(newSignature)
ElMessage.success('签名添加成功')
}
signatureModalVisible.value = false
fetchSignatureList()
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
// 设置默认签名
const setDefaultSignature = async (id: string | number) => {
try {
// await api.post(`/email/signatures/${id}/set-default`);
signatureList.value.forEach((item) => {
item.isDefault = item.id === id
})
ElMessage.success('默认签名设置成功')
} catch (error) {
ElMessage.error('设置默认签名失败')
console.error(error)
}
}
// 编辑签名
const editSignature = (signature: Signature) => {
isEdit.value = true
Object.assign(signatureForm, signature)
signatureModalVisible.value = true
}
// 删除签名
const deleteSignature = async (id: string | number) => {
try {
const confirm = await ElMessageBox.confirm('确定要删除该签名吗?此操作不可撤销', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
if (confirm) {
// await api.delete(`/email/signatures/${id}`);
const index = signatureList.value.findIndex((item) => item.id === id)
if (index > -1) {
signatureList.value.splice(index, 1)
// 如果删除的是默认签名,且还有其他签名,将第一个设为默认
if (signatureList.value.length > 0 && !signatureList.value.some((s) => s.isDefault)) {
signatureList.value[0].isDefault = true
}
}
ElMessage.success('签名删除成功')
}
} catch (error) {
console.error(error)
}
}
// 打开添加签名弹窗
const openAddSignatureModal = () => {
isEdit.value = false
resetForm()
// 初始化默认配置(复制系统默认签名的配置)
const defaultSignature = signatureList.value.find((s) => s.isDefault)
if (defaultSignature && defaultSignature.type === 'template') {
signatureForm.config = { ...defaultSignature.config }
// 取消显示LOGO(避免重复)
signatureForm.config.logoUrl = ''
}
signatureModalVisible.value = true
}
// 初始化
onMounted(() => {
fetchSignatureList()
})
</script>
<style scoped>
.signature-management-container {
@apply bg-gray-50 min-h-[800px];
}
.avatar-uploader {
@apply w-40 h-40 border border-dashed rounded-md flex flex-col items-center justify-center cursor-pointer;
}
.avatar {
@apply w-full h-full object-cover rounded-md;
}
.upload-icon {
@apply flex flex-col items-center justify-center text-gray-400;
}
.upload-icon .text {
@apply mt-2 text-xs;
}
.signature-preview {
@apply max-h-80 overflow-hidden leading-relaxed;
}
.template-preview .preview-content {
@apply max-h-80 overflow-auto;
}
.template-config-section,
.custom-config-section {
@apply border-t pt-4;
}
</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