feat(line): 智电改造线路台账新增「线路数据」弹窗展示最近一次模拟量快照 - 后端新增GET /api/electricity-data/latest(按lineId取最大collect_time的全部模拟量行,RBAC device:list);前端electricityDataApi.getLatestByLine与ElectricityLatestSnapshot类型;台账页表格新增固定右侧「操作」列与「线路数据」按钮,点击弹出720px弹窗显示线路信息(线路/编码/采集时间)及模拟量表格(模拟量名称/类型/数值/单位),无数据时显示空态提示

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
This commit is contained in:
wkandClaude Sonnet 4.6 noreply@anthropic.com authored and trae-solo-agent committed 2026-10-24 15:13:56 +08:00
1 parent c92fc7f298
commit c8a4330f3b
3 files changed
+153 -2

No files matched your search

+25
View File
@@ -272,4 +272,29 @@ export const electricGatewayApi = {
sync: () => sync: () =>
request.post<IccGatewaySyncResult>('/electric-gateways/sync'), request.post<IccGatewaySyncResult>('/electric-gateways/sync'),
};
// ==================== 线路用电(ICC 模拟量) ====================
export interface ElectricityAnalogRow {
analogType?: string;
analogName?: string;
analogUnit?: string;
analogValue?: number | string | null;
}
export interface ElectricityLatestSnapshot {
lineId: number;
lineCode?: string;
lineName?: string;
imei?: string;
customerId?: number;
collectTime?: string | null;
hasData: boolean;
list: ElectricityAnalogRow[];
}
export const electricityDataApi = {
getLatestByLine: (lineId: number) =>
request.get<ElectricityLatestSnapshot>('/electricity-data/latest', { params: { lineId } }),
}; };
+87 -1
View File
@@ -140,6 +140,11 @@
<template v-else-if="column.key === 'syncedAt'"> <template v-else-if="column.key === 'syncedAt'">
<span>{{ formatTime(record.syncedAt) }}</span> <span>{{ formatTime(record.syncedAt) }}</span>
</template> </template>
<template v-else-if="column.key === 'action'">
<a-button type="link" size="small" @click="handleViewData(record)">
线路数据
</a-button>
</template>
</template> </template>
<template #emptyText> <template #emptyText>
@@ -149,9 +154,31 @@
</div> </div>
</div> </div>
</div> </div>
<a-modal
v-model:open="analogModal.open"
:title="analogModal.title"
:width="720"
:footer="null"
>
<div v-if="analogModal.loading" class="modal-tip">加载中...</div>
<div v-else-if="!analogModal.snapshot || !analogModal.snapshot.hasData" class="modal-tip">暂无模拟量数据,可点击「同步线路」或等待定时同步后查看</div>
<template v-else>
<div class="modal-meta">
<span>线路:{{ analogModal.snapshot.lineName || '-' }}</span>
<span>编码:{{ analogModal.snapshot.lineCode || '-' }}</span>
<span>采集时间:{{ formatTime(analogModal.snapshot.collectTime) }}</span>
</div>
<a-table
:columns="analogColumns"
:data-source="analogModal.snapshot.list"
:pagination="false"
row-key="analogType"
size="small"
/>
</template>
</a-modal>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'; import { ref, reactive, onMounted, computed } from 'vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
@@ -162,8 +189,10 @@ import {
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { import {
electricLineApi, electricLineApi,
electricityDataApi,
type ElectricLine, type ElectricLine,
type ElectricLineQuery, type ElectricLineQuery,
type ElectricityLatestSnapshot,
} from '@/api/device'; } from '@/api/device';
import { customerApi, type CustomerInfo } from '@/api/customer'; import { customerApi, type CustomerInfo } from '@/api/customer';
import { useTableScroll } from '@/composables/useTableScroll'; import { useTableScroll } from '@/composables/useTableScroll';
@@ -172,6 +201,25 @@ const { wrapperRef, tableHeight } = useTableScroll();
const loading = ref(false); const loading = ref(false);
const syncing = ref(false); const syncing = ref(false);
const analogModal = reactive<{
open: boolean;
loading: boolean;
title: string;
snapshot: ElectricityLatestSnapshot | null;
}>({
open: false,
loading: false,
title: '线路模拟数据',
snapshot: null,
});
const analogColumns = [
{ title: '模拟量名称', dataIndex: 'analogName', key: 'analogName', ellipsis: true },
{ title: '类型', dataIndex: 'analogType', key: 'analogType', ellipsis: true, width: 100 },
{ title: '数值', dataIndex: 'analogValue', key: 'analogValue', align: 'center' as const, width: 120 },
{ title: '单位', dataIndex: 'analogUnit', key: 'analogUnit', align: 'center' as const, width: 80 },
];
const dataSource = ref<ElectricLine[]>([]); const dataSource = ref<ElectricLine[]>([]);
const total = ref(0); const total = ref(0);
@@ -197,6 +245,7 @@ const columns = [
{ title: '生产日期', dataIndex: 'produceDate', key: 'produceDate', ellipsis: true, width: 120 }, { title: '生产日期', dataIndex: 'produceDate', key: 'produceDate', ellipsis: true, width: 120 },
{ title: '状态', dataIndex: 'status', key: 'status', align: 'center' as const, width: 80 }, { title: '状态', dataIndex: 'status', key: 'status', align: 'center' as const, width: 80 },
{ title: '同步时间', dataIndex: 'syncedAt', key: 'syncedAt', ellipsis: true, width: 170 }, { title: '同步时间', dataIndex: 'syncedAt', key: 'syncedAt', ellipsis: true, width: 170 },
{ title: '操作', key: 'action', align: 'center' as const, fixed: 'right' as const, width: 100 },
]; ];
const pagination = computed(() => ({ const pagination = computed(() => ({
@@ -280,6 +329,25 @@ async function handleSync() {
} }
} }
async function handleViewData(record: ElectricLine) {
analogModal.open = true;
analogModal.loading = true;
analogModal.title = `线路数据 - ${record.lineName || record.lineCode}`;
analogModal.snapshot = null;
try {
const res: any = await electricityDataApi.getLatestByLine(record.id);
if (res.code === 200) {
analogModal.snapshot = res.data;
} else {
message.error(res.message || '获取模拟数据失败');
}
} catch (error) {
message.error('获取模拟数据失败,请稍后重试');
} finally {
analogModal.loading = false;
}
}
function formatTime(val?: string) { function formatTime(val?: string) {
if (!val) return '-'; if (!val) return '-';
const d = new Date(val); const d = new Date(val);
@@ -517,4 +585,22 @@ function formatTime(val?: string) {
background: #136bfb; background: #136bfb;
border-color: #136bfb; border-color: #136bfb;
} }
.modal-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 24px;
font-size: 13px;
color: #5b6b7e;
margin-bottom: 12px;
}
.modal-meta span {
font-weight: 600;
color: #0d1c2c;
}
.modal-tip {
padding: 40px 0;
text-align: center;
color: #5b6b7e;
}
</style> </style>
@@ -1,14 +1,54 @@
import { Controller, Post, Inject } from "@midwayjs/core"; import { Controller, Post, Get, Query, Inject } from "@midwayjs/core";
import { Context } from "@midwayjs/koa"; import { Context } from "@midwayjs/koa";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { IccAnalogSyncService } from "../service/icc_analog_sync.service"; import { IccAnalogSyncService } from "../service/icc_analog_sync.service";
import ElectricityData from "../entity/electricity_data.entity";
@Controller("/api/electricity-data") @Controller("/api/electricity-data")
export class ElectricityDataController { export class ElectricityDataController {
@Inject() ctx!: Context; @Inject() ctx!: Context;
@Inject() iccAnalogSyncService!: IccAnalogSyncService; @Inject() iccAnalogSyncService!: IccAnalogSyncService;
@InjectRepository(ElectricityData)
electricityDataRepo!: Repository<ElectricityData>;
@Post("/sync") async sync() { @Post("/sync") async sync() {
const result = await this.iccAnalogSyncService.syncFromIcc(this.ctx); const result = await this.iccAnalogSyncService.syncFromIcc(this.ctx);
return { code: 200, message: "模拟量数据同步完成", data: result }; return { code: 200, message: "模拟量数据同步完成", data: result };
} }
/** 查询某线路最近一次同步的模拟量快照(取最大 collect_time 的全部模拟量行) */
@Get("/latest") async latest(@Query("lineId") lineId?: string) {
const id = Number(lineId);
if (!id || Number.isNaN(id)) {
return { code: 400, message: "缺少有效的 lineId 参数" };
}
const latest = await this.electricityDataRepo.findOne({
where: { lineId: id },
order: [["collectTime", "DESC"]],
attributes: ["lineCode", "lineName", "imei", "customerId", "collectTime"],
});
if (!latest) {
return { code: 200, message: "该线路暂无模拟量数据", data: { lineId: id, hasData: false, collectTime: null, list: [] } };
}
const rows = await this.electricityDataRepo.findAll({
where: { lineId: id, collectTime: latest.getDataValue("collectTime") as Date },
order: [["analogType", "ASC"]],
attributes: ["analogType", "analogName", "analogUnit", "analogValue"],
});
return {
code: 200,
message: "ok",
data: {
lineId: id,
lineCode: latest.getDataValue("lineCode"),
lineName: latest.getDataValue("lineName"),
imei: latest.getDataValue("imei"),
customerId: latest.getDataValue("customerId"),
collectTime: latest.getDataValue("collectTime"),
hasData: true,
list: rows,
},
};
}
} }