Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
F
frontend-yd-email
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Sweet Zhang
frontend-yd-email
Commits
79fddc42
Commit
79fddc42
authored
Sep 29, 2025
by
Sweet Zhang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
分页对接,编辑数据对接
parent
efadec82
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
467 additions
and
298 deletions
+467
-298
.env.production
+3
-0
src/api/api.ts
+10
-0
src/components/EmailDetailModal.vue
+14
-5
src/components/Pagination.vue
+151
-0
src/types/index.ts
+2
-0
src/views/ComposeEmail.vue
+60
-63
src/views/ContactManagement.vue
+38
-175
src/views/EmailManagement.vue
+109
-15
src/views/ImportDialog.vue
+26
-19
src/views/ImportRecordManager.vue
+2
-6
src/views/SenderManagement.vue
+35
-2
src/views/VariableManagement.vue
+17
-13
No files found.
.env.production
0 → 100644
View file @
79fddc42
VITE_API_BASE_URL='/email/api'
VITE_REMOTE_API_BASE_URL=''
\ No newline at end of file
src/api/api.ts
View file @
79fddc42
...
...
@@ -110,6 +110,12 @@ export const variableGroupApi = {
getEmailVariableGroupList
:
(
params
:
VariableTemplate
):
Promise
<
ApiResponse
>
=>
{
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 = {
getEmailContactImportDetail
:
(
id
:
string
):
Promise
<
ApiResponse
>
=>
{
return
request
.
get
(
`
${
baseEmailUrl
}
/emailContactImport/detail/sessionId?sessionId=
${
id
}
`
)
},
// 删除导入联系人
deleteEmailContactImport
:
(
id
:
string
):
Promise
<
ApiResponse
>
=>
{
return
request
.
delete
(
`
${
baseEmailUrl
}
/emailContactImport/del?importBizId=
${
id
}
`
)
},
}
/**
* 发送邮件
...
...
src/components/EmailDetailModal.vue
View file @
79fddc42
...
...
@@ -24,8 +24,8 @@
<p
class=
"text-gray-900"
>
{{
emailData
?.
sendEmail
||
'无'
}}
</p>
</div>
<div>
<label
class=
"block text-sm font-medium text-gray-700 mb-1"
>
发送
时间
</label>
<p
class=
"text-gray-900"
>
{{
formatDate
(
emailData
?.
s
end
Time
)
||
'无'
}}
</p>
<label
class=
"block text-sm font-medium text-gray-700 mb-1"
>
定时
时间
</label>
<p
class=
"text-gray-900"
>
{{
formatDate
(
emailData
?.
s
chedule
Time
)
||
'无'
}}
</p>
</div>
<div>
<label
class=
"block text-sm font-medium text-gray-700 mb-1"
>
总收件人数
</label>
...
...
@@ -98,7 +98,7 @@
{{
formatDate
(
record
.
sendTime
)
||
'--'
}}
</td>
<td
class=
"px-6 py-4 text-sm text-gray-500 max-w-xs"
>
{{
record
.
failReason
||
'--'
}}
{{
record
.
errorMsg
||
'--'
}}
</td>
</tr>
</tbody>
...
...
@@ -122,13 +122,14 @@
<
script
setup
lang=
"ts"
>
import
{
ref
,
watch
}
from
'vue'
import
type
{
DictItem
}
from
'@/types/index'
// 定义props
interface
EmailRecord
{
receiveEmail
?:
string
status
?:
string
sendTime
?:
string
failReason
?:
string
errorMsg
?:
string
}
interface
EmailTask
{
...
...
@@ -137,11 +138,13 @@ interface EmailTask {
sendEmail
?:
string
sendTime
?:
string
records
?:
EmailRecord
[]
scheduleTime
?:
string
}
const
props
=
defineProps
<
{
visible
:
boolean
emailData
?:
EmailTask
statusOptions
?:
DictItem
[]
}
>
()
const
emit
=
defineEmits
<
{
...
...
@@ -155,8 +158,14 @@ const emailRecords = ref<EmailRecord[]>([])
watch
(
()
=>
props
.
emailData
,
(
newEmailData
)
=>
{
if
(
newEmailData
&&
newEmailData
.
records
)
{
if
(
newEmailData
&&
newEmailData
.
records
&&
props
.
statusOptions
)
{
emailRecords
.
value
=
newEmailData
.
records
// 为每个邮件设置状态标签
emailRecords
.
value
.
forEach
((
email
)
=>
{
email
.
statusLabel
=
props
.
statusOptions
.
find
((
item
)
=>
item
.
itemValue
===
email
.
status
)?.
itemLabel
||
'未知状态'
})
}
else
{
emailRecords
.
value
=
[]
}
...
...
src/components/Pagination.vue
0 → 100644
View file @
79fddc42
<
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
>
src/types/index.ts
View file @
79fddc42
...
...
@@ -48,6 +48,7 @@ export interface VariableTemplate extends Pagination<VariableTemplate> {
description
?:
string
variableBizIdList
?:
string
[]
variableNameEns
?:
string
[]
variableNameEnList
?:
string
[]
}
// 忘记密码表单类型
...
...
@@ -62,6 +63,7 @@ export interface ImportRecord extends Contact<ImportRecord> {
sessionId
?:
string
receiveEmailList
?:
string
[]
ccEmailList
?:
string
[]
ccEmail
?:
string
}
// 选择联系人时,调用接口,获取sessionId
...
...
src/views/ComposeEmail.vue
View file @
79fddc42
...
...
@@ -60,10 +60,6 @@
<button
@
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"
v-if=
"
!selectedVariableTemplate ||
selectedVariableTemplate?.variableGroupBizId == 'email_variable_group_a2Z0lJLQlCuO81ZE'
"
>
<i
class=
"fas fa-address-book mr-1"
></i>
选择联系人
</button>
...
...
@@ -85,14 +81,14 @@
>
<div
class=
"flex flex-wrap gap-2"
>
<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"
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
v-for=
"subtag in tag
.trim()
.split('
,
')
.split('
;
')
.filter((e) => e.trim())"
:key=
"subtag"
class=
"bg-orange-100 text-orange-800 px-2 py-0.5 rounded-full"
...
...
@@ -200,7 +196,7 @@
:records=
"importRecords"
@
update-record=
"updateImportRecord"
@
delete-record=
"deleteImportRecord"
@
close=
"
showImportRecordManager = false
"
@
close=
"
((showImportRecordManager = false), getImportedContacts())
"
/>
<!-- 导入数据弹窗 -->
...
...
@@ -210,22 +206,6 @@
accept=
".csv,.txt,.xlsx"
@
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
v-if=
"showContactSelector"
...
...
@@ -308,7 +288,6 @@ const emailForm = ref<EmailForm>({
})
const
selectedVariableTemplate
=
ref
<
VariableTemplate
|
null
>
(
null
)
const
attachments
=
ref
<
File
[]
>
([])
const
showContactSelector
=
ref
(
false
)
const
showVariableSelector
=
ref
(
false
)
const
showImportContacts
=
ref
(
false
)
...
...
@@ -320,11 +299,11 @@ watch(
()
=>
emailForm
.
value
.
receiveEmail
,
(
newReceiveEmail
)
=>
{
if
(
newReceiveEmail
)
{
const
matchedRecord
=
importRecords
.
value
.
find
(
(
record
)
=>
record
.
receiveEmail
===
newReceiveEmail
,
const
matchedRecord
=
importRecords
.
value
.
find
(
(
record
)
=>
record
.
receiveEmailList
?.
includes
(
newReceiveEmail
)
,
)
if
(
matchedRecord
&&
matchedRecord
.
ccEmailList
)
{
emailForm
.
value
.
ccEmailList
=
matchedRecord
.
ccEmailList
emailForm
.
value
.
ccEmailList
=
matchedRecord
.
ccEmailList
||
[]
}
}
},
...
...
@@ -406,36 +385,53 @@ const confirmContactSelection = (selected: Contact<unknown>[]) => {
// 更新导入记录
const
updateImportRecord
=
(
updatedRecord
:
ImportRecord
)
=>
{
const
index
=
importRecords
.
value
.
findIndex
((
record
)
=>
record
.
id
===
updatedRecord
.
id
)
if
(
index
!==
-
1
)
{
importRecords
.
value
[
index
]
=
{
...
updatedRecord
}
const
params
=
{
...
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
)
=>
{
importRecords
.
value
=
importRecords
.
value
.
filter
((
record
)
=>
record
.
id
!==
id
)
const
deleteImportRecord
=
(
importBizId
:
string
)
=>
{
importContactApi
.
deleteEmailContactImport
(
importBizId
).
then
((
res
)
=>
{
if
(
res
.
code
===
200
)
{
ElMessage
({
message
:
'导入记录删除成功'
,
type
:
'success'
,
})
// 删除成功之后,刷新导入记录列表
getImportedContacts
(
emailForm
.
value
.
sessionId
||
''
)
}
else
{
ElMessage
({
message
:
'导入记录删除失败'
,
type
:
'error'
,
})
}
})
}
// 修改handleImportContacts方法
const
handleImportContacts
=
(
event
:
{
file
:
File
;
content
:
string
})
=>
{
const
{
content
}
=
event
// 解析CSV或文本文件,这里简化处理
const
lines
=
content
.
split
(
'
\
n'
)
lines
.
forEach
((
line
)
=>
{
const
[
to
,
cc
]
=
line
.
split
(
','
)
if
(
to
&&
to
.
includes
(
'@'
))
{
importRecords
.
value
.
push
({
id
:
Date
.
now
().
toString
(),
receiveEmail
:
to
.
trim
(),
ccEmailList
:
cc
?
cc
.
split
(
','
).
map
((
email
)
=>
email
.
trim
())
:
[],
contactBizId
:
''
,
name
:
''
,
type
:
''
,
pageNo
:
1
,
})
const
handleImportContacts
=
(
results
)
=>
{
console
.
log
(
results
)
if
(
results
.
data
)
{
emailForm
.
value
.
sessionId
=
results
.
data
.
sessionId
||
''
}
})
}
// 发送邮件
...
...
@@ -454,6 +450,14 @@ const sendEmail = () => {
message
:
'邮件发送成功'
,
type
:
'success'
,
})
// 发送成功之后,清空表单
emailForm
.
value
=
{
receiveEmail
:
''
,
ccEmailList
:
[],
content
:
''
,
attachmentPath
:
''
,
sessionId
:
''
,
}
}
else
{
ElMessage
({
message
:
'邮件发送失败'
,
...
...
@@ -464,30 +468,22 @@ const sendEmail = () => {
}
// 通过sessionId获取导入的联系人
const
getImportedContacts
=
(
sessionId
:
string
)
=>
{
const
getImportedContacts
=
(
sessionId
?
:
string
)
=>
{
const
params
=
{
sessionId
:
sessionId
||
''
,
sessionId
:
sessionId
||
emailForm
.
value
.
sessionId
||
''
,
source
:
importSource
.
value
,
}
importContactApi
.
getEmailContactImportList
(
params
).
then
((
res
)
=>
{
if
(
res
.
code
===
200
)
{
console
.
log
(
'导入的联系人:'
,
res
.
data
)
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) => {
import
{
ElMessage
}
from
'element-plus'
import
FileUploadComponent
from
'@/components/FileUploadComponent.vue'
import
{
UploadResult
}
from
'@/utils/fileUpload'
import
{
get
}
from
'http'
// 上传成功的文件
const
uploadedFiles
=
ref
<
any
[]
>
([])
...
...
@@ -504,7 +501,7 @@ const handleDocumentUploadSuccess = (results: UploadResult[]) => {
// 发送邮件时,接口入参attachmentPath,填写附件路径,多个有分号隔开,路径在result里面的data,里面的accessUrl
// 过滤出成功上传的文件
const
successFiles
=
results
.
filter
((
r
)
=>
r
.
success
)
const
attachmentPath
=
successFiles
.
map
((
r
)
=>
r
.
data
?.
data
.
accessU
rl
||
''
).
join
(
';'
)
const
attachmentPath
=
successFiles
.
map
((
r
)
=>
r
.
data
?.
data
.
u
rl
||
''
).
join
(
';'
)
emailForm
.
value
.
attachmentPath
=
attachmentPath
ElMessage
.
success
(
`成功上传
${
successCount
}
个文档`
)
...
...
src/views/ContactManagement.vue
View file @
79fddc42
...
...
@@ -157,8 +157,15 @@
</tbody>
</table>
</div>
<!-- 空状态、加载状态和分页组件与之前保持一致 -->
<!-- 分页组件 -->
<Pagination
:total=
"total"
:current=
"currentPage"
:page-size=
"pageSize"
@
change=
"handlePageChange"
@
update:current=
"handleCurrentUpdate"
@
update:page-size=
"handlePageSizeUpdate"
/>
</div>
<!-- 联系人模态框 -->
<div
...
...
@@ -321,6 +328,31 @@ import { ref, computed, onMounted } from 'vue'
// 引入我们创建的api拦截器
import
{
contactApi
}
from
'@/api/api'
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'
// 弹窗提示信息对象
...
...
@@ -371,37 +403,17 @@ const handleCancel = (triggerKey: string) => {
modalVisible
.
value
=
false
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
contacts
=
ref
<
Contact
[]
>
([])
const
searchQuery
=
ref
(
''
)
const
sortBy
=
ref
(
'name'
)
const
currentPage
=
ref
(
1
)
const
pageSize
=
ref
(
10
)
const
totalContacts
=
ref
(
0
)
const
isLoading
=
ref
(
false
)
// 模态框状态
const
showContactModal
=
ref
(
false
)
const
isEditing
=
ref
(
false
)
const
showImportModal
=
ref
(
false
)
const
showImportResultModal
=
ref
(
false
)
// 表单数据
const
form
=
ref
<
Partial
<
Contact
>>
({
...
...
@@ -435,20 +447,8 @@ const resetForm = () => {
isEditing
.
value
=
false
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
showSuccessToast
=
ref
(
false
)
const
showErrorToast
=
ref
(
false
)
const
successMessage
=
ref
(
''
)
const
errorMessage
=
ref
(
''
)
const
isSubmitting
=
ref
(
false
)
// 初始化页面
onMounted
(()
=>
{
...
...
@@ -460,11 +460,6 @@ const filteredContacts = computed<Contact[]>(() => {
return
contacts
.
value
})
// 总页数
const
totalPages
=
computed
(()
=>
{
return
Math
.
ceil
(
totalContacts
.
value
/
pageSize
.
value
)
})
// 获取联系人列表
const
fetchContacts
=
async
()
=>
{
try
{
...
...
@@ -476,7 +471,9 @@ const fetchContacts = async () => {
sortOrder
:
'asc'
,
})
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
)
{
console
.
error
(
'获取联系人失败:'
,
error
)
openModal
({
...
...
@@ -502,13 +499,6 @@ const resetSearch = () => {
fetchContacts
()
}
// 切换页码
const
changePage
=
(
page
:
number
)
=>
{
if
(
page
<
1
||
page
>
totalPages
.
value
)
return
currentPage
.
value
=
page
fetchContacts
()
}
// 打开添加联系人模态框
const
openAddContactModal
=
()
=>
{
form
.
value
=
{
...
...
@@ -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
>
<
style
scoped
>
...
...
src/views/EmailManagement.vue
View file @
79fddc42
...
...
@@ -18,12 +18,12 @@
<select
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"
@
change=
"getEmailTaskMainList"
>
<option
value=
""
>
全部状态
</option>
<option
value=
"sent"
>
已发送
</option>
<option
value=
"scheduled"
>
已定时
</option>
<option
value=
"draft"
>
草稿
</option>
<option
value=
"failed"
>
发送失败
</option>
<option
v-for=
"item in statusOptions"
:key=
"item.itemValue"
:value=
"item.itemValue"
>
{{
item
.
itemLabel
}}
</option>
</select>
</div>
</div>
...
...
@@ -55,6 +55,11 @@
<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
...
...
@@ -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 max-w-xs truncate"
>
{{
email
.
subject
}}
</td>
<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
class=
"px-6 py-4 whitespace-nowrap"
>
<span
...
...
@@ -107,9 +115,19 @@
<i
class=
"fas fa-history text-4xl mb-3 opacity-30"
></i>
<p>
暂无邮件发送记录
</p>
</div>
<!-- 分页组件 -->
<Pagination
:total=
"total"
:current=
"currentPage"
:page-size=
"pageSize"
@
change=
"handlePageChange"
@
update:current=
"handleCurrentUpdate"
@
update:page-size=
"handlePageSizeUpdate"
/>
</div>
<!-- 查看详情 -->
<EmailDetailModal
:statusOptions=
"statusOptions"
:visible=
"detailModalVisible"
:email-data=
"selectedEmail"
@
close=
"detailModalVisible = false"
...
...
@@ -123,6 +141,32 @@ import { sendEmailApi, dictApi } from '@/api/api'
import
type
{
DictItem
}
from
'@/types/index'
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
detailModalVisible
=
ref
(
false
)
...
...
@@ -130,10 +174,27 @@ const selectedEmail = ref<EmailTask>()
const
searchTerm
=
ref
(
''
)
const
filterStatus
=
ref
(
''
)
const
statusOptions
=
ref
<
DictItem
[]
>
([])
const
isDictLoading
=
ref
(
false
)
// 新增:状态选项加载状态
const
isEmailListLoading
=
ref
(
false
)
// 新增:邮件列表加载状态
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
)
=>
{
console
.
log
(
item
)
...
...
@@ -159,14 +220,15 @@ const viewDetail = (item: EmailTask) => {
// 匹配发送状态
const
getDictLists
=
async
()
=>
{
try
{
const
res
=
await
dictApi
.
getDictList
([
'email_task_status'
])
if
(
res
.
code
===
200
)
{
console
.
log
(
res
)
statusOptions
.
value
=
res
.
data
[
0
].
dictItemList
||
[]
emails
.
value
.
forEach
((
email
)
=>
{
email
.
statusLabel
=
statusOptions
.
value
.
find
((
item
)
=>
item
.
itemValue
===
email
.
status
)?.
itemLabel
||
'未知状态'
})
console
.
log
(
'状态选项加载完成:'
,
statusOptions
.
value
)
}
}
catch
(
error
)
{
console
.
error
(
'加载状态选项失败:'
,
error
)
throw
error
// 抛出错误以便上层处理
}
}
...
...
@@ -180,16 +242,48 @@ const reuseEmailContent = (email: EmailTask) => {
// 触发复用邮件内容事件
}
// 发送任务列表查询
// 发送任务列表查询
(改进版本)
const
getEmailTaskMainList
=
async
()
=>
{
// 如果状态选项正在加载,等待加载完成
if
(
isDictLoading
.
value
)
{
console
.
log
(
'等待状态选项加载完成...'
)
// 可以添加一个简单的等待机制
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
100
))
return
getEmailTaskMainList
()
// 递归调用直到状态选项加载完成
}
// 如果状态选项为空,先加载状态选项
if
(
statusOptions
.
value
.
length
===
0
)
{
console
.
log
(
'状态选项为空,先加载状态选项'
)
await
getDictLists
()
}
try
{
isEmailListLoading
.
value
=
true
const
params
:
EmailTask
=
{
pageNum
:
1
,
pageSize
:
100
,
pageNo
:
currentPage
.
value
,
pageSize
:
pageSize
.
value
,
status
:
filterStatus
.
value
,
}
const
res
=
await
sendEmailApi
.
getEmailTaskMainList
(
params
)
if
(
res
.
code
===
200
)
{
emails
.
value
=
res
.
data
.
records
||
[]
getDictLists
()
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
>
src/views/ImportDialog.vue
View file @
79fddc42
...
...
@@ -5,11 +5,10 @@
>
<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"
<FileUploadComponent
v-model=
"uploadedFiles"
:config=
"uploadConfig"
@
success=
"handleDocumentUploadSuccess"
/>
<div
class=
"flex justify-end gap-3"
>
<button
...
...
@@ -37,25 +36,33 @@ const props = defineProps({
},
accept
:
{
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
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
})
const
uploadConfig
:
UploadConfig
=
{
url
:
`
${
import
.
meta
.
env
.
VITE_REMOTE_API_BASE_URL
}
/email/api/emailFile/import/excel/variable`
,
fieldName
:
'file'
,
maxSize
:
10
,
allowedTypes
:
[],
maxCount
:
1
,
multiple
:
false
,
}
// 上传成功的文件
const
uploadedFiles
=
ref
<
any
[]
>
([])
// 处理文档上传成功
const
handleDocumentUploadSuccess
=
(
results
:
UploadResult
[])
=>
{
emit
(
'update:visible'
,
false
)
}
reader
.
readAsText
(
file
)
}
emit
(
'file-selected'
,
results
[
0
])
}
const
emit
=
defineEmits
([
'update:visible'
,
'file-selected'
])
const
handleCancel
=
()
=>
{
emit
(
'update:visible'
,
false
)
...
...
src/views/ImportRecordManager.vue
View file @
79fddc42
...
...
@@ -119,7 +119,7 @@
</div>
</td>
<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.i
mportBizI
d"
class=
"flex gap-2"
>
<button
@
click=
"cancelEdit"
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) => {
}
}
const
formatDate
=
(
dateString
:
string
)
=>
{
return
new
Date
(
dateString
).
toLocaleString
(
'zh-CN'
)
}
const
editRecord
=
(
record
:
ImportRecord
)
=>
{
editingRecordId
.
value
=
record
.
importBizId
editingRecord
.
value
=
{
...
record
}
...
...
@@ -257,11 +253,11 @@ const cancelEdit = () => {
const
saveEdit
=
()
=>
{
if
(
editingRecordId
.
value
&&
editingRecord
.
value
.
receiveEmail
)
{
console
.
log
(
editingRecord
.
value
)
emits
(
'update-record'
,
{
importBizId
:
editingRecordId
.
value
,
receiveEmail
:
editingRecord
.
value
.
receiveEmail
,
ccEmail
:
editingRecord
.
value
.
ccEmail
||
''
,
updateTime
:
new
Date
().
toISOString
(),
})
editingRecordId
.
value
=
null
editingRecord
.
value
=
{}
...
...
src/views/SenderManagement.vue
View file @
79fddc42
...
...
@@ -133,6 +133,15 @@
<i
class=
"fas fa-envelope text-4xl mb-3 opacity-30"
></i>
<p>
暂无发件人邮箱,请添加发件人
</p>
</div>
<!-- 分页组件 -->
<Pagination
:total=
"total"
:current=
"currentPage"
:page-size=
"pageSize"
@
change=
"handlePageChange"
@
update:current=
"handleCurrentUpdate"
@
update:page-size=
"handlePageSizeUpdate"
/>
</div>
<CommonModal
v-model:visible=
"modalVisible"
...
...
@@ -151,6 +160,29 @@
import
{
ref
,
onMounted
}
from
'vue'
import
{
emailProviderApi
,
senderApi
,
getEmailSenderConfigList
}
from
'@/api/api'
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'
// 弹窗提示信息对象
...
...
@@ -241,11 +273,12 @@ const getSenders = () => {
senderApi
.
getEmailSenderConfigList
({
emailSenderConfigName
:
''
,
pageNo
:
1
,
pageSize
:
100
,
pageNo
:
currentPage
.
value
,
pageSize
:
pageSize
.
value
,
})
.
then
((
res
)
=>
{
senders
.
value
=
res
.
data
?.
records
||
[]
total
.
value
=
res
.
data
?.
total
||
0
})
}
const
saveSender
=
()
=>
{
...
...
src/views/VariableManagement.vue
View file @
79fddc42
...
...
@@ -106,11 +106,11 @@
<p
class=
"text-sm text-gray-600 mb-3"
>
{{
template
.
description
||
'无描述'
}}
</p>
<div
class=
"flex flex-wrap gap-2"
>
<span
v-for=
"
variableId in template.variableBizIdList
"
:key=
"
variableId
"
v-for=
"
s in template.variableNameEnList || []
"
:key=
"
s
"
class=
"px-2 py-1 bg-blue-50 text-blue-700 rounded text-xs"
>
{{
variablePrefix
}}{{
variableId
}}{{
variableNextfix
}}
{{
variablePrefix
}}{{
s
}}{{
variableNextfix
}}
</span>
</div>
<div
class=
"mt-3 flex justify-end"
>
...
...
@@ -291,7 +291,7 @@ import { ref, defineProps, defineEmits, onMounted } from 'vue'
import
type
{
Variable
,
VariableTemplate
}
from
'../types'
import
{
variableApi
,
variableGroupApi
}
from
'@/api/api'
import
{
ElMessage
}
from
'element-plus'
// 引入弹窗组件
import
CommonModal
from
'@/components/CommonModal.vue'
// 弹窗提示信息对象
...
...
@@ -672,16 +672,20 @@ const getVariableKeyById = (id: string) => {
}
const
generateExcelTemplate
=
(
template
:
VariableTemplate
)
=>
{
// 模拟生成Excel模板
const
variableNames
=
(
template
.
variableBizIdList
||
[]).
map
((
id
)
=>
{
const
variable
=
variables
.
value
.
find
((
v
)
=>
v
.
variableBizId
===
id
)
return
variable
?
variable
.
variableNameEn
||
''
:
''
variableGroupApi
.
exportEmailVariableGroup
(
template
.
variableGroupBizId
||
''
)
.
then
((
response
)
=>
{
// 处理成功响应,例如下载文件
// 自动下载excel文件, 并命名为变量模版.xlsx
const
a
=
document
.
createElement
(
'a'
)
a
.
href
=
response
.
data
.
url
a
.
download
=
`
${
template
.
groupName
||
'变量模版'
}
.xlsx`
a
.
click
()
// 实际项目中这里应该生成并下载Excel文件
})
openModal
({
title
:
'成功'
,
message
:
`已生成包含以下变量的Excel模板:\n
${
variableNames
.
join
(
', '
)}
`
,
.
catch
((
error
)
=>
{
console
.
error
(
'导出失败:'
,
error
)
ElMessage
.
error
(
error
.
response
?.
data
?.
message
||
'导出变量模版失败'
)
})
// 实际项目中这里应该生成并下载Excel文件
}
</
script
>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment