Commit 2dcb746a4bad300319cf1a73e86f74836d80852b

Authored by xp.Huang
2 parents ae9c2018 5fa3f577

Merge branch 'main_dev' into 'main'

Main dev

See merge request yunteng/thingskit-front!1449
Showing 90 changed files with 3658 additions and 115 deletions

Too many changes to show.

To preserve performance only 90 of 163 files are displayed.

  1 +FROM nginx:latest
  2 +
  3 +COPY ./target/dist/ /usr/share/nginx/html/
  4 +COPY default.conf /etc/nginx/conf.d/default.conf
  5 +
  6 +EXPOSE 80
\ No newline at end of file
... ...
  1 +server {
  2 + listen 80;
  3 + listen [::]:80;
  4 + server_name 192.168.1.48;
  5 + #charset koi8-r;
  6 + #access_log /var/log/nginx/host.access.log main;
  7 +
  8 + root /usr/share/nginx/html;
  9 + index index.html index.htm;
  10 + try_files $uri $uri/ /index.html;
  11 +
  12 +
  13 +
  14 + location /api/ {
  15 + proxy_pass http://192.168.1.48:8080;
  16 + proxy_http_version 1.1;
  17 + proxy_set_header Upgrade $http_upgrade;
  18 + proxy_set_header Connection "Upgrade";
  19 + proxy_set_header Host $host;
  20 + proxy_cache_bypass $http_upgrade;
  21 + }
  22 +
  23 + location /yt/ {
  24 + proxy_pass http://192.168.1.48:8080;
  25 + proxy_http_version 1.1;
  26 + proxy_set_header Upgrade $http_upgrade;
  27 + proxy_set_header Connection "Upgrade";
  28 + proxy_set_header Host $host;
  29 + proxy_cache_bypass $http_upgrade;
  30 + }
  31 +
  32 + location /upload/ {
  33 + proxy_pass http://192.168.1.48:8080;
  34 + proxy_http_version 1.1;
  35 + proxy_set_header Upgrade $http_upgrade;
  36 + proxy_set_header Connection "Upgrade";
  37 + proxy_set_header Host $host;
  38 + proxy_cache_bypass $http_upgrade;
  39 + }
  40 +
  41 + #error_page 404 /404.html;
  42 +
  43 + # redirect server error pages to the static page /50x.html
  44 + #
  45 + error_page 500 502 503 504 /50x.html;
  46 + location = /50x.html {
  47 + root /usr/share/nginx/html;
  48 + }
  49 +}
