Commit fa6e5b55 by Sweet Zhang

封装接口

parent 26f467d0
VITE_API_BASE_URL='http://139.224.145.34:9002/email/api'
\ No newline at end of file
VITE_API_BASE_URL=/email/api
\ No newline at end of file
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --mode development",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview", "preview": "vite preview",
"test:unit": "vitest", "test:unit": "vitest",
...@@ -19,6 +19,8 @@ ...@@ -19,6 +19,8 @@
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^7.0.1", "@fortawesome/fontawesome-free": "^7.0.1",
"axios": "^1.12.2",
"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"
......
...@@ -80,20 +80,20 @@ ...@@ -80,20 +80,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import LoginPage from './components/LoginPage.vue' import LoginPage from './views/LoginPage.vue'
import Sidebar from './components/Sidebar.vue' import Sidebar from './views/Sidebar.vue'
import MobileSidebar from './components/MobileSidebar.vue' import MobileSidebar from './views/MobileSidebar.vue'
import ComposeEmail from './components/ComposeEmail.vue' import ComposeEmail from './views/ComposeEmail.vue'
import ContactManagement from './components/ContactManagement.vue' import ContactManagement from './views/ContactManagement.vue'
import SenderManagement from './components/SenderManagement.vue' import SenderManagement from './views/SenderManagement.vue'
import VariableManagement from './components/VariableManagement.vue' import VariableManagement from './views/VariableManagement.vue'
import EmailManagement from './components/EmailManagement.vue' import EmailManagement from './views/EmailManagement.vue'
import { Contact, Sender, Variable, VariableTemplate, Email } from './types' import type { Contact, Sender, Variable, VariableTemplate, Email } from './types'
// 状态管理 // 状态管理
const isLoginPage = ref(true) const isLoginPage = ref(true)
const isAuthenticated = ref(false) const isAuthenticated = ref(false)
const currentPage = ref('compose') const currentPage = ref<'compose' | 'contacts' | 'senders' | 'variables' | 'emails'>('compose')
const showMobileMenu = ref(false) const showMobileMenu = ref(false)
// 数据存储 // 数据存储
...@@ -127,11 +127,11 @@ const handleLogout = () => { ...@@ -127,11 +127,11 @@ const handleLogout = () => {
} }
const handlePageChange = (page: string) => { const handlePageChange = (page: string) => {
currentPage.value = page currentPage.value = page as 'compose' | 'contacts' | 'senders' | 'variables' | 'emails'
} }
const handleMobilePageChange = (page: string) => { const handleMobilePageChange = (page: string) => {
currentPage.value = page currentPage.value = page as 'compose' | 'contacts' | 'senders' | 'variables' | 'emails'
showMobileMenu.value = false showMobileMenu.value = false
} }
...@@ -155,7 +155,7 @@ const saveEmail = (email: Email) => { ...@@ -155,7 +155,7 @@ const saveEmail = (email: Email) => {
emails.value.push(email) emails.value.push(email)
} }
const reuseEmail = (emailData: any) => { const reuseEmail = (emailData: Email) => {
currentPage.value = 'compose' currentPage.value = 'compose'
// 这里可以传递需要复用的邮件数据到ComposeEmail组件 // 这里可以传递需要复用的邮件数据到ComposeEmail组件
// 实际实现中可以使用状态管理或props // 实际实现中可以使用状态管理或props
...@@ -164,26 +164,7 @@ const reuseEmail = (emailData: any) => { ...@@ -164,26 +164,7 @@ const reuseEmail = (emailData: any) => {
const loadInitialData = () => { const loadInitialData = () => {
// 模拟加载初始数据 // 模拟加载初始数据
// 联系人 // 联系人
contacts.value = [ contacts.value = []
{
id: '1',
name: '张三',
title: '先生',
company: 'ABC公司',
email: 'zhangsan@example.com',
ccEmail: 'zhangsan_cc@example.com',
other: '技术总监',
},
{
id: '2',
name: '李四',
title: '女士',
company: 'XYZ企业',
email: 'lisi@example.com',
ccEmail: '',
other: '市场经理',
},
]
// 发件人 // 发件人
senders.value = [ senders.value = [
...@@ -257,5 +238,8 @@ const loadInitialData = () => { ...@@ -257,5 +238,8 @@ const loadInitialData = () => {
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
// 检查是否已登录(实际项目中应该检查本地存储或令牌) // 检查是否已登录(实际项目中应该检查本地存储或令牌)
if (isAuthenticated.value) {
loadInitialData()
}
}) })
</script> </script>
import request from '@/utils/request'
// 新增联系人
/**
*
* @param data {"companyName":"","name":"","email":"","type":"","appellation":"","other":"","ccEmailList":[]}
* @returns
*/
export const addContact = (data: {
companyName?: string
name?: string
email?: string
type?: string
appellation?: string
other?: string
ccEmailList?: string[]
}) => {
return request.post('/emailContact/add', data)
}
// 编辑联系人
/**
*
* @param data {"contactBizId":1,"companyName":"","name":"","email":"","type":"","appellation":"","other":"","ccEmailList":[]}
* @returns
*/
export const editContact = (data) => {
return request.put('/emailContact/edit', data)
}
// 删除联系人 delete
/**
*
* @param id 联系人id
* @returns
*/
export const deleteContact = (id: number) => {
return request.delete('/emailContact/del?contactBizId=' + id, { params: { contactBizId: id } })
}
// 获取联系人详情
/**
*
* @param id 联系人id
* @returns
*/
export const getContactDetail = (id: number) => {
return request.get('/emailContact/detail', { params: { contactBizId: id } })
}
// 获取联系人列表 post
/**
*
* @param params {
"companyName": "", //公司名称(保险公司等)
"name": "", //联系人姓名
"email": "", //联系人邮箱
"pageNo": 1,
"pageSize": 1,
"sortField": "",
"sortOrder": ""
}
* @returns
*/
export const getContactList = (params: {
companyName?: string
name?: string
email?: string
pageNo?: number
pageSize?: number
sortField?: string
sortOrder?: string
}) => {
return request.post('/emailContact/page', params)
}
// 新增发送配置
/**
*
* @param data {"emailSenderConfigBizId":1,"emailSenderConfigName":"","emailSenderConfigEmail":"","emailSenderConfigType":"","emailSenderConfigAppellation":"","emailSenderConfigOther":"","emailSenderConfigCcEmailList":[]}
* @returns
*/
export const addEmailSenderConfig = (data: {
emailSenderConfigBizId?: number
emailSenderConfigName?: string
emailSenderConfigEmail?: string
emailSenderConfigType?: string
emailSenderConfigAppellation?: string
emailSenderConfigOther?: string
emailSenderConfigCcEmailList?: string[]
}) => {
return request.post('/emailSenderConfig/add', data)
}
// 编辑发送配置
/**
*
* @param data {"emailSenderConfigBizId":1,"emailSenderConfigName":"","emailSenderConfigEmail":"","emailSenderConfigType":"","emailSenderConfigAppellation":"","emailSenderConfigOther":"","emailSenderConfigCcEmailList":[]}
* @returns
*/
export const editEmailSenderConfig = (data: {
emailSenderConfigBizId?: number
emailSenderConfigName?: string
emailSenderConfigEmail?: string
emailSenderConfigType?: string
emailSenderConfigAppellation?: string
emailSenderConfigOther?: string
emailSenderConfigCcEmailList?: string[]
}) => {
return request.put('/emailSenderConfig/edit', data)
}
// 删除发送配置
/**
*
* @param id 发送配置id
* @returns
*/
export const deleteEmailSenderConfig = (id: number) => {
return request.delete('/emailSenderConfig/delete', { params: { emailSenderConfigBizId: id } })
}
// 获取发送配置详情
/**
*
* @param id 发送配置id
* @returns
*/
export const getEmailSenderConfigDetail = (id: number) => {
return request.get('/emailSenderConfig/detail', { params: { emailSenderConfigBizId: id } })
}
// 获取发送配置列表
/**
*
* @param params {
"emailSenderConfigName": "", //发送配置名称
"emailSenderConfigEmail": "", //发送配置邮箱
"pageNo": 1,
"pageSize": 1,
"sortField": "",
"sortOrder": ""
}
* @returns
*/
export const getEmailSenderConfigList = (params: {
emailSenderConfigName?: string
emailSenderConfigEmail?: string
pageNo?: number
pageSize?: number
sortField?: string
sortOrder?: string
}) => {
return request.post('/emailSenderConfig/page', params)
}
// 分页查询变量
// 接口地址:/emailVariable/page
// 请求参数:{"variableNameCn":"","variableNameEn":"","pageNo":1,"pageSize":1,"sortField":"","sortOrder":""}
/**
*
* @param params {
"variableNameCn": "", //变量名称(中文)
"variableNameEn": "", //变量名称(英文)
"pageNo": 1,
"pageSize": 1,
"sortField": "",
"sortOrder": ""
}
* @returns
*/
export const getEmailVariableList = (params: {
variableNameCn?: string
variableNameEn?: string
pageNo?: number
pageSize?: number
sortField?: string
sortOrder?: string
}) => {
return request.post('/emailVariable/page', params)
}
// 新增变量
// 接口地址:/emailVariable/add
/**
*
* @param data {"variableNameCn": "", //变量字段名称中文名
"variableNameEn": "", //变量字段名称英文名
"description": "" //变量描述
}
* @returns
*/
export const addEmailVariable = (data: {
variableNameCn?: string
variableNameEn?: string
description?: string
}) => {
return request.post('/emailVariable/add', data)
}
// 编辑变量
// 接口地址:/emailVariable/edit
/**
*
* @param data {"id": 1, //变量表主键ID
"variableBizId": "", //变量唯一业务ID
"variableNameCn": "", //变量字段名称中文名
"variableNameEn": "", //变量字段名称英文名
"description": "" //变量描述
}
* @returns
*/
export const editEmailVariable = (data: {
id?: number
variableBizId?: string
variableNameCn?: string
variableNameEn?: string
description?: string
}) => {
return request.put('/emailVariable/edit', data)
}
// 删除变量
// 接口地址:/emailVariable/del?variableBizId=
// 请求参数:
/**
*
* @param id 变量id
* @returns
*/
export const deleteEmailVariable = (id: string) => {
return request.delete('/emailVariable/del?variableBizId=' + id)
}
/**
* 新增变量分组
* @param data {"variableGroupBizId": "", //变量分组唯一业务ID
"variableGroupName": "", //变量分组名称
"description": "" //变量分组描述
}
* @returns
*/
export const addEmailVariableGroup = (data: {
variableBizIdList?: string[]
groupName?: string
description?: string
}) => {
return request.post('/emailVariableGroup/add', data)
}
/**
* 编辑变量分组
* @param data {"variableGroupBizId": "", //变量分组唯一业务ID
"variableGroupName": "", //变量分组名称
"description": "" //变量分组描述
}
* @returns
*/
export const editEmailVariableGroup = (data: {
variableGroupBizId?: string
groupName?: string
description?: string
variableBizIdList?: string[]
}) => {
return request.put('/emailVariableGroup/edit', data)
}
/**
* 删除变量分组
* @param id 变量分组id
* @returns
*/
export const deleteEmailVariableGroup = (id: string) => {
return request.delete('/emailVariableGroup/del?variableGroupBizId=' + id)
}
/**
* 列表查询变量分组
* @param params {
"variableGroupName": "", //变量分组名称
"pageNo": 1,
"pageSize": 1,
"sortField": "",
"sortOrder": ""
}
* @returns
*/
export const getEmailVariableGroupList = (params: {
groupName?: string
pageNo?: number
pageSize?: number
sortField?: string
sortOrder?: string
}) => {
return request.post('/emailVariableGroup/page', params)
}
<template>
<el-dialog
v-model="dialogVisible"
:title="title"
:width="width"
:top="top"
:modal="modal"
:close-on-click-modal="closeOnClickModal"
:close-on-press-escape="closeOnPressEscape"
:show-close="showClose"
:destroy-on-close="destroyOnClose"
:custom-class="customClass"
@close="handleClose"
>
<!-- 弹窗头部插槽 -->
<template #header v-if="$slots.header">
<slot name="header"></slot>
</template>
<!-- 弹窗内容 -->
<div class="modal-content">
<!-- 图标区域 -->
<div
v-if="type && showIcon"
class="icon-container mr-4 flex-shrink-0"
:class="iconContainerClass"
>
<component :is="getIconComponent" class="w-6 h-6" />
</div>
<!-- 内容区域 -->
<div class="content-container flex-1">
<!-- 默认消息内容 -->
<template v-if="!$slots.default">
<p class="text-gray-800 text-sm leading-6 mb-0">
{{ message }}
</p>
<p v-if="subMessage" class="text-gray-500 text-xs leading-5 mt-2">
{{ subMessage }}
</p>
</template>
<!-- 自定义内容插槽 -->
<slot></slot>
</div>
</div>
<!-- 弹窗底部 -->
<template #footer>
<slot name="footer">
<div class="flex gap-3 justify-end">
<el-button
v-if="showCancelButton"
@click="handleCancel"
size="default"
:loading="cancelLoading"
class="px-4 py-2"
>
{{ cancelText }}
</el-button>
<el-button
@click="handleConfirm"
:type="getButtonType"
size="default"
:loading="confirmLoading"
class="px-4 py-2"
>
{{ confirmText }}
</el-button>
</div>
</slot>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { defineProps, defineEmits, computed, watch, ref, onUnmounted } from 'vue'
import { Check, Warning, CircleClose, InfoFilled, QuestionFilled } from '@element-plus/icons-vue'
// 弹窗类型
type ModalType = 'success' | 'warning' | 'error' | 'info' | 'confirm' | ''
// 定义属性
const props = defineProps({
/** 控制弹窗显示/隐藏 */
visible: {
type: Boolean,
default: false,
},
/** 弹窗类型 */
type: {
type: String as () => ModalType,
default: '',
},
/** 弹窗标题 */
title: {
type: String,
default: '',
},
/** 主要消息内容 */
message: {
type: String,
default: '',
},
/** 次要消息内容 */
subMessage: {
type: String,
default: '',
},
/** 弹窗宽度 */
width: {
type: String,
default: '500px',
},
/** 弹窗距离顶部的距离 */
top: {
type: String,
default: '15vh',
},
/** 是否显示遮罩层 */
modal: {
type: Boolean,
default: true,
},
/** 点击遮罩层是否关闭弹窗 */
closeOnClickModal: {
type: Boolean,
default: false,
},
/** 按ESC键是否关闭弹窗 */
closeOnPressEscape: {
type: Boolean,
default: true,
},
/** 是否显示关闭按钮 */
showClose: {
type: Boolean,
default: true,
},
/** 是否在关闭弹窗时销毁内容 */
destroyOnClose: {
type: Boolean,
default: true,
},
/** 是否显示取消按钮 */
showCancelButton: {
type: Boolean,
default: false,
},
/** 确认按钮文本 */
confirmText: {
type: String,
default: '确定',
},
/** 取消按钮文本 */
cancelText: {
type: String,
default: '取消',
},
/** 自动关闭时间(毫秒),0表示不自动关闭 */
autoClose: {
type: Number,
default: 0,
},
/** 自定义CSS类 */
customClass: {
type: String,
default: '',
},
/** 是否显示图标 */
showIcon: {
type: Boolean,
default: true,
},
/** 确认按钮加载状态 */
confirmLoading: {
type: Boolean,
default: false,
},
/** 取消按钮加载状态 */
cancelLoading: {
type: Boolean,
default: false,
},
/** 是否可拖拽 */
draggable: {
type: Boolean,
default: false,
},
})
// 定义事件
const emit = defineEmits([
'confirm',
'cancel',
'close',
'update:visible',
'update:confirmLoading',
'update:cancelLoading',
])
// 内部状态管理
const dialogVisible = ref(props.visible)
const autoCloseTimer = ref<NodeJS.Timeout | null>(null)
// 监听visible变化
watch(
() => props.visible,
(newVal) => {
dialogVisible.value = newVal
},
)
// 监听dialogVisible变化
watch(
() => dialogVisible.value,
(newVal) => {
if (newVal) {
// 处理自动关闭
if (props.autoClose > 0) {
clearAutoCloseTimer()
autoCloseTimer.value = setTimeout(() => {
handleClose()
}, props.autoClose)
}
} else {
// 触发update:visible事件
emit('update:visible', false)
clearAutoCloseTimer()
}
},
)
// 清除自动关闭定时器
const clearAutoCloseTimer = () => {
if (autoCloseTimer.value) {
clearTimeout(autoCloseTimer.value)
autoCloseTimer.value = null
}
}
// 根据类型获取按钮样式
const getButtonType = computed(() => {
switch (props.type) {
case 'success':
return 'success'
case 'warning':
return 'warning'
case 'error':
return 'danger'
case 'info':
return 'info'
case 'confirm':
return 'primary'
default:
return 'primary'
}
})
// 获取图标组件
const getIconComponent = computed<Component>(() => {
switch (props.type) {
case 'success':
return Check
case 'warning':
return Warning
case 'error':
return CircleClose
case 'info':
return InfoFilled
case 'confirm':
return QuestionFilled
default:
return InfoFilled
}
})
// 图标容器样式
const iconContainerClass = computed(() => {
const base = 'rounded-full p-2'
switch (props.type) {
case 'success':
return `${base} bg-green-100 text-green-600`
case 'warning':
return `${base} bg-yellow-100 text-yellow-600`
case 'error':
return `${base} bg-red-100 text-red-600`
case 'info':
return `${base} bg-blue-100 text-blue-600`
case 'confirm':
return `${base} bg-gray-100 text-gray-600`
default:
return `${base} bg-gray-100 text-gray-600`
}
})
// 处理关闭事件
const handleClose = () => {
dialogVisible.value = false
emit('close')
}
// 处理确认事件
const handleConfirm = () => {
emit('confirm')
// 不自动关闭,由父组件控制
if (!props.confirmLoading) {
handleClose()
}
}
// 处理取消事件
const handleCancel = () => {
emit('cancel')
handleClose()
}
// 组件卸载时清理定时器
onUnmounted(() => {
clearAutoCloseTimer()
})
</script>
<style scoped>
.modal-content {
@apply flex items-start;
}
.icon-container {
@apply flex items-center justify-center;
}
.content-container {
@apply min-h-0;
}
/* 适配Element Plus的样式 */
:deep(.el-dialog__body) {
@apply px-6 py-4;
}
:deep(.el-dialog__header) {
@apply border-b border-gray-200 pb-4 mb-0;
}
:deep(.el-dialog__footer) {
@apply border-t border-gray-200 pt-4 mt-0;
}
</style>
<template>
<div>
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">添加联系人</h3>
<button
v-if="editingContactId"
@click="resetForm"
class="text-gray-500 hover:text-gray-700"
>
<i class="fas fa-times"></i> 取消编辑
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<label class="block text-gray-700 mb-1 text-sm">姓名 *</label>
<input
v-model="formData.name"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">称谓</label>
<input
v-model="formData.title"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="先生/女士/教授等"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">公司</label>
<input
v-model="formData.company"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">邮箱 *</label>
<input
v-model="formData.email"
type="email"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">关联抄送邮箱</label>
<input
v-model="formData.ccEmail"
type="email"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="当此联系人作为收件人时自动抄送的邮箱"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">其他信息</label>
<input
v-model="formData.other"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
<div class="mt-4 flex justify-end">
<button
@click="saveContact"
class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-700 transition-colors"
:disabled="!formData.name || !formData.email"
>
{{ editingContactId ? '更新联系人' : '添加联系人' }}
</button>
</div>
</div>
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="p-6 border-b border-gray-200">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h3 class="text-lg font-semibold">联系人列表</h3>
<div class="w-full sm:w-auto">
<div class="relative">
<input
v-model="searchTerm"
type="text"
class="w-full px-3 py-2 pl-10 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="搜索联系人..."
/>
<i
class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
></i>
</div>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
姓名
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
称谓
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
公司
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
邮箱
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
抄送邮箱
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
其他信息
</th>
<th
class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
>
操作
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="contact in filteredContacts" :key="contact.id">
<td class="px-6 py-4 whitespace-nowrap">{{ contact.name }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ contact.title || '-' }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ contact.company || '-' }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ contact.email }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ contact.ccEmail || '-' }}</td>
<td class="px-6 py-4">{{ contact.other || '-' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
@click="editContact(contact)"
class="text-blue-600 hover:text-blue-900 mr-3"
>
编辑
</button>
<button @click="deleteContact(contact.id)" class="text-red-600 hover:text-red-900">
删除
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="filteredContacts.length === 0" class="p-8 text-center text-gray-500">
<i class="fas fa-address-book text-4xl mb-3 opacity-30"></i>
<p>暂无联系人,请添加联系人</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
import { Contact } from '../types'
const props = defineProps({
contacts: {
type: Array as () => Contact[],
required: true,
},
})
const emits = defineEmits(['update-contacts'])
// 状态
const contacts = ref<Contact[]>([...props.contacts])
const searchTerm = ref('')
const editingContactId = ref('')
const formData = ref<Partial<Contact>>({
name: '',
title: '',
company: '',
email: '',
ccEmail: '',
other: '',
})
// 计算属性
const filteredContacts = computed(() => {
return contacts.value.filter(
(contact) =>
contact.name.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
contact.email.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
contact.company.toLowerCase().includes(searchTerm.value.toLowerCase()),
)
})
// 方法
const resetForm = () => {
editingContactId.value = ''
formData.value = {
name: '',
title: '',
company: '',
email: '',
ccEmail: '',
other: '',
}
}
const saveContact = () => {
if (!formData.value.name || !formData.value.email) return
if (editingContactId.value) {
// 更新现有联系人
const index = contacts.value.findIndex((c) => c.id === editingContactId.value)
if (index > -1) {
contacts.value[index] = {
...contacts.value[index],
...formData.value,
} as Contact
emits('update-contacts', [...contacts.value])
alert('联系人更新成功')
}
} else {
// 添加新联系人
const newContact: Contact = {
id: Date.now().toString(),
name: formData.value.name || '',
title: formData.value.title || '',
company: formData.value.company || '',
email: formData.value.email || '',
ccEmail: formData.value.ccEmail || '',
other: formData.value.other || '',
}
contacts.value.push(newContact)
emits('update-contacts', [...contacts.value])
alert('联系人添加成功')
}
resetForm()
}
const editContact = (contact: Contact) => {
editingContactId.value = contact.id
formData.value = { ...contact }
}
const deleteContact = (id: string) => {
if (confirm('确定要删除这个联系人吗?')) {
contacts.value = contacts.value.filter((contact) => contact.id !== id)
emits('update-contacts', [...contacts.value])
}
}
</script>
/* 导入Font Awesome */
/* @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css'); */
/* 导入Tailwind CSS */ /* 导入Tailwind CSS */
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* 导入Font Awesome */
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css');
/* 自定义样式 */ /* 自定义样式 */
#app { #app {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
......
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
...@@ -8,6 +10,7 @@ import router from './router' ...@@ -8,6 +10,7 @@ import router from './router'
import './index.css' import './index.css'
const app = createApp(App) const app = createApp(App)
app.use(ElementPlus)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
......
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
export interface Contact { export interface Contact {
id: string id: string
name: string name: string
title: string type: string
company: string companyName: string
email: string email: string
ccEmail: string ccEmailList: string[]
other: string other: string
appellation: string
} }
// 发件人类型 // 发件人类型
...@@ -20,18 +21,19 @@ export interface Sender { ...@@ -20,18 +21,19 @@ export interface Sender {
// 变量类型 // 变量类型
export interface Variable { export interface Variable {
id: string id?: string
name: string variableBizId?: string
key: string variableNameCn?: string
description: string variableNameEn?: string
description?: string
} }
// 变量模板类型 // 变量模板类型
export interface VariableTemplate { export interface VariableTemplate {
id: string id: string
name: string groupName?: string
description: string description?: string
variableIds: string[] variableBizIdList?: string[]
} }
// 邮件类型 // 邮件类型
...@@ -63,3 +65,12 @@ export interface ForgotPasswordForm { ...@@ -63,3 +65,12 @@ export interface ForgotPasswordForm {
newPassword: string newPassword: string
confirmPassword: string confirmPassword: string
} }
// 导入记录类型
export interface ImportRecord {
id: string
to: string
cc: string
createdAt: string
updatedAt: string
}
import axios, { AxiosError } from 'axios'
// 创建axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
// Authorization: 'Bearer ' + localStorage.getItem('authToken'),
Authorization:
'Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyXzEwMDEiLCJyb2xlcyI6W10sImlhdCI6MTc1ODY5OTA3NywiZXhwIjoxNzU4Nzg1NDc3fQ.LR2fGy0aO6EHsHe9Que8rzCaJ0TSAB9KtJndYMSYvvKOSeNvGawCmjE8kgDeRmyFFOFJ2kt0sk-fGaExgzQHSw',
},
})
// 请求拦截器 - 添加Authorization头
request.interceptors.request.use(
(config) => {
// 从本地存储获取token
const token = localStorage.getItem('authToken')
// 如果token存在,添加到请求头
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error: AxiosError) => {
// 处理请求错误
return Promise.reject(error)
},
)
// 响应拦截器 - 处理常见错误
request.interceptors.response.use(
(response) => {
// 直接返回响应数据
return response.data
},
(error: AxiosError) => {
// 处理401未授权错误
if (error.response && error.response.status === 401) {
// 清除无效token
localStorage.removeItem('authToken')
// 如果不是登录页面,跳转到登录页
if (!window.location.pathname.includes('/login')) {
// 保存当前URL,登录后可跳转回来
localStorage.setItem('redirectPath', window.location.pathname)
window.location.href = '/login'
}
}
return Promise.reject(error)
},
)
export default request
<template>
<div
v-if="visible"
class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"
>
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
<h3 class="text-lg font-semibold mb-4">{{ title }}</h3>
<input
type="file"
:accept="accept"
@change="handleFileSelect"
class="w-full px-3 py-2 border border-gray-300 rounded-md mb-4"
/>
<div class="flex justify-end gap-3">
<button
@click="handleCancel"
class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50"
>
取消
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '导入文件',
},
accept: {
type: String,
default: '.csv,.txt',
},
})
const emit = defineEmits(['update:visible', 'file-selected'])
const handleFileSelect = (e: Event) => {
const input = e.target as HTMLInputElement
if (input.files && input.files[0]) {
const file = input.files[0]
const reader = new FileReader()
reader.onload = (e) => {
const content = e.target?.result as string
emit('file-selected', { file, content })
emit('update:visible', false)
}
reader.readAsText(file)
}
}
const handleCancel = () => {
emit('update:visible', false)
}
// 监听visible变化,重置文件输入
watch(
() => props.visible,
(newVisible) => {
if (newVisible) {
// 下次DOM更新后重置文件输入
setTimeout(() => {
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
if (fileInput) {
fileInput.value = ''
}
}, 100)
}
},
)
</script>
<template>
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[80vh] flex flex-col">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-semibold">导入记录管理</h3>
<button @click="$emit('close')">
<i class="fas fa-times text-gray-500"></i>
</button>
</div>
<div class="p-4 border-b border-gray-200">
<div class="flex gap-2">
<input
v-model="searchTerm"
type="text"
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="搜索收件人邮箱..."
/>
<button
@click="clearSearch"
class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
>
清除
</button>
</div>
</div>
<div class="p-4 flex-1 overflow-y-auto">
<div v-if="filteredRecords.length === 0" class="text-center text-gray-500 py-8">
<p>未找到匹配的导入记录</p>
</div>
<div v-else class="space-y-3">
<div
v-for="record in filteredRecords"
:key="record.id"
class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div class="flex justify-between items-start mb-3">
<div class="flex-1">
<div class="font-medium text-gray-900">收件人: {{ record.to }}</div>
<div class="text-sm text-gray-600 mt-1">抄送人: {{ record.cc || '无' }}</div>
<div class="text-xs text-gray-400 mt-2">
创建时间: {{ formatDate(record.createdAt) }}
</div>
</div>
<div class="flex gap-2">
<button
@click="editRecord(record)"
class="px-3 py-1 bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200 transition-colors text-sm"
>
编辑
</button>
<button
@click="deleteRecord(record.id)"
class="px-3 py-1 bg-red-100 text-red-700 rounded-md hover:bg-red-200 transition-colors text-sm"
>
删除
</button>
</div>
</div>
<div v-if="editingRecordId === record.id" class="mt-3 p-3 bg-gray-50 rounded-md">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">收件人</label>
<input
v-model="editingRecord.to"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="收件人邮箱"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">抄送人</label>
<input
v-model="editingRecord.cc"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="抄送人邮箱,多个用逗号分隔"
/>
</div>
</div>
<div class="flex justify-end gap-2 mt-3">
<button
@click="cancelEdit"
class="px-3 py-1 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors text-sm"
>
取消
</button>
<button
@click="saveEdit"
class="px-3 py-1 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors text-sm"
>
保存
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 border-t border-gray-200">
<button
@click="$emit('close')"
class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
>
关闭
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
import { ImportRecord } from '../types'
const props = defineProps({
records: {
type: Array as () => ImportRecord[],
required: true,
},
})
const emits = defineEmits(['update-record', 'delete-record', 'close'])
const searchTerm = ref('')
const editingRecordId = ref<string | null>(null)
const editingRecord = ref<Partial<ImportRecord>>({})
const filteredRecords = computed(() => {
if (!searchTerm.value) return props.records
return props.records.filter(
(record) =>
record.to.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
record.cc.toLowerCase().includes(searchTerm.value.toLowerCase()),
)
})
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString('zh-CN')
}
const editRecord = (record: ImportRecord) => {
editingRecordId.value = record.id
editingRecord.value = { ...record }
}
const cancelEdit = () => {
editingRecordId.value = null
editingRecord.value = {}
}
const saveEdit = () => {
if (editingRecordId.value && editingRecord.value.to) {
emits('update-record', {
id: editingRecordId.value,
to: editingRecord.value.to,
cc: editingRecord.value.cc || '',
updatedAt: new Date().toISOString(),
})
editingRecordId.value = null
editingRecord.value = {}
}
}
const deleteRecord = (id: string) => {
if (confirm('确定要删除这条导入记录吗?')) {
emits('delete-record', id)
}
}
const clearSearch = () => {
searchTerm.value = ''
}
</script>
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }
......
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx' import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
/**设置server转发 */
// https://vite.dev/config/
export default defineConfig({ export default defineConfig({
// 关键配置:设置基础路径为子目录 yd-email // 关键配置:设置基础路径为子目录 yd-email
// 生产环境(部署到服务器)用 '/yd-email/',本地开发用 '/'(避免开发时路径错误)
base: process.env.NODE_ENV === 'production' ? '/yd-email/' : '/', base: process.env.NODE_ENV === 'production' ? '/yd-email/' : '/',
plugins: [vue(), vueJsx(), vueDevTools()], plugins: [vue(), vueJsx(), vueDevTools()],
resolve: { resolve: {
...@@ -16,4 +14,20 @@ export default defineConfig({ ...@@ -16,4 +14,20 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),
}, },
}, },
// 添加CSS配置
css: {
postcss: './postcss.config.js',
},
server: {
port: 5173,
host: 'localhost',
open: true,
proxy: {
'email/api': {
target: 'http://139.224.145.34:9002',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/email\/api/, ''),
},
},
},
}) })
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