新闻采集修改,完成发送邮件

This commit is contained in:
2025-11-18 17:56:10 +08:00
parent 049b6f2cf3
commit 9f3176194b
50 changed files with 3929 additions and 322 deletions

View File

@@ -36,10 +36,10 @@ export const achievementApi = {
/**
* 删除成就
* @param achievement 成就信息包含achievementID
* @returns Promise<ResultDomain<void>>
* @returns Promise<ResultDomain<Boolean>>
*/
async deleteAchievement(achievement: Achievement): Promise<ResultDomain<void>> {
const response = await api.delete<void>('/achievements/achievement', achievement);
async deleteAchievement(achievement: Achievement): Promise<ResultDomain<Boolean>> {
const response = await api.delete<boolean>('/achievements/achievement', achievement);
return response.data;
},
@@ -138,10 +138,10 @@ export const achievementApi = {
* 撤销用户成就
* @param userID 用户ID
* @param achievementID 成就ID
* @returns Promise<ResultDomain<void>>
* @returns Promise<ResultDomain<Boolean>>
*/
async revokeAchievement(userID: string, achievementID: string): Promise<ResultDomain<void>> {
const response = await api.delete<void>('/achievements/revoke', null, {
async revokeAchievement(userID: string, achievementID: string): Promise<ResultDomain<Boolean>> {
const response = await api.delete<boolean>('/achievements/revoke', null, {
params: { userID, achievementID }
});
return response.data;

View File

@@ -169,15 +169,15 @@ export const documentSegmentApi = {
* @param documentId Dify文档ID
* @param segmentId 分段ID
* @param childChunkId 子块ID
* @returns Promise<ResultDomain<void>>
* @returns Promise<ResultDomain<Boolean>>
*/
async deleteChildChunk(
datasetId: string,
documentId: string,
segmentId: string,
childChunkId: string
): Promise<ResultDomain<void>> {
const response = await api.delete<void>(
): Promise<ResultDomain<Boolean>> {
const response = await api.delete<boolean>(
`/ai/dify/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`
);
return response.data;

View File

@@ -147,15 +147,15 @@ export const fileUploadApi = {
* @param datasetId Dify数据集ID
* @param documentId Dify文档ID
* @param enabled 是否启用
* @returns Promise<ResultDomain<void>>
* @returns Promise<ResultDomain<Boolean>>
*/
async updateDocumentStatus(
datasetId: string,
documentId: string,
enabled: boolean
): Promise<ResultDomain<void>> {
): Promise<ResultDomain<Boolean>> {
const action = enabled ? 'enable' : 'disable';
const response = await api.post<void>(
const response = await api.post<boolean>(
`/ai/dify/datasets/${datasetId}/documents/status/${action}`,
{ document_ids: [documentId] }
);

View File

@@ -5,7 +5,18 @@
*/
import { api } from '@/apis/index';
import type { CrontabTask, CrontabLog, DataCollectionItem, CrontabItem, ResultDomain, PageParam } from '@/types';
import type {
CrontabTask,
CrontabLog,
DataCollectionItem,
CrontabItem,
TaskMeta,
EmailDefault,
EmailRecipient,
CreateTaskRequest,
ResultDomain,
PageParam
} from '@/types';
/**
* 定时任务API服务
@@ -16,11 +27,11 @@ export const crontabApi = {
// ==================== 定时任务管理 ====================
/**
* 获取可创建的定时任务模板列表
* @returns Promise<ResultDomain<CrontabItem>>
* 获取可创建的定时任务列表(从数据库获取任务元数据)
* @returns Promise<ResultDomain<TaskMeta>>
*/
async getEnabledCrontabList(): Promise<ResultDomain<CrontabItem>> {
const response = await api.get<CrontabItem>(`${this.baseUrl}/getEnabledCrontabList`);
async getEnabledCrontabList(): Promise<ResultDomain<TaskMeta>> {
const response = await api.get<TaskMeta>(`${this.baseUrl}/getEnabledCrontabList`);
return response.data;
},
@@ -29,18 +40,18 @@ export const crontabApi = {
* @param task 任务对象
* @returns Promise<ResultDomain<CrontabTask>>
*/
async createTask(task: CrontabTask): Promise<ResultDomain<CrontabTask>> {
async createTask(task: CreateTaskRequest): Promise<ResultDomain<CrontabTask>> {
const response = await api.post<CrontabTask>(`${this.baseUrl}/crontabTask`, task);
return response.data;
},
/**
* 更新定时任务
* @param task 任务对象
* @param request 更新任务请求包含任务信息、元数据ID等
* @returns Promise<ResultDomain<CrontabTask>>
*/
async updateTask(task: CrontabTask): Promise<ResultDomain<CrontabTask>> {
const response = await api.put<CrontabTask>(`${this.baseUrl}/crontabTask`, task);
async updateTask(request: CreateTaskRequest): Promise<ResultDomain<CrontabTask>> {
const response = await api.put<CrontabTask>(`${this.baseUrl}/crontabTask`, request);
return response.data;
},
@@ -255,5 +266,254 @@ export const crontabApi = {
const response = await api.put<string>(`${this.baseUrl}/collection/item/${itemId}/status/${status}`);
return response.data;
},
// ==================== 任务元数据管理 ====================
/**
* 创建任务元数据
* @param taskMeta 任务元数据
* @returns Promise<ResultDomain<TaskMeta>>
*/
async createTaskMeta(taskMeta: TaskMeta): Promise<ResultDomain<TaskMeta>> {
const response = await api.post<TaskMeta>(`${this.baseUrl}/meta`, taskMeta);
return response.data;
},
/**
* 更新任务元数据
* @param taskMeta 任务元数据
* @returns Promise<ResultDomain<TaskMeta>>
*/
async updateTaskMeta(taskMeta: TaskMeta): Promise<ResultDomain<TaskMeta>> {
const response = await api.put<TaskMeta>(`${this.baseUrl}/meta`, taskMeta);
return response.data;
},
/**
* 删除任务元数据
* @param metaId 元数据ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteTaskMeta(metaId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/meta/${metaId}`);
return response.data;
},
/**
* 根据ID查询任务元数据
* @param metaId 元数据ID
* @returns Promise<ResultDomain<TaskMeta>>
*/
async getTaskMetaById(metaId: string): Promise<ResultDomain<TaskMeta>> {
const response = await api.get<TaskMeta>(`${this.baseUrl}/meta/${metaId}`);
return response.data;
},
/**
* 查询所有任务元数据
* @returns Promise<ResultDomain<TaskMeta>>
*/
async getAllTaskMeta(): Promise<ResultDomain<TaskMeta>> {
const response = await api.get<TaskMeta>(`${this.baseUrl}/meta/all`);
return response.data;
},
/**
* 根据分类查询任务元数据
* @param category 分类
* @returns Promise<ResultDomain<TaskMeta>>
*/
async getTaskMetaByCategory(category: string): Promise<ResultDomain<TaskMeta>> {
const response = await api.get<TaskMeta>(`${this.baseUrl}/meta/category/${category}`);
return response.data;
},
/**
* 分页查询任务元数据
* @param filter 过滤条件
* @param pageParam 分页参数
* @returns Promise<ResultDomain<TaskMeta>>
*/
async getTaskMetaPage(filter?: Partial<TaskMeta>, pageParam?: PageParam): Promise<ResultDomain<TaskMeta>> {
const response = await api.post<TaskMeta>(`${this.baseUrl}/meta/page`, {
filter,
pageParam: {
pageNumber: pageParam?.pageNumber || 1,
pageSize: pageParam?.pageSize || 10
}
});
return response.data;
},
// ==================== 邮件默认接收人管理 ====================
/**
* 创建默认接收人
* @param emailDefault 默认接收人
* @returns Promise<ResultDomain<EmailDefault>>
*/
async createEmailDefault(emailDefault: EmailDefault): Promise<ResultDomain<EmailDefault>> {
const response = await api.post<EmailDefault>(`${this.baseUrl}/email/default`, emailDefault);
return response.data;
},
/**
* 更新默认接收人
* @param emailDefault 默认接收人
* @returns Promise<ResultDomain<EmailDefault>>
*/
async updateEmailDefault(emailDefault: EmailDefault): Promise<ResultDomain<EmailDefault>> {
const response = await api.put<EmailDefault>(`${this.baseUrl}/email/default`, emailDefault);
return response.data;
},
/**
* 删除默认接收人
* @param defaultId 默认ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteEmailDefault(defaultId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/email/default/${defaultId}`);
return response.data;
},
/**
* 根据defaultId查询
* @param defaultId 默认ID
* @returns Promise<ResultDomain<EmailDefault>>
*/
async getEmailDefaultById(defaultId: string): Promise<ResultDomain<EmailDefault>> {
const response = await api.get<EmailDefault>(`${this.baseUrl}/email/default/${defaultId}`);
return response.data;
},
/**
* 根据metaId查询默认接收人
* @param metaId 元数据ID
* @returns Promise<ResultDomain<EmailDefault>>
*/
async getEmailDefaultByMetaId(metaId: string): Promise<ResultDomain<EmailDefault>> {
const response = await api.get<EmailDefault>(`${this.baseUrl}/email/default/meta/${metaId}`);
return response.data;
},
// ==================== 邮件接收人管理 ====================
/**
* 创建邮件接收人
* @param recipient 邮件接收人
* @returns Promise<ResultDomain<EmailRecipient>>
*/
async createEmailRecipient(recipient: EmailRecipient): Promise<ResultDomain<EmailRecipient>> {
const response = await api.post<EmailRecipient>(`${this.baseUrl}/email/recipient`, recipient);
return response.data;
},
/**
* 批量创建邮件接收人
* @param recipients 邮件接收人列表
* @returns Promise<ResultDomain<boolean>>
*/
async batchCreateEmailRecipient(recipients: EmailRecipient[]): Promise<ResultDomain<boolean>> {
const response = await api.post<boolean>(`${this.baseUrl}/email/recipient/batch`, recipients);
return response.data;
},
/**
* 更新邮件接收人
* @param recipient 邮件接收人
* @returns Promise<ResultDomain<EmailRecipient>>
*/
async updateEmailRecipient(recipient: EmailRecipient): Promise<ResultDomain<EmailRecipient>> {
const response = await api.put<EmailRecipient>(`${this.baseUrl}/email/recipient`, recipient);
return response.data;
},
/**
* 删除邮件接收人
* @param recipientId 接收人ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteEmailRecipient(recipientId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/email/recipient/${recipientId}`);
return response.data;
},
/**
* 根据ID查询接收人
* @param recipientId 接收人ID
* @returns Promise<ResultDomain<EmailRecipient>>
*/
async getEmailRecipientById(recipientId: string): Promise<ResultDomain<EmailRecipient>> {
const response = await api.get<EmailRecipient>(`${this.baseUrl}/email/recipient/${recipientId}`);
return response.data;
},
/**
* 根据default_id查询接收人列表
* @param defaultId 默认ID
* @returns Promise<ResultDomain<EmailRecipient>>
*/
async getRecipientsByDefaultId(defaultId: string): Promise<ResultDomain<EmailRecipient>> {
const response = await api.get<EmailRecipient>(`${this.baseUrl}/email/recipient/default/${defaultId}`);
return response.data;
},
/**
* 根据任务ID查询接收人列表
* @param taskId 任务ID
* @returns Promise<ResultDomain<EmailRecipient>>
*/
async getRecipientsByTaskId(taskId: string): Promise<ResultDomain<EmailRecipient>> {
const response = await api.get<EmailRecipient>(`${this.baseUrl}/email/recipient/task/${taskId}`);
return response.data;
},
/**
* 查询所有启用的接收人
* @returns Promise<ResultDomain<EmailRecipient>>
*/
async getAllEnabledRecipients(): Promise<ResultDomain<EmailRecipient>> {
const response = await api.get<EmailRecipient>(`${this.baseUrl}/email/recipient/enabled`);
return response.data;
},
/**
* 分页查询邮件接收人
* @param filter 过滤条件
* @param pageParam 分页参数
* @returns Promise<ResultDomain<EmailRecipient>>
*/
async getEmailRecipientPage(filter?: Partial<EmailRecipient>, pageParam?: PageParam): Promise<ResultDomain<EmailRecipient>> {
const response = await api.post<EmailRecipient>(`${this.baseUrl}/email/recipient/page`, {
filter,
pageParam: {
pageNumber: pageParam?.pageNumber || 1,
pageSize: pageParam?.pageSize || 10
}
});
return response.data;
},
/**
* 删除default_id的所有接收人
* @param defaultId 默认ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteRecipientsByDefaultId(defaultId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/email/recipient/default/${defaultId}`);
return response.data;
},
/**
* 删除任务的所有接收人
* @param taskId 任务ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteRecipientsByTaskId(taskId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/email/recipient/task/${taskId}`);
return response.data;
},
};

View File

@@ -16,6 +16,10 @@ export interface CrontabTask extends BaseDTO {
taskName?: string;
/** 任务分组 */
taskGroup?: string;
/** 元数据ID关联任务元数据表 */
metaId?: string;
/** 是否使用默认接收人 */
defaultRecipient?: boolean;
/** Bean名称 */
beanName?: string;
/** 方法名称 */
@@ -172,6 +176,8 @@ export interface CrontabMethod {
excuete_method?: string;
/** Python脚本路径 */
path: string;
/** 元数据ID从数据库加载时使用 */
metaId?: string;
/** 参数定义列表 */
params?: CrontabParam[];
}
@@ -186,3 +192,95 @@ export interface CrontabItem {
methods: CrontabMethod[];
}
/**
* 定时任务元数据
*/
export interface TaskMeta extends BaseDTO {
/** 元数据ID */
metaId?: string;
/** 任务名称 */
name?: string;
/** 任务描述 */
description?: string;
/** 任务分类 */
category?: string;
/** Bean名称 */
beanName?: string;
/** 方法名称 */
methodName?: string;
/** 脚本路径 */
scriptPath?: string;
/** 参数模式(JSON Schema) */
paramSchema?: string;
/** 是否自动发布 */
autoPublish?: boolean;
/** 排序 */
sortOrder?: number;
/** 创建者 */
creator?: string;
/** 更新者 */
updater?: string;
}
/**
* 邮件默认接收人
*/
export interface EmailDefault extends BaseDTO {
/** 默认ID */
defaultId?: string;
/** 元数据ID */
metaId?: string;
/** 用户ID */
userId?: string;
userEmail?: string;
username?:string;
/** 创建者 */
creator?: string;
/** 更新者 */
updater?: string;
}
/**
* 邮件接收人
*/
export interface EmailRecipient extends BaseDTO {
/** 接收人ID */
recipientId?: string;
/** 任务ID */
taskId?: string;
/** 用户ID */
userId?: string;
/** 邮箱 */
email?: string;
/** 姓名 */
name?: string;
/** 创建者 */
creator?: string;
/** 更新者 */
updater?: string;
}
/**
* 接收人用户信息
*/
export interface RecipientUserInfo {
/** 用户ID */
userId: string;
/** 用户邮箱 */
userEmail: string;
/** 用户名称 */
username: string;
}
/**
* 创建任务请求
*/
export interface CreateTaskRequest {
/** 任务信息 */
task: CrontabTask;
/** 任务元数据ID */
metaId: string;
/** 额外添加的接收人列表 */
additionalRecipients?: RecipientUserInfo[];
}

View File

@@ -320,7 +320,7 @@ export interface TaskItemVO extends LearningTask {
username?: string;
deptID?: string;
deptName?: string;
parentDeptID?: string;
parentID?: string;
/** 是否必修 */
required?: boolean;
/** 排序号 */

View File

@@ -73,7 +73,7 @@ export interface UserVO extends BaseDTO {
/** 学习等级 */
level?: number;
deptID?: string;
parentDeptID?: string;
parentID?: string;
/** 部门名称 */
deptName?: string;
/** 角色名称 */

View File

@@ -206,16 +206,16 @@
<div class="form-item" v-if="selectedTemplate">
<span class="form-label required">爬取方法</span>
<el-select
v-model="selectedMethod"
v-model="selectedMethodId"
placeholder="请选择爬取方法"
style="width: 100%"
>
<el-option
v-for="method in selectedTemplate.methods"
:key="method.name"
:key="method.metaId"
:label="method.name"
:value="method"
:value="method.metaId"
/>
</el-select>
<span class="form-tip">
@@ -282,15 +282,42 @@
placeholder="请输入爬虫描述"
/>
</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>
<span class="form-tip">
建议禁止并发避免重复抓取
<span class="form-label">邮件通知</span>
<el-checkbox v-model="useDefaultRecipients">
使用默认接收人
</el-checkbox>
<span class="form-tip" v-if="useDefaultRecipients && defaultRecipients.length > 0">
默认接收人{{ defaultRecipients.map(r => r.username).join('、') }}
</span>
<span class="form-tip" v-else-if="useDefaultRecipients && defaultRecipients.length === 0">
该任务模板暂无默认接收人
</span>
</div>
<div class="form-item">
<span class="form-label">额外接收人</span>
<div class="recipient-list">
<el-tag
v-for="recipient in additionalRecipients"
:key="recipient.userId"
closable
@close="removeRecipient(recipient)"
style="margin-right: 8px; margin-bottom: 8px;"
>
{{ recipient.username }}
</el-tag>
</div>
<el-button
@click="showRecipientSelector"
size="small"
style="margin-top: 8px;"
>
选择接收人
</el-button>
<!-- TODO: 在这里添加自定义的用户选择组件 -->
</div>
</div>
@@ -305,17 +332,38 @@
</el-button>
</template>
</el-dialog>
<GenericSelector
v-model:visible="showUserSelector"
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, onMounted, watch } from 'vue';
import { ref, reactive, onMounted, watch, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus, Search, Refresh, DocumentCopy, VideoPlay, VideoPause, Promotion, Edit, Delete } from '@element-plus/icons-vue';
import { crontabApi } from '@/apis/crontab';
import type { CrontabTask, CrontabItem, CrontabMethod, PageParam } from '@/types';
import { userApi } from '@/apis/system/user';
import type { CrontabTask, TaskMeta, CrontabItem, CrontabMethod, CrontabParam, PageParam, CreateTaskRequest, RecipientUserInfo, UserVO, ResultDomain, EmailDefault } from '@/types';
import { AdminLayout } from '@/views/admin';
import { GenericSelector } from '@/components';
defineOptions({
name: 'NewsCrawlerView'
});
@@ -325,12 +373,20 @@ const submitting = ref(false);
const crawlerList = ref<CrontabTask[]>([]);
const total = ref(0);
// 爬虫模板数据
const crawlerTemplates = ref<CrontabItem[]>([]);
// 爬虫数据
const taskMetaList = ref<TaskMeta[]>([]);
const crawlerTemplates = ref<CrontabItem[]>([]); // 转换后的模板结构
const selectedTemplate = ref<CrontabItem | null>(null);
const selectedMethod = ref<CrontabMethod | null>(null);
const selectedMethodId = ref<string>(''); // 选中的方法ID(metaId)
const selectedMetaId = ref<string>(''); // 选中的元数据ID
const dynamicParams = ref<Record<string, any>>({});
// 邮件接收人相关
const useDefaultRecipients = ref<boolean>(false);
const defaultRecipients = ref<RecipientUserInfo[]>([]);
const additionalRecipients = ref<RecipientUserInfo[]>([]);
const showUserSelector = ref<boolean>(false);
// 搜索表单
const searchForm = reactive({
taskName: '',
@@ -361,40 +417,251 @@ const formData = reactive<Partial<CrontabTask>>({
description: ''
});
// 根据selectedMethodId获取完整的method对象
const selectedMethod = computed(() => {
if (!selectedTemplate.value || !selectedMethodId.value) {
return null;
}
return selectedTemplate.value.methods.find(m => m.metaId === selectedMethodId.value) || null;
});
// 计算已选接收人(包括默认接收人+额外添加的接收人)
const selectedRecipients = computed(() => {
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());
} else {
return additionalRecipients.value;
}
});
// 监听模板选择变化
watch(selectedTemplate, (newTemplate, oldTemplate) => {
// 只在用户手动切换模板时重置oldTemplate存在且不为null时才重置
// 编辑回填时oldTemplate为null不会触发重置
if (newTemplate && oldTemplate) {
selectedMethod.value = null;
selectedMethodId.value = '';
dynamicParams.value = {};
}
});
// 监听方法选择变化
watch(selectedMethod, (newMethod) => {
if (newMethod) {
watch(selectedMethodId, (newMethodId) => {
if (newMethodId && selectedMethod.value) {
// 保存metaId
selectedMetaId.value = newMethodId;
dynamicParams.value = {};
// 遍历params数组提取默认值
if (newMethod.params && Array.isArray(newMethod.params)) {
newMethod.params.forEach(param => {
if (selectedMethod.value.params && Array.isArray(selectedMethod.value.params)) {
selectedMethod.value.params.forEach((param: CrontabParam) => {
dynamicParams.value[param.name] = param.value;
});
}
// 加载该任务模板的默认接收人
if (selectedMetaId.value) {
loadDefaultRecipients(selectedMetaId.value);
}
}
});
// 加载爬虫模板
// ==================== 人员选择器相关 ====================
/**
* 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
}));
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. 过滤方法:从可选项中移除已选项
*/
function filterUsers(available: any[], selected: any[]): any[] {
const selectedIds = new Set(selected.map(item => 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<string, any>();
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 // 标记这是部门节点
});
}
// 添加用户到部门的children中
const deptNode = deptMap.get(item.deptID);
if (deptNode) {
deptNode.children.push({
...item,
isDept: false // 标记这是用户节点
});
}
});
// 第二步:构建部门层级关系
const allDepts = Array.from(deptMap.values());
const deptTreeMap = new Map<string, any>();
// 初始化所有部门节点(创建副本)
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 showRecipientSelector() {
showUserSelector.value = true;
}
/**
* 5. 确认选择用户
*/
function handleUserConfirm(selected: any[]) {
// 过滤掉部门节点,只保留用户节点
const userItems = selected.filter(item => item.isDept !== true && !defaultRecipients.value.find(r => r.userId === item.userId));
additionalRecipients.value = userItems.map(item => ({
userId: item.userId,
username: item.username,
userEmail: item.userEmail || ''
}));
}
/**
* 6. 取消/重置选择器
*/
function resetUserSelector() {
console.log('❌ 取消选择');
// 不做任何操作,保持原有选择
}
// 加载爬虫模板从数据库加载TaskMeta转换为CrontabItem结构
async function loadCrawlerTemplates() {
try {
const result = await crontabApi.getEnabledCrontabList();
if (result.success && result.dataList) {
crawlerTemplates.value = result.dataList;
taskMetaList.value = result.dataList;
// 将TaskMeta[]按category分组转换为CrontabItem[]
const grouped = new Map<string, TaskMeta[]>();
result.dataList.forEach((meta: TaskMeta) => {
const category = meta.category || '未分类';
if (!grouped.has(category)) {
grouped.set(category, []);
}
grouped.get(category)!.push(meta);
});
// 转换为CrontabItem结构
crawlerTemplates.value = Array.from(grouped.entries()).map(([category, metas]) => ({
name: category,
methods: metas.map(meta => ({
name: meta.name || '',
clazz: meta.beanName || '',
excuete_method: meta.methodName || '',
path: meta.scriptPath || '',
metaId: meta.metaId || '', // 保存metaId
params: meta.paramSchema ? JSON.parse(meta.paramSchema) : []
}))
}));
} else {
ElMessage.error(result.message || '加载爬虫模板失败');
}
} catch (error) {
console.error('加载爬虫模板失败:', error);
ElMessage.error('加载爬虫模板失败');
}
}
@@ -428,7 +695,6 @@ async function loadCrawlerList() {
total.value = 0;
}
} catch (error) {
console.error('加载爬虫列表失败:', error);
ElMessage.error('加载爬虫列表失败');
crawlerList.value = [];
total.value = 0;
@@ -468,50 +734,68 @@ function handleAdd() {
isEdit.value = false;
resetFormData();
selectedTemplate.value = null;
selectedMethod.value = null;
selectedMethodId.value = '';
dynamicParams.value = {};
dialogVisible.value = true;
}
// 编辑爬虫
function handleEdit(row: CrontabTask) {
async function handleEdit(row: CrontabTask) {
isEdit.value = true;
Object.assign(formData, row);
// 重置选择
selectedTemplate.value = null;
selectedMethod.value = null;
selectedMethodId.value = '';
dynamicParams.value = {};
// 尝试解析methodParams来回填表单
if (row.methodParams) {
// 回填邮件接收人配置
useDefaultRecipients.value = row.defaultRecipient || false;
additionalRecipients.value = [];
// 加载该任务的额外接收人
if (row.taskId) {
try {
const params = JSON.parse(row.methodParams);
// 如果有scriptPath,尝试匹配模板和方法
if (params.scriptPath) {
const template = crawlerTemplates.value.find(t =>
t.methods.some(m => m.path === params.scriptPath)
);
if (template) {
const method = template.methods.find(m => m.path === params.scriptPath);
if (method) {
// 先设置template和method触发watch填充默认值
selectedTemplate.value = template;
selectedMethod.value = method;
// 然后使用nextTick确保watch执行完后再覆盖为实际值
// 回填动态参数排除scriptPath
const { scriptPath, ...restParams } = params;
const recipientsResult = await crontabApi.getRecipientsByTaskId(row.taskId);
if (recipientsResult.success && recipientsResult.dataList) {
additionalRecipients.value = recipientsResult.dataList.map(item => ({
userId: item.userId || '',
username: item.name || '',
userEmail: item.email || ''
}));
}
} catch (error) {
console.error('加载额外接收人失败:', error);
}
}
// 通过metaId直接匹配
if (row.metaId) {
// 遍历所有模板和方法找到匹配的metaId
for (const template of crawlerTemplates.value) {
const method = template.methods.find(m => m.metaId === row.metaId);
if (method) {
// 找到匹配的方法设置template和method
selectedTemplate.value = template;
selectedMethodId.value = method.metaId || '';
selectedMetaId.value = method.metaId || '';
// 回填动态参数
if (row.methodParams) {
try {
const params = JSON.parse(row.methodParams);
// 排除系统参数
const { scriptPath, taskId, logId, ...restParams } = params;
// 延迟设置确保watch先执行完
setTimeout(() => {
dynamicParams.value = restParams;
console.log('📝 编辑回填 - template:', template.name, 'method:', method.name, 'params:', restParams);
}, 0);
} catch (error) {
console.error('解析methodParams失败:', error);
}
}
break;
}
} catch (error) {
console.warn('解析methodParams失败:', error);
}
}
@@ -529,7 +813,6 @@ async function handleStart(row: CrontabTask) {
ElMessage.error(result.message || '启动失败');
}
} catch (error) {
console.error('启动爬虫失败:', error);
ElMessage.error('启动爬虫失败');
}
}
@@ -545,7 +828,6 @@ async function handlePause(row: CrontabTask) {
ElMessage.error(result.message || '暂停失败');
}
} catch (error) {
console.error('暂停爬虫失败:', error);
ElMessage.error('暂停爬虫失败');
}
}
@@ -563,15 +845,21 @@ async function handleExecute(row: CrontabTask) {
}
);
const result = await crontabApi.executeTaskOnce(row.taskId!);
if (result.success) {
ElMessage.success('爬虫执行成功,请稍后查看执行日志');
} else {
ElMessage.error(result.message || '执行失败');
}
// 异步执行,不等待任务完成
crontabApi.executeTaskOnce(row.taskId!).then(result => {
if (result.success) {
ElMessage.success('任务已提交执行,请稍后查看执行日志');
} else {
ElMessage.error(result.message || '提交执行失败');
}
}).catch(() => {
ElMessage.error('提交执行失败');
});
// 立即提示用户任务已触发
ElMessage.info('任务执行已触发,正在后台运行...');
} catch (error: any) {
if (error !== 'cancel') {
console.error('执行爬虫失败:', error);
ElMessage.error('执行爬虫失败');
}
}
@@ -599,7 +887,6 @@ async function handleDelete(row: CrontabTask) {
}
} catch (error: any) {
if (error !== 'cancel') {
console.error('删除爬虫失败:', error);
ElMessage.error('删除爬虫失败');
}
}
@@ -620,7 +907,6 @@ async function validateCron() {
ElMessage.error(result.message || 'Cron表达式格式错误');
}
} catch (error) {
console.error('验证Cron表达式失败:', error);
ElMessage.error('验证失败');
}
}
@@ -640,6 +926,12 @@ async function handleSubmit() {
ElMessage.warning('请输入Cron表达式');
return;
}
// 校验additionRecipients的email存在
const recipientWithoutEmail = additionalRecipients.value.find(recipient => !recipient.userEmail);
if (recipientWithoutEmail) {
ElMessage.warning(`${recipientWithoutEmail.username} 邮箱不能为空`);
return;
}
// 验证必填参数
if (selectedMethod.value.params && Array.isArray(selectedMethod.value.params)) {
@@ -659,26 +951,24 @@ async function handleSubmit() {
submitting.value = true;
try {
// 传递taskGroup和methodName中文名后端根据这两个name查找配置并填充beanName、methodName和scriptPath
const data = {
...formData,
taskGroup: selectedTemplate.value.name, // 模板名称(中文)
methodName: selectedMethod.value.name, // 方法名称(中文)
methodParams: JSON.stringify({
...dynamicParams.value // 只传用户输入的参数scriptPath由后端填充
})
// 构建CreateTaskRequest
const requestData: CreateTaskRequest = {
metaId: selectedMetaId.value,
task: {
...formData,
defaultRecipient: useDefaultRecipients.value,
methodParams: JSON.stringify({
...dynamicParams.value
})
} as CrontabTask,
additionalRecipients: additionalRecipients.value
};
console.log('📤 准备提交的数据:', data);
console.log('📤 taskGroup:', selectedTemplate.value.name);
console.log('📤 methodName:', selectedMethod.value.name);
console.log('📤 动态参数:', dynamicParams.value);
let result;
if (isEdit.value) {
result = await crontabApi.updateTask(data as CrontabTask);
result = await crontabApi.updateTask(requestData);
} else {
result = await crontabApi.createTask(data as CrontabTask);
result = await crontabApi.createTask(requestData);
}
if (result.success) {
@@ -689,7 +979,6 @@ async function handleSubmit() {
ElMessage.error(result.message || (isEdit.value ? '更新失败' : '创建失败'));
}
} catch (error) {
console.error('提交失败:', error);
ElMessage.error('提交失败');
} finally {
submitting.value = false;
@@ -713,8 +1002,41 @@ function resetFormData() {
status: 0,
concurrent: 0,
misfirePolicy: 3,
description: ''
description: '',
defaultRecipient: false
});
useDefaultRecipients.value = false;
defaultRecipients.value = [];
additionalRecipients.value = [];
}
// 邮件接收人相关方法
async function loadDefaultRecipients(metaId: string) {
try {
const result = await crontabApi.getEmailDefaultByMetaId(metaId);
if (result.success && result.dataList && result.dataList.length > 0) {
defaultRecipients.value = result.dataList
.map(item => ({
userId: item.userId!,
username: item.username!,
userEmail: item.userEmail!
}));
} else {
defaultRecipients.value = [];
}
} catch (error) {
defaultRecipients.value = [];
}
}
// TODO: 用户可以在这里添加自定义的邮件接收人选择逻辑
function removeRecipient(recipient: RecipientUserInfo) {
const index = additionalRecipients.value.findIndex(r => r.userId === recipient.userId);
if (index > -1) {
additionalRecipients.value.splice(index, 1);
}
}
// 初始化

View File

@@ -551,7 +551,7 @@ async function fetchTaskUsers() {
username: item.username,
deptID: item.deptID,
deptName: item.deptName,
parentDeptID: item.parentDeptID
parentID: item.parentID
}));
return {
success: true,
@@ -604,7 +604,7 @@ function transformUsersToTree(flatData: any[]): any[] {
displayName: deptName,
deptID: deptID,
deptName: deptName,
parentDeptID: item.parentDeptID,
parentID: item.parentID,
children: [],
isDept: true // 标记这是部门节点
});
@@ -635,12 +635,12 @@ function transformUsersToTree(flatData: any[]): any[] {
const node = deptTreeMap.get(dept.deptID);
if (!node) return;
if (!dept.parentDeptID || dept.parentDeptID === '0' || dept.parentDeptID === '') {
if (!dept.parentID || dept.parentID === '0' || dept.parentID === '') {
// 根部门
tree.push(node);
} else {
// 子部门
const parent = deptTreeMap.get(dept.parentDeptID);
const parent = deptTreeMap.get(dept.parentID);
if (parent) {
// 将用户节点暂存
const users = node.children || [];

View File

@@ -588,8 +588,8 @@ function transformUsersToTree(flatData: any[]): any[] {
flatData.forEach(item => {
const deptID = item.deptID || 'unknown';
const deptName = item.deptName || '未分配部门';
// 优先使用 parentID如果不存在则使用 parentDeptID
const parentID = item.parentID || item.parentDeptID;
// 优先使用 parentID如果不存在则使用 parentID
const parentID = item.parentID || item.parentID;
if (!deptMap.has(deptID)) {
// 创建部门节点
@@ -598,7 +598,7 @@ function transformUsersToTree(flatData: any[]): any[] {
displayName: deptName,
deptID: deptID,
deptName: deptName,
parentDeptID: parentID,
parentID: parentID,
children: [],
isDept: true
});
@@ -629,12 +629,12 @@ function transformUsersToTree(flatData: any[]): any[] {
const node = deptTreeMap.get(dept.deptID);
if (!node) return;
if (!dept.parentDeptID || dept.parentDeptID === '0' || dept.parentDeptID === '') {
if (!dept.parentID || dept.parentID === '0' || dept.parentID === '') {
// 根部门
tree.push(node);
} else {
// 子部门
const parent = deptTreeMap.get(dept.parentDeptID);
const parent = deptTreeMap.get(dept.parentID);
if (parent) {
// 将用户节点暂存
const users = node.children || [];

View File

@@ -607,7 +607,7 @@ async function fetchTaskUsers() {
username: item.username,
deptID: item.deptID,
deptName: item.deptName,
parentDeptID: item.parentDeptID
parentID: item.parentID
}));
return {
success: true,
@@ -656,7 +656,7 @@ function transformUsersToTree(flatData: any[]): any[] {
displayName: deptName,
deptID: deptID,
deptName: deptName,
parentDeptID: item.parentDeptID,
parentID: item.parentID,
children: [],
isDept: true // 标记这是部门节点
});
@@ -687,12 +687,12 @@ function transformUsersToTree(flatData: any[]): any[] {
const node = deptTreeMap.get(dept.deptID);
if (!node) return;
if (!dept.parentDeptID || dept.parentDeptID === '0' || dept.parentDeptID === '') {
if (!dept.parentID || dept.parentID === '0' || dept.parentID === '') {
// 根部门
tree.push(node);
} else {
// 子部门
const parent = deptTreeMap.get(dept.parentDeptID);
const parent = deptTreeMap.get(dept.parentID);
if (parent) {
// 将用户节点暂存
const users = node.children || [];