Compare commits

...

6 Commits

Author SHA1 Message Date
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
d5f91b9b12 跨群调栏 2025-10-23 18:41:08 +08:00
a8d9b033f3 优化展示 2025-10-23 18:27:11 +08:00
10 changed files with 822 additions and 21 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": [
@@ -3918,6 +4061,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": {
@@ -4426,6 +4627,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": {
@@ -4734,6 +4949,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": {
@@ -5586,6 +5842,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 +6468,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": [
@@ -6525,6 +6820,51 @@
"$ref": "#/definitions/models.SensorType" "$ref": "#/definitions/models.SensorType"
} }
} }
},
"notify.NotifierType": {
"type": "string",
"enum": [
"邮件",
"企业微信",
"飞书",
"日志"
],
"x-enum-varnames": [
"NotifierTypeSMTP",
"NotifierTypeWeChat",
"NotifierTypeLark",
"NotifierTypeLog"
]
},
"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

@@ -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

@@ -29,20 +29,35 @@
</div> </div>
</div> </div>
<div class="batch-actions"> <div class="batch-actions">
<el-button size="small" type="primary" @click.stop="showAddPenDialog(batch)" :disabled="!batch.is_active"> <el-dropdown trigger="click" class="batch-dropdown">
增加猪栏 <el-button type="primary" size="small">
</el-button> 管理猪群<el-icon class="el-icon--right"><arrow-down /></el-icon>
<el-button </el-button>
size="small" <template #dropdown>
type="success" <el-dropdown-menu>
@click.stop="emitTransferPigs(batch)" <el-dropdown-item @click="showAddPenDialog(batch)" :disabled="!batch.is_active">增加猪栏</el-dropdown-item>
:disabled="!batch.is_active || !batch.pens || batch.pens.length < 2" <el-dropdown-item @click="emitEditBatch(batch)">编辑</el-dropdown-item>
title="将猪从一个猪栏转移到同一批次的另一个猪栏" <el-dropdown-item @click="emitDeleteBatch(batch)" divided>删除</el-dropdown-item>
> </el-dropdown-menu>
群内调栏 </template>
</el-button> </el-dropdown>
<el-button size="small" @click.stop="emitEditBatch(batch)">编辑</el-button> <el-dropdown trigger="click" class="batch-dropdown">
<el-button size="small" type="danger" @click.stop="emitDeleteBatch(batch)">删除</el-button> <el-button type="success" size="small">
调栏<el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
@click="emitTransferPigs(batch)"
:disabled="!batch.is_active || !batch.pens || batch.pens.length < 2"
>群内调栏</el-dropdown-item>
<el-dropdown-item
@click="emitTransferPigsAcrossBatches(batch)"
:disabled="!batch.is_active || !batch.pens || batch.pens.length === 0"
>跨群调栏</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div> </div>
</div> </div>
<div v-if="batch.isExpanded" class="batch-content"> <div v-if="batch.isExpanded" class="batch-content">
@@ -96,12 +111,14 @@ import PigBatchPenCard from './PigBatchPenCard.vue';
import AllocatePigsDialog from './AllocatePigsDialog.vue'; import AllocatePigsDialog from './AllocatePigsDialog.vue';
import {getAllPens, getAllPigHouses, movePigsIntoPen} from '../api/pigBatch'; import {getAllPens, getAllPigHouses, movePigsIntoPen} from '../api/pigBatch';
import {formatRFC3339} from '../utils/format'; // 导入格式化函数 import {formatRFC3339} from '../utils/format'; // 导入格式化函数
import { ArrowDown } from '@element-plus/icons-vue'; // 导入 ArrowDown 图标
export default { export default {
name: 'PigBatchList', name: 'PigBatchList',
components: { components: {
PigBatchPenCard, PigBatchPenCard,
AllocatePigsDialog AllocatePigsDialog,
ArrowDown // 注册 ArrowDown 图标
}, },
props: { props: {
pigBatches: { pigBatches: {
@@ -109,7 +126,7 @@ export default {
required: true required: true
} }
}, },
emits: ['edit-batch', 'delete-batch', 'add-pen', 'remove-pen', 'assign-pen-to-batch', 'reload-data', 'transfer-pigs'], emits: ['edit-batch', 'delete-batch', 'add-pen', 'remove-pen', 'assign-pen-to-batch', 'reload-data', 'transfer-pigs', 'transfer-pigs-across-batches'],
data() { data() {
return { return {
addPenDialogVisible: false, addPenDialogVisible: false,
@@ -193,6 +210,9 @@ export default {
emitTransferPigs(batch) { emitTransferPigs(batch) {
this.$emit('transfer-pigs', batch); this.$emit('transfer-pigs', batch);
}, },
emitTransferPigsAcrossBatches(batch) {
this.$emit('transfer-pigs-across-batches', batch);
},
// 猪栏操作 // 猪栏操作
emitRemovePen(pen) { emitRemovePen(pen) {
this.$emit('remove-pen', pen); this.$emit('remove-pen', pen);
@@ -253,6 +273,10 @@ export default {
gap: 10px; gap: 10px;
} }
.batch-dropdown {
margin-left: 10px; /* 为下拉菜单添加左边距 */
}
.batch-content { .batch-content {
padding: 16px; padding: 16px;
border-top: 1px solid #eee; border-top: 1px solid #eee;

View File

@@ -0,0 +1,204 @@
<template>
<el-dialog
title="跨群调栏"
:model-value="visible"
@update:model-value="$emit('update:visible', $event)"
width="40%"
@close="resetForm"
>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item label="源猪群批次">
<span>{{ batch.batch_number }}</span>
</el-form-item>
<el-form-item label="源猪栏" prop="fromPenID">
<el-select v-model="form.fromPenID" placeholder="请选择源猪栏" style="width: 100%;">
<el-option
v-for="pen in sourcePens"
:key="pen.id"
:label="`${pen.pen_number} (存栏: ${pen.current_pig_count})`"
:value="pen.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="目标猪群批次" prop="destBatchID">
<el-select v-model="form.destBatchID" placeholder="请选择目标猪群" style="width: 100%;" @change="onDestBatchChange">
<el-option
v-for="b in availableBatches"
:key="b.id"
:label="b.batch_number"
:value="b.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="目标猪栏" prop="toPenID">
<el-select v-model="form.toPenID" placeholder="请选择目标猪栏" style="width: 100%;">
<el-option
v-for="pen in destinationPens"
:key="pen.id"
:label="`${pen.pen_number} (容量: ${pen.capacity})`"
:value="pen.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="调栏数量" prop="quantity">
<el-input-number v-model="form.quantity" :min="1" :max="maxTransferQuantity" />
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input v-model="form.remarks" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="$emit('update:visible', false)"> </el-button>
<el-button type="primary" @click="handleConfirm"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script>
import { getPigBatches, transferPigsAcrossBatches } from '@/api/pigBatch';
import { getPens } from '@/api/pen';
export default {
name: 'TransferPigsAcrossBatchesModal',
props: {
visible: {
type: Boolean,
required: true
},
batch: {
type: Object,
required: true // 源猪群批次信息
}
},
emits: ['update:visible', 'success'],
data() {
return {
form: {
fromPenID: null,
destBatchID: null,
toPenID: null,
quantity: 1,
remarks: ''
},
rules: {
fromPenID: [{ required: true, message: '请选择源猪栏', trigger: 'change' }],
destBatchID: [{ required: true, message: '请选择目标猪群批次', trigger: 'change' }],
toPenID: [{ required: true, message: '请选择目标猪栏', trigger: 'change' }],
quantity: [
{ required: true, message: '请输入调栏数量', trigger: 'blur' },
{ type: 'integer', message: '请输入整数', trigger: 'blur' },
{ validator: this.validateQuantity, trigger: 'blur' }
]
},
sourcePens: [], // 源猪群的猪栏列表
availableBatches: [], // 可用的目标猪群列表
destinationPens: [], // 目标猪群的猪栏列表
maxTransferQuantity: 1 // 最大可调栏数量
};
},
watch: {
visible(newVal) {
if (newVal) {
this.initData();
}
},
'form.fromPenID': function(newVal) {
if (newVal) {
const selectedPen = this.sourcePens.find(pen => pen.id === newVal);
this.maxTransferQuantity = selectedPen ? selectedPen.current_pig_count : 1;
// 如果当前数量大于最大值,重置数量
if (this.form.quantity > this.maxTransferQuantity) {
this.form.quantity = this.maxTransferQuantity;
}
}
}
},
methods: {
async initData() {
this.resetForm();
this.sourcePens = this.batch.pens.filter(pen => pen.current_pig_count > 0); // 过滤掉没有猪的猪栏
await this.fetchAvailableBatchesAndPens();
},
async fetchAvailableBatchesAndPens() {
try {
const [batchesResponse, pensResponse] = await Promise.all([
getPigBatches({ is_active: true }), // 获取所有活跃猪群
getPens() // 获取所有猪栏
]);
const allBatches = batchesResponse.data || [];
const allPens = pensResponse.data || [];
// 过滤掉源猪群自身,以及非活跃的猪群
this.availableBatches = allBatches.filter(b => b.id !== this.batch.id && b.is_active);
// 将所有猪栏按批次ID分组方便后续查找
const pensByBatch = allPens.reduce((acc, pen) => {
if (pen.pig_batch_id) {
if (!acc[pen.pig_batch_id]) {
acc[pen.pig_batch_id] = [];
}
acc[pen.pig_batch_id].push(pen);
}
return acc;
}, {});
this.pensByBatch = pensByBatch; // 存储起来,以便 onDestBatchChange 使用
} catch (error) {
console.error("Error fetching batches or pens:", error);
this.$message.error("获取猪群或猪栏信息失败");
}
},
onDestBatchChange(batchId) {
this.form.toPenID = null; // 重置目标猪栏选择
// 允许选择目标批次下的所有猪栏,包括已满的
this.destinationPens = (this.pensByBatch[batchId] || []);
},
validateQuantity(rule, value, callback) {
if (value > this.maxTransferQuantity) {
callback(new Error(`调栏数量不能超过源猪栏存栏数量 (${this.maxTransferQuantity})`));
} else {
callback();
}
},
handleConfirm() {
this.$refs.form.validate(async valid => {
if (valid) {
try {
await transferPigsAcrossBatches(this.batch.id, {
fromPenID: this.form.fromPenID,
destBatchID: this.form.destBatchID,
toPenID: this.form.toPenID,
quantity: this.form.quantity,
remarks: this.form.remarks
});
this.$message.success('跨群调栏成功');
this.$emit('success');
this.$emit('update:visible', false);
} catch (error) {
console.error('Error transferring pigs across batches:', error);
this.$message.error('跨群调栏失败: ' + (error.response?.data?.message || error.message || '未知错误'));
}
}
});
},
resetForm() {
this.$refs.form?.resetFields();
this.form.fromPenID = null;
this.form.destBatchID = null;
this.form.toPenID = null;
this.form.quantity = 1;
this.form.remarks = '';
this.sourcePens = [];
this.availableBatches = [];
this.destinationPens = [];
this.maxTransferQuantity = 1;
}
}
};
</script>
<style scoped>
</style>

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

@@ -43,6 +43,7 @@
@remove-pen="handleRemovePen" @remove-pen="handleRemovePen"
@assign-pen-to-batch="handleAssignPenToBatch" @assign-pen-to-batch="handleAssignPenToBatch"
@transfer-pigs="handleTransferPigs" @transfer-pigs="handleTransferPigs"
@transfer-pigs-across-batches="handleTransferPigsAcrossBatches"
@reload-data="loadData" @reload-data="loadData"
/> />
<el-empty v-else description="暂无数据" /> <el-empty v-else description="暂无数据" />
@@ -75,6 +76,13 @@
:batch="currentBatchForTransfer" :batch="currentBatchForTransfer"
@success="handleTransferSuccess" @success="handleTransferSuccess"
/> />
<!-- 跨群调栏对话框 -->
<TransferPigsAcrossBatchesModal
v-model:visible="transferAcrossBatchesDialogVisible"
:batch="currentBatchForTransferAcrossBatches"
@success="handleTransferAcrossBatchesSuccess"
/>
</div> </div>
</template> </template>
@@ -86,6 +94,7 @@ import PigBatchList from '@/components/PigBatchList.vue';
import PigBatchForm from '@/components/PigBatchForm.vue'; import PigBatchForm from '@/components/PigBatchForm.vue';
import PenForm from '@/components/PenForm.vue'; import PenForm from '@/components/PenForm.vue';
import TransferPigsModal from '@/components/TransferPigsModal.vue'; import TransferPigsModal from '@/components/TransferPigsModal.vue';
import TransferPigsAcrossBatchesModal from '@/components/TransferPigsAcrossBatchesModal.vue';
import { Refresh } from '@element-plus/icons-vue'; import { Refresh } from '@element-plus/icons-vue';
export default { export default {
@@ -95,6 +104,7 @@ export default {
PigBatchForm, PigBatchForm,
PenForm, PenForm,
TransferPigsModal, TransferPigsModal,
TransferPigsAcrossBatchesModal,
Refresh Refresh
}, },
data() { data() {
@@ -113,6 +123,9 @@ export default {
// 调栏模态框状态 // 调栏模态框状态
transferDialogVisible: false, transferDialogVisible: false,
currentBatchForTransfer: {}, currentBatchForTransfer: {},
// 跨群调栏模态框状态
transferAcrossBatchesDialogVisible: false,
currentBatchForTransferAcrossBatches: {},
// 辅助映射 // 辅助映射
houseMap: new Map(), // 用于猪栏显示猪舍名称 houseMap: new Map(), // 用于猪栏显示猪舍名称
}; };
@@ -222,6 +235,11 @@ export default {
} }
}, },
// --- 猪栏操作 (在猪群管理中) --- // --- 猪栏操作 (在猪群管理中) ---
handleAddPen(house) {
this.currentPen = { ...house }; // 修正这里应该是传入house对象而不是pen对象
this.isEditPen = false;
this.penDialogVisible = true;
},
handleModifyPigCountPen(pen) { handleModifyPigCountPen(pen) {
this.currentPen = { ...pen }; this.currentPen = { ...pen };
this.isEditPen = true; this.isEditPen = true;
@@ -287,6 +305,16 @@ export default {
handleTransferSuccess() { handleTransferSuccess() {
this.transferDialogVisible = false; this.transferDialogVisible = false;
this.loadData(); // 重新加载数据以反映变化 this.loadData(); // 重新加载数据以反映变化
},
handleTransferPigsAcrossBatches(batch) {
console.log('handleTransferPigsAcrossBatches called with batch:', batch);
this.currentBatchForTransferAcrossBatches = batch;
this.transferAcrossBatchesDialogVisible = true;
console.log('transferAcrossBatchesDialogVisible set to:', this.transferAcrossBatchesDialogVisible);
},
handleTransferAcrossBatchesSuccess() {
this.transferAcrossBatchesDialogVisible = false;
this.loadData(); // 重新加载数据以反映变化
} }
} }
} }