<template>
<div class="app-container">
<el-tabs v-model="activeTab" class="setting-tabs" type="border-card">
<!-- 收件人管理标签页 -->
<el-tab-pane label="收件人管理" name="receivers">
<el-card class="setting-card">
<template #header>
<div class="card-header">
<el-icon class="header-icon"><User /></el-icon>
<span>收件人管理</span>
<el-text type="info" class="header-desc">管理接收提醒邮件的人员信息</el-text>
</div>
</template>
<div class="card-content">
<!-- 选择用户区域 -->
<el-form inline class="add-receiver-form">
<el-form-item label="选择用户">
<el-input
v-model="userSearchText"
placeholder="点击搜索框选择用户"
readonly
@click="openUserSelectModal"
class="user-select-input"
>
<template #suffix>
<el-icon class="search-icon" @click.stop="openUserSelectModal">
<Search />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:icon="Plus"
@click="handleAddSelectedUsers"
:disabled="selectedUserIds.length === 0"
>
添加到收件人列表
</el-button>
</el-form-item>
</el-form>
<!-- 已选用户预览 -->
<div v-if="selectedUserIds.length > 0" class="selected-users-preview">
<el-tag
v-for="userId in selectedUserIds"
:key="userId"
closable
@close="removeSelectedUser(userId)"
class="selected-tag"
>
{{ getUserNameById(userId) }}
</el-tag>
</div>
<!-- 已选收件人列表 -->
<div class="receiver-list-container">
<el-table
:data="filteredReceivers"
border
stripe
:loading="receiverLoading"
class="receiver-table"
max-height="400"
row-key="recipientId"
>
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column prop="recipientName" label="姓名" width="160" />
<el-table-column prop="email" label="邮箱地址" min-width="200">
<template #default="scope">
<el-tooltip :content="scope.row.email || '未设置邮箱'" placement="top">
<span class="email-text">{{ scope.row.email || '未设置邮箱' }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="状态" width="120" align="center">
<template #default="scope">
<el-switch
v-model="scope.row.status"
active-value="1"
inactive-value="0"
@change="handleStatusChange(scope.row)"
:loading="scope.row.statusLoading"
/>
</template>
</el-table-column>
<el-table-column label="是否默认" width="120" align="center">
<template #default="scope">
<el-checkbox
v-model="scope.row.isDefault"
:true-label="1"
:false-label="0"
@change="handleDefaultChange(scope.row)"
:disabled="scope.row.defaultDisabled"
/>
</template>
</el-table-column>
<el-table-column label="排序" width="100" align="center">
<template #default="scope">
<el-input-number
v-model="scope.row.sort"
min="0"
@change="handleSortChange(scope.row)"
size="small"
:disabled="scope.row.sortDisabled"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center">
<template #default="scope">
<el-button
size="small"
type="text"
@click="handleEditReceiver(scope.row)"
:icon="Edit"
:disabled="scope.row.operateDisabled"
/>
<el-button
size="small"
type="text"
text-color="#F53F3F"
@click="handleDeleteReceiver(scope.row.recipientId)"
:icon="Delete"
:disabled="scope.row.operateDisabled"
/>
</template>
</el-table-column>
</el-table>
<div v-if="filteredReceivers.length === 0 && !receiverLoading" class="empty-state">
<el-empty description="暂无收件人,请从系统用户中选择" />
</div>
<div class="form-actions">
<el-button
type="primary"
@click="handleSaveReceivers"
:icon="Check"
:loading="saveReceiversLoading"
>
保存收件人设置
</el-button>
</div>
</div>
</div>
</el-card>
</el-tab-pane>
<!-- 发送配置标签页 -->
<el-tab-pane label="发送配置" name="config">
<el-card class="setting-card">
<template #header>
<div class="card-header">
<el-icon class="header-icon"><Clock /></el-icon>
<span>发送配置</span>
<el-text type="info" class="header-desc">配置邮件发送服务器及提醒频率</el-text>
</div>
</template>
<div class="card-content">
<el-form
:model="sendConfig"
:rules="configRules"
ref="configFormRef"
class="config-form"
label-width="160px"
>
<!-- 基础SMTP配置 -->
<el-collapse v-model="activeCollapse" class="config-collapse">
<el-collapse-item title="SMTP服务器配置" name="smtp">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="是否启用邮件发送" prop="enable">
<el-switch
v-model="sendConfig.enable"
active-value="1"
inactive-value="0"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发件人显示名称" prop="senderName">
<el-input
v-model="sendConfig.senderName"
placeholder="请输入发件人显示名称"
class="input-control"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="SMTP服务器地址" prop="host">
<el-input
v-model="sendConfig.host"
placeholder="如: smtp.qq.com"
class="input-control"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="SMTP服务器端口" prop="port">
<el-input-number
v-model="sendConfig.port"
min="1"
max="65535"
class="input-control"
placeholder="如: 587"
/>
</el-form-item>
</el-col>
</el-row>
</el-collapse-item>
<!-- 提醒频率配置 -->
<el-collapse-item title="提醒频率配置" name="frequency">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="发送频率" prop="frequency">
<el-select
v-model="sendConfig.frequency"
placeholder="请选择发送频率"
class="input-control"
@change="handleFrequencyChange"
>
<el-option label="每天" value="daily" />
<el-option label="每周" value="weekly" />
<el-option label="每月" value="monthly" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发送时间" prop="sendTime">
<el-time-picker
v-model="sendConfig.sendTime"
format="HH:mm"
value-format="HH:mm"
placeholder="选择发送时间"
class="input-control"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="提前__天" prop="advanceTime">
<el-input-number
v-model="sendConfig.advanceTime"
min="0"
placeholder="请输入提前天数"
class="input-control"
/>
</el-form-item>
</el-col>
</el-row>
</el-collapse-item>
</el-collapse>
<el-form-item class="form-actions">
<el-button
type="primary"
@click="handleSaveConfig"
:icon="Check"
:loading="saveConfigLoading"
>
保存发送配置
</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</el-tab-pane>
<!-- 邮件模板标签页 -->
<el-tab-pane label="邮件模板" name="template">
<el-card class="setting-card">
<template #header>
<div class="card-header">
<el-icon class="header-icon"><Document /></el-icon>
<span>邮件模板</span>
<el-text type="info" class="header-desc">自定义提醒邮件的模板内容</el-text>
</div>
</template>
<div class="card-content">
<!-- 模板类型选择 -->
<el-radio-group
v-model="activeTemplateType"
class="template-type-group"
@change="handleTemplateTypeChange"
:disabled="templateLoading"
>
<el-radio-button label="license">项目授权到期提醒</el-radio-button>
<el-radio-button label="maintenance">项目运维到期提醒</el-radio-button>
</el-radio-group>
<el-form
:model="currentTemplate"
:rules="templateRules"
ref="templateFormRef"
class="template-form"
label-width="140px"
:disabled="templateLoading"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="模板名称" prop="templateName">
<el-input
v-model="currentTemplate.templateName"
placeholder="请输入模板名称"
class="input-control"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="模板状态" prop="status">
<el-switch
v-model="currentTemplate.status"
active-value="1"
inactive-value="0"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="邮件标题" prop="subject">
<el-input
v-model="currentTemplate.subject"
placeholder="请输入邮件标题"
class="input-control"
/>
<el-text type="info" class="help-text">点击下方变量可快速插入</el-text>
</el-form-item>
<el-form-item label="邮件内容" prop="content">
<el-input
v-model="currentTemplate.content"
type="textarea"
rows="10"
placeholder="请输入邮件内容"
class="input-control"
/>
<div class="template-variables">
<el-text type="info">支持变量:</el-text>
<el-tag
v-for="(desc, key) in getAvailableVariables()"
:key="key"
class="variable-tag"
@click="insertVariable(key)"
>
${key} <span class="variable-desc">={{ desc }}</span>
</el-tag>
</div>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handlePreviewTemplate"
:icon="View"
class="preview-btn"
>
预览
</el-button>
<el-button
type="success"
@click="handleSaveTemplate"
:icon="Check"
:loading="saveTemplateLoading"
>
保存模板
</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</el-tab-pane>
<!-- 提醒规则配置标签页 -->
<el-tab-pane label="提醒规则" name="reminder">
<el-card class="setting-card">
<template #header>
<div class="card-header">
<el-icon class="header-icon"><Star /></el-icon>
<span>提醒规则配置</span>
<el-text type="info" class="header-desc">设置项目到期提醒的触发条件与规则</el-text>
</div>
</template>
<div class="card-content">
<el-form
:model="reminderRule"
:rules="reminderRules"
ref="reminderFormRef"
class="reminder-form"
label-width="180px"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="是否启用提醒规则" prop="enable">
<el-switch
v-model="reminderRule.enable"
active-value="1"
inactive-value="0"
@change="handleRuleEnableChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="提醒触发类型" prop="triggerType">
<el-select
v-model="reminderRule.triggerType"
placeholder="请选择触发类型"
class="input-control"
:disabled="reminderRule.enable === '0'"
>
<el-option label="按到期时间" value="expireDate" />
<el-option label="按剩余天数" value="remainingDays" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" v-if="reminderRule.enable === '1'">
<el-col :span="12">
<el-form-item
label="提前提醒天数"
prop="advanceDays"
:rules="[
{ required: reminderRule.triggerType === 'remainingDays', message: '请输入提前提醒天数', trigger: 'blur' },
{ type: 'number', min: 0, message: '提前天数不能为负数', trigger: 'blur' }
]"
>
<el-input-number
v-model="reminderRule.advanceDays"
min="0"
placeholder="如:7(提前7天提醒)"
class="input-control"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="重复提醒间隔" prop="repeatInterval">
<el-select
v-model="reminderRule.repeatInterval"
placeholder="请选择重复间隔"
class="input-control"
>
<el-option label="不重复" value="none" />
<el-option label="每天" value="daily" />
<el-option label="每3天" value="3days" />
<el-option label="每周" value="weekly" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" v-if="reminderRule.enable === '1'">
<el-col :span="24">
<el-form-item label="适用项目类型" prop="projectTypes">
<el-select
v-model="reminderRule.projectTypes"
placeholder="请选择适用项目类型(可多选)"
class="input-control"
multiple
>
<el-option label="授权项目" value="license" />
<el-option label="运维项目" value="maintenance" />
<el-option label="定制开发项目" value="custom" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item class="form-actions">
<el-button
type="primary"
@click="handleSaveReminderRule"
:icon="Check"
:loading="saveReminderLoading"
>
保存提醒规则
</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</el-tab-pane>
</el-tabs>
<!-- 用户选择弹窗 -->
<el-dialog
v-model="userSelectModalVisible"
title="选择用户"
width="70%"
max-height="80vh"
:before-close="handleModalClose"
:close-on-click-modal="false"
>
<div class="user-modal-content">
<!-- 搜索框 -->
<el-input
v-model="modalSearchQuery"
placeholder="输入用户名或邮箱搜索"
class="modal-search-input"
@input="handleModalSearch"
>
<template #suffix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<!-- 用户列表 -->
<el-table
:data="filteredModalUsers"
border
stripe
:loading="modalUserLoading"
class="user-select-table"
max-height="500px"
@selection-change="handleUserSelectionChange"
row-key="userId"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="userId" label="用户ID" width="100" />
<el-table-column prop="userName" label="姓名" width="120" />
<el-table-column prop="email" label="邮箱地址" min-width="200">
<template #default="scope">
<el-tooltip :content="scope.row.email || '未设置邮箱'" placement="top">
<span class="email-text">{{ scope.row.email || '未设置邮箱' }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="deptName" label="部门" width="150" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'">
{{ scope.row.status === '0' ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="totalUsers"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handlePageSizeChange"
@current-change="handlePageChange"
class="user-pagination"
/>
</div>
<template #footer>
<el-button @click="resetUserSelection">清空选择</el-button>
<el-button type="primary" @click="confirmUserSelection">确认选择</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue'
import {
User, Clock, Document, Plus, Edit, Delete,
Check, View, Search, Star
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox, ElTooltip } from 'element-plus'
import emailApi from '@/api/project/email'
import useUserStore from '@/store/modules/user'
// 基础状态管理
const activeTab = ref('receivers')
const userStore = useUserStore()
const currentUserId = userStore.id || 'admin'
// 加载状态管理
const receiverLoading = ref(false)
const modalUserLoading = ref(false)
const saveReceiversLoading = ref(false)
const saveConfigLoading = ref(false)
const saveTemplateLoading = ref(false)
const templateLoading = ref(false)
const saveReminderLoading = ref(false)
// 收件人管理核心逻辑
const selectedUserIds = ref([])
const userSearchText = ref('')
const receivers = ref([])
// 过滤已删除的收件人(修复:使用数字0判断)
const filteredReceivers = computed(() => {
return receivers.value.filter(item => item.status !== 0)
})
// 用户选择弹窗核心逻辑
const userSelectModalVisible = ref(false)
const modalSearchQuery = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const totalUsers = ref(0)
const modalUsers = ref([])
const selectedModalUsers = ref([])
const filteredModalUsers = computed(() => {
if (!modalSearchQuery.value) return modalUsers.value
const query = modalSearchQuery.value.toLowerCase()
return modalUsers.value.filter(user =>
user.userName.toLowerCase().includes(query) ||
(user.email && user.email.toLowerCase().includes(query)) ||
(user.deptName && user.deptName.toLowerCase().includes(query))
)
})
// 发送配置核心逻辑
const sendConfig = reactive({
enable: '1',
senderName: '',
host: '',
port: 587,
frequency: 'daily',
sendTime: '09:00',
advanceTime: 10,
createBy: currentUserId,
updateBy: currentUserId
})
const configRules = reactive({
senderName: [{ required: true, message: '请输入发件人显示名称', trigger: 'blur' }],
host: [{ required: true, message: '请输入SMTP服务器地址', trigger: 'blur' }],
port: [
{ required: true, message: '请输入SMTP服务器端口', trigger: 'blur' },
{ type: 'number', message: '端口必须为数字', trigger: 'blur' }
],
sendTime: [{ required: true, message: '请选择发送时间', trigger: 'change' }],
advanceTime: [
{ required: true, message: '请输入提前天数', trigger: 'blur' },
{ type: 'number', message: '提前天数必须为数字', trigger: 'blur' }
]
})
const configFormRef = ref(null)
const activeCollapse = ref(['smtp', 'frequency'])
// 邮件模板核心逻辑
const activeTemplateType = ref('license')
const emailTemplates = reactive({
license: {
templateId: '',
templateName: '项目授权到期提醒',
type: 'license',
status: '1',
isDefault: '1',
subject: '【重要提醒】${projectName} 授权即将到期',
content: '尊敬的${userName}:\n\n您负责的项目 "${projectName}" 授权将于 ${expireDate} 到期,请及时处理。\n\n授权信息:\n- 授权类型:${licenseType}\n- 授权范围:${licenseScope}\n- 授权编号:${licenseCode}\n\n到期后,系统将无法正常使用,请提前做好续费或升级准备。\n\n如有疑问,请联系管理员。',
createBy: currentUserId,
updateBy: currentUserId
},
maintenance: {
templateId: '',
templateName: '项目运维到期提醒',
type: 'maintenance',
status: '1',
isDefault: '1',
subject: '【重要提醒】${projectName} 运维服务即将到期',
content: '尊敬的${userName}:\n\n您负责的项目 "${projectName}" 运维服务将于 ${expireDate} 到期,请及时处理。\n\n运维服务信息:\n- 服务内容:${maintenanceContent}\n- 服务提供商:${serviceProvider}\n- 服务级别:${serviceLevel}\n\n到期后,将不再提供技术支持和维护服务,请提前做好准备。\n\n如有疑问,请联系管理员。',
createBy: currentUserId,
updateBy: currentUserId
}
})
const currentTemplate = computed({
get() {
return emailTemplates[activeTemplateType.value]
},
set(value) {
emailTemplates[activeTemplateType.value] = { ...value, updateBy: currentUserId }
}
})
const templateRules = reactive({
templateName: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
subject: [{ required: true, message: '请输入邮件标题', trigger: 'blur' }],
content: [{ required: true, message: '请输入邮件内容', trigger: 'blur' }]
})
const templateFormRef = ref(null)
// 提醒规则核心逻辑
const reminderRule = reactive({
enable: '1',
triggerType: 'remainingDays',
advanceDays: 7,
repeatInterval: 'daily',
projectTypes: ['license', 'maintenance'],
createBy: currentUserId,
updateBy: currentUserId
})
const reminderRules = reactive({
enable: [{ required: true, message: '请选择是否启用规则', trigger: 'change' }],
triggerType: [{ required: true, message: '请选择提醒触发类型', trigger: 'change' }],
repeatInterval: [{ required: true, message: '请选择重复提醒间隔', trigger: 'change' }],
projectTypes: [{ required: true, message: '请选择适用项目类型', trigger: 'change' }]
})
const reminderFormRef = ref(null)
// 工具函数
const debounce = (fn, delay) => {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
const getUserNameById = (userId) => {
let user = modalUsers.value.find(u => u.userId === userId)
if (!user) user = receivers.value.find(r => r.userId === userId)
return user ? user.userName : '未知用户'
}
const getAvailableVariables = () => {
const baseVars = {
projectName: '项目名称',
expireDate: '到期时间',
userName: '接收人姓名',
projectManager: '项目经理',
projectStartDate: '项目开始日期'
}
if (activeTemplateType.value === 'license') {
return {
...baseVars,
licenseType: '授权类型',
licenseScope: '授权范围',
licenseCode: '授权编号',
licenseIssuer: '授权颁发方'
}
}
return {
...baseVars,
maintenanceContent: '运维内容',
serviceProvider: '服务提供商',
serviceLevel: '服务级别',
maintenanceContact: '运维联系人'
}
}
// 事件处理函数
const openUserSelectModal = async () => {
modalSearchQuery.value = ''
currentPage.value = 1
selectedModalUsers.value = []
userSelectModalVisible.value = true
await fetchModalUsers()
const existingIds = receivers.value.map(r => r.userId)
modalUsers.value.forEach(user => {
if (existingIds.includes(user.userId)) {
selectedModalUsers.value.push(user)
}
})
}
const fetchModalUsers = async () => {
modalUserLoading.value = true
try {
const res = await emailApi.getUserList({
keyword: modalSearchQuery.value,
pageNum: currentPage.value,
pageSize: pageSize.value
})
modalUsers.value = res.rows || []
totalUsers.value = res.total || 0
} catch (error) {
ElMessage.error('获取用户列表失败:' + (error.msg || error.message))
console.error('用户列表请求错误:', error)
} finally {
modalUserLoading.value = false
}
}
const removeSelectedUser = (userId) => {
selectedUserIds.value = selectedUserIds.value.filter(id => id !== userId)
}
// 修复:添加选中用户到收件人列表
const handleAddSelectedUsers = async () => {
if (selectedUserIds.length === 0) {
ElMessage.warning('请先在弹窗中选择用户')
return
}
const newReceivers = selectedModalUsers.value
.filter(user => !receivers.value.some(r => r.userId === user.userId))
.map(user => ({
recipientId: '',
recipientName: user.userName,
email: user.email || '',
userId: user.userId,
isDefault: 0, // 修复:使用数字0
status: 1, // 修复:使用数字1表示启用
sort: receivers.value.length + 1,
createBy: currentUserId,
updateBy: currentUserId
}))
if (newReceivers.length === 0) {
ElMessage.info('所选用户已在收件人列表中,无需重复添加')
selectedUserIds.value = []
return
}
saveReceiversLoading.value = true
try {
const res = await emailApi.saveReceivers(newReceivers)
if (res.code === 200) {
// 修复:从Page对象的records属性获取数据
receivers.value = [...receivers.value, ...(res.records || newReceivers)]
ElMessage.success(`成功添加 ${newReceivers.length} 位用户到收件人列表`)
} else {
throw new Error(res.msg || '添加失败')
}
} catch (error) {
ElMessage.error('添加用户失败:' + error.message)
console.error('添加收件人错误:', error)
} finally {
saveReceiversLoading.value = false
selectedUserIds.value = []
}
}
// 修复:切换收件人状态
const handleStatusChange = async (row) => {
row.statusLoading = true
try {
// 修复:使用数字类型状态值
const newStatus = row.status === 1 ? 0 : 1
const res = await emailApi.updateReceiver({
...row,
status: newStatus,
updateBy: currentUserId
})
if (res.code !== 200) throw new Error(res.msg || '状态更新失败')
row.status = newStatus
ElMessage.success('状态更新成功')
} catch (error) {
row.status = row.status === 1 ? 0 : 1
ElMessage.error('状态更新失败:' + error.message)
console.error('更新收件人状态错误:', error)
} finally {
row.statusLoading = false
}
}
// 修复:切换收件人默认状态
const handleDefaultChange = async (row) => {
// 修复:使用数字类型
const newIsDefault = row.isDefault === 1 ? 0 : 1
if (newIsDefault === 1) {
receivers.value.forEach(item => {
if (item.recipientId !== row.recipientId) {
item.isDefault = 0
}
})
}
row.defaultDisabled = true
try {
const res = await emailApi.updateReceiver({
...row,
isDefault: newIsDefault,
updateBy: currentUserId
})
if (res.code !== 200) throw new Error(res.msg || '默认状态更新失败')
row.isDefault = newIsDefault
ElMessage.success('默认状态更新成功')
} catch (error) {
row.isDefault = row.isDefault === 1 ? 0 : 1
ElMessage.error('默认状态更新失败:' + error.message)
console.error('更新默认状态错误:', error)
} finally {
row.defaultDisabled = false
}
}
const handleSortChange = async (row) => {
row.sortDisabled = true
try {
const res = await emailApi.updateReceiver({
...row,
updateBy: currentUserId
})
if (res.code !== 200) throw new Error(res.msg || '排序更新失败')
ElMessage.success('排序更新成功')
} catch (error) {
ElMessage.error('排序更新失败:' + error.message)
console.error('更新排序错误:', error)
} finally {
row.sortDisabled = false
}
}
const handleEditReceiver = async (row) => {
try {
const { value: newEmail } = await ElMessageBox.prompt(
`编辑 ${row.recipientName} 的邮箱`,
'编辑收件人邮箱',
{
inputValue: row.email || '',
inputPlaceholder: '请输入有效的邮箱地址',
inputPattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
inputErrorMessage: '请输入有效的邮箱地址'
}
)
const res = await emailApi.updateReceiver({
...row,
email: newEmail,
updateBy: currentUserId
})
if (res.code === 200) {
row.email = newEmail
ElMessage.success('邮箱编辑成功')
} else {
throw new Error(res.msg || '编辑失败')
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('编辑失败:' + (error.message || '操作取消'))
}
}
}
const handleDeleteReceiver = async (recipientId) => {
try {
await ElMessageBox.confirm(
'确定要删除该收件人吗?删除后将无法恢复',
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
const res = await emailApi.deleteReceiver(recipientId)
if (res.code === 200) {
receivers.value = receivers.value.filter(r => r.recipientId !== recipientId)
ElMessage.success('收件人删除成功')
} else {
throw new Error(res.msg || '删除失败')
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败:' + (error.message || '操作取消'))
}
}
}
const handleSaveReceivers = async () => {
if (receivers.value.length === 0) {
ElMessage.warning('暂无收件人可保存')
return
}
saveReceiversLoading.value = true
try {
const receiversWithUpdateBy = receivers.value.map(r => ({
...r,
updateBy: currentUserId
}))
const res = await emailApi.saveReceivers(receiversWithUpdateBy)
if (res.code === 200) {
ElMessage.success('收件人设置保存成功')
} else {
throw new Error(res.msg || '保存失败')
}
} catch (error) {
ElMessage.error('保存失败:' + error.message)
console.error('保存收件人错误:', error)
} finally {
saveReceiversLoading.value = false
}
}
// 发送配置事件
const handleSaveConfig = async () => {
const form = configFormRef.value
if (!form) return
try {
await form.validate()
sendConfig.updateBy = currentUserId
saveConfigLoading.value = true
const res = await emailApi.saveSendConfig(sendConfig)
if (res.code === 200) {
ElMessage.success('发送配置保存成功')
} else {
throw new Error(res.msg || '保存失败')
}
} catch (error) {
if (error.name !== 'ValidateError') {
ElMessage.error('保存失败:' + (error.message || '表单验证失败'))
}
console.error('保存发送配置错误:', error)
} finally {
saveConfigLoading.value = false
}
}
const handleFrequencyChange = () => {
ElMessage.info(`已选择发送频率:${
sendConfig.frequency === 'daily' ? '每天' :
sendConfig.frequency === 'weekly' ? '每周' : '每月'
}`)
}
// 邮件模板事件
const handleTemplateTypeChange = async () => {
const currentForm = templateFormRef.value
if (currentForm) {
try {
await currentForm.validate()
const hasChange = JSON.stringify(currentTemplate.value) !== JSON.stringify(emailTemplates[activeTemplateType.value])
if (hasChange) {
await ElMessageBox.confirm(
'当前模板有未保存的修改,切换类型将丢失,是否继续?',
'切换模板确认',
{ type: 'warning' }
)
}
} catch (error) {
if (error === 'cancel') {
activeTemplateType.value = activeTemplateType.value === 'license' ? 'maintenance' : 'license'
}
}
}
templateLoading.value = true
try {
await fetchEmailTemplates()
} catch (error) {
ElMessage.error('切换模板失败:' + error.message)
} finally {
templateLoading.value = false
}
}
const insertVariable = (key) => {
currentTemplate.value.content += `\${${key}}`
}
const handlePreviewTemplate = () => {
const basePreviewData = {
projectName: '企业资源管理系统V3.0',
expireDate: '2024-12-31',
userName: '张经理',
projectManager: '李工程师',
projectStartDate: '2023-01-15'
}
let previewData, previewTitle
if (activeTemplateType.value === 'license') {
previewTitle = '项目授权到期提醒预览'
previewData = {
...basePreviewData,
licenseType: '企业版年度授权',
licenseScope: '全模块使用权限',
licenseCode: 'LIC-20230115-8762',
licenseIssuer: '北京科技有限公司'
}
} else {
previewTitle = '项目运维到期提醒预览'
previewData = {
...basePreviewData,
maintenanceContent: '系统日常维护、漏洞修复、性能优化、数据备份',
serviceProvider: '技术支持部',
serviceLevel: '7×24小时响应',
maintenanceContact: '王技术员 (13800138000)'
}
}
let previewSubject = currentTemplate.value.subject
let previewContent = currentTemplate.value.content
Object.keys(previewData).forEach(key => {
const reg = new RegExp(`\\\${${key}}`, 'g')
previewSubject = previewSubject.replace(reg, previewData[key])
previewContent = previewContent.replace(reg, previewData[key])
})
previewContent = previewContent.replace(/\n/g, '<br>')
ElMessageBox.alert(
`<h3 style="margin-bottom: 15px; color: #333;">${previewSubject}</h3>
<div style="text-align: left; line-height: 1.8; color: #666;">${previewContent}</div>`,
previewTitle,
{
dangerouslyUseHTMLString: true,
width: '60%',
confirmButtonText: '关闭预览'
}
)
}
const handleSaveTemplate = async () => {
const form = templateFormRef.value
if (!form) return
try {
await form.validate()
const templateData = {
...currentTemplate.value,
type: activeTemplateType.value,
updateBy: currentUserId
}
saveTemplateLoading.value = true
const res = await emailApi.saveEmailTemplate(templateData)
if (res.code === 200) {
if (res.data?.templateId) {
emailTemplates[activeTemplateType.value].templateId = res.data.templateId
}
ElMessage.success('邮件模板保存成功')
} else {
throw new Error(res.msg || '保存失败')
}
} catch (error) {
if (error.name !== 'ValidateError') {
ElMessage.error('保存失败:' + (error.message || '表单验证失败'))
}
console.error('保存模板错误:', error)
} finally {
saveTemplateLoading.value = false
}
}
// 提醒规则事件
const handleRuleEnableChange = () => {
if (reminderRule.enable === '0') {
ElMessage.info('提醒规则已禁用,将不再触发到期提醒')
}
}
const handleSaveReminderRule = async () => {
const form = reminderFormRef.value
if (!form) return
try {
await form.validate()
reminderRule.updateBy = currentUserId
saveReminderLoading.value = true
const res = await emailApi.saveReminderRule(reminderRule)
if (res.code === 200) {
ElMessage.success('提醒规则保存成功')
} else {
throw new Error(res.msg || '保存失败')
}
} catch (error) {
if (error.name !== 'ValidateError') {
ElMessage.error('保存失败:' + (error.message || '表单验证失败'))
}
console.error('保存提醒规则错误:', error)
} finally {
saveReminderLoading.value = false
}
}
// 弹窗分页与搜索事件
const handleModalSearch = debounce(async () => {
currentPage.value = 1
await fetchModalUsers()
}, 300)
const handlePageSizeChange = async (size) => {
pageSize.value = size
currentPage.value = 1
await fetchModalUsers()
}
const handlePageChange = async (page) => {
currentPage.value = page
await fetchModalUsers()
}
const handleUserSelectionChange = (selection) => {
selectedModalUsers.value = selection
selectedUserIds.value = selection.map(user => user.userId)
}
const confirmUserSelection = () => {
userSelectModalVisible.value = false
}
const resetUserSelection = () => {
selectedModalUsers.value = []
selectedUserIds.value = []
}
const handleModalClose = () => {
selectedModalUsers.value = []
selectedUserIds.value = []
userSelectModalVisible.value = false
}
// 生命周期与数据加载
onMounted(() => {
Promise.all([
fetchReceivers(),
fetchSendConfig(),
fetchEmailTemplates(),
fetchReminderRule()
]).catch(error => {
console.error('初始数据加载失败:', error)
})
})
watch(selectedUserIds, (newVal) => {
userSearchText.value = newVal.length > 0
? `已选择 ${newVal.length} 位用户`
: ''
})
// 修复:从后端获取收件人列表(关键修复)
const fetchReceivers = async () => {
receiverLoading.value = true
try {
const res = await emailApi.listReceivers()
// 关键修复:从Page对象的records属性获取数据列表
receivers.value = res.records || []
console.log('获取到的收件人数据:', receivers.value)
} catch (error) {
ElMessage.error('获取收件人列表失败:' + (error.msg || error.message))
console.error('获取收件人错误:', error)
} finally {
receiverLoading.value = false
}
}
const fetchSendConfig = async () => {
try {
const res = await emailApi.getSendConfig()
if (res.code === 200 && res.data) {
Object.assign(sendConfig, res.data)
}
} catch (error) {
ElMessage.error('获取发送配置失败:' + (error.msg || error.message))
console.error('获取发送配置错误:', error)
}
}
const fetchEmailTemplates = async () => {
templateLoading.value = true
try {
const res = await emailApi.getEmailTemplate()
const templates = res.rows || []
if (Array.isArray(templates)) {
templates.forEach(template => {
if (template.type === 'license' || template.type === 'maintenance') {
emailTemplates[template.type] = {
...emailTemplates[template.type],
...template,
updateBy: currentUserId
}
}
})
}
} catch (error) {
ElMessage.error('获取邮件模板失败:' + (error.msg || error.message))
console.error('获取模板错误:', error)
} finally {
templateLoading.value = false
}
}
const fetchReminderRule = async () => {
try {
const res = await emailApi.getReminderRule()
if (res.code === 200 && res.data) {
Object.assign(reminderRule, res.data)
}
} catch (error) {
ElMessage.error('获取提醒规则失败:' + (error.msg || error.message))
console.error('获取提醒规则错误:', error)
}
}
</script>
<style scoped>
.setting-tabs { width: 100%; margin-bottom: 20px; }
.setting-card { margin-bottom: 24px; border-radius: 8px; }
.card-header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 0;
}
.header-icon { color: #409eff; font-size: 18px; }
.header-desc { margin-left: 8px; font-size: 12px; color: #666; }
.card-content { padding: 20px; }
.add-receiver-form {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
}
.user-select-input { width: 420px; cursor: pointer; }
.search-icon { cursor: pointer; color: #999; }
.selected-users-preview {
margin: 16px 0;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.selected-tag { background-color: #f5f7fa; color: #666; }
.receiver-list-container { margin-top: 16px; }
.receiver-table { width: 100%; border-radius: 4px; }
.empty-state { padding: 60px 0; text-align: center; }
.form-actions {
margin-top: 24px;
text-align: right;
}
.config-collapse { margin-top: 16px; }
.config-form { margin-top: 8px; }
.template-type-group {
margin-bottom: 24px;
padding: 12px;
background-color: #f5f7fa;
border-radius: 4px;
}
.template-variables {
margin-top: 12px;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.variable-tag {
cursor: pointer;
white-space: nowrap;
background-color: #ecf5ff;
color: #409eff;
}
.variable-desc {
font-size: 11px;
color: #888;
margin-left: 5px;
}
.help-text { margin-left: 8px; font-size: 12px; color: #666; }
.preview-btn { margin-right: 12px; }
.reminder-form { margin-top: 16px; }
.user-modal-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.modal-search-input { width: 100%; }
.user-select-table { width: 100%; }
.user-pagination { margin-top: 16px; text-align: right; }
.email-text {
display: inline-block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 1200px) {
.user-select-input { width: 320px; }
}
@media (max-width: 768px) {
.user-select-input { width: 100%; }
.card-content { padding: 12px; }
.template-type-group { flex-direction: column; gap: 8px; }
.el-row { flex-direction: column; gap: 16px !important; }
.el-col { width: 100% !important; }
}
</style>
使用若依框架编写后端代码