Skip to content
Toggle navigation
Projects
Groups
Snippets
Help
YYY
/
saas-v2-report-tool
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit 6e6af76f
authored
Jun 05, 2026
by
niuxing
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
调整监听配置
1 parent
ed210527
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
267 additions
and
56 deletions
server/index.js
server/routes/sql-manage.js
src/components/SendMailModal.jsx
src/components/SqlManagerModal.jsx
server/index.js
View file @
6e6af76
...
...
@@ -11,7 +11,7 @@ import schedulerRouter from './routes/scheduler.js'
const
__dirname
=
path
.
dirname
(
fileURLToPath
(
import
.
meta
.
url
))
const
app
=
express
()
const
PORT
=
4000
const
HOSTNAME
=
"
localhost
"
const
HOSTNAME
=
"
0.0.0.0
"
// 中间件
app
.
use
(
cors
())
...
...
server/routes/sql-manage.js
View file @
6e6af76
...
...
@@ -198,10 +198,10 @@ router.post('/:id/run', async (req, res) => {
}
})
// 手动发送邮件(可指定收件人)
// 手动发送邮件(可指定收件人
,支持群发/逐个发送
)
router
.
post
(
'/:id/send-mail'
,
async
(
req
,
res
)
=>
{
try
{
const
{
recipientEmails
,
emailSubject
}
=
req
.
body
const
{
recipientEmails
,
emailSubject
,
sendMode
}
=
req
.
body
const
scripts
=
loadScripts
()
const
script
=
scripts
.
find
(
s
=>
s
.
id
===
req
.
params
.
id
)
if
(
!
script
)
return
res
.
json
({
success
:
false
,
message
:
'脚本不存在'
})
...
...
@@ -311,6 +311,49 @@ router.post('/:id/send-mail', async (req, res) => {
</div>
`
// 逐个发送 vs 群发
const
recipientList
=
finalRecipients
.
split
(
','
).
map
(
e
=>
e
.
trim
()).
filter
(
e
=>
e
)
const
isIndividual
=
(
sendMode
||
'batch'
)
===
'individual'
if
(
isIndividual
&&
recipientList
.
length
>
1
)
{
// 逐个发送:每人一封单独邮件
let
successCount
=
0
let
failList
=
[]
for
(
const
rcpt
of
recipientList
)
{
try
{
await
sendReportMail
({
to
:
rcpt
,
subject
:
finalSubject
,
html
,
excelBuffer
,
fileName
,
})
successCount
++
}
catch
(
mailErr
)
{
failList
.
push
(
rcpt
)
console
.
error
(
`发送邮件到
${
rcpt
}
失败:`
,
mailErr
.
message
)
}
}
// 更新脚本状态
const
allScripts
=
loadScripts
()
const
idx
=
allScripts
.
findIndex
(
s
=>
s
.
id
===
script
.
id
)
if
(
idx
>=
0
)
{
allScripts
[
idx
].
lastRunAt
=
new
Date
().
toISOString
()
allScripts
[
idx
].
lastRunStatus
=
failList
.
length
===
0
?
'success'
:
(
successCount
>
0
?
'partial'
:
'failed'
)
allScripts
[
idx
].
lastRunMessage
=
failList
.
length
===
0
?
`逐个发送邮件成功,共
${
rows
.
length
}
条数据,
${
successCount
}
人已收到`
:
`逐个发送完成,成功
${
successCount
}
人,失败
${
failList
.
length
}
人:
${
failList
.
join
(
', '
)}
`
saveScripts
(
allScripts
)
}
if
(
failList
.
length
===
0
)
{
res
.
json
({
success
:
true
,
message
:
`逐个发送邮件成功,共
${
rows
.
length
}
条数据,
${
successCount
}
人已收到`
})
}
else
{
res
.
json
({
success
:
true
,
message
:
`逐个发送完成,成功
${
successCount
}
人,失败
${
failList
.
length
}
人:
${
failList
.
join
(
', '
)}
`
})
}
}
else
{
// 群发:一封邮件发送给所有人
await
sendReportMail
({
to
:
finalRecipients
,
subject
:
finalSubject
,
...
...
@@ -325,11 +368,12 @@ router.post('/:id/send-mail', async (req, res) => {
if
(
idx
>=
0
)
{
allScripts
[
idx
].
lastRunAt
=
new
Date
().
toISOString
()
allScripts
[
idx
].
lastRunStatus
=
'success'
allScripts
[
idx
].
lastRunMessage
=
`手动发送
邮件成功,共
${
rows
.
length
}
条数据,收件人:
${
finalRecipients
}
`
allScripts
[
idx
].
lastRunMessage
=
`群发
邮件成功,共
${
rows
.
length
}
条数据,收件人:
${
finalRecipients
}
`
saveScripts
(
allScripts
)
}
res
.
json
({
success
:
true
,
message
:
`邮件发送成功,共
${
rows
.
length
}
条数据,收件人:
${
finalRecipients
}
`
})
res
.
json
({
success
:
true
,
message
:
`群发邮件成功,共
${
rows
.
length
}
条数据,收件人:
${
finalRecipients
}
`
})
}
}
catch
(
e
)
{
// 更新脚本状态为失败
const
allScripts
=
loadScripts
()
...
...
src/components/SendMailModal.jsx
View file @
6e6af76
import
React
,
{
useState
,
useEffect
}
from
'react'
import
{
Modal
,
Form
,
Input
,
Button
,
Space
,
message
,
Tag
,
Alert
}
from
'antd'
import
{
SendOutlined
,
MailOutlined
}
from
'@ant-design/icons'
import
{
Modal
,
Form
,
Input
,
Button
,
Space
,
message
,
Tag
,
Alert
,
Radio
,
Tooltip
}
from
'antd'
import
{
SendOutlined
,
MailOutlined
,
PlusOutlined
,
UserAddOutlined
,
TeamOutlined
,
UserOutlined
,
CloseOutlined
,
ImportOutlined
}
from
'@ant-design/icons'
import
request
from
'../utils/request'
// 验证逗号分隔的多个邮箱
const
validateEmails
=
(
rule
,
value
)
=>
{
if
(
!
value
||
!
value
.
trim
())
return
Promise
.
reject
(
new
Error
(
'请输入收件人邮箱'
))
const
emails
=
value
.
split
(
','
).
map
(
e
=>
e
.
trim
()).
filter
(
e
=>
e
)
const
emailRegex
=
/^
[^\s
@
]
+@
[^\s
@
]
+
\.[^\s
@
]
+$/
const
invalid
=
emails
.
find
(
e
=>
!
emailRegex
.
test
(
e
))
if
(
invalid
)
return
Promise
.
reject
(
new
Error
(
`邮箱格式不正确:
${
invalid
}
`
))
return
Promise
.
resolve
()
}
const
emailRegex
=
/^
[^\s
@
]
+@
[^\s
@
]
+
\.[^\s
@
]
+$/
export
default
function
SendMailModal
({
open
,
onClose
,
script
,
onSent
})
{
const
[
form
]
=
Form
.
useForm
()
const
[
sending
,
setSending
]
=
useState
(
false
)
// 收件人列表
const
[
recipients
,
setRecipients
]
=
useState
([])
// 单个添加输入框
const
[
singleInput
,
setSingleInput
]
=
useState
(
''
)
// 批量导入输入框
const
[
batchInput
,
setBatchInput
]
=
useState
(
''
)
const
[
showBatchInput
,
setShowBatchInput
]
=
useState
(
false
)
// 发送模式: batch=群发(一封邮件所有人), individual=逐个发送(每人一封)
const
[
sendMode
,
setSendMode
]
=
useState
(
'batch'
)
useEffect
(()
=>
{
if
(
open
&&
script
)
{
form
.
resetFields
()
// 从脚本预设的收件人初始化列表
const
savedEmails
=
(
script
.
recipientEmails
||
''
).
split
(
','
).
map
(
e
=>
e
.
trim
()).
filter
(
e
=>
e
&&
emailRegex
.
test
(
e
))
setRecipients
(
savedEmails
)
setSingleInput
(
''
)
setBatchInput
(
''
)
setShowBatchInput
(
false
)
setSendMode
(
'batch'
)
setTimeout
(()
=>
{
form
.
setFieldsValue
({
recipientEmails
:
script
.
recipientEmails
||
''
,
emailSubject
:
script
.
emailSubject
||
''
,
})
},
0
)
}
},
[
open
,
script
])
// 添加单个收件人
const
handleAddSingle
=
()
=>
{
const
email
=
singleInput
.
trim
()
if
(
!
email
)
return
if
(
!
emailRegex
.
test
(
email
))
{
message
.
warning
(
`邮箱格式不正确:
${
email
}
`
)
return
}
if
(
recipients
.
includes
(
email
))
{
message
.
info
(
'该收件人已在列表中'
)
return
}
setRecipients
([...
recipients
,
email
])
setSingleInput
(
''
)
}
// 批量导入收件人
const
handleBatchImport
=
()
=>
{
if
(
!
batchInput
.
trim
())
return
const
emails
=
batchInput
.
split
(
/
[
,;
\n\s]
+/
).
map
(
e
=>
e
.
trim
()).
filter
(
e
=>
e
)
const
invalidEmails
=
emails
.
filter
(
e
=>
!
emailRegex
.
test
(
e
))
if
(
invalidEmails
.
length
>
0
)
{
message
.
warning
(
`以下邮箱格式不正确:
${
invalidEmails
.
join
(
', '
)}
`
)
}
const
validEmails
=
emails
.
filter
(
e
=>
emailRegex
.
test
(
e
))
// 去重
const
newEmails
=
validEmails
.
filter
(
e
=>
!
recipients
.
includes
(
e
))
if
(
newEmails
.
length
>
0
)
{
setRecipients
([...
recipients
,
...
newEmails
])
message
.
success
(
`成功添加
${
newEmails
.
length
}
个收件人`
)
}
else
{
message
.
info
(
'没有新的收件人可添加'
)
}
setBatchInput
(
''
)
setShowBatchInput
(
false
)
}
// 删除收件人
const
handleRemoveRecipient
=
(
email
)
=>
{
setRecipients
(
recipients
.
filter
(
r
=>
r
!==
email
))
}
// 清空所有收件人
const
handleClearAll
=
()
=>
{
setRecipients
([])
}
// 从脚本预设导入
const
handleImportFromScript
=
()
=>
{
const
savedEmails
=
(
script
.
recipientEmails
||
''
).
split
(
','
).
map
(
e
=>
e
.
trim
()).
filter
(
e
=>
e
&&
emailRegex
.
test
(
e
))
const
newEmails
=
savedEmails
.
filter
(
e
=>
!
recipients
.
includes
(
e
))
if
(
newEmails
.
length
>
0
)
{
setRecipients
([...
recipients
,
...
newEmails
])
message
.
success
(
`从脚本配置导入
${
newEmails
.
length
}
个收件人`
)
}
else
{
message
.
info
(
'脚本配置中的收件人已全部在列表中'
)
}
}
const
handleSend
=
async
()
=>
{
if
(
recipients
.
length
===
0
)
{
message
.
warning
(
'请至少添加一个收件人'
)
return
}
try
{
const
values
=
await
form
.
validateFields
()
setSending
(
true
)
const
res
=
await
request
.
post
(
`/sql-manage/
${
script
.
id
}
/send-mail`
,
{
recipientEmails
:
values
.
recipientEmails
,
recipientEmails
:
recipients
.
join
(
','
)
,
emailSubject
:
values
.
emailSubject
,
sendMode
,
})
if
(
res
.
success
)
{
...
...
@@ -47,7 +118,7 @@ export default function SendMailModal({ open, onClose, script, onSent }) {
message
.
error
(
res
.
message
)
}
}
catch
(
e
)
{
if
(
e
.
errorFields
)
return
// 表单验证失败
if
(
e
.
errorFields
)
return
message
.
error
(
e
.
message
||
'发送失败'
)
}
finally
{
setSending
(
false
)
...
...
@@ -56,12 +127,6 @@ export default function SendMailModal({ open, onClose, script, onSent }) {
if
(
!
script
)
return
null
// 解析收件人列表用于标签展示
const
parseEmailTags
=
(
text
)
=>
{
if
(
!
text
)
return
[]
return
text
.
split
(
','
).
map
(
e
=>
e
.
trim
()).
filter
(
e
=>
e
)
}
return
(
<
Modal
title=
{
...
...
@@ -73,7 +138,7 @@ export default function SendMailModal({ open, onClose, script, onSent }) {
}
open=
{
open
}
onCancel=
{
onClose
}
width=
{
56
0
}
width=
{
62
0
}
footer=
{
<
Space
>
<
Button
onClick=
{
onClose
}
>
取消
</
Button
>
...
...
@@ -82,8 +147,12 @@ export default function SendMailModal({ open, onClose, script, onSent }) {
icon=
{
<
SendOutlined
/>
}
loading=
{
sending
}
onClick=
{
handleSend
}
disabled=
{
recipients
.
length
===
0
}
>
发送邮件
{
sendMode
===
'batch'
?
`群发邮件(${recipients.length}人)`
:
`逐个发送(${recipients.length}封)`
}
</
Button
>
</
Space
>
}
...
...
@@ -95,39 +164,118 @@ export default function SendMailModal({ open, onClose, script, onSent }) {
message=
"手动发送将执行脚本SQL,生成Excel附件并发送邮件到指定收件人。收件人可与脚本配置的不同。"
/>
<
Form
form=
{
form
}
layout=
"vertical"
>
<
Form
.
Item
name=
"recipientEmails"
label=
"收件人邮箱"
tooltip=
"多个收件人用英文逗号分隔"
rules=
{
[{
validator
:
validateEmails
}]
}
{
/* 发送模式选择 */
}
<
div
style=
{
{
marginBottom
:
16
}
}
>
<
div
style=
{
{
marginBottom
:
8
,
fontWeight
:
500
}
}
>
发送模式
</
div
>
<
Radio
.
Group
value=
{
sendMode
}
onChange=
{
e
=>
setSendMode
(
e
.
target
.
value
)
}
>
<
Radio
value=
"batch"
>
<
Space
size=
{
4
}
>
<
TeamOutlined
/>
<
span
>
群发
</
span
>
</
Space
>
<
span
style=
{
{
fontSize
:
12
,
color
:
'#999'
,
marginLeft
:
4
}
}
>
— 一封邮件发送给所有人
</
span
>
</
Radio
>
<
Radio
value=
"individual"
>
<
Space
size=
{
4
}
>
<
UserOutlined
/>
<
span
>
逐个发送
</
span
>
</
Space
>
<
span
style=
{
{
fontSize
:
12
,
color
:
'#999'
,
marginLeft
:
4
}
}
>
— 每人单独收到一封邮件
</
span
>
</
Radio
>
</
Radio
.
Group
>
</
div
>
{
/* 收件人管理 */
}
<
div
style=
{
{
marginBottom
:
16
}
}
>
<
div
style=
{
{
display
:
'flex'
,
justifyContent
:
'space-between'
,
alignItems
:
'center'
,
marginBottom
:
8
}
}
>
<
span
style=
{
{
fontWeight
:
500
}
}
>
收件人
{
recipients
.
length
>
0
&&
<
Tag
color=
"blue"
style=
{
{
marginLeft
:
4
}
}
>
{
recipients
.
length
}
人
</
Tag
>
}
</
span
>
<
Space
size=
{
4
}
>
{
script
.
recipientEmails
&&
(
<
Tooltip
title=
"从脚本配置的收件人导入"
>
<
Button
size=
"small"
type=
"dashed"
icon=
{
<
ImportOutlined
/>
}
onClick=
{
handleImportFromScript
}
>
导入预设
</
Button
>
</
Tooltip
>
)
}
{
recipients
.
length
>
0
&&
(
<
Button
size=
"small"
type=
"dashed"
danger
onClick=
{
handleClearAll
}
>
清空
</
Button
>
)
}
</
Space
>
</
div
>
{
/* 逐个添加 */
}
<
Space
.
Compact
style=
{
{
width
:
'100%'
,
marginBottom
:
8
}
}
>
<
Input
placeholder=
"输入邮箱地址,回车添加"
value=
{
singleInput
}
onChange=
{
e
=>
setSingleInput
(
e
.
target
.
value
)
}
onPressEnter=
{
handleAddSingle
}
prefix=
{
<
UserAddOutlined
style=
{
{
color
:
'#bbb'
}
}
/>
}
/>
<
Button
type=
"primary"
icon=
{
<
PlusOutlined
/>
}
onClick=
{
handleAddSingle
}
>
添加
</
Button
>
</
Space
.
Compact
>
{
/* 批量导入切换 */
}
{
!
showBatchInput
?
(
<
Button
size=
"small"
type=
"dashed"
icon=
{
<
TeamOutlined
/>
}
onClick=
{
()
=>
setShowBatchInput
(
true
)
}
style=
{
{
marginBottom
:
8
}
}
>
批量导入
</
Button
>
)
:
(
<
div
style=
{
{
marginBottom
:
8
}
}
>
<
Input
.
TextArea
placeholder=
"例如:zhangsan@company.com,lisi@company.com"
placeholder=
"输入多个邮箱,用逗号、分号或换行分隔"
value=
{
batchInput
}
onChange=
{
e
=>
setBatchInput
(
e
.
target
.
value
)
}
autoSize=
{
{
minRows
:
2
,
maxRows
:
4
}
}
onChange=
{
()
=>
{
// 实时更新标签展示
}
}
/>
</
Form
.
Item
>
<
Space
style=
{
{
marginTop
:
4
}
}
>
<
Button
size=
"small"
type=
"primary"
onClick=
{
handleBatchImport
}
>
确认导入
</
Button
>
<
Button
size=
"small"
onClick=
{
()
=>
{
setShowBatchInput
(
false
);
setBatchInput
(
''
)
}
}
>
取消
</
Button
>
</
Space
>
</
div
>
)
}
<
Form
.
Item
shouldUpdate=
{
(
prev
,
cur
)
=>
prev
.
recipientEmails
!==
cur
.
recipientEmails
}
>
{
({
getFieldValue
})
=>
{
const
emails
=
parseEmailTags
(
getFieldValue
(
'recipientEmails'
))
if
(
emails
.
length
===
0
)
return
null
return
(
<
div
style=
{
{
marginTop
:
-
12
,
marginBottom
:
16
}
}
>
<
span
style=
{
{
fontSize
:
12
,
color
:
'#999'
,
marginRight
:
8
}
}
>
收件人预览:
</
span
>
<
Space
size=
{
4
}
wrap
>
{
emails
.
map
((
email
,
i
)
=>
(
<
Tag
key=
{
i
}
color=
"blue"
icon=
{
<
MailOutlined
/>
}
>
{
email
}
</
Tag
>
{
/* 收件人标签列表 */
}
{
recipients
.
length
>
0
&&
(
<
div
style=
{
{
padding
:
'8px 12px'
,
background
:
'#fafafa'
,
border
:
'1px solid #f0f0f0'
,
borderRadius
:
6
,
maxHeight
:
180
,
overflowY
:
'auto'
,
}
}
>
<
Space
size=
{
[
8
,
8
]
}
wrap
>
{
recipients
.
map
((
email
)
=>
(
<
Tag
key=
{
email
}
color=
"blue"
icon=
{
<
MailOutlined
/>
}
closable
onClose=
{
()
=>
handleRemoveRecipient
(
email
)
}
closeIcon=
{
<
CloseOutlined
style=
{
{
fontSize
:
10
}
}
/>
}
>
{
email
}
</
Tag
>
))
}
</
Space
>
</
div
>
)
}
}
</
Form
.
Item
>
)
}
</
div
>
<
Form
form=
{
form
}
layout=
"vertical"
>
<
Form
.
Item
name=
"emailSubject"
label=
"邮件主题"
...
...
src/components/SqlManagerModal.jsx
View file @
6e6af76
...
...
@@ -440,7 +440,7 @@ export default function SqlManagerModal({ open, onClose, onLoadSql, currentSql,
{
title
:
'操作'
,
key
:
'action'
,
width
:
2
0
0
,
width
:
2
6
0
,
render
:
(
_
,
record
)
=>
(
<
Space
size=
{
4
}
>
<
Tooltip
title=
"加载到编辑器"
>
...
...
@@ -451,8 +451,10 @@ export default function SqlManagerModal({ open, onClose, onLoadSql, currentSql,
<
Tooltip
title=
"编辑脚本配置"
>
<
Button
type=
"link"
size=
"small"
icon=
{
<
EditOutlined
/>
}
onClick=
{
()
=>
handleEdit
(
record
)
}
/>
</
Tooltip
>
<
Tooltip
title=
"手动发送邮件"
>
<
Button
type=
"link"
size=
"small"
icon=
{
<
MailOutlined
/>
}
onClick=
{
()
=>
handleOpenSendMail
(
record
)
}
/>
<
Tooltip
title=
"手动发送邮件(可单独发送/群发)"
>
<
Button
type=
"link"
size=
"small"
style=
{
{
color
:
'#1890ff'
}
}
icon=
{
<
MailOutlined
/>
}
onClick=
{
()
=>
handleOpenSendMail
(
record
)
}
>
发邮件
</
Button
>
</
Tooltip
>
{
record
.
cronExpression
&&
(
<
Tooltip
title=
"手动触发执行"
>
...
...
@@ -530,12 +532,29 @@ export default function SqlManagerModal({ open, onClose, onLoadSql, currentSql,
}
}
>
{
record
.
sql
}
</
pre
>
<
div
style=
{
{
marginTop
:
8
,
display
:
'flex'
,
justifyContent
:
'space-between'
,
alignItems
:
'center'
}
}
>
<
div
>
{
record
.
recipientEmails
&&
(
<
div
style=
{
{
marginTop
:
8
,
fontSize
:
12
,
color
:
'#666'
}
}
>
<
span
style=
{
{
fontSize
:
12
,
color
:
'#666'
,
marginRight
:
16
}
}
>
<
MailOutlined
style=
{
{
marginRight
:
4
}
}
/>
收件人:
{
record
.
recipientEmails
}
</
div
>
</
span
>
)
}
{
record
.
purpose
&&
(
<
span
style=
{
{
fontSize
:
12
,
color
:
'#999'
}
}
>
用途:
{
record
.
purpose
}
</
span
>
)
}
</
div
>
<
Button
type=
"primary"
size=
"small"
icon=
{
<
MailOutlined
/>
}
onClick=
{
()
=>
handleOpenSendMail
(
record
)
}
>
发送邮件
</
Button
>
</
div
>
</
div
>
),
}
}
...
...
Write
Preview
Markdown
is supported
Attach a file
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 post a comment