feat(alarm): 设备报警监控报警提示升级与警灯交互优化 - 报警提示升级:后端新增报警派单完成接口 POST /api/worker-app/alarm-dispatches/:id/complete(供共享电工APP确认完成,归属校验403/不存在404/已完成幂等,status置1并写completed_at);GET /api/device-alarms 的 summary 由 severeIds 改为 activeSevereIds(grade=1 且无已完成派单的报警id,倒序100条,派单完成自动移出即报警取消)并新增轻量接口 GET /api/device-alarms/active-severe;前端新建全局悬浮立体警灯组件 AlarmSirenLight(红蓝双色3D警灯,挂载主布局所有页面可见,点击跳转报警监控页,30秒轮询activeSevereIds新id即时响+每3分钟重复响,Web Audio合成警笛700/900Hz交替,浏览器自动播放静默降级);index.vue 移除原10秒指示灯与880Hz beep、瘦身为纯列表页;警灯交互优化:警灯可鼠标拖动(位移>5px判为拖动不触发跳转)、位置经localStorage跨刷新记忆(默认右上角)、取消「重置报警提示」按钮;后端tsc与前端vue-tsc编译通过,接口实测派单完成排除逻辑生效,浏览器端到端验证通过(全局显示、点击跳转、拖动、刷新记忆、重置按钮无残留)

This commit is contained in:
wk authored and trae-solo-agent committed 2026-10-24 18:40:05 +08:00
1 parent 7ff2405634
commit 2d55947762
13 files changed
+676 -115

No files matched your search

@@ -58,10 +58,13 @@ export class DeviceAlarmController {
const d = new Date(new Date().getTime() + 8 * 60 * 60 * 1000);
const today = `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
const todayStart = new Date(`${today}T00:00:00+08:00`);
// 统计:今日报警数(alarm_time >= 当天0点)/ 严重报警数(grade=1)
const [todayCount, gradeOneCount] = await Promise.all([
this.alarmRepo.count({ where: { lineId: { [Op.ne]: null }, alarmTime: { [Op.gte]: todayStart } } }),
this.alarmRepo.count({ where: { lineId: { [Op.ne]: null }, alarmGrade: 1 } })
]);
// 活跃严重报警 id 集合(grade=1 且无已完成派单,按报警时间倒序取最近 100 条,供前端全局报警灯/声音触发,不受分页筛选影响)
const activeSevereIds = await this.getActiveSevereIds();
const dispatchedAlarms = await this.dispatchRepo.findAll({ where: { status: 0 }, attributes: ["alarmId"], raw: true });
const dispatchedIds = [...new Set(dispatchedAlarms.map((r: any) => Number(r.alarmId)))];
const undispatchedCount = await this.alarmRepo.count({
@@ -79,11 +82,40 @@ export class DeviceAlarmController {
total: count,
page,
pageSize,
summary: { todayCount, gradeOneCount, undispatchedCount }
summary: { todayCount, gradeOneCount, undispatchedCount, activeSevereIds }
}
};
}
/** 活跃严重报警 id 轻量接口(供全局悬浮警灯轮询,rbac 前缀权限 alarm:list 已覆盖) */
@Get("/active-severe") async activeSevere() {
const activeSevereIds = await this.getActiveSevereIds();
return {
code: 200,
message: "获取活跃严重报警成功",
data: { activeSevereIds }
};
}
// 活跃严重报警 id 集合:grade=1、lineId 非空、且无 status=1 已完成派单的报警,
// 按报警时间倒序取最近 100 条,供前端全局报警灯/声音触发,不受分页筛选影响
private async getActiveSevereIds(): Promise<number[]> {
const completedAlarms = await this.dispatchRepo.findAll({ where: { status: 1 }, attributes: ["alarmId"], raw: true });
const completedAlarmIds = [...new Set(completedAlarms.map((r: any) => Number(r.alarmId)))];
const activeSevereAlarms = await this.alarmRepo.findAll({
where: {
lineId: { [Op.ne]: null },
alarmGrade: 1,
id: completedAlarmIds.length ? { [Op.notIn]: completedAlarmIds } : { [Op.ne]: null }
},
attributes: ["id"],
order: [["alarmTime", "DESC"]],
limit: 100,
raw: true
});
return activeSevereAlarms.map((r: any) => Number(r.id));
}
/** 发起派单:校验报警/未完成派单/电工,生成派单号,写入派单并给电工发站内通知 */
@Post("/:id/dispatch") async dispatch(@Param("id") id: string, @Body() body: any) {
const alarmId = Number(id);
@@ -150,6 +150,12 @@ export class WorkerAppController {
return this.workerAppService.completeOrder(this.currentWorkerId(), source, Number(id));
}
// 报警派单完成确认
@Post("/alarm-dispatches/:id/complete")
async completeAlarmDispatch(@Param("id") id: string) {
return this.workerAppService.completeAlarmDispatch(this.currentWorkerId(), Number(id));
}
// 勘察明细维护(仅勘察单):全量替换,含建筑/楼层/房间与现场照片
@Put("/orders/:source/:id/details")
async saveDetails(@Param("source") source: string, @Param("id") id: string, @Body() body: any) {
+17
View File
@@ -8,6 +8,7 @@ import WorkerCert from "../entity/worker_cert.entity";
import SysNotice from "../entity/sys_notice.entity";
import RepairRequest from "../entity/repair_request.entity";
import RepairDispatch from "../entity/repair_dispatch.entity";
import AlarmDispatch from "../entity/alarm_dispatch.entity";
import InstallOrder from "../entity/install_order.entity";
import MaintenanceOrder from "../entity/maintenance_order.entity";
import SurveyOrder from "../entity/survey_order.entity";
@@ -63,6 +64,9 @@ export class WorkerAppService {
@InjectRepository(RepairDispatch)
dispatchModel!: Repository<RepairDispatch>;
@InjectRepository(AlarmDispatch)
alarmDispatchModel!: Repository<AlarmDispatch>;
@InjectRepository(InstallOrder)
installModel!: Repository<InstallOrder>;
@@ -503,6 +507,19 @@ export class WorkerAppService {
return { code: 200, message: "已完成" };
}
// 报警派单完成确认:校验归属(不属于当前电工返回 403),幂等(已处理不重复更新)
async completeAlarmDispatch(workerId: number, id: number): Promise<any> {
const dispatch = await this.alarmDispatchModel.findByPk(id);
if (!dispatch) return { code: 404, message: "派单不存在" };
if (Number(dispatch.workerId) !== Number(workerId)) return { code: 403, message: "无权操作该派单" };
if (Number(dispatch.status) === 1) return { code: 200, message: "该派单已完成" };
await this.sequelize.transaction(async t => {
await dispatch.update({ status: 1, completedAt: new Date() }, { transaction: t });
});
return { code: 200, message: "报警派单完成确认成功" };
}
// ================= 收益汇总 =================
// 收益汇总:累计/可提现/本月/近 7 日趋势/最近 20 条流水