From 16434a0d8819369f07e243833c7b4b2ef6271112 Mon Sep 17 00:00:00 2001 From: lzh Date: Tue, 31 Mar 2026 22:54:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(@vben/web-antd):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=B7=A5=E7=89=8C=E5=87=BA=E5=85=A5=E8=BD=A8=E8=BF=B9=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antd/src/api/ops/trajectory/index.ts | 112 +++ .../web-antd/src/router/routes/modules/ops.ts | 10 + .../web-antd/src/views/ops/trajectory/data.ts | 165 +++++ .../src/views/ops/trajectory/index.vue | 697 ++++++++++++++++++ .../trajectory/modules/area-stay-chart.vue | 170 +++++ .../trajectory/modules/badge-status-card.vue | 218 ++++++ .../ops/trajectory/modules/stats-cards.vue | 154 ++++ .../modules/trajectory-timeline.vue | 272 +++++++ .../ops/trajectory/modules/trend-chart.vue | 180 +++++ 9 files changed, 1978 insertions(+) create mode 100644 apps/web-antd/src/api/ops/trajectory/index.ts create mode 100644 apps/web-antd/src/views/ops/trajectory/data.ts create mode 100644 apps/web-antd/src/views/ops/trajectory/index.vue create mode 100644 apps/web-antd/src/views/ops/trajectory/modules/area-stay-chart.vue create mode 100644 apps/web-antd/src/views/ops/trajectory/modules/badge-status-card.vue create mode 100644 apps/web-antd/src/views/ops/trajectory/modules/stats-cards.vue create mode 100644 apps/web-antd/src/views/ops/trajectory/modules/trajectory-timeline.vue create mode 100644 apps/web-antd/src/views/ops/trajectory/modules/trend-chart.vue diff --git a/apps/web-antd/src/api/ops/trajectory/index.ts b/apps/web-antd/src/api/ops/trajectory/index.ts new file mode 100644 index 000000000..44d10aff3 --- /dev/null +++ b/apps/web-antd/src/api/ops/trajectory/index.ts @@ -0,0 +1,112 @@ +import { requestClient } from '#/api/request'; + +// ==================== 类型定义 ==================== + +export namespace OpsTrajectoryApi { + /** 出入事件离开原因 */ + export type LeaveReason = 'AREA_SWITCH' | 'DEVICE_OFFLINE' | 'SIGNAL_LOSS'; + + /** 工牌设备下拉项 */ + export interface BadgeOption { + deviceId: number; + deviceKey: string; + nickname: string; + personName?: string; + } + + /** 轨迹分页查询参数 */ + export interface TrajectoryPageQuery { + deviceId?: number; + areaId?: number; + enterTime?: string[]; // [startTime, endTime] + pageNo?: number; + pageSize?: number; + } + + /** 轨迹记录 */ + export interface TrajectoryRecord { + id: number; + deviceId: number; + deviceName: string; + nickname: string; + areaId: number; + areaName: string; + buildingName?: string; + floorNo?: number; + beaconMac?: string; + enterTime: string; + leaveTime?: string; + durationSeconds?: number; + leaveReason?: LeaveReason; + enterRssi?: number; + } + + /** 时间线查询参数 */ + export interface TimelineQuery { + deviceId: number; + date: string; // yyyy-MM-dd + } + + /** 设备当前位置 */ + export interface CurrentLocation { + deviceId: number; + areaId?: number; + areaName?: string; + enterTime?: number; // 毫秒时间戳 + beaconMac?: string; + inArea: boolean; + } + + /** 轨迹统计摘要 */ + export interface TrajectorySummary { + totalEvents: number; + uniqueAreaCount: number; + onlineDurationSeconds: number; + firstOnlineTime?: string; + lastOnlineTime?: string; + } +} + +// ==================== API 接口 ==================== + +/** 获取工牌设备下拉列表 */ +export function getBadgeList(keyword?: string) { + return requestClient.get( + '/ops/trajectory/badge-list', + { params: keyword ? { keyword } : undefined }, + ); +} + +/** 分页查询轨迹记录 */ +export function getTrajectoryPage( + params: OpsTrajectoryApi.TrajectoryPageQuery, +) { + return requestClient.get<{ + list: OpsTrajectoryApi.TrajectoryRecord[]; + total: number; + }>('/ops/trajectory/page', { params }); +} + +/** 获取某设备某天的时间线 */ +export function getTrajectoryTimeline(params: OpsTrajectoryApi.TimelineQuery) { + return requestClient.get( + '/ops/trajectory/timeline', + { params }, + ); +} + +/** 获取设备实时位置 */ +export function getCurrentLocation(deviceId: number) { + return requestClient.get( + '/ops/trajectory/current-location', + { params: { deviceId } }, + ); +} + +/** 获取轨迹统计摘要 */ +export function getTrajectorySummary(deviceId: number, date: string) { + return requestClient.get( + '/ops/trajectory/summary', + { params: { deviceId, date } }, + ); +} diff --git a/apps/web-antd/src/router/routes/modules/ops.ts b/apps/web-antd/src/router/routes/modules/ops.ts index b8923ffb6..7ef108667 100644 --- a/apps/web-antd/src/router/routes/modules/ops.ts +++ b/apps/web-antd/src/router/routes/modules/ops.ts @@ -71,6 +71,16 @@ const routes: RouteRecordRaw[] = [ }, component: () => import('#/views/ops/work-order/dashboard/index.vue'), }, + // 工牌出入轨迹 + { + path: 'trajectory', + name: 'OpsTrajectory', + meta: { + title: '工牌出入轨迹', + activePath: '/ops/trajectory', + }, + component: () => import('#/views/ops/trajectory/index.vue'), + }, // 巡检记录 { path: 'inspection-record', diff --git a/apps/web-antd/src/views/ops/trajectory/data.ts b/apps/web-antd/src/views/ops/trajectory/data.ts new file mode 100644 index 000000000..ef354d734 --- /dev/null +++ b/apps/web-antd/src/views/ops/trajectory/data.ts @@ -0,0 +1,165 @@ +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +/** 事件类型:进入/离开 */ +export type EventType = 'ENTER' | 'LEAVE'; + +/** 离开原因文本映射 */ +export const LEAVE_REASON_TEXT: Record = { + SIGNAL_LOSS: '信号丢失', + AREA_SWITCH: '区域切换', + DEVICE_OFFLINE: '设备离线', +}; + +/** 离开原因颜色映射 */ +export const LEAVE_REASON_COLOR: Record = + { + SIGNAL_LOSS: { bg: '#FFF7E6', text: '#D46B08' }, + AREA_SWITCH: { bg: '#E6F4FF', text: '#1677FF' }, + DEVICE_OFFLINE: { bg: '#FFF1F0', text: '#CF1322' }, + }; + +/** 工牌状态颜色映射 */ +export const BADGE_STATUS_MAP: Record< + string, + { color: string; label: string } +> = { + BUSY: { color: '#52c41a', label: '作业中' }, + IDLE: { color: '#1677ff', label: '空闲' }, + OFFLINE: { color: '#bfbfbf', label: '离线' }, + PAUSED: { color: '#fa8c16', label: '暂停' }, +}; + +/** RSSI 信号强度等级 */ +export function getRssiLevel(rssi: null | number | undefined): { + color: string; + icon: string; + label: string; +} { + if (rssi === null || rssi === undefined) + return { + label: '无信号', + icon: 'solar:wifi-router-minimalistic-bold-duotone', + color: '#bfbfbf', + }; + if (rssi >= -60) + return { + label: '强', + icon: 'solar:wifi-router-minimalistic-bold-duotone', + color: '#52c41a', + }; + if (rssi >= -75) + return { + label: '良', + icon: 'solar:wifi-router-minimalistic-bold-duotone', + color: '#1677ff', + }; + if (rssi >= -85) + return { + label: '弱', + icon: 'solar:wifi-router-minimalistic-bold-duotone', + color: '#faad14', + }; + return { + label: '极弱', + icon: 'solar:wifi-router-minimalistic-bold-duotone', + color: '#ff4d4f', + }; +} + +/** 电量等级 */ +export function getBatteryLevel(level: null | number | undefined): { + color: string; + icon: string; +} { + if (level === null || level === undefined) + return { + icon: 'solar:battery-charge-minimalistic-bold-duotone', + color: '#bfbfbf', + }; + if (level >= 60) + return { + icon: 'solar:battery-full-minimalistic-bold-duotone', + color: '#52c41a', + }; + if (level >= 20) + return { + icon: 'solar:battery-low-minimalistic-bold-duotone', + color: '#faad14', + }; + return { + icon: 'solar:battery-low-minimalistic-bold-duotone', + color: '#ff4d4f', + }; +} + +/** 格式化停留时长(秒 → 可读文本) */ +export function formatDuration(seconds: null | number | undefined): string { + if (seconds === null || seconds === undefined || seconds <= 0) return '-'; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + if (h > 0) return `${h}h ${m}m`; + if (m > 0) return `${m}m ${s}s`; + return `${s}s`; +} + +/** 明细表列定义 */ +export function useTrajectoryColumns(): VxeTableGridOptions['columns'] { + return [ + { type: 'seq', width: 60, title: '序号' }, + { + field: 'areaName', + title: '区域名称', + minWidth: 160, + showOverflow: true, + }, + { + field: 'buildingName', + title: '楼栋', + width: 100, + showOverflow: true, + }, + { + field: 'floorNo', + title: '楼层', + width: 70, + align: 'center', + formatter({ cellValue }: { cellValue: null | number }) { + return cellValue === null ? '-' : `${cellValue}F`; + }, + }, + { + field: 'enterTime', + title: '进入时间', + width: 160, + formatter: 'formatDateTime', + }, + { + field: 'leaveTime', + title: '离开时间', + width: 160, + slots: { default: 'leaveTime' }, + }, + { + field: 'durationSeconds', + title: '停留时长', + width: 100, + align: 'center', + slots: { default: 'duration' }, + }, + { + field: 'leaveReason', + title: '离开原因', + width: 100, + align: 'center', + slots: { default: 'leaveReason' }, + }, + { + field: 'enterRssi', + title: '进入信号', + width: 90, + align: 'center', + slots: { default: 'rssi' }, + }, + ]; +} diff --git a/apps/web-antd/src/views/ops/trajectory/index.vue b/apps/web-antd/src/views/ops/trajectory/index.vue new file mode 100644 index 000000000..30db5eabe --- /dev/null +++ b/apps/web-antd/src/views/ops/trajectory/index.vue @@ -0,0 +1,697 @@ + + + + + diff --git a/apps/web-antd/src/views/ops/trajectory/modules/area-stay-chart.vue b/apps/web-antd/src/views/ops/trajectory/modules/area-stay-chart.vue new file mode 100644 index 000000000..530a6ad20 --- /dev/null +++ b/apps/web-antd/src/views/ops/trajectory/modules/area-stay-chart.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/apps/web-antd/src/views/ops/trajectory/modules/badge-status-card.vue b/apps/web-antd/src/views/ops/trajectory/modules/badge-status-card.vue new file mode 100644 index 000000000..d4ca0362f --- /dev/null +++ b/apps/web-antd/src/views/ops/trajectory/modules/badge-status-card.vue @@ -0,0 +1,218 @@ + + + + + diff --git a/apps/web-antd/src/views/ops/trajectory/modules/stats-cards.vue b/apps/web-antd/src/views/ops/trajectory/modules/stats-cards.vue new file mode 100644 index 000000000..6d464eb93 --- /dev/null +++ b/apps/web-antd/src/views/ops/trajectory/modules/stats-cards.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/apps/web-antd/src/views/ops/trajectory/modules/trajectory-timeline.vue b/apps/web-antd/src/views/ops/trajectory/modules/trajectory-timeline.vue new file mode 100644 index 000000000..1e8c43bd3 --- /dev/null +++ b/apps/web-antd/src/views/ops/trajectory/modules/trajectory-timeline.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/apps/web-antd/src/views/ops/trajectory/modules/trend-chart.vue b/apps/web-antd/src/views/ops/trajectory/modules/trend-chart.vue new file mode 100644 index 000000000..ac1940132 --- /dev/null +++ b/apps/web-antd/src/views/ops/trajectory/modules/trend-chart.vue @@ -0,0 +1,180 @@ + + + + +