\ No newline at end of file
... ...
... ... @@ -5,10 +5,13 @@ import { isString } from '/@/utils/is';
5 5 import { OrderByEnum } from '/@/views/device/localtion/cpns/TimePeriodForm/config';
6 6
7 7 // 获取设备配置,很多地方使用,不能修改这里
8   -export const getDeviceProfile = (deviceType?: string) => {
  8 +export const getDeviceProfile = (deviceType?: string, isSceneLinkage?: Boolean) => {
9 9 return defHttp.get<DeviceProfileModel[]>({
10 10 url: '/device_profile/me/list',
11   - params: { deviceType: isString(deviceType) ? deviceType : undefined },
  11 + params: {
  12 + deviceType: isString(deviceType) ? deviceType : undefined,
  13 + isSceneLinkage: isSceneLinkage ? isSceneLinkage : undefined,
  14 + },
12 15 });
13 16 };
14 17
... ...
... ... @@ -9,12 +9,16 @@ enum OrderType {
9 9 ASC = 'ASC',
10 10 DESC = 'DESC',
11 11 }
  12 +enum SortProperty {
  13 + CREATEtIME = 'createdTime',
  14 +}
12 15
13 16 export interface BaseQueryParams {
14 17 pageSize: number;
15 18 page: number;
16 19 orderFiled?: string;
17 20 orderType?: OrderType;
  21 + sortProperty?: SortProperty;
18 22 }
19 23
20 24 export class BaseQueryRequest implements BaseQueryParams {
... ...
... ... @@ -264,13 +264,16 @@ export const getGATEWAYdevice = async (params: {
264 264 );
265 265 };
266 266
267   -export const getGatewayDevice = (params: Record<'organizationId' | 'transportType', string>) => {
268   - const { organizationId, transportType } = params;
  267 +export const getGatewayDevice = (
  268 + params: Record<'organizationId' | 'transportType' | 'gatewayId', string>
  269 +) => {
  270 + const { organizationId, transportType, gatewayId } = params;
269 271 return defHttp.get<DeviceRecord[]>({
270 272 url: DeviceManagerApi.GATEWAY_DEVICE,
271 273 params: {
272 274 organizationId,
273 275 transportType,
  276 + gatewayId,
274 277 },
275 278 });
276 279 };
... ...
... ... @@ -124,6 +124,7 @@ export interface ProfileRecord {
124 124 image: string;
125 125 type: string;
126 126 default: boolean;
  127 + isEdge?: boolean;
127 128
128 129 checked?: boolean;
129 130 }
... ...
... ... @@ -7,6 +7,7 @@ export enum DeviceState {
7 7 INACTIVE = 'INACTIVE',
8 8 ONLINE = 'ONLINE',
9 9 OFFLINE = 'OFFLINE',
  10 + ACTIVE = 'ACTIVE',
10 11 }
11 12 export enum DeviceTypeEnum {
12 13 GATEWAY = 'GATEWAY',
... ...
... ... @@ -102,3 +102,9 @@ export interface VideoChanneControlType {
102 102 tbDeviceId?: string | number | object;
103 103 channelId?: string | number | object;
104 104 }
  105 +
  106 +export interface EzvizControlType {
  107 + entityId: string;
  108 + action: number;
  109 + controllingType: number;
  110 +}
... ...
... ... @@ -3,6 +3,7 @@ import {
3 3 VideoChannelPlayAddressType,
4 4 VideoChannelQueryParamsType,
5 5 VideoChanneControlType,
  6 + EzvizControlType,
6 7 } from './model/videoChannelModel';
7 8 import { PaginationResult } from '/#/axios';
8 9 import { defHttp } from '/@/utils/http/axios';
... ... @@ -12,6 +13,7 @@ enum Api {
12 13 GET_VIDEO_CONTROL_START = '/video/control/start',
13 14 GET_VIDEO_CONTROL_STOP = '/video/control/stop',
14 15 SET_VIDEO_CONTROL_CONTROL = '/video/control/control',
  16 + SET_EZVIZ_CONTROL = '/video/ezviz/controlling',
15 17 }
16 18
17 19 export const getVideoChannelList = (params: VideoChannelQueryParamsType) => {
... ... @@ -43,3 +45,10 @@ export const setVideoControl = (tbDeviceId, channelId, params: VideoChanneContro
43 45 params,
44 46 });
45 47 };
  48 +
  49 +export const setEzvizControl = (params: EzvizControlType) => {
  50 + return defHttp.get({
  51 + url: Api.SET_EZVIZ_CONTROL,
  52 + params,
  53 + });
  54 +};
... ...
  1 +import { EdgeInstanceItemType, QueryEdgeInstancePageParams } from './model/edgeInstance';
  2 +import { PaginationResult } from '/#/axios';
  3 +import { defHttp } from '/@/utils/http/axios';
  4 +
  5 +enum EdgeManageApi {
  6 + PAGE_LIST_GET = '/tenant/edgeInfos',
  7 + EDGE = '/edge',
  8 + EDGE_INFO = '/edge/info',
  9 + EDGE_EVENTS = '/events/EDGE',
  10 + EDGE_DEVICE_INFO = '/device/info',
  11 + EDGE_SYNC = '/edge/sync',
  12 + EDGE_DEVICE_DISTRIBUTION = '/tenant/deviceInfos',
  13 +}
  14 +
  15 +//分页
  16 +export const edgeInstancePage = (params: QueryEdgeInstancePageParams) => {
  17 + return defHttp.get<PaginationResult<EdgeInstanceItemType>>(
  18 + {
  19 + url: EdgeManageApi.PAGE_LIST_GET,
  20 + params,
  21 + },
  22 + {
  23 + joinPrefix: false,
  24 + }
  25 + );
  26 +};
  27 +
  28 +// 创建或编辑
  29 +export const createOrEditEdgeInstance = (data: EdgeInstanceItemType) => {
  30 + return defHttp.post<EdgeInstanceItemType>(
  31 + {
  32 + url: EdgeManageApi.EDGE,
  33 + data,
  34 + },
  35 + {
  36 + joinPrefix: false,
  37 + }
  38 + );
  39 +};
  40 +
  41 +// 删除
  42 +export const deleteEdgeInstance = (id: string) => {
  43 + return defHttp.delete(
  44 + {
  45 + url: `${EdgeManageApi.EDGE}/${id}`,
  46 + },
  47 + {
  48 + joinPrefix: false,
  49 + }
  50 + );
  51 +};
  52 +
  53 +// 详情
  54 +export const infoEdgeInstance = (id: string) => {
  55 + return defHttp.get(
  56 + {
  57 + url: `${EdgeManageApi.EDGE_INFO}/${id}`,
  58 + },
  59 + {
  60 + joinPrefix: false,
  61 + }
  62 + );
  63 +};
  64 +
  65 +// 边缘事件
  66 +export const edgeEventPage = (params: QueryEdgeInstancePageParams, id: string | undefined) => {
  67 + return defHttp.get(
  68 + {
  69 + url: `${EdgeManageApi.EDGE_EVENTS}/${id}`,
  70 + params,
  71 + },
  72 + {
  73 + joinPrefix: false,
  74 + }
  75 + );
  76 +};
  77 +
  78 +// 边缘设备
  79 +export const edgeDevicePage = (params: QueryEdgeInstancePageParams, id: string | undefined) => {
  80 + return defHttp.get(
  81 + {
  82 + url: `${EdgeManageApi.EDGE}/${id}/devices`,
  83 + params,
  84 + },
  85 + {
  86 + joinPrefix: false,
  87 + }
  88 + );
  89 +};
  90 +
  91 +// 边缘设备详情
  92 +export const infoEdgeDevice = (id: string) => {
  93 + return defHttp.get(
  94 + {
  95 + url: `${EdgeManageApi.EDGE_DEVICE_INFO}/${id}`,
  96 + },
  97 + {
  98 + joinPrefix: false,
  99 + }
  100 + );
  101 +};
  102 +
  103 +// 边缘同步
  104 +export const syncEdge = (id: string) => {
  105 + return defHttp.post(
  106 + {
  107 + url: `${EdgeManageApi.EDGE_SYNC}/${id}`,
  108 + },
  109 + {
  110 + joinPrefix: false,
  111 + }
  112 + );
  113 +};
  114 +
  115 +//设备分配查询
  116 +export const edgeDeviceDistributionPage = (params: QueryEdgeInstancePageParams) => {
  117 + return defHttp.get<PaginationResult<EdgeInstanceItemType>>(
  118 + {
  119 + url: EdgeManageApi.EDGE_DEVICE_DISTRIBUTION,
  120 + params,
  121 + },
  122 + {
  123 + joinPrefix: false,
  124 + }
  125 + );
  126 +};
  127 +
  128 +//设备分配给边缘
  129 +export const edgeDeviceDistribution = (edgeId: string | undefined, deviceId: string) => {
  130 + return defHttp.post<PaginationResult<EdgeInstanceItemType>>(
  131 + {
  132 + url: `${EdgeManageApi.EDGE}/${edgeId}/device/${deviceId}`,
  133 + data: {
  134 + headers: {
  135 + normalizedNames: {},
  136 + lazyUpdate: null,
  137 + },
  138 + params: {
  139 + updates: null,
  140 + cloneFrom: null,
  141 + encoder: {},
  142 + map: null,
  143 + interceptorConfig: {
  144 + ignoreLoading: false,
  145 + ignoreErrors: false,
  146 + resendRequest: false,
  147 + },
  148 + },
  149 + },
  150 + },
  151 + {
  152 + joinPrefix: false,
  153 + }
  154 + );
  155 +};
  156 +
  157 +//取消设备分配给边缘
  158 +export const edgeDeviceDeleteDistribution = (edgeId: string | undefined, deviceId: string) => {
  159 + return defHttp.delete<PaginationResult<EdgeInstanceItemType>>(
  160 + {
  161 + url: `${EdgeManageApi.EDGE}/${edgeId}/device/${deviceId}`,
  162 + },
  163 + {
  164 + joinPrefix: false,
  165 + }
  166 + );
  167 +};
... ...
  1 +import {
  2 + Configuration,
  3 + ProvisionConfiguration,
  4 + TransportConfiguration,
  5 +} from '../../device/model/deviceModel';
  6 +import { ModelOfMatterParams } from '../../device/model/modelOfMatterModel';
  7 +
  8 +export type QueryEdgeInstancePageParams = {
  9 + name?: string;
  10 + tenantId?: string;
  11 + textSearch?: string;
  12 + page: number;
  13 + pageSize: number;
  14 + sortOrder?: string;
  15 + sortProperty?: string;
  16 +};
  17 +
  18 +export interface EdgeInstanceItemType {
  19 + id?: {
  20 + entityType: string;
  21 + id: string;
  22 + };
  23 + createdTime?: number;
  24 + tenantId?: {
  25 + entityType: string;
  26 + id: string;
  27 + };
  28 + customerId?: {
  29 + entityType: string;
  30 + id: string;
  31 + };
  32 + rootRuleChainId?: {
  33 + entityType: string;
  34 + id: string;
  35 + };
  36 + name: string;
  37 + label: string;
  38 + additionalInfo: {
  39 + description: string;
  40 + };
  41 + status: number;
  42 + type: string;
  43 + routingKey: string;
  44 + secret: string;
  45 + active?: boolean;
  46 +}
  47 +
  48 +export interface ProfileData {
  49 + configuration: Configuration;
  50 + transportConfiguration: TransportConfiguration;
  51 + provisionConfiguration: ProvisionConfiguration;
  52 + alarms?: any;
  53 + thingsModel?: ModelOfMatterParams[];
  54 +}
  55 +
  56 +export interface EdgeDeviceItemType {
  57 + creator: string;
  58 + createTime: string;
  59 + codeType?: string;
  60 + code?: string;
  61 + name: string;
  62 + transportType: string;
  63 + provisionType: string;
  64 + deviceType: string;
  65 + deviceCount: number;
  66 + tbDeviceId: string;
  67 + tbProfileId: string;
  68 + defaultQueueName: string;
  69 + deviceInfo?: {
  70 + avatar: string;
  71 + };
  72 + image: string;
  73 + type: string;
  74 + default: boolean;
  75 + defaultRuleChainId: string;
  76 + profileId: string;
  77 + alias?: string;
  78 + brand?: string;
  79 + organizationId: string;
  80 + organizationDTO: {
  81 + name: string;
  82 + };
  83 + alarmStatus: number;
  84 + deviceProfile: {
  85 + default: boolean;
  86 + name: string;
  87 + transportType: string;
  88 + profileData: ProfileData;
  89 + };
  90 + customerAdditionalInfo?: {
  91 + isPublic?: boolean;
  92 + };
  93 + ifShowClass?: Boolean;
  94 + sip?: {
  95 + cameraCode: string;
  96 + localIp: string;
  97 + manufacturer: string;
  98 + streamMode: string;
  99 + };
  100 + customerName?: string;
  101 + gatewayId?: string;
  102 + id: {
  103 + entityType: string;
  104 + id: string;
  105 + };
  106 + createdTime: number;
  107 + additionalInfo: {
  108 + gateway: boolean;
  109 + overwriteActivityTime: boolean;
  110 + description: string;
  111 + };
  112 + tenantId: {
  113 + entityType: string;
  114 + id: string;
  115 + };
  116 + customerId: {
  117 + entityType: string;
  118 + id: string;
  119 + };
  120 + label: string;
  121 + deviceProfileId: {
  122 + entityType: string;
  123 + id: string;
  124 + };
  125 + deviceData: {
  126 + configuration: {
  127 + type: string;
  128 + };
  129 + transportConfiguration: {
  130 + type: string;
  131 + };
  132 + };
  133 + firmwareId: null;
  134 + softwareId: null;
  135 + externalId: null;
  136 + customerTitle: null;
  137 + customerIsPublic: boolean;
  138 + deviceProfileName: string;
  139 + active: boolean;
  140 + deviceState: string;
  141 + deviceToken?: string;
  142 + gatewayName?: string;
  143 + gatewayAlias?: string;
  144 + sn: string;
  145 +}
... ...
... ... @@ -3,8 +3,17 @@ import { FileUploadResponse } from '/@/api/oss/FileUploadResponse';
3 3
4 4 enum Api {
5 5 BaseUploadUrl = '/oss/upload',
  6 + BaseDeleteUrl = '/oss',
6 7 }
7 8
8 9 export const upload = (file) => {
9 10 return defHttp.post<FileUploadResponse>({ url: Api.BaseUploadUrl, params: file });
10 11 };
  12 +
  13 +export const deleteFilePath = (deleteFilePath?: string) => {
  14 + if (!deleteFilePath) return;
  15 + const deleteParams = `?deleteFilePath=${deleteFilePath}`;
  16 + return defHttp.delete({
  17 + url: `${Api.BaseDeleteUrl}${deleteParams}`,
  18 + });
  19 +};
... ...
... ... @@ -125,11 +125,16 @@ export const getAttribute = (orgId) => {
125 125 export const byOrganizationIdGetMasterDevice = (params: {
126 126 organizationId: string;
127 127 deviceProfileId?: string;
  128 + isSceneLinkage?: Boolean;
128 129 }) => {
129   - const { organizationId, deviceProfileId } = params;
  130 + const { organizationId, deviceProfileId, isSceneLinkage } = params;
130 131 return defHttp.get<DeviceModel[]>({
131 132 url: `${ScreenManagerApi.MASTER_GET_DEVICE}`,
132   - params: { deviceProfileId, organizationId },
  133 + params: {
  134 + deviceProfileId,
  135 + organizationId,
  136 + isSceneLinkage: isSceneLinkage ? isSceneLinkage : undefined,
  137 + },
133 138 });
134 139 };
135 140 //TODO-fengtao
... ...
  1 +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="15.933" viewBox="0 0 16 15.933"><defs><style>.a{fill:#343c4c;}</style></defs><path class="a" d="M345.333,337.067h-2.667a1.337,1.337,0,0,0-1.333,1.333v2.667a1.337,1.337,0,0,0,1.333,1.333h2.667a1.337,1.337,0,0,0,1.333-1.333V338.4A1.337,1.337,0,0,0,345.333,337.067Zm0,4h-2.667V338.4h2.667Z" transform="translate(-335.999 -331.8)"/><path class="a" d="M15.333,8.533a.667.667,0,1,0,0-1.333H14V5.867h1.333A.63.63,0,0,0,16,5.2a.63.63,0,0,0-.667-.667H14A2.7,2.7,0,0,0,11.333,2V.667A.667.667,0,1,0,10,.667V2H8.667V.667a.667.667,0,0,0-1.333,0V2H6V.667A.63.63,0,0,0,5.333,0a.668.668,0,0,0-.667.667V2A2.7,2.7,0,0,0,2,4.533H.667A.63.63,0,0,0,0,5.2a.63.63,0,0,0,.667.667H2V7.2H.667a.667.667,0,0,0,0,1.333H2V9.867H.667a.667.667,0,1,0,0,1.333H2v.133A2.675,2.675,0,0,0,4.667,14v1.267a.63.63,0,0,0,.667.667A.63.63,0,0,0,6,15.267V14H7.333v1.267a.667.667,0,1,0,1.333,0V14H10v1.267a.667.667,0,1,0,1.333,0V14A2.675,2.675,0,0,0,14,11.333V11.2h1.333a.667.667,0,0,0,0-1.333H14V8.533h1.333Zm-2.667,2.8a1.337,1.337,0,0,1-1.333,1.333H4.667a1.337,1.337,0,0,1-1.333-1.333V4.667A1.337,1.337,0,0,1,4.667,3.333h6.667a1.337,1.337,0,0,1,1.333,1.333v6.667Z"/></svg>
\ No newline at end of file
... ...
  1 +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="15.873" viewBox="0 0 16 15.873"><defs><style>.a{fill:#343c4c;}</style></defs><g transform="translate(-75.041 -99.03)"><path class="a" d="M90.95,111.459l-1.7-11.191,0-.014a1.344,1.344,0,0,0-1.276-1.225H78.36a1.356,1.356,0,0,0-1.236,1.2l0,.011-2.049,11.425.011,0a2.32,2.32,0,0,0-.042.446v.384a2.4,2.4,0,0,0,2.4,2.4h11.2a2.4,2.4,0,0,0,2.4-2.4v-.384a2.4,2.4,0,0,0-.09-.658ZM78.292,100.645c.1-.389.256-.424.344-.434h9.108c.223,0,.3.335.317.416l1.38,9.1a2.292,2.292,0,0,0-1.065-.263H77.706a2.29,2.29,0,0,0-1.046.252ZM89.936,112.4a1.378,1.378,0,0,1-1.415,1.339H77.56a1.378,1.378,0,0,1-1.415-1.339v-.356a1.378,1.378,0,0,1,1.415-1.339H88.522a1.412,1.412,0,0,1,1.34.907l.066.418h.008v.369h0Z"/><path class="a" d="M160.413,229.5H158.5a.48.48,0,1,0,0,.96h1.916a.48.48,0,0,0,0-.96Zm1.964-3.83h5.032a1.864,1.864,0,0,0,1.391-.587,2.344,2.344,0,0,0,.522-1.826,2.536,2.536,0,0,0-1.747-1.987,2.511,2.511,0,0,0-2.416-2.261,2.382,2.382,0,0,0-1.789.835,1.835,1.835,0,0,0-1.685,1.245,2.49,2.49,0,0,0-1.953,2.063,1.87,1.87,0,0,0,.415,1.428A3.433,3.433,0,0,0,162.378,225.673Zm-1.514-2.45c.094-.792.66-1.029,1.454-1.287l.267-.089.054-.261a.852.852,0,0,1,.794-.719.616.616,0,0,1,.121.011l.291.048.167-.24a1.354,1.354,0,0,1,1.12-.617,1.509,1.509,0,0,1,1.414,1.559l0,.293.3.132c.7.31,1.293.614,1.352,1.244a1.34,1.34,0,0,1-.261,1.045.826.826,0,0,1-.652.267h-4.739a2.362,2.362,0,0,1-1.478-.708A.86.86,0,0,1,160.864,223.223Z" transform="translate(-81.382 -117.677)"/></g></svg>
\ No newline at end of file
... ...
  1 +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1722937185750" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5222" id="mx_n_1722937185750" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M0 0v1024a1024 1024 0 0 1 1024-1024H0z" p-id="5223" fill="#1890ff"></path></svg>
... ...
  1 +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:#343c4c;}</style></defs><g transform="translate(-562 -613)"><rect class="a" width="16" height="16" transform="translate(562 613)"/><g transform="translate(563.294 613)"><path class="b" d="M148.64,80.44H138.412a1.594,1.594,0,0,1-1.592-1.592V66.032a1.594,1.594,0,0,1,1.592-1.592h6.36a1.567,1.567,0,0,1,1.132.487l3.9,4.115a1.554,1.554,0,0,1,.428,1.073v8.733a1.594,1.594,0,0,1-1.592,1.592ZM138.412,65.714a.319.319,0,0,0-.318.318V78.848a.319.319,0,0,0,.318.318H148.64a.319.319,0,0,0,.318-.318V70.115a.285.285,0,0,0-.079-.2l-3.9-4.115a.287.287,0,0,0-.208-.089Z" transform="translate(-136.82 -64.44)"/><path class="b" d="M310.723,460.845h-6.046a.7.7,0,0,1-.7-.7v-5.04a.7.7,0,0,1,.7-.7h6.046a.7.7,0,0,1,.7.7v5.04a.7.7,0,0,1-.7.7Zm-5.787-.955h5.53v-4.524h-5.53Z" transform="translate(-300.992 -447.439)"/><path class="b" d="M426.518,592.369a.478.478,0,0,1-.478-.478v-2.774a.478.478,0,0,1,.955,0v2.774A.477.477,0,0,1,426.518,592.369Zm2.378,0a.478.478,0,0,1-.478-.478v-2.774a.478.478,0,0,1,.955,0v2.774A.478.478,0,0,1,428.9,592.369Z" transform="translate(-420.87 -579.27)"/></g></g></svg>
\ No newline at end of file
... ...
... ... @@ -71,10 +71,10 @@
71 71 if (!value) {
72 72 return true;
73 73 }
74   - let { name } = props || {};
  74 + let { title } = props || {};
75 75 value = value.toLowerCase();
76   - name = name.toLowerCase();
77   - return name.includes(value);
  76 + title = title.toLowerCase();
  77 + return title.includes(value);
78 78 }
79 79
80 80 async function fetch() {
... ...
... ... @@ -21,7 +21,7 @@
21 21 }
22 22
23 23 const { prefixCls } = useDesign('api-upload');
24   - const emit = defineEmits(['update:fileList', 'preview', 'download']);
  24 + const emit = defineEmits(['update:fileList', 'preview', 'download', 'delete']);
25 25
26 26 const { createMessage } = useMessage();
27 27
... ... @@ -71,9 +71,12 @@
71 71 return false;
72 72 }
73 73 }
74   -
75 74 if (file.size > props.maxSize) {
76   - createMessage.warning(`文件大小超过${Math.floor(props.maxSize / 1024 / 1024)}mb`);
  75 + createMessage.warning(
  76 + `文件大小超过${Math.floor(
  77 + props.maxSize > 1024 * 1024 ? props.maxSize / 1024 / 1024 : props.maxSize / 1024
  78 + )}${props.maxSize > 1024 * 1024 * 1 ? 'mb' : 'kb'}`
  79 + );
77 80 return false;
78 81 }
79 82 handleUpload(file);
... ... @@ -129,6 +132,7 @@
129 132 const index = _fileList.findIndex((item) => item.uid === file.uid);
130 133 ~index && _fileList.splice(index, 1);
131 134 emit('update:fileList', _fileList);
  135 + emit('delete', file.url);
132 136 };
133 137
134 138 const handlePreview = (file: FileItem) => {
... ...
... ... @@ -136,7 +136,8 @@ export interface FormSchema {
136 136 helpMessage?:
137 137 | string
138 138 | string[]
139   - | ((renderCallbackParams: RenderCallbackParams) => string | string[]);
  139 + | ((renderCallbackParams: RenderCallbackParams) => string | string[])
  140 + | Boolean;
140 141 // BaseHelp component props
141 142 helpComponentProps?: Partial<HelpComponentProps>;
142 143 // Label width, if it is passed, the labelCol and WrapperCol configured by itemProps will be invalid
... ...
... ... @@ -59,7 +59,7 @@
59 59 </span>
60 60 </Menu.Item>
61 61 <Menu.Item :disabled="item.disabled" v-if="item.popconfirm">
62   - <Popconfirm v-bind="item.popconfirm">
  62 + <Popconfirm :disabled="item.disabled" v-bind="item.popconfirm">
63 63 <template v-if="item.popconfirm.icon" #icon>
64 64 <Icon :icon="item.popconfirm.icon" />
65 65 </template>
... ...
... ... @@ -4,6 +4,7 @@ const menuMap = new Map();
4 4
5 5 menuMap.set('/visual/board/detail/:boardId/:boardName/:platform/:organizationId?', '/visual/board');
6 6 menuMap.set('/rule/chain/:id', '/rule/chain');
  7 +menuMap.set('/edge/edge_detail/:id', '/edge');
7 8
8 9 export const useMenuActiveFix = (route: RouteLocationNormalizedLoaded) => {
9 10 let flag = false;
... ...
... ... @@ -2,10 +2,12 @@
2 2 <Dropdown placement="bottomLeft" :overlayClassName="`${prefixCls}-dropdown-overlay`">
3 3 <span :class="[prefixCls, `${prefixCls}--${theme}`]" class="flex">
4 4 <img :class="`${prefixCls}__header`" :src="getUserInfo.avatar" />
5   - <span :class="`${prefixCls}__info hidden md:block`">
6   - <span :class="`${prefixCls}__name `" class="truncate">
7   - {{ getUserInfo.realName }}
8   - </span>
  5 + <span :class="`${prefixCls}__info hidden md:block truncate`">
  6 + <Tooltip :trigger="['click']" :title="getUserInfo.realName">
  7 + <span :class="`${prefixCls}__name `" class="truncate">
  8 + {{ getUserInfo.realName?.slice(0, 10) }}
  9 + </span>
  10 + </Tooltip>
9 11 </span>
10 12 </span>
11 13
... ... @@ -53,7 +55,7 @@
53 55 </template>
54 56 <script lang="ts">
55 57 // components
56   - import { Dropdown, Menu } from 'ant-design-vue';
  58 + import { Dropdown, Menu, Tooltip } from 'ant-design-vue';
57 59 import { defineComponent, computed, ref, reactive } from 'vue';
58 60 import { useUserStore } from '/@/store/modules/user';
59 61 import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
... ... @@ -83,6 +85,7 @@
83 85 LockAction: createAsyncComponent(() => import('../lock/LockModal.vue')),
84 86 PersonalChild: createAsyncComponent(() => import('../personal/index.vue')),
85 87 AboutSoftwareModal,
  88 + Tooltip,
86 89 },
87 90 props: {
88 91 theme: propTypes.oneOf(['dark', 'light']),
... ...
... ... @@ -28,6 +28,7 @@ import { RouteRecordRaw } from 'vue-router';
28 28 import { PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic';
29 29 import { createLocalStorage } from '/@/utils/cache/index';
30 30 import { getEntitiesId } from '/@/api/dashboard/index';
  31 +import { useRole } from '/@/hooks/business/useRole';
31 32
32 33 interface PlatInfoType {
33 34 id: string;
... ... @@ -283,7 +284,10 @@ export const useUserStore = defineStore({
283 284 title: t('sys.app.logoutTip'),
284 285 content: t('sys.app.logoutMessage'),
285 286 onOk: async () => {
286   - await logoutApi(null, 'modal'); //新增退出登录接口
  287 + const { isPlatformAdmin } = useRole();
  288 + if (!isPlatformAdmin.value) {
  289 + await logoutApi(null, 'modal'); //新增退出登录接口
  290 + }
287 291 await this.logout(true);
288 292 },
289 293 });
... ...
... ... @@ -173,7 +173,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
173 173 // authentication schemes,e.g: Bearer
174 174 // authenticationScheme: 'Bearer',
175 175 authenticationScheme: 'Bearer',
176   - timeout: 10 * 1000,
  176 + timeout: 26 * 1000,
177 177 // 基础接口地址
178 178 // baseURL: globSetting.apiUrl,
179 179 // 接口可能会有通用的地址部分,可以统一抽取出来
... ...
... ... @@ -89,3 +89,18 @@ export const withInstall = <T>(component: T, alias?: string) => {
89 89 };
90 90 return component as T & Plugin;
91 91 };
  92 +
  93 +// 字节单位转换
  94 +export const formatSizeUnits = (bytes) => {
  95 + if (bytes < 100) {
  96 + return bytes;
  97 + } else if (bytes < 1024) {
  98 + return bytes + 'bytes';
  99 + } else if (bytes < 1048576) {
  100 + return (bytes / 1024).toFixed(2) + 'KB';
  101 + } else if (bytes < 1073741824) {
  102 + return (bytes / 1048576).toFixed(2) + 'MB';
  103 + } else {
  104 + return (bytes / 1073741824).toFixed(2) + 'GB';
  105 + }
  106 +};
... ...
... ... @@ -7,10 +7,10 @@ export const createOrganizationSearch = () => {
7 7 if (!value) {
8 8 return true;
9 9 }
10   - let { name } = props || {};
  10 + let { title } = props || {};
11 11 value = value?.toLowerCase();
12   - name = name?.toLowerCase();
13   - return name.includes(value);
  12 + title = title?.toLowerCase();
  13 + return title.includes(value);
14 14 },
15 15 };
16 16 };
... ...
... ... @@ -247,11 +247,11 @@ export const alarmSchemasForm: FormSchema[] = [
247 247 disabled: true,
248 248 },
249 249 },
250   - {
251   - field: 'details',
252   - label: '详情',
253   - component: 'InputTextArea',
254   - },
  250 + // {
  251 + // field: 'details',
  252 + // label: '详情',
  253 + // component: 'InputTextArea',
  254 + // },
255 255 ];
256 256
257 257 export function getAlarmStatus({
... ...
... ... @@ -51,6 +51,7 @@
51 51 import { useDrawer } from '/@/components/Drawer';
52 52 import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
53 53 import { buildUUID } from '/@/utils/uuid';
  54 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
54 55
55 56 export default defineComponent({
56 57 name: 'ContactDrawer',
... ... @@ -161,6 +162,11 @@
161 162 } else {
162 163 delete values.id;
163 164 }
  165 +
  166 + if (Reflect.has(values, 'deleteUrl') && values.deleteUrl) {
  167 + await deleteFilePath(values?.deleteUrl);
  168 + Reflect.deleteProperty(values, 'deleteUrl');
  169 + }
164 170 let saveMessage = '添加成功';
165 171 let updateMessage = '修改成功';
166 172
... ...
... ... @@ -100,7 +100,6 @@
100 100 AccessMode,
101 101 PageMode,
102 102 CameraPermission,
103   - VideoPlatformEnum,
104 103 accessModeConfig,
105 104 getPlayUrl,
106 105 } from './config.data';
... ... @@ -199,14 +198,12 @@
199 198 const handleViewVideo = (record: CameraRecord) => {
200 199 const { videoPlatformDTO, params } = record;
201 200 const { type } = videoPlatformDTO || {};
202   -
203 201 openModal(true, {
204 202 record: {
205 203 id: record.id,
206   - canControl:
207   - [AccessMode.Streaming, AccessMode.GBT28181].includes(record.accessMode) &&
208   - type !== VideoPlatformEnum.FLUORITE,
  204 + canControl: [AccessMode.Streaming, AccessMode.GBT28181].includes(record.accessMode),
209 205 isGBT: record.accessMode === AccessMode.GBT28181,
  206 + videoPlatformType: type,
210 207 channelId: params?.channelNo,
211 208 tbDeviceId: params?.deviceId,
212 209 getPlayUrl: async () => {
... ...
... ... @@ -92,10 +92,11 @@
92 92 const { getResult } = useFingerprint();
93 93 const beforeVideoPlay = async (record: CameraRecordItem) => {
94 94 if (isNullOrUnDef(record.accessMode)) return;
95   - const { url, type } = await getPlayUrl(record);
  95 + const { url, type, withToken } = (await getPlayUrl(record)) || {};
96 96 record.playSourceUrl = url;
97 97 record.streamType = type;
98 98 record.isTransform = true;
  99 + record.withToken = withToken;
99 100 };
100 101
101 102 const gridLayout = ref({ gutter: 1, column: 2 });
... ...
... ... @@ -192,7 +192,7 @@ export const formSchema: QFormSchema[] = [
192 192 component: 'ApiUpload',
193 193 changeEvent: 'update:fileList',
194 194 valueField: 'fileList',
195   - componentProps: () => {
  195 + componentProps: ({ formModel }) => {
196 196 return {
197 197 listType: 'picture-card',
198 198 maxFileLimit: 1,
... ... @@ -214,10 +214,19 @@ export const formSchema: QFormSchema[] = [
214 214 onPreview: (fileList: FileItem) => {
215 215 createImgPreview({ imageList: [fileList.url!] });
216 216 },
  217 + onDelete(url: string) {
  218 + formModel.deleteUrl = url!;
  219 + },
217 220 };
218 221 },
219 222 },
220 223 {
  224 + field: 'deleteUrl',
  225 + label: '',
  226 + component: 'Input',
  227 + show: false,
  228 + },
  229 + {
221 230 field: 'name',
222 231 label: '视频名字',
223 232 required: true,
... ... @@ -623,9 +632,7 @@ export const formGBTSchema: QFormSchema[] = [
623 632 },
624 633 ];
625 634
626   -export async function getPlayUrl(
627   - params: CameraRecord
628   -): Promise<{ url: string; type: PlayerStreamType }> {
  635 +export async function getPlayUrl(params: CameraRecord) {
629 636 const { accessMode } = params;
630 637 if (accessMode === AccessMode.ManuallyEnter) {
631 638 const { videoUrl } = params;
... ... @@ -635,7 +642,11 @@ export async function getPlayUrl(
635 642 if (isRTSPPlay) {
636 643 const { getResult } = useFingerprint();
637 644 const fingerprint = await getResult();
638   - return { url: getFlvPlayUrl(videoUrl, fingerprint.visitorId), type: 'flv' };
  645 + return {
  646 + url: getFlvPlayUrl(videoUrl, fingerprint.visitorId),
  647 + type: 'flv',
  648 + withToken: true,
  649 + };
639 650 } else {
640 651 return { url: videoUrl, type: 'auto' };
641 652 }
... ...
... ... @@ -52,6 +52,7 @@
52 52 import { createPickerSearch } from '/@/utils/pickerSearch';
53 53 import type { queryPageParams } from '/@/api/configuration/center/model/configurationCenterModal';
54 54 import { getDeviceProfile } from '/@/api/alarm/position';
  55 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
55 56
56 57 export default defineComponent({
57 58 name: 'ConfigurationDrawer',
... ... @@ -263,6 +264,11 @@
263 264 values.thumbnail = file.url || null;
264 265 }
265 266 setDrawerProps({ confirmLoading: true });
  267 +
  268 + if (Reflect.has(values, 'deleteUrl') && values.deleteUrl) {
  269 + await deleteFilePath(values?.deleteUrl);
  270 + Reflect.deleteProperty(values, 'deleteUrl');
  271 + }
266 272 let saveMessage = '添加成功';
267 273 let updateMessage = '修改成功';
268 274 values.defaultContent = getDefaultContent(values.platform);
... ...
... ... @@ -6,6 +6,7 @@ import { useComponentRegister } from '/@/components/Form';
6 6 import { OrgTreeSelect } from '../../common/OrgTreeSelect';
7 7 import { getDeviceProfile } from '/@/api/alarm/position';
8 8 import { buildUUID } from '/@/utils/uuid';
  9 +import { createPickerSearch } from '/@/utils/pickerSearch';
9 10
10 11 useComponentRegister('OrgTreeSelect', OrgTreeSelect);
11 12 export enum Platform {
... ... @@ -90,7 +91,7 @@ export const formSchema: FormSchema[] = [
90 91 component: 'ApiUpload',
91 92 changeEvent: 'update:fileList',
92 93 valueField: 'fileList',
93   - componentProps: () => {
  94 + componentProps: ({ formModel }) => {
94 95 return {
95 96 listType: 'picture-card',
96 97 maxFileLimit: 1,
... ... @@ -112,10 +113,19 @@ export const formSchema: FormSchema[] = [
112 113 onPreview: (fileList: FileItem) => {
113 114 createImgPreview({ imageList: [fileList.url!] });
114 115 },
  116 + onDelete(url: string) {
  117 + formModel.deleteUrl = url!;
  118 + },
115 119 };
116 120 },
117 121 },
118 122 {
  123 + field: 'deleteUrl',
  124 + label: '',
  125 + component: 'Input',
  126 + show: false,
  127 + },
  128 + {
119 129 field: 'name',
120 130 label: '组态名称',
121 131 required: true,
... ... @@ -210,14 +220,7 @@ export const formSchema: FormSchema[] = [
210 220 labelField: 'name',
211 221 valueField: 'id',
212 222 placeholder: '请选择产品',
213   - getPopupContainer: (triggerNode) => triggerNode.parentNode,
214   - filterOption: (inputValue: string, option: Record<'label' | 'value', string>) => {
215   - let { label, value } = option;
216   - label = label.toLowerCase();
217   - value = value.toLowerCase();
218   - inputValue = inputValue.toLowerCase();
219   - return label.includes(inputValue) || value.includes(inputValue);
220   - },
  223 + ...createPickerSearch(),
221 224 },
222 225 ifShow: ({ values }) => values['enableTemplate'] === 0,
223 226 },
... ...
... ... @@ -21,6 +21,7 @@
21 21 import { buildUUID } from '/@/utils/uuid';
22 22 import { getDeviceProfile } from '/@/api/alarm/position';
23 23 import { PC_DEFAULT_CONTENT, PHONE_DEFAULT_CONTENT, Platform } from '../center/center.data';
  24 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
24 25
25 26 export default defineComponent({
26 27 name: 'ConfigurationDrawer',
... ... @@ -91,6 +92,10 @@
91 92 const file = (values.thumbnail || []).at(0) || {};
92 93 values.thumbnail = file.url || null;
93 94 }
  95 + if (Reflect.has(values, 'deleteUrl') && values.deleteUrl) {
  96 + await deleteFilePath(values?.deleteUrl);
  97 + Reflect.deleteProperty(values, 'deleteUrl');
  98 + }
94 99 setDrawerProps({ confirmLoading: true });
95 100 let saveMessage = '添加成功';
96 101 let updateMessage = '修改成功';
... ...
... ... @@ -6,6 +6,7 @@ import { useComponentRegister } from '/@/components/Form';
6 6 import { OrgTreeSelect } from '../../common/OrgTreeSelect';
7 7 import { getDeviceProfile } from '/@/api/alarm/position';
8 8 import { Platform } from '../center/center.data';
  9 +import { createPickerSearch } from '/@/utils/pickerSearch';
9 10
10 11 useComponentRegister('OrgTreeSelect', OrgTreeSelect);
11 12
... ... @@ -79,7 +80,7 @@ export const formSchema: FormSchema[] = [
79 80 component: 'ApiUpload',
80 81 changeEvent: 'update:fileList',
81 82 valueField: 'fileList',
82   - componentProps: () => {
  83 + componentProps: ({ formModel }) => {
83 84 return {
84 85 listType: 'picture-card',
85 86 maxFileLimit: 1,
... ... @@ -101,9 +102,18 @@ export const formSchema: FormSchema[] = [
101 102 onPreview: (fileList: FileItem) => {
102 103 createImgPreview({ imageList: [fileList.url!] });
103 104 },
  105 + onDelete(url: string) {
  106 + formModel.deleteUrl = url!;
  107 + },
104 108 };
105 109 },
106 110 },
  111 + {
  112 + field: 'deleteUrl',
  113 + label: '',
  114 + component: 'Input',
  115 + show: false,
  116 + },
107 117
108 118 {
109 119 field: 'name',
... ... @@ -131,6 +141,7 @@ export const formSchema: FormSchema[] = [
131 141 mode: 'multiple',
132 142 labelField: 'name',
133 143 valueField: 'id',
  144 + ...createPickerSearch(),
134 145 },
135 146 },
136 147 {
... ...
... ... @@ -19,6 +19,7 @@
19 19 import { saveOrUpdateBigScreenCenter } from '/@/api/bigscreen/center/bigscreenCenter';
20 20 import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
21 21 import { buildUUID } from '/@/utils/uuid';
  22 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
22 23
23 24 export default defineComponent({
24 25 name: 'BigScreenDrawer',
... ... @@ -67,6 +68,10 @@
67 68 const file = (values.thumbnail || []).at(0) || {};
68 69 values.thumbnail = file.url || null;
69 70 }
  71 + if (Reflect.has(values, 'deleteUrl') && values.deleteUrl) {
  72 + await deleteFilePath(values?.deleteUrl);
  73 + Reflect.deleteProperty(values, 'deleteUrl');
  74 + }
70 75 setDrawerProps({ confirmLoading: true });
71 76 let saveMessage = '添加成功';
72 77 let updateMessage = '修改成功';
... ...
... ... @@ -56,7 +56,7 @@ export const formSchema: FormSchema[] = [
56 56 component: 'ApiUpload',
57 57 changeEvent: 'update:fileList',
58 58 valueField: 'fileList',
59   - componentProps: () => {
  59 + componentProps: ({ formModel }) => {
60 60 return {
61 61 listType: 'picture-card',
62 62 maxFileLimit: 1,
... ... @@ -80,9 +80,18 @@ export const formSchema: FormSchema[] = [
80 80 onPreview: (fileList: FileItem) => {
81 81 createImgPreview({ imageList: [fileList.url!] });
82 82 },
  83 + onDelete(url: string) {
  84 + formModel.deleteUrl = url!;
  85 + },
83 86 };
84 87 },
85 88 },
  89 + {
  90 + field: 'deleteUrl',
  91 + label: '',
  92 + component: 'Input',
  93 + show: false,
  94 + },
86 95
87 96 {
88 97 field: 'name',
... ...
... ... @@ -48,7 +48,10 @@
48 48 rowSelection: {
49 49 type: 'checkbox',
50 50 getCheckboxProps: (record: any) => {
51   - return { disabled: !!record.status };
  51 + return {
  52 + disabled:
  53 + getAuthCache(USER_INFO_KEY).tenantId === record.tenantId ? !!record.status : true,
  54 + };
52 55 },
53 56 },
54 57 });
... ... @@ -86,12 +89,14 @@
86 89
87 90 // 详情
88 91 const registerDetailRecord = ref<any>({});
  92 + const isCurrentTenant = ref<Boolean>(false);
89 93 const handleDetail = (record?: any) => {
90 94 openDrawer(true);
91 95 registerDetailRecord.value = {
92 96 ...record,
93 97 ifShowClass: true,
94 98 };
  99 + isCurrentTenant.value = getAuthCache(USER_INFO_KEY).tenantId === record.tenantId;
95 100 };
96 101
97 102 // 状态->编辑
... ... @@ -99,7 +104,7 @@
99 104 switchLoading.value = true;
100 105 await deviceProfileCategory({ ...record, status: e });
101 106 switchLoading.value = false;
102   - createMessage.success('操作成功');
  107 + createMessage.success(`${!e ? '禁用' : '启用'}成功`);
103 108 handleReload();
104 109 };
105 110
... ... @@ -145,6 +150,7 @@
145 150 <Switch
146 151 @click="(e) => handleSwitch(e, record)"
147 152 :loading="switchLoading"
  153 + :disabled="getAuthCache(USER_INFO_KEY).tenantId !== record.tenantId"
148 154 v-model:checked="record.status"
149 155 checked-children="启用"
150 156 un-checked-children="禁用"
... ... @@ -165,14 +171,17 @@
165 171 label: '编辑',
166 172 auth: 'api:yt:product:category:update',
167 173 icon: 'clarity:note-edit-line',
168   - ifShow: authBtn(role),
  174 + ifShow: authBtn(role) && getAuthCache(USER_INFO_KEY).tenantId === record.tenantId,
169 175 onClick: handleEdit.bind(null, record),
170 176 },
171 177 {
172 178 label: '删除',
173 179 auth: 'api:yt:product:category:delete',
174 180 icon: 'ant-design:delete-outlined',
175   - ifShow: authBtn(role) && !record.status,
  181 + ifShow:
  182 + authBtn(role) &&
  183 + !record.status &&
  184 + getAuthCache(USER_INFO_KEY).tenantId === record.tenantId,
176 185 color: 'error',
177 186 popConfirm: {
178 187 title: '是否确认删除',
... ... @@ -185,7 +194,10 @@
185 194 </BasicTable>
186 195 <classModal @register="registerModal" @handleReload="handleReload" />
187 196 <BasicDrawer title="物模型" @register="registerDetailDrawer" width="60%" destroy-on-close>
188   - <PhysicalModelManagementStep :record="registerDetailRecord" />
  197 + <PhysicalModelManagementStep
  198 + :isCurrentTenant="isCurrentTenant"
  199 + :record="registerDetailRecord"
  200 + />
189 201 </BasicDrawer>
190 202 </div>
191 203 </template>
... ...
... ... @@ -49,7 +49,7 @@ export const step1Schemas: FormSchema[] = [
49 49 component: 'ApiUpload',
50 50 changeEvent: 'update:fileList',
51 51 valueField: 'fileList',
52   - componentProps: () => {
  52 + componentProps: ({ formModel }) => {
53 53 return {
54 54 listType: 'picture-card',
55 55 maxFileLimit: 1,
... ... @@ -71,10 +71,19 @@ export const step1Schemas: FormSchema[] = [
71 71 onPreview: (fileList: FileItem) => {
72 72 createImgPreview({ imageList: [fileList.url!] });
73 73 },
  74 + onDelete(url: string) {
  75 + formModel.deleteUrl = url!;
  76 + },
74 77 };
75 78 },
76 79 },
77 80 {
  81 + field: 'deleteUrl',
  82 + label: '',
  83 + component: 'Input',
  84 + show: false,
  85 + },
  86 + {
78 87 field: 'alias',
79 88 label: '别名 ',
80 89 component: 'Input',
... ... @@ -193,8 +202,29 @@ export const step1Schemas: FormSchema[] = [
193 202 },
194 203 },
195 204 {
  205 + field: 'addressType',
  206 + label: '',
  207 + component: 'Input',
  208 + show: false,
  209 + defaultValue: 'HEX',
  210 + },
  211 + {
  212 + field: 'deviceState',
  213 + label: '',
  214 + component: 'Input',
  215 + show: false,
  216 + },
  217 + {
196 218 field: 'addressCode',
197 219 label: '地址码',
  220 + dynamicDisabled({ values }) {
  221 + return (
  222 + values.isUpdate &&
  223 + values.deviceType === DeviceTypeEnum.SENSOR &&
  224 + values.deviceState === 'ONLINE' &&
  225 + values.transportType === TransportTypeEnum.TCP
  226 + );
  227 + },
198 228 dynamicRules({ values }) {
199 229 return [
200 230 {
... ... @@ -202,18 +232,31 @@ export const step1Schemas: FormSchema[] = [
202 232 values?.transportType === TransportTypeEnum.TCP &&
203 233 values?.tcpDeviceProtocol === TCPProtocolTypeEnum.MODBUS_RTU,
204 234 message: '地址码范围为00~FF',
205   - pattern: /^[0-9A-Fa-f]{1,2}$/,
  235 + pattern: values?.addressType === 'HEX' ? /^[0-9A-Fa-f]{2}$/ : /^[0-9A-Fa-f]{1,2}$/,
206 236 },
207 237 ];
208 238 },
209   - helpMessage: ['地址码范围为00~FF'],
  239 + helpMessage({ values }) {
  240 + return [
  241 + '地址码范围为00~FF',
  242 + values.transportType === TransportTypeEnum.TCP &&
  243 + values.deviceType === DeviceTypeEnum.SENSOR
  244 + ? 'tcp网关子设备在线时,不能修改设备标识或地址码'
  245 + : '',
  246 + ];
  247 + },
210 248 component: 'HexInput',
211 249 changeEvent: 'update:value',
212 250 valueField: 'value',
213   - componentProps: {
214   - type: InputTypeEnum.HEX,
215   - maxValue: parseInt('FF', 16),
216   - placeholder: '请输入寄存器地址',
  251 + componentProps: ({ formModel }) => {
  252 + return {
  253 + type: InputTypeEnum.HEX,
  254 + maxValue: parseInt('FF', 16),
  255 + placeholder: '请输入寄存器地址',
  256 + onHexChange: (e) => {
  257 + formModel.addressType = e;
  258 + },
  259 + };
217 260 },
218 261 ifShow: ({ values }) => {
219 262 return (
... ... @@ -235,6 +278,22 @@ export const step1Schemas: FormSchema[] = [
235 278 },
236 279 ];
237 280 },
  281 + dynamicDisabled({ values }) {
  282 + return (
  283 + values.isUpdate &&
  284 + values.deviceType === DeviceTypeEnum.SENSOR &&
  285 + values.deviceState === 'ONLINE' &&
  286 + values.transportType === TransportTypeEnum.TCP
  287 + );
  288 + },
  289 + helpMessage({ values }) {
  290 + return (
  291 + values.transportType === TransportTypeEnum.TCP &&
  292 + values.deviceType === DeviceTypeEnum.SENSOR
  293 + ? ['tcp网关子设备在线时,不能修改设备标识或地址码']
  294 + : false
  295 + ) as any;
  296 + },
238 297 component: 'Input',
239 298 componentProps: () => {
240 299 return {
... ... @@ -276,7 +335,7 @@ export const step1Schemas: FormSchema[] = [
276 335 component: 'ApiSelect',
277 336 ifShow: ({ values }) => values.deviceType === 'SENSOR',
278 337 componentProps: ({ formModel, formActionType }) => {
279   - const { transportType } = formModel;
  338 + const { transportType, deviceType, gatewayId } = formModel;
280 339 const { setFieldsValue } = formActionType;
281 340 if (!transportType) return {};
282 341 return {
... ... @@ -291,6 +350,7 @@ export const step1Schemas: FormSchema[] = [
291 350 showSearch: true,
292 351 params: {
293 352 transportType,
  353 + gatewayId: deviceType === DeviceTypeEnum.SENSOR && gatewayId ? gatewayId : null,
294 354 },
295 355 placeholder: '请选择网关设备',
296 356 valueField: 'tbDeviceId',
... ...
... ... @@ -6,6 +6,7 @@ import { deviceProfile } from '/@/api/device/deviceManager';
6 6 import { h } from 'vue';
7 7 import { Tag, Tooltip } from 'ant-design-vue';
8 8 import { handeleCopy } from '../../profiles/step/topic';
  9 +import edgefornt from '/@/assets/icons/edgefornt.svg';
9 10
10 11 export enum DeviceListAuthEnum {
11 12 /**
... ... @@ -84,8 +85,9 @@ export const columns: BasicColumn[] = [
84 85 title: '别名/设备名称',
85 86 width: 210,
86 87 slots: { customRender: 'name', title: 'deviceTitle' },
  88 + className: 'device-name-edge',
87 89 customRender: ({ record }) => {
88   - return h('div', { style: 'display:flex;flex-direction:column' }, [
  90 + return h('div', { class: 'py-3 px-3.5' }, [
89 91 record.alias &&
90 92 h(
91 93 'div',
... ... @@ -118,6 +120,19 @@ export const columns: BasicColumn[] = [
118 120 () => `${record.name}`
119 121 )
120 122 ),
  123 + record.isEdge
  124 + ? h('div', { class: 'absolute top-0 left-0', fill: '#1890ff' }, [
  125 + h('img', { src: edgefornt, class: 'w-12.5 h-12.5' }),
  126 + h(
  127 + 'span',
  128 + {
  129 + class:
  130 + 'absolute top-0.5 left-0.5 text-light-50 transform -rotate-45 translate-y-0 !text-10px',
  131 + },
  132 + '边'
  133 + ),
  134 + ])
  135 + : '',
121 136 ]);
122 137 },
123 138 },
... ... @@ -162,7 +177,9 @@ export const columns: BasicColumn[] = [
162 177 {
163 178 title: '最后断开时间',
164 179 dataIndex: 'lastOfflineTime',
165   - format: (text) => text && formatToDate(text, 'YYYY-MM-DD HH:mm:ss'),
  180 + format: (text) => {
  181 + return text ? formatToDate(text, 'YYYY-MM-DD HH:mm:ss') : '';
  182 + },
166 183 width: 160,
167 184 },
168 185 ];
... ...
... ... @@ -41,6 +41,7 @@
41 41 import { Steps } from 'ant-design-vue';
42 42 import { useMessage } from '/@/hooks/web/useMessage';
43 43 import { credentialTypeEnum } from '../../config/data';
  44 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
44 45
45 46 export default defineComponent({
46 47 name: 'DeviceModal',
... ... @@ -161,8 +162,13 @@
161 162 avatar: stepRecord?.icon,
162 163 ...DeviceStep1Ref.value?.devicePositionState,
163 164 },
  165 + alarmStatus: currentDeviceData?.alarmStatus,
164 166 };
165 167 validateNameLength(stepRecord.name);
  168 + if (Reflect.has(editData, 'deleteUrl')) {
  169 + await deleteFilePath(editData?.deleteUrl);
  170 + Reflect.deleteProperty(editData, 'deleteUrl');
  171 + }
166 172 await createOrEditDevice(editData);
167 173 } else {
168 174 const stepRecord = unref(stepState);
... ...
... ... @@ -401,16 +401,16 @@
401 401 // 父组件调用更新字段值的方法
402 402 function parentSetFieldsValue(data) {
403 403 const { deviceInfo = {} } = data;
404   - positionState.longitude = deviceInfo.longitude;
405   - positionState.latitude = deviceInfo.latitude;
406   - positionState.address = deviceInfo.address;
  404 + positionState.longitude = deviceInfo?.longitude;
  405 + positionState.latitude = deviceInfo?.latitude;
  406 + positionState.address = deviceInfo?.address;
407 407 devicePositionState.value = { ...toRaw(positionState) };
408   - devicePic.value = deviceInfo.avatar;
  408 + devicePic.value = deviceInfo?.avatar;
409 409
410   - if (deviceInfo.avatar) {
  410 + if (deviceInfo?.avatar) {
411 411 setFieldsValue({
412 412 isUpdate: unref(isUpdate1),
413   - icon: [{ uid: buildUUID(), name: 'name', url: deviceInfo.avatar } as FileItem],
  413 + icon: [{ uid: buildUUID(), name: 'name', url: deviceInfo?.avatar } as FileItem],
414 414 });
415 415 }
416 416 setFieldsValue({
... ...
... ... @@ -119,6 +119,7 @@
119 119 ],
120 120 };
121 121 });
  122 + const cacheSearchValue = ref('');
122 123
123 124 const [registerForm, { getFieldsValue }] = useForm({
124 125 schemas: [
... ... @@ -141,6 +142,8 @@
141 142
142 143 pagination.current = 1;
143 144
  145 + cacheSearchValue.value = value;
  146 +
144 147 socketInfo.filterAttrKeys = value
145 148 ? unref(socketInfo.rawDataSource)
146 149 .filter(
... ... @@ -161,6 +164,7 @@
161 164 resetFunc: async () => {
162 165 try {
163 166 socketInfo.filterAttrKeys = [];
  167 + cacheSearchValue.value = '';
164 168 handleFilterChange();
165 169 unref(mode) === EnumTableCardMode.TABLE && setTableModeData();
166 170 } catch (error) {}
... ... @@ -201,8 +205,15 @@
201 205 const { createMessage } = useMessage();
202 206
203 207 const setDataSource = () => {
  208 + const filterValueByCacheSearchValue = socketInfo.rawDataSource.filter(
  209 + (item) =>
  210 + item.key?.toUpperCase().includes(cacheSearchValue.value.toUpperCase()) ||
  211 + item.name?.toUpperCase().includes(cacheSearchValue.value.toUpperCase())
  212 + );
204 213 socketInfo.dataSource = socketInfo.filterAttrKeys.length
205 214 ? socketInfo.rawDataSource.filter((item) => socketInfo.filterAttrKeys.includes(item.key))
  215 + : filterValueByCacheSearchValue.length === 0
  216 + ? []
206 217 : socketInfo.rawDataSource;
207 218 };
208 219
... ...
1 1 import { h } from 'vue';
2 2 import { BasicColumn, FormSchema } from '/@/components/Table';
3 3 import { Tag } from 'ant-design-vue';
  4 +import { VideoPlatformEnum } from '/@/views/camera/manage/config.data';
4 5 export type VideoCancelModalParamsType = {
5 6 canControl?: boolean;
  7 + videoPlatformType: VideoPlatformEnum;
6 8 isGBT?: boolean;
7 9 tbDeviceId?: string;
8 10 channelId?: string;
... ... @@ -101,3 +103,25 @@ export const searchFormSchema: FormSchema[] | any = [
101 103 },
102 104 },
103 105 ];
  106 +
  107 +export enum VideoControlEnum {
  108 + Up = 'UP',
  109 + Right = 'RIGHT',
  110 + Left = 'LEFT',
  111 + Down = 'DOWN',
  112 + ZoomIn = 'ZOOM_IN',
  113 + ZoomOut = 'ZOOM_OUT',
  114 +}
  115 +
  116 +export enum EzvizVideoControlEnum {
  117 + UP,
  118 + DOWN,
  119 + LEFT,
  120 + RIGHT,
  121 + LEFT_UP,
  122 + LEFT_DOWN,
  123 + RIGHT_UP,
  124 + RIGHT_DOWN,
  125 + ZOOM_IN,
  126 + ZOOM_OUT,
  127 +}
... ...
... ... @@ -13,20 +13,14 @@
13 13 } from '@ant-design/icons-vue';
14 14 import { Button, Slider } from 'ant-design-vue';
15 15 import { controlling } from '/@/api/camera/cameraManager';
16   - import { setVideoControl } from '/@/api/device/videoChannel';
  16 + import { setEzvizControl, setVideoControl } from '/@/api/device/videoChannel';
17 17 import XGPlayer from '/@/components/Video/src/XGPlayer.vue';
18   - import PresetPlayer from 'xgplayer';
  18 + import { EzvizVideoControlEnum, VideoCancelModalParamsType, VideoControlEnum } from './config';
  19 + import { VideoPlatformEnum } from '/@/views/camera/manage/config.data';
19 20
20 21 const props = defineProps<{
21 22 playUrl?: string;
22   - options?: {
23   - canControl?: boolean;
24   - isGBT?: boolean;
25   - tbDeviceId?: string;
26   - channelId?: string;
27   - id?: string;
28   - playerProps?: Recordable;
29   - };
  23 + options?: VideoCancelModalParamsType;
30 24 }>();
31 25
32 26 const playerRef = ref<InstanceType<typeof XGPlayer>>();
... ... @@ -64,12 +58,24 @@
64 58 });
65 59 };
66 60
  61 + const handleEzvizControl = (actionType: string, start: boolean) => {
  62 + const { id } = props.options || {};
  63 + const action = EzvizVideoControlEnum[actionType as VideoControlEnum];
  64 + setEzvizControl({ entityId: id!, action, controllingType: Number(!start) });
  65 + };
  66 +
67 67 //长按开始
68 68 const moveStart = (action: string) => {
69 69 if (unref(props.options?.isGBT)) {
70 70 handleGBTControl(action, unref(sliderValue));
71 71 return;
72 72 }
  73 +
  74 + if (props.options?.videoPlatformType === VideoPlatformEnum.FLUORITE) {
  75 + handleEzvizControl(action, true);
  76 + return;
  77 + }
  78 +
73 79 handleControl(0, action);
74 80 };
75 81
... ... @@ -79,6 +85,14 @@
79 85 handleGBTControl('STOP', unref(sliderValue));
80 86 return;
81 87 }
  88 +
  89 + if (props.options?.videoPlatformType === VideoPlatformEnum.FLUORITE) {
  90 + setTimeout(() => {
  91 + handleEzvizControl(action, false);
  92 + }, 1000);
  93 + return;
  94 + }
  95 +
82 96 handleControl(1, action);
83 97 };
84 98
... ... @@ -122,15 +136,15 @@
122 136 <div>
123 137 <Button
124 138 class="left-top in-block"
125   - @mousedown="moveStart('UP')"
126   - @mouseup="moveStop('UP')"
  139 + @mousedown="moveStart(VideoControlEnum.Up)"
  140 + @mouseup="moveStop(VideoControlEnum.Up)"
127 141 >
128 142 <CaretUpOutlined class="icon-rotate child-icon" />
129 143 </Button>
130 144 <Button
131 145 class="right-top in-block"
132   - @mousedown="moveStart('RIGHT')"
133   - @mouseup="moveStop('RIGHT')"
  146 + @mousedown="moveStart(VideoControlEnum.Right)"
  147 + @mouseup="moveStop(VideoControlEnum.Right)"
134 148 >
135 149 <CaretRightOutlined class="icon-rotate child-icon" />
136 150 </Button>
... ... @@ -138,15 +152,15 @@
138 152 <div>
139 153 <Button
140 154 class="left-bottom in-block"
141   - @mousedown="moveStart('LEFT')"
142   - @mouseup="moveStop('LEFT')"
  155 + @mousedown="moveStart(VideoControlEnum.Left)"
  156 + @mouseup="moveStop(VideoControlEnum.Left)"
143 157 >
144 158 <CaretLeftOutlined class="icon-rotate child-icon" />
145 159 </Button>
146 160 <Button
147 161 class="right-bottom in-block"
148   - @mousedown="moveStart('DOWN')"
149   - @mouseup="moveStop('DOWN')"
  162 + @mousedown="moveStart(VideoControlEnum.Down)"
  163 + @mouseup="moveStop(VideoControlEnum.Down)"
150 164 >
151 165 <CaretDownOutlined class="icon-rotate child-icon" />
152 166 </Button>
... ... @@ -161,16 +175,16 @@
161 175 <div class="flex justify-center mt-8">
162 176 <Button
163 177 class="button-icon"
164   - @mousedown="moveStart('ZOOM_IN')"
165   - @mouseup="moveStop('ZOOM_IN')"
  178 + @mousedown="moveStart(VideoControlEnum.ZoomIn)"
  179 + @mouseup="moveStop(VideoControlEnum.ZoomIn)"
166 180 style="border-radius: 50%"
167 181 >
168 182 <ZoomInOutlined style="color: #315a9c; font-size: 1.5rem" />
169 183 </Button>
170 184 <Button
171 185 class="ml-10 button-icon"
172   - @mousedown="moveStart('ZOOM_OUT')"
173   - @mouseup="moveStop('ZOOM_OUT')"
  186 + @mousedown="moveStart(VideoControlEnum.ZoomOut)"
  187 + @mouseup="moveStop(VideoControlEnum.ZoomOut)"
174 188 style="border-radius: 50%"
175 189 >
176 190 <ZoomOutOutlined style="color: #315a9c; font-size: 1.5rem" />
... ...
... ... @@ -31,7 +31,6 @@
31 31 options.value = null;
32 32 setModalProps({ loading: true, loadingTip: '视频加载中...' });
33 33 const { url, type } = await record.getPlayUrl();
34   -
35 34 playUrl.value = url;
36 35 options.value = record;
37 36 options.value.playerProps = {
... ...
... ... @@ -147,6 +147,8 @@
147 147 ? 'warning'
148 148 : record.deviceState == DeviceState.ONLINE
149 149 ? 'success'
  150 + : record.deviceState == DeviceState.ACTIVE
  151 + ? 'success'
150 152 : 'error'
151 153 "
152 154 class="ml-2"
... ... @@ -156,6 +158,8 @@
156 158 ? '待激活'
157 159 : record.deviceState == DeviceState.ONLINE
158 160 ? '在线'
  161 + : record.deviceState == DeviceState.ACTIVE
  162 + ? '活动'
159 163 : '离线'
160 164 }}
161 165 </Tag>
... ... @@ -231,7 +235,7 @@
231 235 ifShow: authBtn(role) && record.customerId === undefined,
232 236 color: 'error',
233 237 popConfirm: {
234   - title: '是否确认删除',
  238 + title: !!record.isEdge ? '此设备来自边端,请谨慎删除' : '是否确认删除',
235 239 confirm: handleDelete.bind(null, record),
236 240 },
237 241 },
... ... @@ -707,4 +711,11 @@
707 711 border-right: 30px solid transparent;
708 712 }
709 713 }
  714 +
  715 + .device-name-edge {
  716 + width: 100%;
  717 + height: 100%;
  718 + padding: 0 !important;
  719 + position: relative;
  720 + }
710 721 </style>
... ...
... ... @@ -24,6 +24,7 @@
24 24 import { BasicCardList, useCardList } from '/@/components/CardList';
25 25 import { useRoute } from 'vue-router';
26 26 import { ref } from 'vue';
  27 + import edgefornt from '/@/assets/icons/edgefornt.svg';
27 28
28 29 defineProps<{
29 30 mode: EnumTableCardMode;
... ... @@ -154,7 +155,7 @@
154 155 <template #renderItem="{ item }: BasicCardListRenderItem<ProfileRecord>">
155 156 <Card hoverable>
156 157 <template #cover>
157   - <div class="h-full w-full !flex justify-center items-center text-center p-1">
  158 + <div class="h-full w-full !flex justify-center items-center text-center p-1 relative">
158 159 <Image
159 160 @click.stop
160 161 wrapper-class-name="!w-32 !h-32 !flex !items-center overflow-hidden"
... ... @@ -162,6 +163,13 @@
162 163 placeholder
163 164 :fallback="IMAGE_FALLBACK"
164 165 />
  166 + <div v-if="item.isEdge" class="absolute top-0 left-0">
  167 + <img :src="edgefornt" />
  168 + <span
  169 + class="absolute top-0 left-0 text-light-50 transform -rotate-45 translate-y-1 translate-x-0.5"
  170 + >边</span
  171 + >
  172 + </div>
165 173 </div>
166 174 </template>
167 175 <template class="ant-card-actions" #actions>
... ... @@ -199,7 +207,7 @@
199 207 auth: ProductPermission.DELETE,
200 208 icon: 'ant-design:delete-outlined',
201 209 popconfirm: {
202   - title: '是否确认删除操作?',
  210 + title: !!item.isEdge ? '此产品来自边端,请谨慎删除' : '是否确认删除',
203 211 onConfirm: handleDelete.bind(null, [item.id]),
204 212 disabled: item.default || item.name == 'default',
205 213 },
... ...
... ... @@ -20,7 +20,7 @@
20 20 key="modelOfMatter"
21 21 tab="物模型管理"
22 22 >
23   - <PhysicalModelManagementStep :record="record" />
  23 + <PhysicalModelManagementStep :record="record" :isCurrentTenant="true" />
24 24 </Tabs.TabPane>
25 25 </Tabs>
26 26 </BasicDrawer>
... ...
... ... @@ -86,6 +86,7 @@
86 86 import TransportConfigurationStep from './step/TransportConfigurationStep.vue';
87 87 import PhysicalModelManagementStep from './step/PhysicalModelManagementStep.vue';
88 88 import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel';
  89 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
89 90
90 91 const emits = defineEmits(['success', 'register']);
91 92 const activeKey = ref('1');
... ... @@ -188,6 +189,10 @@
188 189 await getDeviceConfFormData();
189 190 await getTransConfData();
190 191 const isEmptyObj = isEmpty(transportConfData.profileData.transportConfiguration);
  192 + if (Reflect.has(postSubmitFormData.deviceConfData, 'deleteUrl') && values.deleteUrl.deleteUrl) {
  193 + await deleteFilePath(postSubmitFormData.deviceConfData?.deleteUrl);
  194 + Reflect.deleteProperty(postSubmitFormData.deviceConfData, 'deleteUrl');
  195 + }
191 196 await deviceConfigAddOrEdit({
192 197 ...postSubmitFormData.deviceConfData,
193 198 ...{ transportType: !isEmptyObj ? transportTypeStr.value : 'DEFAULT' },
... ...
... ... @@ -79,7 +79,7 @@
79 79 icon: 'ant-design:delete-outlined',
80 80 color: 'error',
81 81 popConfirm: {
82   - title: '是否确认删除',
  82 + title: !!record.isEdge ? '此设备来自边端,请谨慎删除' : '是否确认删除',
83 83 confirm: handleDeleteOrBatchDelete.bind(null, record),
84 84 },
85 85 ifShow: () => {
... ...
... ... @@ -23,7 +23,7 @@
23 23 }
24 24 );
25 25
26   - const emits = defineEmits(['update:value', 'change']);
  26 + const emits = defineEmits(['update:value', 'change', 'hexChange']);
27 27
28 28 const typOptions = Object.values(InputTypeEnum).map((value) => ({ label: value, value }));
29 29
... ... @@ -114,6 +114,10 @@
114 114 ? getHexToDec(unref(getInputValue)!)
115 115 : `${hexWithPrefix ? '0x' : ''}${Number(unref(getInputValue)).toString(16).toUpperCase()}`;
116 116 });
  117 +
  118 + const handleInputTypeChange = (e) => {
  119 + emits('hexChange', e);
  120 + };
117 121 </script>
118 122
119 123 <template>
... ... @@ -124,6 +128,7 @@
124 128 class="min-w-20"
125 129 :options="typOptions"
126 130 :disabled="disabled"
  131 + @change="handleInputTypeChange"
127 132 />
128 133 <Input v-bind="$attrs" v-model:value="getInputValue" class="!w-full" :disabled="disabled" />
129 134 <div class="min-w-14 h-full px-2 flex-grow flex-shrink-0 text-center leading-8 bg-gray-200">
... ...
... ... @@ -13,7 +13,13 @@
13 13 <BasicForm @register="registerForm">
14 14 <template #importType="{ model }">
15 15 <RadioGroup v-model:value="model.importType">
16   - <Authority :value="['api:yt:things_model:category:import', 'api:yt:things_model:import']">
  16 + <Authority
  17 + :value="
  18 + record?.ifShowClass
  19 + ? ['api:yt:things_model:category:import']
  20 + : ['api:yt:things_model:import']
  21 + "
  22 + >
17 23 <Radio value="2">JSON导入</Radio>
18 24 </Authority>
19 25 <Authority :value="['api:yt:things_model:excel_import']">
... ... @@ -80,10 +86,11 @@
80 86 label: '导入类型',
81 87 component: 'RadioGroup',
82 88 slot: 'importType',
83   - defaultValue: hasPermission([
84   - 'api:yt:things_model:category:import',
85   - 'api:yt:things_model:import',
86   - ])
  89 + defaultValue: (
  90 + props.record?.ifShowClass
  91 + ? hasPermission(['api:yt:things_model:category:import'])
  92 + : hasPermission(['api:yt:things_model:import'])
  93 + )
87 94 ? '2'
88 95 : '1',
89 96 helpMessage:
... ...
... ... @@ -182,7 +182,7 @@ export const step1Schemas: FormSchema[] = [
182 182 component: 'ApiUpload',
183 183 changeEvent: 'update:fileList',
184 184 valueField: 'fileList',
185   - componentProps: () => {
  185 + componentProps: ({ formModel }) => {
186 186 return {
187 187 listType: 'picture-card',
188 188 maxFileLimit: 1,
... ... @@ -204,10 +204,19 @@ export const step1Schemas: FormSchema[] = [
204 204 onPreview: (fileList: FileItem) => {
205 205 createImgPreview({ imageList: [fileList.url!] });
206 206 },
  207 + onDelete(url: string) {
  208 + formModel.deleteUrl = url!;
  209 + },
207 210 };
208 211 },
209 212 },
210 213 {
  214 + field: 'deleteUrl',
  215 + label: '',
  216 + component: 'Input',
  217 + show: false,
  218 + },
  219 + {
211 220 field: 'deviceType',
212 221 component: 'ApiRadioGroup',
213 222 label: '设备类型',
... ...
... ... @@ -8,7 +8,7 @@
8 8 >
9 9 <template #toolbar>
10 10 <div class="flex-auto">
11   - <div class="mb-2">
  11 + <div class="mb-2" v-if="isCurrentTenant">
12 12 <Alert type="info" show-icon>
13 13 <template #message>
14 14 <span v-if="!isShowBtn">
... ... @@ -39,6 +39,7 @@
39 39 <Button type="primary" @click="handleExport" :loading="loading">导出物模型</Button>
40 40 </Authority>
41 41 <Authority
  42 + v-if="isCurrentTenant"
42 43 :value="[
43 44 'api:yt:things_model:import',
44 45 'api:yt:things_model:category:import',
... ... @@ -47,12 +48,18 @@
47 48 >
48 49 <Button type="primary" @click="handleSelectImport">导入物模型</Button>
49 50 </Authority>
50   - <Button type="primary" v-if="!isShowBtn" @click="handleEditPhysicalModel"
  51 + <Button
  52 + type="primary"
  53 + v-if="!isShowBtn && isCurrentTenant"
  54 + @click="handleEditPhysicalModel"
51 55 >编辑物模型</Button
52 56 >
53 57 </div>
54 58 <div class="flex gap-2">
55   - <Authority :value="[ModelOfMatterPermission.RELEASE]">
  59 + <Authority
  60 + v-if="!props.record.ifShowClass"
  61 + :value="[ModelOfMatterPermission.RELEASE]"
  62 + >
56 63 <Popconfirm
57 64 title="是否需要发布上线?"
58 65 ok-text="确定"
... ... @@ -159,11 +166,12 @@
159 166 import { DataActionModeEnum } from '/@/enums/toolEnum';
160 167 import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel';
161 168
162   - const { isPlatformAdmin, isSysadmin } = useRole();
  169 + const { isPlatformAdmin, isSysadmin, isTenantAdmin } = useRole();
163 170 defineEmits(['register']);
164 171
165 172 const props = defineProps<{
166 173 record: DeviceProfileDetail;
  174 + isCurrentTenant: Boolean;
167 175 }>();
168 176
169 177 const { createMessage } = useMessage();
... ... @@ -224,7 +232,8 @@
224 232
225 233 const handleDeleteOrBatchDelete = async (record?: ModelOfMatterItemRecordType) => {
226 234 const deleteFn =
227   - props.record.ifShowClass && (unref(isPlatformAdmin) || unref(isSysadmin))
  235 + props.record.ifShowClass &&
  236 + (unref(isPlatformAdmin) || unref(isSysadmin) || unref(isTenantAdmin))
228 237 ? deleteModelCategory
229 238 : deleteModel;
230 239
... ... @@ -265,7 +274,10 @@
265 274 const handleSelectImport = () => {
266 275 openModalSelect(true, {
267 276 id: props.record.id,
268   - isCateGory: (unref(isPlatformAdmin) || unref(isSysadmin)) && props.record.ifShowClass,
  277 + isCateGory:
  278 + unref(isPlatformAdmin) ||
  279 + unref(isSysadmin) ||
  280 + (unref(isTenantAdmin) && props.record.ifShowClass),
269 281 });
270 282 };
271 283
... ...
... ... @@ -31,7 +31,7 @@
31 31 import { DataActionModeEnum, DataActionModeNameEnum } from '/@/enums/toolEnum';
32 32 import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel';
33 33
34   - const { isPlatformAdmin, isSysadmin } = useRole();
  34 + const { isPlatformAdmin, isSysadmin, isTenantAdmin } = useRole();
35 35
36 36 const emit = defineEmits(['register', 'success']);
37 37
... ... @@ -77,7 +77,9 @@
77 77 const value = unref(objectModelElRef)!.getFieldsValue();
78 78
79 79 const isCategoryAction =
80   - (unref(isSysadmin) || unref(isPlatformAdmin)) && props.record.ifShowClass;
  80 + unref(isSysadmin) ||
  81 + unref(isPlatformAdmin) ||
  82 + (unref(isTenantAdmin) && props.record.ifShowClass);
81 83
82 84 const submitFn =
83 85 unref(openModalMode) === DataActionModeEnum.CREATE
... ...
  1 +export { default as CardMode } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { Button, Tooltip, Card, Popconfirm } from 'ant-design-vue';
  3 + import { AuthIcon, EnumTableCardMode } from '/@/components/Widget';
  4 + import { useMessage } from '/@/hooks/web/useMessage';
  5 + import { BasicCardList, useCardList } from '/@/components/CardList';
  6 + import { useRoute } from 'vue-router';
  7 + import { ref } from 'vue';
  8 + import { HandleOperationEnum, HandleOperationNameEnum } from '../../config';
  9 + import { searchFormSchema } from '../EdgeInstance/config';
  10 + import { deleteEdgeInstance, edgeInstancePage } from '/@/api/edgeManage/edgeInstance';
  11 + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  12 + import { EdgeInstanceFormDrawer } from '../EdgeInstance';
  13 + import { useDrawer } from '/@/components/Drawer';
  14 + import moment from 'moment';
  15 + import { isArray } from '/@/utils/is';
  16 + import AuthDropDown from '/@/components/Widget/AuthDropDown.vue';
  17 + import edgeStatusIsOnlinePng from '/@/assets/images/edgeStatusIsOnline.png';
  18 + import edgeStatusIsOfflinePng from '/@/assets/images/edgeStatusIsOffline.png';
  19 + import { Image } from 'ant-design-vue';
  20 + import { useGo } from '/@/hooks/web/usePage';
  21 +
  22 + defineProps<{
  23 + mode: EnumTableCardMode;
  24 + }>();
  25 +
  26 + defineEmits(['register']);
  27 +
  28 + enum DropMenuEvent {
  29 + DELETE = 'delete',
  30 + }
  31 +
  32 + const { createMessage } = useMessage();
  33 +
  34 + const go = useGo();
  35 +
  36 + const { query } = useRoute();
  37 +
  38 + const disabledDeleteFlag = ref(true);
  39 +
  40 + const [registerCardList, { reload, getSelectedRecords, clearSelectedKeys }] = useCardList({
  41 + api: async ({ page, pageSize, textSearch }) => {
  42 + const res = await edgeInstancePage({
  43 + page: page === 1 ? 0 : page,
  44 + pageSize,
  45 + textSearch,
  46 + sortProperty: 'createdTime',
  47 + sortOrder: 'DESC',
  48 + });
  49 + return {
  50 + total: res?.totalElements,
  51 + items: res?.data,
  52 + };
  53 + },
  54 + useSearchForm: true,
  55 + gutter: 4,
  56 + rowKey: 'routingKey', //id是对象
  57 + formConfig: {
  58 + schemas: searchFormSchema,
  59 + labelWidth: 100,
  60 + model: {
  61 + name: (query as Recordable)?.name ? decodeURIComponent((query as Recordable)?.name) : null,
  62 + },
  63 + },
  64 + selections: {
  65 + beforeSelectValidate: () => {
  66 + return true;
  67 + },
  68 + onSelect: (_record, _flag, allSelecteds) => {
  69 + disabledDeleteFlag.value = !allSelecteds.length;
  70 + },
  71 + onSelectAll: () => {
  72 + // 全选事件
  73 + disabledDeleteFlag.value = false;
  74 + },
  75 + onUnSelectAll: () => {
  76 + // 反选事件
  77 + disabledDeleteFlag.value = true;
  78 + },
  79 + onSelectToggle: (status: boolean) => {
  80 + // 全选是false,反选是true
  81 + if (!status) disabledDeleteFlag.value = false;
  82 + else disabledDeleteFlag.value = true;
  83 + },
  84 + },
  85 + });
  86 +
  87 + const [registerEdgeInstanceFormDrawer, { openDrawer: openEdgeInstanceFormDrawer }] = useDrawer();
  88 +
  89 + const handleEventIsSuccess = () => reload();
  90 +
  91 + const handleGoDetail = (record: EdgeInstanceItemType | null) => {
  92 + go('/edge/edge_detail/' + record?.id?.id);
  93 + };
  94 +
  95 + const handleOperationEvent = (
  96 + event: HandleOperationEnum,
  97 + record: EdgeInstanceItemType | null
  98 + ) => {
  99 + const isUpdate = event === HandleOperationEnum.CREATE ? false : true;
  100 + const isUpdateText =
  101 + event === HandleOperationEnum.CREATE
  102 + ? HandleOperationNameEnum.CREATE
  103 + : event === HandleOperationEnum.UPDATE
  104 + ? HandleOperationNameEnum.UPDATE
  105 + : HandleOperationNameEnum.VIEW;
  106 + if (event === HandleOperationEnum.VIEW) {
  107 + } else {
  108 + openEdgeInstanceFormDrawer(true, { isUpdate, record, isUpdateText, event });
  109 + }
  110 + };
  111 +
  112 + const handleDelete = async (event: HandleOperationEnum, id?: string | null) => {
  113 + try {
  114 + if (event === HandleOperationEnum.BATCH_DELETE) {
  115 + const batchDeleteIds = getSelectedRecords().map(
  116 + (rowRecord: EdgeInstanceItemType) => rowRecord?.id?.id
  117 + );
  118 + if (isArray(batchDeleteIds) && batchDeleteIds.length === 0) return;
  119 + for (let item of batchDeleteIds) await deleteEdgeInstance(item!);
  120 + } else {
  121 + await deleteEdgeInstance(id!);
  122 + }
  123 + createMessage.success('删除成功');
  124 + clearSelectedKeys();
  125 + disabledDeleteFlag.value = true;
  126 + await reload();
  127 + } catch (error) {
  128 + throw error;
  129 + }
  130 + };
  131 +</script>
  132 +
  133 +<template>
  134 + <section>
  135 + <BasicCardList @register="registerCardList">
  136 + <template #toolbar>
  137 + <Button type="primary" @click="handleOperationEvent(HandleOperationEnum.CREATE, null)"
  138 + >新增实例</Button
  139 + >
  140 + <Popconfirm
  141 + title="您确定要批量删除数据"
  142 + ok-text="确定"
  143 + cancel-text="取消"
  144 + @confirm="handleDelete(HandleOperationEnum.BATCH_DELETE, null)"
  145 + :disabled="disabledDeleteFlag"
  146 + >
  147 + <Button type="primary" danger :disabled="disabledDeleteFlag"> 批量删除 </Button>
  148 + </Popconfirm>
  149 + </template>
  150 + <template #renderItem="{ item }: BasicCardListRenderItem<EdgeInstanceItemType>">
  151 + <Card hoverable>
  152 + <template #cover>
  153 + <div class="w-full h-full !flex flex-col justify-between m-3">
  154 + <div class="!flex justify-between align-center text-center">
  155 + <Tooltip :title="item.name">
  156 + <span class="truncate font-bold fill-dark-900 text-sm"> {{ item.name }} </span>
  157 + </Tooltip>
  158 + <div class="mr-6">
  159 + <a-tag class="!flex items-center" :color="item.active ? '#E8FFEA' : '#FFECE8'">
  160 + <template #icon>
  161 + <template v-if="item.active">
  162 + <Image :width="12" :height="12" :src="edgeStatusIsOnlinePng" />
  163 + </template>
  164 + <template v-else>
  165 + <Image :width="12" :height="12" :src="edgeStatusIsOfflinePng" />
  166 + </template>
  167 + </template>
  168 + <span class="ml-1" :style="{ color: item.active ? '#00B42A' : '#F53F3F' }">{{
  169 + item.active ? '在线' : '离线'
  170 + }}</span>
  171 + </a-tag>
  172 + </div>
  173 + </div>
  174 + <div class="!flex justify-between align-center text-center">
  175 + <span class="truncate text-xs" style="color: #86909c">
  176 + {{ moment(item.createdTime).format('YYYY-MM-DD HH:mm:ss') }}
  177 + </span>
  178 + </div>
  179 + </div>
  180 + </template>
  181 + <template class="ant-card-actions" #actions>
  182 + <Tooltip title="详情">
  183 + <AuthIcon
  184 + class="!text-lg"
  185 + icon="ant-design:eye-outlined"
  186 + @click.stop="handleGoDetail(item)"
  187 + />
  188 + </Tooltip>
  189 + <Tooltip title="编辑">
  190 + <AuthIcon
  191 + class="!text-lg"
  192 + icon="ant-design:form-outlined"
  193 + @click.stop="handleOperationEvent(HandleOperationEnum.UPDATE, item)"
  194 + />
  195 + </Tooltip>
  196 + <AuthDropDown
  197 + @click.stop
  198 + :trigger="['hover']"
  199 + :drop-menu-list="[
  200 + {
  201 + text: '删除',
  202 + event: DropMenuEvent.DELETE,
  203 + icon: 'ant-design:delete-outlined',
  204 + popconfirm: {
  205 + title: '是否确认删除操作?',
  206 + onConfirm: handleDelete.bind(null, HandleOperationEnum.DELETE, item?.id?.id),
  207 + },
  208 + },
  209 + ]"
  210 + />
  211 + </template>
  212 + <Card.Meta>
  213 + <template #description>
  214 + <div class="truncate h-17 !flex justify-between flex-col">
  215 + <div class="truncate !flex">
  216 + <span class="text-xs" style="color: #86909c">标签</span>
  217 + <Tooltip :title="item.label">
  218 + <span class="truncate ml-7.5 text-xs" style="color: #00b42a">{{
  219 + item.label
  220 + }}</span>
  221 + </Tooltip>
  222 + </div>
  223 + <div class="truncate !flex">
  224 + <span class="text-xs" style="color: #86909c">边缘类型</span>
  225 + <Tooltip :title="item.type">
  226 + <span style="color: #4e5969" class="truncate ml-7.5 text-xs">{{
  227 + item.type
  228 + }}</span>
  229 + </Tooltip>
  230 + </div>
  231 + <div class="truncate !flex">
  232 + <span class="text-xs" style="color: #86909c">描述</span>
  233 + <Tooltip :title="item?.additionalInfo?.description">
  234 + <span style="color: #4e5969" class="truncate ml-7.5 text-xs">
  235 + {{ item?.additionalInfo?.description }}
  236 + </span>
  237 + </Tooltip>
  238 + </div>
  239 + </div>
  240 + </template>
  241 + </Card.Meta>
  242 + </Card>
  243 + </template>
  244 + </BasicCardList>
  245 + <EdgeInstanceFormDrawer
  246 + @register="registerEdgeInstanceFormDrawer"
  247 + @success="handleEventIsSuccess"
  248 + />
  249 + </section>
  250 +</template>
  251 +
  252 +<style lang="less" scoped>
  253 + .profile-list:deep(.ant-image-img) {
  254 + @apply !w-full !h-full;
  255 + }
  256 +
  257 + .profile-list:deep(.ant-card-body) {
  258 + @apply !p-4;
  259 + }
  260 +
  261 + :deep(.ant-card-body) {
  262 + padding: 12px;
  263 + }
  264 +</style>
... ...
  1 +<script lang="ts" setup>
  2 + import { ref } from 'vue';
  3 + import { EdgeDeviceBasicInfo } from '../EdgeDeviceBasicInfo';
  4 + import { EdgeDeviceTabInfo } from '../EdgeDeviceTabInfo';
  5 + import { EdgeDeviceItemType, EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  6 + import { infoEdgeDevice } from '/@/api/edgeManage/edgeInstance';
  7 + import { useRoute } from 'vue-router';
  8 + import { PageWrapper } from '/@/components/Page';
  9 + import { useGo } from '/@/hooks/web/usePage';
  10 +
  11 + const emits = defineEmits(['register', 'success']);
  12 +
  13 + defineProps({
  14 + recordEdgeInstanceData: {
  15 + type: Object as PropType<EdgeInstanceItemType>,
  16 + default: () => {},
  17 + },
  18 + });
  19 +
  20 + const route = useRoute();
  21 +
  22 + const go = useGo();
  23 +
  24 + const deviceId = ref(route.params?.id);
  25 +
  26 + const edgeId = ref(route.params?.edgeId as string);
  27 +
  28 + const recordData = ref<EdgeDeviceItemType>();
  29 +
  30 + const handleEventIsSuccess = () => {
  31 + emits('success');
  32 + };
  33 +
  34 + infoEdgeDevice(deviceId.value as string).then((res) => {
  35 + recordData.value = res;
  36 + });
  37 +
  38 + function goBack() {
  39 + go('/edge/edge_detail/' + edgeId.value);
  40 + }
  41 +</script>
  42 +
  43 +<template>
  44 + <PageWrapper :title="`边缘设备详情`" contentBackground @back="goBack">
  45 + <!-- 基础信息 -->
  46 + <div class="m-4">
  47 + <EdgeDeviceBasicInfo
  48 + :recordData="recordData"
  49 + :edgeId="edgeId"
  50 + @success="handleEventIsSuccess"
  51 + />
  52 + </div>
  53 + <!-- Tab信息 -->
  54 + <div>
  55 + <EdgeDeviceTabInfo v-if="recordData" :recordData="recordData" />
  56 + </div>
  57 + </PageWrapper>
  58 +</template>
  59 +
  60 +<style lang="less" scoped></style>
... ...
  1 +import { BasicColumn, FormSchema } from '/@/components/Table';
  2 +import { Tag, Tooltip } from 'ant-design-vue';
  3 +import { h } from 'vue';
  4 +import { transformTime } from '/@/hooks/web/useDateToLocaleString';
  5 +import { DeviceState, DeviceTypeEnum } from '/@/api/device/model/deviceModel';
  6 +import { handeleCopy } from '/@/views/device/profiles/step/topic';
  7 +
  8 +// 表格配置
  9 +export const columns: BasicColumn[] = [
  10 + {
  11 + title: '设备状态',
  12 + dataIndex: 'deviceState',
  13 + width: 100,
  14 + customRender: ({ record }) => {
  15 + const color =
  16 + record.deviceState == DeviceState.INACTIVE
  17 + ? 'warning'
  18 + : record.deviceState == DeviceState.ONLINE
  19 + ? 'success'
  20 + : 'error';
  21 + const text =
  22 + record.deviceState == DeviceState.INACTIVE
  23 + ? '待激活'
  24 + : record.deviceState == DeviceState.ONLINE
  25 + ? '在线'
  26 + : '离线';
  27 + return h(Tag, { color: color }, () => text);
  28 + },
  29 + },
  30 + {
  31 + dataIndex: 'name',
  32 + title: '别名/设备名称',
  33 + width: 210,
  34 + slots: { customRender: 'name', title: 'deviceTitle' },
  35 + customRender: ({ record }) => {
  36 + return h('div', { style: 'display:flex;flex-direction:column' }, [
  37 + record.alias &&
  38 + h(
  39 + 'div',
  40 + {
  41 + class: 'cursor-pointer truncate',
  42 + },
  43 + h(
  44 + Tooltip,
  45 + {
  46 + placement: 'topLeft',
  47 + title: `${record.alias}`,
  48 + },
  49 + () => `${record.alias}`
  50 + )
  51 + ),
  52 + h(
  53 + 'div',
  54 + {
  55 + class: 'cursor-pointer text-blue-500 truncate',
  56 + onClick: () => {
  57 + handeleCopy(`${record.name}`);
  58 + },
  59 + },
  60 + h(
  61 + Tooltip,
  62 + {
  63 + placement: 'topLeft',
  64 + title: `${record.name}`,
  65 + },
  66 + () => `${record.name}`
  67 + )
  68 + ),
  69 + ]);
  70 + },
  71 + },
  72 + {
  73 + title: '所属产品',
  74 + width: 160,
  75 + dataIndex: 'deviceProfileName',
  76 + },
  77 + {
  78 + title: '所属组织',
  79 + dataIndex: 'organizationDTO.name',
  80 + width: 160,
  81 + },
  82 + {
  83 + title: '设备类型',
  84 + width: 100,
  85 + dataIndex: 'deviceType',
  86 + customRender: ({ record }) => {
  87 + const color = 'success';
  88 + const text =
  89 + record.deviceType === DeviceTypeEnum.GATEWAY
  90 + ? '网关设备'
  91 + : record.deviceType === DeviceTypeEnum.DIRECT_CONNECTION
  92 + ? '直连设备'
  93 + : '网关子设备';
  94 + return h(Tag, { color: color }, () => text);
  95 + },
  96 + },
  97 + {
  98 + title: '创建时间',
  99 + width: 120,
  100 + dataIndex: 'createdTime',
  101 + format: (_text: string, record: Recordable) => {
  102 + return transformTime(record.createdTime);
  103 + },
  104 + },
  105 +];
  106 +
  107 +// 表格查询表单
  108 +export const searchFormSchema: FormSchema[] = [
  109 + {
  110 + field: 'textSearch',
  111 + label: '设备名称',
  112 + component: 'Input',
  113 + colProps: { span: 6 },
  114 + componentProps: {
  115 + maxLength: 255,
  116 + placeholder: '请输入设备名称',
  117 + },
  118 + },
  119 +];
... ...
  1 +export { default as EdgeDevice } from './index.vue';
  2 +export { default as EdgeDeviceDetail } from './EdgeDeviceDetailDrawer.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { CSSProperties, h, ref } from 'vue';
  3 + import { EdgeDeviceItemType, EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  4 + import { BasicTable, useTable, TableAction } from '/@/components/Table';
  5 + import { columns, searchFormSchema } from './config';
  6 + import { edgeDeviceDeleteDistribution, edgeDevicePage } from '/@/api/edgeManage/edgeInstance';
  7 + import { useModal } from '/@/components/Modal';
  8 + import { EdgeDeviceDistribution } from '../EdgeDeviceDistribution';
  9 + import { useMessage } from '/@/hooks/web/useMessage';
  10 + import { useRoute } from 'vue-router';
  11 + import { useGo } from '/@/hooks/web/usePage';
  12 + import { Badge } from 'ant-design-vue';
  13 + import Icon from '/@/components/Icon';
  14 +
  15 + defineEmits(['register']);
  16 +
  17 + defineProps({
  18 + recordData: {
  19 + type: Object as PropType<EdgeInstanceItemType>,
  20 + default: () => {},
  21 + },
  22 + });
  23 +
  24 + const route = useRoute();
  25 +
  26 + const edgeId = ref(route.params?.id as string);
  27 +
  28 + const go = useGo();
  29 +
  30 + const { createMessage } = useMessage();
  31 +
  32 + const [registerEdgeDeviceDistributionModal, { openModal }] = useModal();
  33 +
  34 + const handleEventIsDistribution = () => {
  35 + openModal(true, {
  36 + record: 1,
  37 + });
  38 + };
  39 +
  40 + const handleEventIsCancelDistribution = async (record: EdgeDeviceItemType) => {
  41 + await edgeDeviceDeleteDistribution(edgeId.value as string, record.id.id);
  42 + createMessage.success('取消分配成功');
  43 + reload();
  44 + };
  45 +
  46 + const AlarmDetailActionButton = ({ hasAlarm }: { hasAlarm?: boolean }) =>
  47 + h(
  48 + Badge,
  49 + { offset: [0, -5] },
  50 + {
  51 + default: () => h('span', { style: { color: '#377dff' } }, '详情'),
  52 + count: () =>
  53 + h(
  54 + 'div',
  55 + {
  56 + style: {
  57 + visibility: hasAlarm ? 'visible' : 'hidden',
  58 + width: '14px',
  59 + height: '14px',
  60 + display: 'flex',
  61 + justifyContent: 'center',
  62 + alignItems: 'center',
  63 + border: '1px solid #f46161',
  64 + borderRadius: '50%',
  65 + } as CSSProperties,
  66 + },
  67 + h(Icon, { icon: 'mdi:bell-warning', color: '#f46161', size: 12 })
  68 + ),
  69 + }
  70 + );
  71 +
  72 + const [registerTable, { reload }] = useTable({
  73 + title: '边缘设备',
  74 + columns,
  75 + api: async ({ page, pageSize, textSearch }) => {
  76 + const res = await edgeDevicePage(
  77 + {
  78 + page: page - 1 < 0 ? 0 : page - 1,
  79 + pageSize,
  80 + textSearch,
  81 + sortProperty: 'createdTime',
  82 + sortOrder: 'DESC',
  83 + },
  84 + edgeId.value as string
  85 + );
  86 + return {
  87 + total: res?.totalElements,
  88 + items: res?.data,
  89 + };
  90 + },
  91 + formConfig: {
  92 + labelWidth: 100,
  93 + schemas: searchFormSchema,
  94 + },
  95 + useSearchForm: true,
  96 + showIndexColumn: false,
  97 + clickToRowSelect: false,
  98 + showTableSetting: true,
  99 + bordered: true,
  100 + rowKey: 'name',
  101 + actionColumn: {
  102 + width: 140,
  103 + title: '操作',
  104 + slots: { customRender: 'action' },
  105 + fixed: 'right',
  106 + },
  107 + });
  108 +
  109 + const handleEventIsSuccess = () => reload();
  110 +
  111 + function handleGoDeviceDetail(record: Recordable) {
  112 + go(`/edge/edge_device/edge_device_detail/${record?.id?.id}/${edgeId.value}`);
  113 + }
  114 +</script>
  115 +
  116 +<template>
  117 + <BasicTable :clickToRowSelect="false" @register="registerTable">
  118 + <template #toolbar>
  119 + <a-button type="primary" @click="handleEventIsDistribution"> 分配设备 </a-button>
  120 + </template>
  121 + <template #action="{ record }">
  122 + <TableAction
  123 + :actions="[
  124 + {
  125 + // label: '详情',
  126 + label: AlarmDetailActionButton({ hasAlarm: !!record.alarmStatus }),
  127 + icon: 'ant-design:eye-outlined',
  128 + onClick: handleGoDeviceDetail.bind(null, record),
  129 + },
  130 + {
  131 + label: '取消分配',
  132 + icon: 'mdi:account-arrow-left',
  133 + onClick: handleEventIsCancelDistribution.bind(null, record),
  134 + },
  135 + ]"
  136 + />
  137 + </template>
  138 + </BasicTable>
  139 + <EdgeDeviceDistribution
  140 + @register="registerEdgeDeviceDistributionModal"
  141 + :edgeId="edgeId"
  142 + @success="handleEventIsSuccess"
  143 + />
  144 +</template>
  145 +
  146 +<style lang="less" scoped></style>
... ...
  1 +import { findDictItemByCode } from '/@/api/system/dict';
  2 +import { BasicColumn, FormSchema } from '/@/components/Table';
  3 +import { AlarmStatus } from '/@/enums/alarmEnum';
  4 +import { alarmLevel } from '/@/views/device/list/config/detail.config';
  5 +import { Tag } from 'ant-design-vue';
  6 +import { h } from 'vue';
  7 +
  8 +// 表格配置
  9 +export const columns: BasicColumn[] = [
  10 + {
  11 + title: '告警状态',
  12 + dataIndex: 'status',
  13 + customRender({ record }: { record }) {
  14 + const flag = !!record.cleared;
  15 + return h(Tag, { color: flag ? 'green' : 'red' }, () => (flag ? '清除' : '激活'));
  16 + },
  17 + width: 90,
  18 + },
  19 + {
  20 + title: '告警设备',
  21 + dataIndex: 'deviceName',
  22 + customRender: ({ record }) => {
  23 + const { deviceAlias, deviceName } = record || {};
  24 + return deviceAlias || deviceName;
  25 + },
  26 + },
  27 + {
  28 + title: '告警场景',
  29 + dataIndex: 'type',
  30 + },
  31 + {
  32 + title: '告警级别',
  33 + dataIndex: 'severity',
  34 + format: (text) => alarmLevel(text),
  35 + },
  36 + {
  37 + title: '告警时间',
  38 + dataIndex: 'createdTime',
  39 + },
  40 + {
  41 + title: '告警详情',
  42 + dataIndex: 'details',
  43 + slots: { customRender: 'details' },
  44 + },
  45 +];
  46 +
  47 +// 表格查询表单
  48 +export const searchFormSchema: FormSchema[] = [
  49 + {
  50 + field: 'status',
  51 + label: '告警/确认状态',
  52 + component: 'Cascader',
  53 + helpMessage: [
  54 + '激活未确认: 可以处理,清除',
  55 + '激活已确认: 只可清除,已经处理',
  56 + '清除未确认: 只可处理,已经清除',
  57 + '清除已确认: 不需要做处理和清除',
  58 + ],
  59 + colProps: { span: 6 },
  60 + componentProps: {
  61 + popupClassName: 'alarm-stauts-cascader',
  62 + options: [
  63 + {
  64 + value: '0',
  65 + label: '清除',
  66 + children: [
  67 + {
  68 + value: AlarmStatus.CLEARED_UN_ACK,
  69 + label: '清除未确认',
  70 + },
  71 + {
  72 + value: AlarmStatus.CLEARED_ACK,
  73 + label: '清除已确认',
  74 + },
  75 + ],
  76 + },
  77 + {
  78 + value: '1',
  79 + label: '激活',
  80 + children: [
  81 + {
  82 + value: AlarmStatus.ACTIVE_UN_ACK,
  83 + label: '激活未确认',
  84 + },
  85 + {
  86 + value: AlarmStatus.ACTIVE_ACK,
  87 + label: '激活已确认',
  88 + },
  89 + ],
  90 + },
  91 + ],
  92 + placeholder: '请选择告警/确认状态',
  93 + },
  94 + },
  95 + {
  96 + field: 'severity',
  97 + label: '告警级别',
  98 + component: 'ApiSelect',
  99 + colProps: { span: 6 },
  100 + componentProps: {
  101 + placeholder: '请选择告警级别',
  102 + api: findDictItemByCode,
  103 + params: {
  104 + dictCode: 'severity_type',
  105 + },
  106 + labelField: 'itemText',
  107 + valueField: 'itemValue',
  108 + },
  109 + },
  110 +];
... ...
  1 +export { default as EdgeDeviceAlarm } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { BasicTable, useTable, TableAction } from '/@/components/Table';
  3 + import { columns, searchFormSchema } from './config';
  4 + import { BasicModal, useModal } from '/@/components/Modal';
  5 + import { Input, Button } from 'ant-design-vue';
  6 + import { ref, computed, nextTick } from 'vue';
  7 + import { EyeOutlined } from '@ant-design/icons-vue';
  8 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  9 + import { getDeviceAlarm, doBatchAckAlarm, doBatchClearAlarm } from '/@/api/device/deviceManager';
  10 + import { AlarmLogItem } from '/@/api/device/model/deviceConfigModel';
  11 + import { AlarmStatus } from '/@/enums/alarmEnum';
  12 + import { useMessage } from '/@/hooks/web/useMessage';
  13 + import { getDeviceDetail } from '/@/api/device/deviceManager';
  14 + import { getAttribute } from '/@/api/ruleengine/ruleengineApi';
  15 + import {
  16 + operationBoolean,
  17 + operationNumber_OR_TIME,
  18 + operationString,
  19 + } from '/@/views/rule/linkedge/config/formatData';
  20 + import { clearOrAckAlarm } from '/@/api/device/deviceManager';
  21 +
  22 + const props = defineProps({
  23 + recordData: {
  24 + type: Object as PropType<EdgeDeviceItemType>,
  25 + default: () => {},
  26 + },
  27 + });
  28 +
  29 + defineEmits(['register']);
  30 +
  31 + const outputData = ref<string>();
  32 +
  33 + const { createMessage } = useMessage();
  34 +
  35 + const [registerTable, { reload, getSelectRows, clearSelectedRowKeys, getRowSelection }] =
  36 + useTable({
  37 + columns,
  38 + api: getDeviceAlarm,
  39 + beforeFetch: (params) => {
  40 + const { status } = params;
  41 + const obj = {
  42 + ...params,
  43 + deviceId: props?.recordData?.id?.id,
  44 + ...{
  45 + status: status ? status.at(-1) : null,
  46 + },
  47 + };
  48 + return obj;
  49 + },
  50 + formConfig: {
  51 + labelWidth: 130,
  52 + schemas: searchFormSchema,
  53 + },
  54 + useSearchForm: true,
  55 + showIndexColumn: false,
  56 + clickToRowSelect: false,
  57 + showTableSetting: true,
  58 + bordered: true,
  59 + rowKey: 'id',
  60 + rowSelection: {
  61 + type: 'checkbox',
  62 + getCheckboxProps: (record: AlarmLogItem) => {
  63 + return {
  64 + disabled: record.status === AlarmStatus.CLEARED_ACK,
  65 + };
  66 + },
  67 + },
  68 + actionColumn: {
  69 + title: '操作',
  70 + slots: { customRender: 'action' },
  71 + fixed: 'right',
  72 + },
  73 + });
  74 +
  75 + const [registerModal, { openModal, closeModal }] = useModal();
  76 +
  77 + const findName = (item: Recordable, curr: Recordable) => {
  78 + return item.attribute.find((item) => item.identifier === curr?.key)?.name;
  79 + };
  80 +
  81 + const findLogin = (curr: Recordable) => {
  82 + return [...operationNumber_OR_TIME, ...operationString, ...operationBoolean].find(
  83 + (item) => item.value === curr?.logic
  84 + )?.symbol;
  85 + };
  86 +
  87 + const findAttribute = (item: Recordable, curr: Recordable) => {
  88 + item.attribute.find((findItem) => findItem.identifier === curr?.key);
  89 + };
  90 +
  91 + const findValue = (item: Recordable, curr: Recordable) => {
  92 + return {
  93 + ['触发属性']: findName(item, curr),
  94 + ['触发条件']: `${findLogin(curr)}${curr?.logicValue}`,
  95 + ['触发值']: `${curr?.realValue}${
  96 + (findAttribute(item, curr) as any)?.detail?.dataType?.specs?.unit?.key ?? ''
  97 + }`,
  98 + };
  99 + };
  100 +
  101 + const handleAlarmText = (text: string) => (text === 'triggerData' ? '触发器' : '执行条件');
  102 +
  103 + const handleViewDetail = async (record: Recordable) => {
  104 + await nextTick();
  105 + const { details } = record;
  106 + if (!details) return;
  107 + const deviceIdKeys = Object.keys(details);
  108 + const dataFormat = await handleAlarmDetailFormat(deviceIdKeys);
  109 + const mapDataFormat = deviceIdKeys.map((deviceKey: string) => {
  110 + const findDataFormat = dataFormat.find(
  111 + (dataItem: Recordable) => dataItem.tbDeviceId === deviceKey
  112 + );
  113 + const dataKeys = Object.keys(details[deviceKey]);
  114 + const data: any = dataKeys.map((dataItem: string) => {
  115 + if (dataItem !== 'triggerData' && dataItem !== 'conditionData') {
  116 + return findValue(findDataFormat, details[deviceKey]);
  117 + } else {
  118 + return {
  119 + [handleAlarmText(dataItem)]: findValue(findDataFormat, details[deviceKey][dataItem]),
  120 + };
  121 + }
  122 + });
  123 + const objectDataFormat = data.reduce((acc: Recordable, curr: Recordable) => {
  124 + return {
  125 + ...acc,
  126 + ...curr,
  127 + };
  128 + });
  129 + return {
  130 + [findDataFormat.name]: objectDataFormat,
  131 + };
  132 + });
  133 + const objectDataFormats = mapDataFormat.reduce((acc: Recordable, curr: Recordable) => {
  134 + return {
  135 + ...acc,
  136 + ...curr,
  137 + };
  138 + });
  139 + outputData.value = JSON.stringify(objectDataFormats, null, 2);
  140 + openModal(true);
  141 + };
  142 +
  143 + const handleAlarmDetailFormat = async (keys: string[]) => {
  144 + const temp: Recordable = [];
  145 + for (let item of keys) {
  146 + if (item === 'key' || item === 'data') return []; //旧数据则终止
  147 + const deviceDetailRes = await getDeviceDetail(item);
  148 + const { deviceProfileId } = deviceDetailRes;
  149 + if (!deviceProfileId) return [];
  150 + const attributeRes = await getAttribute(deviceProfileId);
  151 + const dataFormat: Recordable = handleDataFormat(deviceDetailRes, attributeRes);
  152 + temp.push(dataFormat);
  153 + }
  154 + return temp;
  155 + };
  156 +
  157 + const handleDataFormat = (deviceDetail: Recordable, attributes: Recordable) => {
  158 + const { name, tbDeviceId, alias } = deviceDetail;
  159 + const attribute = attributes.map((item) => ({
  160 + identifier: item.identifier,
  161 + name: item.name,
  162 + detail: item.detail,
  163 + }));
  164 + return {
  165 + name: alias || name,
  166 + tbDeviceId,
  167 + attribute,
  168 + };
  169 + };
  170 +
  171 + const handleEventIsDone = async (record: Recordable) => {
  172 + if (!record.id) return;
  173 + await clearOrAckAlarm(record.id, false);
  174 + createMessage.success('操作成功');
  175 + reload();
  176 + };
  177 +
  178 + const handleEventIsClear = async (record: Recordable) => {
  179 + if (!record.id) return;
  180 + await clearOrAckAlarm(record.id, true);
  181 + createMessage.success('操作成功');
  182 + reload();
  183 + };
  184 +
  185 + const getCanBatchClear = computed(() => {
  186 + const rowSelection = getRowSelection();
  187 + const getRows: AlarmLogItem[] = getSelectRows();
  188 + return !rowSelection.selectedRowKeys?.length || !getRows.every((item) => !item.cleared);
  189 + });
  190 +
  191 + const getCanBatchAck = computed(() => {
  192 + const rowSelection = getRowSelection();
  193 + const getRows: AlarmLogItem[] = getSelectRows();
  194 + return !rowSelection.selectedRowKeys?.length || !getRows.every((item) => !item.acknowledged);
  195 + });
  196 +
  197 + const handleBatchClear = async () => {
  198 + const ids = getSelectRows<AlarmLogItem>().map((item) => item.id);
  199 + if (!ids.length) return;
  200 + await doBatchClearAlarm(ids);
  201 + createMessage.success('操作成功');
  202 + clearSelectedRowKeys();
  203 + reload();
  204 + };
  205 +
  206 + const handleBatchAck = async () => {
  207 + const ids = getSelectRows<AlarmLogItem>().map((item) => item.id);
  208 + if (!ids.length) return;
  209 + await doBatchAckAlarm(ids);
  210 + createMessage.success('操作成功');
  211 + clearSelectedRowKeys();
  212 + reload();
  213 + };
  214 +</script>
  215 +
  216 +<template>
  217 + <div>
  218 + <BasicTable @register="registerTable">
  219 + <template #details="{ record }">
  220 + <span class="cursor-pointer text-blue-500" @click="handleViewDetail(record)">
  221 + <EyeOutlined class="svg:text-blue-500" />
  222 + <span class="ml-2">查看告警详情</span>
  223 + </span>
  224 + </template>
  225 + <template #action="{ record }">
  226 + <TableAction
  227 + :actions="[
  228 + {
  229 + label: '处理',
  230 + icon: 'ant-design:edit-outlined',
  231 + onClick: handleEventIsDone.bind(null, record),
  232 + ifShow: !record.acknowledged,
  233 + },
  234 + {
  235 + label: '清除',
  236 + icon: 'ant-design:close-circle-outlined',
  237 + onClick: handleEventIsClear.bind(null, record),
  238 + ifShow: !record.cleared,
  239 + },
  240 + ]"
  241 + />
  242 + </template>
  243 + <template #toolbar>
  244 + <a-button danger :disabled="getCanBatchClear" @click="handleBatchClear">
  245 + 批量清除
  246 + </a-button>
  247 + <Button @click="handleBatchAck" type="primary" :disabled="getCanBatchAck">
  248 + <span>批量处理</span>
  249 + </Button>
  250 + </template>
  251 + </BasicTable>
  252 + <BasicModal title="告警详情" @register="registerModal" @ok="closeModal">
  253 + <Input.TextArea v-model:value="outputData" :autosize="true" />
  254 + </BasicModal>
  255 + </div>
  256 +</template>
  257 +
  258 +<style>
  259 + .alarm-stauts-cascader {
  260 + .ant-cascader-menu {
  261 + height: fit-content;
  262 + }
  263 + }
  264 +</style>
... ...
  1 +import { Tag, Tooltip } from 'ant-design-vue';
  2 +import { DeviceTypeEnum } from '/@/api/device/model/deviceModel';
  3 +import { DescItem } from '/@/components/Description/src/typing';
  4 +import { formatToDateTime } from '/@/utils/dateUtil';
  5 +import { CSSProperties, h } from 'vue';
  6 +
  7 +export const descSchema = (): DescItem[] => {
  8 + return [
  9 + {
  10 + field: 'name',
  11 + label: '设备名称',
  12 + render(val, data: Record<'alias' | 'name', string>) {
  13 + return h(Tooltip, { title: data.alias || val }, () =>
  14 + h('span', { style: { cursor: 'pointer' } as CSSProperties }, data.alias || val)
  15 + );
  16 + },
  17 + },
  18 + {
  19 + field: 'label',
  20 + label: '设备标签',
  21 + render: (text) => {
  22 + return text
  23 + ? h(
  24 + Tag,
  25 + {
  26 + color: '#00B42A',
  27 + },
  28 + text
  29 + )
  30 + : '';
  31 + },
  32 + },
  33 + {
  34 + field: 'gatewayName',
  35 + label: '所属网关',
  36 + render: (text) => {
  37 + return text
  38 + ? h(
  39 + Tag,
  40 + {
  41 + color: '#00B42A',
  42 + },
  43 + text
  44 + )
  45 + : '';
  46 + },
  47 + },
  48 + {
  49 + field: 'deviceProfileName',
  50 + label: '产品',
  51 + },
  52 + {
  53 + field: 'deviceType',
  54 + label: '设备类型',
  55 + render: (_, data) => {
  56 + const text =
  57 + data.deviceType === DeviceTypeEnum.GATEWAY
  58 + ? '网关设备'
  59 + : data.deviceType === DeviceTypeEnum.DIRECT_CONNECTION
  60 + ? '直连设备'
  61 + : '网关子设备';
  62 + return h(
  63 + 'span',
  64 + {
  65 + style: { cursor: 'pointer' },
  66 + },
  67 + text
  68 + );
  69 + },
  70 + },
  71 + {
  72 + field: 'createdTime',
  73 + label: '创建时间',
  74 + render: (_, data) => {
  75 + return formatToDateTime(data.createdTime, 'YYYY-MM-DD HH:mm:ss');
  76 + },
  77 + },
  78 + {
  79 + field: 'additionalInfo.description',
  80 + label: '描述',
  81 + },
  82 + ];
  83 +};
... ...
  1 +export { default as EdgeDeviceBasicInfo } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { Description, useDescription } from '/@/components/Description';
  3 + import { descSchema } from './config';
  4 + import type { PropType } from 'vue';
  5 + import { unref } from 'vue';
  6 + import edgeDevicePng from '/@/assets/images/edgeDevice.png';
  7 + import { Image, Popconfirm } from 'ant-design-vue';
  8 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  9 + import { getDeviceToken } from '/@/api/device/deviceManager';
  10 + import { useClipboard } from '@vueuse/core';
  11 + import { useMessage } from '/@/hooks/web/useMessage';
  12 + import ManageDeviceTokenModal from '/@/views/device/list/cpns/modal/ManageDeviceTokenModal.vue';
  13 + import { useModal } from '/@/components/Modal';
  14 + import { edgeDeviceDeleteDistribution } from '/@/api/edgeManage/edgeInstance';
  15 + import { useGo } from '/@/hooks/web/usePage';
  16 +
  17 + const emits = defineEmits(['success']);
  18 +
  19 + const props = defineProps({
  20 + recordData: {
  21 + type: Object as PropType<EdgeDeviceItemType>,
  22 + default: () => {},
  23 + },
  24 + edgeId: {
  25 + type: String,
  26 + default: '',
  27 + },
  28 + });
  29 +
  30 + const CS = {
  31 + 'word-break': 'break-all',
  32 + overflow: 'hidden',
  33 + display: '-webkit-box',
  34 + '-webkit-line-clamp': 2,
  35 + '-webkit-box-orient': 'vertical',
  36 + };
  37 +
  38 + const [register] = useDescription({
  39 + layout: 'vertical',
  40 + schema: descSchema(),
  41 + column: 5,
  42 + });
  43 +
  44 + const { createMessage } = useMessage();
  45 +
  46 + const go = useGo();
  47 +
  48 + const { copied, copy } = useClipboard({ legacy: true });
  49 +
  50 + const handleEventIsCopyDeviceToken = async () => {
  51 + if (!props?.recordData?.id?.id) return;
  52 + const token = await getDeviceToken(props?.recordData?.id?.id);
  53 + if (token.credentialsType === 'ACCESS_TOKEN') {
  54 + await copy(token.credentialsId);
  55 + } else {
  56 + await copy(token.credentialsValue);
  57 + }
  58 + if (unref(copied)) {
  59 + createMessage.success('复制成功');
  60 + }
  61 + };
  62 +
  63 + const [registerModal, { openModal }] = useModal();
  64 +
  65 + const handleEventIsManageDeviceToken = async () => {
  66 + if (!props?.recordData?.id?.id) return;
  67 + const token = await getDeviceToken(props?.recordData?.id?.id);
  68 + openModal(true, token);
  69 + };
  70 +
  71 + const handleEventIsCancelDistribution = async () => {
  72 + await edgeDeviceDeleteDistribution(props.edgeId, props.recordData.id?.id);
  73 + createMessage.success('取消分配成功');
  74 + emits('success');
  75 + goBack();
  76 + };
  77 +
  78 + function goBack() {
  79 + go('/edge/edge_device/' + props.edgeId);
  80 + }
  81 +</script>
  82 +
  83 +<template>
  84 + <a-row :gutter="{ xs: 8, sm: 16, md: 24, lg: 32 }">
  85 + <a-col class="gutter-row" :span="3">
  86 + <div class="!flex flex-col justify-between items-center">
  87 + <div><Image :src="recordData?.deviceInfo?.avatar || edgeDevicePng" :width="180" /></div>
  88 + <div class="!flex flex-col mt-3">
  89 + <span style="color: #1d2129" class="font-bold">{{
  90 + recordData?.alias ? recordData?.alias : recordData?.name
  91 + }}</span>
  92 + <span style="color: #3d3d3d">边缘设备详情</span>
  93 + </div>
  94 + </div>
  95 + </a-col>
  96 + <a-col class="gutter-row" :span="21">
  97 + <div class="!flex flex-col justify-between">
  98 + <Description v-if="recordData" @register="register" :data="recordData" :contentStyle="CS" />
  99 + <div class="!flex mt-3">
  100 + <a-button type="primary" @click="handleEventIsCopyDeviceToken">复制访问令牌</a-button>
  101 + <a-button class="ml-4" type="primary" @click="handleEventIsManageDeviceToken"
  102 + >管理凭证</a-button
  103 + >
  104 + <Popconfirm
  105 + title="您是否要取消分配边缘"
  106 + ok-text="是"
  107 + cancel-text="否"
  108 + @confirm="handleEventIsCancelDistribution"
  109 + >
  110 + <a-button class="ml-4" type="primary">取消分配边缘</a-button>
  111 + </Popconfirm>
  112 + </div>
  113 + </div>
  114 + </a-col>
  115 + </a-row>
  116 + <ManageDeviceTokenModal @register="registerModal" />
  117 +</template>
  118 +
  119 +<style lang="less" scoped>
  120 + :deep(.ant-image-img) {
  121 + height: 157px;
  122 + }
  123 +</style>
... ...
  1 +import { ModelOfMatterParams } from '/@/api/device/model/modelOfMatterModel';
  2 +import { getModelServices } from '/@/api/device/modelOfMatter';
  3 +import { FormProps, FormSchema, useComponentRegister } from '/@/components/Form';
  4 +import { validateTCPCustomCommand } from '/@/components/Form/src/components/ThingsModelForm';
  5 +import { JSONEditor, JSONEditorValidator } from '/@/components/CodeEditor';
  6 +import {
  7 + TransportTypeEnum,
  8 + CommandTypeNameEnum,
  9 + CommandTypeEnum,
  10 + ServiceCallTypeEnum,
  11 + CommandDeliveryWayEnum,
  12 + CommandDeliveryWayNameEnum,
  13 + TCPProtocolTypeEnum,
  14 +} from '/@/enums/deviceEnum';
  15 +import { DeviceTypeEnum } from '/@/api/device/model/deviceModel';
  16 +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  17 +
  18 +export interface CommandDeliveryFormFieldType {
  19 + [CommandFieldsEnum.COMMAND_TYPE]: CommandTypeEnum;
  20 + [CommandFieldsEnum.TCP_COMMAND_VALUE]?: string;
  21 + [CommandFieldsEnum.COMAND_VALUE]?: string;
  22 + [CommandFieldsEnum.SERVICE]?: string;
  23 + [CommandFieldsEnum.MODEL_INPUT]?: ModelOfMatterParams;
  24 + [CommandFieldsEnum.CALL_TYPE]: CommandDeliveryWayEnum;
  25 +}
  26 +
  27 +export enum CommandFieldsEnum {
  28 + COMMAND_TYPE = 'commandType',
  29 + TCP_COMMAND_VALUE = 'tcpCommandValue',
  30 + COMAND_VALUE = 'commandValue',
  31 + SERVICE = 'service',
  32 + MODEL_INPUT = 'modelInput',
  33 + CALL_TYPE = 'callType',
  34 +
  35 + SERVICE_COMMAND = 'serviceCommand',
  36 +
  37 + SERVICE_OBJECT_MODEL = 'serviceObjectModel',
  38 +}
  39 +
  40 +useComponentRegister('JSONEditor', JSONEditor);
  41 +
  42 +export const CommandSchemas = (deviceRecord: EdgeDeviceItemType): FormSchema[] => {
  43 + const { deviceData, deviceProfileId, deviceType } = deviceRecord;
  44 +
  45 + const isTCPTransport = deviceData.transportConfiguration.type === TransportTypeEnum.TCP;
  46 +
  47 + const isTCPModbus =
  48 + isTCPTransport &&
  49 + deviceRecord.deviceProfile?.profileData?.transportConfiguration?.protocol ===
  50 + TCPProtocolTypeEnum.MODBUS_RTU;
  51 +
  52 + return [
  53 + {
  54 + field: CommandFieldsEnum.COMMAND_TYPE,
  55 + component: 'RadioGroup',
  56 + label: '下发类型',
  57 + defaultValue: CommandTypeEnum.CUSTOM,
  58 + required: true,
  59 + componentProps: ({ formActionType }) => {
  60 + const { setFieldsValue } = formActionType;
  61 +
  62 + const getOptions = () => {
  63 + const options = [{ label: CommandTypeNameEnum.CUSTOM, value: CommandTypeEnum.CUSTOM }];
  64 +
  65 + if (isTCPModbus || (isTCPTransport && deviceType === DeviceTypeEnum.SENSOR))
  66 + return options;
  67 +
  68 + options.push({ label: CommandTypeNameEnum.SERVICE, value: CommandTypeEnum.SERVICE });
  69 +
  70 + return options;
  71 + };
  72 + return {
  73 + options: getOptions(),
  74 + onChange() {
  75 + setFieldsValue({
  76 + [CommandFieldsEnum.SERVICE]: null,
  77 + [CommandFieldsEnum.MODEL_INPUT]: null,
  78 + [CommandFieldsEnum.COMAND_VALUE]: null,
  79 + [CommandFieldsEnum.TCP_COMMAND_VALUE]: null,
  80 + });
  81 + },
  82 + };
  83 + },
  84 + },
  85 + {
  86 + field: CommandFieldsEnum.CALL_TYPE,
  87 + component: 'RadioGroup',
  88 + label: '单向/双向',
  89 + required: true,
  90 + defaultValue: CommandDeliveryWayEnum.ONE_WAY,
  91 + ifShow: ({ model }) => model[CommandFieldsEnum.COMMAND_TYPE] === CommandTypeEnum.CUSTOM,
  92 + componentProps: {
  93 + options: Object.keys(CommandDeliveryWayEnum).map((key) => ({
  94 + label: CommandDeliveryWayNameEnum[key],
  95 + value: CommandDeliveryWayEnum[key],
  96 + })),
  97 + },
  98 + },
  99 + {
  100 + field: CommandFieldsEnum.TCP_COMMAND_VALUE,
  101 + label: '命令',
  102 + required: true,
  103 + ifShow: ({ model }) =>
  104 + deviceData.transportConfiguration.type === TransportTypeEnum.TCP &&
  105 + model[CommandFieldsEnum.COMMAND_TYPE] === CommandTypeEnum.CUSTOM,
  106 + component: 'Input',
  107 + rules: [{ validator: validateTCPCustomCommand }],
  108 + componentProps: {
  109 + placeholder: '请输入命令',
  110 + },
  111 + },
  112 + {
  113 + field: CommandFieldsEnum.COMAND_VALUE,
  114 + label: '命令',
  115 + component: 'JSONEditor',
  116 + colProps: { span: 20 },
  117 + changeEvent: 'update:value',
  118 + valueField: 'value',
  119 + required: true,
  120 + rules: JSONEditorValidator(),
  121 + ifShow: ({ model }) =>
  122 + deviceData.transportConfiguration.type !== TransportTypeEnum.TCP &&
  123 + model[CommandFieldsEnum.COMMAND_TYPE] === CommandTypeEnum.CUSTOM,
  124 + componentProps: {
  125 + height: 250,
  126 + },
  127 + },
  128 + {
  129 + field: CommandFieldsEnum.SERVICE,
  130 + label: '服务',
  131 + component: 'ApiSelect',
  132 + required: true,
  133 + ifShow: ({ model }) => model[CommandFieldsEnum.COMMAND_TYPE] !== CommandTypeEnum.CUSTOM,
  134 + rules: [{ required: true, message: '请选择服务' }],
  135 + componentProps: ({ formActionType }) => {
  136 + const { setFieldsValue } = formActionType;
  137 + return {
  138 + api: getModelServices,
  139 + params: {
  140 + deviceProfileId: deviceProfileId.id,
  141 + },
  142 + valueField: 'identifier',
  143 + labelField: 'functionName',
  144 + getPopupContainer: () => document.body,
  145 + placeholder: '请选择服务',
  146 + onChange(
  147 + value: string,
  148 + options: ModelOfMatterParams & Record<'label' | 'value', string>
  149 + ) {
  150 + if (!value) return;
  151 + setFieldsValue({
  152 + [CommandFieldsEnum.CALL_TYPE]:
  153 + options.callType === ServiceCallTypeEnum.ASYNC
  154 + ? CommandDeliveryWayEnum.ONE_WAY
  155 + : CommandDeliveryWayEnum.TWO_WAY,
  156 + [CommandFieldsEnum.MODEL_INPUT]: null,
  157 + [CommandFieldsEnum.SERVICE_OBJECT_MODEL]: Object.assign(options, {
  158 + functionName: options.label,
  159 + identifier: options.value,
  160 + }),
  161 + });
  162 + },
  163 + };
  164 + },
  165 + },
  166 + {
  167 + field: CommandFieldsEnum.SERVICE_OBJECT_MODEL,
  168 + label: '服务物模型',
  169 + component: 'Input',
  170 + ifShow: false,
  171 + },
  172 + {
  173 + field: CommandFieldsEnum.MODEL_INPUT,
  174 + component: 'Input',
  175 + label: '输入参数',
  176 + changeEvent: 'update:value',
  177 + valueField: 'value',
  178 + ifShow: ({ model }) =>
  179 + model[CommandFieldsEnum.SERVICE] &&
  180 + model[CommandFieldsEnum.SERVICE_OBJECT_MODEL] &&
  181 + model[CommandFieldsEnum.COMMAND_TYPE] !== CommandTypeEnum.CUSTOM,
  182 + componentProps: {
  183 + formProps: {
  184 + wrapperCol: { span: 24 },
  185 + } as FormProps,
  186 + },
  187 + slot: 'serviceCommand',
  188 + },
  189 + ];
  190 +};
... ...
  1 +export { default as CommandDeliveryModal } from './index.vue';
... ...
  1 +<template>
  2 + <BasicModal
  3 + title="命令下发"
  4 + :width="650"
  5 + @register="registerModal"
  6 + @ok="handleOk"
  7 + @cancel="handleCancel"
  8 + >
  9 + <BasicForm @register="registerForm">
  10 + <template #serviceCommand="{ field, model }">
  11 + <ThingsModelForm
  12 + :disabled="
  13 + deviceDetail?.deviceData?.transportConfiguration?.type === TransportTypeEnum.TCP
  14 + "
  15 + ref="thingsModelFormRef"
  16 + v-model:value="model[field]"
  17 + :key="model[CommandFieldsEnum.SERVICE_OBJECT_MODEL]?.identifier"
  18 + :inputData="model[CommandFieldsEnum.SERVICE_OBJECT_MODEL]?.functionJson?.inputData"
  19 + :transportType="deviceDetail?.deviceData?.transportConfiguration?.type"
  20 + />
  21 + </template>
  22 + </BasicForm>
  23 + </BasicModal>
  24 +</template>
  25 +<script lang="ts" setup>
  26 + import { nextTick, ref, unref } from 'vue';
  27 + import { BasicForm, ThingsModelForm, useForm } from '/@/components/Form';
  28 + import { CommandDeliveryFormFieldType, CommandFieldsEnum, CommandSchemas } from './config';
  29 + import { useMessage } from '/@/hooks/web/useMessage';
  30 + import { CommandDeliveryWayEnum } from '/@/enums/deviceEnum';
  31 + import { CommandTypeEnum, RPCCommandMethodEnum, TransportTypeEnum } from '/@/enums/deviceEnum';
  32 + import { BasicModal, useModalInner } from '/@/components/Modal';
  33 + import { getDeviceActiveTime } from '/@/api/alarm/position';
  34 + import { RpcCommandType } from '/@/api/device/model/deviceConfigModel';
  35 + import { parseStringToJSON } from '/@/components/CodeEditor';
  36 + import { commandIssuanceApi } from '/@/api/device/deviceManager';
  37 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  38 +
  39 + defineEmits(['register']);
  40 +
  41 + const thingsModelFormRef = ref<InstanceType<typeof ThingsModelForm>>();
  42 + const props = defineProps<{
  43 + deviceDetail: EdgeDeviceItemType;
  44 + }>();
  45 +
  46 + const [registerModal, { setModalProps }] = useModalInner(
  47 + (params: ModalParamsType<EdgeDeviceItemType>) => {
  48 + const { record } = params;
  49 + setProps({
  50 + schemas: CommandSchemas(record),
  51 + });
  52 + }
  53 + );
  54 +
  55 + const { createMessage } = useMessage();
  56 +
  57 + const [registerForm, { setProps, getFieldsValue, validate, resetFields, clearValidate }] =
  58 + useForm({
  59 + labelWidth: 120,
  60 + baseColProps: { span: 20 },
  61 + labelAlign: 'right',
  62 + showSubmitButton: false,
  63 + showResetButton: false,
  64 + });
  65 +
  66 + const handleCancel = async () => {
  67 + await resetFields();
  68 + await nextTick();
  69 + await clearValidate();
  70 + };
  71 +
  72 + const handleValidate = async () => {
  73 + await validate();
  74 + await unref(thingsModelFormRef)?.validate?.();
  75 + };
  76 +
  77 + const handleValidateDeviceActive = async (): Promise<boolean> => {
  78 + const result = await getDeviceActiveTime(unref(props.deviceDetail)!.id?.id);
  79 + const [firstItem] = result;
  80 + return !!firstItem.value;
  81 + };
  82 +
  83 + const handleCommandParams = (
  84 + values: CommandDeliveryFormFieldType,
  85 + serviceCommand: Recordable
  86 + ) => {
  87 + const { commandType, service } = values;
  88 +
  89 + const isTcpDevice =
  90 + unref(props.deviceDetail)?.deviceData?.transportConfiguration?.type === TransportTypeEnum.TCP;
  91 + if (commandType === CommandTypeEnum.CUSTOM) {
  92 + if (isTcpDevice) {
  93 + const value = values.tcpCommandValue;
  94 + return value?.replaceAll(/\s/g, '');
  95 + }
  96 + return parseStringToJSON(values.commandValue!).json;
  97 + } else {
  98 + if (isTcpDevice) return Reflect.get(serviceCommand, CommandFieldsEnum.SERVICE_COMMAND);
  99 + return {
  100 + [service!]: serviceCommand,
  101 + };
  102 + }
  103 + };
  104 +
  105 + const handleOk = async () => {
  106 + await handleValidate();
  107 +
  108 + try {
  109 + setModalProps({ loading: true, confirmLoading: true });
  110 +
  111 + const values = getFieldsValue() as CommandDeliveryFormFieldType;
  112 + const { callType, commandType } = values;
  113 + const serviceCommand = unref(thingsModelFormRef)?.getFieldsValue() || {};
  114 +
  115 + if (callType === CommandDeliveryWayEnum.TWO_WAY && !(await handleValidateDeviceActive())) {
  116 + createMessage.warn('当前设备不在线');
  117 + return;
  118 + }
  119 +
  120 + const rpcCommands: RpcCommandType = {
  121 + additionalInfo: {
  122 + cmdType:
  123 + commandType === CommandTypeEnum.CUSTOM
  124 + ? CommandTypeEnum.CUSTOM
  125 + : CommandTypeEnum.SERVICE,
  126 + },
  127 + method: RPCCommandMethodEnum.THINGSKIT,
  128 + persistent: true,
  129 + params: handleCommandParams(values, serviceCommand),
  130 + };
  131 +
  132 + await commandIssuanceApi(callType, unref(props.deviceDetail)!.id?.id, rpcCommands);
  133 +
  134 + createMessage.success('命令下发成功');
  135 + } finally {
  136 + setModalProps({ loading: false, confirmLoading: false });
  137 + }
  138 + };
  139 +</script>
  140 +<style scoped lang="less"></style>
... ...
  1 +import { BasicColumn, FormSchema } from '/@/components/Table';
  2 +import moment from 'moment';
  3 +import { Tag } from 'ant-design-vue';
  4 +import { h } from 'vue';
  5 +
  6 +// 表格配置
  7 +export const columns: BasicColumn[] = [
  8 + {
  9 + title: '命令下发时间',
  10 + dataIndex: 'createTime',
  11 + format: (text) => {
  12 + return moment(text).format('YYYY-MM-DD HH:mm:ss');
  13 + },
  14 + },
  15 + {
  16 + title: '命令类型',
  17 + dataIndex: 'additionalInfo.cmdType',
  18 + format: (text) => {
  19 + return h(
  20 + Tag,
  21 + {
  22 + color: Number(text) === 1 ? 'green' : 'blue',
  23 + },
  24 + () => (Number(text) === 1 ? '服务' : '自定义')
  25 + ) as unknown as any;
  26 + },
  27 + },
  28 + {
  29 + title: '响应类型',
  30 + dataIndex: 'request.oneway',
  31 + format: (text) => {
  32 + return !text ? '双向' : '单向';
  33 + },
  34 + },
  35 + {
  36 + title: '命令状态',
  37 + dataIndex: 'status',
  38 + customRender: ({ text, record }) => {
  39 + return h(
  40 + Tag,
  41 + {
  42 + color:
  43 + text == 'EXPIRED'
  44 + ? 'red'
  45 + : text == 'DELIVERED'
  46 + ? 'blue'
  47 + : text == 'QUEUED'
  48 + ? '#00C9A7'
  49 + : text == 'TIMEOUT'
  50 + ? 'red'
  51 + : text == 'SENT'
  52 + ? '#00C9A7'
  53 + : text == 'FAILED'
  54 + ? 'red'
  55 + : text == 'SUCCESSFUL'
  56 + ? 'green'
  57 + : 'red',
  58 + },
  59 + () => record?.statusName
  60 + );
  61 + },
  62 + },
  63 + {
  64 + title: '响应内容',
  65 + dataIndex: 'response111',
  66 + slots: { customRender: 'responseContent' },
  67 + },
  68 + {
  69 + title: '命令内容',
  70 + dataIndex: 'request.body',
  71 + slots: { customRender: 'recordContent' },
  72 + },
  73 +];
  74 +
  75 +// 表格查询表单
  76 +export const searchFormSchema: FormSchema[] = [
  77 + {
  78 + field: 'status',
  79 + label: '命令状态',
  80 + component: 'Select',
  81 + colProps: { span: 6 },
  82 + componentProps: {
  83 + options: [
  84 + {
  85 + label: '队列中',
  86 + value: 'QUEUED',
  87 + },
  88 + {
  89 + label: '已发送',
  90 + value: 'SENT',
  91 + },
  92 + {
  93 + label: '发送成功',
  94 + value: 'DELIVERED',
  95 + },
  96 +
  97 + {
  98 + label: '响应成功',
  99 + value: 'SUCCESSFUL',
  100 + },
  101 + {
  102 + label: '超时',
  103 + value: 'TIMEOUT',
  104 + },
  105 + {
  106 + label: '已过期',
  107 + value: 'EXPIRED',
  108 + },
  109 + {
  110 + label: '响应失败',
  111 + value: 'FAILED',
  112 + },
  113 + {
  114 + label: '已删除',
  115 + value: 'DELETED',
  116 + },
  117 + ],
  118 + placeholder: '请选择命令状态',
  119 + },
  120 + },
  121 + {
  122 + field: 'sendTime',
  123 + label: '命令下发时间',
  124 + component: 'RangePicker',
  125 + componentProps: {
  126 + showTime: {
  127 + defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
  128 + },
  129 + },
  130 + colProps: { span: 10 },
  131 + },
  132 +];
... ...
  1 +export { default as EdgeDeviceCommand } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { BasicTable, useTable } from '/@/components/Table';
  3 + import { columns, searchFormSchema } from './config';
  4 + import { JsonPreview } from '/@/components/CodeEditor';
  5 + import { Button, Modal, Space } from 'ant-design-vue';
  6 + import { h } from 'vue';
  7 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  8 + import { deviceCommandRecordGetQuery } from '/@/api/device/deviceConfigApi';
  9 + import { useModal } from '/@/components/Modal';
  10 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  11 + import { CommandDeliveryModal } from './CommandDeliveryModal';
  12 +
  13 + const props = defineProps({
  14 + recordData: {
  15 + type: Object as PropType<EdgeDeviceItemType>,
  16 + default: () => {},
  17 + },
  18 + });
  19 +
  20 + defineEmits(['register']);
  21 +
  22 + const [registerCommandDeliverModal, { openModal }] = useModal();
  23 +
  24 + const [registerTable] = useTable({
  25 + columns,
  26 + api: deviceCommandRecordGetQuery,
  27 + beforeFetch: (params) => {
  28 + return {
  29 + ...params,
  30 + tbDeviceId: props?.recordData?.id?.id,
  31 + };
  32 + },
  33 + formConfig: {
  34 + labelWidth: 120,
  35 + schemas: searchFormSchema,
  36 + fieldMapToTime: [['sendTime', ['startTime', 'endTime'], 'x']],
  37 + },
  38 + useSearchForm: true,
  39 + showIndexColumn: false,
  40 + clickToRowSelect: false,
  41 + showTableSetting: true,
  42 + bordered: true,
  43 + rowKey: 'id',
  44 + });
  45 +
  46 + const commonModalInfo = (title, value) => {
  47 + Modal.info({
  48 + title,
  49 + width: 600,
  50 + content: h(JsonPreview, { data: value }),
  51 + });
  52 + };
  53 +
  54 + const handleRecordContent = (record) => {
  55 + if (!record?.request?.body) return;
  56 + if (Object.prototype.toString.call(record?.request?.body) !== '[object Object]') return;
  57 + const jsonParams = record?.request?.body?.params;
  58 + commonModalInfo('命令下发内容', jsonParams);
  59 + };
  60 +
  61 + const handleRecordResponseContent = (record) => {
  62 + const jsonParams = record?.response;
  63 + commonModalInfo('响应内容', jsonParams);
  64 + };
  65 +
  66 + const handleEventIsCommand = () => {
  67 + openModal(true, {
  68 + mode: DataActionModeEnum.READ,
  69 + record: props.recordData,
  70 + } as ModalParamsType<EdgeDeviceItemType>);
  71 + };
  72 +</script>
  73 +
  74 +<template>
  75 + <div>
  76 + <BasicTable :clickToRowSelect="false" @register="registerTable">
  77 + <template #toolbar>
  78 + <Space>
  79 + <Button type="primary" @click="handleEventIsCommand">命令下发</Button>
  80 + </Space>
  81 + </template>
  82 + <template #recordContent="{ record }">
  83 + <Button type="link" class="ml-2" @click="handleRecordContent(record)"> 查看 </Button>
  84 + </template>
  85 + <template #responseContent="{ record }">
  86 + <div v-if="!record.request?.oneway">
  87 + <Button v-if="record?.response === null" type="text" class="ml-2"> 未响应 </Button>
  88 + <Button v-else type="link" class="ml-2" @click="handleRecordResponseContent(record)">
  89 + 查看
  90 + </Button>
  91 + </div>
  92 + <div v-else>--</div>
  93 + </template>
  94 + </BasicTable>
  95 + <CommandDeliveryModal @register="registerCommandDeliverModal" :deviceDetail="recordData" />
  96 + </div>
  97 +</template>
... ...
  1 +export { default as EdgeDeviceDistribution } from './index.vue';
... ...
  1 +<template>
  2 + <BasicModal
  3 + v-bind="$attrs"
  4 + @register="registerModal"
  5 + title="将设备分配给边缘"
  6 + :canFullscreen="false"
  7 + centered
  8 + :minHeight="300"
  9 + @ok="handleEventIsDistribution"
  10 + @cancel="handleEventIsCancel"
  11 + okText="分配"
  12 + >
  13 + <div class="!flex items-center gap-5">
  14 + <span>分配设备</span>
  15 + <Select
  16 + v-if="edgeId"
  17 + placeholder="请选择设备"
  18 + v-model:value="deviceIds"
  19 + class="!w-1/2"
  20 + :options="selectOptions"
  21 + v-bind="createPickerSearch()"
  22 + mode="multiple"
  23 + allowClear
  24 + />
  25 + </div>
  26 + </BasicModal>
  27 +</template>
  28 +
  29 +<script lang="ts" setup>
  30 + import { BasicModal, useModalInner } from '/@/components/Modal';
  31 + import { edgeDeviceDistribution } from '/@/api/edgeManage/edgeInstance';
  32 + import { useMessage } from '/@/hooks/web/useMessage';
  33 + import { onMounted, ref } from 'vue';
  34 + import { edgeDeviceDistributionPage } from '/@/api/edgeManage/edgeInstance';
  35 + import { Select } from 'ant-design-vue';
  36 + import { isArray } from '/@/utils/is';
  37 + import { createPickerSearch } from '/@/utils/pickerSearch';
  38 +
  39 + const emits = defineEmits(['success', 'register']);
  40 +
  41 + const { createMessage } = useMessage();
  42 +
  43 + const props = defineProps({
  44 + edgeId: {
  45 + type: String,
  46 + default: '',
  47 + },
  48 + });
  49 +
  50 + const [registerModal, { closeModal }] = useModalInner((data) => {
  51 + const { _ } = data;
  52 + deviceIds.value = [];
  53 + handleGetDeviceDistributionList();
  54 + });
  55 +
  56 + const selectOptions = ref<{ label: string; value: string }[]>([]);
  57 +
  58 + const deviceIds = ref<string[]>([]);
  59 +
  60 + const handleGetDeviceDistributionList = async () => {
  61 + const { data } = await edgeDeviceDistributionPage({ page: 0, pageSize: 50 });
  62 + selectOptions.value = data?.map((dataItem: Recordable) => ({
  63 + label: dataItem.alias || dataItem.name,
  64 + value: dataItem.id.id,
  65 + })) as { label: string; value: string }[];
  66 + };
  67 +
  68 + onMounted(() => {
  69 + handleGetDeviceDistributionList();
  70 + });
  71 +
  72 + const handleEventIsDistribution = async () => {
  73 + if (!props.edgeId) return;
  74 + if (!deviceIds.value) return createMessage.error('请选择设备');
  75 + if (isArray(deviceIds.value) && deviceIds.value.length === 0)
  76 + return createMessage.error('请选择设备');
  77 + for (let item of deviceIds.value) await edgeDeviceDistribution(props?.edgeId, item);
  78 + createMessage.success('分配成功');
  79 + handleEventIsCancel();
  80 + emits('success');
  81 + handleGetDeviceDistributionList();
  82 + };
  83 +
  84 + const handleEventIsCancel = () => {
  85 + deviceIds.value.length = 0;
  86 + closeModal();
  87 + };
  88 +</script>
... ...
  1 +import moment from 'moment';
  2 +import { findDictItemByCode } from '/@/api/system/dict';
  3 +import { BasicColumn, FormSchema } from '/@/components/Table';
  4 +import {
  5 + EventType,
  6 + EventTypeColor,
  7 + EventTypeName,
  8 +} from '/@/views/device/list/cpns/tabs/EventManage/config';
  9 +import { Tag } from 'ant-design-vue';
  10 +import { h } from 'vue';
  11 +import { formatToDateTime } from '/@/utils/dateUtil';
  12 +
  13 +// 表格配置
  14 +export const columns: BasicColumn[] = [
  15 + {
  16 + title: '时间',
  17 + dataIndex: 'eventTime',
  18 + format(text) {
  19 + return formatToDateTime(text, 'YYYY-MM-DD HH:mm:ss');
  20 + },
  21 + },
  22 + {
  23 + title: '标识符',
  24 + dataIndex: 'eventIdentifier',
  25 + },
  26 + {
  27 + title: '事件名称',
  28 + dataIndex: 'eventName',
  29 + },
  30 + {
  31 + title: '事件类型',
  32 + dataIndex: 'eventType',
  33 + customRender({ text }) {
  34 + return h(
  35 + Tag,
  36 + {
  37 + color: EventTypeColor[text as EventType],
  38 + },
  39 + () => EventTypeName[text as EventType]
  40 + );
  41 + },
  42 + },
  43 + {
  44 + title: '输出参数',
  45 + dataIndex: 'outputParams',
  46 + slots: { customRender: 'outputParams' },
  47 + },
  48 +];
  49 +
  50 +export const formSchemas: FormSchema[] = [
  51 + {
  52 + field: 'eventIdentifier',
  53 + label: '标识符',
  54 + component: 'Input',
  55 + componentProps: {
  56 + placeholder: '请输入标识符',
  57 + },
  58 + colProps: { span: 6 },
  59 + },
  60 + {
  61 + field: 'eventType',
  62 + label: '事件类型',
  63 + component: 'ApiSelect',
  64 + componentProps: {
  65 + placeholder: '请选择事件类型',
  66 + api: findDictItemByCode,
  67 + params: {
  68 + dictCode: 'event_type',
  69 + },
  70 + labelField: 'itemText',
  71 + valueField: 'itemValue',
  72 + },
  73 + colProps: { span: 6 },
  74 + },
  75 + {
  76 + field: 'dateRange',
  77 + label: '时间范围',
  78 + component: 'RangePicker',
  79 + componentProps: {
  80 + showTime: {
  81 + defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
  82 + },
  83 + },
  84 + colProps: { span: 10 },
  85 + },
  86 +];
... ...
  1 +export { default as EdgeDeviceEvent } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { BasicTable, useTable } from '/@/components/Table';
  3 + import { columns, formSchemas } from './config';
  4 + import { BasicModal, useModal } from '/@/components/Modal';
  5 + import { Input } from 'ant-design-vue';
  6 + import { ref } from 'vue';
  7 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  8 + import { EventManageRequest } from '/@/api/device/model/eventManageModel';
  9 + import { getEventManageListRecord } from '/@/api/device/eventManage';
  10 + import { EyeOutlined } from '@ant-design/icons-vue';
  11 +
  12 + const props = defineProps({
  13 + recordData: {
  14 + type: Object as PropType<EdgeDeviceItemType>,
  15 + default: () => {},
  16 + },
  17 + });
  18 +
  19 + const outputData = ref<string>();
  20 +
  21 + const [registerTable] = useTable({
  22 + columns,
  23 + api: getEventManageListRecord,
  24 + fetchSetting: {
  25 + totalField: 'totalElements',
  26 + listField: 'data',
  27 + },
  28 + beforeFetch: (params: EventManageRequest) => {
  29 + const page = params.page - 1 < 0 ? 0 : params.page - 1;
  30 + const _params = Object.keys(params)
  31 + .filter((key) => params[key])
  32 + .reduce((prev, next) => ({ ...prev, [next]: params[next] }), {});
  33 + return {
  34 + ..._params,
  35 + page,
  36 + startTime: params.startTime ? new Date(params.startTime).getTime() : params.startTime,
  37 + endTime: params.endTime ? new Date(params.endTime).getTime() : params.endTime,
  38 + tbDeviceId: props?.recordData?.id?.id,
  39 + };
  40 + },
  41 + formConfig: {
  42 + labelWidth: 80,
  43 + schemas: formSchemas,
  44 + fieldMapToTime: [['dateRange', ['startTime', 'endTime'], 'YYYY-MM-DD HH:mm:ss']],
  45 + },
  46 + useSearchForm: true,
  47 + showIndexColumn: false,
  48 + clickToRowSelect: false,
  49 + showTableSetting: true,
  50 + bordered: true,
  51 + rowKey: 'id',
  52 + });
  53 +
  54 + const [registerModal, { openModal, closeModal }] = useModal();
  55 +
  56 + const handleViewDetail = (record: Record<'eventValue', Recordable>) => {
  57 + outputData.value = JSON.stringify(record.eventValue, null, 2);
  58 + openModal(true);
  59 + };
  60 +</script>
  61 +
  62 +<template>
  63 + <div>
  64 + <BasicTable :clickToRowSelect="false" @register="registerTable">
  65 + <template #outputParams="{ record }">
  66 + <span class="cursor-pointer text-blue-500" @click="handleViewDetail(record)">
  67 + <EyeOutlined class="svg:text-blue-500" />
  68 + <span class="ml-2">详情</span>
  69 + </span>
  70 + </template>
  71 + </BasicTable>
  72 + <BasicModal title="输出参数" @register="registerModal" @ok="closeModal">
  73 + <Input.TextArea v-model:value="outputData" :autosize="true" />
  74 + </BasicModal>
  75 + </div>
  76 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { computed, nextTick, onMounted, onUnmounted, Ref, ref, unref } from 'vue';
  3 + import { getDeviceHistoryInfo } from '/@/api/alarm/position';
  4 + import { Empty, Spin } from 'ant-design-vue';
  5 + import { useECharts } from '/@/hooks/web/useECharts';
  6 + import { AggregateDataEnum, selectDeviceAttrSchema } from '/@/views/device/localtion/config.data';
  7 + import { useTimePeriodForm } from '/@/views/device/localtion/cpns/TimePeriodForm';
  8 + import {
  9 + defaultSchemas,
  10 + OrderByEnum,
  11 + } from '/@/views/device/localtion/cpns/TimePeriodForm/config';
  12 + import TimePeriodForm from '/@/views/device/localtion/cpns/TimePeriodForm/TimePeriodForm.vue';
  13 + import { useGridLayout } from '/@/hooks/component/useGridLayout';
  14 + import { ColEx } from '/@/components/Form/src/types';
  15 + import { useHistoryData } from './hook/useHistoryData';
  16 + import { formatToDateTime } from '/@/utils/dateUtil';
  17 + import { useTable, BasicTable, BasicColumn, SorterResult } from '/@/components/Table';
  18 + import {
  19 + ModeSwitchButton,
  20 + TABLE_CHART_MODE_LIST,
  21 + EnumTableChartMode,
  22 + } from '/@/components/Widget';
  23 + import { SchemaFiled } from '/@/views/visual/palette/components/HistoryTrendModal/config';
  24 + import { DataTypeEnum } from '/@/enums/objectModelEnum';
  25 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  26 +
  27 + // interface DeviceDetail {
  28 + // tbDeviceId: string;
  29 + // deviceProfileId: string;
  30 + // }
  31 +
  32 + const props = defineProps<{
  33 + deviceDetail: EdgeDeviceItemType;
  34 + attr?: string;
  35 + }>();
  36 +
  37 + const mode = ref<EnumTableChartMode>(EnumTableChartMode.CHART);
  38 +
  39 + const chartRef = ref();
  40 +
  41 + const loading = ref(false);
  42 +
  43 + const isNull = ref(false);
  44 +
  45 + const historyData = ref<{ ts: number; value: string; name: string }[]>([]);
  46 +
  47 + const { deviceAttrs, getDeviceKeys, getSearchParams, setChartOptions, getDeviceAttribute } =
  48 + useHistoryData();
  49 +
  50 + const getIdentifierNameMapping = computed(() => {
  51 + const mapping = {};
  52 + unref(deviceAttrs).forEach((item) => {
  53 + const { identifier, name } = item;
  54 + mapping[identifier] = name;
  55 + });
  56 + return mapping;
  57 + });
  58 +
  59 + function hasDeviceAttr() {
  60 + if (!unref(deviceAttrs).length) {
  61 + return false;
  62 + } else {
  63 + return true;
  64 + }
  65 + }
  66 +
  67 + const sortOrder = ref<any>('descend');
  68 + const columns = computed<BasicColumn[]>(() => {
  69 + return [
  70 + {
  71 + title: '属性',
  72 + dataIndex: 'name',
  73 + format: (text) => {
  74 + return unref(getIdentifierNameMapping)[text];
  75 + },
  76 + },
  77 + {
  78 + title: '值',
  79 + dataIndex: 'value',
  80 + },
  81 + {
  82 + title: '更新时间',
  83 + dataIndex: 'ts',
  84 + format: (val) => {
  85 + return formatToDateTime(val, 'YYYY-MM-DD HH:mm:ss');
  86 + },
  87 + sorter: true,
  88 + sortOrder: unref(sortOrder),
  89 + sortDirections: ['descend', 'ascend', 'descend'],
  90 + },
  91 + ];
  92 + });
  93 +
  94 + const [registerTable, { setColumns }] = useTable({
  95 + showIndexColumn: false,
  96 + showTableSetting: false,
  97 + dataSource: historyData,
  98 + maxHeight: 300,
  99 + columns: unref(columns),
  100 + size: 'small',
  101 + });
  102 +
  103 + const getTableList = async (orderBy?: OrderByEnum) => {
  104 + // 表单验证
  105 + await method.validate();
  106 + const value = method.getFieldsValue();
  107 + const searchParams = getSearchParams(value);
  108 +
  109 + if (!hasDeviceAttr()) return;
  110 + // 发送请求
  111 + loading.value = true;
  112 + const res = await getDeviceHistoryInfo(
  113 + {
  114 + ...searchParams,
  115 + entityId: props.deviceDetail?.id?.id,
  116 + },
  117 + orderBy
  118 + );
  119 + historyData.value = getTableHistoryData(res);
  120 + loading.value = false;
  121 + };
  122 +
  123 + const handleTableChange = async (_pag, _filters, sorter: SorterResult) => {
  124 + sortOrder.value = sorter.order;
  125 + await setColumns(unref(columns));
  126 + if (sorter.field == 'ts') {
  127 + if (sorter.order == 'descend') {
  128 + getTableList(OrderByEnum.DESC);
  129 + } else {
  130 + getTableList(OrderByEnum.ASC);
  131 + }
  132 + }
  133 + };
  134 +
  135 + const { setOptions, getInstance } = useECharts(chartRef as Ref<HTMLDivElement>);
  136 +
  137 + const [register, method] = useTimePeriodForm({
  138 + schemas: [...defaultSchemas, ...selectDeviceAttrSchema],
  139 + baseColProps: useGridLayout(2, 3, 4) as unknown as ColEx,
  140 + async submitFunc() {
  141 + // 表单验证
  142 + await method.validate();
  143 + const value = method.getFieldsValue();
  144 + const searchParams = getSearchParams(value);
  145 +
  146 + if (!hasDeviceAttr()) return;
  147 + // 发送请求
  148 + loading.value = true;
  149 + const res = await getDeviceHistoryInfo(
  150 + {
  151 + ...searchParams,
  152 + entityId: props.deviceDetail?.id?.id,
  153 + },
  154 + searchParams.orderBy as OrderByEnum
  155 + );
  156 + historyData.value = getTableHistoryData(res);
  157 + loading.value = false;
  158 + // 判断数据对象是否为空
  159 + if (!Object.keys(res).length) {
  160 + isNull.value = false;
  161 + return;
  162 + } else {
  163 + isNull.value = true;
  164 + }
  165 +
  166 + const selectedKeys = unref(deviceAttrs).find(
  167 + (item) => item.identifier === value[SchemaFiled.KEYS]
  168 + );
  169 +
  170 + setOptions(setChartOptions(res, selectedKeys));
  171 + },
  172 + });
  173 +
  174 + const getTableHistoryData = (record: Recordable<{ ts: number; value: string }[]>) => {
  175 + const keys = Object.keys(record);
  176 + const list = keys.reduce((prev, next) => {
  177 + const list = record[next].map((item) => {
  178 + return {
  179 + ...item,
  180 + name: next,
  181 + };
  182 + });
  183 + return [...prev, ...list];
  184 + }, []);
  185 + return list;
  186 + };
  187 +
  188 + const getDeviceDataKey = async () => {
  189 + try {
  190 + await getDeviceAttribute(props.deviceDetail);
  191 + if (props.attr) {
  192 + method.setFieldsValue({ keys: props.attr });
  193 + const attrInfo = unref(deviceAttrs).find((item) => item.identifier === props.attr);
  194 + if (
  195 + [DataTypeEnum.STRING, DataTypeEnum.STRUCT].includes(
  196 + attrInfo?.detail.dataType.type as unknown as DataTypeEnum
  197 + )
  198 + ) {
  199 + mode.value = EnumTableChartMode.TABLE;
  200 + } else {
  201 + mode.value = EnumTableChartMode.CHART;
  202 + }
  203 + }
  204 + } catch (error) {}
  205 + };
  206 +
  207 + const openHistoryPanel = async (orderBy = OrderByEnum.ASC) => {
  208 + await nextTick();
  209 + method.updateSchema({
  210 + field: 'keys',
  211 + componentProps: {
  212 + options: unref(deviceAttrs).map((item) => ({ label: item.name, value: item.identifier })),
  213 + },
  214 + });
  215 +
  216 + method.setFieldsValue({
  217 + [SchemaFiled.START_TS]: 1 * 24 * 60 * 60 * 1000,
  218 + [SchemaFiled.LIMIT]: 7,
  219 + [SchemaFiled.AGG]: AggregateDataEnum.NONE,
  220 + });
  221 +
  222 + if (!hasDeviceAttr()) return;
  223 +
  224 + const keys = props.attr ? props.attr : unref(getDeviceKeys).join();
  225 +
  226 + const res = await getDeviceHistoryInfo(
  227 + {
  228 + entityId: props.deviceDetail?.id?.id,
  229 + keys,
  230 + startTs: Date.now() - 1 * 24 * 60 * 60 * 1000,
  231 + endTs: Date.now(),
  232 + agg: AggregateDataEnum.NONE,
  233 + limit: 7,
  234 + },
  235 + orderBy
  236 + );
  237 + historyData.value = getTableHistoryData(res);
  238 +
  239 + // 判断对象是否为空
  240 + if (!Object.keys(res).length) {
  241 + isNull.value = false;
  242 + return;
  243 + } else {
  244 + isNull.value = true;
  245 + }
  246 + const selectedKeys = unref(deviceAttrs).find((item) => item.identifier === props.attr);
  247 +
  248 + setOptions(setChartOptions(res, selectedKeys));
  249 + };
  250 +
  251 + const switchMode = async (flag: EnumTableChartMode) => {
  252 + mode.value = flag;
  253 + };
  254 +
  255 + onMounted(async () => {
  256 + await getDeviceDataKey();
  257 + await openHistoryPanel();
  258 + });
  259 +
  260 + onUnmounted(() => {
  261 + getInstance()?.clear();
  262 + });
  263 +</script>
  264 +
  265 +<template>
  266 + <section class="flex flex-col p-4 h-full" style="background-color: #f0f2f5">
  267 + <section class="bg-white p-3 mb-4">
  268 + <TimePeriodForm @register="register" />
  269 + </section>
  270 + <section class="bg-white p-3">
  271 + <Spin :spinning="loading" :absolute="true">
  272 + <div
  273 + v-show="mode === EnumTableChartMode.CHART"
  274 + class="flex h-70px items-center justify-end p-2"
  275 + >
  276 + <ModeSwitchButton
  277 + v-model:value="mode"
  278 + :mode="TABLE_CHART_MODE_LIST"
  279 + @change="switchMode"
  280 + />
  281 + </div>
  282 + <div
  283 + v-show="isNull && mode === EnumTableChartMode.CHART"
  284 + ref="chartRef"
  285 + :style="{ height: '400px', width: '100%' }"
  286 + >
  287 + </div>
  288 + <Empty v-show="!isNull && mode === EnumTableChartMode.CHART" />
  289 +
  290 + <BasicTable
  291 + @change="handleTableChange"
  292 + v-show="mode === EnumTableChartMode.TABLE"
  293 + @register="registerTable"
  294 + >
  295 + <template #toolbar>
  296 + <div class="flex h-70px items-center justify-end p-2">
  297 + <ModeSwitchButton
  298 + v-model:value="mode"
  299 + :mode="TABLE_CHART_MODE_LIST"
  300 + @change="switchMode"
  301 + />
  302 + </div>
  303 + </template>
  304 + </BasicTable>
  305 + </Spin>
  306 + </section>
  307 + </section>
  308 +</template>
  309 +
  310 +<style scoped></style>
... ...
  1 +import { DeviceModelOfMatterAttrs } from '/@/api/device/model/deviceModel';
  2 +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  3 +import { DataType, Specs } from '/@/api/device/model/modelOfMatterModel';
  4 +import { TCPProtocolTypeEnum, TransportTypeEnum } from '/@/enums/deviceEnum';
  5 +import { DataTypeEnum } from '/@/enums/objectModelEnum';
  6 +import { isArray } from '/@/utils/is';
  7 +
  8 +export interface StructValueItemType extends BaseAdditionalInfo {
  9 + name: string;
  10 + key: string;
  11 + value?: string | number | boolean;
  12 +}
  13 +
  14 +export interface BaseAdditionalInfo {
  15 + unit?: string;
  16 + boolClose?: string;
  17 + boolOpen?: string;
  18 + unitName?: string;
  19 +}
  20 +
  21 +export interface SocketInfoDataSourceItemType extends BaseAdditionalInfo {
  22 + accessMode: string;
  23 + key: string;
  24 + name: string;
  25 + type: DataTypeEnum;
  26 + detail: DeviceModelOfMatterAttrs;
  27 + time?: number;
  28 + value?: string | number | boolean | Record<string, StructValueItemType>;
  29 + expand?: boolean;
  30 + showHistoryDataButton?: boolean;
  31 + rawValue?: any;
  32 + enum?: Record<string, string>;
  33 +}
  34 +
  35 +export function buildTableDataSourceByObjectModel(
  36 + models: DeviceModelOfMatterAttrs[],
  37 + deviceDetail: EdgeDeviceItemType
  38 +): SocketInfoDataSourceItemType[] {
  39 + const isTCPTransportType =
  40 + deviceDetail.deviceData.transportConfiguration.type === TransportTypeEnum.TCP;
  41 +
  42 + const isModbusDevice =
  43 + isTCPTransportType &&
  44 + deviceDetail?.deviceProfile?.profileData?.transportConfiguration?.protocol ===
  45 + TCPProtocolTypeEnum.MODBUS_RTU;
  46 +
  47 + function getAdditionalInfoByDataType(dataType?: DataType) {
  48 + const { specs, specsList, type } = dataType || {};
  49 + if (isArray(specs)) return {};
  50 + const { unit, boolClose, boolOpen, unitName } = (specs as Partial<Specs>) || {};
  51 + const result = { unit, boolClose, boolOpen, unitName };
  52 + if ((type == DataTypeEnum.ENUM && specsList && specsList.length) || isModbusDevice) {
  53 + Reflect.set(
  54 + result,
  55 + 'enum',
  56 + (specsList || []).reduce((prev, next) => ({ ...prev, [next.value!]: next.name }), {})
  57 + );
  58 + }
  59 +
  60 + if (isModbusDevice) {
  61 + result.boolClose = '关';
  62 + result.boolOpen = '开';
  63 + }
  64 +
  65 + return result;
  66 + }
  67 +
  68 + return models.map((item) => {
  69 + const { accessMode, identifier, name, detail } = item;
  70 + const { dataType } = detail;
  71 + const { type, specs } = dataType || {};
  72 +
  73 + const res = {
  74 + accessMode,
  75 + name,
  76 + key: identifier,
  77 + type: type!,
  78 + detail: item,
  79 + };
  80 +
  81 + if (isArray(specs) && type === DataTypeEnum.STRUCT) {
  82 + const value: Record<string, StructValueItemType> = specs
  83 + .filter((item) => item.dataType?.type !== DataTypeEnum.STRUCT)
  84 + .reduce((prev, next) => {
  85 + const { identifier, functionName, dataType } = next;
  86 + return {
  87 + ...prev,
  88 + [identifier]: {
  89 + key: identifier!,
  90 + name: functionName!,
  91 + type: dataType?.type,
  92 + ...getAdditionalInfoByDataType(dataType),
  93 + },
  94 + };
  95 + }, {});
  96 +
  97 + Object.assign(res, { value });
  98 + } else {
  99 + Object.assign(res, getAdditionalInfoByDataType(dataType));
  100 + }
  101 + return res;
  102 + });
  103 +}
... ...
  1 +export { default as ObjectModelCommandDeliveryModal } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { ref, unref } from 'vue';
  3 + import { useGenerateFormSchemasByObjectModel } from './useGenerateFormSchemasByObjectModel';
  4 + import { ModalParamsType } from '/#/utils';
  5 + import { DeviceModelOfMatterAttrs } from '/@/api/device/model/deviceModel';
  6 + import { BasicForm, useForm } from '/@/components/Form';
  7 + import { BasicModal, useModalInner } from '/@/components/Modal';
  8 + import { CommandTypeEnum } from '/@/enums/deviceEnum';
  9 + import { DataTypeEnum, FunctionTypeEnum } from '/@/enums/objectModelEnum';
  10 + import { useCommandDelivery } from './useCommandDelivery';
  11 + import { useMessage } from '/@/hooks/web/useMessage';
  12 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  13 +
  14 + defineEmits(['register']);
  15 + const props = defineProps<{ deviceId: string; deviceName: string }>();
  16 +
  17 + const [registerForm, { setProps, getFieldsValue, resetFields, validate }] = useForm({
  18 + schemas: [],
  19 + showActionButtonGroup: false,
  20 + layout: 'vertical',
  21 + });
  22 +
  23 + const { getFormByObjectModel } = useGenerateFormSchemasByObjectModel();
  24 +
  25 + const currentParams = ref<{
  26 + deviceDetail: EdgeDeviceItemType;
  27 + objectModel: DeviceModelOfMatterAttrs;
  28 + }>();
  29 +
  30 + const [register] = useModalInner(
  31 + async (params: {
  32 + record: ModalParamsType<{
  33 + objectModel: DeviceModelOfMatterAttrs;
  34 + deviceDetail: EdgeDeviceItemType;
  35 + }>;
  36 + }) => {
  37 + const { record } = params;
  38 + currentParams.value = record as unknown as {
  39 + objectModel: DeviceModelOfMatterAttrs;
  40 + deviceDetail: EdgeDeviceItemType;
  41 + };
  42 + const { objectModel, deviceDetail } = record;
  43 + const schemas = getFormByObjectModel(objectModel, deviceDetail);
  44 + setProps({ schemas });
  45 + resetFields();
  46 + }
  47 + );
  48 +
  49 + const { createMessage } = useMessage();
  50 + const loading = ref(false);
  51 + const handleSend = async () => {
  52 + try {
  53 + loading.value = true;
  54 + if (!props.deviceId) return;
  55 +
  56 + await validate();
  57 + const { deviceDetail, objectModel } = unref(currentParams) || {};
  58 +
  59 + let value = getFieldsValue();
  60 + if (objectModel?.detail.dataType.type !== DataTypeEnum.STRUCT) {
  61 + value = value[objectModel!.identifier];
  62 + }
  63 +
  64 + const { doCommandDelivery } = useCommandDelivery();
  65 +
  66 + await doCommandDelivery({
  67 + deviceDetail,
  68 + objectModel: {
  69 + ...(objectModel || {}),
  70 + functionName: objectModel!.name,
  71 + identifier: objectModel!.identifier,
  72 + functionType: FunctionTypeEnum.PROPERTIES,
  73 + specs: objectModel?.detail,
  74 + },
  75 + cmdType: CommandTypeEnum.ATTRIBUTE,
  76 + value,
  77 + });
  78 +
  79 + createMessage.success('属性下发成功');
  80 + } catch (error) {
  81 + throw error;
  82 + } finally {
  83 + loading.value = false;
  84 + }
  85 + };
  86 +</script>
  87 +
  88 +<template>
  89 + <BasicModal
  90 + title="属性下发"
  91 + @register="register"
  92 + :min-height="60"
  93 + ok-text="属性下发"
  94 + wrap-class-name="model-of-matter-send-command-modal"
  95 + @ok="handleSend"
  96 + :ok-button-props="{ loading }"
  97 + >
  98 + <BasicForm @register="registerForm" class="dynamic-form" />
  99 + </BasicModal>
  100 +</template>
  101 +
  102 +<style lang="less">
  103 + .model-of-matter-send-command-modal {
  104 + .dynamic-form {
  105 + .ant-input-number {
  106 + @apply !w-full;
  107 + }
  108 + }
  109 + }
  110 +</style>
... ...
  1 +import { ref } from 'vue';
  2 +import { commandIssuanceApi, getDeviceDetail } from '/@/api/device/deviceManager';
  3 +import { RpcCommandType } from '/@/api/device/model/deviceConfigModel';
  4 +import { DeviceProfileModel } from '/@/api/device/model/deviceModel';
  5 +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  6 +import { Tsl } from '/@/api/device/model/modelOfMatterModel';
  7 +import { getModelTsl } from '/@/api/device/modelOfMatter';
  8 +import {
  9 + CommandDeliveryWayEnum,
  10 + CommandTypeEnum,
  11 + RPCCommandMethodEnum,
  12 + ServiceCallTypeEnum,
  13 + TCPProtocolTypeEnum,
  14 + TransportTypeEnum,
  15 +} from '/@/enums/deviceEnum';
  16 +import { FunctionTypeEnum } from '/@/enums/objectModelEnum';
  17 +import { isFunction, isNullOrUnDef } from '/@/utils/is';
  18 +import { getDeviceActiveTime } from '/@/api/alarm/position';
  19 +import { useMessage } from '/@/hooks/web/useMessage';
  20 +import { useCoverModbusCommand } from './useCoverModbusCommand';
  21 +
  22 +interface SetupType {
  23 + entityId: string;
  24 + transportType: TransportTypeEnum;
  25 + isTCPModbus: boolean;
  26 + deviceCode: string | undefined;
  27 + deviceDetail: EdgeDeviceItemType;
  28 + objectModel: Tsl | undefined;
  29 + identifier: string;
  30 +}
  31 +
  32 +interface DoCommandDeliverParamsType {
  33 + value: any;
  34 + deviceDetail?: EdgeDeviceItemType;
  35 + deviceId?: string;
  36 + identifier?: string;
  37 + objectModel?: Tsl;
  38 + deviceProfileId?: string;
  39 + deviceProfileDetail?: DeviceProfileModel;
  40 + way?: CommandDeliveryWayEnum;
  41 + cmdType?: CommandTypeEnum;
  42 + transportType?: TransportTypeEnum;
  43 + beforeFetch?: (
  44 + rpcCommand: RpcCommandType,
  45 + setup: SetupType
  46 + ) =>
  47 + | { rpcCommand: RpcCommandType; way?: CommandDeliveryWayEnum }
  48 + | Promise<{ rpcCommand: RpcCommandType; way?: CommandDeliveryWayEnum }>;
  49 +}
  50 +
  51 +export function useCommandDelivery() {
  52 + const loading = ref(false);
  53 + async function doSetup(params: DoCommandDeliverParamsType) {
  54 + let { deviceDetail, identifier, deviceProfileId, objectModel, transportType } = params;
  55 + const { deviceId } = params;
  56 +
  57 + const entityId = deviceId || deviceDetail?.id?.id;
  58 + if (!entityId) {
  59 + throw new Error('not found entityId');
  60 + }
  61 +
  62 + identifier = identifier || objectModel?.identifier;
  63 +
  64 + if (!identifier) {
  65 + throw new Error('not found identifier');
  66 + }
  67 +
  68 + transportType = transportType || (deviceDetail?.transportType as TransportTypeEnum);
  69 +
  70 + if (
  71 + !transportType ||
  72 + (transportType === TransportTypeEnum.TCP && !deviceDetail) ||
  73 + !deviceDetail?.deviceProfile
  74 + ) {
  75 + deviceDetail = (await getDeviceDetail(entityId)) as any as EdgeDeviceItemType;
  76 + transportType = deviceDetail?.transportType as TransportTypeEnum;
  77 + }
  78 +
  79 + const isTCPModbus =
  80 + transportType === TransportTypeEnum.TCP &&
  81 + deviceDetail?.deviceProfile?.profileData?.transportConfiguration?.protocol ===
  82 + TCPProtocolTypeEnum.MODBUS_RTU;
  83 +
  84 + if (isTCPModbus && !objectModel?.extensionDesc) {
  85 + // deviceProfileId = deviceDetail?.deviceProfileId || deviceProfileId || deviceProfileDetail?.id;
  86 + deviceProfileId = deviceDetail?.deviceProfileId?.id;
  87 + if (!deviceProfileId) {
  88 + throw new Error('not found deviceProfile');
  89 + }
  90 +
  91 + const objectModels = await getModelTsl({ deviceProfileId });
  92 + objectModel = objectModels.find((item) => item.identifier === identifier);
  93 + }
  94 +
  95 + const deviceCode = deviceDetail?.code;
  96 +
  97 + return {
  98 + entityId,
  99 + transportType,
  100 + isTCPModbus,
  101 + deviceCode,
  102 + deviceDetail,
  103 + objectModel,
  104 + identifier,
  105 + };
  106 + }
  107 +
  108 + async function doCommandDelivery(params: DoCommandDeliverParamsType) {
  109 + try {
  110 + loading.value = true;
  111 +
  112 + const setupResult = await doSetup(params);
  113 +
  114 + const { entityId, transportType, isTCPModbus, deviceCode, objectModel, identifier } =
  115 + setupResult;
  116 +
  117 + let command = params.value;
  118 +
  119 + if (transportType === TransportTypeEnum.TCP) {
  120 + command = params.value;
  121 + if (isTCPModbus) {
  122 + const { doCoverCommand } = useCoverModbusCommand();
  123 + command = await doCoverCommand(params.value, objectModel!, deviceCode, entityId);
  124 + }
  125 + } else {
  126 + command = {
  127 + [identifier]: command,
  128 + };
  129 + }
  130 +
  131 + let rpcCommand: RpcCommandType = {
  132 + persistent: true,
  133 + method: RPCCommandMethodEnum.THINGSKIT,
  134 + additionalInfo: {
  135 + cmdType: params.cmdType ?? CommandTypeEnum.API,
  136 + },
  137 + params: command,
  138 + };
  139 +
  140 + let way = params.way ?? CommandDeliveryWayEnum.ONE_WAY;
  141 +
  142 + if (objectModel?.functionType === FunctionTypeEnum.SERVICE) {
  143 + rpcCommand.additionalInfo.cmdType = CommandTypeEnum.SERVICE;
  144 + way =
  145 + objectModel.callType === ServiceCallTypeEnum.ASYNC
  146 + ? CommandDeliveryWayEnum.ONE_WAY
  147 + : CommandDeliveryWayEnum.TWO_WAY;
  148 + }
  149 +
  150 + const sendApi = commandIssuanceApi;
  151 +
  152 + if (params.beforeFetch && isFunction(params.beforeFetch)) {
  153 + const { rpcCommand: _rpcCommand, way: _way } = await params.beforeFetch(
  154 + rpcCommand,
  155 + setupResult
  156 + );
  157 + !isNullOrUnDef(rpcCommand) && (rpcCommand = _rpcCommand);
  158 + if (_way) way = _way;
  159 + }
  160 +
  161 + if (way === CommandDeliveryWayEnum.TWO_WAY) {
  162 + const result = await getDeviceActiveTime(entityId);
  163 + const [firsetItem] = result || [];
  164 +
  165 + if (!firsetItem.value) {
  166 + const { createMessage } = useMessage();
  167 + const message = '当前设备不在线';
  168 + createMessage.warning(message);
  169 + throw Error(message);
  170 + }
  171 + }
  172 + await sendApi(way, entityId, rpcCommand);
  173 + } finally {
  174 + loading.value = false;
  175 + }
  176 + }
  177 +
  178 + return {
  179 + loading,
  180 + doCommandDelivery,
  181 + };
  182 +}
... ...
  1 +import { useParseOriginalDataType } from './useParseOriginalDataType';
  2 +import { getDeviceHistoryInfo } from '/@/api/alarm/position';
  3 +import { getDeviceDetail } from '/@/api/device/deviceManager';
  4 +import { ExtensionDesc, Specs, Tsl } from '/@/api/device/model/modelOfMatterModel';
  5 +import { genModbusCommand } from '/@/api/task';
  6 +import { GenModbusCommandType } from '/@/api/task/model';
  7 +import { ModbusCRCEnum, OriginalDataTypeEnum } from '/@/enums/objectModelEnum';
  8 +import { useBaseConversion } from '/@/hooks/business/useBaseConversion';
  9 +import { useMessage } from '/@/hooks/web/useMessage';
  10 +import { isNullOrUnDef } from '/@/utils/is';
  11 +import {
  12 + isFloatType,
  13 + isNumberType,
  14 + useParseOperationType,
  15 +} from '/@/views/device/profiles/components/ObjectModelForm/ExtendDesc/useParseOperationType';
  16 +
  17 +const getFloatPart = (number: string | number) => {
  18 + const isLessZero = Number(number) < 0;
  19 + number = number.toString();
  20 + const floatPartStartIndex = number.indexOf('.');
  21 + const value = ~floatPartStartIndex
  22 + ? `${isLessZero ? '-' : ''}0.${number.substring(floatPartStartIndex + 1)}`
  23 + : '0';
  24 + return Number(value);
  25 +};
  26 +
  27 +function getValueFromValueRange(value: number, valueRange?: Record<'min' | 'max', number>) {
  28 + const { min, max } = valueRange || {};
  29 + if (!isNullOrUnDef(min) && value < min) return min;
  30 + if (!isNullOrUnDef(max) && value > max) return max;
  31 + return value;
  32 +}
  33 +
  34 +async function getCurrentBitCommand(entityId: string, objectModel: Tsl, value: number) {
  35 + const deviceDetail = await getDeviceDetail(entityId);
  36 + const thingsModels = deviceDetail.deviceProfile.profileData.thingsModel;
  37 +
  38 + const { registerAddress } = objectModel.extensionDesc || {};
  39 +
  40 + const bitsModel = thingsModels?.filter(
  41 + (item) =>
  42 + item.extensionDesc?.originalDataType === OriginalDataTypeEnum.BITS &&
  43 + item.extensionDesc.registerAddress === registerAddress
  44 + );
  45 +
  46 + const valuePositionMap =
  47 + bitsModel?.reduce((prev, next) => {
  48 + return { ...prev, [next.identifier]: next.extensionDesc?.bitMask };
  49 + }, {} as Record<string, number>) || {};
  50 +
  51 + const attrKeys = Object.keys(valuePositionMap);
  52 +
  53 + const latestBitsValues = await getDeviceHistoryInfo({ entityId, keys: attrKeys.join(',') });
  54 +
  55 + const binaryArr = Array.from({ length: 16 }, () => 0);
  56 +
  57 + for (const key of attrKeys) {
  58 + const index = valuePositionMap[key];
  59 +
  60 + if (!isNullOrUnDef(index)) {
  61 + const [latest] = latestBitsValues[key];
  62 + const { value } = latest;
  63 + binaryArr[index] = Number(value);
  64 + }
  65 + }
  66 +
  67 + if (objectModel.extensionDesc?.bitMask) {
  68 + binaryArr[objectModel.extensionDesc.bitMask] = value;
  69 + }
  70 +
  71 + return [parseInt(binaryArr.reverse().join(''), 2)];
  72 +}
  73 +
  74 +export function useCoverModbusCommand() {
  75 + const { createMessage } = useMessage();
  76 +
  77 + const doCoverCommand = async (
  78 + value: number,
  79 + objectModel: Tsl,
  80 + deviceAddressCode?: string,
  81 + entityId?: string
  82 + ) => {
  83 + if (!deviceAddressCode) {
  84 + const message = '当前设备未绑定设备地址码';
  85 + createMessage.warning(message);
  86 + throw new Error(message);
  87 + }
  88 +
  89 + const {
  90 + registerAddress,
  91 + operationType,
  92 + scaling,
  93 + originalDataType,
  94 + bitMask,
  95 + registerCount: registerNumber,
  96 + } = objectModel.extensionDesc as Required<ExtensionDesc>;
  97 +
  98 + const { writeRegisterAddress } = useParseOperationType(operationType);
  99 + const { unsigned, exchangeSortFlag, registerCount } =
  100 + useParseOriginalDataType(originalDataType);
  101 +
  102 + const params: GenModbusCommandType = {
  103 + crc: ModbusCRCEnum.CRC_16_LOWER,
  104 + registerNumber: registerCount || registerNumber,
  105 + deviceCode: deviceAddressCode,
  106 + registerAddress: parseInt(registerAddress, 16),
  107 + method: writeRegisterAddress!,
  108 + registerValues: [value],
  109 + };
  110 +
  111 + if (exchangeSortFlag) params.hexByteOrderEnum = exchangeSortFlag;
  112 +
  113 + const { getRegisterValueByOriginalDataType } = useBaseConversion();
  114 +
  115 + if (isNumberType(originalDataType)) {
  116 + let newValue = Math.trunc(value) * scaling + getFloatPart(value) * scaling;
  117 +
  118 + newValue = unsigned ? newValue : Math.abs(newValue);
  119 +
  120 + newValue = getValueFromValueRange(
  121 + newValue,
  122 + (objectModel.specs?.dataType.specs as Specs).valueRange
  123 + );
  124 +
  125 + if (!isFloatType(originalDataType) && newValue % 1 !== 0) {
  126 + const message = `属性下发类型必须是整数,缩放因子为${scaling}`;
  127 + createMessage.warning(message);
  128 + throw Error(message);
  129 + }
  130 +
  131 + value = newValue;
  132 + }
  133 +
  134 + params.registerValues =
  135 + originalDataType === OriginalDataTypeEnum.BITS
  136 + ? await getCurrentBitCommand(entityId!, objectModel, value)
  137 + : getRegisterValueByOriginalDataType(value, originalDataType, {
  138 + bitMask,
  139 + registerNumber,
  140 + });
  141 +
  142 + if (!params.method) {
  143 + const message = '物模型操作类型无法进行写入';
  144 + createMessage.warning(message);
  145 + throw Error(message);
  146 + }
  147 +
  148 + return await genModbusCommand(params);
  149 + };
  150 +
  151 + return {
  152 + doCoverCommand,
  153 + };
  154 +}
... ...