解决人员选择器的问题
This commit is contained in:
@@ -143,7 +143,7 @@ CREATE TABLE `tb_crontab_email_default` (
|
||||
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除(0:否 1:是)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_default_id` (`default_id`),
|
||||
UNIQUE KEY `uk_meta_id` (`meta_id`),
|
||||
UNIQUE KEY `uk_meta_id` (`meta_id`, `user_id`),
|
||||
KEY `idx_deleted` (`deleted`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='定时任务邮件通知默认接收人员表';
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ INSERT INTO `tb_sys_menu` VALUES
|
||||
('7002', 'menu_admin_login_logs', '登录日志', 'menu_admin_logs_manage', '/admin/manage/logs/login', 'admin/manage/logs/LoginLogsView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
||||
('7003', 'menu_admin_system_config', '系统配置', 'menu_sys_manage', '/admin/manage/system/config', 'admin/manage/system/SystemConfigView', NULL, 7, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
||||
('8000', 'menu_admin_crontab_manage', '定时任务管理', NULL, '', '', 'admin/crontab.svg', 8, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
||||
('8001', 'menu_admin_meta_email_default', '默认接收人配置', 'menu_admin_crontab_manage', '/admin/manage/crontab/meta-email-default', 'admin/manage/crontab/MetaEmailDefaultView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-11-18 18:00:00', '2025-11-18 18:00:00', NULL, 0),
|
||||
('8002', 'menu_admin_crontab_log', '执行日志', 'menu_admin_crontab_manage', '/admin/manage/crontab/log', 'admin/manage/crontab/LogManagementView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
||||
('8003', 'menu_admin_news_crawler', '新闻爬虫配置', 'menu_admin_crontab_manage', '/admin/manage/crontab/news-crawler', 'admin/manage/crontab/NewsCrawlerView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
||||
-- 消息通知模块菜单 (9000-9999)
|
||||
@@ -227,6 +228,7 @@ INSERT INTO `tb_sys_menu_permission` (id, permission_id, menu_id, creator, creat
|
||||
|
||||
-- 定时任务管理菜单权限关联
|
||||
('232', 'perm_crontab_manage', 'menu_admin_crontab_manage', '1', now()),
|
||||
('236', 'perm_crontab_manage', 'menu_admin_meta_email_default', '1', now()),
|
||||
('233', 'perm_crontab_manage', 'menu_admin_crontab_task', '1', now()),
|
||||
('234', 'perm_crontab_manage', 'menu_admin_crontab_log', '1', now()),
|
||||
('235', 'perm_crontab_manage', 'menu_admin_news_crawler', '1', now()),
|
||||
|
||||
@@ -65,12 +65,11 @@
|
||||
|
||||
<!-- 根据metaId查询 -->
|
||||
<select id="selectDefaultByMetaId" resultMap="BaseResultMap">
|
||||
SELECT tced.id, tced.default_id, tced.meta_id, tced.user_id, tu.email as user_email, tu.username
|
||||
SELECT tced.id, tced.default_id, tced.meta_id, tced.user_id, tu.email as user_email, tu.username, tced.create_time
|
||||
FROM tb_crontab_email_default tced
|
||||
LEFT JOIN tb_sys_user tu ON tced.user_id = tu.id
|
||||
WHERE tced.meta_id = #{metaId}
|
||||
AND tced.deleted = 0
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -878,7 +878,6 @@ function moveBackAll() {
|
||||
// 重新展开所有节点
|
||||
expandAllNodes(treeData.value);
|
||||
}
|
||||
|
||||
selectedTarget.value = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,495 @@
|
||||
<template>
|
||||
<AdminLayout title="默认接收人配置" subtitle="配置任务元数据的默认邮件接收人">
|
||||
<div class="meta-email-default">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="search-bar">
|
||||
<div class="search-item">
|
||||
<span class="search-label">任务分类</span>
|
||||
<el-select
|
||||
v-model="searchForm.category"
|
||||
placeholder="请选择任务分类"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@change="handleCategoryChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="category in categoryList"
|
||||
:key="category"
|
||||
:label="category"
|
||||
:value="category"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<span class="search-label">任务元数据</span>
|
||||
<el-select
|
||||
v-model="searchForm.metaId"
|
||||
placeholder="请选择任务元数据"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 300px"
|
||||
@change="loadDefaultRecipients"
|
||||
:disabled="!searchForm.category"
|
||||
>
|
||||
<el-option
|
||||
v-for="meta in filteredMetaList"
|
||||
:key="meta.metaId"
|
||||
:label="meta.name"
|
||||
:value="meta.metaId"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="search-actions">
|
||||
<el-button type="primary" @click="handleAdd" :disabled="!searchForm.metaId">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加接收人
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<el-alert
|
||||
v-if="!searchForm.metaId"
|
||||
title="请先选择一个任务元数据"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px"
|
||||
/>
|
||||
|
||||
<!-- 接收人列表 -->
|
||||
<el-table
|
||||
v-if="searchForm.metaId"
|
||||
:data="recipientList"
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" />
|
||||
<el-table-column prop="username" label="用户名" width="150" />
|
||||
<el-table-column prop="userEmail" label="邮箱地址" min-width="200" />
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 添加接收人 - GenericSelector -->
|
||||
<GenericSelector
|
||||
v-model:visible="dialogVisible"
|
||||
title="选择默认接收人"
|
||||
left-title="可选人员"
|
||||
right-title="已选人员"
|
||||
:fetch-available-api="fetchAllUsers"
|
||||
:initialTargetItems="selectedRecipients"
|
||||
:filter-selected="filterUsers"
|
||||
:item-config="{ id: 'userId', label: 'username', sublabel: 'userEmail' }"
|
||||
:use-tree="true"
|
||||
:tree-transform="transformUserToTree"
|
||||
:tree-props="{ children: 'children', label: 'username', id: 'userId' }"
|
||||
:only-leaf-selectable="true"
|
||||
unit-name="人"
|
||||
search-placeholder="搜索用户姓名或邮箱..."
|
||||
@confirm="handleUserConfirm"
|
||||
@cancel="resetUserSelector"
|
||||
/>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { AdminLayout } from '@/views/admin';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { crontabApi } from '@/apis/crontab';
|
||||
import { userApi } from '@/apis/system';
|
||||
import {GenericSelector} from '@/components';
|
||||
import type { TaskMeta, EmailDefault, UserVO, ResultDomain } from '@/types';
|
||||
|
||||
defineOptions({
|
||||
name: 'MetaEmailDefaultView'
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
category: '',
|
||||
metaId: ''
|
||||
});
|
||||
|
||||
// Meta列表
|
||||
const metaList = ref<TaskMeta[]>([]);
|
||||
|
||||
// 分类列表 - 从metaList中提取唯一的category
|
||||
const categoryList = computed(() => {
|
||||
const categories = metaList.value
|
||||
.map(meta => meta.category)
|
||||
.filter((category): category is string => !!category);
|
||||
return Array.from(new Set(categories));
|
||||
});
|
||||
|
||||
// 根据选择的分类过滤Meta列表
|
||||
const filteredMetaList = computed(() => {
|
||||
if (!searchForm.category) {
|
||||
return [];
|
||||
}
|
||||
return metaList.value.filter(meta => meta.category === searchForm.category);
|
||||
});
|
||||
|
||||
// 接收人列表
|
||||
const recipientList = ref<EmailDefault[]>([]);
|
||||
|
||||
// 缓存完整用户列表(包含部门信息),供 selectedRecipients 反查使用
|
||||
const allUsers = ref<any[]>([]);
|
||||
|
||||
// 转换为 GenericSelector 需要的格式(含部门信息)
|
||||
const selectedRecipients = computed(() => {
|
||||
// 用 userId 建索引,方便按默认接收人反查完整用户信息
|
||||
const userMap = new Map<string, any>(
|
||||
allUsers.value.map(user => [user.userId, user])
|
||||
);
|
||||
|
||||
const result = recipientList.value.map(item => {
|
||||
const userId = item.userId || '';
|
||||
const user = userMap.get(userId);
|
||||
|
||||
if (user) {
|
||||
// 有完整用户信息时,直接复用(带 deptID / deptName / parentID)
|
||||
return {
|
||||
...user,
|
||||
isDept: false
|
||||
};
|
||||
}
|
||||
|
||||
// 找不到对应用户时,至少保证选择器可用
|
||||
return {
|
||||
userId,
|
||||
username: item.username || '',
|
||||
userEmail: item.userEmail || '',
|
||||
isDept: false
|
||||
};
|
||||
});
|
||||
|
||||
console.log('🔵 selectedRecipients computed:', result);
|
||||
return result;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadMetaList();
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载Meta列表
|
||||
*/
|
||||
async function loadMetaList() {
|
||||
try {
|
||||
const result = await crontabApi.getAllTaskMeta();
|
||||
if (result.success && result.dataList) {
|
||||
metaList.value = result.dataList;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载Meta列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类变化处理
|
||||
*/
|
||||
function handleCategoryChange() {
|
||||
// 清空 metaId 选择
|
||||
searchForm.metaId = '';
|
||||
recipientList.value = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载默认接收人列表
|
||||
*/
|
||||
async function loadDefaultRecipients() {
|
||||
if (!searchForm.metaId) {
|
||||
recipientList.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
const result = await crontabApi.getEmailDefaultByMetaId(searchForm.metaId);
|
||||
if (result.success && result.dataList) {
|
||||
console.log('🟢 加载到的默认接收人:', result.dataList);
|
||||
recipientList.value = result.dataList;
|
||||
} else {
|
||||
recipientList.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载默认接收人失败:', error);
|
||||
ElMessage.error('加载默认接收人失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 获取所有人员列表的接口方法
|
||||
*/
|
||||
async function fetchAllUsers(): Promise<ResultDomain<any>> {
|
||||
try {
|
||||
const result = await userApi.getUserList({});
|
||||
if (result.success && result.dataList) {
|
||||
// 转换为 GenericSelector 需要的格式
|
||||
const users = result.dataList.map((user: UserVO) => ({
|
||||
userId: user.id || '',
|
||||
username: user.username || user.email || 'Unknown',
|
||||
userEmail: user.email || '',
|
||||
deptID: user.deptID,
|
||||
deptName: user.deptName,
|
||||
parentID: user.parentID
|
||||
}));
|
||||
|
||||
// 缓存完整用户列表,给 selectedRecipients 使用
|
||||
allUsers.value = users;
|
||||
|
||||
return {
|
||||
...result,
|
||||
dataList: users
|
||||
};
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户列表失败');
|
||||
return {
|
||||
code: 500,
|
||||
success: false,
|
||||
login: true,
|
||||
auth: true,
|
||||
message: '获取用户列表失败',
|
||||
dataList: []
|
||||
} as ResultDomain<any>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. 过滤方法:从可选项中移除已选项
|
||||
* selected 参数已经包含了右侧所有已选的(包括 initial-target-items)
|
||||
*/
|
||||
function filterUsers(available: any[], selected: any[]): any[] {
|
||||
const selectedIds = new Set(selected.map((item: any) => item.userId));
|
||||
return available.filter(item => !selectedIds.has(item.userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. 构建多级部门树的方法
|
||||
*/
|
||||
function transformUserToTree(flatData: any[]): any[] {
|
||||
if (!flatData || flatData.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const deptMap = new Map();
|
||||
const tree: any[] = [];
|
||||
|
||||
flatData.forEach(item => {
|
||||
if (!item.deptID) return;
|
||||
|
||||
if (!deptMap.has(item.deptID)) {
|
||||
deptMap.set(item.deptID, {
|
||||
userId: `dept_${item.deptID}`,
|
||||
username: item.deptName || '未分配部门',
|
||||
userEmail: '',
|
||||
deptID: item.deptID,
|
||||
deptName: item.deptName,
|
||||
parentID: item.parentID,
|
||||
children: [],
|
||||
isDept: true
|
||||
});
|
||||
}
|
||||
|
||||
const deptNode = deptMap.get(item.deptID);
|
||||
if (deptNode) {
|
||||
deptNode.children.push({
|
||||
...item,
|
||||
isDept: false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const allDepts = Array.from(deptMap.values());
|
||||
const deptTreeMap = new Map();
|
||||
|
||||
allDepts.forEach(dept => {
|
||||
deptTreeMap.set(dept.deptID, { ...dept });
|
||||
});
|
||||
|
||||
allDepts.forEach(dept => {
|
||||
const node = deptTreeMap.get(dept.deptID);
|
||||
if (!node) return;
|
||||
|
||||
if (!dept.parentID || dept.parentID === '0' || dept.parentID === '') {
|
||||
tree.push(node);
|
||||
} else {
|
||||
const parent = deptTreeMap.get(dept.parentID);
|
||||
if (parent) {
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
}
|
||||
const users = node.children || [];
|
||||
node.children = [];
|
||||
parent.children.push(node);
|
||||
node.children = users;
|
||||
} else {
|
||||
tree.push(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 4. 显示用户选择器
|
||||
*/
|
||||
function handleAdd() {
|
||||
if (!searchForm.metaId) {
|
||||
ElMessage.warning('请先选择任务元数据');
|
||||
return;
|
||||
}
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 5. 确认选择用户
|
||||
*/
|
||||
async function handleUserConfirm(selected: any[]) {
|
||||
// 过滤掉部门节点
|
||||
const userItems = selected.filter(item => item.isDept !== true);
|
||||
|
||||
// 找出新增的用户(不在 recipientList 中的)
|
||||
const existingIds = new Set(recipientList.value.map(item => item.userId));
|
||||
const newUsers = userItems.filter(user => !existingIds.has(user.userId));
|
||||
|
||||
if (newUsers.length === 0) {
|
||||
ElMessage.info('没有新增的用户');
|
||||
dialogVisible.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证邮箱
|
||||
const userWithoutEmail = newUsers.find(user => !user.userEmail);
|
||||
if (userWithoutEmail) {
|
||||
ElMessage.warning(`用户 ${userWithoutEmail.username} 没有邮箱地址,无法添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
submitting.value = true;
|
||||
|
||||
// 批量添加新用户
|
||||
const promises = newUsers.map(user => {
|
||||
return crontabApi.createEmailDefault({
|
||||
metaId: searchForm.metaId,
|
||||
userId: user.userId,
|
||||
userEmail: user.userEmail,
|
||||
username: user.username
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
|
||||
if (successCount > 0) {
|
||||
ElMessage.success(`成功添加 ${successCount} 个默认接收人`);
|
||||
dialogVisible.value = false;
|
||||
loadDefaultRecipients();
|
||||
} else {
|
||||
ElMessage.error('添加失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加默认接收人失败:', error);
|
||||
ElMessage.error('添加默认接收人失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 6. 取消/重置选择器
|
||||
*/
|
||||
function resetUserSelector() {
|
||||
console.log('✖ 取消选择');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除接收人
|
||||
*/
|
||||
async function handleDelete(row: EmailDefault) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除用户"${row.username}"作为默认接收人吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
|
||||
const result = await crontabApi.deleteEmailDefault(row.defaultId!);
|
||||
if (result.success) {
|
||||
ElMessage.success('删除成功');
|
||||
loadDefaultRecipients();
|
||||
} else {
|
||||
ElMessage.error(result.message || '删除失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error);
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.meta-email-default {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.search-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.search-label {
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -386,6 +386,8 @@ const useDefaultRecipients = ref<boolean>(false);
|
||||
const defaultRecipients = ref<RecipientUserInfo[]>([]);
|
||||
const additionalRecipients = ref<RecipientUserInfo[]>([]);
|
||||
const showUserSelector = ref<boolean>(false);
|
||||
// 所有用户列表(带部门信息),供 selectedRecipients 反查使用
|
||||
const allUsers = ref<any[]>([]);
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
@@ -425,17 +427,45 @@ const selectedMethod = computed(() => {
|
||||
return selectedTemplate.value.methods.find(m => m.metaId === selectedMethodId.value) || null;
|
||||
});
|
||||
|
||||
// 计算已选接收人(包括默认接收人+额外添加的接收人)
|
||||
// 计算已选接收人(包括默认接收人+额外接收人),并转换为带部门信息的完整用户对象
|
||||
const selectedRecipients = computed(() => {
|
||||
// 先根据开关计算出合并后的接收人(RecipientUserInfo)列表
|
||||
let baseList: RecipientUserInfo[];
|
||||
if (useDefaultRecipients.value) {
|
||||
// 合并默认接收人和额外接收人,去重
|
||||
const all = [...defaultRecipients.value, ...additionalRecipients.value];
|
||||
const uniqueMap = new Map<string, RecipientUserInfo>();
|
||||
all.forEach(r => uniqueMap.set(r.userId, r));
|
||||
return Array.from(uniqueMap.values());
|
||||
baseList = Array.from(uniqueMap.values());
|
||||
} else {
|
||||
return additionalRecipients.value;
|
||||
baseList = additionalRecipients.value;
|
||||
}
|
||||
|
||||
// 用 userId 建索引,按 userId 反查完整用户信息(含 deptID / deptName / parentID)
|
||||
const userMap = new Map<string, any>(
|
||||
allUsers.value.map(user => [user.userId, user])
|
||||
);
|
||||
|
||||
const result = baseList.map(item => {
|
||||
const userId = item.userId;
|
||||
const user = userMap.get(userId);
|
||||
|
||||
if (user) {
|
||||
return {
|
||||
...user,
|
||||
isDept: false
|
||||
};
|
||||
}
|
||||
|
||||
// 找不到对应用户时,至少保证选择器可用
|
||||
return {
|
||||
userId: item.userId,
|
||||
username: item.username,
|
||||
userEmail: item.userEmail,
|
||||
isDept: false
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// 监听模板选择变化
|
||||
@@ -489,6 +519,9 @@ async function fetchAllUsers(): Promise<ResultDomain<any>> {
|
||||
parentID: user.parentID
|
||||
}));
|
||||
|
||||
// 缓存完整用户列表,给 selectedRecipients 使用
|
||||
allUsers.value = users;
|
||||
|
||||
return {
|
||||
...result,
|
||||
dataList: users
|
||||
|
||||
@@ -1,633 +0,0 @@
|
||||
<template>
|
||||
<AdminLayout title="定时任务" subtitle="定时任务管理">
|
||||
<div class="task-management">
|
||||
<div class="header">
|
||||
<h2>定时任务管理</h2>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增任务
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="search-bar">
|
||||
<div class="search-item">
|
||||
<span class="search-label">任务名称</span>
|
||||
<el-input
|
||||
v-model="searchForm.taskName"
|
||||
placeholder="请输入任务名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<span class="search-label">任务分组</span>
|
||||
<el-input
|
||||
v-model="searchForm.taskGroup"
|
||||
placeholder="请输入任务分组"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<span class="search-label">状态</span>
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option label="运行中" :value="1" />
|
||||
<el-option label="已暂停" :value="0" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="search-actions">
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<el-table
|
||||
:data="taskList"
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
>
|
||||
<el-table-column prop="taskName" label="任务名称" min-width="150" />
|
||||
<el-table-column prop="taskGroup" label="任务分组" width="120" />
|
||||
<el-table-column prop="beanName" label="Bean名称" min-width="150" />
|
||||
<el-table-column prop="methodName" label="方法名称" width="120" />
|
||||
<el-table-column prop="cronExpression" label="Cron表达式" min-width="150" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '运行中' : '已暂停' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="并发" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.concurrent === 1 ? 'success' : 'warning'" size="small">
|
||||
{{ row.concurrent === 1 ? '允许' : '禁止' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="300" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 0"
|
||||
type="success"
|
||||
size="small"
|
||||
@click="handleStart(row)"
|
||||
>
|
||||
启动
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="handlePause(row)"
|
||||
>
|
||||
暂停
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleExecute(row)"
|
||||
>
|
||||
执行一次
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="pageParam.pageNumber"
|
||||
v-model:page-size="pageParam.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑任务' : '新增任务'"
|
||||
width="700px"
|
||||
@close="resetForm"
|
||||
>
|
||||
<div class="form-content">
|
||||
<div class="form-item">
|
||||
<span class="form-label required">任务名称</span>
|
||||
<el-input
|
||||
v-model="formData.taskName"
|
||||
placeholder="请输入任务名称"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="form-label required">任务分组</span>
|
||||
<el-input
|
||||
v-model="formData.taskGroup"
|
||||
placeholder="请输入任务分组(如:SYSTEM、BUSINESS)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="form-label required">Bean名称</span>
|
||||
<el-input
|
||||
v-model="formData.beanName"
|
||||
placeholder="请输入Spring Bean名称"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="form-label required">方法名称</span>
|
||||
<el-input
|
||||
v-model="formData.methodName"
|
||||
placeholder="请输入要执行的方法名"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="form-label">方法参数</span>
|
||||
<el-input
|
||||
v-model="formData.methodParams"
|
||||
placeholder="请输入方法参数(JSON格式,可选)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="form-label required">Cron表达式</span>
|
||||
<el-input
|
||||
v-model="formData.cronExpression"
|
||||
placeholder="请输入Cron表达式(如:0 0 2 * * ?)"
|
||||
clearable
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="validateCron">验证</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="form-tip">
|
||||
格式:秒 分 时 日 月 周 年(年可选)。
|
||||
示例:0 0 2 * * ? 表示每天凌晨2点执行
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="form-label">是否允许并发</span>
|
||||
<el-radio-group v-model="formData.concurrent">
|
||||
<el-radio :label="1">允许</el-radio>
|
||||
<el-radio :label="0">禁止</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="form-label">错过执行策略</span>
|
||||
<el-select v-model="formData.misfirePolicy" placeholder="请选择策略">
|
||||
<el-option label="立即执行" :value="1" />
|
||||
<el-option label="执行一次" :value="2" />
|
||||
<el-option label="放弃执行" :value="3" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="form-label">任务描述</span>
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入任务描述"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitting"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus, Search, Refresh } from '@element-plus/icons-vue';
|
||||
import { crontabApi } from '@/apis/crontab';
|
||||
import type { CrontabTask, PageParam } from '@/types';
|
||||
import { AdminLayout } from '@/views/admin';
|
||||
defineOptions({
|
||||
name: 'TaskManagementView'
|
||||
});
|
||||
// 数据状态
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const taskList = ref<CrontabTask[]>([]);
|
||||
const total = ref(0);
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
taskName: '',
|
||||
taskGroup: '',
|
||||
status: undefined as number | undefined
|
||||
});
|
||||
|
||||
// 分页参数
|
||||
const pageParam = reactive<PageParam>({
|
||||
pageNumber: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
// 对话框状态
|
||||
const dialogVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<Partial<CrontabTask>>({
|
||||
taskName: '',
|
||||
taskGroup: '',
|
||||
beanName: '',
|
||||
methodName: '',
|
||||
methodParams: '',
|
||||
cronExpression: '',
|
||||
status: 0,
|
||||
concurrent: 0,
|
||||
misfirePolicy: 2,
|
||||
description: ''
|
||||
});
|
||||
|
||||
// 加载任务列表
|
||||
const loadTaskList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const filter: Partial<CrontabTask> = {};
|
||||
if (searchForm.taskName) filter.taskName = searchForm.taskName;
|
||||
if (searchForm.taskGroup) filter.taskGroup = searchForm.taskGroup;
|
||||
if (searchForm.status !== undefined) filter.status = searchForm.status;
|
||||
|
||||
const result = await crontabApi.getTaskPage(filter, pageParam);
|
||||
if (result.success) {
|
||||
// 根据后端返回结构处理数据
|
||||
if (result.pageDomain) {
|
||||
taskList.value = result.pageDomain.dataList || [];
|
||||
total.value = result.pageDomain.pageParam?.totalElements || 0;
|
||||
} else if (result.dataList) {
|
||||
taskList.value = result.dataList;
|
||||
total.value = result.pageParam?.totalElements || 0;
|
||||
} else {
|
||||
taskList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(result.message || '加载任务列表失败');
|
||||
taskList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载任务列表失败:', error);
|
||||
ElMessage.error('加载任务列表失败');
|
||||
taskList.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
pageParam.pageNumber = 1;
|
||||
loadTaskList();
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
function handleReset() {
|
||||
searchForm.taskName = '';
|
||||
searchForm.taskGroup = '';
|
||||
searchForm.status = undefined;
|
||||
pageParam.pageNumber = 1;
|
||||
loadTaskList();
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
function handlePageChange(page: number) {
|
||||
pageParam.pageNumber = page;
|
||||
loadTaskList();
|
||||
}
|
||||
|
||||
function handleSizeChange(size: number) {
|
||||
pageParam.pageSize = size;
|
||||
pageParam.pageNumber = 1;
|
||||
loadTaskList();
|
||||
}
|
||||
|
||||
// 新增任务
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
resetFormData();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 编辑任务
|
||||
function handleEdit(row: CrontabTask) {
|
||||
isEdit.value = true;
|
||||
Object.assign(formData, row);
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 启动任务
|
||||
async function handleStart(row: CrontabTask) {
|
||||
try {
|
||||
const result = await crontabApi.startTask(row.taskId!);
|
||||
if (result.success) {
|
||||
ElMessage.success('任务已启动');
|
||||
loadTaskList();
|
||||
} else {
|
||||
ElMessage.error(result.message || '启动失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动任务失败:', error);
|
||||
ElMessage.error('启动任务失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停任务
|
||||
async function handlePause(row: CrontabTask) {
|
||||
try {
|
||||
const result = await crontabApi.pauseTask(row.taskId!);
|
||||
if (result.success) {
|
||||
ElMessage.success('任务已暂停');
|
||||
loadTaskList();
|
||||
} else {
|
||||
ElMessage.error(result.message || '暂停失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('暂停任务失败:', error);
|
||||
ElMessage.error('暂停任务失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 执行一次
|
||||
async function handleExecute(row: CrontabTask) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定立即执行任务"${row.taskName}"吗?`,
|
||||
'确认执行',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
|
||||
const result = await crontabApi.executeTaskOnce(row.taskId!);
|
||||
if (result.success) {
|
||||
ElMessage.success('任务执行成功');
|
||||
} else {
|
||||
ElMessage.error(result.message || '执行失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('执行任务失败:', error);
|
||||
ElMessage.error('执行任务失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
async function handleDelete(row: CrontabTask) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除任务"${row.taskName}"吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
|
||||
const result = await crontabApi.deleteTask(row);
|
||||
if (result.success) {
|
||||
ElMessage.success('删除成功');
|
||||
loadTaskList();
|
||||
} else {
|
||||
ElMessage.error(result.message || '删除失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除任务失败:', error);
|
||||
ElMessage.error('删除任务失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证Cron表达式
|
||||
async function validateCron() {
|
||||
if (!formData.cronExpression) {
|
||||
ElMessage.warning('请输入Cron表达式');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await crontabApi.validateCronExpression(formData.cronExpression);
|
||||
if (result.success) {
|
||||
ElMessage.success('Cron表达式验证通过');
|
||||
} else {
|
||||
ElMessage.error(result.message || 'Cron表达式格式错误');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('验证Cron表达式失败:', error);
|
||||
ElMessage.error('验证失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
// 表单验证
|
||||
if (!formData.taskName) {
|
||||
ElMessage.warning('请输入任务名称');
|
||||
return;
|
||||
}
|
||||
if (!formData.taskGroup) {
|
||||
ElMessage.warning('请输入任务分组');
|
||||
return;
|
||||
}
|
||||
if (!formData.beanName) {
|
||||
ElMessage.warning('请输入Bean名称');
|
||||
return;
|
||||
}
|
||||
if (!formData.methodName) {
|
||||
ElMessage.warning('请输入方法名称');
|
||||
return;
|
||||
}
|
||||
if (!formData.cronExpression) {
|
||||
ElMessage.warning('请输入Cron表达式');
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
const data = { ...formData };
|
||||
let result;
|
||||
|
||||
if (isEdit.value) {
|
||||
result = await crontabApi.updateTask(data as CrontabTask);
|
||||
} else {
|
||||
result = await crontabApi.createTask(data as CrontabTask);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
dialogVisible.value = false;
|
||||
loadTaskList();
|
||||
} else {
|
||||
ElMessage.error(result.message || (isEdit.value ? '更新失败' : '创建失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
ElMessage.error('提交失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
resetFormData();
|
||||
}
|
||||
|
||||
// 重置表单数据
|
||||
function resetFormData() {
|
||||
Object.assign(formData, {
|
||||
taskName: '',
|
||||
taskGroup: '',
|
||||
beanName: '',
|
||||
methodName: '',
|
||||
methodParams: '',
|
||||
cronExpression: '',
|
||||
status: 0,
|
||||
concurrent: 0,
|
||||
misfirePolicy: 2,
|
||||
description: ''
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadTaskList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.task-management {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #141F38;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.search-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.search-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.form-content {
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
|
||||
&.required::before {
|
||||
content: '*';
|
||||
color: #f56c6c;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user