Compare commits

...

5 Commits

Author SHA1 Message Date
0ff82657ab 更新接口和调用方 2025-10-29 19:42:48 +08:00
fff309f56b 修bug 2025-10-25 15:42:19 +08:00
d3700c8835 实现通知记录界面 2025-10-25 15:34:28 +08:00
ea44c9caea 增加新接口定义 2025-10-25 15:27:27 +08:00
ab8ea6977f 优化逻辑 2025-10-24 14:23:58 +08:00
12 changed files with 732 additions and 45 deletions

View File

@@ -970,6 +970,149 @@
} }
} }
}, },
"/api/v1/monitor/notifications": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "根据提供的过滤条件,分页获取通知列表",
"produces": [
"application/json"
],
"tags": [
"数据监控"
],
"summary": "批量查询通知",
"parameters": [
{
"type": "string",
"name": "end_time",
"in": "query"
},
{
"enum": [
7,
-1,
0,
1,
2,
3,
4,
5,
-1,
5,
6
],
"type": "integer",
"format": "int32",
"x-enum-varnames": [
"_numLevels",
"DebugLevel",
"InfoLevel",
"WarnLevel",
"ErrorLevel",
"DPanicLevel",
"PanicLevel",
"FatalLevel",
"_minLevel",
"_maxLevel",
"InvalidLevel"
],
"name": "level",
"in": "query"
},
{
"enum": [
"邮件",
"企业微信",
"飞书",
"日志"
],
"type": "string",
"x-enum-varnames": [
"NotifierTypeSMTP",
"NotifierTypeWeChat",
"NotifierTypeLark",
"NotifierTypeLog"
],
"name": "notifier_type",
"in": "query"
},
{
"type": "string",
"name": "order_by",
"in": "query"
},
{
"type": "integer",
"name": "page",
"in": "query"
},
{
"type": "integer",
"name": "pageSize",
"in": "query"
},
{
"type": "string",
"name": "start_time",
"in": "query"
},
{
"enum": [
"发送成功",
"发送失败",
"已跳过"
],
"type": "string",
"x-enum-comments": {
"NotificationStatusFailed": "通知发送失败",
"NotificationStatusSkipped": "通知因某些原因被跳过(例如:用户未配置联系方式)",
"NotificationStatusSuccess": "通知已成功发送"
},
"x-enum-descriptions": [
"通知已成功发送",
"通知发送失败",
"通知因某些原因被跳过(例如:用户未配置联系方式)"
],
"x-enum-varnames": [
"NotificationStatusSuccess",
"NotificationStatusFailed",
"NotificationStatusSkipped"
],
"name": "status",
"in": "query"
},
{
"type": "integer",
"name": "user_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.ListNotificationResponse"
}
}
}
]
}
}
}
}
},
"/api/v1/monitor/pending-collections": { "/api/v1/monitor/pending-collections": {
"get": { "get": {
"security": [ "security": [
@@ -3452,7 +3595,7 @@
"BearerAuth": [] "BearerAuth": []
} }
], ],
"description": "获取所有计划的列表", "description": "获取所有计划的列表,支持按类型过滤和分页",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -3460,6 +3603,36 @@
"计划管理" "计划管理"
], ],
"summary": "获取计划列表", "summary": "获取计划列表",
"parameters": [
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页大小",
"name": "pageSize",
"in": "query"
},
{
"enum": [
"所有任务",
"自定义任务",
"系统任务"
],
"type": "string",
"x-enum-varnames": [
"PlanTypeFilterAll",
"PlanTypeFilterCustom",
"PlanTypeFilterSystem"
],
"description": "计划类型",
"name": "planType",
"in": "query"
}
],
"responses": { "responses": {
"200": { "200": {
"description": "业务码为200代表成功获取列表", "description": "业务码为200代表成功获取列表",
@@ -3472,10 +3645,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"data": { "data": {
"type": "array", "$ref": "#/definitions/dto.ListPlansResponse"
"items": {
"$ref": "#/definitions/dto.PlanResponse"
}
} }
} }
} }
@@ -3585,7 +3755,7 @@
"BearerAuth": [] "BearerAuth": []
} }
], ],
"description": "根据计划ID更新计划的详细信息。", "description": "根据计划ID更新计划的详细信息。系统计划不允许修改。",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -3641,7 +3811,7 @@
"BearerAuth": [] "BearerAuth": []
} }
], ],
"description": "根据计划ID删除计划。软删除", "description": "根据计划ID删除计划。软删除系统计划不允许删除。",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -3675,7 +3845,7 @@
"BearerAuth": [] "BearerAuth": []
} }
], ],
"description": "根据计划ID启动一个计划的执行。", "description": "根据计划ID启动一个计划的执行。系统计划不允许手动启动。",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -3709,7 +3879,7 @@
"BearerAuth": [] "BearerAuth": []
} }
], ],
"description": "根据计划ID停止一个正在执行的计划。", "description": "根据计划ID停止一个正在执行的计划。系统计划不能被停止。",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -3918,6 +4088,64 @@
} }
} }
} }
},
"/api/v1/users/{id}/notifications/test": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "为指定用户发送一条特定渠道的测试消息,以验证其配置是否正确。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"用户管理"
],
"summary": "发送测试通知",
"parameters": [
{
"type": "integer",
"description": "用户ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "请求体",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SendTestNotificationRequest"
}
}
],
"responses": {
"200": {
"description": "成功响应",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@@ -3948,6 +4176,7 @@
2001, 2001,
4000, 4000,
4001, 4001,
4003,
4004, 4004,
4009, 4009,
5000, 5000,
@@ -3957,6 +4186,7 @@
"CodeBadRequest": "请求参数错误", "CodeBadRequest": "请求参数错误",
"CodeConflict": "资源冲突", "CodeConflict": "资源冲突",
"CodeCreated": "创建成功", "CodeCreated": "创建成功",
"CodeForbidden": "禁止访问",
"CodeInternalError": "服务器内部错误", "CodeInternalError": "服务器内部错误",
"CodeNotFound": "资源未找到", "CodeNotFound": "资源未找到",
"CodeServiceUnavailable": "服务不可用", "CodeServiceUnavailable": "服务不可用",
@@ -3968,6 +4198,7 @@
"创建成功", "创建成功",
"请求参数错误", "请求参数错误",
"未授权", "未授权",
"禁止访问",
"资源未找到", "资源未找到",
"资源冲突", "资源冲突",
"服务器内部错误", "服务器内部错误",
@@ -3978,6 +4209,7 @@
"CodeCreated", "CodeCreated",
"CodeBadRequest", "CodeBadRequest",
"CodeUnauthorized", "CodeUnauthorized",
"CodeForbidden",
"CodeNotFound", "CodeNotFound",
"CodeConflict", "CodeConflict",
"CodeInternalError", "CodeInternalError",
@@ -4426,6 +4658,20 @@
} }
} }
}, },
"dto.ListNotificationResponse": {
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.NotificationDTO"
}
},
"pagination": {
"$ref": "#/definitions/dto.PaginationDTO"
}
}
},
"dto.ListPendingCollectionResponse": { "dto.ListPendingCollectionResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -4524,6 +4770,21 @@
} }
} }
}, },
"dto.ListPlansResponse": {
"type": "object",
"properties": {
"plans": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.PlanResponse"
}
},
"total": {
"type": "integer",
"example": 100
}
}
},
"dto.ListRawMaterialPurchaseResponse": { "dto.ListRawMaterialPurchaseResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -4734,6 +4995,47 @@
} }
} }
}, },
"dto.NotificationDTO": {
"type": "object",
"properties": {
"alarm_timestamp": {
"type": "string"
},
"created_at": {
"type": "string"
},
"error_message": {
"type": "string"
},
"id": {
"type": "integer"
},
"level": {
"$ref": "#/definitions/zapcore.Level"
},
"message": {
"type": "string"
},
"notifier_type": {
"$ref": "#/definitions/notify.NotifierType"
},
"status": {
"$ref": "#/definitions/models.NotificationStatus"
},
"title": {
"type": "string"
},
"to_address": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"user_id": {
"type": "integer"
}
}
},
"dto.PaginationDTO": { "dto.PaginationDTO": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -5178,6 +5480,9 @@
"plan_id": { "plan_id": {
"type": "integer" "type": "integer"
}, },
"plan_name": {
"type": "string"
},
"started_at": { "started_at": {
"type": "string" "type": "string"
}, },
@@ -5232,6 +5537,14 @@
"type": "string", "type": "string",
"example": "猪舍温度控制计划" "example": "猪舍温度控制计划"
}, },
"plan_type": {
"allOf": [
{
"$ref": "#/definitions/models.PlanType"
}
],
"example": "自定义任务"
},
"status": { "status": {
"allOf": [ "allOf": [
{ {
@@ -5586,6 +5899,22 @@
} }
} }
}, },
"dto.SendTestNotificationRequest": {
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"description": "Type 指定要测试的通知渠道",
"allOf": [
{
"$ref": "#/definitions/notify.NotifierType"
}
]
}
}
},
"dto.SensorDataDTO": { "dto.SensorDataDTO": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -6196,6 +6525,29 @@
"ReasonTypeHealthCare" "ReasonTypeHealthCare"
] ]
}, },
"models.NotificationStatus": {
"type": "string",
"enum": [
"发送成功",
"发送失败",
"已跳过"
],
"x-enum-comments": {
"NotificationStatusFailed": "通知发送失败",
"NotificationStatusSkipped": "通知因某些原因被跳过(例如:用户未配置联系方式)",
"NotificationStatusSuccess": "通知已成功发送"
},
"x-enum-descriptions": [
"通知已成功发送",
"通知发送失败",
"通知因某些原因被跳过(例如:用户未配置联系方式)"
],
"x-enum-varnames": [
"NotificationStatusSuccess",
"NotificationStatusFailed",
"NotificationStatusSkipped"
]
},
"models.PenStatus": { "models.PenStatus": {
"type": "string", "type": "string",
"enum": [ "enum": [
@@ -6437,6 +6789,17 @@
"PlanStatusFailed" "PlanStatusFailed"
] ]
}, },
"models.PlanType": {
"type": "string",
"enum": [
"自定义任务",
"系统任务"
],
"x-enum-varnames": [
"PlanTypeCustom",
"PlanTypeSystem"
]
},
"models.SensorType": { "models.SensorType": {
"type": "string", "type": "string",
"enum": [ "enum": [
@@ -6492,22 +6855,26 @@
"enum": [ "enum": [
"计划分析", "计划分析",
"等待", "等待",
"下料" "下料",
"全量采集"
], ],
"x-enum-comments": { "x-enum-comments": {
"TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务",
"TaskTypeFullCollection": "新增的全量采集任务",
"TaskTypeReleaseFeedWeight": "下料口释放指定重量任务", "TaskTypeReleaseFeedWeight": "下料口释放指定重量任务",
"TaskTypeWaiting": "等待任务" "TaskTypeWaiting": "等待任务"
}, },
"x-enum-descriptions": [ "x-enum-descriptions": [
"解析Plan的Task列表并添加到待执行队列的特殊任务", "解析Plan的Task列表并添加到待执行队列的特殊任务",
"等待任务", "等待任务",
"下料口释放指定重量任务" "下料口释放指定重量任务",
"新增的全量采集任务"
], ],
"x-enum-varnames": [ "x-enum-varnames": [
"TaskPlanAnalysis", "TaskPlanAnalysis",
"TaskTypeWaiting", "TaskTypeWaiting",
"TaskTypeReleaseFeedWeight" "TaskTypeReleaseFeedWeight",
"TaskTypeFullCollection"
] ]
}, },
"models.ValueDescriptor": { "models.ValueDescriptor": {
@@ -6525,6 +6892,64 @@
"$ref": "#/definitions/models.SensorType" "$ref": "#/definitions/models.SensorType"
} }
} }
},
"notify.NotifierType": {
"type": "string",
"enum": [
"邮件",
"企业微信",
"飞书",
"日志"
],
"x-enum-varnames": [
"NotifierTypeSMTP",
"NotifierTypeWeChat",
"NotifierTypeLark",
"NotifierTypeLog"
]
},
"repository.PlanTypeFilter": {
"type": "string",
"enum": [
"所有任务",
"自定义任务",
"系统任务"
],
"x-enum-varnames": [
"PlanTypeFilterAll",
"PlanTypeFilterCustom",
"PlanTypeFilterSystem"
]
},
"zapcore.Level": {
"type": "integer",
"format": "int32",
"enum": [
7,
-1,
0,
1,
2,
3,
4,
5,
-1,
5,
6
],
"x-enum-varnames": [
"_numLevels",
"DebugLevel",
"InfoLevel",
"WarnLevel",
"ErrorLevel",
"DPanicLevel",
"PanicLevel",
"FatalLevel",
"_minLevel",
"_maxLevel",
"InvalidLevel"
]
} }
}, },
"securityDefinitions": { "securityDefinitions": {

View File

@@ -121,6 +121,49 @@ import http from '../utils/http';
* @property {number} [operator_id] * @property {number} [operator_id]
*/ */
/**
* @typedef {('邮件'|'企业微信'|'飞书'|'日志')} NotifierType
*/
/**
* @typedef {('发送成功'|'发送失败'|'已跳过')} NotificationStatus
*/
/**
* @typedef {object} NotificationDTO
* @property {number} id
* @property {number} user_id
* @property {NotifierType} notifier_type
* @property {string} to_address
* @property {string} title
* @property {string} message
* @property {number} level - 日志级别, 见 ZapcoreLevel 枚举
* @property {string} alarm_timestamp
* @property {NotificationStatus} status
* @property {string} error_message
* @property {string} created_at
* @property {string} updated_at
*/
/**
* @typedef {object} ListNotificationResponse
* @property {Array<NotificationDTO>} list
* @property {PaginationDTO} pagination
*/
/**
* @typedef {object} NotificationsParams
* @property {number} [page]
* @property {number} [pageSize]
* @property {string} [order_by]
* @property {string} [start_time]
* @property {string} [end_time]
* @property {number} [level] - 日志级别, 见 ZapcoreLevel 枚举
* @property {NotifierType} [notifier_type]
* @property {NotificationStatus} [status]
* @property {number} [user_id]
*/
/** /**
* @typedef {('等待中'|'已完成'|'已超时')} PendingCollectionStatus * @typedef {('等待中'|'已完成'|'已超时')} PendingCollectionStatus
*/ */
@@ -606,6 +649,22 @@ import http from '../utils/http';
* @property {number} [operator_id] * @property {number} [operator_id]
*/ */
// --- Enums ---
/**
* 日志级别, 对应后端的 zapcore.Level
* @enum {number}
*/
export const ZapcoreLevel = {
Debug: -1,
Info: 0,
Warn: 1,
Error: 2,
DPanic: 3,
Panic: 4,
Fatal: 5,
Invalid: 6,
};
// --- Functions --- // --- Functions ---
@@ -647,6 +706,16 @@ export const getMedicationLogs = async (params) => {
return processResponse(responseData); return processResponse(responseData);
}; };
/**
* 批量查询通知
* @param {NotificationsParams} params - 查询参数
* @returns {Promise<{list: Array<NotificationDTO>, total: number}>}
*/
export const getNotifications = async (params) => {
const responseData = await http.get('/api/v1/monitor/notifications', { params });
return processResponse(responseData);
};
/** /**
* 获取待采集请求列表 * 获取待采集请求列表
* @param {PendingCollectionsParams} params - 查询参数 * @param {PendingCollectionsParams} params - 查询参数

View File

@@ -1,7 +1,7 @@
import http from '../utils/http'; import http from '../utils/http';
/** /**
* @typedef {('计划分析'|'等待'|'下料')} TaskType * @typedef {('计划分析'|'等待'|'下料'|'全量采集')} TaskType
*/ */
/** /**
@@ -67,6 +67,10 @@ import http from '../utils/http';
* @typedef {('子计划'|'任务')} PlanContentType * @typedef {('子计划'|'任务')} PlanContentType
*/ */
/**
* @typedef {('自定义任务'|'系统任务')} PlanType
*/
/** /**
* @typedef {object} PlanResponse * @typedef {object} PlanResponse
* @property {number} id * @property {number} id
@@ -78,17 +82,41 @@ import http from '../utils/http';
* @property {number} execute_count * @property {number} execute_count
* @property {PlanStatus} status * @property {PlanStatus} status
* @property {PlanContentType} content_type * @property {PlanContentType} content_type
* @property {PlanType} plan_type
* @property {Array<TaskResponse>} tasks * @property {Array<TaskResponse>} tasks
* @property {Array<SubPlanResponse>} sub_plans * @property {Array<SubPlanResponse>} sub_plans
*/ */
/**
* @typedef {object} ListPlansResponse
* @property {Array<PlanResponse>} plans
* @property {number} total
*/
/**
* @typedef {object} PlanExecutionLogDTO
* @property {string} created_at
* @property {string} ended_at
* @property {string} error
* @property {number} id
* @property {number} plan_id
* @property {string} plan_name
* @property {string} started_at
* @property {string} status
* @property {string} updated_at
*/
/** /**
* 获取所有计划的列表 * 获取所有计划的列表
* @returns {Promise<Array<PlanResponse>>} * @param {object} params - 查询参数
* @param {number} [params.page] - 页码
* @param {number} [params.pageSize] - 每页大小
* @param {('所有任务'|'自定义任务'|'系统任务')} [params.planType] - 计划类型
* @returns {Promise<ListPlansResponse>}
*/ */
const getPlans = () => { const getPlans = (params) => {
return http.get('/api/v1/plans'); return http.get('/api/v1/plans', { params });
}; };
/** /**
@@ -110,7 +138,7 @@ const getPlanById = (id) => {
}; };
/** /**
* 根据计划ID更新计划的详细信息 * 根据计划ID更新计划的详细信息。系统计划不允许修改。
* @param {number} id - 计划ID * @param {number} id - 计划ID
* @param {UpdatePlanRequest} planData - 更新后的计划信息 * @param {UpdatePlanRequest} planData - 更新后的计划信息
* @returns {Promise<PlanResponse>} * @returns {Promise<PlanResponse>}
@@ -120,7 +148,7 @@ const updatePlan = (id, planData) => {
}; };
/** /**
* 根据计划ID删除计划软删除 * 根据计划ID删除计划(软删除)系统计划不允许删除。
* @param {number} id - 计划ID * @param {number} id - 计划ID
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
@@ -129,7 +157,7 @@ const deletePlan = (id) => {
}; };
/** /**
* 根据计划ID启动一个计划的执行 * 根据计划ID启动一个计划的执行。系统计划不允许手动启动。
* @param {number} id - 计划ID * @param {number} id - 计划ID
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
@@ -138,7 +166,7 @@ const startPlan = (id) => {
}; };
/** /**
* 根据计划ID停止一个正在执行的计划 * 根据计划ID停止一个正在执行的计划。系统计划不能被停止。
* @param {number} id - 计划ID * @param {number} id - 计划ID
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */

View File

@@ -71,6 +71,15 @@ import http from '../utils/http';
* @property {string} [username] * @property {string} [username]
*/ */
/**
* @typedef {('邮件'|'企业微信'|'飞书'|'日志')} NotifierType
*/
/**
* @typedef {object} SendTestNotificationRequest
* @property {NotifierType} type - Type 指定要测试的通知渠道
*/
/** /**
* 创建一个新用户 * 创建一个新用户
* @param {CreateUserRequest} userData - 用户信息 * @param {CreateUserRequest} userData - 用户信息
@@ -99,8 +108,19 @@ const getUserHistory = (id, params) => {
return http.get(`/api/v1/users/${id}/history`, { params }); return http.get(`/api/v1/users/${id}/history`, { params });
}; };
/**
* 发送测试通知
* @param {number} id - 用户ID
* @param {SendTestNotificationRequest} data - 请求体
* @returns {Promise<string>}
*/
const sendTestNotification = (id, data) => {
return http.post(`/api/v1/users/${id}/notifications/test`, data);
};
export const UserApi = { export const UserApi = {
createUser, createUser,
login, login,
getUserHistory, getUserHistory,
sendTestNotification,
}; };

View File

@@ -20,9 +20,9 @@
<span>{{ plan.name }} - 内容</span> <span>{{ plan.name }} - 内容</span>
<div> <div>
<template v-if="!isSubPlan"> <template v-if="!isSubPlan">
<el-button class="button" type="primary" @click="savePlanContent" v-if="isEditingContent">保存</el-button> <el-button class="button" type="primary" @click="savePlanContent" v-if="isEditingContent" :disabled="plan.plan_type === '系统任务'">保存</el-button>
<el-button class="button" type="danger" @click="cancelEdit" v-if="isEditingContent">取消</el-button> <el-button class="button" type="danger" @click="cancelEdit" v-if="isEditingContent" :disabled="plan.plan_type === '系统任务'">取消</el-button>
<el-button class="button" @click="enterEditMode" v-else>编辑内容</el-button> <el-button class="button" @click="enterEditMode" v-else :disabled="plan.plan_type === '系统任务'">编辑内容</el-button>
</template> </template>
<!-- Dynamic Add Buttons --> <!-- Dynamic Add Buttons -->
@@ -32,12 +32,14 @@
type="primary" type="primary"
size="small" size="small"
@click="showAddSubPlanDialog" @click="showAddSubPlanDialog"
:disabled="plan.plan_type === '系统任务'"
>增加子计划</el-button> >增加子计划</el-button>
<el-button <el-button
v-if="plan.content_type === 'tasks' || !plan.content_type" v-if="plan.content_type === 'tasks' || !plan.content_type"
type="primary" type="primary"
size="small" size="small"
@click="showTaskEditorDialog()" @click="showTaskEditorDialog()"
:disabled="plan.plan_type === '系统任务'"
>增加子任务</el-button> >增加子任务</el-button>
</template> </template>
</div> </div>
@@ -61,8 +63,8 @@
延时: {{ task.parameters.delay_duration }} 延时: {{ task.parameters.delay_duration }}
</p> </p>
<el-button-group v-if="isEditingContent"> <el-button-group v-if="isEditingContent">
<el-button type="primary" size="small" @click="editTask(task)">编辑</el-button> <el-button type="primary" size="small" @click="editTask(task)" :disabled="plan.plan_type === '系统任务'">编辑</el-button>
<el-button type="danger" size="small" @click="deleteTask(task)">删除</el-button> <el-button type="danger" size="small" @click="deleteTask(task)" :disabled="plan.plan_type === '系统任务'">删除</el-button>
</el-button-group> </el-button-group>
</el-card> </el-card>
</el-timeline-item> </el-timeline-item>
@@ -80,7 +82,7 @@
<!-- Pass child_plan_id to recursive PlanDetail --> <!-- Pass child_plan_id to recursive PlanDetail -->
<plan-detail :plan-id="subPlan.child_plan_id" :is-sub-plan="true" /> <plan-detail :plan-id="subPlan.child_plan_id" :is-sub-plan="true" />
<el-button-group v-if="isEditingContent" class="sub-plan-actions"> <el-button-group v-if="isEditingContent" class="sub-plan-actions">
<el-button type="danger" size="small" @click="deleteSubPlan(subPlan)">删除</el-button> <el-button type="danger" size="small" @click="deleteSubPlan(subPlan)" :disabled="plan.plan_type === '系统任务'">删除</el-button>
</el-button-group> </el-button-group>
</div> </div>
</el-card> </el-card>
@@ -135,16 +137,16 @@
> >
<el-form :model="currentTaskForm" ref="taskFormRef" :rules="taskFormRules" label-width="100px"> <el-form :model="currentTaskForm" ref="taskFormRef" :rules="taskFormRules" label-width="100px">
<el-form-item label="任务类型" prop="type"> <el-form-item label="任务类型" prop="type">
<el-select v-model="currentTaskForm.type" placeholder="请选择任务类型" style="width: 100%;" :disabled="isEditingTask"> <el-select v-model="currentTaskForm.type" placeholder="请选择任务类型" style="width: 100%;" :disabled="isEditingTask || plan.plan_type === '系统任务'">
<!-- Only Delay Task for now --> <!-- Only Delay Task for now -->
<el-option label="延时任务" value="delay_task"></el-option> <el-option label="延时任务" value="delay_task"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="任务名称" prop="name"> <el-form-item label="任务名称" prop="name">
<el-input v-model="currentTaskForm.name" placeholder="请输入任务名称"></el-input> <el-input v-model="currentTaskForm.name" placeholder="请输入任务名称" :disabled="plan.plan_type === '系统任务'"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="任务描述" prop="description"> <el-form-item label="任务描述" prop="description">
<el-input type="textarea" v-model="currentTaskForm.description" placeholder="请输入任务描述"></el-input> <el-input type="textarea" v-model="currentTaskForm.description" placeholder="请输入任务描述" :disabled="plan.plan_type === '系统任务'"></el-input>
</el-form-item> </el-form-item>
<!-- Dynamic task component for specific parameters --> <!-- Dynamic task component for specific parameters -->
<template v-if="currentTaskForm.type === 'delay_task'"> <template v-if="currentTaskForm.type === 'delay_task'">
@@ -153,6 +155,7 @@
@update:parameters="val => currentTaskForm.parameters = val" @update:parameters="val => currentTaskForm.parameters = val"
prop-path="parameters.delay_duration" prop-path="parameters.delay_duration"
:is-editing="true" :is-editing="true"
:disabled="plan.plan_type === '系统任务'"
/> />
</template> </template>
<!-- More task types can be rendered here --> <!-- More task types can be rendered here -->
@@ -160,7 +163,7 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="taskEditorDialogVisible = false">取消</el-button> <el-button @click="taskEditorDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmTaskEdit">确定</el-button> <el-button type="primary" @click="confirmTaskEdit" :disabled="plan.plan_type === '系统任务'">确定</el-button>
</span> </span>
</template> </template>
</el-dialog> </el-dialog>
@@ -355,7 +358,7 @@ export default {
}, },
async fetchAvailablePlans() { async fetchAvailablePlans() {
try { try {
const response = await apiClient.plans.getPlans(); // 更正此处 const response = await apiClient.plans.getPlans({ planType: '自定义任务' });
this.availablePlans = response.data.plans.filter(p => this.availablePlans = response.data.plans.filter(p =>
p.id !== this.planId p.id !== this.planId
); );
@@ -483,7 +486,8 @@ export default {
} else { } else {
ElMessage.error('未找到要编辑的任务'); ElMessage.error('未找到要编辑的任务');
} }
} else { }
else {
// Add a new task // Add a new task
const newTask = { const newTask = {
id: Date.now(), id: Date.now(),

View File

@@ -153,8 +153,8 @@ export default {
}, },
onDestBatchChange(batchId) { onDestBatchChange(batchId) {
this.form.toPenID = null; // 重置目标猪栏选择 this.form.toPenID = null; // 重置目标猪栏选择
// 过滤出目标批次下,且未满的猪栏 // 允许选择目标批次下的所有猪栏,包括已满的
this.destinationPens = (this.pensByBatch[batchId] || []).filter(pen => pen.current_pig_count < pen.capacity); this.destinationPens = (this.pensByBatch[batchId] || []);
}, },
validateQuantity(rule, value, callback) { validateQuantity(rule, value, callback) {
if (value > this.maxTransferQuantity) { if (value > this.maxTransferQuantity) {

View File

@@ -77,6 +77,10 @@
<el-icon><FirstAidKit /></el-icon> <el-icon><FirstAidKit /></el-icon>
<template #title>用药记录</template> <template #title>用药记录</template>
</el-menu-item> </el-menu-item>
<el-menu-item index="/monitor/notifications">
<el-icon><Bell /></el-icon>
<template #title>通知记录</template>
</el-menu-item>
<el-menu-item index="/monitor/pending-collections"> <el-menu-item index="/monitor/pending-collections">
<el-icon><Clock /></el-icon> <el-icon><Clock /></el-icon>
<template #title>待采集请求</template> <template #title>待采集请求</template>
@@ -183,14 +187,14 @@ import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { import {
House, Monitor, Calendar, ArrowDown, Menu, Fold, Expand, Setting, Tickets, DataAnalysis, Document, Food, House, Monitor, Calendar, ArrowDown, Menu, Fold, Expand, Setting, Tickets, DataAnalysis, Document, Food,
FirstAidKit, Clock, Files, ShoppingCart, SoldOut, Warning, Switch, List, Shop, Coin, DataLine, Finished, User, ScaleToOriginal, OfficeBuilding, Management FirstAidKit, Clock, Files, ShoppingCart, SoldOut, Warning, Switch, List, Shop, Coin, DataLine, Finished, User, ScaleToOriginal, OfficeBuilding, Management, Bell
} from '@element-plus/icons-vue'; } from '@element-plus/icons-vue';
export default { export default {
name: 'MainLayout', name: 'MainLayout',
components: { components: {
House, Monitor, Calendar, ArrowDown, Menu, Fold, Expand, Setting, Tickets, DataAnalysis, Document, Food, House, Monitor, Calendar, ArrowDown, Menu, Fold, Expand, Setting, Tickets, DataAnalysis, Document, Food,
FirstAidKit, Clock, Files, ShoppingCart, SoldOut, Warning, Switch, List, Shop, Coin, DataLine, Finished, User, ScaleToOriginal, OfficeBuilding, Management FirstAidKit, Clock, Files, ShoppingCart, SoldOut, Warning, Switch, List, Shop, Coin, DataLine, Finished, User, ScaleToOriginal, OfficeBuilding, Management, Bell
}, },
setup() { setup() {
const route = useRoute(); const route = useRoute();
@@ -233,6 +237,7 @@ export default {
'/monitor/device-command-logs': '设备命令日志', '/monitor/device-command-logs': '设备命令日志',
'/monitor/feed-usage-records': '饲料使用记录', '/monitor/feed-usage-records': '饲料使用记录',
'/monitor/medication-logs': '用药记录', '/monitor/medication-logs': '用药记录',
'/monitor/notifications': '通知记录',
'/monitor/pending-collections': '待采集请求', '/monitor/pending-collections': '待采集请求',
'/monitor/pig-batch-logs': '猪批次日志', '/monitor/pig-batch-logs': '猪批次日志',
'/monitor/pig-purchases': '猪只采购记录', '/monitor/pig-purchases': '猪只采购记录',

View File

@@ -17,6 +17,7 @@ import PigBatchManagementView from './views/pms/PigBatchManagementView.vue'; //
import DeviceCommandLogView from './views/monitor/DeviceCommandLogView.vue'; import DeviceCommandLogView from './views/monitor/DeviceCommandLogView.vue';
import FeedUsageRecordsView from './views/monitor/FeedUsageRecordsView.vue'; import FeedUsageRecordsView from './views/monitor/FeedUsageRecordsView.vue';
import MedicationLogsView from './views/monitor/MedicationLogsView.vue'; import MedicationLogsView from './views/monitor/MedicationLogsView.vue';
import NotificationLogView from './views/monitor/NotificationLogView.vue';
import PendingCollectionsView from './views/monitor/PendingCollectionsView.vue'; import PendingCollectionsView from './views/monitor/PendingCollectionsView.vue';
import PigBatchLogsView from './views/monitor/PigBatchLogsView.vue'; import PigBatchLogsView from './views/monitor/PigBatchLogsView.vue';
import PigPurchasesView from './views/monitor/PigPurchasesView.vue'; import PigPurchasesView from './views/monitor/PigPurchasesView.vue';
@@ -50,6 +51,7 @@ const routes = [
{path: '/monitor/device-command-logs', component: DeviceCommandLogView, meta: {requiresAuth: true}}, {path: '/monitor/device-command-logs', component: DeviceCommandLogView, meta: {requiresAuth: true}},
{path: '/monitor/feed-usage-records', component: FeedUsageRecordsView, meta: {requiresAuth: true}}, {path: '/monitor/feed-usage-records', component: FeedUsageRecordsView, meta: {requiresAuth: true}},
{path: '/monitor/medication-logs', component: MedicationLogsView, meta: {requiresAuth: true}}, {path: '/monitor/medication-logs', component: MedicationLogsView, meta: {requiresAuth: true}},
{path: '/monitor/notifications', component: NotificationLogView, meta: {requiresAuth: true}},
{path: '/monitor/pending-collections', component: PendingCollectionsView, meta: {requiresAuth: true}}, {path: '/monitor/pending-collections', component: PendingCollectionsView, meta: {requiresAuth: true}},
{path: '/monitor/pig-batch-logs', component: PigBatchLogsView, meta: {requiresAuth: true}}, {path: '/monitor/pig-batch-logs', component: PigBatchLogsView, meta: {requiresAuth: true}},
{path: '/monitor/pig-purchases', component: PigPurchasesView, meta: {requiresAuth: true}}, {path: '/monitor/pig-purchases', component: PigPurchasesView, meta: {requiresAuth: true}},

View File

@@ -53,9 +53,7 @@ http.interceptors.response.use(
if (error.response.status === 401) { if (error.response.status === 401) {
// 清除token并重定向到登录页 // 清除token并重定向到登录页
localStorage.removeItem('jwt_token'); localStorage.removeItem('jwt_token');
// 这里需要访问router但http.js是纯工具文件不应直接依赖Vue Router实例 window.location.href = '/login';
// 可以在main.js的全局错误处理或组件中处理401错误
// 例如window.location.href = '/login';
} }
} else if (error.request) { } else if (error.request) {
// 请求发出但没有收到响应 // 请求发出但没有收到响应

View File

@@ -0,0 +1,111 @@
<template>
<div class="notification-log-view">
<GenericMonitorList
:fetchData="fetchNotifications"
:columnsConfig="notificationLogColumns"
/>
</div>
</template>
<script setup>
import GenericMonitorList from '../../components/GenericMonitorList.vue';
import { getNotifications, ZapcoreLevel } from '../../api/monitor.js';
import { formatRFC3339 } from '../../utils/format.js';
// 适配通用组件的 fetchData prop
const fetchNotifications = async (params) => {
return await getNotifications(params);
};
// 定义表格的列
const notificationLogColumns = [
{
title: '用户ID',
dataIndex: 'user_id',
key: 'user_id',
sorter: true,
filterType: 'number',
minWidth: 100,
},
{
title: '通知渠道',
dataIndex: 'notifier_type',
key: 'notifier_type',
filterType: 'select',
filterOptions: [
{ value: '邮件', text: '邮件' },
{ value: '企业微信', text: '企业微信' },
{ value: '飞书', text: '飞书' },
{ value: '日志', text: '日志' },
],
minWidth: 120,
},
{
title: '目标地址',
dataIndex: 'to_address',
key: 'to_address',
minWidth: 200,
},
{
title: '标题',
dataIndex: 'title',
key: 'title',
minWidth: 200,
},
{
title: '消息',
dataIndex: 'message',
key: 'message',
minWidth: 300,
},
{
title: '日志级别',
dataIndex: 'level',
key: 'level',
sorter: true,
filterType: 'select',
filterOptions: Object.entries(ZapcoreLevel).map(([text, value]) => ({ value, text })),
minWidth: 110,
},
{
title: '告警时间',
dataIndex: 'alarm_timestamp',
key: 'alarm_timestamp',
sorter: true,
formatter: (row, column, cellValue) => formatRFC3339(cellValue),
minWidth: 180,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
filterType: 'select',
filterOptions: [
{ value: '发送成功', text: '发送成功' },
{ value: '发送失败', text: '发送失败' },
{ value: '已跳过', text: '已跳过' },
],
minWidth: 120,
},
{
title: '错误信息',
dataIndex: 'error_message',
key: 'error_message',
minWidth: 200,
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
sorter: true,
formatter: (row, column, cellValue) => formatRFC3339(cellValue),
minWidth: 180,
},
];
</script>
<style scoped>
.notification-log-view {
/* 视图容器样式 */
}
</style>

View File

@@ -34,6 +34,12 @@ const planExecutionLogColumns = [
sorter: true, sorter: true,
minWidth: 120, minWidth: 120,
}, },
{
title: '计划名称',
dataIndex: 'plan_name',
key: 'plan_name',
minWidth: 150,
},
{ {
title: '状态', title: '状态',
dataIndex: 'status', dataIndex: 'status',

View File

@@ -9,8 +9,16 @@
<el-icon :size="20"><Refresh /></el-icon> <el-icon :size="20"><Refresh /></el-icon>
</el-button> </el-button>
</div> </div>
<div class="filter-and-add">
<el-select v-model="planTypeFilter" placeholder="选择计划类型" @change="loadPlans" style="width: 150px; margin-right: 10px;">
<el-option label="所有任务" value="所有任务"></el-option>
<el-option label="自定义任务" value="自定义任务"></el-option>
<el-option label="系统任务" value="系统任务"></el-option>
</el-select>
<el-button type="primary" @click="addPlan">添加计划</el-button> <el-button type="primary" @click="addPlan">添加计划</el-button>
</div> </div>
</div>
</template> </template>
<!-- 加载状态 --> <!-- 加载状态 -->
@@ -65,17 +73,18 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" width="280"> <el-table-column label="操作" width="280">
<template #default="scope"> <template #default="scope">
<el-button size="small" @click="editPlan(scope.row)">编辑</el-button> <el-button size="small" @click="editPlan(scope.row)" :disabled="scope.row.plan_type === '系统任务'">编辑</el-button>
<el-button size="small" @click="showDetails(scope.row)">详情</el-button> <el-button size="small" @click="showDetails(scope.row)">详情</el-button>
<el-button <el-button
size="small" size="small"
:type="scope.row.status === 1 ? 'warning' : 'primary'" :type="scope.row.status === 1 ? 'warning' : 'primary'"
@click="scope.row.status === 1 ? stopPlan(scope.row) : startPlan(scope.row)" @click="scope.row.status === 1 ? stopPlan(scope.row) : startPlan(scope.row)"
:loading="stoppingPlanId === scope.row.id || startingPlanId === scope.row.id" :loading="stoppingPlanId === scope.row.id || startingPlanId === scope.row.id"
:disabled="scope.row.plan_type === '系统任务'"
> >
{{ scope.row.status === 1 ? '停止' : '启动' }} {{ scope.row.status === 1 ? '停止' : '启动' }}
</el-button> </el-button>
<el-button size="small" type="danger" @click="deletePlan(scope.row)">删除</el-button> <el-button size="small" type="danger" @click="deletePlan(scope.row)" :disabled="scope.row.plan_type === '系统任务'">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -145,7 +154,8 @@ export default {
}, },
selectedPlanIdForDetails: null, // 当前要查看详情的计划ID selectedPlanIdForDetails: null, // 当前要查看详情的计划ID
startingPlanId: null, startingPlanId: null,
stoppingPlanId: null stoppingPlanId: null,
planTypeFilter: '自定义任务', // 新增:计划类型筛选,默认自定义任务
}; };
}, },
async mounted() { async mounted() {
@@ -158,7 +168,7 @@ export default {
this.error = null; this.error = null;
try { try {
const response = await apiClient.plans.getPlans(); // 更正此处 const response = await apiClient.plans.getPlans({ planType: this.planTypeFilter }); // 传递 planTypeFilter
let fetchedPlans = response.data?.plans || []; let fetchedPlans = response.data?.plans || [];
// Default sort by ID ascending // Default sort by ID ascending
fetchedPlans.sort((a, b) => a.id - b.id); fetchedPlans.sort((a, b) => a.id - b.id);
@@ -326,6 +336,10 @@ export default {
handlePlanCancel() { handlePlanCancel() {
this.dialogVisible = false; this.dialogVisible = false;
} }
},
watch: {
// 监听 planTypeFilter 变化,重新加载计划列表
planTypeFilter: 'loadPlans'
} }
}; };
</script> </script>
@@ -388,6 +402,11 @@ export default {
height: 20px; height: 20px;
} }
.filter-and-add {
display: flex;
align-items: center;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.plan-list { .plan-list {
padding: 10px; padding: 10px;