Commit 79fddc42 by Sweet Zhang

分页对接,编辑数据对接

parent efadec82
VITE_API_BASE_URL='/email/api'
VITE_REMOTE_API_BASE_URL=''
\ No newline at end of file
...@@ -110,6 +110,12 @@ export const variableGroupApi = { ...@@ -110,6 +110,12 @@ export const variableGroupApi = {
getEmailVariableGroupList: (params: VariableTemplate): Promise<ApiResponse> => { getEmailVariableGroupList: (params: VariableTemplate): Promise<ApiResponse> => {
return request.post(`${baseEmailUrl}/emailVariableGroup/page`, params) return request.post(`${baseEmailUrl}/emailVariableGroup/page`, params)
}, },
// 导出变量模版
exportEmailVariableGroup: (id: string): Promise<ApiResponse> => {
return request.post(`${baseEmailUrl}/emailFile/export/excel/variable`, {
variableGroupBizId: id,
})
},
} }
/** /**
...@@ -136,6 +142,10 @@ export const importContactApi = { ...@@ -136,6 +142,10 @@ export const importContactApi = {
getEmailContactImportDetail: (id: string): Promise<ApiResponse> => { getEmailContactImportDetail: (id: string): Promise<ApiResponse> => {
return request.get(`${baseEmailUrl}/emailContactImport/detail/sessionId?sessionId=${id}`) return request.get(`${baseEmailUrl}/emailContactImport/detail/sessionId?sessionId=${id}`)
}, },
// 删除导入联系人
deleteEmailContactImport: (id: string): Promise<ApiResponse> => {
return request.delete(`${baseEmailUrl}/emailContactImport/del?importBizId=${id}`)
},
} }
/** /**
* 发送邮件 * 发送邮件
......
...@@ -24,8 +24,8 @@ ...@@ -24,8 +24,8 @@
<p class="text-gray-900">{{ emailData?.sendEmail || '无' }}</p> <p class="text-gray-900">{{ emailData?.sendEmail || '无' }}</p>
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">发送时间</label> <label class="block text-sm font-medium text-gray-700 mb-1">定时时间</label>
<p class="text-gray-900">{{ formatDate(emailData?.sendTime) || '无' }}</p> <p class="text-gray-900">{{ formatDate(emailData?.scheduleTime) || '无' }}</p>
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">总收件人数</label> <label class="block text-sm font-medium text-gray-700 mb-1">总收件人数</label>
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
{{ formatDate(record.sendTime) || '--' }} {{ formatDate(record.sendTime) || '--' }}
</td> </td>
<td class="px-6 py-4 text-sm text-gray-500 max-w-xs"> <td class="px-6 py-4 text-sm text-gray-500 max-w-xs">
{{ record.failReason || '--' }} {{ record.errorMsg || '--' }}
</td> </td>
</tr> </tr>
</tbody> </tbody>
...@@ -122,13 +122,14 @@ ...@@ -122,13 +122,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import type { DictItem } from '@/types/index'
// 定义props // 定义props
interface EmailRecord { interface EmailRecord {
receiveEmail?: string receiveEmail?: string
status?: string status?: string
sendTime?: string sendTime?: string
failReason?: string errorMsg?: string
} }
interface EmailTask { interface EmailTask {
...@@ -137,11 +138,13 @@ interface EmailTask { ...@@ -137,11 +138,13 @@ interface EmailTask {
sendEmail?: string sendEmail?: string
sendTime?: string sendTime?: string
records?: EmailRecord[] records?: EmailRecord[]
scheduleTime?: string
} }
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
emailData?: EmailTask emailData?: EmailTask
statusOptions?: DictItem[]
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
...@@ -155,8 +158,14 @@ const emailRecords = ref<EmailRecord[]>([]) ...@@ -155,8 +158,14 @@ const emailRecords = ref<EmailRecord[]>([])
watch( watch(
() => props.emailData, () => props.emailData,
(newEmailData) => { (newEmailData) => {
if (newEmailData && newEmailData.records) { if (newEmailData && newEmailData.records && props.statusOptions) {
emailRecords.value = newEmailData.records emailRecords.value = newEmailData.records
// 为每个邮件设置状态标签
emailRecords.value.forEach((email) => {
email.statusLabel =
props.statusOptions.find((item) => item.itemValue === email.status)?.itemLabel ||
'未知状态'
})
} else { } else {
emailRecords.value = [] emailRecords.value = []
} }
......
<template>
<div
class="pagination-container flex items-center justify-between px-4 py-3 bg-white border-t border-gray-200"
>
<!-- 左侧信息 -->
<div class="pagination-info flex items-center text-sm text-gray-700">
<span>显示第 {{ startItem }} 到第 {{ endItem }} 条,共 {{ total }} 条记录</span>
</div>
<!-- 右侧分页控件 -->
<div class="pagination-controls flex items-center space-x-2">
<!-- 每页显示数量选择器 -->
<div class="page-size-selector flex items-center space-x-2">
<span class="text-sm text-gray-700">每页显示</span>
<el-select
v-model="pageSize"
:disabled="disabled"
size="small"
style="width: 100px"
@change="handlePageSizeChange"
>
<el-option
v-for="size in pageSizeOptions"
:key="size"
:label="`${size} 条`"
:value="size"
/>
</el-select>
</div>
<!-- 分页器 -->
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
:disabled="disabled"
:background="background"
:layout="layout"
:page-sizes="pageSizeOptions"
:pager-count="pagerCount"
@size-change="handlePageSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
// 定义组件属性
interface Props {
total: number
current?: number
pageSize?: number
pageSizes?: number[]
layout?: string
background?: boolean
disabled?: boolean
pagerCount?: number
}
// 定义组件事件
interface Emits {
(e: 'update:current', value: number): void
(e: 'update:pageSize', value: number): void
(e: 'change', page: number, pageSize: number): void
}
// 默认属性值
const props = withDefaults(defineProps<Props>(), {
current: 1,
pageSize: 10,
pageSizes: () => [10, 20, 50, 100],
layout: 'prev, pager, next, jumper',
background: true,
disabled: false,
pagerCount: 7,
})
const emit = defineEmits<Emits>()
// 响应式数据
const currentPage = ref(props.current)
const pageSize = ref(props.pageSize)
const pageSizeOptions = ref(props.pageSizes)
// 计算属性
const startItem = computed(() => {
return (currentPage.value - 1) * pageSize.value + 1
})
const endItem = computed(() => {
const end = currentPage.value * pageSize.value
return end > props.total ? props.total : end
})
// 监听外部属性变化
watch(
() => props.current,
(newVal) => {
currentPage.value = newVal
},
)
watch(
() => props.pageSize,
(newVal) => {
pageSize.value = newVal
},
)
watch(
() => props.pageSizes,
(newVal) => {
pageSizeOptions.value = newVal
},
)
// 事件处理
const handlePageSizeChange = (newSize: number) => {
pageSize.value = newSize
emit('update:pageSize', newSize)
emit('change', currentPage.value, newSize)
}
const handleCurrentChange = (newPage: number) => {
currentPage.value = newPage
emit('update:current', newPage)
emit('change', newPage, pageSize.value)
}
</script>
<style scoped>
.pagination-container {
min-height: 56px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.pagination-container {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.pagination-controls {
justify-content: space-between;
}
}
</style>
...@@ -48,6 +48,7 @@ export interface VariableTemplate extends Pagination<VariableTemplate> { ...@@ -48,6 +48,7 @@ export interface VariableTemplate extends Pagination<VariableTemplate> {
description?: string description?: string
variableBizIdList?: string[] variableBizIdList?: string[]
variableNameEns?: string[] variableNameEns?: string[]
variableNameEnList?: string[]
} }
// 忘记密码表单类型 // 忘记密码表单类型
...@@ -62,6 +63,7 @@ export interface ImportRecord extends Contact<ImportRecord> { ...@@ -62,6 +63,7 @@ export interface ImportRecord extends Contact<ImportRecord> {
sessionId?: string sessionId?: string
receiveEmailList?: string[] receiveEmailList?: string[]
ccEmailList?: string[] ccEmailList?: string[]
ccEmail?: string
} }
// 选择联系人时,调用接口,获取sessionId // 选择联系人时,调用接口,获取sessionId
......
...@@ -60,10 +60,6 @@ ...@@ -60,10 +60,6 @@
<button <button
@click="((showContactSelector = true), (importSource = 0))" @click="((showContactSelector = true), (importSource = 0))"
class="bg-blue-50 hover:bg-blue-100 text-blue-600 px-4 py-2 rounded-md border border-blue-200 transition-colors flex items-center" class="bg-blue-50 hover:bg-blue-100 text-blue-600 px-4 py-2 rounded-md border border-blue-200 transition-colors flex items-center"
v-if="
!selectedVariableTemplate ||
selectedVariableTemplate?.variableGroupBizId == 'email_variable_group_a2Z0lJLQlCuO81ZE'
"
> >
<i class="fas fa-address-book mr-1"></i> 选择联系人 <i class="fas fa-address-book mr-1"></i> 选择联系人
</button> </button>
...@@ -85,14 +81,14 @@ ...@@ -85,14 +81,14 @@
> >
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<div <div
v-for="(tag, index) in emailForm.ccEmails.split(';').filter((e) => e.trim())" v-for="(tag, index) in emailForm.ccEmails.split(',').filter((e) => e.trim())"
:key="index" :key="index"
class="inline-flex flex-wrap items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium" class="inline-flex flex-wrap items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium"
> >
<span <span
v-for="subtag in tag v-for="subtag in tag
.trim() .trim()
.split(',') .split(';')
.filter((e) => e.trim())" .filter((e) => e.trim())"
:key="subtag" :key="subtag"
class="bg-orange-100 text-orange-800 px-2 py-0.5 rounded-full" class="bg-orange-100 text-orange-800 px-2 py-0.5 rounded-full"
...@@ -200,7 +196,7 @@ ...@@ -200,7 +196,7 @@
:records="importRecords" :records="importRecords"
@update-record="updateImportRecord" @update-record="updateImportRecord"
@delete-record="deleteImportRecord" @delete-record="deleteImportRecord"
@close="showImportRecordManager = false" @close="((showImportRecordManager = false), getImportedContacts())"
/> />
<!-- 导入数据弹窗 --> <!-- 导入数据弹窗 -->
...@@ -210,22 +206,6 @@ ...@@ -210,22 +206,6 @@
accept=".csv,.txt,.xlsx" accept=".csv,.txt,.xlsx"
@file-selected="handleImportContacts" @file-selected="handleImportContacts"
/> />
<div
v-if="showImportContacts"
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">导入联系人</h3>
<div class="flex justify-end gap-3">
<button
@click="showImportContacts = false"
class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50"
>
取消
</button>
</div>
</div>
</div>
<!-- 联系人选择弹窗 --> <!-- 联系人选择弹窗 -->
<ContactSelector <ContactSelector
v-if="showContactSelector" v-if="showContactSelector"
...@@ -308,7 +288,6 @@ const emailForm = ref<EmailForm>({ ...@@ -308,7 +288,6 @@ const emailForm = ref<EmailForm>({
}) })
const selectedVariableTemplate = ref<VariableTemplate | null>(null) const selectedVariableTemplate = ref<VariableTemplate | null>(null)
const attachments = ref<File[]>([])
const showContactSelector = ref(false) const showContactSelector = ref(false)
const showVariableSelector = ref(false) const showVariableSelector = ref(false)
const showImportContacts = ref(false) const showImportContacts = ref(false)
...@@ -320,11 +299,11 @@ watch( ...@@ -320,11 +299,11 @@ watch(
() => emailForm.value.receiveEmail, () => emailForm.value.receiveEmail,
(newReceiveEmail) => { (newReceiveEmail) => {
if (newReceiveEmail) { if (newReceiveEmail) {
const matchedRecord = importRecords.value.find( const matchedRecord = importRecords.value.find((record) =>
(record) => record.receiveEmail === newReceiveEmail, record.receiveEmailList?.includes(newReceiveEmail),
) )
if (matchedRecord && matchedRecord.ccEmailList) { if (matchedRecord && matchedRecord.ccEmailList) {
emailForm.value.ccEmailList = matchedRecord.ccEmailList emailForm.value.ccEmailList = matchedRecord.ccEmailList || []
} }
} }
}, },
...@@ -406,38 +385,55 @@ const confirmContactSelection = (selected: Contact<unknown>[]) => { ...@@ -406,38 +385,55 @@ const confirmContactSelection = (selected: Contact<unknown>[]) => {
// 更新导入记录 // 更新导入记录
const updateImportRecord = (updatedRecord: ImportRecord) => { const updateImportRecord = (updatedRecord: ImportRecord) => {
const index = importRecords.value.findIndex((record) => record.id === updatedRecord.id) const params = {
if (index !== -1) { ...updatedRecord,
importRecords.value[index] = { ...updatedRecord } ccEmailList: updatedRecord.ccEmail.split(';') || '',
ccEmail: undefined,
} }
importContactApi.editEmailContactImport(params).then((res) => {
if (res.code === 200) {
ElMessage({
message: '导入记录更新成功',
type: 'success',
})
// 更新成功之后,刷新导入记录列表
getImportedContacts(emailForm.value.sessionId || '')
} else {
ElMessage({
message: '导入记录更新失败',
type: 'error',
})
}
})
} }
// 删除导入记录 // 删除导入记录
const deleteImportRecord = (id: string) => { const deleteImportRecord = (importBizId: string) => {
importRecords.value = importRecords.value.filter((record) => record.id !== id) importContactApi.deleteEmailContactImport(importBizId).then((res) => {
} if (res.code === 200) {
ElMessage({
// 修改handleImportContacts方法 message: '导入记录删除成功',
const handleImportContacts = (event: { file: File; content: string }) => { type: 'success',
const { content } = event })
// 解析CSV或文本文件,这里简化处理 // 删除成功之后,刷新导入记录列表
const lines = content.split('\n') getImportedContacts(emailForm.value.sessionId || '')
lines.forEach((line) => { } else {
const [to, cc] = line.split(',') ElMessage({
if (to && to.includes('@')) { message: '导入记录删除失败',
importRecords.value.push({ type: 'error',
id: Date.now().toString(),
receiveEmail: to.trim(),
ccEmailList: cc ? cc.split(',').map((email) => email.trim()) : [],
contactBizId: '',
name: '',
type: '',
pageNo: 1,
}) })
} }
}) })
} }
// 修改handleImportContacts方法
const handleImportContacts = (results) => {
console.log(results)
if (results.data) {
emailForm.value.sessionId = results.data.sessionId || ''
}
}
// 发送邮件 // 发送邮件
const sendEmail = () => { const sendEmail = () => {
const params = { const params = {
...@@ -454,6 +450,14 @@ const sendEmail = () => { ...@@ -454,6 +450,14 @@ const sendEmail = () => {
message: '邮件发送成功', message: '邮件发送成功',
type: 'success', type: 'success',
}) })
// 发送成功之后,清空表单
emailForm.value = {
receiveEmail: '',
ccEmailList: [],
content: '',
attachmentPath: '',
sessionId: '',
}
} else { } else {
ElMessage({ ElMessage({
message: '邮件发送失败', message: '邮件发送失败',
...@@ -464,30 +468,22 @@ const sendEmail = () => { ...@@ -464,30 +468,22 @@ const sendEmail = () => {
} }
// 通过sessionId获取导入的联系人 // 通过sessionId获取导入的联系人
const getImportedContacts = (sessionId: string) => { const getImportedContacts = (sessionId?: string) => {
const params = { const params = {
sessionId: sessionId || '', sessionId: sessionId || emailForm.value.sessionId || '',
source: importSource.value, source: importSource.value,
} }
importContactApi.getEmailContactImportList(params).then((res) => { importContactApi.getEmailContactImportList(params).then((res) => {
if (res.code === 200) { if (res.code === 200) {
console.log('导入的联系人:', res.data) console.log('导入的联系人:', res.data)
importRecords.value = res.data.records || [] importRecords.value = res.data.records || []
// 更新页面展示的抄送人和收件人
// emailForm.value.receiveEmail = res.data?.receiveEmails || ''
// emailForm.value.ccEmails = res.data?.ccEmails || ''
} }
}) })
} }
// 编辑数据导入记录
const editImportRecord = (record: ImportRecord) => {
console.log('编辑导入记录:', record)
// 这里可以添加编辑逻辑,例如打开编辑弹窗
importContactApi.editEmailContactImport(record).then((res) => {
if (res.code === 200) {
console.log('编辑导入记录成功:', res.data)
updateImportRecord(res.data)
}
})
}
/** /**
* 文件上传配置 * 文件上传配置
*/ */
...@@ -495,6 +491,7 @@ const editImportRecord = (record: ImportRecord) => { ...@@ -495,6 +491,7 @@ const editImportRecord = (record: ImportRecord) => {
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import FileUploadComponent from '@/components/FileUploadComponent.vue' import FileUploadComponent from '@/components/FileUploadComponent.vue'
import { UploadResult } from '@/utils/fileUpload' import { UploadResult } from '@/utils/fileUpload'
import { get } from 'http'
// 上传成功的文件 // 上传成功的文件
const uploadedFiles = ref<any[]>([]) const uploadedFiles = ref<any[]>([])
...@@ -504,7 +501,7 @@ const handleDocumentUploadSuccess = (results: UploadResult[]) => { ...@@ -504,7 +501,7 @@ const handleDocumentUploadSuccess = (results: UploadResult[]) => {
// 发送邮件时,接口入参attachmentPath,填写附件路径,多个有分号隔开,路径在result里面的data,里面的accessUrl // 发送邮件时,接口入参attachmentPath,填写附件路径,多个有分号隔开,路径在result里面的data,里面的accessUrl
// 过滤出成功上传的文件 // 过滤出成功上传的文件
const successFiles = results.filter((r) => r.success) const successFiles = results.filter((r) => r.success)
const attachmentPath = successFiles.map((r) => r.data?.data.accessUrl || '').join(';') const attachmentPath = successFiles.map((r) => r.data?.data.url || '').join(';')
emailForm.value.attachmentPath = attachmentPath emailForm.value.attachmentPath = attachmentPath
ElMessage.success(`成功上传 ${successCount} 个文档`) ElMessage.success(`成功上传 ${successCount} 个文档`)
......
...@@ -157,8 +157,15 @@ ...@@ -157,8 +157,15 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- 分页组件 -->
<!-- 空状态、加载状态和分页组件与之前保持一致 --> <Pagination
:total="total"
:current="currentPage"
:page-size="pageSize"
@change="handlePageChange"
@update:current="handleCurrentUpdate"
@update:page-size="handlePageSizeUpdate"
/>
</div> </div>
<!-- 联系人模态框 --> <!-- 联系人模态框 -->
<div <div
...@@ -321,6 +328,31 @@ import { ref, computed, onMounted } from 'vue' ...@@ -321,6 +328,31 @@ import { ref, computed, onMounted } from 'vue'
// 引入我们创建的api拦截器 // 引入我们创建的api拦截器
import { contactApi } from '@/api/api' import { contactApi } from '@/api/api'
import type { Contact } from '@/types/index' import type { Contact } from '@/types/index'
// 引入分页组件
import Pagination from '@/components/Pagination.vue'
// 初始数据
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
// 处理分页变化
const handlePageChange = (page: number, size: number) => {
console.log('分页变化:', page, size)
fetchContacts()
// 这里可以发起API请求获取新数据
}
const handleCurrentUpdate = (page: number) => {
currentPage.value = page
}
const handlePageSizeUpdate = (size: number) => {
pageSize.value = size
currentPage.value = 1 // 重置到第一页
}
// 引入弹窗组件 // 引入弹窗组件
import CommonModal from '@/components/CommonModal.vue' import CommonModal from '@/components/CommonModal.vue'
// 弹窗提示信息对象 // 弹窗提示信息对象
...@@ -371,37 +403,17 @@ const handleCancel = (triggerKey: string) => { ...@@ -371,37 +403,17 @@ const handleCancel = (triggerKey: string) => {
modalVisible.value = false modalVisible.value = false
console.log('用户取消操作', triggerKey) console.log('用户取消操作', triggerKey)
} }
// 导入错误数据结构
interface ImportError {
row: number
message: string
}
// 导入结果数据结构
interface ImportResult {
success: boolean
message: string
stats?: {
success: number
skipped: number
failed: number
}
errors?: ImportError[]
}
const editingSenderId = ref('') const editingSenderId = ref('')
// 页面状态 // 页面状态
const contacts = ref<Contact[]>([]) const contacts = ref<Contact[]>([])
const searchQuery = ref('') const searchQuery = ref('')
const sortBy = ref('name') const sortBy = ref('name')
const currentPage = ref(1)
const pageSize = ref(10)
const totalContacts = ref(0)
const isLoading = ref(false) const isLoading = ref(false)
// 模态框状态 // 模态框状态
const showContactModal = ref(false) const showContactModal = ref(false)
const isEditing = ref(false) const isEditing = ref(false)
const showImportModal = ref(false) const showImportModal = ref(false)
const showImportResultModal = ref(false)
// 表单数据 // 表单数据
const form = ref<Partial<Contact>>({ const form = ref<Partial<Contact>>({
...@@ -435,20 +447,8 @@ const resetForm = () => { ...@@ -435,20 +447,8 @@ const resetForm = () => {
isEditing.value = false isEditing.value = false
showContactModal.value = true showContactModal.value = true
} }
// 导入相关状态
const importFile = ref<File | null>(null)
const isImporting = ref(false)
const isSubmitting = ref(false)
const importResult = ref<ImportResult>({
success: false,
message: '',
})
// 提示信息状态 const isSubmitting = ref(false)
const showSuccessToast = ref(false)
const showErrorToast = ref(false)
const successMessage = ref('')
const errorMessage = ref('')
// 初始化页面 // 初始化页面
onMounted(() => { onMounted(() => {
...@@ -460,11 +460,6 @@ const filteredContacts = computed<Contact[]>(() => { ...@@ -460,11 +460,6 @@ const filteredContacts = computed<Contact[]>(() => {
return contacts.value return contacts.value
}) })
// 总页数
const totalPages = computed(() => {
return Math.ceil(totalContacts.value / pageSize.value)
})
// 获取联系人列表 // 获取联系人列表
const fetchContacts = async () => { const fetchContacts = async () => {
try { try {
...@@ -476,7 +471,9 @@ const fetchContacts = async () => { ...@@ -476,7 +471,9 @@ const fetchContacts = async () => {
sortOrder: 'asc', sortOrder: 'asc',
}) })
contacts.value = data.data.records contacts.value = data.data.records
totalContacts.value = data.data.total total.value = data.data.total
currentPage.value = data.data.current
pageSize.value = data.data.size
} catch (error) { } catch (error) {
console.error('获取联系人失败:', error) console.error('获取联系人失败:', error)
openModal({ openModal({
...@@ -502,13 +499,6 @@ const resetSearch = () => { ...@@ -502,13 +499,6 @@ const resetSearch = () => {
fetchContacts() fetchContacts()
} }
// 切换页码
const changePage = (page: number) => {
if (page < 1 || page > totalPages.value) return
currentPage.value = page
fetchContacts()
}
// 打开添加联系人模态框 // 打开添加联系人模态框
const openAddContactModal = () => { const openAddContactModal = () => {
form.value = { form.value = {
...@@ -659,133 +649,6 @@ const deleteContactModal = async (contact: Contact) => { ...@@ -659,133 +649,6 @@ const deleteContactModal = async (contact: Contact) => {
}) })
} }
} }
// 处理导入文件选择
const handleFileSelected = (e: Event) => {
const input = e.target as HTMLInputElement
if (!input.files || input.files.length === 0) return
importFile.value = input.files[0]
}
// 移除导入文件
const removeImportFile = () => {
importFile.value = null
// 重置文件输入
const input = document.getElementById('contact-import-file') as HTMLInputElement
if (input) input.value = ''
}
// 格式化文件大小
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
// 提交导入文件 - 使用api拦截器
const submitImportFile = async () => {
if (!importFile.value) return
try {
isImporting.value = true
const formData = new FormData()
formData.append('file', importFile.value as File)
// 调用后端接口处理文件导入,会自动添加Authorization头
const result = await api.post('/contacts/import', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
// 导入成功,显示结果
importResult.value = {
success: true,
message: `导入完成,共处理 ${result.stats.success + result.stats.skipped + result.stats.failed} 条记录`,
stats: result.stats,
errors: result.errors,
}
showImportModal.value = false
showImportResultModal.value = true
// 刷新联系人列表
fetchContacts()
} catch (error) {
console.error('导入失败:', error)
importResult.value = {
success: false,
message: error instanceof Error ? error.message : '导入过程中发生错误,请稍后重试',
}
showImportModal.value = false
showImportResultModal.value = true
} finally {
isImporting.value = false
}
}
// 下载导入模板 - 使用api拦截器
const downloadImportTemplate = async () => {
try {
// 调用后端接口下载模板文件
const response = await api.get('/contacts/import/template', {
responseType: 'blob',
})
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '联系人导入模板.xlsx'
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (error) {
console.error('下载模板失败:', error)
showError('下载模板失败,请稍后重试')
}
}
// 关闭导入模态框
const closeImportModal = () => {
showImportModal.value = false
importFile.value = null
// 重置文件输入
const input = document.getElementById('contact-import-file') as HTMLInputElement
if (input) input.value = ''
}
// 关闭导入结果模态框
const closeImportResultModal = () => {
showImportResultModal.value = false
}
// 显示成功提示
const showSuccess = (message: string) => {
successMessage.value = message
showSuccessToast.value = true
setTimeout(() => {
showSuccessToast.value = false
}, 3000)
}
// 显示错误提示
const showError = (message: string) => {
errorMessage.value = message
showErrorToast.value = true
setTimeout(() => {
showErrorToast.value = false
}, 3000)
}
</script> </script>
<style scoped> <style scoped>
......
...@@ -18,12 +18,12 @@ ...@@ -18,12 +18,12 @@
<select <select
v-model="filterStatus" v-model="filterStatus"
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
@change="getEmailTaskMainList"
> >
<option value="">全部状态</option> <option value="">全部状态</option>
<option value="sent">已发送</option> <option v-for="item in statusOptions" :key="item.itemValue" :value="item.itemValue">
<option value="scheduled">已定时</option> {{ item.itemLabel }}
<option value="draft">草稿</option> </option>
<option value="failed">发送失败</option>
</select> </select>
</div> </div>
</div> </div>
...@@ -55,6 +55,11 @@ ...@@ -55,6 +55,11 @@
<th <th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" 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>
<th <th
...@@ -70,7 +75,10 @@ ...@@ -70,7 +75,10 @@
<td class="px-6 py-4 whitespace-nowrap max-w-xs truncate">{{ email.receiveEmails }}</td> <td class="px-6 py-4 whitespace-nowrap max-w-xs truncate">{{ email.receiveEmails }}</td>
<td class="px-6 py-4 max-w-xs truncate">{{ email.subject }}</td> <td class="px-6 py-4 max-w-xs truncate">{{ email.subject }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ formatDate(email.sendTime) }} {{ email.sendTime ? formatDate(email.sendTime) : '无' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ email.scheduleTime ? formatDate(email.scheduleTime) : '无' }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<span <span
...@@ -107,9 +115,19 @@ ...@@ -107,9 +115,19 @@
<i class="fas fa-history text-4xl mb-3 opacity-30"></i> <i class="fas fa-history text-4xl mb-3 opacity-30"></i>
<p>暂无邮件发送记录</p> <p>暂无邮件发送记录</p>
</div> </div>
<!-- 分页组件 -->
<Pagination
:total="total"
:current="currentPage"
:page-size="pageSize"
@change="handlePageChange"
@update:current="handleCurrentUpdate"
@update:page-size="handlePageSizeUpdate"
/>
</div> </div>
<!-- 查看详情 --> <!-- 查看详情 -->
<EmailDetailModal <EmailDetailModal
:statusOptions="statusOptions"
:visible="detailModalVisible" :visible="detailModalVisible"
:email-data="selectedEmail" :email-data="selectedEmail"
@close="detailModalVisible = false" @close="detailModalVisible = false"
...@@ -123,6 +141,32 @@ import { sendEmailApi, dictApi } from '@/api/api' ...@@ -123,6 +141,32 @@ import { sendEmailApi, dictApi } from '@/api/api'
import type { DictItem } from '@/types/index' import type { DictItem } from '@/types/index'
import EmailDetailModal from '@/components/EmailDetailModal.vue' import EmailDetailModal from '@/components/EmailDetailModal.vue'
// 引入分页组件
import Pagination from '@/components/Pagination.vue'
// 初始数据
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
// 处理分页变化
const handlePageChange = (page: number, size: number) => {
console.log('分页变化:', page, size)
currentPage.value = page
pageSize.value = size
getEmailTaskMainList()
}
const handleCurrentUpdate = (page: number) => {
currentPage.value = page
}
const handlePageSizeUpdate = (size: number) => {
pageSize.value = size
currentPage.value = 1 // 重置到第一页
getEmailTaskMainList()
}
// 状态 // 状态
const emails = ref<EmailTask[]>([]) const emails = ref<EmailTask[]>([])
const detailModalVisible = ref(false) const detailModalVisible = ref(false)
...@@ -130,10 +174,27 @@ const selectedEmail = ref<EmailTask>() ...@@ -130,10 +174,27 @@ const selectedEmail = ref<EmailTask>()
const searchTerm = ref('') const searchTerm = ref('')
const filterStatus = ref('') const filterStatus = ref('')
const statusOptions = ref<DictItem[]>([]) const statusOptions = ref<DictItem[]>([])
const isDictLoading = ref(false) // 新增:状态选项加载状态
const isEmailListLoading = ref(false) // 新增:邮件列表加载状态
onMounted(() => { onMounted(() => {
getEmailTaskMainList() loadDataSequentially()
}) })
// 顺序加载数据:先状态选项,后邮件列表
const loadDataSequentially = async () => {
try {
isDictLoading.value = true
await getDictLists()
isDictLoading.value = false
// 状态选项加载完成后,自动加载邮件列表
await getEmailTaskMainList()
} catch (error) {
console.error('数据加载失败:', error)
isDictLoading.value = false
isEmailListLoading.value = false
}
}
const viewDetail = (item: EmailTask) => { const viewDetail = (item: EmailTask) => {
console.log(item) console.log(item)
...@@ -159,14 +220,15 @@ const viewDetail = (item: EmailTask) => { ...@@ -159,14 +220,15 @@ const viewDetail = (item: EmailTask) => {
// 匹配发送状态 // 匹配发送状态
const getDictLists = async () => { const getDictLists = async () => {
const res = await dictApi.getDictList(['email_task_status']) try {
if (res.code === 200) { const res = await dictApi.getDictList(['email_task_status'])
console.log(res) if (res.code === 200) {
statusOptions.value = res.data[0].dictItemList || [] statusOptions.value = res.data[0].dictItemList || []
emails.value.forEach((email) => { console.log('状态选项加载完成:', statusOptions.value)
email.statusLabel = }
statusOptions.value.find((item) => item.itemValue === email.status)?.itemLabel || '未知状态' } catch (error) {
}) console.error('加载状态选项失败:', error)
throw error // 抛出错误以便上层处理
} }
} }
...@@ -180,16 +242,48 @@ const reuseEmailContent = (email: EmailTask) => { ...@@ -180,16 +242,48 @@ const reuseEmailContent = (email: EmailTask) => {
// 触发复用邮件内容事件 // 触发复用邮件内容事件
} }
// 发送任务列表查询 // 发送任务列表查询(改进版本)
const getEmailTaskMainList = async () => { const getEmailTaskMainList = async () => {
const params: EmailTask = { // 如果状态选项正在加载,等待加载完成
pageNum: 1, if (isDictLoading.value) {
pageSize: 100, console.log('等待状态选项加载完成...')
// 可以添加一个简单的等待机制
await new Promise((resolve) => setTimeout(resolve, 100))
return getEmailTaskMainList() // 递归调用直到状态选项加载完成
} }
const res = await sendEmailApi.getEmailTaskMainList(params)
if (res.code === 200) { // 如果状态选项为空,先加载状态选项
emails.value = res.data.records || [] if (statusOptions.value.length === 0) {
getDictLists() console.log('状态选项为空,先加载状态选项')
await getDictLists()
}
try {
isEmailListLoading.value = true
const params: EmailTask = {
pageNo: currentPage.value,
pageSize: pageSize.value,
status: filterStatus.value,
}
const res = await sendEmailApi.getEmailTaskMainList(params)
if (res.code === 200) {
emails.value = res.data.records || []
total.value = res.data.total || 0
// 为每个邮件设置状态标签
emails.value.forEach((email) => {
email.statusLabel =
statusOptions.value.find((item) => item.itemValue === email.status)?.itemLabel ||
'未知状态'
})
console.log('邮件列表加载完成,共', emails.value.length, '条记录')
}
} catch (error) {
console.error('获取邮件列表失败:', error)
} finally {
isEmailListLoading.value = false
} }
} }
</script> </script>
...@@ -5,11 +5,10 @@ ...@@ -5,11 +5,10 @@
> >
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md"> <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> <h3 class="text-lg font-semibold mb-4">{{ title }}</h3>
<input <FileUploadComponent
type="file" v-model="uploadedFiles"
:accept="accept" :config="uploadConfig"
@change="handleFileSelect" @success="handleDocumentUploadSuccess"
class="w-full px-3 py-2 border border-gray-300 rounded-md mb-4"
/> />
<div class="flex justify-end gap-3"> <div class="flex justify-end gap-3">
<button <button
...@@ -37,25 +36,33 @@ const props = defineProps({ ...@@ -37,25 +36,33 @@ const props = defineProps({
}, },
accept: { accept: {
type: String, type: String,
default: '.csv,.txt', default: '.csv,.xlsx',
}, },
}) })
/**
* 文件上传配置
*/
// 上传附件
import { ElMessage } from 'element-plus'
import FileUploadComponent from '@/components/FileUploadComponent.vue'
import { UploadResult, UploadConfig } from '@/utils/fileUpload'
const emit = defineEmits(['update:visible', 'file-selected']) const uploadConfig: UploadConfig = {
url: `${import.meta.env.VITE_REMOTE_API_BASE_URL}/email/api/emailFile/import/excel/variable`,
const handleFileSelect = (e: Event) => { fieldName: 'file',
const input = e.target as HTMLInputElement maxSize: 10,
if (input.files && input.files[0]) { allowedTypes: [],
const file = input.files[0] maxCount: 1,
const reader = new FileReader() multiple: false,
reader.onload = (e) => {
const content = e.target?.result as string
emit('file-selected', { file, content })
emit('update:visible', false)
}
reader.readAsText(file)
}
} }
// 上传成功的文件
const uploadedFiles = ref<any[]>([])
// 处理文档上传成功
const handleDocumentUploadSuccess = (results: UploadResult[]) => {
emit('update:visible', false)
emit('file-selected', results[0])
}
const emit = defineEmits(['update:visible', 'file-selected'])
const handleCancel = () => { const handleCancel = () => {
emit('update:visible', false) emit('update:visible', false)
......
...@@ -119,7 +119,7 @@ ...@@ -119,7 +119,7 @@
</div> </div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium"> <td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div v-if="editingRecordId === record.id" class="flex gap-2"> <div v-if="editingRecordId === record.importBizId" class="flex gap-2">
<button <button
@click="cancelEdit" @click="cancelEdit"
class="px-3 py-1 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors text-sm" class="px-3 py-1 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors text-sm"
...@@ -236,10 +236,6 @@ const handleCcInputKeydown = (event: KeyboardEvent) => { ...@@ -236,10 +236,6 @@ const handleCcInputKeydown = (event: KeyboardEvent) => {
} }
} }
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString('zh-CN')
}
const editRecord = (record: ImportRecord) => { const editRecord = (record: ImportRecord) => {
editingRecordId.value = record.importBizId editingRecordId.value = record.importBizId
editingRecord.value = { ...record } editingRecord.value = { ...record }
...@@ -257,11 +253,11 @@ const cancelEdit = () => { ...@@ -257,11 +253,11 @@ const cancelEdit = () => {
const saveEdit = () => { const saveEdit = () => {
if (editingRecordId.value && editingRecord.value.receiveEmail) { if (editingRecordId.value && editingRecord.value.receiveEmail) {
console.log(editingRecord.value)
emits('update-record', { emits('update-record', {
importBizId: editingRecordId.value, importBizId: editingRecordId.value,
receiveEmail: editingRecord.value.receiveEmail, receiveEmail: editingRecord.value.receiveEmail,
ccEmail: editingRecord.value.ccEmail || '', ccEmail: editingRecord.value.ccEmail || '',
updateTime: new Date().toISOString(),
}) })
editingRecordId.value = null editingRecordId.value = null
editingRecord.value = {} editingRecord.value = {}
......
...@@ -133,6 +133,15 @@ ...@@ -133,6 +133,15 @@
<i class="fas fa-envelope text-4xl mb-3 opacity-30"></i> <i class="fas fa-envelope text-4xl mb-3 opacity-30"></i>
<p>暂无发件人邮箱,请添加发件人</p> <p>暂无发件人邮箱,请添加发件人</p>
</div> </div>
<!-- 分页组件 -->
<Pagination
:total="total"
:current="currentPage"
:page-size="pageSize"
@change="handlePageChange"
@update:current="handleCurrentUpdate"
@update:page-size="handlePageSizeUpdate"
/>
</div> </div>
<CommonModal <CommonModal
v-model:visible="modalVisible" v-model:visible="modalVisible"
...@@ -151,6 +160,29 @@ ...@@ -151,6 +160,29 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { emailProviderApi, senderApi, getEmailSenderConfigList } from '@/api/api' import { emailProviderApi, senderApi, getEmailSenderConfigList } from '@/api/api'
import type { Sender } from '@/types/index' import type { Sender } from '@/types/index'
// 引入分页组件
import Pagination from '@/components/Pagination.vue'
// 初始数据
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
// 处理分页变化
const handlePageChange = (page: number, size: number) => {
console.log('分页变化:', page, size)
getSenders()
// 这里可以发起API请求获取新数据
}
const handleCurrentUpdate = (page: number) => {
currentPage.value = page
}
const handlePageSizeUpdate = (size: number) => {
pageSize.value = size
currentPage.value = 1 // 重置到第一页
}
// 引入弹窗组件 // 引入弹窗组件
import CommonModal from '@/components/CommonModal.vue' import CommonModal from '@/components/CommonModal.vue'
// 弹窗提示信息对象 // 弹窗提示信息对象
...@@ -241,11 +273,12 @@ const getSenders = () => { ...@@ -241,11 +273,12 @@ const getSenders = () => {
senderApi senderApi
.getEmailSenderConfigList({ .getEmailSenderConfigList({
emailSenderConfigName: '', emailSenderConfigName: '',
pageNo: 1, pageNo: currentPage.value,
pageSize: 100, pageSize: pageSize.value,
}) })
.then((res) => { .then((res) => {
senders.value = res.data?.records || [] senders.value = res.data?.records || []
total.value = res.data?.total || 0
}) })
} }
const saveSender = () => { const saveSender = () => {
......
...@@ -106,11 +106,11 @@ ...@@ -106,11 +106,11 @@
<p class="text-sm text-gray-600 mb-3">{{ template.description || '无描述' }}</p> <p class="text-sm text-gray-600 mb-3">{{ template.description || '无描述' }}</p>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<span <span
v-for="variableId in template.variableBizIdList" v-for="s in template.variableNameEnList || []"
:key="variableId" :key="s"
class="px-2 py-1 bg-blue-50 text-blue-700 rounded text-xs" class="px-2 py-1 bg-blue-50 text-blue-700 rounded text-xs"
> >
{{ variablePrefix }}{{ variableId }}{{ variableNextfix }} {{ variablePrefix }}{{ s }}{{ variableNextfix }}
</span> </span>
</div> </div>
<div class="mt-3 flex justify-end"> <div class="mt-3 flex justify-end">
...@@ -291,7 +291,7 @@ import { ref, defineProps, defineEmits, onMounted } from 'vue' ...@@ -291,7 +291,7 @@ import { ref, defineProps, defineEmits, onMounted } from 'vue'
import type { Variable, VariableTemplate } from '../types' import type { Variable, VariableTemplate } from '../types'
import { variableApi, variableGroupApi } from '@/api/api' import { variableApi, variableGroupApi } from '@/api/api'
import { ElMessage } from 'element-plus'
// 引入弹窗组件 // 引入弹窗组件
import CommonModal from '@/components/CommonModal.vue' import CommonModal from '@/components/CommonModal.vue'
// 弹窗提示信息对象 // 弹窗提示信息对象
...@@ -672,16 +672,20 @@ const getVariableKeyById = (id: string) => { ...@@ -672,16 +672,20 @@ const getVariableKeyById = (id: string) => {
} }
const generateExcelTemplate = (template: VariableTemplate) => { const generateExcelTemplate = (template: VariableTemplate) => {
// 模拟生成Excel模板 variableGroupApi
const variableNames = (template.variableBizIdList || []).map((id) => { .exportEmailVariableGroup(template.variableGroupBizId || '')
const variable = variables.value.find((v) => v.variableBizId === id) .then((response) => {
return variable ? variable.variableNameEn || '' : '' // 处理成功响应,例如下载文件
}) // 自动下载excel文件, 并命名为变量模版.xlsx
const a = document.createElement('a')
openModal({ a.href = response.data.url
title: '成功', a.download = `${template.groupName || '变量模版'}.xlsx`
message: `已生成包含以下变量的Excel模板:\n${variableNames.join(', ')}`, a.click()
}) // 实际项目中这里应该生成并下载Excel文件
// 实际项目中这里应该生成并下载Excel文件 })
.catch((error) => {
console.error('导出失败:', error)
ElMessage.error(error.response?.data?.message || '导出变量模版失败')
})
} }
</script> </script>
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