Commit 6e6af76f by niuxing

调整监听配置

1 parent ed210527
......@@ -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())
......
......@@ -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,25 +311,69 @@ router.post('/:id/send-mail', async (req, res) => {
</div>
`
await sendReportMail({
to: finalRecipients,
subject: finalSubject,
html,
excelBuffer,
fileName,
})
// 逐个发送 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 = 'success'
allScripts[idx].lastRunMessage = `手动发送邮件成功,共${rows.length}条数据,收件人:${finalRecipients}`
saveScripts(allScripts)
}
// 更新脚本状态
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,
html,
excelBuffer,
fileName,
})
res.json({ success: true, message: `邮件发送成功,共${rows.length}条数据,收件人:${finalRecipients}` })
// 更新脚本状态
const allScripts = loadScripts()
const idx = allScripts.findIndex(s => s.id === script.id)
if (idx >= 0) {
allScripts[idx].lastRunAt = new Date().toISOString()
allScripts[idx].lastRunStatus = 'success'
allScripts[idx].lastRunMessage = `群发邮件成功,共${rows.length}条数据,收件人:${finalRecipients}`
saveScripts(allScripts)
}
res.json({ success: true, message: `群发邮件成功,共${rows.length}条数据,收件人:${finalRecipients}` })
}
} catch (e) {
// 更新脚本状态为失败
const allScripts = loadScripts()
......
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={560}
width={620}
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 }]}
>
<Input.TextArea
placeholder="例如:zhangsan@company.com,lisi@company.com"
autoSize={{ minRows: 2, maxRows: 4 }}
onChange={() => {
// 实时更新标签展示
}}
{/* 发送模式选择 */}
<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' }} />}
/>
</Form.Item>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddSingle}>
添加
</Button>
</Space.Compact>
<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>
))}
</Space>
</div>
)
}}
</Form.Item>
{/* 批量导入切换 */}
{!showBatchInput ? (
<Button
size="small"
type="dashed"
icon={<TeamOutlined />}
onClick={() => setShowBatchInput(true)}
style={{ marginBottom: 8 }}
>
批量导入
</Button>
) : (
<div style={{ marginBottom: 8 }}>
<Input.TextArea
placeholder="输入多个邮箱,用逗号、分号或换行分隔"
value={batchInput}
onChange={e => setBatchInput(e.target.value)}
autoSize={{ minRows: 2, maxRows: 4 }}
/>
<Space style={{ marginTop: 4 }}>
<Button size="small" type="primary" onClick={handleBatchImport}>确认导入</Button>
<Button size="small" onClick={() => { setShowBatchInput(false); setBatchInput('') }}>取消</Button>
</Space>
</div>
)}
{/* 收件人标签列表 */}
{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>
)}
</div>
<Form form={form} layout="vertical">
<Form.Item
name="emailSubject"
label="邮件主题"
......
......@@ -440,7 +440,7 @@ export default function SqlManagerModal({ open, onClose, onLoadSql, currentSql,
{
title: '操作',
key: 'action',
width: 200,
width: 260,
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>
{record.recipientEmails && (
<div style={{ marginTop: 8, fontSize: 12, color: '#666' }}>
<MailOutlined style={{ marginRight: 4 }} />
收件人: {record.recipientEmails}
<div style={{ marginTop: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
{record.recipientEmails && (
<span style={{ fontSize: 12, color: '#666', marginRight: 16 }}>
<MailOutlined style={{ marginRight: 4 }} />
收件人: {record.recipientEmails}
</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>
),
}}
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!