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
  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 +}
@@ -5,10 +5,13 @@ import { isString } from '/@/utils/is'; @@ -5,10 +5,13 @@ import { isString } from '/@/utils/is';
5 import { OrderByEnum } from '/@/views/device/localtion/cpns/TimePeriodForm/config'; 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 return defHttp.get<DeviceProfileModel[]>({ 9 return defHttp.get<DeviceProfileModel[]>({
10 url: '/device_profile/me/list', 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,12 +9,16 @@ enum OrderType {
9 ASC = 'ASC', 9 ASC = 'ASC',
10 DESC = 'DESC', 10 DESC = 'DESC',
11 } 11 }
  12 +enum SortProperty {
  13 + CREATEtIME = 'createdTime',
  14 +}
12 15
13 export interface BaseQueryParams { 16 export interface BaseQueryParams {
14 pageSize: number; 17 pageSize: number;
15 page: number; 18 page: number;
16 orderFiled?: string; 19 orderFiled?: string;
17 orderType?: OrderType; 20 orderType?: OrderType;
  21 + sortProperty?: SortProperty;
18 } 22 }
19 23
20 export class BaseQueryRequest implements BaseQueryParams { 24 export class BaseQueryRequest implements BaseQueryParams {
@@ -264,13 +264,16 @@ export const getGATEWAYdevice = async (params: { @@ -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 return defHttp.get<DeviceRecord[]>({ 271 return defHttp.get<DeviceRecord[]>({
270 url: DeviceManagerApi.GATEWAY_DEVICE, 272 url: DeviceManagerApi.GATEWAY_DEVICE,
271 params: { 273 params: {
272 organizationId, 274 organizationId,
273 transportType, 275 transportType,
  276 + gatewayId,
274 }, 277 },
275 }); 278 });
276 }; 279 };
@@ -124,6 +124,7 @@ export interface ProfileRecord { @@ -124,6 +124,7 @@ export interface ProfileRecord {
124 image: string; 124 image: string;
125 type: string; 125 type: string;
126 default: boolean; 126 default: boolean;
  127 + isEdge?: boolean;
127 128
128 checked?: boolean; 129 checked?: boolean;
129 } 130 }
@@ -7,6 +7,7 @@ export enum DeviceState { @@ -7,6 +7,7 @@ export enum DeviceState {
7 INACTIVE = 'INACTIVE', 7 INACTIVE = 'INACTIVE',
8 ONLINE = 'ONLINE', 8 ONLINE = 'ONLINE',
9 OFFLINE = 'OFFLINE', 9 OFFLINE = 'OFFLINE',
  10 + ACTIVE = 'ACTIVE',
10 } 11 }
11 export enum DeviceTypeEnum { 12 export enum DeviceTypeEnum {
12 GATEWAY = 'GATEWAY', 13 GATEWAY = 'GATEWAY',
@@ -102,3 +102,9 @@ export interface VideoChanneControlType { @@ -102,3 +102,9 @@ export interface VideoChanneControlType {
102 tbDeviceId?: string | number | object; 102 tbDeviceId?: string | number | object;
103 channelId?: string | number | object; 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,6 +3,7 @@ import {
3 VideoChannelPlayAddressType, 3 VideoChannelPlayAddressType,
4 VideoChannelQueryParamsType, 4 VideoChannelQueryParamsType,
5 VideoChanneControlType, 5 VideoChanneControlType,
  6 + EzvizControlType,
6 } from './model/videoChannelModel'; 7 } from './model/videoChannelModel';
7 import { PaginationResult } from '/#/axios'; 8 import { PaginationResult } from '/#/axios';
8 import { defHttp } from '/@/utils/http/axios'; 9 import { defHttp } from '/@/utils/http/axios';
@@ -12,6 +13,7 @@ enum Api { @@ -12,6 +13,7 @@ enum Api {
12 GET_VIDEO_CONTROL_START = '/video/control/start', 13 GET_VIDEO_CONTROL_START = '/video/control/start',
13 GET_VIDEO_CONTROL_STOP = '/video/control/stop', 14 GET_VIDEO_CONTROL_STOP = '/video/control/stop',
14 SET_VIDEO_CONTROL_CONTROL = '/video/control/control', 15 SET_VIDEO_CONTROL_CONTROL = '/video/control/control',
  16 + SET_EZVIZ_CONTROL = '/video/ezviz/controlling',
15 } 17 }
16 18
17 export const getVideoChannelList = (params: VideoChannelQueryParamsType) => { 19 export const getVideoChannelList = (params: VideoChannelQueryParamsType) => {
@@ -43,3 +45,10 @@ export const setVideoControl = (tbDeviceId, channelId, params: VideoChanneContro @@ -43,3 +45,10 @@ export const setVideoControl = (tbDeviceId, channelId, params: VideoChanneContro
43 params, 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,8 +3,17 @@ import { FileUploadResponse } from '/@/api/oss/FileUploadResponse';
3 3
4 enum Api { 4 enum Api {
5 BaseUploadUrl = '/oss/upload', 5 BaseUploadUrl = '/oss/upload',
  6 + BaseDeleteUrl = '/oss',
6 } 7 }
7 8
8 export const upload = (file) => { 9 export const upload = (file) => {
9 return defHttp.post<FileUploadResponse>({ url: Api.BaseUploadUrl, params: file }); 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,11 +125,16 @@ export const getAttribute = (orgId) => {
125 export const byOrganizationIdGetMasterDevice = (params: { 125 export const byOrganizationIdGetMasterDevice = (params: {
126 organizationId: string; 126 organizationId: string;
127 deviceProfileId?: string; 127 deviceProfileId?: string;
  128 + isSceneLinkage?: Boolean;
128 }) => { 129 }) => {
129 - const { organizationId, deviceProfileId } = params; 130 + const { organizationId, deviceProfileId, isSceneLinkage } = params;
130 return defHttp.get<DeviceModel[]>({ 131 return defHttp.get<DeviceModel[]>({
131 url: `${ScreenManagerApi.MASTER_GET_DEVICE}`, 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 //TODO-fengtao 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>
  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>
  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>
@@ -71,10 +71,10 @@ @@ -71,10 +71,10 @@
71 if (!value) { 71 if (!value) {
72 return true; 72 return true;
73 } 73 }
74 - let { name } = props || {}; 74 + let { title } = props || {};
75 value = value.toLowerCase(); 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 async function fetch() { 80 async function fetch() {
@@ -21,7 +21,7 @@ @@ -21,7 +21,7 @@
21 } 21 }
22 22
23 const { prefixCls } = useDesign('api-upload'); 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 const { createMessage } = useMessage(); 26 const { createMessage } = useMessage();
27 27
@@ -71,9 +71,12 @@ @@ -71,9 +71,12 @@
71 return false; 71 return false;
72 } 72 }
73 } 73 }
74 -  
75 if (file.size > props.maxSize) { 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 return false; 80 return false;
78 } 81 }
79 handleUpload(file); 82 handleUpload(file);
@@ -129,6 +132,7 @@ @@ -129,6 +132,7 @@
129 const index = _fileList.findIndex((item) => item.uid === file.uid); 132 const index = _fileList.findIndex((item) => item.uid === file.uid);
130 ~index && _fileList.splice(index, 1); 133 ~index && _fileList.splice(index, 1);
131 emit('update:fileList', _fileList); 134 emit('update:fileList', _fileList);
  135 + emit('delete', file.url);
132 }; 136 };
133 137
134 const handlePreview = (file: FileItem) => { 138 const handlePreview = (file: FileItem) => {
@@ -136,7 +136,8 @@ export interface FormSchema { @@ -136,7 +136,8 @@ export interface FormSchema {
136 helpMessage?: 136 helpMessage?:
137 | string 137 | string
138 | string[] 138 | string[]
139 - | ((renderCallbackParams: RenderCallbackParams) => string | string[]); 139 + | ((renderCallbackParams: RenderCallbackParams) => string | string[])
  140 + | Boolean;
140 // BaseHelp component props 141 // BaseHelp component props
141 helpComponentProps?: Partial<HelpComponentProps>; 142 helpComponentProps?: Partial<HelpComponentProps>;
142 // Label width, if it is passed, the labelCol and WrapperCol configured by itemProps will be invalid 143 // Label width, if it is passed, the labelCol and WrapperCol configured by itemProps will be invalid
@@ -59,7 +59,7 @@ @@ -59,7 +59,7 @@
59 </span> 59 </span>
60 </Menu.Item> 60 </Menu.Item>
61 <Menu.Item :disabled="item.disabled" v-if="item.popconfirm"> 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 <template v-if="item.popconfirm.icon" #icon> 63 <template v-if="item.popconfirm.icon" #icon>
64 <Icon :icon="item.popconfirm.icon" /> 64 <Icon :icon="item.popconfirm.icon" />
65 </template> 65 </template>
@@ -4,6 +4,7 @@ const menuMap = new Map(); @@ -4,6 +4,7 @@ const menuMap = new Map();
4 4
5 menuMap.set('/visual/board/detail/:boardId/:boardName/:platform/:organizationId?', '/visual/board'); 5 menuMap.set('/visual/board/detail/:boardId/:boardName/:platform/:organizationId?', '/visual/board');
6 menuMap.set('/rule/chain/:id', '/rule/chain'); 6 menuMap.set('/rule/chain/:id', '/rule/chain');
  7 +menuMap.set('/edge/edge_detail/:id', '/edge');
7 8
8 export const useMenuActiveFix = (route: RouteLocationNormalizedLoaded) => { 9 export const useMenuActiveFix = (route: RouteLocationNormalizedLoaded) => {
9 let flag = false; 10 let flag = false;
@@ -2,10 +2,12 @@ @@ -2,10 +2,12 @@
2 <Dropdown placement="bottomLeft" :overlayClassName="`${prefixCls}-dropdown-overlay`"> 2 <Dropdown placement="bottomLeft" :overlayClassName="`${prefixCls}-dropdown-overlay`">
3 <span :class="[prefixCls, `${prefixCls}--${theme}`]" class="flex"> 3 <span :class="[prefixCls, `${prefixCls}--${theme}`]" class="flex">
4 <img :class="`${prefixCls}__header`" :src="getUserInfo.avatar" /> 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 </span> 11 </span>
10 </span> 12 </span>
11 13
@@ -53,7 +55,7 @@ @@ -53,7 +55,7 @@
53 </template> 55 </template>
54 <script lang="ts"> 56 <script lang="ts">
55 // components 57 // components
56 - import { Dropdown, Menu } from 'ant-design-vue'; 58 + import { Dropdown, Menu, Tooltip } from 'ant-design-vue';
57 import { defineComponent, computed, ref, reactive } from 'vue'; 59 import { defineComponent, computed, ref, reactive } from 'vue';
58 import { useUserStore } from '/@/store/modules/user'; 60 import { useUserStore } from '/@/store/modules/user';
59 import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting'; 61 import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
@@ -83,6 +85,7 @@ @@ -83,6 +85,7 @@
83 LockAction: createAsyncComponent(() => import('../lock/LockModal.vue')), 85 LockAction: createAsyncComponent(() => import('../lock/LockModal.vue')),
84 PersonalChild: createAsyncComponent(() => import('../personal/index.vue')), 86 PersonalChild: createAsyncComponent(() => import('../personal/index.vue')),
85 AboutSoftwareModal, 87 AboutSoftwareModal,
  88 + Tooltip,
86 }, 89 },
87 props: { 90 props: {
88 theme: propTypes.oneOf(['dark', 'light']), 91 theme: propTypes.oneOf(['dark', 'light']),
@@ -28,6 +28,7 @@ import { RouteRecordRaw } from 'vue-router'; @@ -28,6 +28,7 @@ import { RouteRecordRaw } from 'vue-router';
28 import { PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic'; 28 import { PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic';
29 import { createLocalStorage } from '/@/utils/cache/index'; 29 import { createLocalStorage } from '/@/utils/cache/index';
30 import { getEntitiesId } from '/@/api/dashboard/index'; 30 import { getEntitiesId } from '/@/api/dashboard/index';
  31 +import { useRole } from '/@/hooks/business/useRole';
31 32
32 interface PlatInfoType { 33 interface PlatInfoType {
33 id: string; 34 id: string;
@@ -283,7 +284,10 @@ export const useUserStore = defineStore({ @@ -283,7 +284,10 @@ export const useUserStore = defineStore({
283 title: t('sys.app.logoutTip'), 284 title: t('sys.app.logoutTip'),
284 content: t('sys.app.logoutMessage'), 285 content: t('sys.app.logoutMessage'),
285 onOk: async () => { 286 onOk: async () => {
286 - await logoutApi(null, 'modal'); //新增退出登录接口 287 + const { isPlatformAdmin } = useRole();
  288 + if (!isPlatformAdmin.value) {
  289 + await logoutApi(null, 'modal'); //新增退出登录接口
  290 + }
287 await this.logout(true); 291 await this.logout(true);
288 }, 292 },
289 }); 293 });
@@ -173,7 +173,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) { @@ -173,7 +173,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
173 // authentication schemes,e.g: Bearer 173 // authentication schemes,e.g: Bearer
174 // authenticationScheme: 'Bearer', 174 // authenticationScheme: 'Bearer',
175 authenticationScheme: 'Bearer', 175 authenticationScheme: 'Bearer',
176 - timeout: 10 * 1000, 176 + timeout: 26 * 1000,
177 // 基础接口地址 177 // 基础接口地址
178 // baseURL: globSetting.apiUrl, 178 // baseURL: globSetting.apiUrl,
179 // 接口可能会有通用的地址部分,可以统一抽取出来 179 // 接口可能会有通用的地址部分,可以统一抽取出来
@@ -89,3 +89,18 @@ export const withInstall = <T>(component: T, alias?: string) => { @@ -89,3 +89,18 @@ export const withInstall = <T>(component: T, alias?: string) => {
89 }; 89 };
90 return component as T & Plugin; 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,10 +7,10 @@ export const createOrganizationSearch = () => {
7 if (!value) { 7 if (!value) {
8 return true; 8 return true;
9 } 9 }
10 - let { name } = props || {}; 10 + let { title } = props || {};
11 value = value?.toLowerCase(); 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,11 +247,11 @@ export const alarmSchemasForm: FormSchema[] = [
247 disabled: true, 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 export function getAlarmStatus({ 257 export function getAlarmStatus({
@@ -51,6 +51,7 @@ @@ -51,6 +51,7 @@
51 import { useDrawer } from '/@/components/Drawer'; 51 import { useDrawer } from '/@/components/Drawer';
52 import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; 52 import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
53 import { buildUUID } from '/@/utils/uuid'; 53 import { buildUUID } from '/@/utils/uuid';
  54 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
54 55
55 export default defineComponent({ 56 export default defineComponent({
56 name: 'ContactDrawer', 57 name: 'ContactDrawer',
@@ -161,6 +162,11 @@ @@ -161,6 +162,11 @@
161 } else { 162 } else {
162 delete values.id; 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 let saveMessage = '添加成功'; 170 let saveMessage = '添加成功';
165 let updateMessage = '修改成功'; 171 let updateMessage = '修改成功';
166 172
@@ -100,7 +100,6 @@ @@ -100,7 +100,6 @@
100 AccessMode, 100 AccessMode,
101 PageMode, 101 PageMode,
102 CameraPermission, 102 CameraPermission,
103 - VideoPlatformEnum,  
104 accessModeConfig, 103 accessModeConfig,
105 getPlayUrl, 104 getPlayUrl,
106 } from './config.data'; 105 } from './config.data';
@@ -199,14 +198,12 @@ @@ -199,14 +198,12 @@
199 const handleViewVideo = (record: CameraRecord) => { 198 const handleViewVideo = (record: CameraRecord) => {
200 const { videoPlatformDTO, params } = record; 199 const { videoPlatformDTO, params } = record;
201 const { type } = videoPlatformDTO || {}; 200 const { type } = videoPlatformDTO || {};
202 -  
203 openModal(true, { 201 openModal(true, {
204 record: { 202 record: {
205 id: record.id, 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 isGBT: record.accessMode === AccessMode.GBT28181, 205 isGBT: record.accessMode === AccessMode.GBT28181,
  206 + videoPlatformType: type,
210 channelId: params?.channelNo, 207 channelId: params?.channelNo,
211 tbDeviceId: params?.deviceId, 208 tbDeviceId: params?.deviceId,
212 getPlayUrl: async () => { 209 getPlayUrl: async () => {
@@ -92,10 +92,11 @@ @@ -92,10 +92,11 @@
92 const { getResult } = useFingerprint(); 92 const { getResult } = useFingerprint();
93 const beforeVideoPlay = async (record: CameraRecordItem) => { 93 const beforeVideoPlay = async (record: CameraRecordItem) => {
94 if (isNullOrUnDef(record.accessMode)) return; 94 if (isNullOrUnDef(record.accessMode)) return;
95 - const { url, type } = await getPlayUrl(record); 95 + const { url, type, withToken } = (await getPlayUrl(record)) || {};
96 record.playSourceUrl = url; 96 record.playSourceUrl = url;
97 record.streamType = type; 97 record.streamType = type;
98 record.isTransform = true; 98 record.isTransform = true;
  99 + record.withToken = withToken;
99 }; 100 };
100 101
101 const gridLayout = ref({ gutter: 1, column: 2 }); 102 const gridLayout = ref({ gutter: 1, column: 2 });
@@ -192,7 +192,7 @@ export const formSchema: QFormSchema[] = [ @@ -192,7 +192,7 @@ export const formSchema: QFormSchema[] = [
192 component: 'ApiUpload', 192 component: 'ApiUpload',
193 changeEvent: 'update:fileList', 193 changeEvent: 'update:fileList',
194 valueField: 'fileList', 194 valueField: 'fileList',
195 - componentProps: () => { 195 + componentProps: ({ formModel }) => {
196 return { 196 return {
197 listType: 'picture-card', 197 listType: 'picture-card',
198 maxFileLimit: 1, 198 maxFileLimit: 1,
@@ -214,10 +214,19 @@ export const formSchema: QFormSchema[] = [ @@ -214,10 +214,19 @@ export const formSchema: QFormSchema[] = [
214 onPreview: (fileList: FileItem) => { 214 onPreview: (fileList: FileItem) => {
215 createImgPreview({ imageList: [fileList.url!] }); 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 field: 'name', 230 field: 'name',
222 label: '视频名字', 231 label: '视频名字',
223 required: true, 232 required: true,
@@ -623,9 +632,7 @@ export const formGBTSchema: QFormSchema[] = [ @@ -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 const { accessMode } = params; 636 const { accessMode } = params;
630 if (accessMode === AccessMode.ManuallyEnter) { 637 if (accessMode === AccessMode.ManuallyEnter) {
631 const { videoUrl } = params; 638 const { videoUrl } = params;
@@ -635,7 +642,11 @@ export async function getPlayUrl( @@ -635,7 +642,11 @@ export async function getPlayUrl(
635 if (isRTSPPlay) { 642 if (isRTSPPlay) {
636 const { getResult } = useFingerprint(); 643 const { getResult } = useFingerprint();
637 const fingerprint = await getResult(); 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 } else { 650 } else {
640 return { url: videoUrl, type: 'auto' }; 651 return { url: videoUrl, type: 'auto' };
641 } 652 }
@@ -52,6 +52,7 @@ @@ -52,6 +52,7 @@
52 import { createPickerSearch } from '/@/utils/pickerSearch'; 52 import { createPickerSearch } from '/@/utils/pickerSearch';
53 import type { queryPageParams } from '/@/api/configuration/center/model/configurationCenterModal'; 53 import type { queryPageParams } from '/@/api/configuration/center/model/configurationCenterModal';
54 import { getDeviceProfile } from '/@/api/alarm/position'; 54 import { getDeviceProfile } from '/@/api/alarm/position';
  55 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
55 56
56 export default defineComponent({ 57 export default defineComponent({
57 name: 'ConfigurationDrawer', 58 name: 'ConfigurationDrawer',
@@ -263,6 +264,11 @@ @@ -263,6 +264,11 @@
263 values.thumbnail = file.url || null; 264 values.thumbnail = file.url || null;
264 } 265 }
265 setDrawerProps({ confirmLoading: true }); 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 let saveMessage = '添加成功'; 272 let saveMessage = '添加成功';
267 let updateMessage = '修改成功'; 273 let updateMessage = '修改成功';
268 values.defaultContent = getDefaultContent(values.platform); 274 values.defaultContent = getDefaultContent(values.platform);
@@ -6,6 +6,7 @@ import { useComponentRegister } from '/@/components/Form'; @@ -6,6 +6,7 @@ import { useComponentRegister } from '/@/components/Form';
6 import { OrgTreeSelect } from '../../common/OrgTreeSelect'; 6 import { OrgTreeSelect } from '../../common/OrgTreeSelect';
7 import { getDeviceProfile } from '/@/api/alarm/position'; 7 import { getDeviceProfile } from '/@/api/alarm/position';
8 import { buildUUID } from '/@/utils/uuid'; 8 import { buildUUID } from '/@/utils/uuid';
  9 +import { createPickerSearch } from '/@/utils/pickerSearch';
9 10
10 useComponentRegister('OrgTreeSelect', OrgTreeSelect); 11 useComponentRegister('OrgTreeSelect', OrgTreeSelect);
11 export enum Platform { 12 export enum Platform {
@@ -90,7 +91,7 @@ export const formSchema: FormSchema[] = [ @@ -90,7 +91,7 @@ export const formSchema: FormSchema[] = [
90 component: 'ApiUpload', 91 component: 'ApiUpload',
91 changeEvent: 'update:fileList', 92 changeEvent: 'update:fileList',
92 valueField: 'fileList', 93 valueField: 'fileList',
93 - componentProps: () => { 94 + componentProps: ({ formModel }) => {
94 return { 95 return {
95 listType: 'picture-card', 96 listType: 'picture-card',
96 maxFileLimit: 1, 97 maxFileLimit: 1,
@@ -112,10 +113,19 @@ export const formSchema: FormSchema[] = [ @@ -112,10 +113,19 @@ export const formSchema: FormSchema[] = [
112 onPreview: (fileList: FileItem) => { 113 onPreview: (fileList: FileItem) => {
113 createImgPreview({ imageList: [fileList.url!] }); 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 field: 'name', 129 field: 'name',
120 label: '组态名称', 130 label: '组态名称',
121 required: true, 131 required: true,
@@ -210,14 +220,7 @@ export const formSchema: FormSchema[] = [ @@ -210,14 +220,7 @@ export const formSchema: FormSchema[] = [
210 labelField: 'name', 220 labelField: 'name',
211 valueField: 'id', 221 valueField: 'id',
212 placeholder: '请选择产品', 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 ifShow: ({ values }) => values['enableTemplate'] === 0, 225 ifShow: ({ values }) => values['enableTemplate'] === 0,
223 }, 226 },
@@ -21,6 +21,7 @@ @@ -21,6 +21,7 @@
21 import { buildUUID } from '/@/utils/uuid'; 21 import { buildUUID } from '/@/utils/uuid';
22 import { getDeviceProfile } from '/@/api/alarm/position'; 22 import { getDeviceProfile } from '/@/api/alarm/position';
23 import { PC_DEFAULT_CONTENT, PHONE_DEFAULT_CONTENT, Platform } from '../center/center.data'; 23 import { PC_DEFAULT_CONTENT, PHONE_DEFAULT_CONTENT, Platform } from '../center/center.data';
  24 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
24 25
25 export default defineComponent({ 26 export default defineComponent({
26 name: 'ConfigurationDrawer', 27 name: 'ConfigurationDrawer',
@@ -91,6 +92,10 @@ @@ -91,6 +92,10 @@
91 const file = (values.thumbnail || []).at(0) || {}; 92 const file = (values.thumbnail || []).at(0) || {};
92 values.thumbnail = file.url || null; 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 setDrawerProps({ confirmLoading: true }); 99 setDrawerProps({ confirmLoading: true });
95 let saveMessage = '添加成功'; 100 let saveMessage = '添加成功';
96 let updateMessage = '修改成功'; 101 let updateMessage = '修改成功';
@@ -6,6 +6,7 @@ import { useComponentRegister } from '/@/components/Form'; @@ -6,6 +6,7 @@ import { useComponentRegister } from '/@/components/Form';
6 import { OrgTreeSelect } from '../../common/OrgTreeSelect'; 6 import { OrgTreeSelect } from '../../common/OrgTreeSelect';
7 import { getDeviceProfile } from '/@/api/alarm/position'; 7 import { getDeviceProfile } from '/@/api/alarm/position';
8 import { Platform } from '../center/center.data'; 8 import { Platform } from '../center/center.data';
  9 +import { createPickerSearch } from '/@/utils/pickerSearch';
9 10
10 useComponentRegister('OrgTreeSelect', OrgTreeSelect); 11 useComponentRegister('OrgTreeSelect', OrgTreeSelect);
11 12
@@ -79,7 +80,7 @@ export const formSchema: FormSchema[] = [ @@ -79,7 +80,7 @@ export const formSchema: FormSchema[] = [
79 component: 'ApiUpload', 80 component: 'ApiUpload',
80 changeEvent: 'update:fileList', 81 changeEvent: 'update:fileList',
81 valueField: 'fileList', 82 valueField: 'fileList',
82 - componentProps: () => { 83 + componentProps: ({ formModel }) => {
83 return { 84 return {
84 listType: 'picture-card', 85 listType: 'picture-card',
85 maxFileLimit: 1, 86 maxFileLimit: 1,
@@ -101,9 +102,18 @@ export const formSchema: FormSchema[] = [ @@ -101,9 +102,18 @@ export const formSchema: FormSchema[] = [
101 onPreview: (fileList: FileItem) => { 102 onPreview: (fileList: FileItem) => {
102 createImgPreview({ imageList: [fileList.url!] }); 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 field: 'name', 119 field: 'name',
@@ -131,6 +141,7 @@ export const formSchema: FormSchema[] = [ @@ -131,6 +141,7 @@ export const formSchema: FormSchema[] = [
131 mode: 'multiple', 141 mode: 'multiple',
132 labelField: 'name', 142 labelField: 'name',
133 valueField: 'id', 143 valueField: 'id',
  144 + ...createPickerSearch(),
134 }, 145 },
135 }, 146 },
136 { 147 {
@@ -19,6 +19,7 @@ @@ -19,6 +19,7 @@
19 import { saveOrUpdateBigScreenCenter } from '/@/api/bigscreen/center/bigscreenCenter'; 19 import { saveOrUpdateBigScreenCenter } from '/@/api/bigscreen/center/bigscreenCenter';
20 import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; 20 import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
21 import { buildUUID } from '/@/utils/uuid'; 21 import { buildUUID } from '/@/utils/uuid';
  22 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
22 23
23 export default defineComponent({ 24 export default defineComponent({
24 name: 'BigScreenDrawer', 25 name: 'BigScreenDrawer',
@@ -67,6 +68,10 @@ @@ -67,6 +68,10 @@
67 const file = (values.thumbnail || []).at(0) || {}; 68 const file = (values.thumbnail || []).at(0) || {};
68 values.thumbnail = file.url || null; 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 setDrawerProps({ confirmLoading: true }); 75 setDrawerProps({ confirmLoading: true });
71 let saveMessage = '添加成功'; 76 let saveMessage = '添加成功';
72 let updateMessage = '修改成功'; 77 let updateMessage = '修改成功';
@@ -56,7 +56,7 @@ export const formSchema: FormSchema[] = [ @@ -56,7 +56,7 @@ export const formSchema: FormSchema[] = [
56 component: 'ApiUpload', 56 component: 'ApiUpload',
57 changeEvent: 'update:fileList', 57 changeEvent: 'update:fileList',
58 valueField: 'fileList', 58 valueField: 'fileList',
59 - componentProps: () => { 59 + componentProps: ({ formModel }) => {
60 return { 60 return {
61 listType: 'picture-card', 61 listType: 'picture-card',
62 maxFileLimit: 1, 62 maxFileLimit: 1,
@@ -80,9 +80,18 @@ export const formSchema: FormSchema[] = [ @@ -80,9 +80,18 @@ export const formSchema: FormSchema[] = [
80 onPreview: (fileList: FileItem) => { 80 onPreview: (fileList: FileItem) => {
81 createImgPreview({ imageList: [fileList.url!] }); 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 field: 'name', 97 field: 'name',
@@ -48,7 +48,10 @@ @@ -48,7 +48,10 @@
48 rowSelection: { 48 rowSelection: {
49 type: 'checkbox', 49 type: 'checkbox',
50 getCheckboxProps: (record: any) => { 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,12 +89,14 @@
86 89
87 // 详情 90 // 详情
88 const registerDetailRecord = ref<any>({}); 91 const registerDetailRecord = ref<any>({});
  92 + const isCurrentTenant = ref<Boolean>(false);
89 const handleDetail = (record?: any) => { 93 const handleDetail = (record?: any) => {
90 openDrawer(true); 94 openDrawer(true);
91 registerDetailRecord.value = { 95 registerDetailRecord.value = {
92 ...record, 96 ...record,
93 ifShowClass: true, 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,7 +104,7 @@
99 switchLoading.value = true; 104 switchLoading.value = true;
100 await deviceProfileCategory({ ...record, status: e }); 105 await deviceProfileCategory({ ...record, status: e });
101 switchLoading.value = false; 106 switchLoading.value = false;
102 - createMessage.success('操作成功'); 107 + createMessage.success(`${!e ? '禁用' : '启用'}成功`);
103 handleReload(); 108 handleReload();
104 }; 109 };
105 110
@@ -145,6 +150,7 @@ @@ -145,6 +150,7 @@
145 <Switch 150 <Switch
146 @click="(e) => handleSwitch(e, record)" 151 @click="(e) => handleSwitch(e, record)"
147 :loading="switchLoading" 152 :loading="switchLoading"
  153 + :disabled="getAuthCache(USER_INFO_KEY).tenantId !== record.tenantId"
148 v-model:checked="record.status" 154 v-model:checked="record.status"
149 checked-children="启用" 155 checked-children="启用"
150 un-checked-children="禁用" 156 un-checked-children="禁用"
@@ -165,14 +171,17 @@ @@ -165,14 +171,17 @@
165 label: '编辑', 171 label: '编辑',
166 auth: 'api:yt:product:category:update', 172 auth: 'api:yt:product:category:update',
167 icon: 'clarity:note-edit-line', 173 icon: 'clarity:note-edit-line',
168 - ifShow: authBtn(role), 174 + ifShow: authBtn(role) && getAuthCache(USER_INFO_KEY).tenantId === record.tenantId,
169 onClick: handleEdit.bind(null, record), 175 onClick: handleEdit.bind(null, record),
170 }, 176 },
171 { 177 {
172 label: '删除', 178 label: '删除',
173 auth: 'api:yt:product:category:delete', 179 auth: 'api:yt:product:category:delete',
174 icon: 'ant-design:delete-outlined', 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 color: 'error', 185 color: 'error',
177 popConfirm: { 186 popConfirm: {
178 title: '是否确认删除', 187 title: '是否确认删除',
@@ -185,7 +194,10 @@ @@ -185,7 +194,10 @@
185 </BasicTable> 194 </BasicTable>
186 <classModal @register="registerModal" @handleReload="handleReload" /> 195 <classModal @register="registerModal" @handleReload="handleReload" />
187 <BasicDrawer title="物模型" @register="registerDetailDrawer" width="60%" destroy-on-close> 196 <BasicDrawer title="物模型" @register="registerDetailDrawer" width="60%" destroy-on-close>
188 - <PhysicalModelManagementStep :record="registerDetailRecord" /> 197 + <PhysicalModelManagementStep
  198 + :isCurrentTenant="isCurrentTenant"
  199 + :record="registerDetailRecord"
  200 + />
189 </BasicDrawer> 201 </BasicDrawer>
190 </div> 202 </div>
191 </template> 203 </template>
@@ -49,7 +49,7 @@ export const step1Schemas: FormSchema[] = [ @@ -49,7 +49,7 @@ export const step1Schemas: FormSchema[] = [
49 component: 'ApiUpload', 49 component: 'ApiUpload',
50 changeEvent: 'update:fileList', 50 changeEvent: 'update:fileList',
51 valueField: 'fileList', 51 valueField: 'fileList',
52 - componentProps: () => { 52 + componentProps: ({ formModel }) => {
53 return { 53 return {
54 listType: 'picture-card', 54 listType: 'picture-card',
55 maxFileLimit: 1, 55 maxFileLimit: 1,
@@ -71,10 +71,19 @@ export const step1Schemas: FormSchema[] = [ @@ -71,10 +71,19 @@ export const step1Schemas: FormSchema[] = [
71 onPreview: (fileList: FileItem) => { 71 onPreview: (fileList: FileItem) => {
72 createImgPreview({ imageList: [fileList.url!] }); 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 field: 'alias', 87 field: 'alias',
79 label: '别名 ', 88 label: '别名 ',
80 component: 'Input', 89 component: 'Input',
@@ -193,8 +202,29 @@ export const step1Schemas: FormSchema[] = [ @@ -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 field: 'addressCode', 218 field: 'addressCode',
197 label: '地址码', 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 dynamicRules({ values }) { 228 dynamicRules({ values }) {
199 return [ 229 return [
200 { 230 {
@@ -202,18 +232,31 @@ export const step1Schemas: FormSchema[] = [ @@ -202,18 +232,31 @@ export const step1Schemas: FormSchema[] = [
202 values?.transportType === TransportTypeEnum.TCP && 232 values?.transportType === TransportTypeEnum.TCP &&
203 values?.tcpDeviceProtocol === TCPProtocolTypeEnum.MODBUS_RTU, 233 values?.tcpDeviceProtocol === TCPProtocolTypeEnum.MODBUS_RTU,
204 message: '地址码范围为00~FF', 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 component: 'HexInput', 248 component: 'HexInput',
211 changeEvent: 'update:value', 249 changeEvent: 'update:value',
212 valueField: 'value', 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 ifShow: ({ values }) => { 261 ifShow: ({ values }) => {
219 return ( 262 return (
@@ -235,6 +278,22 @@ export const step1Schemas: FormSchema[] = [ @@ -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 component: 'Input', 297 component: 'Input',
239 componentProps: () => { 298 componentProps: () => {
240 return { 299 return {
@@ -276,7 +335,7 @@ export const step1Schemas: FormSchema[] = [ @@ -276,7 +335,7 @@ export const step1Schemas: FormSchema[] = [
276 component: 'ApiSelect', 335 component: 'ApiSelect',
277 ifShow: ({ values }) => values.deviceType === 'SENSOR', 336 ifShow: ({ values }) => values.deviceType === 'SENSOR',
278 componentProps: ({ formModel, formActionType }) => { 337 componentProps: ({ formModel, formActionType }) => {
279 - const { transportType } = formModel; 338 + const { transportType, deviceType, gatewayId } = formModel;
280 const { setFieldsValue } = formActionType; 339 const { setFieldsValue } = formActionType;
281 if (!transportType) return {}; 340 if (!transportType) return {};
282 return { 341 return {
@@ -291,6 +350,7 @@ export const step1Schemas: FormSchema[] = [ @@ -291,6 +350,7 @@ export const step1Schemas: FormSchema[] = [
291 showSearch: true, 350 showSearch: true,
292 params: { 351 params: {
293 transportType, 352 transportType,
  353 + gatewayId: deviceType === DeviceTypeEnum.SENSOR && gatewayId ? gatewayId : null,
294 }, 354 },
295 placeholder: '请选择网关设备', 355 placeholder: '请选择网关设备',
296 valueField: 'tbDeviceId', 356 valueField: 'tbDeviceId',
@@ -6,6 +6,7 @@ import { deviceProfile } from '/@/api/device/deviceManager'; @@ -6,6 +6,7 @@ import { deviceProfile } from '/@/api/device/deviceManager';
6 import { h } from 'vue'; 6 import { h } from 'vue';
7 import { Tag, Tooltip } from 'ant-design-vue'; 7 import { Tag, Tooltip } from 'ant-design-vue';
8 import { handeleCopy } from '../../profiles/step/topic'; 8 import { handeleCopy } from '../../profiles/step/topic';
  9 +import edgefornt from '/@/assets/icons/edgefornt.svg';
9 10
10 export enum DeviceListAuthEnum { 11 export enum DeviceListAuthEnum {
11 /** 12 /**
@@ -84,8 +85,9 @@ export const columns: BasicColumn[] = [ @@ -84,8 +85,9 @@ export const columns: BasicColumn[] = [
84 title: '别名/设备名称', 85 title: '别名/设备名称',
85 width: 210, 86 width: 210,
86 slots: { customRender: 'name', title: 'deviceTitle' }, 87 slots: { customRender: 'name', title: 'deviceTitle' },
  88 + className: 'device-name-edge',
87 customRender: ({ record }) => { 89 customRender: ({ record }) => {
88 - return h('div', { style: 'display:flex;flex-direction:column' }, [ 90 + return h('div', { class: 'py-3 px-3.5' }, [
89 record.alias && 91 record.alias &&
90 h( 92 h(
91 'div', 93 'div',
@@ -118,6 +120,19 @@ export const columns: BasicColumn[] = [ @@ -118,6 +120,19 @@ export const columns: BasicColumn[] = [
118 () => `${record.name}` 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,7 +177,9 @@ export const columns: BasicColumn[] = [
162 { 177 {
163 title: '最后断开时间', 178 title: '最后断开时间',
164 dataIndex: 'lastOfflineTime', 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 width: 160, 183 width: 160,
167 }, 184 },
168 ]; 185 ];
@@ -41,6 +41,7 @@ @@ -41,6 +41,7 @@
41 import { Steps } from 'ant-design-vue'; 41 import { Steps } from 'ant-design-vue';
42 import { useMessage } from '/@/hooks/web/useMessage'; 42 import { useMessage } from '/@/hooks/web/useMessage';
43 import { credentialTypeEnum } from '../../config/data'; 43 import { credentialTypeEnum } from '../../config/data';
  44 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
44 45
45 export default defineComponent({ 46 export default defineComponent({
46 name: 'DeviceModal', 47 name: 'DeviceModal',
@@ -161,8 +162,13 @@ @@ -161,8 +162,13 @@
161 avatar: stepRecord?.icon, 162 avatar: stepRecord?.icon,
162 ...DeviceStep1Ref.value?.devicePositionState, 163 ...DeviceStep1Ref.value?.devicePositionState,
163 }, 164 },
  165 + alarmStatus: currentDeviceData?.alarmStatus,
164 }; 166 };
165 validateNameLength(stepRecord.name); 167 validateNameLength(stepRecord.name);
  168 + if (Reflect.has(editData, 'deleteUrl')) {
  169 + await deleteFilePath(editData?.deleteUrl);
  170 + Reflect.deleteProperty(editData, 'deleteUrl');
  171 + }
166 await createOrEditDevice(editData); 172 await createOrEditDevice(editData);
167 } else { 173 } else {
168 const stepRecord = unref(stepState); 174 const stepRecord = unref(stepState);
@@ -401,16 +401,16 @@ @@ -401,16 +401,16 @@
401 // 父组件调用更新字段值的方法 401 // 父组件调用更新字段值的方法
402 function parentSetFieldsValue(data) { 402 function parentSetFieldsValue(data) {
403 const { deviceInfo = {} } = data; 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 devicePositionState.value = { ...toRaw(positionState) }; 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 setFieldsValue({ 411 setFieldsValue({
412 isUpdate: unref(isUpdate1), 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 setFieldsValue({ 416 setFieldsValue({
@@ -119,6 +119,7 @@ @@ -119,6 +119,7 @@
119 ], 119 ],
120 }; 120 };
121 }); 121 });
  122 + const cacheSearchValue = ref('');
122 123
123 const [registerForm, { getFieldsValue }] = useForm({ 124 const [registerForm, { getFieldsValue }] = useForm({
124 schemas: [ 125 schemas: [
@@ -141,6 +142,8 @@ @@ -141,6 +142,8 @@
141 142
142 pagination.current = 1; 143 pagination.current = 1;
143 144
  145 + cacheSearchValue.value = value;
  146 +
144 socketInfo.filterAttrKeys = value 147 socketInfo.filterAttrKeys = value
145 ? unref(socketInfo.rawDataSource) 148 ? unref(socketInfo.rawDataSource)
146 .filter( 149 .filter(
@@ -161,6 +164,7 @@ @@ -161,6 +164,7 @@
161 resetFunc: async () => { 164 resetFunc: async () => {
162 try { 165 try {
163 socketInfo.filterAttrKeys = []; 166 socketInfo.filterAttrKeys = [];
  167 + cacheSearchValue.value = '';
164 handleFilterChange(); 168 handleFilterChange();
165 unref(mode) === EnumTableCardMode.TABLE && setTableModeData(); 169 unref(mode) === EnumTableCardMode.TABLE && setTableModeData();
166 } catch (error) {} 170 } catch (error) {}
@@ -201,8 +205,15 @@ @@ -201,8 +205,15 @@
201 const { createMessage } = useMessage(); 205 const { createMessage } = useMessage();
202 206
203 const setDataSource = () => { 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 socketInfo.dataSource = socketInfo.filterAttrKeys.length 213 socketInfo.dataSource = socketInfo.filterAttrKeys.length
205 ? socketInfo.rawDataSource.filter((item) => socketInfo.filterAttrKeys.includes(item.key)) 214 ? socketInfo.rawDataSource.filter((item) => socketInfo.filterAttrKeys.includes(item.key))
  215 + : filterValueByCacheSearchValue.length === 0
  216 + ? []
206 : socketInfo.rawDataSource; 217 : socketInfo.rawDataSource;
207 }; 218 };
208 219
1 import { h } from 'vue'; 1 import { h } from 'vue';
2 import { BasicColumn, FormSchema } from '/@/components/Table'; 2 import { BasicColumn, FormSchema } from '/@/components/Table';
3 import { Tag } from 'ant-design-vue'; 3 import { Tag } from 'ant-design-vue';
  4 +import { VideoPlatformEnum } from '/@/views/camera/manage/config.data';
4 export type VideoCancelModalParamsType = { 5 export type VideoCancelModalParamsType = {
5 canControl?: boolean; 6 canControl?: boolean;
  7 + videoPlatformType: VideoPlatformEnum;
6 isGBT?: boolean; 8 isGBT?: boolean;
7 tbDeviceId?: string; 9 tbDeviceId?: string;
8 channelId?: string; 10 channelId?: string;
@@ -101,3 +103,25 @@ export const searchFormSchema: FormSchema[] | any = [ @@ -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,20 +13,14 @@
13 } from '@ant-design/icons-vue'; 13 } from '@ant-design/icons-vue';
14 import { Button, Slider } from 'ant-design-vue'; 14 import { Button, Slider } from 'ant-design-vue';
15 import { controlling } from '/@/api/camera/cameraManager'; 15 import { controlling } from '/@/api/camera/cameraManager';
16 - import { setVideoControl } from '/@/api/device/videoChannel'; 16 + import { setEzvizControl, setVideoControl } from '/@/api/device/videoChannel';
17 import XGPlayer from '/@/components/Video/src/XGPlayer.vue'; 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 const props = defineProps<{ 21 const props = defineProps<{
21 playUrl?: string; 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 const playerRef = ref<InstanceType<typeof XGPlayer>>(); 26 const playerRef = ref<InstanceType<typeof XGPlayer>>();
@@ -64,12 +58,24 @@ @@ -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 const moveStart = (action: string) => { 68 const moveStart = (action: string) => {
69 if (unref(props.options?.isGBT)) { 69 if (unref(props.options?.isGBT)) {
70 handleGBTControl(action, unref(sliderValue)); 70 handleGBTControl(action, unref(sliderValue));
71 return; 71 return;
72 } 72 }
  73 +
  74 + if (props.options?.videoPlatformType === VideoPlatformEnum.FLUORITE) {
  75 + handleEzvizControl(action, true);
  76 + return;
  77 + }
  78 +
73 handleControl(0, action); 79 handleControl(0, action);
74 }; 80 };
75 81
@@ -79,6 +85,14 @@ @@ -79,6 +85,14 @@
79 handleGBTControl('STOP', unref(sliderValue)); 85 handleGBTControl('STOP', unref(sliderValue));
80 return; 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 handleControl(1, action); 96 handleControl(1, action);
83 }; 97 };
84 98
@@ -122,15 +136,15 @@ @@ -122,15 +136,15 @@
122 <div> 136 <div>
123 <Button 137 <Button
124 class="left-top in-block" 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 <CaretUpOutlined class="icon-rotate child-icon" /> 142 <CaretUpOutlined class="icon-rotate child-icon" />
129 </Button> 143 </Button>
130 <Button 144 <Button
131 class="right-top in-block" 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 <CaretRightOutlined class="icon-rotate child-icon" /> 149 <CaretRightOutlined class="icon-rotate child-icon" />
136 </Button> 150 </Button>
@@ -138,15 +152,15 @@ @@ -138,15 +152,15 @@
138 <div> 152 <div>
139 <Button 153 <Button
140 class="left-bottom in-block" 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 <CaretLeftOutlined class="icon-rotate child-icon" /> 158 <CaretLeftOutlined class="icon-rotate child-icon" />
145 </Button> 159 </Button>
146 <Button 160 <Button
147 class="right-bottom in-block" 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 <CaretDownOutlined class="icon-rotate child-icon" /> 165 <CaretDownOutlined class="icon-rotate child-icon" />
152 </Button> 166 </Button>
@@ -161,16 +175,16 @@ @@ -161,16 +175,16 @@
161 <div class="flex justify-center mt-8"> 175 <div class="flex justify-center mt-8">
162 <Button 176 <Button
163 class="button-icon" 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 style="border-radius: 50%" 180 style="border-radius: 50%"
167 > 181 >
168 <ZoomInOutlined style="color: #315a9c; font-size: 1.5rem" /> 182 <ZoomInOutlined style="color: #315a9c; font-size: 1.5rem" />
169 </Button> 183 </Button>
170 <Button 184 <Button
171 class="ml-10 button-icon" 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 style="border-radius: 50%" 188 style="border-radius: 50%"
175 > 189 >
176 <ZoomOutOutlined style="color: #315a9c; font-size: 1.5rem" /> 190 <ZoomOutOutlined style="color: #315a9c; font-size: 1.5rem" />
@@ -31,7 +31,6 @@ @@ -31,7 +31,6 @@
31 options.value = null; 31 options.value = null;
32 setModalProps({ loading: true, loadingTip: '视频加载中...' }); 32 setModalProps({ loading: true, loadingTip: '视频加载中...' });
33 const { url, type } = await record.getPlayUrl(); 33 const { url, type } = await record.getPlayUrl();
34 -  
35 playUrl.value = url; 34 playUrl.value = url;
36 options.value = record; 35 options.value = record;
37 options.value.playerProps = { 36 options.value.playerProps = {
@@ -147,6 +147,8 @@ @@ -147,6 +147,8 @@
147 ? 'warning' 147 ? 'warning'
148 : record.deviceState == DeviceState.ONLINE 148 : record.deviceState == DeviceState.ONLINE
149 ? 'success' 149 ? 'success'
  150 + : record.deviceState == DeviceState.ACTIVE
  151 + ? 'success'
150 : 'error' 152 : 'error'
151 " 153 "
152 class="ml-2" 154 class="ml-2"
@@ -156,6 +158,8 @@ @@ -156,6 +158,8 @@
156 ? '待激活' 158 ? '待激活'
157 : record.deviceState == DeviceState.ONLINE 159 : record.deviceState == DeviceState.ONLINE
158 ? '在线' 160 ? '在线'
  161 + : record.deviceState == DeviceState.ACTIVE
  162 + ? '活动'
159 : '离线' 163 : '离线'
160 }} 164 }}
161 </Tag> 165 </Tag>
@@ -231,7 +235,7 @@ @@ -231,7 +235,7 @@
231 ifShow: authBtn(role) && record.customerId === undefined, 235 ifShow: authBtn(role) && record.customerId === undefined,
232 color: 'error', 236 color: 'error',
233 popConfirm: { 237 popConfirm: {
234 - title: '是否确认删除', 238 + title: !!record.isEdge ? '此设备来自边端,请谨慎删除' : '是否确认删除',
235 confirm: handleDelete.bind(null, record), 239 confirm: handleDelete.bind(null, record),
236 }, 240 },
237 }, 241 },
@@ -707,4 +711,11 @@ @@ -707,4 +711,11 @@
707 border-right: 30px solid transparent; 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 </style> 721 </style>
@@ -24,6 +24,7 @@ @@ -24,6 +24,7 @@
24 import { BasicCardList, useCardList } from '/@/components/CardList'; 24 import { BasicCardList, useCardList } from '/@/components/CardList';
25 import { useRoute } from 'vue-router'; 25 import { useRoute } from 'vue-router';
26 import { ref } from 'vue'; 26 import { ref } from 'vue';
  27 + import edgefornt from '/@/assets/icons/edgefornt.svg';
27 28
28 defineProps<{ 29 defineProps<{
29 mode: EnumTableCardMode; 30 mode: EnumTableCardMode;
@@ -154,7 +155,7 @@ @@ -154,7 +155,7 @@
154 <template #renderItem="{ item }: BasicCardListRenderItem<ProfileRecord>"> 155 <template #renderItem="{ item }: BasicCardListRenderItem<ProfileRecord>">
155 <Card hoverable> 156 <Card hoverable>
156 <template #cover> 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 <Image 159 <Image
159 @click.stop 160 @click.stop
160 wrapper-class-name="!w-32 !h-32 !flex !items-center overflow-hidden" 161 wrapper-class-name="!w-32 !h-32 !flex !items-center overflow-hidden"
@@ -162,6 +163,13 @@ @@ -162,6 +163,13 @@
162 placeholder 163 placeholder
163 :fallback="IMAGE_FALLBACK" 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 </div> 173 </div>
166 </template> 174 </template>
167 <template class="ant-card-actions" #actions> 175 <template class="ant-card-actions" #actions>
@@ -199,7 +207,7 @@ @@ -199,7 +207,7 @@
199 auth: ProductPermission.DELETE, 207 auth: ProductPermission.DELETE,
200 icon: 'ant-design:delete-outlined', 208 icon: 'ant-design:delete-outlined',
201 popconfirm: { 209 popconfirm: {
202 - title: '是否确认删除操作?', 210 + title: !!item.isEdge ? '此产品来自边端,请谨慎删除' : '是否确认删除',
203 onConfirm: handleDelete.bind(null, [item.id]), 211 onConfirm: handleDelete.bind(null, [item.id]),
204 disabled: item.default || item.name == 'default', 212 disabled: item.default || item.name == 'default',
205 }, 213 },
@@ -20,7 +20,7 @@ @@ -20,7 +20,7 @@
20 key="modelOfMatter" 20 key="modelOfMatter"
21 tab="物模型管理" 21 tab="物模型管理"
22 > 22 >
23 - <PhysicalModelManagementStep :record="record" /> 23 + <PhysicalModelManagementStep :record="record" :isCurrentTenant="true" />
24 </Tabs.TabPane> 24 </Tabs.TabPane>
25 </Tabs> 25 </Tabs>
26 </BasicDrawer> 26 </BasicDrawer>
@@ -86,6 +86,7 @@ @@ -86,6 +86,7 @@
86 import TransportConfigurationStep from './step/TransportConfigurationStep.vue'; 86 import TransportConfigurationStep from './step/TransportConfigurationStep.vue';
87 import PhysicalModelManagementStep from './step/PhysicalModelManagementStep.vue'; 87 import PhysicalModelManagementStep from './step/PhysicalModelManagementStep.vue';
88 import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel'; 88 import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel';
  89 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
89 90
90 const emits = defineEmits(['success', 'register']); 91 const emits = defineEmits(['success', 'register']);
91 const activeKey = ref('1'); 92 const activeKey = ref('1');
@@ -188,6 +189,10 @@ @@ -188,6 +189,10 @@
188 await getDeviceConfFormData(); 189 await getDeviceConfFormData();
189 await getTransConfData(); 190 await getTransConfData();
190 const isEmptyObj = isEmpty(transportConfData.profileData.transportConfiguration); 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 await deviceConfigAddOrEdit({ 196 await deviceConfigAddOrEdit({
192 ...postSubmitFormData.deviceConfData, 197 ...postSubmitFormData.deviceConfData,
193 ...{ transportType: !isEmptyObj ? transportTypeStr.value : 'DEFAULT' }, 198 ...{ transportType: !isEmptyObj ? transportTypeStr.value : 'DEFAULT' },
@@ -79,7 +79,7 @@ @@ -79,7 +79,7 @@
79 icon: 'ant-design:delete-outlined', 79 icon: 'ant-design:delete-outlined',
80 color: 'error', 80 color: 'error',
81 popConfirm: { 81 popConfirm: {
82 - title: '是否确认删除', 82 + title: !!record.isEdge ? '此设备来自边端,请谨慎删除' : '是否确认删除',
83 confirm: handleDeleteOrBatchDelete.bind(null, record), 83 confirm: handleDeleteOrBatchDelete.bind(null, record),
84 }, 84 },
85 ifShow: () => { 85 ifShow: () => {
@@ -23,7 +23,7 @@ @@ -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 const typOptions = Object.values(InputTypeEnum).map((value) => ({ label: value, value })); 28 const typOptions = Object.values(InputTypeEnum).map((value) => ({ label: value, value }));
29 29
@@ -114,6 +114,10 @@ @@ -114,6 +114,10 @@
114 ? getHexToDec(unref(getInputValue)!) 114 ? getHexToDec(unref(getInputValue)!)
115 : `${hexWithPrefix ? '0x' : ''}${Number(unref(getInputValue)).toString(16).toUpperCase()}`; 115 : `${hexWithPrefix ? '0x' : ''}${Number(unref(getInputValue)).toString(16).toUpperCase()}`;
116 }); 116 });
  117 +
  118 + const handleInputTypeChange = (e) => {
  119 + emits('hexChange', e);
  120 + };
117 </script> 121 </script>
118 122
119 <template> 123 <template>
@@ -124,6 +128,7 @@ @@ -124,6 +128,7 @@
124 class="min-w-20" 128 class="min-w-20"
125 :options="typOptions" 129 :options="typOptions"
126 :disabled="disabled" 130 :disabled="disabled"
  131 + @change="handleInputTypeChange"
127 /> 132 />
128 <Input v-bind="$attrs" v-model:value="getInputValue" class="!w-full" :disabled="disabled" /> 133 <Input v-bind="$attrs" v-model:value="getInputValue" class="!w-full" :disabled="disabled" />
129 <div class="min-w-14 h-full px-2 flex-grow flex-shrink-0 text-center leading-8 bg-gray-200"> 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,7 +13,13 @@
13 <BasicForm @register="registerForm"> 13 <BasicForm @register="registerForm">
14 <template #importType="{ model }"> 14 <template #importType="{ model }">
15 <RadioGroup v-model:value="model.importType"> 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 <Radio value="2">JSON导入</Radio> 23 <Radio value="2">JSON导入</Radio>
18 </Authority> 24 </Authority>
19 <Authority :value="['api:yt:things_model:excel_import']"> 25 <Authority :value="['api:yt:things_model:excel_import']">
@@ -80,10 +86,11 @@ @@ -80,10 +86,11 @@
80 label: '导入类型', 86 label: '导入类型',
81 component: 'RadioGroup', 87 component: 'RadioGroup',
82 slot: 'importType', 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 ? '2' 94 ? '2'
88 : '1', 95 : '1',
89 helpMessage: 96 helpMessage:
@@ -182,7 +182,7 @@ export const step1Schemas: FormSchema[] = [ @@ -182,7 +182,7 @@ export const step1Schemas: FormSchema[] = [
182 component: 'ApiUpload', 182 component: 'ApiUpload',
183 changeEvent: 'update:fileList', 183 changeEvent: 'update:fileList',
184 valueField: 'fileList', 184 valueField: 'fileList',
185 - componentProps: () => { 185 + componentProps: ({ formModel }) => {
186 return { 186 return {
187 listType: 'picture-card', 187 listType: 'picture-card',
188 maxFileLimit: 1, 188 maxFileLimit: 1,
@@ -204,10 +204,19 @@ export const step1Schemas: FormSchema[] = [ @@ -204,10 +204,19 @@ export const step1Schemas: FormSchema[] = [
204 onPreview: (fileList: FileItem) => { 204 onPreview: (fileList: FileItem) => {
205 createImgPreview({ imageList: [fileList.url!] }); 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 field: 'deviceType', 220 field: 'deviceType',
212 component: 'ApiRadioGroup', 221 component: 'ApiRadioGroup',
213 label: '设备类型', 222 label: '设备类型',
@@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
8 > 8 >
9 <template #toolbar> 9 <template #toolbar>
10 <div class="flex-auto"> 10 <div class="flex-auto">
11 - <div class="mb-2"> 11 + <div class="mb-2" v-if="isCurrentTenant">
12 <Alert type="info" show-icon> 12 <Alert type="info" show-icon>
13 <template #message> 13 <template #message>
14 <span v-if="!isShowBtn"> 14 <span v-if="!isShowBtn">
@@ -39,6 +39,7 @@ @@ -39,6 +39,7 @@
39 <Button type="primary" @click="handleExport" :loading="loading">导出物模型</Button> 39 <Button type="primary" @click="handleExport" :loading="loading">导出物模型</Button>
40 </Authority> 40 </Authority>
41 <Authority 41 <Authority
  42 + v-if="isCurrentTenant"
42 :value="[ 43 :value="[
43 'api:yt:things_model:import', 44 'api:yt:things_model:import',
44 'api:yt:things_model:category:import', 45 'api:yt:things_model:category:import',
@@ -47,12 +48,18 @@ @@ -47,12 +48,18 @@
47 > 48 >
48 <Button type="primary" @click="handleSelectImport">导入物模型</Button> 49 <Button type="primary" @click="handleSelectImport">导入物模型</Button>
49 </Authority> 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 >编辑物模型</Button 55 >编辑物模型</Button
52 > 56 >
53 </div> 57 </div>
54 <div class="flex gap-2"> 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 <Popconfirm 63 <Popconfirm
57 title="是否需要发布上线?" 64 title="是否需要发布上线?"
58 ok-text="确定" 65 ok-text="确定"
@@ -159,11 +166,12 @@ @@ -159,11 +166,12 @@
159 import { DataActionModeEnum } from '/@/enums/toolEnum'; 166 import { DataActionModeEnum } from '/@/enums/toolEnum';
160 import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel'; 167 import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel';
161 168
162 - const { isPlatformAdmin, isSysadmin } = useRole(); 169 + const { isPlatformAdmin, isSysadmin, isTenantAdmin } = useRole();
163 defineEmits(['register']); 170 defineEmits(['register']);
164 171
165 const props = defineProps<{ 172 const props = defineProps<{
166 record: DeviceProfileDetail; 173 record: DeviceProfileDetail;
  174 + isCurrentTenant: Boolean;
167 }>(); 175 }>();
168 176
169 const { createMessage } = useMessage(); 177 const { createMessage } = useMessage();
@@ -224,7 +232,8 @@ @@ -224,7 +232,8 @@
224 232
225 const handleDeleteOrBatchDelete = async (record?: ModelOfMatterItemRecordType) => { 233 const handleDeleteOrBatchDelete = async (record?: ModelOfMatterItemRecordType) => {
226 const deleteFn = 234 const deleteFn =
227 - props.record.ifShowClass && (unref(isPlatformAdmin) || unref(isSysadmin)) 235 + props.record.ifShowClass &&
  236 + (unref(isPlatformAdmin) || unref(isSysadmin) || unref(isTenantAdmin))
228 ? deleteModelCategory 237 ? deleteModelCategory
229 : deleteModel; 238 : deleteModel;
230 239
@@ -265,7 +274,10 @@ @@ -265,7 +274,10 @@
265 const handleSelectImport = () => { 274 const handleSelectImport = () => {
266 openModalSelect(true, { 275 openModalSelect(true, {
267 id: props.record.id, 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,7 +31,7 @@
31 import { DataActionModeEnum, DataActionModeNameEnum } from '/@/enums/toolEnum'; 31 import { DataActionModeEnum, DataActionModeNameEnum } from '/@/enums/toolEnum';
32 import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel'; 32 import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel';
33 33
34 - const { isPlatformAdmin, isSysadmin } = useRole(); 34 + const { isPlatformAdmin, isSysadmin, isTenantAdmin } = useRole();
35 35
36 const emit = defineEmits(['register', 'success']); 36 const emit = defineEmits(['register', 'success']);
37 37
@@ -77,7 +77,9 @@ @@ -77,7 +77,9 @@
77 const value = unref(objectModelElRef)!.getFieldsValue(); 77 const value = unref(objectModelElRef)!.getFieldsValue();
78 78
79 const isCategoryAction = 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 const submitFn = 84 const submitFn =
83 unref(openModalMode) === DataActionModeEnum.CREATE 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 +}