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 163 changed files with 7310 additions and 366 deletions
  1 +FROM nginx:latest
  2 +
  3 +COPY ./target/dist/ /usr/share/nginx/html/
  4 +COPY default.conf /etc/nginx/conf.d/default.conf
  5 +
  6 +EXPOSE 80
\ No newline at end of file
... ...
  1 +server {
  2 + listen 80;
  3 + listen [::]:80;
  4 + server_name 192.168.1.48;
  5 + #charset koi8-r;
  6 + #access_log /var/log/nginx/host.access.log main;
  7 +
  8 + root /usr/share/nginx/html;
  9 + index index.html index.htm;
  10 + try_files $uri $uri/ /index.html;
  11 +
  12 +
  13 +
  14 + location /api/ {
  15 + proxy_pass http://192.168.1.48:8080;
  16 + proxy_http_version 1.1;
  17 + proxy_set_header Upgrade $http_upgrade;
  18 + proxy_set_header Connection "Upgrade";
  19 + proxy_set_header Host $host;
  20 + proxy_cache_bypass $http_upgrade;
  21 + }
  22 +
  23 + location /yt/ {
  24 + proxy_pass http://192.168.1.48:8080;
  25 + proxy_http_version 1.1;
  26 + proxy_set_header Upgrade $http_upgrade;
  27 + proxy_set_header Connection "Upgrade";
  28 + proxy_set_header Host $host;
  29 + proxy_cache_bypass $http_upgrade;
  30 + }
  31 +
  32 + location /upload/ {
  33 + proxy_pass http://192.168.1.48:8080;
  34 + proxy_http_version 1.1;
  35 + proxy_set_header Upgrade $http_upgrade;
  36 + proxy_set_header Connection "Upgrade";
  37 + proxy_set_header Host $host;
  38 + proxy_cache_bypass $http_upgrade;
  39 + }
  40 +
  41 + #error_page 404 /404.html;
  42 +
  43 + # redirect server error pages to the static page /50x.html
  44 + #
  45 + error_page 500 502 503 504 /50x.html;
  46 + location = /50x.html {
  47 + root /usr/share/nginx/html;
  48 + }
  49 +}
\ No newline at end of file
... ...
... ... @@ -5,10 +5,13 @@ import { isString } from '/@/utils/is';
5 5 import { OrderByEnum } from '/@/views/device/localtion/cpns/TimePeriodForm/config';
6 6
7 7 // 获取设备配置,很多地方使用,不能修改这里
8   -export const getDeviceProfile = (deviceType?: string) => {
  8 +export const getDeviceProfile = (deviceType?: string, isSceneLinkage?: Boolean) => {
9 9 return defHttp.get<DeviceProfileModel[]>({
10 10 url: '/device_profile/me/list',
11   - params: { deviceType: isString(deviceType) ? deviceType : undefined },
  11 + params: {
  12 + deviceType: isString(deviceType) ? deviceType : undefined,
  13 + isSceneLinkage: isSceneLinkage ? isSceneLinkage : undefined,
  14 + },
12 15 });
13 16 };
14 17
... ...
... ... @@ -9,12 +9,16 @@ enum OrderType {
9 9 ASC = 'ASC',
10 10 DESC = 'DESC',
11 11 }
  12 +enum SortProperty {
  13 + CREATEtIME = 'createdTime',
  14 +}
12 15
13 16 export interface BaseQueryParams {
14 17 pageSize: number;
15 18 page: number;
16 19 orderFiled?: string;
17 20 orderType?: OrderType;
  21 + sortProperty?: SortProperty;
18 22 }
19 23
20 24 export class BaseQueryRequest implements BaseQueryParams {
... ...
... ... @@ -264,13 +264,16 @@ export const getGATEWAYdevice = async (params: {
264 264 );
265 265 };
266 266
267   -export const getGatewayDevice = (params: Record<'organizationId' | 'transportType', string>) => {
268   - const { organizationId, transportType } = params;
  267 +export const getGatewayDevice = (
  268 + params: Record<'organizationId' | 'transportType' | 'gatewayId', string>
  269 +) => {
  270 + const { organizationId, transportType, gatewayId } = params;
269 271 return defHttp.get<DeviceRecord[]>({
270 272 url: DeviceManagerApi.GATEWAY_DEVICE,
271 273 params: {
272 274 organizationId,
273 275 transportType,
  276 + gatewayId,
274 277 },
275 278 });
276 279 };
... ...
... ... @@ -124,6 +124,7 @@ export interface ProfileRecord {
124 124 image: string;
125 125 type: string;
126 126 default: boolean;
  127 + isEdge?: boolean;
127 128
128 129 checked?: boolean;
129 130 }
... ...
... ... @@ -7,6 +7,7 @@ export enum DeviceState {
7 7 INACTIVE = 'INACTIVE',
8 8 ONLINE = 'ONLINE',
9 9 OFFLINE = 'OFFLINE',
  10 + ACTIVE = 'ACTIVE',
10 11 }
11 12 export enum DeviceTypeEnum {
12 13 GATEWAY = 'GATEWAY',
... ...
... ... @@ -102,3 +102,9 @@ export interface VideoChanneControlType {
102 102 tbDeviceId?: string | number | object;
103 103 channelId?: string | number | object;
104 104 }
  105 +
  106 +export interface EzvizControlType {
  107 + entityId: string;
  108 + action: number;
  109 + controllingType: number;
  110 +}
... ...
... ... @@ -3,6 +3,7 @@ import {
3 3 VideoChannelPlayAddressType,
4 4 VideoChannelQueryParamsType,
5 5 VideoChanneControlType,
  6 + EzvizControlType,
6 7 } from './model/videoChannelModel';
7 8 import { PaginationResult } from '/#/axios';
8 9 import { defHttp } from '/@/utils/http/axios';
... ... @@ -12,6 +13,7 @@ enum Api {
12 13 GET_VIDEO_CONTROL_START = '/video/control/start',
13 14 GET_VIDEO_CONTROL_STOP = '/video/control/stop',
14 15 SET_VIDEO_CONTROL_CONTROL = '/video/control/control',
  16 + SET_EZVIZ_CONTROL = '/video/ezviz/controlling',
15 17 }
16 18
17 19 export const getVideoChannelList = (params: VideoChannelQueryParamsType) => {
... ... @@ -43,3 +45,10 @@ export const setVideoControl = (tbDeviceId, channelId, params: VideoChanneContro
43 45 params,
44 46 });
45 47 };
  48 +
  49 +export const setEzvizControl = (params: EzvizControlType) => {
  50 + return defHttp.get({
  51 + url: Api.SET_EZVIZ_CONTROL,
  52 + params,
  53 + });
  54 +};
... ...
  1 +import { EdgeInstanceItemType, QueryEdgeInstancePageParams } from './model/edgeInstance';
  2 +import { PaginationResult } from '/#/axios';
  3 +import { defHttp } from '/@/utils/http/axios';
  4 +
  5 +enum EdgeManageApi {
  6 + PAGE_LIST_GET = '/tenant/edgeInfos',
  7 + EDGE = '/edge',
  8 + EDGE_INFO = '/edge/info',
  9 + EDGE_EVENTS = '/events/EDGE',
  10 + EDGE_DEVICE_INFO = '/device/info',
  11 + EDGE_SYNC = '/edge/sync',
  12 + EDGE_DEVICE_DISTRIBUTION = '/tenant/deviceInfos',
  13 +}
  14 +
  15 +//分页
  16 +export const edgeInstancePage = (params: QueryEdgeInstancePageParams) => {
  17 + return defHttp.get<PaginationResult<EdgeInstanceItemType>>(
  18 + {
  19 + url: EdgeManageApi.PAGE_LIST_GET,
  20 + params,
  21 + },
  22 + {
  23 + joinPrefix: false,
  24 + }
  25 + );
  26 +};
  27 +
  28 +// 创建或编辑
  29 +export const createOrEditEdgeInstance = (data: EdgeInstanceItemType) => {
  30 + return defHttp.post<EdgeInstanceItemType>(
  31 + {
  32 + url: EdgeManageApi.EDGE,
  33 + data,
  34 + },
  35 + {
  36 + joinPrefix: false,
  37 + }
  38 + );
  39 +};
  40 +
  41 +// 删除
  42 +export const deleteEdgeInstance = (id: string) => {
  43 + return defHttp.delete(
  44 + {
  45 + url: `${EdgeManageApi.EDGE}/${id}`,
  46 + },
  47 + {
  48 + joinPrefix: false,
  49 + }
  50 + );
  51 +};
  52 +
  53 +// 详情
  54 +export const infoEdgeInstance = (id: string) => {
  55 + return defHttp.get(
  56 + {
  57 + url: `${EdgeManageApi.EDGE_INFO}/${id}`,
  58 + },
  59 + {
  60 + joinPrefix: false,
  61 + }
  62 + );
  63 +};
  64 +
  65 +// 边缘事件
  66 +export const edgeEventPage = (params: QueryEdgeInstancePageParams, id: string | undefined) => {
  67 + return defHttp.get(
  68 + {
  69 + url: `${EdgeManageApi.EDGE_EVENTS}/${id}`,
  70 + params,
  71 + },
  72 + {
  73 + joinPrefix: false,
  74 + }
  75 + );
  76 +};
  77 +
  78 +// 边缘设备
  79 +export const edgeDevicePage = (params: QueryEdgeInstancePageParams, id: string | undefined) => {
  80 + return defHttp.get(
  81 + {
  82 + url: `${EdgeManageApi.EDGE}/${id}/devices`,
  83 + params,
  84 + },
  85 + {
  86 + joinPrefix: false,
  87 + }
  88 + );
  89 +};
  90 +
  91 +// 边缘设备详情
  92 +export const infoEdgeDevice = (id: string) => {
  93 + return defHttp.get(
  94 + {
  95 + url: `${EdgeManageApi.EDGE_DEVICE_INFO}/${id}`,
  96 + },
  97 + {
  98 + joinPrefix: false,
  99 + }
  100 + );
  101 +};
  102 +
  103 +// 边缘同步
  104 +export const syncEdge = (id: string) => {
  105 + return defHttp.post(
  106 + {
  107 + url: `${EdgeManageApi.EDGE_SYNC}/${id}`,
  108 + },
  109 + {
  110 + joinPrefix: false,
  111 + }
  112 + );
  113 +};
  114 +
  115 +//设备分配查询
  116 +export const edgeDeviceDistributionPage = (params: QueryEdgeInstancePageParams) => {
  117 + return defHttp.get<PaginationResult<EdgeInstanceItemType>>(
  118 + {
  119 + url: EdgeManageApi.EDGE_DEVICE_DISTRIBUTION,
  120 + params,
  121 + },
  122 + {
  123 + joinPrefix: false,
  124 + }
  125 + );
  126 +};
  127 +
  128 +//设备分配给边缘
  129 +export const edgeDeviceDistribution = (edgeId: string | undefined, deviceId: string) => {
  130 + return defHttp.post<PaginationResult<EdgeInstanceItemType>>(
  131 + {
  132 + url: `${EdgeManageApi.EDGE}/${edgeId}/device/${deviceId}`,
  133 + data: {
  134 + headers: {
  135 + normalizedNames: {},
  136 + lazyUpdate: null,
  137 + },
  138 + params: {
  139 + updates: null,
  140 + cloneFrom: null,
  141 + encoder: {},
  142 + map: null,
  143 + interceptorConfig: {
  144 + ignoreLoading: false,
  145 + ignoreErrors: false,
  146 + resendRequest: false,
  147 + },
  148 + },
  149 + },
  150 + },
  151 + {
  152 + joinPrefix: false,
  153 + }
  154 + );
  155 +};
  156 +
  157 +//取消设备分配给边缘
  158 +export const edgeDeviceDeleteDistribution = (edgeId: string | undefined, deviceId: string) => {
  159 + return defHttp.delete<PaginationResult<EdgeInstanceItemType>>(
  160 + {
  161 + url: `${EdgeManageApi.EDGE}/${edgeId}/device/${deviceId}`,
  162 + },
  163 + {
  164 + joinPrefix: false,
  165 + }
  166 + );
  167 +};
... ...
  1 +import {
  2 + Configuration,
  3 + ProvisionConfiguration,
  4 + TransportConfiguration,
  5 +} from '../../device/model/deviceModel';
  6 +import { ModelOfMatterParams } from '../../device/model/modelOfMatterModel';
  7 +
  8 +export type QueryEdgeInstancePageParams = {
  9 + name?: string;
  10 + tenantId?: string;
  11 + textSearch?: string;
  12 + page: number;
  13 + pageSize: number;
  14 + sortOrder?: string;
  15 + sortProperty?: string;
  16 +};
  17 +
  18 +export interface EdgeInstanceItemType {
  19 + id?: {
  20 + entityType: string;
  21 + id: string;
  22 + };
  23 + createdTime?: number;
  24 + tenantId?: {
  25 + entityType: string;
  26 + id: string;
  27 + };
  28 + customerId?: {
  29 + entityType: string;
  30 + id: string;
  31 + };
  32 + rootRuleChainId?: {
  33 + entityType: string;
  34 + id: string;
  35 + };
  36 + name: string;
  37 + label: string;
  38 + additionalInfo: {
  39 + description: string;
  40 + };
  41 + status: number;
  42 + type: string;
  43 + routingKey: string;
  44 + secret: string;
  45 + active?: boolean;
  46 +}
  47 +
  48 +export interface ProfileData {
  49 + configuration: Configuration;
  50 + transportConfiguration: TransportConfiguration;
  51 + provisionConfiguration: ProvisionConfiguration;
  52 + alarms?: any;
  53 + thingsModel?: ModelOfMatterParams[];
  54 +}
  55 +
  56 +export interface EdgeDeviceItemType {
  57 + creator: string;
  58 + createTime: string;
  59 + codeType?: string;
  60 + code?: string;
  61 + name: string;
  62 + transportType: string;
  63 + provisionType: string;
  64 + deviceType: string;
  65 + deviceCount: number;
  66 + tbDeviceId: string;
  67 + tbProfileId: string;
  68 + defaultQueueName: string;
  69 + deviceInfo?: {
  70 + avatar: string;
  71 + };
  72 + image: string;
  73 + type: string;
  74 + default: boolean;
  75 + defaultRuleChainId: string;
  76 + profileId: string;
  77 + alias?: string;
  78 + brand?: string;
  79 + organizationId: string;
  80 + organizationDTO: {
  81 + name: string;
  82 + };
  83 + alarmStatus: number;
  84 + deviceProfile: {
  85 + default: boolean;
  86 + name: string;
  87 + transportType: string;
  88 + profileData: ProfileData;
  89 + };
  90 + customerAdditionalInfo?: {
  91 + isPublic?: boolean;
  92 + };
  93 + ifShowClass?: Boolean;
  94 + sip?: {
  95 + cameraCode: string;
  96 + localIp: string;
  97 + manufacturer: string;
  98 + streamMode: string;
  99 + };
  100 + customerName?: string;
  101 + gatewayId?: string;
  102 + id: {
  103 + entityType: string;
  104 + id: string;
  105 + };
  106 + createdTime: number;
  107 + additionalInfo: {
  108 + gateway: boolean;
  109 + overwriteActivityTime: boolean;
  110 + description: string;
  111 + };
  112 + tenantId: {
  113 + entityType: string;
  114 + id: string;
  115 + };
  116 + customerId: {
  117 + entityType: string;
  118 + id: string;
  119 + };
  120 + label: string;
  121 + deviceProfileId: {
  122 + entityType: string;
  123 + id: string;
  124 + };
  125 + deviceData: {
  126 + configuration: {
  127 + type: string;
  128 + };
  129 + transportConfiguration: {
  130 + type: string;
  131 + };
  132 + };
  133 + firmwareId: null;
  134 + softwareId: null;
  135 + externalId: null;
  136 + customerTitle: null;
  137 + customerIsPublic: boolean;
  138 + deviceProfileName: string;
  139 + active: boolean;
  140 + deviceState: string;
  141 + deviceToken?: string;
  142 + gatewayName?: string;
  143 + gatewayAlias?: string;
  144 + sn: string;
  145 +}
... ...
... ... @@ -3,8 +3,17 @@ import { FileUploadResponse } from '/@/api/oss/FileUploadResponse';
3 3
4 4 enum Api {
5 5 BaseUploadUrl = '/oss/upload',
  6 + BaseDeleteUrl = '/oss',
6 7 }
7 8
8 9 export const upload = (file) => {
9 10 return defHttp.post<FileUploadResponse>({ url: Api.BaseUploadUrl, params: file });
10 11 };
  12 +
  13 +export const deleteFilePath = (deleteFilePath?: string) => {
  14 + if (!deleteFilePath) return;
  15 + const deleteParams = `?deleteFilePath=${deleteFilePath}`;
  16 + return defHttp.delete({
  17 + url: `${Api.BaseDeleteUrl}${deleteParams}`,
  18 + });
  19 +};
... ...
... ... @@ -125,11 +125,16 @@ export const getAttribute = (orgId) => {
125 125 export const byOrganizationIdGetMasterDevice = (params: {
126 126 organizationId: string;
127 127 deviceProfileId?: string;
  128 + isSceneLinkage?: Boolean;
128 129 }) => {
129   - const { organizationId, deviceProfileId } = params;
  130 + const { organizationId, deviceProfileId, isSceneLinkage } = params;
130 131 return defHttp.get<DeviceModel[]>({
131 132 url: `${ScreenManagerApi.MASTER_GET_DEVICE}`,
132   - params: { deviceProfileId, organizationId },
  133 + params: {
  134 + deviceProfileId,
  135 + organizationId,
  136 + isSceneLinkage: isSceneLinkage ? isSceneLinkage : undefined,
  137 + },
133 138 });
134 139 };
135 140 //TODO-fengtao
... ...
  1 +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="15.933" viewBox="0 0 16 15.933"><defs><style>.a{fill:#343c4c;}</style></defs><path class="a" d="M345.333,337.067h-2.667a1.337,1.337,0,0,0-1.333,1.333v2.667a1.337,1.337,0,0,0,1.333,1.333h2.667a1.337,1.337,0,0,0,1.333-1.333V338.4A1.337,1.337,0,0,0,345.333,337.067Zm0,4h-2.667V338.4h2.667Z" transform="translate(-335.999 -331.8)"/><path class="a" d="M15.333,8.533a.667.667,0,1,0,0-1.333H14V5.867h1.333A.63.63,0,0,0,16,5.2a.63.63,0,0,0-.667-.667H14A2.7,2.7,0,0,0,11.333,2V.667A.667.667,0,1,0,10,.667V2H8.667V.667a.667.667,0,0,0-1.333,0V2H6V.667A.63.63,0,0,0,5.333,0a.668.668,0,0,0-.667.667V2A2.7,2.7,0,0,0,2,4.533H.667A.63.63,0,0,0,0,5.2a.63.63,0,0,0,.667.667H2V7.2H.667a.667.667,0,0,0,0,1.333H2V9.867H.667a.667.667,0,1,0,0,1.333H2v.133A2.675,2.675,0,0,0,4.667,14v1.267a.63.63,0,0,0,.667.667A.63.63,0,0,0,6,15.267V14H7.333v1.267a.667.667,0,1,0,1.333,0V14H10v1.267a.667.667,0,1,0,1.333,0V14A2.675,2.675,0,0,0,14,11.333V11.2h1.333a.667.667,0,0,0,0-1.333H14V8.533h1.333Zm-2.667,2.8a1.337,1.337,0,0,1-1.333,1.333H4.667a1.337,1.337,0,0,1-1.333-1.333V4.667A1.337,1.337,0,0,1,4.667,3.333h6.667a1.337,1.337,0,0,1,1.333,1.333v6.667Z"/></svg>
\ No newline at end of file
... ...
  1 +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="15.873" viewBox="0 0 16 15.873"><defs><style>.a{fill:#343c4c;}</style></defs><g transform="translate(-75.041 -99.03)"><path class="a" d="M90.95,111.459l-1.7-11.191,0-.014a1.344,1.344,0,0,0-1.276-1.225H78.36a1.356,1.356,0,0,0-1.236,1.2l0,.011-2.049,11.425.011,0a2.32,2.32,0,0,0-.042.446v.384a2.4,2.4,0,0,0,2.4,2.4h11.2a2.4,2.4,0,0,0,2.4-2.4v-.384a2.4,2.4,0,0,0-.09-.658ZM78.292,100.645c.1-.389.256-.424.344-.434h9.108c.223,0,.3.335.317.416l1.38,9.1a2.292,2.292,0,0,0-1.065-.263H77.706a2.29,2.29,0,0,0-1.046.252ZM89.936,112.4a1.378,1.378,0,0,1-1.415,1.339H77.56a1.378,1.378,0,0,1-1.415-1.339v-.356a1.378,1.378,0,0,1,1.415-1.339H88.522a1.412,1.412,0,0,1,1.34.907l.066.418h.008v.369h0Z"/><path class="a" d="M160.413,229.5H158.5a.48.48,0,1,0,0,.96h1.916a.48.48,0,0,0,0-.96Zm1.964-3.83h5.032a1.864,1.864,0,0,0,1.391-.587,2.344,2.344,0,0,0,.522-1.826,2.536,2.536,0,0,0-1.747-1.987,2.511,2.511,0,0,0-2.416-2.261,2.382,2.382,0,0,0-1.789.835,1.835,1.835,0,0,0-1.685,1.245,2.49,2.49,0,0,0-1.953,2.063,1.87,1.87,0,0,0,.415,1.428A3.433,3.433,0,0,0,162.378,225.673Zm-1.514-2.45c.094-.792.66-1.029,1.454-1.287l.267-.089.054-.261a.852.852,0,0,1,.794-.719.616.616,0,0,1,.121.011l.291.048.167-.24a1.354,1.354,0,0,1,1.12-.617,1.509,1.509,0,0,1,1.414,1.559l0,.293.3.132c.7.31,1.293.614,1.352,1.244a1.34,1.34,0,0,1-.261,1.045.826.826,0,0,1-.652.267h-4.739a2.362,2.362,0,0,1-1.478-.708A.86.86,0,0,1,160.864,223.223Z" transform="translate(-81.382 -117.677)"/></g></svg>
\ No newline at end of file
... ...
  1 +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1722937185750" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5222" id="mx_n_1722937185750" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M0 0v1024a1024 1024 0 0 1 1024-1024H0z" p-id="5223" fill="#1890ff"></path></svg>
... ...
  1 +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:#343c4c;}</style></defs><g transform="translate(-562 -613)"><rect class="a" width="16" height="16" transform="translate(562 613)"/><g transform="translate(563.294 613)"><path class="b" d="M148.64,80.44H138.412a1.594,1.594,0,0,1-1.592-1.592V66.032a1.594,1.594,0,0,1,1.592-1.592h6.36a1.567,1.567,0,0,1,1.132.487l3.9,4.115a1.554,1.554,0,0,1,.428,1.073v8.733a1.594,1.594,0,0,1-1.592,1.592ZM138.412,65.714a.319.319,0,0,0-.318.318V78.848a.319.319,0,0,0,.318.318H148.64a.319.319,0,0,0,.318-.318V70.115a.285.285,0,0,0-.079-.2l-3.9-4.115a.287.287,0,0,0-.208-.089Z" transform="translate(-136.82 -64.44)"/><path class="b" d="M310.723,460.845h-6.046a.7.7,0,0,1-.7-.7v-5.04a.7.7,0,0,1,.7-.7h6.046a.7.7,0,0,1,.7.7v5.04a.7.7,0,0,1-.7.7Zm-5.787-.955h5.53v-4.524h-5.53Z" transform="translate(-300.992 -447.439)"/><path class="b" d="M426.518,592.369a.478.478,0,0,1-.478-.478v-2.774a.478.478,0,0,1,.955,0v2.774A.477.477,0,0,1,426.518,592.369Zm2.378,0a.478.478,0,0,1-.478-.478v-2.774a.478.478,0,0,1,.955,0v2.774A.478.478,0,0,1,428.9,592.369Z" transform="translate(-420.87 -579.27)"/></g></g></svg>
\ No newline at end of file
... ...
... ... @@ -71,10 +71,10 @@
71 71 if (!value) {
72 72 return true;
73 73 }
74   - let { name } = props || {};
  74 + let { title } = props || {};
75 75 value = value.toLowerCase();
76   - name = name.toLowerCase();
77   - return name.includes(value);
  76 + title = title.toLowerCase();
  77 + return title.includes(value);
78 78 }
79 79
80 80 async function fetch() {
... ...
... ... @@ -21,7 +21,7 @@
21 21 }
22 22
23 23 const { prefixCls } = useDesign('api-upload');
24   - const emit = defineEmits(['update:fileList', 'preview', 'download']);
  24 + const emit = defineEmits(['update:fileList', 'preview', 'download', 'delete']);
25 25
26 26 const { createMessage } = useMessage();
27 27
... ... @@ -71,9 +71,12 @@
71 71 return false;
72 72 }
73 73 }
74   -
75 74 if (file.size > props.maxSize) {
76   - createMessage.warning(`文件大小超过${Math.floor(props.maxSize / 1024 / 1024)}mb`);
  75 + createMessage.warning(
  76 + `文件大小超过${Math.floor(
  77 + props.maxSize > 1024 * 1024 ? props.maxSize / 1024 / 1024 : props.maxSize / 1024
  78 + )}${props.maxSize > 1024 * 1024 * 1 ? 'mb' : 'kb'}`
  79 + );
77 80 return false;
78 81 }
79 82 handleUpload(file);
... ... @@ -129,6 +132,7 @@
129 132 const index = _fileList.findIndex((item) => item.uid === file.uid);
130 133 ~index && _fileList.splice(index, 1);
131 134 emit('update:fileList', _fileList);
  135 + emit('delete', file.url);
132 136 };
133 137
134 138 const handlePreview = (file: FileItem) => {
... ...
... ... @@ -136,7 +136,8 @@ export interface FormSchema {
136 136 helpMessage?:
137 137 | string
138 138 | string[]
139   - | ((renderCallbackParams: RenderCallbackParams) => string | string[]);
  139 + | ((renderCallbackParams: RenderCallbackParams) => string | string[])
  140 + | Boolean;
140 141 // BaseHelp component props
141 142 helpComponentProps?: Partial<HelpComponentProps>;
142 143 // Label width, if it is passed, the labelCol and WrapperCol configured by itemProps will be invalid
... ...
... ... @@ -59,7 +59,7 @@
59 59 </span>
60 60 </Menu.Item>
61 61 <Menu.Item :disabled="item.disabled" v-if="item.popconfirm">
62   - <Popconfirm v-bind="item.popconfirm">
  62 + <Popconfirm :disabled="item.disabled" v-bind="item.popconfirm">
63 63 <template v-if="item.popconfirm.icon" #icon>
64 64 <Icon :icon="item.popconfirm.icon" />
65 65 </template>
... ...
... ... @@ -4,6 +4,7 @@ const menuMap = new Map();
4 4
5 5 menuMap.set('/visual/board/detail/:boardId/:boardName/:platform/:organizationId?', '/visual/board');
6 6 menuMap.set('/rule/chain/:id', '/rule/chain');
  7 +menuMap.set('/edge/edge_detail/:id', '/edge');
7 8
8 9 export const useMenuActiveFix = (route: RouteLocationNormalizedLoaded) => {
9 10 let flag = false;
... ...
... ... @@ -2,10 +2,12 @@
2 2 <Dropdown placement="bottomLeft" :overlayClassName="`${prefixCls}-dropdown-overlay`">
3 3 <span :class="[prefixCls, `${prefixCls}--${theme}`]" class="flex">
4 4 <img :class="`${prefixCls}__header`" :src="getUserInfo.avatar" />
5   - <span :class="`${prefixCls}__info hidden md:block`">
6   - <span :class="`${prefixCls}__name `" class="truncate">
7   - {{ getUserInfo.realName }}
8   - </span>
  5 + <span :class="`${prefixCls}__info hidden md:block truncate`">
  6 + <Tooltip :trigger="['click']" :title="getUserInfo.realName">
  7 + <span :class="`${prefixCls}__name `" class="truncate">
  8 + {{ getUserInfo.realName?.slice(0, 10) }}
  9 + </span>
  10 + </Tooltip>
9 11 </span>
10 12 </span>
11 13
... ... @@ -53,7 +55,7 @@
53 55 </template>
54 56 <script lang="ts">
55 57 // components
56   - import { Dropdown, Menu } from 'ant-design-vue';
  58 + import { Dropdown, Menu, Tooltip } from 'ant-design-vue';
57 59 import { defineComponent, computed, ref, reactive } from 'vue';
58 60 import { useUserStore } from '/@/store/modules/user';
59 61 import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
... ... @@ -83,6 +85,7 @@
83 85 LockAction: createAsyncComponent(() => import('../lock/LockModal.vue')),
84 86 PersonalChild: createAsyncComponent(() => import('../personal/index.vue')),
85 87 AboutSoftwareModal,
  88 + Tooltip,
86 89 },
87 90 props: {
88 91 theme: propTypes.oneOf(['dark', 'light']),
... ...
... ... @@ -28,6 +28,7 @@ import { RouteRecordRaw } from 'vue-router';
28 28 import { PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic';
29 29 import { createLocalStorage } from '/@/utils/cache/index';
30 30 import { getEntitiesId } from '/@/api/dashboard/index';
  31 +import { useRole } from '/@/hooks/business/useRole';
31 32
32 33 interface PlatInfoType {
33 34 id: string;
... ... @@ -283,7 +284,10 @@ export const useUserStore = defineStore({
283 284 title: t('sys.app.logoutTip'),
284 285 content: t('sys.app.logoutMessage'),
285 286 onOk: async () => {
286   - await logoutApi(null, 'modal'); //新增退出登录接口
  287 + const { isPlatformAdmin } = useRole();
  288 + if (!isPlatformAdmin.value) {
  289 + await logoutApi(null, 'modal'); //新增退出登录接口
  290 + }
287 291 await this.logout(true);
288 292 },
289 293 });
... ...
... ... @@ -173,7 +173,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
173 173 // authentication schemes,e.g: Bearer
174 174 // authenticationScheme: 'Bearer',
175 175 authenticationScheme: 'Bearer',
176   - timeout: 10 * 1000,
  176 + timeout: 26 * 1000,
177 177 // 基础接口地址
178 178 // baseURL: globSetting.apiUrl,
179 179 // 接口可能会有通用的地址部分,可以统一抽取出来
... ...
... ... @@ -89,3 +89,18 @@ export const withInstall = <T>(component: T, alias?: string) => {
89 89 };
90 90 return component as T & Plugin;
91 91 };
  92 +
  93 +// 字节单位转换
  94 +export const formatSizeUnits = (bytes) => {
  95 + if (bytes < 100) {
  96 + return bytes;
  97 + } else if (bytes < 1024) {
  98 + return bytes + 'bytes';
  99 + } else if (bytes < 1048576) {
  100 + return (bytes / 1024).toFixed(2) + 'KB';
  101 + } else if (bytes < 1073741824) {
  102 + return (bytes / 1048576).toFixed(2) + 'MB';
  103 + } else {
  104 + return (bytes / 1073741824).toFixed(2) + 'GB';
  105 + }
  106 +};
... ...
... ... @@ -7,10 +7,10 @@ export const createOrganizationSearch = () => {
7 7 if (!value) {
8 8 return true;
9 9 }
10   - let { name } = props || {};
  10 + let { title } = props || {};
11 11 value = value?.toLowerCase();
12   - name = name?.toLowerCase();
13   - return name.includes(value);
  12 + title = title?.toLowerCase();
  13 + return title.includes(value);
14 14 },
15 15 };
16 16 };
... ...
... ... @@ -247,11 +247,11 @@ export const alarmSchemasForm: FormSchema[] = [
247 247 disabled: true,
248 248 },
249 249 },
250   - {
251   - field: 'details',
252   - label: '详情',
253   - component: 'InputTextArea',
254   - },
  250 + // {
  251 + // field: 'details',
  252 + // label: '详情',
  253 + // component: 'InputTextArea',
  254 + // },
255 255 ];
256 256
257 257 export function getAlarmStatus({
... ...
... ... @@ -51,6 +51,7 @@
51 51 import { useDrawer } from '/@/components/Drawer';
52 52 import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
53 53 import { buildUUID } from '/@/utils/uuid';
  54 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
54 55
55 56 export default defineComponent({
56 57 name: 'ContactDrawer',
... ... @@ -161,6 +162,11 @@
161 162 } else {
162 163 delete values.id;
163 164 }
  165 +
  166 + if (Reflect.has(values, 'deleteUrl') && values.deleteUrl) {
  167 + await deleteFilePath(values?.deleteUrl);
  168 + Reflect.deleteProperty(values, 'deleteUrl');
  169 + }
164 170 let saveMessage = '添加成功';
165 171 let updateMessage = '修改成功';
166 172
... ...
... ... @@ -100,7 +100,6 @@
100 100 AccessMode,
101 101 PageMode,
102 102 CameraPermission,
103   - VideoPlatformEnum,
104 103 accessModeConfig,
105 104 getPlayUrl,
106 105 } from './config.data';
... ... @@ -199,14 +198,12 @@
199 198 const handleViewVideo = (record: CameraRecord) => {
200 199 const { videoPlatformDTO, params } = record;
201 200 const { type } = videoPlatformDTO || {};
202   -
203 201 openModal(true, {
204 202 record: {
205 203 id: record.id,
206   - canControl:
207   - [AccessMode.Streaming, AccessMode.GBT28181].includes(record.accessMode) &&
208   - type !== VideoPlatformEnum.FLUORITE,
  204 + canControl: [AccessMode.Streaming, AccessMode.GBT28181].includes(record.accessMode),
209 205 isGBT: record.accessMode === AccessMode.GBT28181,
  206 + videoPlatformType: type,
210 207 channelId: params?.channelNo,
211 208 tbDeviceId: params?.deviceId,
212 209 getPlayUrl: async () => {
... ...
... ... @@ -92,10 +92,11 @@
92 92 const { getResult } = useFingerprint();
93 93 const beforeVideoPlay = async (record: CameraRecordItem) => {
94 94 if (isNullOrUnDef(record.accessMode)) return;
95   - const { url, type } = await getPlayUrl(record);
  95 + const { url, type, withToken } = (await getPlayUrl(record)) || {};
96 96 record.playSourceUrl = url;
97 97 record.streamType = type;
98 98 record.isTransform = true;
  99 + record.withToken = withToken;
99 100 };
100 101
101 102 const gridLayout = ref({ gutter: 1, column: 2 });
... ...
... ... @@ -192,7 +192,7 @@ export const formSchema: QFormSchema[] = [
192 192 component: 'ApiUpload',
193 193 changeEvent: 'update:fileList',
194 194 valueField: 'fileList',
195   - componentProps: () => {
  195 + componentProps: ({ formModel }) => {
196 196 return {
197 197 listType: 'picture-card',
198 198 maxFileLimit: 1,
... ... @@ -214,10 +214,19 @@ export const formSchema: QFormSchema[] = [
214 214 onPreview: (fileList: FileItem) => {
215 215 createImgPreview({ imageList: [fileList.url!] });
216 216 },
  217 + onDelete(url: string) {
  218 + formModel.deleteUrl = url!;
  219 + },
217 220 };
218 221 },
219 222 },
220 223 {
  224 + field: 'deleteUrl',
  225 + label: '',
  226 + component: 'Input',
  227 + show: false,
  228 + },
  229 + {
221 230 field: 'name',
222 231 label: '视频名字',
223 232 required: true,
... ... @@ -623,9 +632,7 @@ export const formGBTSchema: QFormSchema[] = [
623 632 },
624 633 ];
625 634
626   -export async function getPlayUrl(
627   - params: CameraRecord
628   -): Promise<{ url: string; type: PlayerStreamType }> {
  635 +export async function getPlayUrl(params: CameraRecord) {
629 636 const { accessMode } = params;
630 637 if (accessMode === AccessMode.ManuallyEnter) {
631 638 const { videoUrl } = params;
... ... @@ -635,7 +642,11 @@ export async function getPlayUrl(
635 642 if (isRTSPPlay) {
636 643 const { getResult } = useFingerprint();
637 644 const fingerprint = await getResult();
638   - return { url: getFlvPlayUrl(videoUrl, fingerprint.visitorId), type: 'flv' };
  645 + return {
  646 + url: getFlvPlayUrl(videoUrl, fingerprint.visitorId),
  647 + type: 'flv',
  648 + withToken: true,
  649 + };
639 650 } else {
640 651 return { url: videoUrl, type: 'auto' };
641 652 }
... ...
... ... @@ -52,6 +52,7 @@
52 52 import { createPickerSearch } from '/@/utils/pickerSearch';
53 53 import type { queryPageParams } from '/@/api/configuration/center/model/configurationCenterModal';
54 54 import { getDeviceProfile } from '/@/api/alarm/position';
  55 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
55 56
56 57 export default defineComponent({
57 58 name: 'ConfigurationDrawer',
... ... @@ -263,6 +264,11 @@
263 264 values.thumbnail = file.url || null;
264 265 }
265 266 setDrawerProps({ confirmLoading: true });
  267 +
  268 + if (Reflect.has(values, 'deleteUrl') && values.deleteUrl) {
  269 + await deleteFilePath(values?.deleteUrl);
  270 + Reflect.deleteProperty(values, 'deleteUrl');
  271 + }
266 272 let saveMessage = '添加成功';
267 273 let updateMessage = '修改成功';
268 274 values.defaultContent = getDefaultContent(values.platform);
... ...
... ... @@ -6,6 +6,7 @@ import { useComponentRegister } from '/@/components/Form';
6 6 import { OrgTreeSelect } from '../../common/OrgTreeSelect';
7 7 import { getDeviceProfile } from '/@/api/alarm/position';
8 8 import { buildUUID } from '/@/utils/uuid';
  9 +import { createPickerSearch } from '/@/utils/pickerSearch';
9 10
10 11 useComponentRegister('OrgTreeSelect', OrgTreeSelect);
11 12 export enum Platform {
... ... @@ -90,7 +91,7 @@ export const formSchema: FormSchema[] = [
90 91 component: 'ApiUpload',
91 92 changeEvent: 'update:fileList',
92 93 valueField: 'fileList',
93   - componentProps: () => {
  94 + componentProps: ({ formModel }) => {
94 95 return {
95 96 listType: 'picture-card',
96 97 maxFileLimit: 1,
... ... @@ -112,10 +113,19 @@ export const formSchema: FormSchema[] = [
112 113 onPreview: (fileList: FileItem) => {
113 114 createImgPreview({ imageList: [fileList.url!] });
114 115 },
  116 + onDelete(url: string) {
  117 + formModel.deleteUrl = url!;
  118 + },
115 119 };
116 120 },
117 121 },
118 122 {
  123 + field: 'deleteUrl',
  124 + label: '',
  125 + component: 'Input',
  126 + show: false,
  127 + },
  128 + {
119 129 field: 'name',
120 130 label: '组态名称',
121 131 required: true,
... ... @@ -210,14 +220,7 @@ export const formSchema: FormSchema[] = [
210 220 labelField: 'name',
211 221 valueField: 'id',
212 222 placeholder: '请选择产品',
213   - getPopupContainer: (triggerNode) => triggerNode.parentNode,
214   - filterOption: (inputValue: string, option: Record<'label' | 'value', string>) => {
215   - let { label, value } = option;
216   - label = label.toLowerCase();
217   - value = value.toLowerCase();
218   - inputValue = inputValue.toLowerCase();
219   - return label.includes(inputValue) || value.includes(inputValue);
220   - },
  223 + ...createPickerSearch(),
221 224 },
222 225 ifShow: ({ values }) => values['enableTemplate'] === 0,
223 226 },
... ...
... ... @@ -21,6 +21,7 @@
21 21 import { buildUUID } from '/@/utils/uuid';
22 22 import { getDeviceProfile } from '/@/api/alarm/position';
23 23 import { PC_DEFAULT_CONTENT, PHONE_DEFAULT_CONTENT, Platform } from '../center/center.data';
  24 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
24 25
25 26 export default defineComponent({
26 27 name: 'ConfigurationDrawer',
... ... @@ -91,6 +92,10 @@
91 92 const file = (values.thumbnail || []).at(0) || {};
92 93 values.thumbnail = file.url || null;
93 94 }
  95 + if (Reflect.has(values, 'deleteUrl') && values.deleteUrl) {
  96 + await deleteFilePath(values?.deleteUrl);
  97 + Reflect.deleteProperty(values, 'deleteUrl');
  98 + }
94 99 setDrawerProps({ confirmLoading: true });
95 100 let saveMessage = '添加成功';
96 101 let updateMessage = '修改成功';
... ...
... ... @@ -6,6 +6,7 @@ import { useComponentRegister } from '/@/components/Form';
6 6 import { OrgTreeSelect } from '../../common/OrgTreeSelect';
7 7 import { getDeviceProfile } from '/@/api/alarm/position';
8 8 import { Platform } from '../center/center.data';
  9 +import { createPickerSearch } from '/@/utils/pickerSearch';
9 10
10 11 useComponentRegister('OrgTreeSelect', OrgTreeSelect);
11 12
... ... @@ -79,7 +80,7 @@ export const formSchema: FormSchema[] = [
79 80 component: 'ApiUpload',
80 81 changeEvent: 'update:fileList',
81 82 valueField: 'fileList',
82   - componentProps: () => {
  83 + componentProps: ({ formModel }) => {
83 84 return {
84 85 listType: 'picture-card',
85 86 maxFileLimit: 1,
... ... @@ -101,9 +102,18 @@ export const formSchema: FormSchema[] = [
101 102 onPreview: (fileList: FileItem) => {
102 103 createImgPreview({ imageList: [fileList.url!] });
103 104 },
  105 + onDelete(url: string) {
  106 + formModel.deleteUrl = url!;
  107 + },
104 108 };
105 109 },
106 110 },
  111 + {
  112 + field: 'deleteUrl',
  113 + label: '',
  114 + component: 'Input',
  115 + show: false,
  116 + },
107 117
108 118 {
109 119 field: 'name',
... ... @@ -131,6 +141,7 @@ export const formSchema: FormSchema[] = [
131 141 mode: 'multiple',
132 142 labelField: 'name',
133 143 valueField: 'id',
  144 + ...createPickerSearch(),
134 145 },
135 146 },
136 147 {
... ...
... ... @@ -19,6 +19,7 @@
19 19 import { saveOrUpdateBigScreenCenter } from '/@/api/bigscreen/center/bigscreenCenter';
20 20 import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
21 21 import { buildUUID } from '/@/utils/uuid';
  22 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
22 23
23 24 export default defineComponent({
24 25 name: 'BigScreenDrawer',
... ... @@ -67,6 +68,10 @@
67 68 const file = (values.thumbnail || []).at(0) || {};
68 69 values.thumbnail = file.url || null;
69 70 }
  71 + if (Reflect.has(values, 'deleteUrl') && values.deleteUrl) {
  72 + await deleteFilePath(values?.deleteUrl);
  73 + Reflect.deleteProperty(values, 'deleteUrl');
  74 + }
70 75 setDrawerProps({ confirmLoading: true });
71 76 let saveMessage = '添加成功';
72 77 let updateMessage = '修改成功';
... ...
... ... @@ -56,7 +56,7 @@ export const formSchema: FormSchema[] = [
56 56 component: 'ApiUpload',
57 57 changeEvent: 'update:fileList',
58 58 valueField: 'fileList',
59   - componentProps: () => {
  59 + componentProps: ({ formModel }) => {
60 60 return {
61 61 listType: 'picture-card',
62 62 maxFileLimit: 1,
... ... @@ -80,9 +80,18 @@ export const formSchema: FormSchema[] = [
80 80 onPreview: (fileList: FileItem) => {
81 81 createImgPreview({ imageList: [fileList.url!] });
82 82 },
  83 + onDelete(url: string) {
  84 + formModel.deleteUrl = url!;
  85 + },
83 86 };
84 87 },
85 88 },
  89 + {
  90 + field: 'deleteUrl',
  91 + label: '',
  92 + component: 'Input',
  93 + show: false,
  94 + },
86 95
87 96 {
88 97 field: 'name',
... ...
... ... @@ -48,7 +48,10 @@
48 48 rowSelection: {
49 49 type: 'checkbox',
50 50 getCheckboxProps: (record: any) => {
51   - return { disabled: !!record.status };
  51 + return {
  52 + disabled:
  53 + getAuthCache(USER_INFO_KEY).tenantId === record.tenantId ? !!record.status : true,
  54 + };
52 55 },
53 56 },
54 57 });
... ... @@ -86,12 +89,14 @@
86 89
87 90 // 详情
88 91 const registerDetailRecord = ref<any>({});
  92 + const isCurrentTenant = ref<Boolean>(false);
89 93 const handleDetail = (record?: any) => {
90 94 openDrawer(true);
91 95 registerDetailRecord.value = {
92 96 ...record,
93 97 ifShowClass: true,
94 98 };
  99 + isCurrentTenant.value = getAuthCache(USER_INFO_KEY).tenantId === record.tenantId;
95 100 };
96 101
97 102 // 状态->编辑
... ... @@ -99,7 +104,7 @@
99 104 switchLoading.value = true;
100 105 await deviceProfileCategory({ ...record, status: e });
101 106 switchLoading.value = false;
102   - createMessage.success('操作成功');
  107 + createMessage.success(`${!e ? '禁用' : '启用'}成功`);
103 108 handleReload();
104 109 };
105 110
... ... @@ -145,6 +150,7 @@
145 150 <Switch
146 151 @click="(e) => handleSwitch(e, record)"
147 152 :loading="switchLoading"
  153 + :disabled="getAuthCache(USER_INFO_KEY).tenantId !== record.tenantId"
148 154 v-model:checked="record.status"
149 155 checked-children="启用"
150 156 un-checked-children="禁用"
... ... @@ -165,14 +171,17 @@
165 171 label: '编辑',
166 172 auth: 'api:yt:product:category:update',
167 173 icon: 'clarity:note-edit-line',
168   - ifShow: authBtn(role),
  174 + ifShow: authBtn(role) && getAuthCache(USER_INFO_KEY).tenantId === record.tenantId,
169 175 onClick: handleEdit.bind(null, record),
170 176 },
171 177 {
172 178 label: '删除',
173 179 auth: 'api:yt:product:category:delete',
174 180 icon: 'ant-design:delete-outlined',
175   - ifShow: authBtn(role) && !record.status,
  181 + ifShow:
  182 + authBtn(role) &&
  183 + !record.status &&
  184 + getAuthCache(USER_INFO_KEY).tenantId === record.tenantId,
176 185 color: 'error',
177 186 popConfirm: {
178 187 title: '是否确认删除',
... ... @@ -185,7 +194,10 @@
185 194 </BasicTable>
186 195 <classModal @register="registerModal" @handleReload="handleReload" />
187 196 <BasicDrawer title="物模型" @register="registerDetailDrawer" width="60%" destroy-on-close>
188   - <PhysicalModelManagementStep :record="registerDetailRecord" />
  197 + <PhysicalModelManagementStep
  198 + :isCurrentTenant="isCurrentTenant"
  199 + :record="registerDetailRecord"
  200 + />
189 201 </BasicDrawer>
190 202 </div>
191 203 </template>
... ...
... ... @@ -49,7 +49,7 @@ export const step1Schemas: FormSchema[] = [
49 49 component: 'ApiUpload',
50 50 changeEvent: 'update:fileList',
51 51 valueField: 'fileList',
52   - componentProps: () => {
  52 + componentProps: ({ formModel }) => {
53 53 return {
54 54 listType: 'picture-card',
55 55 maxFileLimit: 1,
... ... @@ -71,10 +71,19 @@ export const step1Schemas: FormSchema[] = [
71 71 onPreview: (fileList: FileItem) => {
72 72 createImgPreview({ imageList: [fileList.url!] });
73 73 },
  74 + onDelete(url: string) {
  75 + formModel.deleteUrl = url!;
  76 + },
74 77 };
75 78 },
76 79 },
77 80 {
  81 + field: 'deleteUrl',
  82 + label: '',
  83 + component: 'Input',
  84 + show: false,
  85 + },
  86 + {
78 87 field: 'alias',
79 88 label: '别名 ',
80 89 component: 'Input',
... ... @@ -193,8 +202,29 @@ export const step1Schemas: FormSchema[] = [
193 202 },
194 203 },
195 204 {
  205 + field: 'addressType',
  206 + label: '',
  207 + component: 'Input',
  208 + show: false,
  209 + defaultValue: 'HEX',
  210 + },
  211 + {
  212 + field: 'deviceState',
  213 + label: '',
  214 + component: 'Input',
  215 + show: false,
  216 + },
  217 + {
196 218 field: 'addressCode',
197 219 label: '地址码',
  220 + dynamicDisabled({ values }) {
  221 + return (
  222 + values.isUpdate &&
  223 + values.deviceType === DeviceTypeEnum.SENSOR &&
  224 + values.deviceState === 'ONLINE' &&
  225 + values.transportType === TransportTypeEnum.TCP
  226 + );
  227 + },
198 228 dynamicRules({ values }) {
199 229 return [
200 230 {
... ... @@ -202,18 +232,31 @@ export const step1Schemas: FormSchema[] = [
202 232 values?.transportType === TransportTypeEnum.TCP &&
203 233 values?.tcpDeviceProtocol === TCPProtocolTypeEnum.MODBUS_RTU,
204 234 message: '地址码范围为00~FF',
205   - pattern: /^[0-9A-Fa-f]{1,2}$/,
  235 + pattern: values?.addressType === 'HEX' ? /^[0-9A-Fa-f]{2}$/ : /^[0-9A-Fa-f]{1,2}$/,
206 236 },
207 237 ];
208 238 },
209   - helpMessage: ['地址码范围为00~FF'],
  239 + helpMessage({ values }) {
  240 + return [
  241 + '地址码范围为00~FF',
  242 + values.transportType === TransportTypeEnum.TCP &&
  243 + values.deviceType === DeviceTypeEnum.SENSOR
  244 + ? 'tcp网关子设备在线时,不能修改设备标识或地址码'
  245 + : '',
  246 + ];
  247 + },
210 248 component: 'HexInput',
211 249 changeEvent: 'update:value',
212 250 valueField: 'value',
213   - componentProps: {
214   - type: InputTypeEnum.HEX,
215   - maxValue: parseInt('FF', 16),
216   - placeholder: '请输入寄存器地址',
  251 + componentProps: ({ formModel }) => {
  252 + return {
  253 + type: InputTypeEnum.HEX,
  254 + maxValue: parseInt('FF', 16),
  255 + placeholder: '请输入寄存器地址',
  256 + onHexChange: (e) => {
  257 + formModel.addressType = e;
  258 + },
  259 + };
217 260 },
218 261 ifShow: ({ values }) => {
219 262 return (
... ... @@ -235,6 +278,22 @@ export const step1Schemas: FormSchema[] = [
235 278 },
236 279 ];
237 280 },
  281 + dynamicDisabled({ values }) {
  282 + return (
  283 + values.isUpdate &&
  284 + values.deviceType === DeviceTypeEnum.SENSOR &&
  285 + values.deviceState === 'ONLINE' &&
  286 + values.transportType === TransportTypeEnum.TCP
  287 + );
  288 + },
  289 + helpMessage({ values }) {
  290 + return (
  291 + values.transportType === TransportTypeEnum.TCP &&
  292 + values.deviceType === DeviceTypeEnum.SENSOR
  293 + ? ['tcp网关子设备在线时,不能修改设备标识或地址码']
  294 + : false
  295 + ) as any;
  296 + },
238 297 component: 'Input',
239 298 componentProps: () => {
240 299 return {
... ... @@ -276,7 +335,7 @@ export const step1Schemas: FormSchema[] = [
276 335 component: 'ApiSelect',
277 336 ifShow: ({ values }) => values.deviceType === 'SENSOR',
278 337 componentProps: ({ formModel, formActionType }) => {
279   - const { transportType } = formModel;
  338 + const { transportType, deviceType, gatewayId } = formModel;
280 339 const { setFieldsValue } = formActionType;
281 340 if (!transportType) return {};
282 341 return {
... ... @@ -291,6 +350,7 @@ export const step1Schemas: FormSchema[] = [
291 350 showSearch: true,
292 351 params: {
293 352 transportType,
  353 + gatewayId: deviceType === DeviceTypeEnum.SENSOR && gatewayId ? gatewayId : null,
294 354 },
295 355 placeholder: '请选择网关设备',
296 356 valueField: 'tbDeviceId',
... ...
... ... @@ -6,6 +6,7 @@ import { deviceProfile } from '/@/api/device/deviceManager';
6 6 import { h } from 'vue';
7 7 import { Tag, Tooltip } from 'ant-design-vue';
8 8 import { handeleCopy } from '../../profiles/step/topic';
  9 +import edgefornt from '/@/assets/icons/edgefornt.svg';
9 10
10 11 export enum DeviceListAuthEnum {
11 12 /**
... ... @@ -84,8 +85,9 @@ export const columns: BasicColumn[] = [
84 85 title: '别名/设备名称',
85 86 width: 210,
86 87 slots: { customRender: 'name', title: 'deviceTitle' },
  88 + className: 'device-name-edge',
87 89 customRender: ({ record }) => {
88   - return h('div', { style: 'display:flex;flex-direction:column' }, [
  90 + return h('div', { class: 'py-3 px-3.5' }, [
89 91 record.alias &&
90 92 h(
91 93 'div',
... ... @@ -118,6 +120,19 @@ export const columns: BasicColumn[] = [
118 120 () => `${record.name}`
119 121 )
120 122 ),
  123 + record.isEdge
  124 + ? h('div', { class: 'absolute top-0 left-0', fill: '#1890ff' }, [
  125 + h('img', { src: edgefornt, class: 'w-12.5 h-12.5' }),
  126 + h(
  127 + 'span',
  128 + {
  129 + class:
  130 + 'absolute top-0.5 left-0.5 text-light-50 transform -rotate-45 translate-y-0 !text-10px',
  131 + },
  132 + '边'
  133 + ),
  134 + ])
  135 + : '',
121 136 ]);
122 137 },
123 138 },
... ... @@ -162,7 +177,9 @@ export const columns: BasicColumn[] = [
162 177 {
163 178 title: '最后断开时间',
164 179 dataIndex: 'lastOfflineTime',
165   - format: (text) => text && formatToDate(text, 'YYYY-MM-DD HH:mm:ss'),
  180 + format: (text) => {
  181 + return text ? formatToDate(text, 'YYYY-MM-DD HH:mm:ss') : '';
  182 + },
166 183 width: 160,
167 184 },
168 185 ];
... ...
... ... @@ -41,6 +41,7 @@
41 41 import { Steps } from 'ant-design-vue';
42 42 import { useMessage } from '/@/hooks/web/useMessage';
43 43 import { credentialTypeEnum } from '../../config/data';
  44 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
44 45
45 46 export default defineComponent({
46 47 name: 'DeviceModal',
... ... @@ -161,8 +162,13 @@
161 162 avatar: stepRecord?.icon,
162 163 ...DeviceStep1Ref.value?.devicePositionState,
163 164 },
  165 + alarmStatus: currentDeviceData?.alarmStatus,
164 166 };
165 167 validateNameLength(stepRecord.name);
  168 + if (Reflect.has(editData, 'deleteUrl')) {
  169 + await deleteFilePath(editData?.deleteUrl);
  170 + Reflect.deleteProperty(editData, 'deleteUrl');
  171 + }
166 172 await createOrEditDevice(editData);
167 173 } else {
168 174 const stepRecord = unref(stepState);
... ...
... ... @@ -401,16 +401,16 @@
401 401 // 父组件调用更新字段值的方法
402 402 function parentSetFieldsValue(data) {
403 403 const { deviceInfo = {} } = data;
404   - positionState.longitude = deviceInfo.longitude;
405   - positionState.latitude = deviceInfo.latitude;
406   - positionState.address = deviceInfo.address;
  404 + positionState.longitude = deviceInfo?.longitude;
  405 + positionState.latitude = deviceInfo?.latitude;
  406 + positionState.address = deviceInfo?.address;
407 407 devicePositionState.value = { ...toRaw(positionState) };
408   - devicePic.value = deviceInfo.avatar;
  408 + devicePic.value = deviceInfo?.avatar;
409 409
410   - if (deviceInfo.avatar) {
  410 + if (deviceInfo?.avatar) {
411 411 setFieldsValue({
412 412 isUpdate: unref(isUpdate1),
413   - icon: [{ uid: buildUUID(), name: 'name', url: deviceInfo.avatar } as FileItem],
  413 + icon: [{ uid: buildUUID(), name: 'name', url: deviceInfo?.avatar } as FileItem],
414 414 });
415 415 }
416 416 setFieldsValue({
... ...
... ... @@ -119,6 +119,7 @@
119 119 ],
120 120 };
121 121 });
  122 + const cacheSearchValue = ref('');
122 123
123 124 const [registerForm, { getFieldsValue }] = useForm({
124 125 schemas: [
... ... @@ -141,6 +142,8 @@
141 142
142 143 pagination.current = 1;
143 144
  145 + cacheSearchValue.value = value;
  146 +
144 147 socketInfo.filterAttrKeys = value
145 148 ? unref(socketInfo.rawDataSource)
146 149 .filter(
... ... @@ -161,6 +164,7 @@
161 164 resetFunc: async () => {
162 165 try {
163 166 socketInfo.filterAttrKeys = [];
  167 + cacheSearchValue.value = '';
164 168 handleFilterChange();
165 169 unref(mode) === EnumTableCardMode.TABLE && setTableModeData();
166 170 } catch (error) {}
... ... @@ -201,8 +205,15 @@
201 205 const { createMessage } = useMessage();
202 206
203 207 const setDataSource = () => {
  208 + const filterValueByCacheSearchValue = socketInfo.rawDataSource.filter(
  209 + (item) =>
  210 + item.key?.toUpperCase().includes(cacheSearchValue.value.toUpperCase()) ||
  211 + item.name?.toUpperCase().includes(cacheSearchValue.value.toUpperCase())
  212 + );
204 213 socketInfo.dataSource = socketInfo.filterAttrKeys.length
205 214 ? socketInfo.rawDataSource.filter((item) => socketInfo.filterAttrKeys.includes(item.key))
  215 + : filterValueByCacheSearchValue.length === 0
  216 + ? []
206 217 : socketInfo.rawDataSource;
207 218 };
208 219
... ...
1 1 import { h } from 'vue';
2 2 import { BasicColumn, FormSchema } from '/@/components/Table';
3 3 import { Tag } from 'ant-design-vue';
  4 +import { VideoPlatformEnum } from '/@/views/camera/manage/config.data';
4 5 export type VideoCancelModalParamsType = {
5 6 canControl?: boolean;
  7 + videoPlatformType: VideoPlatformEnum;
6 8 isGBT?: boolean;
7 9 tbDeviceId?: string;
8 10 channelId?: string;
... ... @@ -101,3 +103,25 @@ export const searchFormSchema: FormSchema[] | any = [
101 103 },
102 104 },
103 105 ];
  106 +
  107 +export enum VideoControlEnum {
  108 + Up = 'UP',
  109 + Right = 'RIGHT',
  110 + Left = 'LEFT',
  111 + Down = 'DOWN',
  112 + ZoomIn = 'ZOOM_IN',
  113 + ZoomOut = 'ZOOM_OUT',
  114 +}
  115 +
  116 +export enum EzvizVideoControlEnum {
  117 + UP,
  118 + DOWN,
  119 + LEFT,
  120 + RIGHT,
  121 + LEFT_UP,
  122 + LEFT_DOWN,
  123 + RIGHT_UP,
  124 + RIGHT_DOWN,
  125 + ZOOM_IN,
  126 + ZOOM_OUT,
  127 +}
... ...
... ... @@ -13,20 +13,14 @@
13 13 } from '@ant-design/icons-vue';
14 14 import { Button, Slider } from 'ant-design-vue';
15 15 import { controlling } from '/@/api/camera/cameraManager';
16   - import { setVideoControl } from '/@/api/device/videoChannel';
  16 + import { setEzvizControl, setVideoControl } from '/@/api/device/videoChannel';
17 17 import XGPlayer from '/@/components/Video/src/XGPlayer.vue';
18   - import PresetPlayer from 'xgplayer';
  18 + import { EzvizVideoControlEnum, VideoCancelModalParamsType, VideoControlEnum } from './config';
  19 + import { VideoPlatformEnum } from '/@/views/camera/manage/config.data';
19 20
20 21 const props = defineProps<{
21 22 playUrl?: string;
22   - options?: {
23   - canControl?: boolean;
24   - isGBT?: boolean;
25   - tbDeviceId?: string;
26   - channelId?: string;
27   - id?: string;
28   - playerProps?: Recordable;
29   - };
  23 + options?: VideoCancelModalParamsType;
30 24 }>();
31 25
32 26 const playerRef = ref<InstanceType<typeof XGPlayer>>();
... ... @@ -64,12 +58,24 @@
64 58 });
65 59 };
66 60
  61 + const handleEzvizControl = (actionType: string, start: boolean) => {
  62 + const { id } = props.options || {};
  63 + const action = EzvizVideoControlEnum[actionType as VideoControlEnum];
  64 + setEzvizControl({ entityId: id!, action, controllingType: Number(!start) });
  65 + };
  66 +
67 67 //长按开始
68 68 const moveStart = (action: string) => {
69 69 if (unref(props.options?.isGBT)) {
70 70 handleGBTControl(action, unref(sliderValue));
71 71 return;
72 72 }
  73 +
  74 + if (props.options?.videoPlatformType === VideoPlatformEnum.FLUORITE) {
  75 + handleEzvizControl(action, true);
  76 + return;
  77 + }
  78 +
73 79 handleControl(0, action);
74 80 };
75 81
... ... @@ -79,6 +85,14 @@
79 85 handleGBTControl('STOP', unref(sliderValue));
80 86 return;
81 87 }
  88 +
  89 + if (props.options?.videoPlatformType === VideoPlatformEnum.FLUORITE) {
  90 + setTimeout(() => {
  91 + handleEzvizControl(action, false);
  92 + }, 1000);
  93 + return;
  94 + }
  95 +
82 96 handleControl(1, action);
83 97 };
84 98
... ... @@ -122,15 +136,15 @@
122 136 <div>
123 137 <Button
124 138 class="left-top in-block"
125   - @mousedown="moveStart('UP')"
126   - @mouseup="moveStop('UP')"
  139 + @mousedown="moveStart(VideoControlEnum.Up)"
  140 + @mouseup="moveStop(VideoControlEnum.Up)"
127 141 >
128 142 <CaretUpOutlined class="icon-rotate child-icon" />
129 143 </Button>
130 144 <Button
131 145 class="right-top in-block"
132   - @mousedown="moveStart('RIGHT')"
133   - @mouseup="moveStop('RIGHT')"
  146 + @mousedown="moveStart(VideoControlEnum.Right)"
  147 + @mouseup="moveStop(VideoControlEnum.Right)"
134 148 >
135 149 <CaretRightOutlined class="icon-rotate child-icon" />
136 150 </Button>
... ... @@ -138,15 +152,15 @@
138 152 <div>
139 153 <Button
140 154 class="left-bottom in-block"
141   - @mousedown="moveStart('LEFT')"
142   - @mouseup="moveStop('LEFT')"
  155 + @mousedown="moveStart(VideoControlEnum.Left)"
  156 + @mouseup="moveStop(VideoControlEnum.Left)"
143 157 >
144 158 <CaretLeftOutlined class="icon-rotate child-icon" />
145 159 </Button>
146 160 <Button
147 161 class="right-bottom in-block"
148   - @mousedown="moveStart('DOWN')"
149   - @mouseup="moveStop('DOWN')"
  162 + @mousedown="moveStart(VideoControlEnum.Down)"
  163 + @mouseup="moveStop(VideoControlEnum.Down)"
150 164 >
151 165 <CaretDownOutlined class="icon-rotate child-icon" />
152 166 </Button>
... ... @@ -161,16 +175,16 @@
161 175 <div class="flex justify-center mt-8">
162 176 <Button
163 177 class="button-icon"
164   - @mousedown="moveStart('ZOOM_IN')"
165   - @mouseup="moveStop('ZOOM_IN')"
  178 + @mousedown="moveStart(VideoControlEnum.ZoomIn)"
  179 + @mouseup="moveStop(VideoControlEnum.ZoomIn)"
166 180 style="border-radius: 50%"
167 181 >
168 182 <ZoomInOutlined style="color: #315a9c; font-size: 1.5rem" />
169 183 </Button>
170 184 <Button
171 185 class="ml-10 button-icon"
172   - @mousedown="moveStart('ZOOM_OUT')"
173   - @mouseup="moveStop('ZOOM_OUT')"
  186 + @mousedown="moveStart(VideoControlEnum.ZoomOut)"
  187 + @mouseup="moveStop(VideoControlEnum.ZoomOut)"
174 188 style="border-radius: 50%"
175 189 >
176 190 <ZoomOutOutlined style="color: #315a9c; font-size: 1.5rem" />
... ...
... ... @@ -31,7 +31,6 @@
31 31 options.value = null;
32 32 setModalProps({ loading: true, loadingTip: '视频加载中...' });
33 33 const { url, type } = await record.getPlayUrl();
34   -
35 34 playUrl.value = url;
36 35 options.value = record;
37 36 options.value.playerProps = {
... ...
... ... @@ -147,6 +147,8 @@
147 147 ? 'warning'
148 148 : record.deviceState == DeviceState.ONLINE
149 149 ? 'success'
  150 + : record.deviceState == DeviceState.ACTIVE
  151 + ? 'success'
150 152 : 'error'
151 153 "
152 154 class="ml-2"
... ... @@ -156,6 +158,8 @@
156 158 ? '待激活'
157 159 : record.deviceState == DeviceState.ONLINE
158 160 ? '在线'
  161 + : record.deviceState == DeviceState.ACTIVE
  162 + ? '活动'
159 163 : '离线'
160 164 }}
161 165 </Tag>
... ... @@ -231,7 +235,7 @@
231 235 ifShow: authBtn(role) && record.customerId === undefined,
232 236 color: 'error',
233 237 popConfirm: {
234   - title: '是否确认删除',
  238 + title: !!record.isEdge ? '此设备来自边端,请谨慎删除' : '是否确认删除',
235 239 confirm: handleDelete.bind(null, record),
236 240 },
237 241 },
... ... @@ -707,4 +711,11 @@
707 711 border-right: 30px solid transparent;
708 712 }
709 713 }
  714 +
  715 + .device-name-edge {
  716 + width: 100%;
  717 + height: 100%;
  718 + padding: 0 !important;
  719 + position: relative;
  720 + }
710 721 </style>
... ...
... ... @@ -24,6 +24,7 @@
24 24 import { BasicCardList, useCardList } from '/@/components/CardList';
25 25 import { useRoute } from 'vue-router';
26 26 import { ref } from 'vue';
  27 + import edgefornt from '/@/assets/icons/edgefornt.svg';
27 28
28 29 defineProps<{
29 30 mode: EnumTableCardMode;
... ... @@ -154,7 +155,7 @@
154 155 <template #renderItem="{ item }: BasicCardListRenderItem<ProfileRecord>">
155 156 <Card hoverable>
156 157 <template #cover>
157   - <div class="h-full w-full !flex justify-center items-center text-center p-1">
  158 + <div class="h-full w-full !flex justify-center items-center text-center p-1 relative">
158 159 <Image
159 160 @click.stop
160 161 wrapper-class-name="!w-32 !h-32 !flex !items-center overflow-hidden"
... ... @@ -162,6 +163,13 @@
162 163 placeholder
163 164 :fallback="IMAGE_FALLBACK"
164 165 />
  166 + <div v-if="item.isEdge" class="absolute top-0 left-0">
  167 + <img :src="edgefornt" />
  168 + <span
  169 + class="absolute top-0 left-0 text-light-50 transform -rotate-45 translate-y-1 translate-x-0.5"
  170 + >边</span
  171 + >
  172 + </div>
165 173 </div>
166 174 </template>
167 175 <template class="ant-card-actions" #actions>
... ... @@ -199,7 +207,7 @@
199 207 auth: ProductPermission.DELETE,
200 208 icon: 'ant-design:delete-outlined',
201 209 popconfirm: {
202   - title: '是否确认删除操作?',
  210 + title: !!item.isEdge ? '此产品来自边端,请谨慎删除' : '是否确认删除',
203 211 onConfirm: handleDelete.bind(null, [item.id]),
204 212 disabled: item.default || item.name == 'default',
205 213 },
... ...
... ... @@ -20,7 +20,7 @@
20 20 key="modelOfMatter"
21 21 tab="物模型管理"
22 22 >
23   - <PhysicalModelManagementStep :record="record" />
  23 + <PhysicalModelManagementStep :record="record" :isCurrentTenant="true" />
24 24 </Tabs.TabPane>
25 25 </Tabs>
26 26 </BasicDrawer>
... ...
... ... @@ -86,6 +86,7 @@
86 86 import TransportConfigurationStep from './step/TransportConfigurationStep.vue';
87 87 import PhysicalModelManagementStep from './step/PhysicalModelManagementStep.vue';
88 88 import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel';
  89 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
89 90
90 91 const emits = defineEmits(['success', 'register']);
91 92 const activeKey = ref('1');
... ... @@ -188,6 +189,10 @@
188 189 await getDeviceConfFormData();
189 190 await getTransConfData();
190 191 const isEmptyObj = isEmpty(transportConfData.profileData.transportConfiguration);
  192 + if (Reflect.has(postSubmitFormData.deviceConfData, 'deleteUrl') && values.deleteUrl.deleteUrl) {
  193 + await deleteFilePath(postSubmitFormData.deviceConfData?.deleteUrl);
  194 + Reflect.deleteProperty(postSubmitFormData.deviceConfData, 'deleteUrl');
  195 + }
191 196 await deviceConfigAddOrEdit({
192 197 ...postSubmitFormData.deviceConfData,
193 198 ...{ transportType: !isEmptyObj ? transportTypeStr.value : 'DEFAULT' },
... ...
... ... @@ -79,7 +79,7 @@
79 79 icon: 'ant-design:delete-outlined',
80 80 color: 'error',
81 81 popConfirm: {
82   - title: '是否确认删除',
  82 + title: !!record.isEdge ? '此设备来自边端,请谨慎删除' : '是否确认删除',
83 83 confirm: handleDeleteOrBatchDelete.bind(null, record),
84 84 },
85 85 ifShow: () => {
... ...
... ... @@ -23,7 +23,7 @@
23 23 }
24 24 );
25 25
26   - const emits = defineEmits(['update:value', 'change']);
  26 + const emits = defineEmits(['update:value', 'change', 'hexChange']);
27 27
28 28 const typOptions = Object.values(InputTypeEnum).map((value) => ({ label: value, value }));
29 29
... ... @@ -114,6 +114,10 @@
114 114 ? getHexToDec(unref(getInputValue)!)
115 115 : `${hexWithPrefix ? '0x' : ''}${Number(unref(getInputValue)).toString(16).toUpperCase()}`;
116 116 });
  117 +
  118 + const handleInputTypeChange = (e) => {
  119 + emits('hexChange', e);
  120 + };
117 121 </script>
118 122
119 123 <template>
... ... @@ -124,6 +128,7 @@
124 128 class="min-w-20"
125 129 :options="typOptions"
126 130 :disabled="disabled"
  131 + @change="handleInputTypeChange"
127 132 />
128 133 <Input v-bind="$attrs" v-model:value="getInputValue" class="!w-full" :disabled="disabled" />
129 134 <div class="min-w-14 h-full px-2 flex-grow flex-shrink-0 text-center leading-8 bg-gray-200">
... ...
... ... @@ -13,7 +13,13 @@
13 13 <BasicForm @register="registerForm">
14 14 <template #importType="{ model }">
15 15 <RadioGroup v-model:value="model.importType">
16   - <Authority :value="['api:yt:things_model:category:import', 'api:yt:things_model:import']">
  16 + <Authority
  17 + :value="
  18 + record?.ifShowClass
  19 + ? ['api:yt:things_model:category:import']
  20 + : ['api:yt:things_model:import']
  21 + "
  22 + >
17 23 <Radio value="2">JSON导入</Radio>
18 24 </Authority>
19 25 <Authority :value="['api:yt:things_model:excel_import']">
... ... @@ -80,10 +86,11 @@
80 86 label: '导入类型',
81 87 component: 'RadioGroup',
82 88 slot: 'importType',
83   - defaultValue: hasPermission([
84   - 'api:yt:things_model:category:import',
85   - 'api:yt:things_model:import',
86   - ])
  89 + defaultValue: (
  90 + props.record?.ifShowClass
  91 + ? hasPermission(['api:yt:things_model:category:import'])
  92 + : hasPermission(['api:yt:things_model:import'])
  93 + )
87 94 ? '2'
88 95 : '1',
89 96 helpMessage:
... ...
... ... @@ -182,7 +182,7 @@ export const step1Schemas: FormSchema[] = [
182 182 component: 'ApiUpload',
183 183 changeEvent: 'update:fileList',
184 184 valueField: 'fileList',
185   - componentProps: () => {
  185 + componentProps: ({ formModel }) => {
186 186 return {
187 187 listType: 'picture-card',
188 188 maxFileLimit: 1,
... ... @@ -204,10 +204,19 @@ export const step1Schemas: FormSchema[] = [
204 204 onPreview: (fileList: FileItem) => {
205 205 createImgPreview({ imageList: [fileList.url!] });
206 206 },
  207 + onDelete(url: string) {
  208 + formModel.deleteUrl = url!;
  209 + },
207 210 };
208 211 },
209 212 },
210 213 {
  214 + field: 'deleteUrl',
  215 + label: '',
  216 + component: 'Input',
  217 + show: false,
  218 + },
  219 + {
211 220 field: 'deviceType',
212 221 component: 'ApiRadioGroup',
213 222 label: '设备类型',
... ...
... ... @@ -8,7 +8,7 @@
8 8 >
9 9 <template #toolbar>
10 10 <div class="flex-auto">
11   - <div class="mb-2">
  11 + <div class="mb-2" v-if="isCurrentTenant">
12 12 <Alert type="info" show-icon>
13 13 <template #message>
14 14 <span v-if="!isShowBtn">
... ... @@ -39,6 +39,7 @@
39 39 <Button type="primary" @click="handleExport" :loading="loading">导出物模型</Button>
40 40 </Authority>
41 41 <Authority
  42 + v-if="isCurrentTenant"
42 43 :value="[
43 44 'api:yt:things_model:import',
44 45 'api:yt:things_model:category:import',
... ... @@ -47,12 +48,18 @@
47 48 >
48 49 <Button type="primary" @click="handleSelectImport">导入物模型</Button>
49 50 </Authority>
50   - <Button type="primary" v-if="!isShowBtn" @click="handleEditPhysicalModel"
  51 + <Button
  52 + type="primary"
  53 + v-if="!isShowBtn && isCurrentTenant"
  54 + @click="handleEditPhysicalModel"
51 55 >编辑物模型</Button
52 56 >
53 57 </div>
54 58 <div class="flex gap-2">
55   - <Authority :value="[ModelOfMatterPermission.RELEASE]">
  59 + <Authority
  60 + v-if="!props.record.ifShowClass"
  61 + :value="[ModelOfMatterPermission.RELEASE]"
  62 + >
56 63 <Popconfirm
57 64 title="是否需要发布上线?"
58 65 ok-text="确定"
... ... @@ -159,11 +166,12 @@
159 166 import { DataActionModeEnum } from '/@/enums/toolEnum';
160 167 import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel';
161 168
162   - const { isPlatformAdmin, isSysadmin } = useRole();
  169 + const { isPlatformAdmin, isSysadmin, isTenantAdmin } = useRole();
163 170 defineEmits(['register']);
164 171
165 172 const props = defineProps<{
166 173 record: DeviceProfileDetail;
  174 + isCurrentTenant: Boolean;
167 175 }>();
168 176
169 177 const { createMessage } = useMessage();
... ... @@ -224,7 +232,8 @@
224 232
225 233 const handleDeleteOrBatchDelete = async (record?: ModelOfMatterItemRecordType) => {
226 234 const deleteFn =
227   - props.record.ifShowClass && (unref(isPlatformAdmin) || unref(isSysadmin))
  235 + props.record.ifShowClass &&
  236 + (unref(isPlatformAdmin) || unref(isSysadmin) || unref(isTenantAdmin))
228 237 ? deleteModelCategory
229 238 : deleteModel;
230 239
... ... @@ -265,7 +274,10 @@
265 274 const handleSelectImport = () => {
266 275 openModalSelect(true, {
267 276 id: props.record.id,
268   - isCateGory: (unref(isPlatformAdmin) || unref(isSysadmin)) && props.record.ifShowClass,
  277 + isCateGory:
  278 + unref(isPlatformAdmin) ||
  279 + unref(isSysadmin) ||
  280 + (unref(isTenantAdmin) && props.record.ifShowClass),
269 281 });
270 282 };
271 283
... ...
... ... @@ -31,7 +31,7 @@
31 31 import { DataActionModeEnum, DataActionModeNameEnum } from '/@/enums/toolEnum';
32 32 import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel';
33 33
34   - const { isPlatformAdmin, isSysadmin } = useRole();
  34 + const { isPlatformAdmin, isSysadmin, isTenantAdmin } = useRole();
35 35
36 36 const emit = defineEmits(['register', 'success']);
37 37
... ... @@ -77,7 +77,9 @@
77 77 const value = unref(objectModelElRef)!.getFieldsValue();
78 78
79 79 const isCategoryAction =
80   - (unref(isSysadmin) || unref(isPlatformAdmin)) && props.record.ifShowClass;
  80 + unref(isSysadmin) ||
  81 + unref(isPlatformAdmin) ||
  82 + (unref(isTenantAdmin) && props.record.ifShowClass);
81 83
82 84 const submitFn =
83 85 unref(openModalMode) === DataActionModeEnum.CREATE
... ...
  1 +export { default as CardMode } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { Button, Tooltip, Card, Popconfirm } from 'ant-design-vue';
  3 + import { AuthIcon, EnumTableCardMode } from '/@/components/Widget';
  4 + import { useMessage } from '/@/hooks/web/useMessage';
  5 + import { BasicCardList, useCardList } from '/@/components/CardList';
  6 + import { useRoute } from 'vue-router';
  7 + import { ref } from 'vue';
  8 + import { HandleOperationEnum, HandleOperationNameEnum } from '../../config';
  9 + import { searchFormSchema } from '../EdgeInstance/config';
  10 + import { deleteEdgeInstance, edgeInstancePage } from '/@/api/edgeManage/edgeInstance';
  11 + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  12 + import { EdgeInstanceFormDrawer } from '../EdgeInstance';
  13 + import { useDrawer } from '/@/components/Drawer';
  14 + import moment from 'moment';
  15 + import { isArray } from '/@/utils/is';
  16 + import AuthDropDown from '/@/components/Widget/AuthDropDown.vue';
  17 + import edgeStatusIsOnlinePng from '/@/assets/images/edgeStatusIsOnline.png';
  18 + import edgeStatusIsOfflinePng from '/@/assets/images/edgeStatusIsOffline.png';
  19 + import { Image } from 'ant-design-vue';
  20 + import { useGo } from '/@/hooks/web/usePage';
  21 +
  22 + defineProps<{
  23 + mode: EnumTableCardMode;
  24 + }>();
  25 +
  26 + defineEmits(['register']);
  27 +
  28 + enum DropMenuEvent {
  29 + DELETE = 'delete',
  30 + }
  31 +
  32 + const { createMessage } = useMessage();
  33 +
  34 + const go = useGo();
  35 +
  36 + const { query } = useRoute();
  37 +
  38 + const disabledDeleteFlag = ref(true);
  39 +
  40 + const [registerCardList, { reload, getSelectedRecords, clearSelectedKeys }] = useCardList({
  41 + api: async ({ page, pageSize, textSearch }) => {
  42 + const res = await edgeInstancePage({
  43 + page: page === 1 ? 0 : page,
  44 + pageSize,
  45 + textSearch,
  46 + sortProperty: 'createdTime',
  47 + sortOrder: 'DESC',
  48 + });
  49 + return {
  50 + total: res?.totalElements,
  51 + items: res?.data,
  52 + };
  53 + },
  54 + useSearchForm: true,
  55 + gutter: 4,
  56 + rowKey: 'routingKey', //id是对象
  57 + formConfig: {
  58 + schemas: searchFormSchema,
  59 + labelWidth: 100,
  60 + model: {
  61 + name: (query as Recordable)?.name ? decodeURIComponent((query as Recordable)?.name) : null,
  62 + },
  63 + },
  64 + selections: {
  65 + beforeSelectValidate: () => {
  66 + return true;
  67 + },
  68 + onSelect: (_record, _flag, allSelecteds) => {
  69 + disabledDeleteFlag.value = !allSelecteds.length;
  70 + },
  71 + onSelectAll: () => {
  72 + // 全选事件
  73 + disabledDeleteFlag.value = false;
  74 + },
  75 + onUnSelectAll: () => {
  76 + // 反选事件
  77 + disabledDeleteFlag.value = true;
  78 + },
  79 + onSelectToggle: (status: boolean) => {
  80 + // 全选是false,反选是true
  81 + if (!status) disabledDeleteFlag.value = false;
  82 + else disabledDeleteFlag.value = true;
  83 + },
  84 + },
  85 + });
  86 +
  87 + const [registerEdgeInstanceFormDrawer, { openDrawer: openEdgeInstanceFormDrawer }] = useDrawer();
  88 +
  89 + const handleEventIsSuccess = () => reload();
  90 +
  91 + const handleGoDetail = (record: EdgeInstanceItemType | null) => {
  92 + go('/edge/edge_detail/' + record?.id?.id);
  93 + };
  94 +
  95 + const handleOperationEvent = (
  96 + event: HandleOperationEnum,
  97 + record: EdgeInstanceItemType | null
  98 + ) => {
  99 + const isUpdate = event === HandleOperationEnum.CREATE ? false : true;
  100 + const isUpdateText =
  101 + event === HandleOperationEnum.CREATE
  102 + ? HandleOperationNameEnum.CREATE
  103 + : event === HandleOperationEnum.UPDATE
  104 + ? HandleOperationNameEnum.UPDATE
  105 + : HandleOperationNameEnum.VIEW;
  106 + if (event === HandleOperationEnum.VIEW) {
  107 + } else {
  108 + openEdgeInstanceFormDrawer(true, { isUpdate, record, isUpdateText, event });
  109 + }
  110 + };
  111 +
  112 + const handleDelete = async (event: HandleOperationEnum, id?: string | null) => {
  113 + try {
  114 + if (event === HandleOperationEnum.BATCH_DELETE) {
  115 + const batchDeleteIds = getSelectedRecords().map(
  116 + (rowRecord: EdgeInstanceItemType) => rowRecord?.id?.id
  117 + );
  118 + if (isArray(batchDeleteIds) && batchDeleteIds.length === 0) return;
  119 + for (let item of batchDeleteIds) await deleteEdgeInstance(item!);
  120 + } else {
  121 + await deleteEdgeInstance(id!);
  122 + }
  123 + createMessage.success('删除成功');
  124 + clearSelectedKeys();
  125 + disabledDeleteFlag.value = true;
  126 + await reload();
  127 + } catch (error) {
  128 + throw error;
  129 + }
  130 + };
  131 +</script>
  132 +
  133 +<template>
  134 + <section>
  135 + <BasicCardList @register="registerCardList">
  136 + <template #toolbar>
  137 + <Button type="primary" @click="handleOperationEvent(HandleOperationEnum.CREATE, null)"
  138 + >新增实例</Button
  139 + >
  140 + <Popconfirm
  141 + title="您确定要批量删除数据"
  142 + ok-text="确定"
  143 + cancel-text="取消"
  144 + @confirm="handleDelete(HandleOperationEnum.BATCH_DELETE, null)"
  145 + :disabled="disabledDeleteFlag"
  146 + >
  147 + <Button type="primary" danger :disabled="disabledDeleteFlag"> 批量删除 </Button>
  148 + </Popconfirm>
  149 + </template>
  150 + <template #renderItem="{ item }: BasicCardListRenderItem<EdgeInstanceItemType>">
  151 + <Card hoverable>
  152 + <template #cover>
  153 + <div class="w-full h-full !flex flex-col justify-between m-3">
  154 + <div class="!flex justify-between align-center text-center">
  155 + <Tooltip :title="item.name">
  156 + <span class="truncate font-bold fill-dark-900 text-sm"> {{ item.name }} </span>
  157 + </Tooltip>
  158 + <div class="mr-6">
  159 + <a-tag class="!flex items-center" :color="item.active ? '#E8FFEA' : '#FFECE8'">
  160 + <template #icon>
  161 + <template v-if="item.active">
  162 + <Image :width="12" :height="12" :src="edgeStatusIsOnlinePng" />
  163 + </template>
  164 + <template v-else>
  165 + <Image :width="12" :height="12" :src="edgeStatusIsOfflinePng" />
  166 + </template>
  167 + </template>
  168 + <span class="ml-1" :style="{ color: item.active ? '#00B42A' : '#F53F3F' }">{{
  169 + item.active ? '在线' : '离线'
  170 + }}</span>
  171 + </a-tag>
  172 + </div>
  173 + </div>
  174 + <div class="!flex justify-between align-center text-center">
  175 + <span class="truncate text-xs" style="color: #86909c">
  176 + {{ moment(item.createdTime).format('YYYY-MM-DD HH:mm:ss') }}
  177 + </span>
  178 + </div>
  179 + </div>
  180 + </template>
  181 + <template class="ant-card-actions" #actions>
  182 + <Tooltip title="详情">
  183 + <AuthIcon
  184 + class="!text-lg"
  185 + icon="ant-design:eye-outlined"
  186 + @click.stop="handleGoDetail(item)"
  187 + />
  188 + </Tooltip>
  189 + <Tooltip title="编辑">
  190 + <AuthIcon
  191 + class="!text-lg"
  192 + icon="ant-design:form-outlined"
  193 + @click.stop="handleOperationEvent(HandleOperationEnum.UPDATE, item)"
  194 + />
  195 + </Tooltip>
  196 + <AuthDropDown
  197 + @click.stop
  198 + :trigger="['hover']"
  199 + :drop-menu-list="[
  200 + {
  201 + text: '删除',
  202 + event: DropMenuEvent.DELETE,
  203 + icon: 'ant-design:delete-outlined',
  204 + popconfirm: {
  205 + title: '是否确认删除操作?',
  206 + onConfirm: handleDelete.bind(null, HandleOperationEnum.DELETE, item?.id?.id),
  207 + },
  208 + },
  209 + ]"
  210 + />
  211 + </template>
  212 + <Card.Meta>
  213 + <template #description>
  214 + <div class="truncate h-17 !flex justify-between flex-col">
  215 + <div class="truncate !flex">
  216 + <span class="text-xs" style="color: #86909c">标签</span>
  217 + <Tooltip :title="item.label">
  218 + <span class="truncate ml-7.5 text-xs" style="color: #00b42a">{{
  219 + item.label
  220 + }}</span>
  221 + </Tooltip>
  222 + </div>
  223 + <div class="truncate !flex">
  224 + <span class="text-xs" style="color: #86909c">边缘类型</span>
  225 + <Tooltip :title="item.type">
  226 + <span style="color: #4e5969" class="truncate ml-7.5 text-xs">{{
  227 + item.type
  228 + }}</span>
  229 + </Tooltip>
  230 + </div>
  231 + <div class="truncate !flex">
  232 + <span class="text-xs" style="color: #86909c">描述</span>
  233 + <Tooltip :title="item?.additionalInfo?.description">
  234 + <span style="color: #4e5969" class="truncate ml-7.5 text-xs">
  235 + {{ item?.additionalInfo?.description }}
  236 + </span>
  237 + </Tooltip>
  238 + </div>
  239 + </div>
  240 + </template>
  241 + </Card.Meta>
  242 + </Card>
  243 + </template>
  244 + </BasicCardList>
  245 + <EdgeInstanceFormDrawer
  246 + @register="registerEdgeInstanceFormDrawer"
  247 + @success="handleEventIsSuccess"
  248 + />
  249 + </section>
  250 +</template>
  251 +
  252 +<style lang="less" scoped>
  253 + .profile-list:deep(.ant-image-img) {
  254 + @apply !w-full !h-full;
  255 + }
  256 +
  257 + .profile-list:deep(.ant-card-body) {
  258 + @apply !p-4;
  259 + }
  260 +
  261 + :deep(.ant-card-body) {
  262 + padding: 12px;
  263 + }
  264 +</style>
... ...
  1 +<script lang="ts" setup>
  2 + import { ref } from 'vue';
  3 + import { EdgeDeviceBasicInfo } from '../EdgeDeviceBasicInfo';
  4 + import { EdgeDeviceTabInfo } from '../EdgeDeviceTabInfo';
  5 + import { EdgeDeviceItemType, EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  6 + import { infoEdgeDevice } from '/@/api/edgeManage/edgeInstance';
  7 + import { useRoute } from 'vue-router';
  8 + import { PageWrapper } from '/@/components/Page';
  9 + import { useGo } from '/@/hooks/web/usePage';
  10 +
  11 + const emits = defineEmits(['register', 'success']);
  12 +
  13 + defineProps({
  14 + recordEdgeInstanceData: {
  15 + type: Object as PropType<EdgeInstanceItemType>,
  16 + default: () => {},
  17 + },
  18 + });
  19 +
  20 + const route = useRoute();
  21 +
  22 + const go = useGo();
  23 +
  24 + const deviceId = ref(route.params?.id);
  25 +
  26 + const edgeId = ref(route.params?.edgeId as string);
  27 +
  28 + const recordData = ref<EdgeDeviceItemType>();
  29 +
  30 + const handleEventIsSuccess = () => {
  31 + emits('success');
  32 + };
  33 +
  34 + infoEdgeDevice(deviceId.value as string).then((res) => {
  35 + recordData.value = res;
  36 + });
  37 +
  38 + function goBack() {
  39 + go('/edge/edge_detail/' + edgeId.value);
  40 + }
  41 +</script>
  42 +
  43 +<template>
  44 + <PageWrapper :title="`边缘设备详情`" contentBackground @back="goBack">
  45 + <!-- 基础信息 -->
  46 + <div class="m-4">
  47 + <EdgeDeviceBasicInfo
  48 + :recordData="recordData"
  49 + :edgeId="edgeId"
  50 + @success="handleEventIsSuccess"
  51 + />
  52 + </div>
  53 + <!-- Tab信息 -->
  54 + <div>
  55 + <EdgeDeviceTabInfo v-if="recordData" :recordData="recordData" />
  56 + </div>
  57 + </PageWrapper>
  58 +</template>
  59 +
  60 +<style lang="less" scoped></style>
... ...
  1 +import { BasicColumn, FormSchema } from '/@/components/Table';
  2 +import { Tag, Tooltip } from 'ant-design-vue';
  3 +import { h } from 'vue';
  4 +import { transformTime } from '/@/hooks/web/useDateToLocaleString';
  5 +import { DeviceState, DeviceTypeEnum } from '/@/api/device/model/deviceModel';
  6 +import { handeleCopy } from '/@/views/device/profiles/step/topic';
  7 +
  8 +// 表格配置
  9 +export const columns: BasicColumn[] = [
  10 + {
  11 + title: '设备状态',
  12 + dataIndex: 'deviceState',
  13 + width: 100,
  14 + customRender: ({ record }) => {
  15 + const color =
  16 + record.deviceState == DeviceState.INACTIVE
  17 + ? 'warning'
  18 + : record.deviceState == DeviceState.ONLINE
  19 + ? 'success'
  20 + : 'error';
  21 + const text =
  22 + record.deviceState == DeviceState.INACTIVE
  23 + ? '待激活'
  24 + : record.deviceState == DeviceState.ONLINE
  25 + ? '在线'
  26 + : '离线';
  27 + return h(Tag, { color: color }, () => text);
  28 + },
  29 + },
  30 + {
  31 + dataIndex: 'name',
  32 + title: '别名/设备名称',
  33 + width: 210,
  34 + slots: { customRender: 'name', title: 'deviceTitle' },
  35 + customRender: ({ record }) => {
  36 + return h('div', { style: 'display:flex;flex-direction:column' }, [
  37 + record.alias &&
  38 + h(
  39 + 'div',
  40 + {
  41 + class: 'cursor-pointer truncate',
  42 + },
  43 + h(
  44 + Tooltip,
  45 + {
  46 + placement: 'topLeft',
  47 + title: `${record.alias}`,
  48 + },
  49 + () => `${record.alias}`
  50 + )
  51 + ),
  52 + h(
  53 + 'div',
  54 + {
  55 + class: 'cursor-pointer text-blue-500 truncate',
  56 + onClick: () => {
  57 + handeleCopy(`${record.name}`);
  58 + },
  59 + },
  60 + h(
  61 + Tooltip,
  62 + {
  63 + placement: 'topLeft',
  64 + title: `${record.name}`,
  65 + },
  66 + () => `${record.name}`
  67 + )
  68 + ),
  69 + ]);
  70 + },
  71 + },
  72 + {
  73 + title: '所属产品',
  74 + width: 160,
  75 + dataIndex: 'deviceProfileName',
  76 + },
  77 + {
  78 + title: '所属组织',
  79 + dataIndex: 'organizationDTO.name',
  80 + width: 160,
  81 + },
  82 + {
  83 + title: '设备类型',
  84 + width: 100,
  85 + dataIndex: 'deviceType',
  86 + customRender: ({ record }) => {
  87 + const color = 'success';
  88 + const text =
  89 + record.deviceType === DeviceTypeEnum.GATEWAY
  90 + ? '网关设备'
  91 + : record.deviceType === DeviceTypeEnum.DIRECT_CONNECTION
  92 + ? '直连设备'
  93 + : '网关子设备';
  94 + return h(Tag, { color: color }, () => text);
  95 + },
  96 + },
  97 + {
  98 + title: '创建时间',
  99 + width: 120,
  100 + dataIndex: 'createdTime',
  101 + format: (_text: string, record: Recordable) => {
  102 + return transformTime(record.createdTime);
  103 + },
  104 + },
  105 +];
  106 +
  107 +// 表格查询表单
  108 +export const searchFormSchema: FormSchema[] = [
  109 + {
  110 + field: 'textSearch',
  111 + label: '设备名称',
  112 + component: 'Input',
  113 + colProps: { span: 6 },
  114 + componentProps: {
  115 + maxLength: 255,
  116 + placeholder: '请输入设备名称',
  117 + },
  118 + },
  119 +];
... ...
  1 +export { default as EdgeDevice } from './index.vue';
  2 +export { default as EdgeDeviceDetail } from './EdgeDeviceDetailDrawer.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { CSSProperties, h, ref } from 'vue';
  3 + import { EdgeDeviceItemType, EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  4 + import { BasicTable, useTable, TableAction } from '/@/components/Table';
  5 + import { columns, searchFormSchema } from './config';
  6 + import { edgeDeviceDeleteDistribution, edgeDevicePage } from '/@/api/edgeManage/edgeInstance';
  7 + import { useModal } from '/@/components/Modal';
  8 + import { EdgeDeviceDistribution } from '../EdgeDeviceDistribution';
  9 + import { useMessage } from '/@/hooks/web/useMessage';
  10 + import { useRoute } from 'vue-router';
  11 + import { useGo } from '/@/hooks/web/usePage';
  12 + import { Badge } from 'ant-design-vue';
  13 + import Icon from '/@/components/Icon';
  14 +
  15 + defineEmits(['register']);
  16 +
  17 + defineProps({
  18 + recordData: {
  19 + type: Object as PropType<EdgeInstanceItemType>,
  20 + default: () => {},
  21 + },
  22 + });
  23 +
  24 + const route = useRoute();
  25 +
  26 + const edgeId = ref(route.params?.id as string);
  27 +
  28 + const go = useGo();
  29 +
  30 + const { createMessage } = useMessage();
  31 +
  32 + const [registerEdgeDeviceDistributionModal, { openModal }] = useModal();
  33 +
  34 + const handleEventIsDistribution = () => {
  35 + openModal(true, {
  36 + record: 1,
  37 + });
  38 + };
  39 +
  40 + const handleEventIsCancelDistribution = async (record: EdgeDeviceItemType) => {
  41 + await edgeDeviceDeleteDistribution(edgeId.value as string, record.id.id);
  42 + createMessage.success('取消分配成功');
  43 + reload();
  44 + };
  45 +
  46 + const AlarmDetailActionButton = ({ hasAlarm }: { hasAlarm?: boolean }) =>
  47 + h(
  48 + Badge,
  49 + { offset: [0, -5] },
  50 + {
  51 + default: () => h('span', { style: { color: '#377dff' } }, '详情'),
  52 + count: () =>
  53 + h(
  54 + 'div',
  55 + {
  56 + style: {
  57 + visibility: hasAlarm ? 'visible' : 'hidden',
  58 + width: '14px',
  59 + height: '14px',
  60 + display: 'flex',
  61 + justifyContent: 'center',
  62 + alignItems: 'center',
  63 + border: '1px solid #f46161',
  64 + borderRadius: '50%',
  65 + } as CSSProperties,
  66 + },
  67 + h(Icon, { icon: 'mdi:bell-warning', color: '#f46161', size: 12 })
  68 + ),
  69 + }
  70 + );
  71 +
  72 + const [registerTable, { reload }] = useTable({
  73 + title: '边缘设备',
  74 + columns,
  75 + api: async ({ page, pageSize, textSearch }) => {
  76 + const res = await edgeDevicePage(
  77 + {
  78 + page: page - 1 < 0 ? 0 : page - 1,
  79 + pageSize,
  80 + textSearch,
  81 + sortProperty: 'createdTime',
  82 + sortOrder: 'DESC',
  83 + },
  84 + edgeId.value as string
  85 + );
  86 + return {
  87 + total: res?.totalElements,
  88 + items: res?.data,
  89 + };
  90 + },
  91 + formConfig: {
  92 + labelWidth: 100,
  93 + schemas: searchFormSchema,
  94 + },
  95 + useSearchForm: true,
  96 + showIndexColumn: false,
  97 + clickToRowSelect: false,
  98 + showTableSetting: true,
  99 + bordered: true,
  100 + rowKey: 'name',
  101 + actionColumn: {
  102 + width: 140,
  103 + title: '操作',
  104 + slots: { customRender: 'action' },
  105 + fixed: 'right',
  106 + },
  107 + });
  108 +
  109 + const handleEventIsSuccess = () => reload();
  110 +
  111 + function handleGoDeviceDetail(record: Recordable) {
  112 + go(`/edge/edge_device/edge_device_detail/${record?.id?.id}/${edgeId.value}`);
  113 + }
  114 +</script>
  115 +
  116 +<template>
  117 + <BasicTable :clickToRowSelect="false" @register="registerTable">
  118 + <template #toolbar>
  119 + <a-button type="primary" @click="handleEventIsDistribution"> 分配设备 </a-button>
  120 + </template>
  121 + <template #action="{ record }">
  122 + <TableAction
  123 + :actions="[
  124 + {
  125 + // label: '详情',
  126 + label: AlarmDetailActionButton({ hasAlarm: !!record.alarmStatus }),
  127 + icon: 'ant-design:eye-outlined',
  128 + onClick: handleGoDeviceDetail.bind(null, record),
  129 + },
  130 + {
  131 + label: '取消分配',
  132 + icon: 'mdi:account-arrow-left',
  133 + onClick: handleEventIsCancelDistribution.bind(null, record),
  134 + },
  135 + ]"
  136 + />
  137 + </template>
  138 + </BasicTable>
  139 + <EdgeDeviceDistribution
  140 + @register="registerEdgeDeviceDistributionModal"
  141 + :edgeId="edgeId"
  142 + @success="handleEventIsSuccess"
  143 + />
  144 +</template>
  145 +
  146 +<style lang="less" scoped></style>
... ...
  1 +import { findDictItemByCode } from '/@/api/system/dict';
  2 +import { BasicColumn, FormSchema } from '/@/components/Table';
  3 +import { AlarmStatus } from '/@/enums/alarmEnum';
  4 +import { alarmLevel } from '/@/views/device/list/config/detail.config';
  5 +import { Tag } from 'ant-design-vue';
  6 +import { h } from 'vue';
  7 +
  8 +// 表格配置
  9 +export const columns: BasicColumn[] = [
  10 + {
  11 + title: '告警状态',
  12 + dataIndex: 'status',
  13 + customRender({ record }: { record }) {
  14 + const flag = !!record.cleared;
  15 + return h(Tag, { color: flag ? 'green' : 'red' }, () => (flag ? '清除' : '激活'));
  16 + },
  17 + width: 90,
  18 + },
  19 + {
  20 + title: '告警设备',
  21 + dataIndex: 'deviceName',
  22 + customRender: ({ record }) => {
  23 + const { deviceAlias, deviceName } = record || {};
  24 + return deviceAlias || deviceName;
  25 + },
  26 + },
  27 + {
  28 + title: '告警场景',
  29 + dataIndex: 'type',
  30 + },
  31 + {
  32 + title: '告警级别',
  33 + dataIndex: 'severity',
  34 + format: (text) => alarmLevel(text),
  35 + },
  36 + {
  37 + title: '告警时间',
  38 + dataIndex: 'createdTime',
  39 + },
  40 + {
  41 + title: '告警详情',
  42 + dataIndex: 'details',
  43 + slots: { customRender: 'details' },
  44 + },
  45 +];
  46 +
  47 +// 表格查询表单
  48 +export const searchFormSchema: FormSchema[] = [
  49 + {
  50 + field: 'status',
  51 + label: '告警/确认状态',
  52 + component: 'Cascader',
  53 + helpMessage: [
  54 + '激活未确认: 可以处理,清除',
  55 + '激活已确认: 只可清除,已经处理',
  56 + '清除未确认: 只可处理,已经清除',
  57 + '清除已确认: 不需要做处理和清除',
  58 + ],
  59 + colProps: { span: 6 },
  60 + componentProps: {
  61 + popupClassName: 'alarm-stauts-cascader',
  62 + options: [
  63 + {
  64 + value: '0',
  65 + label: '清除',
  66 + children: [
  67 + {
  68 + value: AlarmStatus.CLEARED_UN_ACK,
  69 + label: '清除未确认',
  70 + },
  71 + {
  72 + value: AlarmStatus.CLEARED_ACK,
  73 + label: '清除已确认',
  74 + },
  75 + ],
  76 + },
  77 + {
  78 + value: '1',
  79 + label: '激活',
  80 + children: [
  81 + {
  82 + value: AlarmStatus.ACTIVE_UN_ACK,
  83 + label: '激活未确认',
  84 + },
  85 + {
  86 + value: AlarmStatus.ACTIVE_ACK,
  87 + label: '激活已确认',
  88 + },
  89 + ],
  90 + },
  91 + ],
  92 + placeholder: '请选择告警/确认状态',
  93 + },
  94 + },
  95 + {
  96 + field: 'severity',
  97 + label: '告警级别',
  98 + component: 'ApiSelect',
  99 + colProps: { span: 6 },
  100 + componentProps: {
  101 + placeholder: '请选择告警级别',
  102 + api: findDictItemByCode,
  103 + params: {
  104 + dictCode: 'severity_type',
  105 + },
  106 + labelField: 'itemText',
  107 + valueField: 'itemValue',
  108 + },
  109 + },
  110 +];
... ...
  1 +export { default as EdgeDeviceAlarm } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { BasicTable, useTable, TableAction } from '/@/components/Table';
  3 + import { columns, searchFormSchema } from './config';
  4 + import { BasicModal, useModal } from '/@/components/Modal';
  5 + import { Input, Button } from 'ant-design-vue';
  6 + import { ref, computed, nextTick } from 'vue';
  7 + import { EyeOutlined } from '@ant-design/icons-vue';
  8 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  9 + import { getDeviceAlarm, doBatchAckAlarm, doBatchClearAlarm } from '/@/api/device/deviceManager';
  10 + import { AlarmLogItem } from '/@/api/device/model/deviceConfigModel';
  11 + import { AlarmStatus } from '/@/enums/alarmEnum';
  12 + import { useMessage } from '/@/hooks/web/useMessage';
  13 + import { getDeviceDetail } from '/@/api/device/deviceManager';
  14 + import { getAttribute } from '/@/api/ruleengine/ruleengineApi';
  15 + import {
  16 + operationBoolean,
  17 + operationNumber_OR_TIME,
  18 + operationString,
  19 + } from '/@/views/rule/linkedge/config/formatData';
  20 + import { clearOrAckAlarm } from '/@/api/device/deviceManager';
  21 +
  22 + const props = defineProps({
  23 + recordData: {
  24 + type: Object as PropType<EdgeDeviceItemType>,
  25 + default: () => {},
  26 + },
  27 + });
  28 +
  29 + defineEmits(['register']);
  30 +
  31 + const outputData = ref<string>();
  32 +
  33 + const { createMessage } = useMessage();
  34 +
  35 + const [registerTable, { reload, getSelectRows, clearSelectedRowKeys, getRowSelection }] =
  36 + useTable({
  37 + columns,
  38 + api: getDeviceAlarm,
  39 + beforeFetch: (params) => {
  40 + const { status } = params;
  41 + const obj = {
  42 + ...params,
  43 + deviceId: props?.recordData?.id?.id,
  44 + ...{
  45 + status: status ? status.at(-1) : null,
  46 + },
  47 + };
  48 + return obj;
  49 + },
  50 + formConfig: {
  51 + labelWidth: 130,
  52 + schemas: searchFormSchema,
  53 + },
  54 + useSearchForm: true,
  55 + showIndexColumn: false,
  56 + clickToRowSelect: false,
  57 + showTableSetting: true,
  58 + bordered: true,
  59 + rowKey: 'id',
  60 + rowSelection: {
  61 + type: 'checkbox',
  62 + getCheckboxProps: (record: AlarmLogItem) => {
  63 + return {
  64 + disabled: record.status === AlarmStatus.CLEARED_ACK,
  65 + };
  66 + },
  67 + },
  68 + actionColumn: {
  69 + title: '操作',
  70 + slots: { customRender: 'action' },
  71 + fixed: 'right',
  72 + },
  73 + });
  74 +
  75 + const [registerModal, { openModal, closeModal }] = useModal();
  76 +
  77 + const findName = (item: Recordable, curr: Recordable) => {
  78 + return item.attribute.find((item) => item.identifier === curr?.key)?.name;
  79 + };
  80 +
  81 + const findLogin = (curr: Recordable) => {
  82 + return [...operationNumber_OR_TIME, ...operationString, ...operationBoolean].find(
  83 + (item) => item.value === curr?.logic
  84 + )?.symbol;
  85 + };
  86 +
  87 + const findAttribute = (item: Recordable, curr: Recordable) => {
  88 + item.attribute.find((findItem) => findItem.identifier === curr?.key);
  89 + };
  90 +
  91 + const findValue = (item: Recordable, curr: Recordable) => {
  92 + return {
  93 + ['触发属性']: findName(item, curr),
  94 + ['触发条件']: `${findLogin(curr)}${curr?.logicValue}`,
  95 + ['触发值']: `${curr?.realValue}${
  96 + (findAttribute(item, curr) as any)?.detail?.dataType?.specs?.unit?.key ?? ''
  97 + }`,
  98 + };
  99 + };
  100 +
  101 + const handleAlarmText = (text: string) => (text === 'triggerData' ? '触发器' : '执行条件');
  102 +
  103 + const handleViewDetail = async (record: Recordable) => {
  104 + await nextTick();
  105 + const { details } = record;
  106 + if (!details) return;
  107 + const deviceIdKeys = Object.keys(details);
  108 + const dataFormat = await handleAlarmDetailFormat(deviceIdKeys);
  109 + const mapDataFormat = deviceIdKeys.map((deviceKey: string) => {
  110 + const findDataFormat = dataFormat.find(
  111 + (dataItem: Recordable) => dataItem.tbDeviceId === deviceKey
  112 + );
  113 + const dataKeys = Object.keys(details[deviceKey]);
  114 + const data: any = dataKeys.map((dataItem: string) => {
  115 + if (dataItem !== 'triggerData' && dataItem !== 'conditionData') {
  116 + return findValue(findDataFormat, details[deviceKey]);
  117 + } else {
  118 + return {
  119 + [handleAlarmText(dataItem)]: findValue(findDataFormat, details[deviceKey][dataItem]),
  120 + };
  121 + }
  122 + });
  123 + const objectDataFormat = data.reduce((acc: Recordable, curr: Recordable) => {
  124 + return {
  125 + ...acc,
  126 + ...curr,
  127 + };
  128 + });
  129 + return {
  130 + [findDataFormat.name]: objectDataFormat,
  131 + };
  132 + });
  133 + const objectDataFormats = mapDataFormat.reduce((acc: Recordable, curr: Recordable) => {
  134 + return {
  135 + ...acc,
  136 + ...curr,
  137 + };
  138 + });
  139 + outputData.value = JSON.stringify(objectDataFormats, null, 2);
  140 + openModal(true);
  141 + };
  142 +
  143 + const handleAlarmDetailFormat = async (keys: string[]) => {
  144 + const temp: Recordable = [];
  145 + for (let item of keys) {
  146 + if (item === 'key' || item === 'data') return []; //旧数据则终止
  147 + const deviceDetailRes = await getDeviceDetail(item);
  148 + const { deviceProfileId } = deviceDetailRes;
  149 + if (!deviceProfileId) return [];
  150 + const attributeRes = await getAttribute(deviceProfileId);
  151 + const dataFormat: Recordable = handleDataFormat(deviceDetailRes, attributeRes);
  152 + temp.push(dataFormat);
  153 + }
  154 + return temp;
  155 + };
  156 +
  157 + const handleDataFormat = (deviceDetail: Recordable, attributes: Recordable) => {
  158 + const { name, tbDeviceId, alias } = deviceDetail;
  159 + const attribute = attributes.map((item) => ({
  160 + identifier: item.identifier,
  161 + name: item.name,
  162 + detail: item.detail,
  163 + }));
  164 + return {
  165 + name: alias || name,
  166 + tbDeviceId,
  167 + attribute,
  168 + };
  169 + };
  170 +
  171 + const handleEventIsDone = async (record: Recordable) => {
  172 + if (!record.id) return;
  173 + await clearOrAckAlarm(record.id, false);
  174 + createMessage.success('操作成功');
  175 + reload();
  176 + };
  177 +
  178 + const handleEventIsClear = async (record: Recordable) => {
  179 + if (!record.id) return;
  180 + await clearOrAckAlarm(record.id, true);
  181 + createMessage.success('操作成功');
  182 + reload();
  183 + };
  184 +
  185 + const getCanBatchClear = computed(() => {
  186 + const rowSelection = getRowSelection();
  187 + const getRows: AlarmLogItem[] = getSelectRows();
  188 + return !rowSelection.selectedRowKeys?.length || !getRows.every((item) => !item.cleared);
  189 + });
  190 +
  191 + const getCanBatchAck = computed(() => {
  192 + const rowSelection = getRowSelection();
  193 + const getRows: AlarmLogItem[] = getSelectRows();
  194 + return !rowSelection.selectedRowKeys?.length || !getRows.every((item) => !item.acknowledged);
  195 + });
  196 +
  197 + const handleBatchClear = async () => {
  198 + const ids = getSelectRows<AlarmLogItem>().map((item) => item.id);
  199 + if (!ids.length) return;
  200 + await doBatchClearAlarm(ids);
  201 + createMessage.success('操作成功');
  202 + clearSelectedRowKeys();
  203 + reload();
  204 + };
  205 +
  206 + const handleBatchAck = async () => {
  207 + const ids = getSelectRows<AlarmLogItem>().map((item) => item.id);
  208 + if (!ids.length) return;
  209 + await doBatchAckAlarm(ids);
  210 + createMessage.success('操作成功');
  211 + clearSelectedRowKeys();
  212 + reload();
  213 + };
  214 +</script>
  215 +
  216 +<template>
  217 + <div>
  218 + <BasicTable @register="registerTable">
  219 + <template #details="{ record }">
  220 + <span class="cursor-pointer text-blue-500" @click="handleViewDetail(record)">
  221 + <EyeOutlined class="svg:text-blue-500" />
  222 + <span class="ml-2">查看告警详情</span>
  223 + </span>
  224 + </template>
  225 + <template #action="{ record }">
  226 + <TableAction
  227 + :actions="[
  228 + {
  229 + label: '处理',
  230 + icon: 'ant-design:edit-outlined',
  231 + onClick: handleEventIsDone.bind(null, record),
  232 + ifShow: !record.acknowledged,
  233 + },
  234 + {
  235 + label: '清除',
  236 + icon: 'ant-design:close-circle-outlined',
  237 + onClick: handleEventIsClear.bind(null, record),
  238 + ifShow: !record.cleared,
  239 + },
  240 + ]"
  241 + />
  242 + </template>
  243 + <template #toolbar>
  244 + <a-button danger :disabled="getCanBatchClear" @click="handleBatchClear">
  245 + 批量清除
  246 + </a-button>
  247 + <Button @click="handleBatchAck" type="primary" :disabled="getCanBatchAck">
  248 + <span>批量处理</span>
  249 + </Button>
  250 + </template>
  251 + </BasicTable>
  252 + <BasicModal title="告警详情" @register="registerModal" @ok="closeModal">
  253 + <Input.TextArea v-model:value="outputData" :autosize="true" />
  254 + </BasicModal>
  255 + </div>
  256 +</template>
  257 +
  258 +<style>
  259 + .alarm-stauts-cascader {
  260 + .ant-cascader-menu {
  261 + height: fit-content;
  262 + }
  263 + }
  264 +</style>
... ...
  1 +import { Tag, Tooltip } from 'ant-design-vue';
  2 +import { DeviceTypeEnum } from '/@/api/device/model/deviceModel';
  3 +import { DescItem } from '/@/components/Description/src/typing';
  4 +import { formatToDateTime } from '/@/utils/dateUtil';
  5 +import { CSSProperties, h } from 'vue';
  6 +
  7 +export const descSchema = (): DescItem[] => {
  8 + return [
  9 + {
  10 + field: 'name',
  11 + label: '设备名称',
  12 + render(val, data: Record<'alias' | 'name', string>) {
  13 + return h(Tooltip, { title: data.alias || val }, () =>
  14 + h('span', { style: { cursor: 'pointer' } as CSSProperties }, data.alias || val)
  15 + );
  16 + },
  17 + },
  18 + {
  19 + field: 'label',
  20 + label: '设备标签',
  21 + render: (text) => {
  22 + return text
  23 + ? h(
  24 + Tag,
  25 + {
  26 + color: '#00B42A',
  27 + },
  28 + text
  29 + )
  30 + : '';
  31 + },
  32 + },
  33 + {
  34 + field: 'gatewayName',
  35 + label: '所属网关',
  36 + render: (text) => {
  37 + return text
  38 + ? h(
  39 + Tag,
  40 + {
  41 + color: '#00B42A',
  42 + },
  43 + text
  44 + )
  45 + : '';
  46 + },
  47 + },
  48 + {
  49 + field: 'deviceProfileName',
  50 + label: '产品',
  51 + },
  52 + {
  53 + field: 'deviceType',
  54 + label: '设备类型',
  55 + render: (_, data) => {
  56 + const text =
  57 + data.deviceType === DeviceTypeEnum.GATEWAY
  58 + ? '网关设备'
  59 + : data.deviceType === DeviceTypeEnum.DIRECT_CONNECTION
  60 + ? '直连设备'
  61 + : '网关子设备';
  62 + return h(
  63 + 'span',
  64 + {
  65 + style: { cursor: 'pointer' },
  66 + },
  67 + text
  68 + );
  69 + },
  70 + },
  71 + {
  72 + field: 'createdTime',
  73 + label: '创建时间',
  74 + render: (_, data) => {
  75 + return formatToDateTime(data.createdTime, 'YYYY-MM-DD HH:mm:ss');
  76 + },
  77 + },
  78 + {
  79 + field: 'additionalInfo.description',
  80 + label: '描述',
  81 + },
  82 + ];
  83 +};
... ...
  1 +export { default as EdgeDeviceBasicInfo } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { Description, useDescription } from '/@/components/Description';
  3 + import { descSchema } from './config';
  4 + import type { PropType } from 'vue';
  5 + import { unref } from 'vue';
  6 + import edgeDevicePng from '/@/assets/images/edgeDevice.png';
  7 + import { Image, Popconfirm } from 'ant-design-vue';
  8 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  9 + import { getDeviceToken } from '/@/api/device/deviceManager';
  10 + import { useClipboard } from '@vueuse/core';
  11 + import { useMessage } from '/@/hooks/web/useMessage';
  12 + import ManageDeviceTokenModal from '/@/views/device/list/cpns/modal/ManageDeviceTokenModal.vue';
  13 + import { useModal } from '/@/components/Modal';
  14 + import { edgeDeviceDeleteDistribution } from '/@/api/edgeManage/edgeInstance';
  15 + import { useGo } from '/@/hooks/web/usePage';
  16 +
  17 + const emits = defineEmits(['success']);
  18 +
  19 + const props = defineProps({
  20 + recordData: {
  21 + type: Object as PropType<EdgeDeviceItemType>,
  22 + default: () => {},
  23 + },
  24 + edgeId: {
  25 + type: String,
  26 + default: '',
  27 + },
  28 + });
  29 +
  30 + const CS = {
  31 + 'word-break': 'break-all',
  32 + overflow: 'hidden',
  33 + display: '-webkit-box',
  34 + '-webkit-line-clamp': 2,
  35 + '-webkit-box-orient': 'vertical',
  36 + };
  37 +
  38 + const [register] = useDescription({
  39 + layout: 'vertical',
  40 + schema: descSchema(),
  41 + column: 5,
  42 + });
  43 +
  44 + const { createMessage } = useMessage();
  45 +
  46 + const go = useGo();
  47 +
  48 + const { copied, copy } = useClipboard({ legacy: true });
  49 +
  50 + const handleEventIsCopyDeviceToken = async () => {
  51 + if (!props?.recordData?.id?.id) return;
  52 + const token = await getDeviceToken(props?.recordData?.id?.id);
  53 + if (token.credentialsType === 'ACCESS_TOKEN') {
  54 + await copy(token.credentialsId);
  55 + } else {
  56 + await copy(token.credentialsValue);
  57 + }
  58 + if (unref(copied)) {
  59 + createMessage.success('复制成功');
  60 + }
  61 + };
  62 +
  63 + const [registerModal, { openModal }] = useModal();
  64 +
  65 + const handleEventIsManageDeviceToken = async () => {
  66 + if (!props?.recordData?.id?.id) return;
  67 + const token = await getDeviceToken(props?.recordData?.id?.id);
  68 + openModal(true, token);
  69 + };
  70 +
  71 + const handleEventIsCancelDistribution = async () => {
  72 + await edgeDeviceDeleteDistribution(props.edgeId, props.recordData.id?.id);
  73 + createMessage.success('取消分配成功');
  74 + emits('success');
  75 + goBack();
  76 + };
  77 +
  78 + function goBack() {
  79 + go('/edge/edge_device/' + props.edgeId);
  80 + }
  81 +</script>
  82 +
  83 +<template>
  84 + <a-row :gutter="{ xs: 8, sm: 16, md: 24, lg: 32 }">
  85 + <a-col class="gutter-row" :span="3">
  86 + <div class="!flex flex-col justify-between items-center">
  87 + <div><Image :src="recordData?.deviceInfo?.avatar || edgeDevicePng" :width="180" /></div>
  88 + <div class="!flex flex-col mt-3">
  89 + <span style="color: #1d2129" class="font-bold">{{
  90 + recordData?.alias ? recordData?.alias : recordData?.name
  91 + }}</span>
  92 + <span style="color: #3d3d3d">边缘设备详情</span>
  93 + </div>
  94 + </div>
  95 + </a-col>
  96 + <a-col class="gutter-row" :span="21">
  97 + <div class="!flex flex-col justify-between">
  98 + <Description v-if="recordData" @register="register" :data="recordData" :contentStyle="CS" />
  99 + <div class="!flex mt-3">
  100 + <a-button type="primary" @click="handleEventIsCopyDeviceToken">复制访问令牌</a-button>
  101 + <a-button class="ml-4" type="primary" @click="handleEventIsManageDeviceToken"
  102 + >管理凭证</a-button
  103 + >
  104 + <Popconfirm
  105 + title="您是否要取消分配边缘"
  106 + ok-text="是"
  107 + cancel-text="否"
  108 + @confirm="handleEventIsCancelDistribution"
  109 + >
  110 + <a-button class="ml-4" type="primary">取消分配边缘</a-button>
  111 + </Popconfirm>
  112 + </div>
  113 + </div>
  114 + </a-col>
  115 + </a-row>
  116 + <ManageDeviceTokenModal @register="registerModal" />
  117 +</template>
  118 +
  119 +<style lang="less" scoped>
  120 + :deep(.ant-image-img) {
  121 + height: 157px;
  122 + }
  123 +</style>
... ...
  1 +import { ModelOfMatterParams } from '/@/api/device/model/modelOfMatterModel';
  2 +import { getModelServices } from '/@/api/device/modelOfMatter';
  3 +import { FormProps, FormSchema, useComponentRegister } from '/@/components/Form';
  4 +import { validateTCPCustomCommand } from '/@/components/Form/src/components/ThingsModelForm';
  5 +import { JSONEditor, JSONEditorValidator } from '/@/components/CodeEditor';
  6 +import {
  7 + TransportTypeEnum,
  8 + CommandTypeNameEnum,
  9 + CommandTypeEnum,
  10 + ServiceCallTypeEnum,
  11 + CommandDeliveryWayEnum,
  12 + CommandDeliveryWayNameEnum,
  13 + TCPProtocolTypeEnum,
  14 +} from '/@/enums/deviceEnum';
  15 +import { DeviceTypeEnum } from '/@/api/device/model/deviceModel';
  16 +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  17 +
  18 +export interface CommandDeliveryFormFieldType {
  19 + [CommandFieldsEnum.COMMAND_TYPE]: CommandTypeEnum;
  20 + [CommandFieldsEnum.TCP_COMMAND_VALUE]?: string;
  21 + [CommandFieldsEnum.COMAND_VALUE]?: string;
  22 + [CommandFieldsEnum.SERVICE]?: string;
  23 + [CommandFieldsEnum.MODEL_INPUT]?: ModelOfMatterParams;
  24 + [CommandFieldsEnum.CALL_TYPE]: CommandDeliveryWayEnum;
  25 +}
  26 +
  27 +export enum CommandFieldsEnum {
  28 + COMMAND_TYPE = 'commandType',
  29 + TCP_COMMAND_VALUE = 'tcpCommandValue',
  30 + COMAND_VALUE = 'commandValue',
  31 + SERVICE = 'service',
  32 + MODEL_INPUT = 'modelInput',
  33 + CALL_TYPE = 'callType',
  34 +
  35 + SERVICE_COMMAND = 'serviceCommand',
  36 +
  37 + SERVICE_OBJECT_MODEL = 'serviceObjectModel',
  38 +}
  39 +
  40 +useComponentRegister('JSONEditor', JSONEditor);
  41 +
  42 +export const CommandSchemas = (deviceRecord: EdgeDeviceItemType): FormSchema[] => {
  43 + const { deviceData, deviceProfileId, deviceType } = deviceRecord;
  44 +
  45 + const isTCPTransport = deviceData.transportConfiguration.type === TransportTypeEnum.TCP;
  46 +
  47 + const isTCPModbus =
  48 + isTCPTransport &&
  49 + deviceRecord.deviceProfile?.profileData?.transportConfiguration?.protocol ===
  50 + TCPProtocolTypeEnum.MODBUS_RTU;
  51 +
  52 + return [
  53 + {
  54 + field: CommandFieldsEnum.COMMAND_TYPE,
  55 + component: 'RadioGroup',
  56 + label: '下发类型',
  57 + defaultValue: CommandTypeEnum.CUSTOM,
  58 + required: true,
  59 + componentProps: ({ formActionType }) => {
  60 + const { setFieldsValue } = formActionType;
  61 +
  62 + const getOptions = () => {
  63 + const options = [{ label: CommandTypeNameEnum.CUSTOM, value: CommandTypeEnum.CUSTOM }];
  64 +
  65 + if (isTCPModbus || (isTCPTransport && deviceType === DeviceTypeEnum.SENSOR))
  66 + return options;
  67 +
  68 + options.push({ label: CommandTypeNameEnum.SERVICE, value: CommandTypeEnum.SERVICE });
  69 +
  70 + return options;
  71 + };
  72 + return {
  73 + options: getOptions(),
  74 + onChange() {
  75 + setFieldsValue({
  76 + [CommandFieldsEnum.SERVICE]: null,
  77 + [CommandFieldsEnum.MODEL_INPUT]: null,
  78 + [CommandFieldsEnum.COMAND_VALUE]: null,
  79 + [CommandFieldsEnum.TCP_COMMAND_VALUE]: null,
  80 + });
  81 + },
  82 + };
  83 + },
  84 + },
  85 + {
  86 + field: CommandFieldsEnum.CALL_TYPE,
  87 + component: 'RadioGroup',
  88 + label: '单向/双向',
  89 + required: true,
  90 + defaultValue: CommandDeliveryWayEnum.ONE_WAY,
  91 + ifShow: ({ model }) => model[CommandFieldsEnum.COMMAND_TYPE] === CommandTypeEnum.CUSTOM,
  92 + componentProps: {
  93 + options: Object.keys(CommandDeliveryWayEnum).map((key) => ({
  94 + label: CommandDeliveryWayNameEnum[key],
  95 + value: CommandDeliveryWayEnum[key],
  96 + })),
  97 + },
  98 + },
  99 + {
  100 + field: CommandFieldsEnum.TCP_COMMAND_VALUE,
  101 + label: '命令',
  102 + required: true,
  103 + ifShow: ({ model }) =>
  104 + deviceData.transportConfiguration.type === TransportTypeEnum.TCP &&
  105 + model[CommandFieldsEnum.COMMAND_TYPE] === CommandTypeEnum.CUSTOM,
  106 + component: 'Input',
  107 + rules: [{ validator: validateTCPCustomCommand }],
  108 + componentProps: {
  109 + placeholder: '请输入命令',
  110 + },
  111 + },
  112 + {
  113 + field: CommandFieldsEnum.COMAND_VALUE,
  114 + label: '命令',
  115 + component: 'JSONEditor',
  116 + colProps: { span: 20 },
  117 + changeEvent: 'update:value',
  118 + valueField: 'value',
  119 + required: true,
  120 + rules: JSONEditorValidator(),
  121 + ifShow: ({ model }) =>
  122 + deviceData.transportConfiguration.type !== TransportTypeEnum.TCP &&
  123 + model[CommandFieldsEnum.COMMAND_TYPE] === CommandTypeEnum.CUSTOM,
  124 + componentProps: {
  125 + height: 250,
  126 + },
  127 + },
  128 + {
  129 + field: CommandFieldsEnum.SERVICE,
  130 + label: '服务',
  131 + component: 'ApiSelect',
  132 + required: true,
  133 + ifShow: ({ model }) => model[CommandFieldsEnum.COMMAND_TYPE] !== CommandTypeEnum.CUSTOM,
  134 + rules: [{ required: true, message: '请选择服务' }],
  135 + componentProps: ({ formActionType }) => {
  136 + const { setFieldsValue } = formActionType;
  137 + return {
  138 + api: getModelServices,
  139 + params: {
  140 + deviceProfileId: deviceProfileId.id,
  141 + },
  142 + valueField: 'identifier',
  143 + labelField: 'functionName',
  144 + getPopupContainer: () => document.body,
  145 + placeholder: '请选择服务',
  146 + onChange(
  147 + value: string,
  148 + options: ModelOfMatterParams & Record<'label' | 'value', string>
  149 + ) {
  150 + if (!value) return;
  151 + setFieldsValue({
  152 + [CommandFieldsEnum.CALL_TYPE]:
  153 + options.callType === ServiceCallTypeEnum.ASYNC
  154 + ? CommandDeliveryWayEnum.ONE_WAY
  155 + : CommandDeliveryWayEnum.TWO_WAY,
  156 + [CommandFieldsEnum.MODEL_INPUT]: null,
  157 + [CommandFieldsEnum.SERVICE_OBJECT_MODEL]: Object.assign(options, {
  158 + functionName: options.label,
  159 + identifier: options.value,
  160 + }),
  161 + });
  162 + },
  163 + };
  164 + },
  165 + },
  166 + {
  167 + field: CommandFieldsEnum.SERVICE_OBJECT_MODEL,
  168 + label: '服务物模型',
  169 + component: 'Input',
  170 + ifShow: false,
  171 + },
  172 + {
  173 + field: CommandFieldsEnum.MODEL_INPUT,
  174 + component: 'Input',
  175 + label: '输入参数',
  176 + changeEvent: 'update:value',
  177 + valueField: 'value',
  178 + ifShow: ({ model }) =>
  179 + model[CommandFieldsEnum.SERVICE] &&
  180 + model[CommandFieldsEnum.SERVICE_OBJECT_MODEL] &&
  181 + model[CommandFieldsEnum.COMMAND_TYPE] !== CommandTypeEnum.CUSTOM,
  182 + componentProps: {
  183 + formProps: {
  184 + wrapperCol: { span: 24 },
  185 + } as FormProps,
  186 + },
  187 + slot: 'serviceCommand',
  188 + },
  189 + ];
  190 +};
... ...
  1 +export { default as CommandDeliveryModal } from './index.vue';
... ...
  1 +<template>
  2 + <BasicModal
  3 + title="命令下发"
  4 + :width="650"
  5 + @register="registerModal"
  6 + @ok="handleOk"
  7 + @cancel="handleCancel"
  8 + >
  9 + <BasicForm @register="registerForm">
  10 + <template #serviceCommand="{ field, model }">
  11 + <ThingsModelForm
  12 + :disabled="
  13 + deviceDetail?.deviceData?.transportConfiguration?.type === TransportTypeEnum.TCP
  14 + "
  15 + ref="thingsModelFormRef"
  16 + v-model:value="model[field]"
  17 + :key="model[CommandFieldsEnum.SERVICE_OBJECT_MODEL]?.identifier"
  18 + :inputData="model[CommandFieldsEnum.SERVICE_OBJECT_MODEL]?.functionJson?.inputData"
  19 + :transportType="deviceDetail?.deviceData?.transportConfiguration?.type"
  20 + />
  21 + </template>
  22 + </BasicForm>
  23 + </BasicModal>
  24 +</template>
  25 +<script lang="ts" setup>
  26 + import { nextTick, ref, unref } from 'vue';
  27 + import { BasicForm, ThingsModelForm, useForm } from '/@/components/Form';
  28 + import { CommandDeliveryFormFieldType, CommandFieldsEnum, CommandSchemas } from './config';
  29 + import { useMessage } from '/@/hooks/web/useMessage';
  30 + import { CommandDeliveryWayEnum } from '/@/enums/deviceEnum';
  31 + import { CommandTypeEnum, RPCCommandMethodEnum, TransportTypeEnum } from '/@/enums/deviceEnum';
  32 + import { BasicModal, useModalInner } from '/@/components/Modal';
  33 + import { getDeviceActiveTime } from '/@/api/alarm/position';
  34 + import { RpcCommandType } from '/@/api/device/model/deviceConfigModel';
  35 + import { parseStringToJSON } from '/@/components/CodeEditor';
  36 + import { commandIssuanceApi } from '/@/api/device/deviceManager';
  37 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  38 +
  39 + defineEmits(['register']);
  40 +
  41 + const thingsModelFormRef = ref<InstanceType<typeof ThingsModelForm>>();
  42 + const props = defineProps<{
  43 + deviceDetail: EdgeDeviceItemType;
  44 + }>();
  45 +
  46 + const [registerModal, { setModalProps }] = useModalInner(
  47 + (params: ModalParamsType<EdgeDeviceItemType>) => {
  48 + const { record } = params;
  49 + setProps({
  50 + schemas: CommandSchemas(record),
  51 + });
  52 + }
  53 + );
  54 +
  55 + const { createMessage } = useMessage();
  56 +
  57 + const [registerForm, { setProps, getFieldsValue, validate, resetFields, clearValidate }] =
  58 + useForm({
  59 + labelWidth: 120,
  60 + baseColProps: { span: 20 },
  61 + labelAlign: 'right',
  62 + showSubmitButton: false,
  63 + showResetButton: false,
  64 + });
  65 +
  66 + const handleCancel = async () => {
  67 + await resetFields();
  68 + await nextTick();
  69 + await clearValidate();
  70 + };
  71 +
  72 + const handleValidate = async () => {
  73 + await validate();
  74 + await unref(thingsModelFormRef)?.validate?.();
  75 + };
  76 +
  77 + const handleValidateDeviceActive = async (): Promise<boolean> => {
  78 + const result = await getDeviceActiveTime(unref(props.deviceDetail)!.id?.id);
  79 + const [firstItem] = result;
  80 + return !!firstItem.value;
  81 + };
  82 +
  83 + const handleCommandParams = (
  84 + values: CommandDeliveryFormFieldType,
  85 + serviceCommand: Recordable
  86 + ) => {
  87 + const { commandType, service } = values;
  88 +
  89 + const isTcpDevice =
  90 + unref(props.deviceDetail)?.deviceData?.transportConfiguration?.type === TransportTypeEnum.TCP;
  91 + if (commandType === CommandTypeEnum.CUSTOM) {
  92 + if (isTcpDevice) {
  93 + const value = values.tcpCommandValue;
  94 + return value?.replaceAll(/\s/g, '');
  95 + }
  96 + return parseStringToJSON(values.commandValue!).json;
  97 + } else {
  98 + if (isTcpDevice) return Reflect.get(serviceCommand, CommandFieldsEnum.SERVICE_COMMAND);
  99 + return {
  100 + [service!]: serviceCommand,
  101 + };
  102 + }
  103 + };
  104 +
  105 + const handleOk = async () => {
  106 + await handleValidate();
  107 +
  108 + try {
  109 + setModalProps({ loading: true, confirmLoading: true });
  110 +
  111 + const values = getFieldsValue() as CommandDeliveryFormFieldType;
  112 + const { callType, commandType } = values;
  113 + const serviceCommand = unref(thingsModelFormRef)?.getFieldsValue() || {};
  114 +
  115 + if (callType === CommandDeliveryWayEnum.TWO_WAY && !(await handleValidateDeviceActive())) {
  116 + createMessage.warn('当前设备不在线');
  117 + return;
  118 + }
  119 +
  120 + const rpcCommands: RpcCommandType = {
  121 + additionalInfo: {
  122 + cmdType:
  123 + commandType === CommandTypeEnum.CUSTOM
  124 + ? CommandTypeEnum.CUSTOM
  125 + : CommandTypeEnum.SERVICE,
  126 + },
  127 + method: RPCCommandMethodEnum.THINGSKIT,
  128 + persistent: true,
  129 + params: handleCommandParams(values, serviceCommand),
  130 + };
  131 +
  132 + await commandIssuanceApi(callType, unref(props.deviceDetail)!.id?.id, rpcCommands);
  133 +
  134 + createMessage.success('命令下发成功');
  135 + } finally {
  136 + setModalProps({ loading: false, confirmLoading: false });
  137 + }
  138 + };
  139 +</script>
  140 +<style scoped lang="less"></style>
... ...
  1 +import { BasicColumn, FormSchema } from '/@/components/Table';
  2 +import moment from 'moment';
  3 +import { Tag } from 'ant-design-vue';
  4 +import { h } from 'vue';
  5 +
  6 +// 表格配置
  7 +export const columns: BasicColumn[] = [
  8 + {
  9 + title: '命令下发时间',
  10 + dataIndex: 'createTime',
  11 + format: (text) => {
  12 + return moment(text).format('YYYY-MM-DD HH:mm:ss');
  13 + },
  14 + },
  15 + {
  16 + title: '命令类型',
  17 + dataIndex: 'additionalInfo.cmdType',
  18 + format: (text) => {
  19 + return h(
  20 + Tag,
  21 + {
  22 + color: Number(text) === 1 ? 'green' : 'blue',
  23 + },
  24 + () => (Number(text) === 1 ? '服务' : '自定义')
  25 + ) as unknown as any;
  26 + },
  27 + },
  28 + {
  29 + title: '响应类型',
  30 + dataIndex: 'request.oneway',
  31 + format: (text) => {
  32 + return !text ? '双向' : '单向';
  33 + },
  34 + },
  35 + {
  36 + title: '命令状态',
  37 + dataIndex: 'status',
  38 + customRender: ({ text, record }) => {
  39 + return h(
  40 + Tag,
  41 + {
  42 + color:
  43 + text == 'EXPIRED'
  44 + ? 'red'
  45 + : text == 'DELIVERED'
  46 + ? 'blue'
  47 + : text == 'QUEUED'
  48 + ? '#00C9A7'
  49 + : text == 'TIMEOUT'
  50 + ? 'red'
  51 + : text == 'SENT'
  52 + ? '#00C9A7'
  53 + : text == 'FAILED'
  54 + ? 'red'
  55 + : text == 'SUCCESSFUL'
  56 + ? 'green'
  57 + : 'red',
  58 + },
  59 + () => record?.statusName
  60 + );
  61 + },
  62 + },
  63 + {
  64 + title: '响应内容',
  65 + dataIndex: 'response111',
  66 + slots: { customRender: 'responseContent' },
  67 + },
  68 + {
  69 + title: '命令内容',
  70 + dataIndex: 'request.body',
  71 + slots: { customRender: 'recordContent' },
  72 + },
  73 +];
  74 +
  75 +// 表格查询表单
  76 +export const searchFormSchema: FormSchema[] = [
  77 + {
  78 + field: 'status',
  79 + label: '命令状态',
  80 + component: 'Select',
  81 + colProps: { span: 6 },
  82 + componentProps: {
  83 + options: [
  84 + {
  85 + label: '队列中',
  86 + value: 'QUEUED',
  87 + },
  88 + {
  89 + label: '已发送',
  90 + value: 'SENT',
  91 + },
  92 + {
  93 + label: '发送成功',
  94 + value: 'DELIVERED',
  95 + },
  96 +
  97 + {
  98 + label: '响应成功',
  99 + value: 'SUCCESSFUL',
  100 + },
  101 + {
  102 + label: '超时',
  103 + value: 'TIMEOUT',
  104 + },
  105 + {
  106 + label: '已过期',
  107 + value: 'EXPIRED',
  108 + },
  109 + {
  110 + label: '响应失败',
  111 + value: 'FAILED',
  112 + },
  113 + {
  114 + label: '已删除',
  115 + value: 'DELETED',
  116 + },
  117 + ],
  118 + placeholder: '请选择命令状态',
  119 + },
  120 + },
  121 + {
  122 + field: 'sendTime',
  123 + label: '命令下发时间',
  124 + component: 'RangePicker',
  125 + componentProps: {
  126 + showTime: {
  127 + defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
  128 + },
  129 + },
  130 + colProps: { span: 10 },
  131 + },
  132 +];
... ...
  1 +export { default as EdgeDeviceCommand } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { BasicTable, useTable } from '/@/components/Table';
  3 + import { columns, searchFormSchema } from './config';
  4 + import { JsonPreview } from '/@/components/CodeEditor';
  5 + import { Button, Modal, Space } from 'ant-design-vue';
  6 + import { h } from 'vue';
  7 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  8 + import { deviceCommandRecordGetQuery } from '/@/api/device/deviceConfigApi';
  9 + import { useModal } from '/@/components/Modal';
  10 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  11 + import { CommandDeliveryModal } from './CommandDeliveryModal';
  12 +
  13 + const props = defineProps({
  14 + recordData: {
  15 + type: Object as PropType<EdgeDeviceItemType>,
  16 + default: () => {},
  17 + },
  18 + });
  19 +
  20 + defineEmits(['register']);
  21 +
  22 + const [registerCommandDeliverModal, { openModal }] = useModal();
  23 +
  24 + const [registerTable] = useTable({
  25 + columns,
  26 + api: deviceCommandRecordGetQuery,
  27 + beforeFetch: (params) => {
  28 + return {
  29 + ...params,
  30 + tbDeviceId: props?.recordData?.id?.id,
  31 + };
  32 + },
  33 + formConfig: {
  34 + labelWidth: 120,
  35 + schemas: searchFormSchema,
  36 + fieldMapToTime: [['sendTime', ['startTime', 'endTime'], 'x']],
  37 + },
  38 + useSearchForm: true,
  39 + showIndexColumn: false,
  40 + clickToRowSelect: false,
  41 + showTableSetting: true,
  42 + bordered: true,
  43 + rowKey: 'id',
  44 + });
  45 +
  46 + const commonModalInfo = (title, value) => {
  47 + Modal.info({
  48 + title,
  49 + width: 600,
  50 + content: h(JsonPreview, { data: value }),
  51 + });
  52 + };
  53 +
  54 + const handleRecordContent = (record) => {
  55 + if (!record?.request?.body) return;
  56 + if (Object.prototype.toString.call(record?.request?.body) !== '[object Object]') return;
  57 + const jsonParams = record?.request?.body?.params;
  58 + commonModalInfo('命令下发内容', jsonParams);
  59 + };
  60 +
  61 + const handleRecordResponseContent = (record) => {
  62 + const jsonParams = record?.response;
  63 + commonModalInfo('响应内容', jsonParams);
  64 + };
  65 +
  66 + const handleEventIsCommand = () => {
  67 + openModal(true, {
  68 + mode: DataActionModeEnum.READ,
  69 + record: props.recordData,
  70 + } as ModalParamsType<EdgeDeviceItemType>);
  71 + };
  72 +</script>
  73 +
  74 +<template>
  75 + <div>
  76 + <BasicTable :clickToRowSelect="false" @register="registerTable">
  77 + <template #toolbar>
  78 + <Space>
  79 + <Button type="primary" @click="handleEventIsCommand">命令下发</Button>
  80 + </Space>
  81 + </template>
  82 + <template #recordContent="{ record }">
  83 + <Button type="link" class="ml-2" @click="handleRecordContent(record)"> 查看 </Button>
  84 + </template>
  85 + <template #responseContent="{ record }">
  86 + <div v-if="!record.request?.oneway">
  87 + <Button v-if="record?.response === null" type="text" class="ml-2"> 未响应 </Button>
  88 + <Button v-else type="link" class="ml-2" @click="handleRecordResponseContent(record)">
  89 + 查看
  90 + </Button>
  91 + </div>
  92 + <div v-else>--</div>
  93 + </template>
  94 + </BasicTable>
  95 + <CommandDeliveryModal @register="registerCommandDeliverModal" :deviceDetail="recordData" />
  96 + </div>
  97 +</template>
... ...
  1 +export { default as EdgeDeviceDistribution } from './index.vue';
... ...
  1 +<template>
  2 + <BasicModal
  3 + v-bind="$attrs"
  4 + @register="registerModal"
  5 + title="将设备分配给边缘"
  6 + :canFullscreen="false"
  7 + centered
  8 + :minHeight="300"
  9 + @ok="handleEventIsDistribution"
  10 + @cancel="handleEventIsCancel"
  11 + okText="分配"
  12 + >
  13 + <div class="!flex items-center gap-5">
  14 + <span>分配设备</span>
  15 + <Select
  16 + v-if="edgeId"
  17 + placeholder="请选择设备"
  18 + v-model:value="deviceIds"
  19 + class="!w-1/2"
  20 + :options="selectOptions"
  21 + v-bind="createPickerSearch()"
  22 + mode="multiple"
  23 + allowClear
  24 + />
  25 + </div>
  26 + </BasicModal>
  27 +</template>
  28 +
  29 +<script lang="ts" setup>
  30 + import { BasicModal, useModalInner } from '/@/components/Modal';
  31 + import { edgeDeviceDistribution } from '/@/api/edgeManage/edgeInstance';
  32 + import { useMessage } from '/@/hooks/web/useMessage';
  33 + import { onMounted, ref } from 'vue';
  34 + import { edgeDeviceDistributionPage } from '/@/api/edgeManage/edgeInstance';
  35 + import { Select } from 'ant-design-vue';
  36 + import { isArray } from '/@/utils/is';
  37 + import { createPickerSearch } from '/@/utils/pickerSearch';
  38 +
  39 + const emits = defineEmits(['success', 'register']);
  40 +
  41 + const { createMessage } = useMessage();
  42 +
  43 + const props = defineProps({
  44 + edgeId: {
  45 + type: String,
  46 + default: '',
  47 + },
  48 + });
  49 +
  50 + const [registerModal, { closeModal }] = useModalInner((data) => {
  51 + const { _ } = data;
  52 + deviceIds.value = [];
  53 + handleGetDeviceDistributionList();
  54 + });
  55 +
  56 + const selectOptions = ref<{ label: string; value: string }[]>([]);
  57 +
  58 + const deviceIds = ref<string[]>([]);
  59 +
  60 + const handleGetDeviceDistributionList = async () => {
  61 + const { data } = await edgeDeviceDistributionPage({ page: 0, pageSize: 50 });
  62 + selectOptions.value = data?.map((dataItem: Recordable) => ({
  63 + label: dataItem.alias || dataItem.name,
  64 + value: dataItem.id.id,
  65 + })) as { label: string; value: string }[];
  66 + };
  67 +
  68 + onMounted(() => {
  69 + handleGetDeviceDistributionList();
  70 + });
  71 +
  72 + const handleEventIsDistribution = async () => {
  73 + if (!props.edgeId) return;
  74 + if (!deviceIds.value) return createMessage.error('请选择设备');
  75 + if (isArray(deviceIds.value) && deviceIds.value.length === 0)
  76 + return createMessage.error('请选择设备');
  77 + for (let item of deviceIds.value) await edgeDeviceDistribution(props?.edgeId, item);
  78 + createMessage.success('分配成功');
  79 + handleEventIsCancel();
  80 + emits('success');
  81 + handleGetDeviceDistributionList();
  82 + };
  83 +
  84 + const handleEventIsCancel = () => {
  85 + deviceIds.value.length = 0;
  86 + closeModal();
  87 + };
  88 +</script>
... ...
  1 +import moment from 'moment';
  2 +import { findDictItemByCode } from '/@/api/system/dict';
  3 +import { BasicColumn, FormSchema } from '/@/components/Table';
  4 +import {
  5 + EventType,
  6 + EventTypeColor,
  7 + EventTypeName,
  8 +} from '/@/views/device/list/cpns/tabs/EventManage/config';
  9 +import { Tag } from 'ant-design-vue';
  10 +import { h } from 'vue';
  11 +import { formatToDateTime } from '/@/utils/dateUtil';
  12 +
  13 +// 表格配置
  14 +export const columns: BasicColumn[] = [
  15 + {
  16 + title: '时间',
  17 + dataIndex: 'eventTime',
  18 + format(text) {
  19 + return formatToDateTime(text, 'YYYY-MM-DD HH:mm:ss');
  20 + },
  21 + },
  22 + {
  23 + title: '标识符',
  24 + dataIndex: 'eventIdentifier',
  25 + },
  26 + {
  27 + title: '事件名称',
  28 + dataIndex: 'eventName',
  29 + },
  30 + {
  31 + title: '事件类型',
  32 + dataIndex: 'eventType',
  33 + customRender({ text }) {
  34 + return h(
  35 + Tag,
  36 + {
  37 + color: EventTypeColor[text as EventType],
  38 + },
  39 + () => EventTypeName[text as EventType]
  40 + );
  41 + },
  42 + },
  43 + {
  44 + title: '输出参数',
  45 + dataIndex: 'outputParams',
  46 + slots: { customRender: 'outputParams' },
  47 + },
  48 +];
  49 +
  50 +export const formSchemas: FormSchema[] = [
  51 + {
  52 + field: 'eventIdentifier',
  53 + label: '标识符',
  54 + component: 'Input',
  55 + componentProps: {
  56 + placeholder: '请输入标识符',
  57 + },
  58 + colProps: { span: 6 },
  59 + },
  60 + {
  61 + field: 'eventType',
  62 + label: '事件类型',
  63 + component: 'ApiSelect',
  64 + componentProps: {
  65 + placeholder: '请选择事件类型',
  66 + api: findDictItemByCode,
  67 + params: {
  68 + dictCode: 'event_type',
  69 + },
  70 + labelField: 'itemText',
  71 + valueField: 'itemValue',
  72 + },
  73 + colProps: { span: 6 },
  74 + },
  75 + {
  76 + field: 'dateRange',
  77 + label: '时间范围',
  78 + component: 'RangePicker',
  79 + componentProps: {
  80 + showTime: {
  81 + defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
  82 + },
  83 + },
  84 + colProps: { span: 10 },
  85 + },
  86 +];
... ...
  1 +export { default as EdgeDeviceEvent } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { BasicTable, useTable } from '/@/components/Table';
  3 + import { columns, formSchemas } from './config';
  4 + import { BasicModal, useModal } from '/@/components/Modal';
  5 + import { Input } from 'ant-design-vue';
  6 + import { ref } from 'vue';
  7 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  8 + import { EventManageRequest } from '/@/api/device/model/eventManageModel';
  9 + import { getEventManageListRecord } from '/@/api/device/eventManage';
  10 + import { EyeOutlined } from '@ant-design/icons-vue';
  11 +
  12 + const props = defineProps({
  13 + recordData: {
  14 + type: Object as PropType<EdgeDeviceItemType>,
  15 + default: () => {},
  16 + },
  17 + });
  18 +
  19 + const outputData = ref<string>();
  20 +
  21 + const [registerTable] = useTable({
  22 + columns,
  23 + api: getEventManageListRecord,
  24 + fetchSetting: {
  25 + totalField: 'totalElements',
  26 + listField: 'data',
  27 + },
  28 + beforeFetch: (params: EventManageRequest) => {
  29 + const page = params.page - 1 < 0 ? 0 : params.page - 1;
  30 + const _params = Object.keys(params)
  31 + .filter((key) => params[key])
  32 + .reduce((prev, next) => ({ ...prev, [next]: params[next] }), {});
  33 + return {
  34 + ..._params,
  35 + page,
  36 + startTime: params.startTime ? new Date(params.startTime).getTime() : params.startTime,
  37 + endTime: params.endTime ? new Date(params.endTime).getTime() : params.endTime,
  38 + tbDeviceId: props?.recordData?.id?.id,
  39 + };
  40 + },
  41 + formConfig: {
  42 + labelWidth: 80,
  43 + schemas: formSchemas,
  44 + fieldMapToTime: [['dateRange', ['startTime', 'endTime'], 'YYYY-MM-DD HH:mm:ss']],
  45 + },
  46 + useSearchForm: true,
  47 + showIndexColumn: false,
  48 + clickToRowSelect: false,
  49 + showTableSetting: true,
  50 + bordered: true,
  51 + rowKey: 'id',
  52 + });
  53 +
  54 + const [registerModal, { openModal, closeModal }] = useModal();
  55 +
  56 + const handleViewDetail = (record: Record<'eventValue', Recordable>) => {
  57 + outputData.value = JSON.stringify(record.eventValue, null, 2);
  58 + openModal(true);
  59 + };
  60 +</script>
  61 +
  62 +<template>
  63 + <div>
  64 + <BasicTable :clickToRowSelect="false" @register="registerTable">
  65 + <template #outputParams="{ record }">
  66 + <span class="cursor-pointer text-blue-500" @click="handleViewDetail(record)">
  67 + <EyeOutlined class="svg:text-blue-500" />
  68 + <span class="ml-2">详情</span>
  69 + </span>
  70 + </template>
  71 + </BasicTable>
  72 + <BasicModal title="输出参数" @register="registerModal" @ok="closeModal">
  73 + <Input.TextArea v-model:value="outputData" :autosize="true" />
  74 + </BasicModal>
  75 + </div>
  76 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { computed, nextTick, onMounted, onUnmounted, Ref, ref, unref } from 'vue';
  3 + import { getDeviceHistoryInfo } from '/@/api/alarm/position';
  4 + import { Empty, Spin } from 'ant-design-vue';
  5 + import { useECharts } from '/@/hooks/web/useECharts';
  6 + import { AggregateDataEnum, selectDeviceAttrSchema } from '/@/views/device/localtion/config.data';
  7 + import { useTimePeriodForm } from '/@/views/device/localtion/cpns/TimePeriodForm';
  8 + import {
  9 + defaultSchemas,
  10 + OrderByEnum,
  11 + } from '/@/views/device/localtion/cpns/TimePeriodForm/config';
  12 + import TimePeriodForm from '/@/views/device/localtion/cpns/TimePeriodForm/TimePeriodForm.vue';
  13 + import { useGridLayout } from '/@/hooks/component/useGridLayout';
  14 + import { ColEx } from '/@/components/Form/src/types';
  15 + import { useHistoryData } from './hook/useHistoryData';
  16 + import { formatToDateTime } from '/@/utils/dateUtil';
  17 + import { useTable, BasicTable, BasicColumn, SorterResult } from '/@/components/Table';
  18 + import {
  19 + ModeSwitchButton,
  20 + TABLE_CHART_MODE_LIST,
  21 + EnumTableChartMode,
  22 + } from '/@/components/Widget';
  23 + import { SchemaFiled } from '/@/views/visual/palette/components/HistoryTrendModal/config';
  24 + import { DataTypeEnum } from '/@/enums/objectModelEnum';
  25 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  26 +
  27 + // interface DeviceDetail {
  28 + // tbDeviceId: string;
  29 + // deviceProfileId: string;
  30 + // }
  31 +
  32 + const props = defineProps<{
  33 + deviceDetail: EdgeDeviceItemType;
  34 + attr?: string;
  35 + }>();
  36 +
  37 + const mode = ref<EnumTableChartMode>(EnumTableChartMode.CHART);
  38 +
  39 + const chartRef = ref();
  40 +
  41 + const loading = ref(false);
  42 +
  43 + const isNull = ref(false);
  44 +
  45 + const historyData = ref<{ ts: number; value: string; name: string }[]>([]);
  46 +
  47 + const { deviceAttrs, getDeviceKeys, getSearchParams, setChartOptions, getDeviceAttribute } =
  48 + useHistoryData();
  49 +
  50 + const getIdentifierNameMapping = computed(() => {
  51 + const mapping = {};
  52 + unref(deviceAttrs).forEach((item) => {
  53 + const { identifier, name } = item;
  54 + mapping[identifier] = name;
  55 + });
  56 + return mapping;
  57 + });
  58 +
  59 + function hasDeviceAttr() {
  60 + if (!unref(deviceAttrs).length) {
  61 + return false;
  62 + } else {
  63 + return true;
  64 + }
  65 + }
  66 +
  67 + const sortOrder = ref<any>('descend');
  68 + const columns = computed<BasicColumn[]>(() => {
  69 + return [
  70 + {
  71 + title: '属性',
  72 + dataIndex: 'name',
  73 + format: (text) => {
  74 + return unref(getIdentifierNameMapping)[text];
  75 + },
  76 + },
  77 + {
  78 + title: '值',
  79 + dataIndex: 'value',
  80 + },
  81 + {
  82 + title: '更新时间',
  83 + dataIndex: 'ts',
  84 + format: (val) => {
  85 + return formatToDateTime(val, 'YYYY-MM-DD HH:mm:ss');
  86 + },
  87 + sorter: true,
  88 + sortOrder: unref(sortOrder),
  89 + sortDirections: ['descend', 'ascend', 'descend'],
  90 + },
  91 + ];
  92 + });
  93 +
  94 + const [registerTable, { setColumns }] = useTable({
  95 + showIndexColumn: false,
  96 + showTableSetting: false,
  97 + dataSource: historyData,
  98 + maxHeight: 300,
  99 + columns: unref(columns),
  100 + size: 'small',
  101 + });
  102 +
  103 + const getTableList = async (orderBy?: OrderByEnum) => {
  104 + // 表单验证
  105 + await method.validate();
  106 + const value = method.getFieldsValue();
  107 + const searchParams = getSearchParams(value);
  108 +
  109 + if (!hasDeviceAttr()) return;
  110 + // 发送请求
  111 + loading.value = true;
  112 + const res = await getDeviceHistoryInfo(
  113 + {
  114 + ...searchParams,
  115 + entityId: props.deviceDetail?.id?.id,
  116 + },
  117 + orderBy
  118 + );
  119 + historyData.value = getTableHistoryData(res);
  120 + loading.value = false;
  121 + };
  122 +
  123 + const handleTableChange = async (_pag, _filters, sorter: SorterResult) => {
  124 + sortOrder.value = sorter.order;
  125 + await setColumns(unref(columns));
  126 + if (sorter.field == 'ts') {
  127 + if (sorter.order == 'descend') {
  128 + getTableList(OrderByEnum.DESC);
  129 + } else {
  130 + getTableList(OrderByEnum.ASC);
  131 + }
  132 + }
  133 + };
  134 +
  135 + const { setOptions, getInstance } = useECharts(chartRef as Ref<HTMLDivElement>);
  136 +
  137 + const [register, method] = useTimePeriodForm({
  138 + schemas: [...defaultSchemas, ...selectDeviceAttrSchema],
  139 + baseColProps: useGridLayout(2, 3, 4) as unknown as ColEx,
  140 + async submitFunc() {
  141 + // 表单验证
  142 + await method.validate();
  143 + const value = method.getFieldsValue();
  144 + const searchParams = getSearchParams(value);
  145 +
  146 + if (!hasDeviceAttr()) return;
  147 + // 发送请求
  148 + loading.value = true;
  149 + const res = await getDeviceHistoryInfo(
  150 + {
  151 + ...searchParams,
  152 + entityId: props.deviceDetail?.id?.id,
  153 + },
  154 + searchParams.orderBy as OrderByEnum
  155 + );
  156 + historyData.value = getTableHistoryData(res);
  157 + loading.value = false;
  158 + // 判断数据对象是否为空
  159 + if (!Object.keys(res).length) {
  160 + isNull.value = false;
  161 + return;
  162 + } else {
  163 + isNull.value = true;
  164 + }
  165 +
  166 + const selectedKeys = unref(deviceAttrs).find(
  167 + (item) => item.identifier === value[SchemaFiled.KEYS]
  168 + );
  169 +
  170 + setOptions(setChartOptions(res, selectedKeys));
  171 + },
  172 + });
  173 +
  174 + const getTableHistoryData = (record: Recordable<{ ts: number; value: string }[]>) => {
  175 + const keys = Object.keys(record);
  176 + const list = keys.reduce((prev, next) => {
  177 + const list = record[next].map((item) => {
  178 + return {
  179 + ...item,
  180 + name: next,
  181 + };
  182 + });
  183 + return [...prev, ...list];
  184 + }, []);
  185 + return list;
  186 + };
  187 +
  188 + const getDeviceDataKey = async () => {
  189 + try {
  190 + await getDeviceAttribute(props.deviceDetail);
  191 + if (props.attr) {
  192 + method.setFieldsValue({ keys: props.attr });
  193 + const attrInfo = unref(deviceAttrs).find((item) => item.identifier === props.attr);
  194 + if (
  195 + [DataTypeEnum.STRING, DataTypeEnum.STRUCT].includes(
  196 + attrInfo?.detail.dataType.type as unknown as DataTypeEnum
  197 + )
  198 + ) {
  199 + mode.value = EnumTableChartMode.TABLE;
  200 + } else {
  201 + mode.value = EnumTableChartMode.CHART;
  202 + }
  203 + }
  204 + } catch (error) {}
  205 + };
  206 +
  207 + const openHistoryPanel = async (orderBy = OrderByEnum.ASC) => {
  208 + await nextTick();
  209 + method.updateSchema({
  210 + field: 'keys',
  211 + componentProps: {
  212 + options: unref(deviceAttrs).map((item) => ({ label: item.name, value: item.identifier })),
  213 + },
  214 + });
  215 +
  216 + method.setFieldsValue({
  217 + [SchemaFiled.START_TS]: 1 * 24 * 60 * 60 * 1000,
  218 + [SchemaFiled.LIMIT]: 7,
  219 + [SchemaFiled.AGG]: AggregateDataEnum.NONE,
  220 + });
  221 +
  222 + if (!hasDeviceAttr()) return;
  223 +
  224 + const keys = props.attr ? props.attr : unref(getDeviceKeys).join();
  225 +
  226 + const res = await getDeviceHistoryInfo(
  227 + {
  228 + entityId: props.deviceDetail?.id?.id,
  229 + keys,
  230 + startTs: Date.now() - 1 * 24 * 60 * 60 * 1000,
  231 + endTs: Date.now(),
  232 + agg: AggregateDataEnum.NONE,
  233 + limit: 7,
  234 + },
  235 + orderBy
  236 + );
  237 + historyData.value = getTableHistoryData(res);
  238 +
  239 + // 判断对象是否为空
  240 + if (!Object.keys(res).length) {
  241 + isNull.value = false;
  242 + return;
  243 + } else {
  244 + isNull.value = true;
  245 + }
  246 + const selectedKeys = unref(deviceAttrs).find((item) => item.identifier === props.attr);
  247 +
  248 + setOptions(setChartOptions(res, selectedKeys));
  249 + };
  250 +
  251 + const switchMode = async (flag: EnumTableChartMode) => {
  252 + mode.value = flag;
  253 + };
  254 +
  255 + onMounted(async () => {
  256 + await getDeviceDataKey();
  257 + await openHistoryPanel();
  258 + });
  259 +
  260 + onUnmounted(() => {
  261 + getInstance()?.clear();
  262 + });
  263 +</script>
  264 +
  265 +<template>
  266 + <section class="flex flex-col p-4 h-full" style="background-color: #f0f2f5">
  267 + <section class="bg-white p-3 mb-4">
  268 + <TimePeriodForm @register="register" />
  269 + </section>
  270 + <section class="bg-white p-3">
  271 + <Spin :spinning="loading" :absolute="true">
  272 + <div
  273 + v-show="mode === EnumTableChartMode.CHART"
  274 + class="flex h-70px items-center justify-end p-2"
  275 + >
  276 + <ModeSwitchButton
  277 + v-model:value="mode"
  278 + :mode="TABLE_CHART_MODE_LIST"
  279 + @change="switchMode"
  280 + />
  281 + </div>
  282 + <div
  283 + v-show="isNull && mode === EnumTableChartMode.CHART"
  284 + ref="chartRef"
  285 + :style="{ height: '400px', width: '100%' }"
  286 + >
  287 + </div>
  288 + <Empty v-show="!isNull && mode === EnumTableChartMode.CHART" />
  289 +
  290 + <BasicTable
  291 + @change="handleTableChange"
  292 + v-show="mode === EnumTableChartMode.TABLE"
  293 + @register="registerTable"
  294 + >
  295 + <template #toolbar>
  296 + <div class="flex h-70px items-center justify-end p-2">
  297 + <ModeSwitchButton
  298 + v-model:value="mode"
  299 + :mode="TABLE_CHART_MODE_LIST"
  300 + @change="switchMode"
  301 + />
  302 + </div>
  303 + </template>
  304 + </BasicTable>
  305 + </Spin>
  306 + </section>
  307 + </section>
  308 +</template>
  309 +
  310 +<style scoped></style>
... ...
  1 +import { DeviceModelOfMatterAttrs } from '/@/api/device/model/deviceModel';
  2 +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  3 +import { DataType, Specs } from '/@/api/device/model/modelOfMatterModel';
  4 +import { TCPProtocolTypeEnum, TransportTypeEnum } from '/@/enums/deviceEnum';
  5 +import { DataTypeEnum } from '/@/enums/objectModelEnum';
  6 +import { isArray } from '/@/utils/is';
  7 +
  8 +export interface StructValueItemType extends BaseAdditionalInfo {
  9 + name: string;
  10 + key: string;
  11 + value?: string | number | boolean;
  12 +}
  13 +
  14 +export interface BaseAdditionalInfo {
  15 + unit?: string;
  16 + boolClose?: string;
  17 + boolOpen?: string;
  18 + unitName?: string;
  19 +}
  20 +
  21 +export interface SocketInfoDataSourceItemType extends BaseAdditionalInfo {
  22 + accessMode: string;
  23 + key: string;
  24 + name: string;
  25 + type: DataTypeEnum;
  26 + detail: DeviceModelOfMatterAttrs;
  27 + time?: number;
  28 + value?: string | number | boolean | Record<string, StructValueItemType>;
  29 + expand?: boolean;
  30 + showHistoryDataButton?: boolean;
  31 + rawValue?: any;
  32 + enum?: Record<string, string>;
  33 +}
  34 +
  35 +export function buildTableDataSourceByObjectModel(
  36 + models: DeviceModelOfMatterAttrs[],
  37 + deviceDetail: EdgeDeviceItemType
  38 +): SocketInfoDataSourceItemType[] {
  39 + const isTCPTransportType =
  40 + deviceDetail.deviceData.transportConfiguration.type === TransportTypeEnum.TCP;
  41 +
  42 + const isModbusDevice =
  43 + isTCPTransportType &&
  44 + deviceDetail?.deviceProfile?.profileData?.transportConfiguration?.protocol ===
  45 + TCPProtocolTypeEnum.MODBUS_RTU;
  46 +
  47 + function getAdditionalInfoByDataType(dataType?: DataType) {
  48 + const { specs, specsList, type } = dataType || {};
  49 + if (isArray(specs)) return {};
  50 + const { unit, boolClose, boolOpen, unitName } = (specs as Partial<Specs>) || {};
  51 + const result = { unit, boolClose, boolOpen, unitName };
  52 + if ((type == DataTypeEnum.ENUM && specsList && specsList.length) || isModbusDevice) {
  53 + Reflect.set(
  54 + result,
  55 + 'enum',
  56 + (specsList || []).reduce((prev, next) => ({ ...prev, [next.value!]: next.name }), {})
  57 + );
  58 + }
  59 +
  60 + if (isModbusDevice) {
  61 + result.boolClose = '关';
  62 + result.boolOpen = '开';
  63 + }
  64 +
  65 + return result;
  66 + }
  67 +
  68 + return models.map((item) => {
  69 + const { accessMode, identifier, name, detail } = item;
  70 + const { dataType } = detail;
  71 + const { type, specs } = dataType || {};
  72 +
  73 + const res = {
  74 + accessMode,
  75 + name,
  76 + key: identifier,
  77 + type: type!,
  78 + detail: item,
  79 + };
  80 +
  81 + if (isArray(specs) && type === DataTypeEnum.STRUCT) {
  82 + const value: Record<string, StructValueItemType> = specs
  83 + .filter((item) => item.dataType?.type !== DataTypeEnum.STRUCT)
  84 + .reduce((prev, next) => {
  85 + const { identifier, functionName, dataType } = next;
  86 + return {
  87 + ...prev,
  88 + [identifier]: {
  89 + key: identifier!,
  90 + name: functionName!,
  91 + type: dataType?.type,
  92 + ...getAdditionalInfoByDataType(dataType),
  93 + },
  94 + };
  95 + }, {});
  96 +
  97 + Object.assign(res, { value });
  98 + } else {
  99 + Object.assign(res, getAdditionalInfoByDataType(dataType));
  100 + }
  101 + return res;
  102 + });
  103 +}
... ...
  1 +export { default as ObjectModelCommandDeliveryModal } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { ref, unref } from 'vue';
  3 + import { useGenerateFormSchemasByObjectModel } from './useGenerateFormSchemasByObjectModel';
  4 + import { ModalParamsType } from '/#/utils';
  5 + import { DeviceModelOfMatterAttrs } from '/@/api/device/model/deviceModel';
  6 + import { BasicForm, useForm } from '/@/components/Form';
  7 + import { BasicModal, useModalInner } from '/@/components/Modal';
  8 + import { CommandTypeEnum } from '/@/enums/deviceEnum';
  9 + import { DataTypeEnum, FunctionTypeEnum } from '/@/enums/objectModelEnum';
  10 + import { useCommandDelivery } from './useCommandDelivery';
  11 + import { useMessage } from '/@/hooks/web/useMessage';
  12 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  13 +
  14 + defineEmits(['register']);
  15 + const props = defineProps<{ deviceId: string; deviceName: string }>();
  16 +
  17 + const [registerForm, { setProps, getFieldsValue, resetFields, validate }] = useForm({
  18 + schemas: [],
  19 + showActionButtonGroup: false,
  20 + layout: 'vertical',
  21 + });
  22 +
  23 + const { getFormByObjectModel } = useGenerateFormSchemasByObjectModel();
  24 +
  25 + const currentParams = ref<{
  26 + deviceDetail: EdgeDeviceItemType;
  27 + objectModel: DeviceModelOfMatterAttrs;
  28 + }>();
  29 +
  30 + const [register] = useModalInner(
  31 + async (params: {
  32 + record: ModalParamsType<{
  33 + objectModel: DeviceModelOfMatterAttrs;
  34 + deviceDetail: EdgeDeviceItemType;
  35 + }>;
  36 + }) => {
  37 + const { record } = params;
  38 + currentParams.value = record as unknown as {
  39 + objectModel: DeviceModelOfMatterAttrs;
  40 + deviceDetail: EdgeDeviceItemType;
  41 + };
  42 + const { objectModel, deviceDetail } = record;
  43 + const schemas = getFormByObjectModel(objectModel, deviceDetail);
  44 + setProps({ schemas });
  45 + resetFields();
  46 + }
  47 + );
  48 +
  49 + const { createMessage } = useMessage();
  50 + const loading = ref(false);
  51 + const handleSend = async () => {
  52 + try {
  53 + loading.value = true;
  54 + if (!props.deviceId) return;
  55 +
  56 + await validate();
  57 + const { deviceDetail, objectModel } = unref(currentParams) || {};
  58 +
  59 + let value = getFieldsValue();
  60 + if (objectModel?.detail.dataType.type !== DataTypeEnum.STRUCT) {
  61 + value = value[objectModel!.identifier];
  62 + }
  63 +
  64 + const { doCommandDelivery } = useCommandDelivery();
  65 +
  66 + await doCommandDelivery({
  67 + deviceDetail,
  68 + objectModel: {
  69 + ...(objectModel || {}),
  70 + functionName: objectModel!.name,
  71 + identifier: objectModel!.identifier,
  72 + functionType: FunctionTypeEnum.PROPERTIES,
  73 + specs: objectModel?.detail,
  74 + },
  75 + cmdType: CommandTypeEnum.ATTRIBUTE,
  76 + value,
  77 + });
  78 +
  79 + createMessage.success('属性下发成功');
  80 + } catch (error) {
  81 + throw error;
  82 + } finally {
  83 + loading.value = false;
  84 + }
  85 + };
  86 +</script>
  87 +
  88 +<template>
  89 + <BasicModal
  90 + title="属性下发"
  91 + @register="register"
  92 + :min-height="60"
  93 + ok-text="属性下发"
  94 + wrap-class-name="model-of-matter-send-command-modal"
  95 + @ok="handleSend"
  96 + :ok-button-props="{ loading }"
  97 + >
  98 + <BasicForm @register="registerForm" class="dynamic-form" />
  99 + </BasicModal>
  100 +</template>
  101 +
  102 +<style lang="less">
  103 + .model-of-matter-send-command-modal {
  104 + .dynamic-form {
  105 + .ant-input-number {
  106 + @apply !w-full;
  107 + }
  108 + }
  109 + }
  110 +</style>
... ...
  1 +import { ref } from 'vue';
  2 +import { commandIssuanceApi, getDeviceDetail } from '/@/api/device/deviceManager';
  3 +import { RpcCommandType } from '/@/api/device/model/deviceConfigModel';
  4 +import { DeviceProfileModel } from '/@/api/device/model/deviceModel';
  5 +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  6 +import { Tsl } from '/@/api/device/model/modelOfMatterModel';
  7 +import { getModelTsl } from '/@/api/device/modelOfMatter';
  8 +import {
  9 + CommandDeliveryWayEnum,
  10 + CommandTypeEnum,
  11 + RPCCommandMethodEnum,
  12 + ServiceCallTypeEnum,
  13 + TCPProtocolTypeEnum,
  14 + TransportTypeEnum,
  15 +} from '/@/enums/deviceEnum';
  16 +import { FunctionTypeEnum } from '/@/enums/objectModelEnum';
  17 +import { isFunction, isNullOrUnDef } from '/@/utils/is';
  18 +import { getDeviceActiveTime } from '/@/api/alarm/position';
  19 +import { useMessage } from '/@/hooks/web/useMessage';
  20 +import { useCoverModbusCommand } from './useCoverModbusCommand';
  21 +
  22 +interface SetupType {
  23 + entityId: string;
  24 + transportType: TransportTypeEnum;
  25 + isTCPModbus: boolean;
  26 + deviceCode: string | undefined;
  27 + deviceDetail: EdgeDeviceItemType;
  28 + objectModel: Tsl | undefined;
  29 + identifier: string;
  30 +}
  31 +
  32 +interface DoCommandDeliverParamsType {
  33 + value: any;
  34 + deviceDetail?: EdgeDeviceItemType;
  35 + deviceId?: string;
  36 + identifier?: string;
  37 + objectModel?: Tsl;
  38 + deviceProfileId?: string;
  39 + deviceProfileDetail?: DeviceProfileModel;
  40 + way?: CommandDeliveryWayEnum;
  41 + cmdType?: CommandTypeEnum;
  42 + transportType?: TransportTypeEnum;
  43 + beforeFetch?: (
  44 + rpcCommand: RpcCommandType,
  45 + setup: SetupType
  46 + ) =>
  47 + | { rpcCommand: RpcCommandType; way?: CommandDeliveryWayEnum }
  48 + | Promise<{ rpcCommand: RpcCommandType; way?: CommandDeliveryWayEnum }>;
  49 +}
  50 +
  51 +export function useCommandDelivery() {
  52 + const loading = ref(false);
  53 + async function doSetup(params: DoCommandDeliverParamsType) {
  54 + let { deviceDetail, identifier, deviceProfileId, objectModel, transportType } = params;
  55 + const { deviceId } = params;
  56 +
  57 + const entityId = deviceId || deviceDetail?.id?.id;
  58 + if (!entityId) {
  59 + throw new Error('not found entityId');
  60 + }
  61 +
  62 + identifier = identifier || objectModel?.identifier;
  63 +
  64 + if (!identifier) {
  65 + throw new Error('not found identifier');
  66 + }
  67 +
  68 + transportType = transportType || (deviceDetail?.transportType as TransportTypeEnum);
  69 +
  70 + if (
  71 + !transportType ||
  72 + (transportType === TransportTypeEnum.TCP && !deviceDetail) ||
  73 + !deviceDetail?.deviceProfile
  74 + ) {
  75 + deviceDetail = (await getDeviceDetail(entityId)) as any as EdgeDeviceItemType;
  76 + transportType = deviceDetail?.transportType as TransportTypeEnum;
  77 + }
  78 +
  79 + const isTCPModbus =
  80 + transportType === TransportTypeEnum.TCP &&
  81 + deviceDetail?.deviceProfile?.profileData?.transportConfiguration?.protocol ===
  82 + TCPProtocolTypeEnum.MODBUS_RTU;
  83 +
  84 + if (isTCPModbus && !objectModel?.extensionDesc) {
  85 + // deviceProfileId = deviceDetail?.deviceProfileId || deviceProfileId || deviceProfileDetail?.id;
  86 + deviceProfileId = deviceDetail?.deviceProfileId?.id;
  87 + if (!deviceProfileId) {
  88 + throw new Error('not found deviceProfile');
  89 + }
  90 +
  91 + const objectModels = await getModelTsl({ deviceProfileId });
  92 + objectModel = objectModels.find((item) => item.identifier === identifier);
  93 + }
  94 +
  95 + const deviceCode = deviceDetail?.code;
  96 +
  97 + return {
  98 + entityId,
  99 + transportType,
  100 + isTCPModbus,
  101 + deviceCode,
  102 + deviceDetail,
  103 + objectModel,
  104 + identifier,
  105 + };
  106 + }
  107 +
  108 + async function doCommandDelivery(params: DoCommandDeliverParamsType) {
  109 + try {
  110 + loading.value = true;
  111 +
  112 + const setupResult = await doSetup(params);
  113 +
  114 + const { entityId, transportType, isTCPModbus, deviceCode, objectModel, identifier } =
  115 + setupResult;
  116 +
  117 + let command = params.value;
  118 +
  119 + if (transportType === TransportTypeEnum.TCP) {
  120 + command = params.value;
  121 + if (isTCPModbus) {
  122 + const { doCoverCommand } = useCoverModbusCommand();
  123 + command = await doCoverCommand(params.value, objectModel!, deviceCode, entityId);
  124 + }
  125 + } else {
  126 + command = {
  127 + [identifier]: command,
  128 + };
  129 + }
  130 +
  131 + let rpcCommand: RpcCommandType = {
  132 + persistent: true,
  133 + method: RPCCommandMethodEnum.THINGSKIT,
  134 + additionalInfo: {
  135 + cmdType: params.cmdType ?? CommandTypeEnum.API,
  136 + },
  137 + params: command,
  138 + };
  139 +
  140 + let way = params.way ?? CommandDeliveryWayEnum.ONE_WAY;
  141 +
  142 + if (objectModel?.functionType === FunctionTypeEnum.SERVICE) {
  143 + rpcCommand.additionalInfo.cmdType = CommandTypeEnum.SERVICE;
  144 + way =
  145 + objectModel.callType === ServiceCallTypeEnum.ASYNC
  146 + ? CommandDeliveryWayEnum.ONE_WAY
  147 + : CommandDeliveryWayEnum.TWO_WAY;
  148 + }
  149 +
  150 + const sendApi = commandIssuanceApi;
  151 +
  152 + if (params.beforeFetch && isFunction(params.beforeFetch)) {
  153 + const { rpcCommand: _rpcCommand, way: _way } = await params.beforeFetch(
  154 + rpcCommand,
  155 + setupResult
  156 + );
  157 + !isNullOrUnDef(rpcCommand) && (rpcCommand = _rpcCommand);
  158 + if (_way) way = _way;
  159 + }
  160 +
  161 + if (way === CommandDeliveryWayEnum.TWO_WAY) {
  162 + const result = await getDeviceActiveTime(entityId);
  163 + const [firsetItem] = result || [];
  164 +
  165 + if (!firsetItem.value) {
  166 + const { createMessage } = useMessage();
  167 + const message = '当前设备不在线';
  168 + createMessage.warning(message);
  169 + throw Error(message);
  170 + }
  171 + }
  172 + await sendApi(way, entityId, rpcCommand);
  173 + } finally {
  174 + loading.value = false;
  175 + }
  176 + }
  177 +
  178 + return {
  179 + loading,
  180 + doCommandDelivery,
  181 + };
  182 +}
... ...
  1 +import { useParseOriginalDataType } from './useParseOriginalDataType';
  2 +import { getDeviceHistoryInfo } from '/@/api/alarm/position';
  3 +import { getDeviceDetail } from '/@/api/device/deviceManager';
  4 +import { ExtensionDesc, Specs, Tsl } from '/@/api/device/model/modelOfMatterModel';
  5 +import { genModbusCommand } from '/@/api/task';
  6 +import { GenModbusCommandType } from '/@/api/task/model';
  7 +import { ModbusCRCEnum, OriginalDataTypeEnum } from '/@/enums/objectModelEnum';
  8 +import { useBaseConversion } from '/@/hooks/business/useBaseConversion';
  9 +import { useMessage } from '/@/hooks/web/useMessage';
  10 +import { isNullOrUnDef } from '/@/utils/is';
  11 +import {
  12 + isFloatType,
  13 + isNumberType,
  14 + useParseOperationType,
  15 +} from '/@/views/device/profiles/components/ObjectModelForm/ExtendDesc/useParseOperationType';
  16 +
  17 +const getFloatPart = (number: string | number) => {
  18 + const isLessZero = Number(number) < 0;
  19 + number = number.toString();
  20 + const floatPartStartIndex = number.indexOf('.');
  21 + const value = ~floatPartStartIndex
  22 + ? `${isLessZero ? '-' : ''}0.${number.substring(floatPartStartIndex + 1)}`
  23 + : '0';
  24 + return Number(value);
  25 +};
  26 +
  27 +function getValueFromValueRange(value: number, valueRange?: Record<'min' | 'max', number>) {
  28 + const { min, max } = valueRange || {};
  29 + if (!isNullOrUnDef(min) && value < min) return min;
  30 + if (!isNullOrUnDef(max) && value > max) return max;
  31 + return value;
  32 +}
  33 +
  34 +async function getCurrentBitCommand(entityId: string, objectModel: Tsl, value: number) {
  35 + const deviceDetail = await getDeviceDetail(entityId);
  36 + const thingsModels = deviceDetail.deviceProfile.profileData.thingsModel;
  37 +
  38 + const { registerAddress } = objectModel.extensionDesc || {};
  39 +
  40 + const bitsModel = thingsModels?.filter(
  41 + (item) =>
  42 + item.extensionDesc?.originalDataType === OriginalDataTypeEnum.BITS &&
  43 + item.extensionDesc.registerAddress === registerAddress
  44 + );
  45 +
  46 + const valuePositionMap =
  47 + bitsModel?.reduce((prev, next) => {
  48 + return { ...prev, [next.identifier]: next.extensionDesc?.bitMask };
  49 + }, {} as Record<string, number>) || {};
  50 +
  51 + const attrKeys = Object.keys(valuePositionMap);
  52 +
  53 + const latestBitsValues = await getDeviceHistoryInfo({ entityId, keys: attrKeys.join(',') });
  54 +
  55 + const binaryArr = Array.from({ length: 16 }, () => 0);
  56 +
  57 + for (const key of attrKeys) {
  58 + const index = valuePositionMap[key];
  59 +
  60 + if (!isNullOrUnDef(index)) {
  61 + const [latest] = latestBitsValues[key];
  62 + const { value } = latest;
  63 + binaryArr[index] = Number(value);
  64 + }
  65 + }
  66 +
  67 + if (objectModel.extensionDesc?.bitMask) {
  68 + binaryArr[objectModel.extensionDesc.bitMask] = value;
  69 + }
  70 +
  71 + return [parseInt(binaryArr.reverse().join(''), 2)];
  72 +}
  73 +
  74 +export function useCoverModbusCommand() {
  75 + const { createMessage } = useMessage();
  76 +
  77 + const doCoverCommand = async (
  78 + value: number,
  79 + objectModel: Tsl,
  80 + deviceAddressCode?: string,
  81 + entityId?: string
  82 + ) => {
  83 + if (!deviceAddressCode) {
  84 + const message = '当前设备未绑定设备地址码';
  85 + createMessage.warning(message);
  86 + throw new Error(message);
  87 + }
  88 +
  89 + const {
  90 + registerAddress,
  91 + operationType,
  92 + scaling,
  93 + originalDataType,
  94 + bitMask,
  95 + registerCount: registerNumber,
  96 + } = objectModel.extensionDesc as Required<ExtensionDesc>;
  97 +
  98 + const { writeRegisterAddress } = useParseOperationType(operationType);
  99 + const { unsigned, exchangeSortFlag, registerCount } =
  100 + useParseOriginalDataType(originalDataType);
  101 +
  102 + const params: GenModbusCommandType = {
  103 + crc: ModbusCRCEnum.CRC_16_LOWER,
  104 + registerNumber: registerCount || registerNumber,
  105 + deviceCode: deviceAddressCode,
  106 + registerAddress: parseInt(registerAddress, 16),
  107 + method: writeRegisterAddress!,
  108 + registerValues: [value],
  109 + };
  110 +
  111 + if (exchangeSortFlag) params.hexByteOrderEnum = exchangeSortFlag;
  112 +
  113 + const { getRegisterValueByOriginalDataType } = useBaseConversion();
  114 +
  115 + if (isNumberType(originalDataType)) {
  116 + let newValue = Math.trunc(value) * scaling + getFloatPart(value) * scaling;
  117 +
  118 + newValue = unsigned ? newValue : Math.abs(newValue);
  119 +
  120 + newValue = getValueFromValueRange(
  121 + newValue,
  122 + (objectModel.specs?.dataType.specs as Specs).valueRange
  123 + );
  124 +
  125 + if (!isFloatType(originalDataType) && newValue % 1 !== 0) {
  126 + const message = `属性下发类型必须是整数,缩放因子为${scaling}`;
  127 + createMessage.warning(message);
  128 + throw Error(message);
  129 + }
  130 +
  131 + value = newValue;
  132 + }
  133 +
  134 + params.registerValues =
  135 + originalDataType === OriginalDataTypeEnum.BITS
  136 + ? await getCurrentBitCommand(entityId!, objectModel, value)
  137 + : getRegisterValueByOriginalDataType(value, originalDataType, {
  138 + bitMask,
  139 + registerNumber,
  140 + });
  141 +
  142 + if (!params.method) {
  143 + const message = '物模型操作类型无法进行写入';
  144 + createMessage.warning(message);
  145 + throw Error(message);
  146 + }
  147 +
  148 + return await genModbusCommand(params);
  149 + };
  150 +
  151 + return {
  152 + doCoverCommand,
  153 + };
  154 +}
... ...
  1 +import { unref } from 'vue';
  2 +import { DeviceModelOfMatterAttrs } from '/@/api/device/model/deviceModel';
  3 +import { DataType, Specs, StructJSON } from '/@/api/device/model/modelOfMatterModel';
  4 +import { FormSchema } from '/@/components/Form';
  5 +import { DataTypeEnum, OriginalDataTypeEnum } from '/@/enums/objectModelEnum';
  6 +import { TCPProtocolTypeEnum } from '/@/enums/deviceEnum';
  7 +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  8 +
  9 +export interface BasicCreateFormParams {
  10 + identifier: string;
  11 + functionName: string;
  12 + dataType: DataType;
  13 +}
  14 +
  15 +const validateDouble = (value: number, min?: number | string, max?: number | string) => {
  16 + min = Number(min) ?? Number.MIN_SAFE_INTEGER;
  17 + max = Number(max) ?? Number.MAX_SAFE_INTEGER;
  18 +
  19 + return {
  20 + flag: value < min || value > max,
  21 + message: `取值范围在${min}~${max}之间`,
  22 + };
  23 +};
  24 +
  25 +export const useGenerateFormSchemasByObjectModel = () => {
  26 + const createInputNumber = ({
  27 + identifier,
  28 + functionName,
  29 + dataType,
  30 + }: BasicCreateFormParams): FormSchema => {
  31 + const { specs, type } = dataType;
  32 + const { valueRange, step } = specs! as Partial<Specs>;
  33 + const { max, min } = valueRange || {};
  34 + return {
  35 + field: identifier,
  36 + label: functionName,
  37 + component: 'InputNumber',
  38 + rules: [
  39 + {
  40 + type: 'number',
  41 + trigger: 'change',
  42 + validator: (_rule, value) => {
  43 + const { flag, message } = validateDouble(value, min, max);
  44 + if (flag) {
  45 + return Promise.reject(`${functionName}${message}`);
  46 + }
  47 + return Promise.resolve(value);
  48 + },
  49 + },
  50 + ],
  51 + componentProps: {
  52 + max: max ?? Number.MAX_SAFE_INTEGER,
  53 + min: min ?? Number.MIN_SAFE_INTEGER,
  54 + step,
  55 + placeholder: `请输入${functionName}`,
  56 + precision: type === DataTypeEnum.NUMBER_INT ? 0 : 2,
  57 + },
  58 + } as FormSchema;
  59 + };
  60 +
  61 + const createInput = ({
  62 + identifier,
  63 + functionName,
  64 + dataType,
  65 + }: BasicCreateFormParams): FormSchema => {
  66 + const { specs } = dataType;
  67 + const { length = 10240 } = specs! as Partial<Specs>;
  68 + return {
  69 + field: identifier,
  70 + label: functionName,
  71 + component: 'Input',
  72 + rules: [
  73 + {
  74 + type: 'string',
  75 + trigger: 'change',
  76 + validator: (_rule, value) => {
  77 + if (value?.length > length) {
  78 + return Promise.reject(`${functionName}数据长度应该小于${length}`);
  79 + }
  80 + return Promise.resolve(value);
  81 + },
  82 + },
  83 + ],
  84 + componentProps: {
  85 + maxLength: length,
  86 + placeholder: `请输入${functionName}`,
  87 + },
  88 + } as FormSchema;
  89 + };
  90 +
  91 + const createSelect = ({
  92 + identifier,
  93 + functionName,
  94 + dataType,
  95 + }: BasicCreateFormParams): FormSchema => {
  96 + const { specs } = dataType;
  97 + const { boolClose, boolOpen } = specs! as Partial<Specs>;
  98 + return {
  99 + field: identifier,
  100 + label: functionName,
  101 + component: 'Select',
  102 + componentProps: {
  103 + options: [
  104 + { label: `${boolClose}-0`, value: 0 },
  105 + { label: `${boolOpen}-1`, value: 1 },
  106 + ],
  107 + placeholder: `请选择${functionName}`,
  108 + getPopupContainer: () => document.body,
  109 + },
  110 + };
  111 + };
  112 +
  113 + const createEnumSelect = ({
  114 + identifier,
  115 + functionName,
  116 + dataType,
  117 + }: BasicCreateFormParams): FormSchema => {
  118 + const { specsList } = dataType;
  119 + return {
  120 + field: identifier,
  121 + label: functionName,
  122 + component: 'Select',
  123 + componentProps: {
  124 + options: specsList?.map((item) => ({ label: item.name, value: item.value })),
  125 + placeholder: `请选择${functionName}`,
  126 + getPopupContainer: () => document.body,
  127 + },
  128 + };
  129 + };
  130 +
  131 + const createModbusValueInput = (objectModel: DeviceModelOfMatterAttrs): FormSchema => {
  132 + const { identifier, name, detail, extensionDesc } = objectModel;
  133 +
  134 + const { dataType } = detail || {};
  135 + const { specs } = dataType || {};
  136 + const { valueRange } = specs as Specs;
  137 + const { max, min } = valueRange || {};
  138 +
  139 + if (extensionDesc?.originalDataType === OriginalDataTypeEnum.BOOLEAN) {
  140 + const options = [
  141 + { label: '闭合', value: parseInt('FF00', 16) },
  142 + { label: '断开', value: parseInt('0000', 16) },
  143 + ];
  144 +
  145 + return {
  146 + field: identifier,
  147 + label: name,
  148 + component: 'Select',
  149 + componentProps: () => {
  150 + return {
  151 + options,
  152 + placeholder: `请选择${name}`,
  153 + getPopupContainer: () => document.body,
  154 + };
  155 + },
  156 + };
  157 + }
  158 +
  159 + const isStringType = extensionDesc?.originalDataType === OriginalDataTypeEnum.STRING;
  160 + return {
  161 + field: identifier,
  162 + label: name,
  163 + component: isStringType ? 'Input' : 'InputNumber',
  164 + rules: isStringType
  165 + ? []
  166 + : [
  167 + {
  168 + type: 'number',
  169 + validator: (_rule, value) => {
  170 + const { flag, message } = validateDouble(value, min, max);
  171 + if (flag) {
  172 + return Promise.reject(`${name}${message}`);
  173 + }
  174 + return Promise.resolve(value);
  175 + },
  176 + },
  177 + ],
  178 + componentProps: {
  179 + max: max ?? Number.MAX_SAFE_INTEGER,
  180 + min: min ?? Number.MIN_SAFE_INTEGER,
  181 + placeholder: `请输入${name}`,
  182 + // precision: floatType.includes(extensionDesc!.originalDataType) ? 2 : 0,
  183 + },
  184 + };
  185 + };
  186 +
  187 + const schemaMethod = {
  188 + [DataTypeEnum.BOOL]: createSelect,
  189 + [DataTypeEnum.NUMBER_DOUBLE]: createInputNumber,
  190 + [DataTypeEnum.NUMBER_INT]: createInputNumber,
  191 + [DataTypeEnum.STRING]: createInput,
  192 + [DataTypeEnum.ENUM]: createEnumSelect,
  193 + };
  194 +
  195 + const getFormByObjectModel = (
  196 + objectModel: DeviceModelOfMatterAttrs,
  197 + deviceDetail: EdgeDeviceItemType
  198 + ): FormSchema[] => {
  199 + const { name, identifier, detail } = objectModel;
  200 +
  201 + const isTCPModbusProduct =
  202 + unref(deviceDetail).deviceProfile?.profileData?.transportConfiguration?.protocol ===
  203 + TCPProtocolTypeEnum.MODBUS_RTU;
  204 +
  205 + const { dataType } = detail;
  206 + const { type } = dataType || {};
  207 +
  208 + if (isTCPModbusProduct) {
  209 + return [createModbusValueInput(objectModel)];
  210 + }
  211 +
  212 + if (type === DataTypeEnum.STRUCT) {
  213 + return (dataType?.specs as StructJSON[]).map((item) => {
  214 + const { functionName, identifier, dataType } = item;
  215 + const { type } = dataType || {};
  216 +
  217 + return schemaMethod[type!]?.({ identifier, functionName, dataType });
  218 + });
  219 + }
  220 +
  221 + const result = schemaMethod[type!]?.({ identifier, functionName: name, dataType: dataType! });
  222 + return result ? [result] : [];
  223 + };
  224 +
  225 + return { getFormByObjectModel };
  226 +};
... ...
  1 +import { OriginalDataTypeEnum } from '/@/enums/objectModelEnum';
  2 +
  3 +export type OriginalDataTypePrefixType<S = `${OriginalDataTypeEnum}`> = S extends string
  4 + ? S extends `${infer D}_${string}`
  5 + ? D
  6 + : S
  7 + : '';
  8 +
  9 +function getRegisterCount(originalDataType: OriginalDataTypeEnum) {
  10 + switch (originalDataType) {
  11 + case OriginalDataTypeEnum.INT16_AB:
  12 + case OriginalDataTypeEnum.INT16_BA:
  13 + case OriginalDataTypeEnum.UINT16_AB:
  14 + case OriginalDataTypeEnum.UINT16_BA:
  15 + case OriginalDataTypeEnum.BITS:
  16 + case OriginalDataTypeEnum.BOOLEAN:
  17 + return 1;
  18 +
  19 + case OriginalDataTypeEnum.INT32_AB_CD:
  20 + case OriginalDataTypeEnum.INT32_BA_DC:
  21 + case OriginalDataTypeEnum.INT32_CD_AB:
  22 + case OriginalDataTypeEnum.INT32_DC_BA:
  23 + case OriginalDataTypeEnum.UINT32_AB_CD:
  24 + case OriginalDataTypeEnum.UINT32_BA_DC:
  25 + case OriginalDataTypeEnum.UINT32_CD_AB:
  26 + case OriginalDataTypeEnum.UINT32_DC_BA:
  27 + return 2;
  28 +
  29 + case OriginalDataTypeEnum.FLOAT_AB_CD:
  30 + case OriginalDataTypeEnum.FLOAT_BA_DC:
  31 + case OriginalDataTypeEnum.FLOAT_CD_AB:
  32 + case OriginalDataTypeEnum.FLOAT_DC_BA:
  33 + return 2;
  34 +
  35 + case OriginalDataTypeEnum.DOUBLE:
  36 + return 4;
  37 + }
  38 +}
  39 +
  40 +export function useParseOriginalDataType(originalDataType: OriginalDataTypeEnum) {
  41 + const signedMatchRef = /^UN/;
  42 +
  43 + const splitArray = originalDataType.split('_') as [OriginalDataTypePrefixType];
  44 +
  45 + const [dataType] = splitArray;
  46 +
  47 + const exchangeSortFlag = splitArray.slice(1).join('_');
  48 +
  49 + return {
  50 + registerCount: getRegisterCount(originalDataType),
  51 + unsigned: !signedMatchRef.test(originalDataType),
  52 + dataType,
  53 + exchangeSortFlag: exchangeSortFlag || null,
  54 + };
  55 +}
... ...
  1 +<script lang="ts">
  2 + import { defineComponent, reactive } from 'vue';
  3 + import { BasicTable, useTable } from '/@/components/Table';
  4 + import { realTimeDataColumns } from './config';
  5 + import { useWebSocket } from '@vueuse/core';
  6 + import { JWT_TOKEN_KEY } from '/@/enums/cacheEnum';
  7 + import { getAuthCache } from '/@/utils/auth';
  8 + import { useMessage } from '/@/hooks/web/useMessage';
  9 + import type { socketDataType } from './types';
  10 + import { useGlobSetting } from '/@/hooks/setting';
  11 +
  12 + export default defineComponent({
  13 + name: 'RealTimeData',
  14 + components: {
  15 + BasicTable,
  16 + },
  17 + props: {
  18 + deviceDetail: {
  19 + type: Object,
  20 + required: true,
  21 + },
  22 + },
  23 + setup(props) {
  24 + const token: string = getAuthCache(JWT_TOKEN_KEY);
  25 + const { socketUrl } = useGlobSetting();
  26 + const state: any = reactive({
  27 + server: `${socketUrl}${token}`,
  28 + sendValue: JSON.stringify({
  29 + attrSubCmds: [],
  30 + tsSubCmds: [
  31 + {
  32 + entityType: 'DEVICE',
  33 + entityId: props.deviceDetail.tbDeviceId,
  34 + scope: 'LATEST_TELEMETRY',
  35 + cmdId: 1,
  36 + },
  37 + ],
  38 + historyCmds: [],
  39 + entityDataCmds: [],
  40 + entityDataUnsubscribeCmds: [],
  41 + alarmDataCmds: [],
  42 + alarmDataUnsubscribeCmds: [],
  43 + entityCountCmds: [],
  44 + entityCountUnsubscribeCmds: [],
  45 + }),
  46 + recordList: Array<socketDataType>(),
  47 + });
  48 + const { createMessage } = useMessage();
  49 + const [registerTable, { getForm, setTableData }] = useTable({
  50 + columns: realTimeDataColumns,
  51 + showTableSetting: true,
  52 + bordered: true,
  53 + showIndexColumn: false,
  54 + dataSource: state.recordList,
  55 + useSearchForm: true,
  56 + formConfig: {
  57 + labelWidth: 100,
  58 + schemas: [
  59 + {
  60 + field: 'key',
  61 + label: '键/值',
  62 + component: 'Input',
  63 + colProps: { span: 6 },
  64 + componentProps: {
  65 + placeholder: '请输入 键/值',
  66 + maxLength: 255,
  67 + },
  68 + },
  69 + ],
  70 + // 自定义前端查询
  71 + submitFunc() {
  72 + const { getFieldsValue } = getForm();
  73 + const { key } = getFieldsValue();
  74 + if (!key) {
  75 + setTableData(state.recordList);
  76 + return;
  77 + }
  78 + let newRecordList = [];
  79 + let len = state.recordList.length;
  80 + for (let i = 0; i < len; i++) {
  81 + if (
  82 + state.recordList[i].key.indexOf(key) >= 0 ||
  83 + state.recordList[i].value.indexOf(key) >= 0
  84 + ) {
  85 + newRecordList.push(state.recordList[i] as any as never);
  86 + }
  87 + }
  88 + setTableData(newRecordList);
  89 + },
  90 + resetFunc() {
  91 + setTableData(state.recordList);
  92 + },
  93 + },
  94 + });
  95 +
  96 + const { send, close } = useWebSocket(state.server, {
  97 + onConnected() {
  98 + send(state.sendValue);
  99 + },
  100 + onMessage(_, e) {
  101 + const { data } = JSON.parse(e.data);
  102 + const newArray: socketDataType[] = [];
  103 + for (const key in data) {
  104 + const [time, value] = data[key].flat(1);
  105 + let obj = {
  106 + key,
  107 + time,
  108 + value,
  109 + };
  110 + if (state.recordList.length === 0) {
  111 + state.recordList.unshift(obj);
  112 + } else {
  113 + newArray.push(obj);
  114 + }
  115 + }
  116 + newArray.forEach((item) => {
  117 + let flag = false;
  118 + state.recordList.forEach((item1) => {
  119 + if (item1.key === item.key) {
  120 + item1.value = item.value;
  121 + item1.time = item.time;
  122 + flag = true;
  123 + }
  124 + });
  125 + if (!flag) {
  126 + state.recordList.unshift(item);
  127 + }
  128 + });
  129 + },
  130 + onDisconnected() {
  131 + close();
  132 + },
  133 + onError() {
  134 + createMessage.error('webSocket连接超时,请联系管理员');
  135 + },
  136 + });
  137 + return {
  138 + registerTable,
  139 + };
  140 + },
  141 + });
  142 +</script>
  143 +
  144 +<template>
  145 + <div style="background-color: #f0f2f5">
  146 + <BasicTable @register="registerTable" />
  147 + </div>
  148 +</template>
... ...
  1 +import { BasicColumn } from '/@/components/Table';
  2 +import { formatToDateTime } from '/@/utils/dateUtil';
  3 +import { isNullOrUnDef } from '/@/utils/is';
  4 +
  5 +// 实时数据表格
  6 +export const realTimeDataColumns: BasicColumn[] = [
  7 + {
  8 + title: '键',
  9 + dataIndex: 'name',
  10 + width: 100,
  11 + },
  12 + {
  13 + title: '值',
  14 + dataIndex: 'rawValue',
  15 + width: 160,
  16 + format(text) {
  17 + return isNullOrUnDef(text) ? '--' : text;
  18 + },
  19 + },
  20 + {
  21 + title: '最后更新时间',
  22 + dataIndex: 'time',
  23 + width: 120,
  24 + format: (text) => formatToDateTime(text, 'YYYY-MM-DD HH:mm:ss'),
  25 + },
  26 +];
... ...
  1 +import { EChartsOption } from 'echarts';
  2 +import moment from 'moment';
  3 +import { computed, ref, unref } from 'vue';
  4 +import { HistoryData } from '/@/api/alarm/position/model';
  5 +import { getDeviceAttributes } from '/@/api/dataBoard';
  6 +import { DeviceAttributeRecord } from '/@/api/dataBoard/model';
  7 +import { dateUtil } from '/@/utils/dateUtil';
  8 +import { isArray } from '/@/utils/is';
  9 +import { DEFAULT_DATE_FORMAT } from '/@/views/visual/board/detail/config/util';
  10 +import { QueryWay, SchemaFiled } from '/@/views/visual/palette/components/HistoryTrendModal/config';
  11 +import { eChartOptions } from '/@/views/device/localtion/config.data';
  12 +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  13 +
  14 +// interface DeviceOption {
  15 +// deviceProfileId: string;
  16 +// }
  17 +
  18 +export function useHistoryData() {
  19 + const deviceAttrs = ref<DeviceAttributeRecord[]>([]);
  20 +
  21 + const getDeviceKeys = computed(() => {
  22 + return unref(deviceAttrs).map((item) => item.identifier);
  23 + });
  24 +
  25 + const getDeviceAttribute = async (record: EdgeDeviceItemType) => {
  26 + try {
  27 + const { deviceProfileId } = record;
  28 + const { id } = deviceProfileId;
  29 + const list = (await getDeviceAttributes({ deviceProfileId: id })) || [];
  30 + deviceAttrs.value = isArray(list) ? list : [];
  31 + } catch (error) {
  32 + throw error;
  33 + }
  34 + };
  35 +
  36 + function getSearchParams(value: Partial<Record<SchemaFiled, string>>) {
  37 + const { startTs, endTs, interval, agg, limit, keys, way, deviceId, orderBy } = value;
  38 + const basicRecord = {
  39 + entityId: deviceId,
  40 + keys: keys ? keys : unref(getDeviceKeys).join(),
  41 + interval,
  42 + agg,
  43 + limit,
  44 + orderBy,
  45 + };
  46 + if (way === QueryWay.LATEST) {
  47 + return Object.assign(basicRecord, {
  48 + startTs: moment().subtract(startTs, 'ms').valueOf(),
  49 + endTs: Date.now(),
  50 + });
  51 + } else {
  52 + return Object.assign(basicRecord, {
  53 + startTs: moment(startTs).valueOf(),
  54 + endTs: moment(endTs).valueOf(),
  55 + });
  56 + }
  57 + }
  58 +
  59 + function setChartOptions(
  60 + data: HistoryData,
  61 + keys?: DeviceAttributeRecord | DeviceAttributeRecord[]
  62 + ) {
  63 + const dataArray: [string, string, string][] = [];
  64 + for (const key in data) {
  65 + for (const item of data[key]) {
  66 + let { value } = item;
  67 + const { ts } = item;
  68 + const time = dateUtil(ts).format(DEFAULT_DATE_FORMAT);
  69 + value = Number(value).toFixed(2);
  70 + dataArray.push([time, value, key as string]);
  71 + }
  72 + }
  73 +
  74 + keys = keys ? [keys as DeviceAttributeRecord] : unref(deviceAttrs);
  75 + const legend = keys.map((item) => item.name);
  76 +
  77 + const series: EChartsOption['series'] = (keys as DeviceAttributeRecord[]).map((item) => {
  78 + return {
  79 + name: item.name,
  80 + type: 'line',
  81 + data: dataArray.filter((temp) => temp[2] === item.identifier),
  82 + };
  83 + });
  84 +
  85 + return eChartOptions(series, legend);
  86 + }
  87 +
  88 + return {
  89 + deviceAttrs,
  90 + getDeviceKeys,
  91 + getDeviceAttribute,
  92 + getSearchParams,
  93 + setChartOptions,
  94 + };
  95 +}
... ...
  1 +export { default as EdgeDevicePhysicalModel } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { nextTick, onMounted, onUnmounted, reactive, ref, unref, computed } from 'vue';
  3 + import { List, Card, Tooltip, Space, PaginationProps } from 'ant-design-vue';
  4 + import { PageWrapper } from '/@/components/Page';
  5 + import { BasicTable, useTable } from '/@/components/Table';
  6 + import { realTimeDataColumns } from './config';
  7 + import { useWebSocket } from '@vueuse/core';
  8 + import { getAuthCache } from '/@/utils/auth';
  9 + import { JWT_TOKEN_KEY } from '/@/enums/cacheEnum';
  10 + import { useMessage } from '/@/hooks/web/useMessage';
  11 + import { formatToDateTime } from '/@/utils/dateUtil';
  12 + import { BasicForm, useForm } from '/@/components/Form';
  13 + import HistoryData from './HistoryData.vue';
  14 + import { BasicModal, useModal } from '/@/components/Modal';
  15 + import { getDeviceAttrs } from '/@/api/device/deviceManager';
  16 + import { isArray, isNull, isNullOrUnDef, isObject } from '/@/utils/is';
  17 + import { useGlobSetting } from '/@/hooks/setting';
  18 + import { ModeSwitchButton, EnumTableCardMode } from '/@/components/Widget';
  19 + import { toRaw } from 'vue';
  20 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  21 + import { ReadAndWriteEnum, TCPProtocolTypeEnum, TransportTypeEnum } from '/@/enums/deviceEnum';
  22 + import { ObjectModelCommandDeliveryModal } from './ObjectModelCommandDeliveryModal';
  23 + import { ModalParamsType } from '/#/utils';
  24 + import { AreaChartOutlined } from '@ant-design/icons-vue';
  25 + import { SvgIcon, Icon } from '/@/components/Icon';
  26 + import { DataTypeEnum } from '/@/enums/objectModelEnum';
  27 + import {
  28 + buildTableDataSourceByObjectModel,
  29 + SocketInfoDataSourceItemType,
  30 + StructValueItemType,
  31 + } from './ModelOfMatter.config';
  32 + import { useJsonParse } from '/@/hooks/business/useJsonParse';
  33 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  34 + import { DeviceModelOfMatterAttrs } from '/@/api/device/model/deviceModel';
  35 +
  36 + interface ReceiveMessage {
  37 + data: {
  38 + [key: string]: [number, string][];
  39 + };
  40 + }
  41 +
  42 + const props = defineProps<{
  43 + deviceDetail: EdgeDeviceItemType;
  44 + }>();
  45 +
  46 + const grid = {
  47 + gutter: 8,
  48 + column: 3,
  49 + } as any;
  50 +
  51 + const listElRef = ref<Nullable<ComponentElRef<HTMLDivElement>>>(null);
  52 +
  53 + const token = getAuthCache(JWT_TOKEN_KEY);
  54 +
  55 + const { socketUrl } = useGlobSetting();
  56 +
  57 + const pagination = reactive<PaginationProps>({
  58 + size: 'small',
  59 + showSizeChanger: true,
  60 + showQuickJumper: true,
  61 + hideOnSinglePage: false,
  62 + showTotal: (total: number) => `共${total}条数据`,
  63 + onChange: handleFilterChange,
  64 + onShowSizeChange: handleFilterChange,
  65 + });
  66 +
  67 + const socketInfo = reactive({
  68 + cmdId: 2,
  69 + origin: `${socketUrl}${token}`,
  70 + attr: undefined as string | undefined,
  71 + dataSource: [] as SocketInfoDataSourceItemType[],
  72 + rawDataSource: [] as SocketInfoDataSourceItemType[],
  73 + message: {} as ReceiveMessage['data'],
  74 + attrKeys: [] as string[],
  75 + filterAttrKeys: [] as string[],
  76 + });
  77 +
  78 + function createUnsubscribeMessage(cmdId: number) {
  79 + return {
  80 + tsSubCmds: [
  81 + {
  82 + cmdId,
  83 + unsubscribe: true,
  84 + },
  85 + ],
  86 + };
  87 + }
  88 +
  89 + const getSendValue = computed(() => {
  90 + return {
  91 + tsSubCmds: [
  92 + {
  93 + entityType: 'DEVICE',
  94 + entityId: props.deviceDetail?.id?.id,
  95 + scope: 'LATEST_TELEMETRY',
  96 + cmdId: socketInfo.cmdId,
  97 + type: 'TIMESERIES',
  98 + },
  99 + ],
  100 + };
  101 + });
  102 +
  103 + const getFilterSendValue = computed(() => {
  104 + return {
  105 + tsSubCmds: [
  106 + {
  107 + entityType: 'DEVICE',
  108 + entityId: props.deviceDetail?.id?.id,
  109 + scope: 'LATEST_TELEMETRY',
  110 + cmdId: socketInfo.cmdId,
  111 + type: 'TIMESERIES',
  112 + },
  113 + ],
  114 + };
  115 + });
  116 +
  117 + const [registerForm, { getFieldsValue }] = useForm({
  118 + schemas: [
  119 + {
  120 + field: 'value',
  121 + label: '键/值',
  122 + component: 'Input',
  123 + colProps: { span: 6 },
  124 + componentProps: {
  125 + placeholder: '请输入键/值',
  126 + },
  127 + },
  128 + ],
  129 + labelWidth: 100,
  130 + compact: true,
  131 + showAdvancedButton: true,
  132 + submitFunc: async () => {
  133 + try {
  134 + const { value } = getFieldsValue() || {};
  135 +
  136 + pagination.current = 1;
  137 +
  138 + socketInfo.filterAttrKeys = value
  139 + ? unref(socketInfo.rawDataSource)
  140 + .filter(
  141 + (item) =>
  142 + item.key?.toUpperCase().includes(value.toUpperCase()) ||
  143 + item.name?.toUpperCase().includes(value.toUpperCase())
  144 + )
  145 + .map((item) => item.key)
  146 + : socketInfo.rawDataSource.map((item) => item.key);
  147 +
  148 + await nextTick();
  149 + handleFilterChange();
  150 + unref(mode) === EnumTableCardMode.TABLE && setTableModeData();
  151 + } catch (error) {}
  152 + },
  153 + resetFunc: async () => {
  154 + try {
  155 + socketInfo.filterAttrKeys = [];
  156 + handleFilterChange();
  157 + unref(mode) === EnumTableCardMode.TABLE && setTableModeData();
  158 + } catch (error) {}
  159 + },
  160 + });
  161 +
  162 + const [registerTable, { setTableData }] = useTable({
  163 + columns: realTimeDataColumns,
  164 + showTableSetting: true,
  165 + pagination: pagination as any,
  166 + bordered: true,
  167 + resizeHeightOffset: 16,
  168 + showIndexColumn: false,
  169 + });
  170 +
  171 + function handleFilterChange(
  172 + page: number = pagination.current || 1,
  173 + pageSize: number = pagination.pageSize || 10
  174 + ) {
  175 + pagination.current = page;
  176 + pagination.pageSize = pageSize;
  177 + send(JSON.stringify(createUnsubscribeMessage(socketInfo.cmdId)));
  178 + socketInfo.cmdId = socketInfo.cmdId + 1;
  179 + send(JSON.stringify(unref(getFilterSendValue)));
  180 + }
  181 +
  182 + const [registerModal, { openModal }] = useModal();
  183 +
  184 + const mode = ref<EnumTableCardMode>(EnumTableCardMode.CARD);
  185 +
  186 + const switchMode = async (value: EnumTableCardMode) => {
  187 + mode.value = value;
  188 + await nextTick();
  189 + unref(mode) === EnumTableCardMode.TABLE && setTableModeData();
  190 + socketInfo.filterAttrKeys = [];
  191 + };
  192 +
  193 + const { createMessage } = useMessage();
  194 +
  195 + const setDataSource = () => {
  196 + socketInfo.dataSource = socketInfo.filterAttrKeys.length
  197 + ? socketInfo.rawDataSource.filter((item) => socketInfo.filterAttrKeys.includes(item.key))
  198 + : socketInfo.rawDataSource;
  199 + };
  200 +
  201 + const { send, close, data, open } = useWebSocket(socketInfo.origin, {
  202 + immediate: false,
  203 + autoReconnect: true,
  204 + async onConnected() {
  205 + send(JSON.stringify(unref(getSendValue)));
  206 + },
  207 + async onMessage() {
  208 + try {
  209 + const value = JSON.parse(unref(data)) as ReceiveMessage;
  210 + if (value) {
  211 + const { data } = value;
  212 + const keys = Object.keys(data);
  213 + for (const key of keys) {
  214 + const item = socketInfo.rawDataSource.find((item) => item.key === key);
  215 + if (!item) continue;
  216 + const { type } = item;
  217 + const [firstItem] = data[key] || [];
  218 + const [time, value] = firstItem;
  219 + item.rawValue = value;
  220 + if (type === DataTypeEnum.STRUCT) {
  221 + const { flag, value: structJSON } = useJsonParse(value);
  222 + if (!flag || !isObject(item.value)) continue;
  223 + const structKeys = Object.keys(structJSON);
  224 + structKeys.forEach((key) => {
  225 + if ((item.value as Record<string, StructValueItemType>)?.[key]) {
  226 + (item.value as Record<string, StructValueItemType>)[key].value = structJSON[key];
  227 + }
  228 + });
  229 + item.time = time;
  230 + continue;
  231 + }
  232 + item.value = value;
  233 + item.time = time;
  234 + }
  235 + setDataSource();
  236 + await nextTick();
  237 + unref(mode) === EnumTableCardMode.TABLE && setTableModeData();
  238 + }
  239 + } catch (error) {}
  240 + },
  241 + onDisconnected() {
  242 + // close();
  243 + },
  244 + onError() {
  245 + createMessage.error('webSocket连接超时,请联系管理员');
  246 + },
  247 + });
  248 +
  249 + function setTableModeData() {
  250 + setTableData(socketInfo.dataSource.filter((item) => !isNull(item.value)));
  251 + }
  252 +
  253 + const handleShowDetail = (record: SocketInfoDataSourceItemType) => {
  254 + const { key } = record;
  255 + socketInfo.attr = key;
  256 + openModal(true);
  257 + };
  258 +
  259 + const getIsModbusDevice = computed(
  260 + () =>
  261 + props.deviceDetail.deviceData.transportConfiguration.type === TransportTypeEnum.TCP &&
  262 + props.deviceDetail?.deviceProfile?.profileData?.transportConfiguration?.protocol ===
  263 + TCPProtocolTypeEnum.MODBUS_RTU
  264 + );
  265 +
  266 + onMounted(async () => {
  267 + const { deviceProfileId } = props.deviceDetail;
  268 + const { id } = deviceProfileId;
  269 + const value = await getDeviceAttrs({ deviceProfileId: id });
  270 + socketInfo.attrKeys = isArray(value) ? value.map((item) => item.identifier) : [];
  271 + socketInfo.rawDataSource = buildTableDataSourceByObjectModel(
  272 + value as never as DeviceModelOfMatterAttrs[],
  273 + props.deviceDetail
  274 + );
  275 + setDataSource();
  276 + open();
  277 + });
  278 +
  279 + const formatValue = (item: SocketInfoDataSourceItemType) => {
  280 + if (isNullOrUnDef(item) || isNullOrUnDef(item.value)) return '--';
  281 + if (unref(getIsModbusDevice) && item.type === DataTypeEnum.BOOL) {
  282 + const _result = Reflect.get(item.enum || {}, item.value as string);
  283 + return isNullOrUnDef(_result) ? item.value : _result;
  284 + }
  285 + switch (item.type) {
  286 + case DataTypeEnum.BOOL:
  287 + return !!Number(item.value) ? item.boolOpen : item.boolClose;
  288 + case DataTypeEnum.ENUM:
  289 + return item.enum?.[item.value as string];
  290 + default:
  291 + return item.value || '--';
  292 + }
  293 + };
  294 +
  295 + const [register, { openModal: openSendCommandModal }] = useModal();
  296 + const handleSendCommandModal = (data: SocketInfoDataSourceItemType) => {
  297 + openSendCommandModal(true, {
  298 + mode: DataActionModeEnum.READ,
  299 + record: {
  300 + objectModel: toRaw(unref(data.detail)),
  301 + deviceDetail: props.deviceDetail as EdgeDeviceItemType,
  302 + },
  303 + } as ModalParamsType);
  304 + };
  305 +
  306 + onMounted(() => {
  307 + const element = unref(listElRef)?.$el;
  308 + if (!element) return;
  309 + const totalHeight = document.documentElement.clientHeight;
  310 + const { top } = element?.getBoundingClientRect() || {};
  311 + const containerEl = element.querySelector('.ant-spin-container') as HTMLDivElement;
  312 + containerEl.style.height = `${totalHeight - top - 20 - 50}px`;
  313 + containerEl.style.overflowY = 'auto';
  314 + containerEl.style.overflowX = 'hidden';
  315 + });
  316 +
  317 + onUnmounted(() => close());
  318 +</script>
  319 +
  320 +<template>
  321 + <PageWrapper
  322 + dense
  323 + content-class="flex flex-col p-4 dark:text-gray-300 dark:bg-dark-700 bg-gray-100"
  324 + >
  325 + <section
  326 + class="flex flex-col justify-between w-full bg-light-50 pt-3 mb-4 dark:text-gray-300 dark:bg-dark-900"
  327 + >
  328 + <div class="flex-auto">
  329 + <BasicForm @register="registerForm" />
  330 + </div>
  331 + </section>
  332 + <section class="bg-light-50 !dark:text-gray-300 !dark:bg-dark-900">
  333 + <div
  334 + v-show="mode === EnumTableCardMode.CARD"
  335 + class="flex h-70px items-center justify-end p-2"
  336 + >
  337 + <ModeSwitchButton v-model:value="mode" @change="switchMode" />
  338 + </div>
  339 + <List
  340 + v-show="mode === EnumTableCardMode.CARD"
  341 + ref="listElRef"
  342 + class="list-mode !px-2"
  343 + :data-source="socketInfo.dataSource"
  344 + :grid="grid"
  345 + :pagination="pagination"
  346 + >
  347 + <template #renderItem="{ item }: { item: SocketInfoDataSourceItemType }">
  348 + <List.Item>
  349 + <Card class="shadow-md">
  350 + <template #title>
  351 + <span class="text-base font-normal mr-2">{{ item.name }}</span>
  352 + </template>
  353 + <template #extra>
  354 + <Space>
  355 + <Tooltip
  356 + :title="item.expand ? '收起' : '展开'"
  357 + v-if="item.type === DataTypeEnum.STRUCT"
  358 + >
  359 + <Icon
  360 + :icon="item.expand ? 'ant-design:up-outlined' : 'ant-design:down-outlined'"
  361 + class="cursor-pointer svg:text-blue-500"
  362 + @click="item.expand = !item.expand"
  363 + />
  364 + </Tooltip>
  365 + <Tooltip title="属性下发">
  366 + <SvgIcon
  367 + name="send-command"
  368 + prefix="iconfont"
  369 + v-if="ReadAndWriteEnum.READ_AND_WRITE === item.accessMode"
  370 + class="cursor-pointer text-lg text-blue-500"
  371 + @click="handleSendCommandModal(item)"
  372 + />
  373 + </Tooltip>
  374 +
  375 + <Tooltip title="历史趋势">
  376 + <AreaChartOutlined
  377 + class="cursor-pointer text-lg svg:fill-blue-500"
  378 + @click="handleShowDetail(item)"
  379 + />
  380 + </Tooltip>
  381 + </Space>
  382 + </template>
  383 + <section class="min-h-16 flex flex-col justify-between">
  384 + <div
  385 + v-if="item.type !== DataTypeEnum.STRUCT"
  386 + class="flex font-bold text-lg mb-4 gap-2"
  387 + >
  388 + <Tooltip :title="formatValue(item) as any" placement="topLeft">
  389 + <div class="truncate">{{ formatValue(item) }}</div>
  390 + </Tooltip>
  391 + <div class="text-xs flex items-center">{{ item.unitName }}</div>
  392 + </div>
  393 + <div
  394 + class="mb-4 overflow-hidden flex flex-col gap-2 relative"
  395 + v-if="item.type == DataTypeEnum.STRUCT && isObject(item.value)"
  396 + :style="{ height: item.expand ? 'fit-content' : '28px' }"
  397 + >
  398 + <div
  399 + v-for="key in Object.keys(item.value)"
  400 + :key="key"
  401 + class="cursor-pointer flex justify-between items-center gap-2"
  402 + >
  403 + <div class="font-medium whitespace-nowrap">{{ item.value[key].name }}</div>
  404 + <div class="flex-auto font-bold text-lg text-left ml-4 truncate">
  405 + <Tooltip
  406 + :title="formatValue(item.value[key] as SocketInfoDataSourceItemType) as any"
  407 + placement="topLeft"
  408 + >
  409 + <span>
  410 + {{ formatValue(item.value[key] as SocketInfoDataSourceItemType) }}
  411 + </span>
  412 + </Tooltip>
  413 + <span class="text-xs font-normal ml-2">
  414 + {{ item.value[key].unitName }}
  415 + </span>
  416 + </div>
  417 + </div>
  418 + <Tooltip title="点击展开">
  419 + <div
  420 + v-show="!item.expand"
  421 + class="absolute top-2 right-0 text-blue-400 cursor-pointer text-xs"
  422 + @click="item.expand = !item.expand"
  423 + >
  424 + 更多
  425 + </div>
  426 + </Tooltip>
  427 + </div>
  428 + <div class="text-dark-800 text-xs">
  429 + {{
  430 + item.time && item?.rawValue
  431 + ? formatToDateTime(item.time, 'YYYY-MM-DD HH:mm:ss')
  432 + : '--'
  433 + }}
  434 + </div>
  435 + </section>
  436 + </Card>
  437 + </List.Item>
  438 + </template>
  439 + </List>
  440 + </section>
  441 + <BasicTable
  442 + v-if="mode === EnumTableCardMode.TABLE"
  443 + @register="registerTable"
  444 + class="device-things-model-table-mode"
  445 + >
  446 + <template #toolbar>
  447 + <div
  448 + v-show="mode === EnumTableCardMode.TABLE"
  449 + class="flex h-70px items-center justify-end p-2"
  450 + >
  451 + <ModeSwitchButton v-model:value="mode" @change="switchMode" />
  452 + </div>
  453 + </template>
  454 + </BasicTable>
  455 + <BasicModal
  456 + @register="registerModal"
  457 + title="历史数据"
  458 + width="50%"
  459 + destroy-on-close
  460 + dialogClass="history-modal"
  461 + >
  462 + <HistoryData :deviceDetail="props.deviceDetail" :attr="socketInfo.attr" />
  463 + </BasicModal>
  464 + <ObjectModelCommandDeliveryModal
  465 + @register="register"
  466 + :deviceId="deviceDetail.id.id"
  467 + :device-name="deviceDetail.name"
  468 + />
  469 + </PageWrapper>
  470 +</template>
  471 +
  472 +<style scoped lang="less">
  473 + .list-mode:deep(.ant-card-head) {
  474 + border-bottom: 0;
  475 + }
  476 +
  477 + .list-mode:deep(.ant-card-body) {
  478 + padding-top: 0;
  479 + }
  480 +
  481 + .list-mode:deep(.ant-card-head-title) {
  482 + height: 64px;
  483 + display: flex;
  484 + align-items: center;
  485 + }
  486 +
  487 + .device-things-model-table-mode:deep(.ant-table-placeholder) {
  488 + height: auto;
  489 + }
  490 +
  491 + .model-collapse {
  492 + border: none;
  493 +
  494 + :deep(.ant-collapse-header) {
  495 + display: none;
  496 + }
  497 +
  498 + :deep(.ant-collapse-item) {
  499 + border: none;
  500 + }
  501 +
  502 + :deep(.ant-collapse-content) {
  503 + border: none;
  504 + }
  505 +
  506 + :deep(.ant-collapse-content-box) {
  507 + padding: 0;
  508 + }
  509 + }
  510 +</style>
  511 +
  512 +<style>
  513 + .history-modal .ant-input-number {
  514 + min-width: 0 !important;
  515 + width: 100% !important;
  516 + }
  517 +</style>
... ...
  1 +export interface DeviceType {
  2 + alarmStatus: number;
  3 + createTime: string;
  4 + creator: string;
  5 + deviceInfo: Object;
  6 + deviceProfile: Object;
  7 + deviceState: string;
  8 + deviceToken: string;
  9 + deviceType: string;
  10 + enabled: boolean;
  11 + id: string;
  12 + key: string;
  13 + label: string;
  14 + name: string;
  15 + organizationDTO: Object;
  16 + organizationId: string;
  17 + profileId: string;
  18 + tenantCode: string;
  19 + updateTime: string;
  20 +}
  21 +
  22 +export interface socketDataType {
  23 + key: string;
  24 + value: string;
  25 + time: number;
  26 +}
... ...
  1 +import { DescItem } from '/@/components/Description/src/typing';
  2 +
  3 +export const descSchema = (): DescItem[] => {
  4 + return [
  5 + {
  6 + field: 'name',
  7 + label: '设备名称',
  8 + },
  9 + {
  10 + field: 'label',
  11 + label: '设备标签',
  12 + },
  13 + {
  14 + field: 'routingKey',
  15 + label: '边缘键',
  16 + },
  17 + {
  18 + field: 'secret',
  19 + label: '边缘密钥',
  20 + },
  21 + {
  22 + field: 'createdTime',
  23 + label: '创建时间',
  24 + },
  25 + {
  26 + field: 'additionalInfo.description',
  27 + label: '描述',
  28 + },
  29 + ];
  30 +};
... ...
  1 +export { default as EdgeDeviceTabInfo } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { Tabs } from 'ant-design-vue';
  3 + import { ref } from 'vue';
  4 + import type { PropType } from 'vue';
  5 + import { EdgeDevicePhysicalModel } from '../EdgeDevicePhysicalModel';
  6 + import { EdgeDeviceAlarm } from '../EdgeDeviceAlarm';
  7 + import { EdgeDeviceEvent } from '../EdgeDeviceEvent';
  8 + import { EdgeDeviceCommand } from '../EdgeDeviceCommand';
  9 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  10 +
  11 + defineProps({
  12 + recordData: {
  13 + type: Object as PropType<EdgeDeviceItemType>,
  14 + default: () => {},
  15 + },
  16 + });
  17 +
  18 + enum ActiveKey {
  19 + PHYSICALMODEL = 'PhysicalModel',
  20 + ALARM = 'Alarm',
  21 + COMMANDISSUANCERECORD = 'CommandIssuanceRecord',
  22 + EVENTMANAGEMENT = 'EventManagement',
  23 + }
  24 +
  25 + const activeKey = ref(ActiveKey.PHYSICALMODEL);
  26 +</script>
  27 +
  28 +<template>
  29 + <section class="w-full h-full p-1">
  30 + <main>
  31 + <Tabs
  32 + v-model:activeKey="activeKey"
  33 + type="card"
  34 + class="w-full h-full bg-light-50 !p-4 dark:bg-dark-900"
  35 + >
  36 + <Tabs.TabPane tab="物模型" :key="ActiveKey.PHYSICALMODEL">
  37 + <EdgeDevicePhysicalModel :deviceDetail="recordData" />
  38 + </Tabs.TabPane>
  39 + <Tabs.TabPane tab="告警" :key="ActiveKey.ALARM">
  40 + <EdgeDeviceAlarm :recordData="recordData" />
  41 + </Tabs.TabPane>
  42 + <Tabs.TabPane tab="命令下发" :key="ActiveKey.COMMANDISSUANCERECORD">
  43 + <EdgeDeviceCommand :recordData="recordData" />
  44 + </Tabs.TabPane>
  45 + <Tabs.TabPane tab="事件管理" :key="ActiveKey.EVENTMANAGEMENT">
  46 + <EdgeDeviceEvent :recordData="recordData" />
  47 + </Tabs.TabPane>
  48 + </Tabs>
  49 + </main>
  50 + </section>
  51 +</template>
  52 +
  53 +<style lang="less" scoped></style>
... ...
  1 +import { Tag } from 'ant-design-vue';
  2 +import { BasicColumn } from '/@/components/Table';
  3 +import { formatToDateTime } from '/@/utils/dateUtil';
  4 +import { h } from 'vue';
  5 +
  6 +// 表格配置
  7 +export const columns: BasicColumn[] = [
  8 + {
  9 + title: '事件时间',
  10 + dataIndex: 'createdTime',
  11 + format(text) {
  12 + return formatToDateTime(text, 'YYYY-MM-DD HH:mm:ss');
  13 + },
  14 + },
  15 + {
  16 + title: '服务器',
  17 + dataIndex: 'body.server',
  18 + },
  19 + {
  20 + title: '事件',
  21 + dataIndex: 'body.event',
  22 + },
  23 + {
  24 + title: '状态',
  25 + dataIndex: 'body.success',
  26 + customRender: ({ record }) => {
  27 + const color = record.body.success ? 'success' : 'error';
  28 + const text = record.body.success ? '成功' : '失败';
  29 + return h(Tag, { color: color }, () => text);
  30 + },
  31 + },
  32 + {
  33 + title: '错误',
  34 + dataIndex: 'error',
  35 + slots: { customRender: 'errorDetail' },
  36 + },
  37 +];
... ...
  1 +export { default as EdgeEvents } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { BasicTable, useTable } from '/@/components/Table';
  3 + import { columns } from './config';
  4 + import { edgeEventPage } from '/@/api/edgeManage/edgeInstance';
  5 + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  6 + import { BasicModal, useModal } from '/@/components/Modal';
  7 + import { Input, Button } from 'ant-design-vue';
  8 + import { ref } from 'vue';
  9 +
  10 + const props = defineProps({
  11 + recordData: {
  12 + type: Object as PropType<EdgeInstanceItemType>,
  13 + default: () => {},
  14 + },
  15 + });
  16 +
  17 + const outputData = ref<string>();
  18 +
  19 + const [registerTable] = useTable({
  20 + columns,
  21 + api: async ({ page, pageSize }) => {
  22 + const res = await edgeEventPage(
  23 + {
  24 + page: page - 1 < 0 ? 0 : page - 1,
  25 + pageSize,
  26 + tenantId: props?.recordData?.tenantId?.id,
  27 + sortOrder: 'DESC',
  28 + sortProperty: 'ts',
  29 + },
  30 + props?.recordData?.id?.id
  31 + );
  32 + return {
  33 + total: res?.totalElements,
  34 + items: res?.data,
  35 + };
  36 + },
  37 + showIndexColumn: false,
  38 + clickToRowSelect: false,
  39 + showTableSetting: true,
  40 + bordered: true,
  41 + });
  42 +
  43 + const [registerModal, { openModal, closeModal }] = useModal();
  44 +
  45 + const handleEventIsDetail = (record: Recordable) => {
  46 + outputData.value = record?.body?.error;
  47 + openModal(true);
  48 + };
  49 +</script>
  50 +
  51 +<template>
  52 + <div>
  53 + <BasicTable :clickToRowSelect="false" @register="registerTable">
  54 + <template #errorDetail="{ record }">
  55 + <Button
  56 + v-if="record?.body?.error"
  57 + type="link"
  58 + class="ml-2"
  59 + @click="handleEventIsDetail(record)"
  60 + >
  61 + 查看
  62 + </Button>
  63 + <Button v-else type="link" class="ml-2"> - </Button>
  64 + </template>
  65 + </BasicTable>
  66 + <BasicModal title="详情" @register="registerModal" @ok="closeModal">
  67 + <Input.TextArea v-model:value="outputData" :autosize="true" />
  68 + </BasicModal>
  69 + </div>
  70 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { nextTick, ref, unref, computed } from 'vue';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
  5 + import { FormFieldsEnum, formSchema } from './config';
  6 + import { HandleOperationEnum } from '../../config';
  7 + import { useMessage } from '/@/hooks/web/useMessage';
  8 + import { createOrEditEdgeInstance } from '/@/api/edgeManage/edgeInstance';
  9 + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  10 + import { buildUUID, randomString } from '/@/utils/uuid';
  11 + import { handeleCopy } from '/@/views/device/profiles/step/topic';
  12 +
  13 + const emits = defineEmits(['success', 'register']);
  14 +
  15 + const { createMessage } = useMessage();
  16 +
  17 + const isUpdate = ref<Boolean>();
  18 +
  19 + const isUpdateText = ref<String>();
  20 +
  21 + const recordData = ref<EdgeInstanceItemType>();
  22 +
  23 + const [registerForm, { resetFields, validate, setFieldsValue }] = useForm({
  24 + labelWidth: 120,
  25 + schemas: formSchema,
  26 + showActionButtonGroup: false,
  27 + });
  28 +
  29 + const cacheTitle = computed(() => (!unref(isUpdate) ? '新增实例' : '编辑实例'));
  30 +
  31 + const handleCopyRoutingKey = (text: string) => handeleCopy(text);
  32 +
  33 + const handleCopySecret = (text: string) => handeleCopy(text);
  34 +
  35 + const setDefaultValue = (event: HandleOperationEnum) => {
  36 + if (event === HandleOperationEnum.CREATE) {
  37 + setFieldsValue({
  38 + [FormFieldsEnum.ROUTINGKEY]: buildUUID(),
  39 + [FormFieldsEnum.SECRET]: randomString(),
  40 + });
  41 + }
  42 + };
  43 +
  44 + const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
  45 + setDrawerProps({ loading: true });
  46 + await resetFields();
  47 + setDefaultValue(data.event);
  48 + isUpdate.value = data.isUpdate;
  49 + isUpdateText.value = data.isUpdateText;
  50 + recordData.value = data.record;
  51 + try {
  52 + await nextTick();
  53 + setFieldsValue(data.record);
  54 + setFieldsValue({
  55 + [FormFieldsEnum.DESCRIPTION]: data.record?.additionalInfo[FormFieldsEnum.DESCRIPTION],
  56 + });
  57 + } finally {
  58 + setDrawerProps({ loading: false });
  59 + }
  60 + });
  61 +
  62 + const getValue = async () => {
  63 + const values = await validate();
  64 + if (!values) return;
  65 + const additionalInfo = {
  66 + description: values[FormFieldsEnum.DESCRIPTION],
  67 + };
  68 + const mergeValues = {
  69 + ...recordData.value,
  70 + ...values,
  71 + ...{ additionalInfo },
  72 + };
  73 + Reflect.deleteProperty(mergeValues, FormFieldsEnum.DESCRIPTION);
  74 + await createOrEditEdgeInstance(mergeValues);
  75 + createMessage.success(`${isUpdateText.value}成功`);
  76 + closeDrawer();
  77 + setTimeout(() => {
  78 + emits('success');
  79 + }, 500);
  80 + };
  81 +
  82 + const handleSubmit = () => getValue();
  83 +</script>
  84 +
  85 +<template>
  86 + <div>
  87 + <BasicDrawer
  88 + destroyOnClose
  89 + v-bind="$attrs"
  90 + showFooter
  91 + :title="cacheTitle"
  92 + width="30%"
  93 + :maskClosable="true"
  94 + @register="registerDrawer"
  95 + @ok="handleSubmit"
  96 + >
  97 + <BasicForm @register="registerForm">
  98 + <template #routingKey="{ model, field }">
  99 + <div class="!flex justify-between items-center gap-5">
  100 + <a-input disabled v-model:value="model[field]" />
  101 + <a-button type="link" @click="handleCopyRoutingKey(model[field])" class="cursor-pointer"
  102 + >复制</a-button
  103 + >
  104 + </div>
  105 + </template>
  106 + <template #secret="{ model, field }">
  107 + <div class="!flex justify-between items-center gap-5">
  108 + <a-input disabled v-model:value="model[field]" />
  109 + <a-button type="link" @click="handleCopySecret(model[field])" class="cursor-pointer"
  110 + >复制</a-button
  111 + >
  112 + </div>
  113 + </template>
  114 + </BasicForm>
  115 + </BasicDrawer>
  116 + </div>
  117 +</template>
  118 +
  119 +<style lang="less" scoped></style>
... ...
  1 +import { FormSchema } from '/@/components/Form';
  2 +
  3 +export enum FormFieldsEnum {
  4 + NAME = 'name',
  5 + LABEL = 'label',
  6 + ROUTINGKEY = 'routingKey',
  7 + SECRET = 'secret',
  8 + TYPE = 'type',
  9 + DESCRIPTION = 'description',
  10 + TEXTSEARCH = 'textSearch',
  11 +}
  12 +
  13 +enum FormFieldsNameEnum {
  14 + NAME = '名称',
  15 + LABEL = '标签',
  16 + ROUTINGKEY = '边缘Key',
  17 + SECRET = '边缘密钥',
  18 + TYPE = '边缘类型',
  19 + DESCRIPTION = '描述',
  20 + TEXTSEARCH = '实例名称',
  21 +}
  22 +
  23 +export const formSchema: FormSchema[] = [
  24 + {
  25 + field: FormFieldsEnum.NAME,
  26 + label: FormFieldsNameEnum.NAME,
  27 + component: 'Input',
  28 + required: true,
  29 + componentProps: {
  30 + maxLength: 255,
  31 + placeholder: '请输入实例名称',
  32 + },
  33 + },
  34 + {
  35 + field: FormFieldsEnum.TYPE,
  36 + label: FormFieldsNameEnum.TYPE,
  37 + component: 'Input',
  38 + required: true,
  39 + defaultValue: 'default',
  40 + componentProps: {
  41 + disabled: true,
  42 + },
  43 + },
  44 + {
  45 + field: FormFieldsEnum.ROUTINGKEY,
  46 + label: FormFieldsNameEnum.ROUTINGKEY,
  47 + slot: 'routingKey',
  48 + required: true,
  49 + component: 'Input',
  50 + },
  51 + {
  52 + field: FormFieldsEnum.SECRET,
  53 + label: FormFieldsNameEnum.SECRET,
  54 + slot: 'secret',
  55 + required: true,
  56 + component: 'Input',
  57 + },
  58 + {
  59 + field: FormFieldsEnum.LABEL,
  60 + label: FormFieldsNameEnum.LABEL,
  61 + component: 'Input',
  62 + componentProps: {
  63 + maxLength: 255,
  64 + placeholder: '请输入标签',
  65 + },
  66 + },
  67 + {
  68 + field: FormFieldsEnum.DESCRIPTION,
  69 + label: FormFieldsNameEnum.DESCRIPTION,
  70 + component: 'InputTextArea',
  71 + componentProps: {
  72 + maxLength: 255,
  73 + placeholder: '请输入描述',
  74 + },
  75 + },
  76 +];
  77 +
  78 +export const searchFormSchema: FormSchema[] = [
  79 + {
  80 + field: FormFieldsEnum.TEXTSEARCH,
  81 + label: FormFieldsNameEnum.TEXTSEARCH,
  82 + component: 'Input',
  83 + colProps: { span: 6 },
  84 + componentProps: {
  85 + maxLength: 255,
  86 + placeholder: '请输入实例名称',
  87 + },
  88 + },
  89 +];
... ...
  1 +export { default as EdgeInstanceDetailDrawer } from './index.vue';
  2 +export { default as EdgeInstanceFormDrawer } from './EdgeInstanceFormDrawer.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { ref } from 'vue';
  3 + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  4 + import { infoEdgeInstance } from '/@/api/edgeManage/edgeInstance';
  5 + import { EdgeInstanceBasicInfo } from '../EdgeInstanceBasicInfo';
  6 + import { EdgeInstanceTabInfo } from '../EdgeInstanceTabInfo';
  7 + import { useRoute } from 'vue-router';
  8 + import { PageWrapper } from '/@/components/Page';
  9 + import { useGo } from '/@/hooks/web/usePage';
  10 +
  11 + defineEmits(['register']);
  12 +
  13 + defineOptions({ name: 'EdgeDetail' });
  14 +
  15 + const route = useRoute();
  16 +
  17 + const go = useGo();
  18 +
  19 + const edgeId = ref(route.params?.id);
  20 +
  21 + const recordData = ref<EdgeInstanceItemType>();
  22 +
  23 + infoEdgeInstance(edgeId.value as string).then((res) => {
  24 + recordData.value = res;
  25 + });
  26 +
  27 + function goBack() {
  28 + go('/edge/instance');
  29 + }
  30 +</script>
  31 +
  32 +<template>
  33 + <PageWrapper :title="`边缘详情`" contentBackground @back="goBack">
  34 + <!-- 基础信息 -->
  35 + <div class="m-4">
  36 + <EdgeInstanceBasicInfo v-if="recordData" :recordData="recordData" />
  37 + </div>
  38 + <!-- Tab信息 -->
  39 + <div>
  40 + <EdgeInstanceTabInfo v-if="recordData" :recordData="recordData" />
  41 + </div>
  42 + </PageWrapper>
  43 +</template>
  44 +
  45 +<style lang="less" scoped></style>
... ...
  1 +import { Tag } from 'ant-design-vue';
  2 +import { h } from 'vue';
  3 +import { DescItem } from '/@/components/Description/src/typing';
  4 +import { handeleCopy } from '/@/views/device/profiles/step/topic';
  5 +import { formatToDateTime } from '/@/utils/dateUtil';
  6 +
  7 +export const descSchema = (): DescItem[] => {
  8 + return [
  9 + {
  10 + field: 'name',
  11 + label: '边缘名称',
  12 + },
  13 + {
  14 + field: 'label',
  15 + label: '设备标签',
  16 + render: (text) => {
  17 + return text
  18 + ? h(
  19 + Tag,
  20 + {
  21 + color: '#00B42A',
  22 + },
  23 + text
  24 + )
  25 + : '';
  26 + },
  27 + },
  28 + {
  29 + field: 'active',
  30 + label: '状态',
  31 + render: (text) => {
  32 + const color = text ? 'success' : 'error';
  33 + const textStr = text ? ' 在线' : '离线';
  34 + return h(
  35 + Tag,
  36 + {
  37 + color,
  38 + },
  39 + textStr
  40 + );
  41 + },
  42 + },
  43 + {
  44 + field: 'routingKey',
  45 + label: '边缘键',
  46 + render: (text) => {
  47 + return h(
  48 + 'span',
  49 + {
  50 + style: { cursor: 'pointer', color: '#165DFF' },
  51 + onClick: () => {
  52 + handeleCopy(text);
  53 + },
  54 + },
  55 + text
  56 + );
  57 + },
  58 + },
  59 + {
  60 + field: 'secret',
  61 + label: '边缘密钥',
  62 + render: (text) => {
  63 + return h(
  64 + 'span',
  65 + {
  66 + style: { cursor: 'pointer', color: '#165DFF' },
  67 + onClick: () => {
  68 + handeleCopy(text);
  69 + },
  70 + },
  71 + text
  72 + );
  73 + },
  74 + },
  75 + {
  76 + field: 'createdTime',
  77 + label: '创建时间',
  78 + render: (_, data) => {
  79 + return formatToDateTime(data.createdTime, 'YYYY-MM-DD HH:mm:ss');
  80 + },
  81 + },
  82 + {
  83 + field: 'additionalInfo.description',
  84 + label: '描述',
  85 + },
  86 + ];
  87 +};
... ...
  1 +export { default as EdgeInstanceBasicInfo } 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 { ref } from 'vue';
  6 + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  7 + import edgeInstancePng from '/@/assets/images/edgeInstance.png';
  8 + import { Image } from 'ant-design-vue';
  9 + import { useMessage } from '/@/hooks/web/useMessage';
  10 + import { syncEdge } from '/@/api/edgeManage/edgeInstance';
  11 + // import { useGo } from '/@/hooks/web/usePage';
  12 +
  13 + const props = defineProps({
  14 + recordData: {
  15 + type: Object as PropType<EdgeInstanceItemType>,
  16 + default: () => {},
  17 + },
  18 + });
  19 +
  20 + defineEmits(['register']);
  21 +
  22 + const CS = {
  23 + 'max-width': '600px',
  24 + 'word-break': 'break-all',
  25 + overflow: 'hidden',
  26 + display: '-webkit-box',
  27 + '-webkit-line-clamp': 2,
  28 + '-webkit-box-orient': 'vertical',
  29 + };
  30 +
  31 + const { createMessage } = useMessage();
  32 +
  33 + // const go = useGo();
  34 +
  35 + const loadStatus = ref(false);
  36 +
  37 + const [register] = useDescription({
  38 + layout: 'vertical',
  39 + schema: descSchema(),
  40 + column: 5,
  41 + });
  42 +
  43 + const handleEventIsSyncEdge = async () => {
  44 + loadStatus.value = true;
  45 + try {
  46 + if (!props.recordData?.id?.id) return;
  47 + await syncEdge(props.recordData?.id?.id);
  48 + createMessage.success('同步边缘处理成功');
  49 + } finally {
  50 + loadStatus.value = false;
  51 + }
  52 + };
  53 +
  54 + // const handleEventIsOpenEdgeDevice = () => {
  55 + // go('/edge/edge_device/' + props.recordData?.id?.id);
  56 + // };
  57 +</script>
  58 +
  59 +<template>
  60 + <a-row :gutter="{ xs: 8, sm: 16, md: 24, lg: 32 }">
  61 + <a-col class="gutter-row" :span="3">
  62 + <div class="!flex flex-col justify-between items-center">
  63 + <div><Image :src="edgeInstancePng" :width="180" /></div>
  64 + <div class="!flex flex-col mt-3 justify-between">
  65 + <span style="color: #1d2129" class="truncate font-bold">{{ recordData?.name }}</span>
  66 + <span style="color: #3d3d3d">边缘详情</span>
  67 + </div>
  68 + </div>
  69 + </a-col>
  70 + <a-col class="gutter-row" :span="21">
  71 + <div class="!flex flex-col justify-between">
  72 + <Description v-if="recordData" @register="register" :data="recordData" :contentStyle="CS" />
  73 + <div class="!flex mt-3">
  74 + <a-button
  75 + :disabled="!recordData.active"
  76 + :loading="loadStatus"
  77 + type="primary"
  78 + @click="handleEventIsSyncEdge"
  79 + >同步边缘</a-button
  80 + >
  81 + <!-- <a-button class="ml-4" type="primary" @click="handleEventIsOpenEdgeDevice"
  82 + >边缘设备</a-button
  83 + > -->
  84 + </div>
  85 + </div>
  86 + </a-col>
  87 + </a-row>
  88 +</template>
  89 +
  90 +<style lang="less" scoped>
  91 + :deep(.ant-image-img) {
  92 + height: 157px;
  93 + }
  94 +</style>
... ...
  1 +import { DescItem } from '/@/components/Description/src/typing';
  2 +
  3 +export const descSchema = (): DescItem[] => {
  4 + return [
  5 + {
  6 + field: 'name',
  7 + label: '设备名称',
  8 + },
  9 + {
  10 + field: 'label',
  11 + label: '设备标签',
  12 + },
  13 + {
  14 + field: 'routingKey',
  15 + label: '边缘键',
  16 + },
  17 + {
  18 + field: 'secret',
  19 + label: '边缘密钥',
  20 + },
  21 + {
  22 + field: 'createdTime',
  23 + label: '创建时间',
  24 + },
  25 + {
  26 + field: 'additionalInfo.description',
  27 + label: '描述',
  28 + },
  29 + ];
  30 +};
... ...
  1 +export { default as EdgeInstanceTabInfo } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { Tabs } from 'ant-design-vue';
  3 + import { ref } from 'vue';
  4 + import { EdgeMonitoring } from '../EdgeMonitoring';
  5 + import { EdgeEvents } from '../EdgeEvents';
  6 + import { EdgeDevice } from '../EdgeDevice';
  7 + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  8 + import type { PropType } from 'vue';
  9 +
  10 + defineProps({
  11 + recordData: {
  12 + type: Object as PropType<EdgeInstanceItemType>,
  13 + default: () => {},
  14 + },
  15 + });
  16 +
  17 + enum ActiveKey {
  18 + EDGE_DEVICE = 'EdgeDevice',
  19 + EDGEMONITORING = 'EdgeMonitoring',
  20 + EDGEEVENTS = 'EdgeEvents',
  21 + }
  22 +
  23 + const activeKey = ref(ActiveKey.EDGE_DEVICE);
  24 +</script>
  25 +
  26 +<template>
  27 + <section class="w-full h-full p-1">
  28 + <main>
  29 + <Tabs
  30 + v-model:activeKey="activeKey"
  31 + type="card"
  32 + class="w-full h-full bg-light-50 !p-4 dark:bg-dark-900"
  33 + >
  34 + <Tabs.TabPane tab="边缘设备" :key="ActiveKey.EDGE_DEVICE">
  35 + <EdgeDevice
  36 + v-if="recordData"
  37 + :recordData="recordData"
  38 + class="p-4 bg-neutral-100 dark:bg-dark-700"
  39 + />
  40 + </Tabs.TabPane>
  41 + <Tabs.TabPane tab="边缘监控" :key="ActiveKey.EDGEMONITORING">
  42 + <EdgeMonitoring
  43 + v-if="recordData"
  44 + :recordData="recordData"
  45 + class="p-4 bg-fff dark:bg-dark-700"
  46 + />
  47 + </Tabs.TabPane>
  48 + <Tabs.TabPane tab="边缘事件" :key="ActiveKey.EDGEEVENTS">
  49 + <EdgeEvents :recordData="recordData" class="p-4 bg-neutral-100 dark:bg-dark-700" />
  50 + </Tabs.TabPane>
  51 + </Tabs>
  52 + </main>
  53 + </section>
  54 +</template>
  55 +
  56 +<style lang="less" scoped></style>
... ...
  1 +export { default as EdgeMonitoring } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import {
  3 + ref,
  4 + onMounted,
  5 + unref,
  6 + shallowReactive,
  7 + nextTick,
  8 + reactive,
  9 + computed,
  10 + onUnmounted,
  11 + } from 'vue';
  12 + import * as echarts from 'echarts';
  13 + import { Empty } from 'ant-design-vue';
  14 + import { useMessage } from '/@/hooks/web/useMessage';
  15 + import { useWebSocket } from '@vueuse/core';
  16 + import { getAuthCache } from '/@/utils/auth';
  17 + import { JWT_TOKEN_KEY } from '/@/enums/cacheEnum';
  18 + import { useGlobSetting } from '/@/hooks/setting';
  19 + import moment from 'moment';
  20 + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  21 + import { isArray, isNumber } from '/@/utils/is';
  22 + import { formatSizeUnits } from '/@/utils';
  23 + import cpuSvg from '/@/assets/icons/cpu.svg';
  24 + import diskSvg from '/@/assets/icons/disk.svg';
  25 + import memorySvg from '/@/assets/icons/memory.svg';
  26 +
  27 + interface ChartInstance {
  28 + name: string;
  29 + currentValue: number;
  30 + totalKey: string;
  31 + totalCount: Recordable;
  32 + type: string;
  33 + text: string;
  34 + xAxisData: string[];
  35 + seriesData: number[];
  36 + icon: any;
  37 + }
  38 +
  39 + enum DeviceInfoOfEdge {
  40 + CPU_USAGE_OF_EDGE = 'cpuUsageOfEdge',
  41 + DISC_USAGE_OF_EDGE = 'discUsageOfEdge',
  42 + MEMORY_USAGE_OF_EDGE = 'memoryUsageOfEdge',
  43 + CPU_COUNT_OF_EDGE = 'cpuCountOfEdge',
  44 + TOTAL_MEMORY_OF_EDGE = 'totalMemoryOfEdge',
  45 + TOTAL_DISC_SPACE_OF_EDGE = 'totalDiscSpaceOfEdge',
  46 + }
  47 +
  48 + const props = defineProps({
  49 + recordData: {
  50 + type: Object as PropType<EdgeInstanceItemType>,
  51 + default: () => {},
  52 + },
  53 + });
  54 +
  55 + const token = getAuthCache(JWT_TOKEN_KEY);
  56 +
  57 + const { socketUrl } = useGlobSetting();
  58 +
  59 + const socketInfo = reactive({
  60 + entityId: '',
  61 + origin: `${socketUrl}${token}`,
  62 + });
  63 +
  64 + const { createMessage } = useMessage();
  65 +
  66 + const getSendValue = computed(() => {
  67 + return {
  68 + tsSubCmds: [
  69 + {
  70 + entityId: '',
  71 + cmdId: 11,
  72 + entityType: 'EDGE',
  73 + keys: `${DeviceInfoOfEdge.CPU_USAGE_OF_EDGE},${DeviceInfoOfEdge.DISC_USAGE_OF_EDGE},${DeviceInfoOfEdge.MEMORY_USAGE_OF_EDGE},${DeviceInfoOfEdge.CPU_COUNT_OF_EDGE},${DeviceInfoOfEdge.TOTAL_MEMORY_OF_EDGE},${DeviceInfoOfEdge.TOTAL_DISC_SPACE_OF_EDGE}`,
  74 + startTs: moment(moment().subtract(1, 'minute')).valueOf(),
  75 + timeWindow: 3600000,
  76 + interval: 10000,
  77 + intervalType: 'MILLISECONDS',
  78 + // limit: 360,
  79 + timeZoneId: 'Asia/Shanghai',
  80 + agg: 'AVG',
  81 + unsubscribe: false,
  82 + },
  83 + ],
  84 + };
  85 + });
  86 +
  87 + const chartInstance = ref<ChartInstance[]>([
  88 + {
  89 + name: 'CPU',
  90 + text: 'CPU使用率',
  91 + currentValue: 0,
  92 + totalKey: DeviceInfoOfEdge.CPU_COUNT_OF_EDGE,
  93 + totalCount: {},
  94 + type: DeviceInfoOfEdge.CPU_USAGE_OF_EDGE,
  95 + xAxisData: [],
  96 + seriesData: [],
  97 + icon: cpuSvg,
  98 + },
  99 + {
  100 + name: '内存',
  101 + text: '内存使用率',
  102 + currentValue: 0,
  103 + totalKey: DeviceInfoOfEdge.TOTAL_MEMORY_OF_EDGE,
  104 + totalCount: {},
  105 + type: DeviceInfoOfEdge.MEMORY_USAGE_OF_EDGE,
  106 + xAxisData: [],
  107 + seriesData: [],
  108 + icon: memorySvg,
  109 + },
  110 + {
  111 + name: '磁盘',
  112 + text: '磁盘使用率',
  113 + currentValue: 0,
  114 + totalKey: DeviceInfoOfEdge.TOTAL_DISC_SPACE_OF_EDGE,
  115 + totalCount: {},
  116 + type: DeviceInfoOfEdge.DISC_USAGE_OF_EDGE,
  117 + xAxisData: [],
  118 + seriesData: [],
  119 + icon: diskSvg,
  120 + },
  121 + ]);
  122 +
  123 + const chartsInstance = shallowReactive<{ [key: string]: echarts.ECharts }>({});
  124 +
  125 + // const cacheUsedSpace = (total, percentage) => {
  126 + // if (!total) return;
  127 + // const takeValue = total.split('.')[0];
  128 + // const takeUnit = total.split('.')[1];
  129 + // return `${(takeValue * (percentage / 100)).toFixed(1)}${takeUnit}`;
  130 + // };
  131 +
  132 + const { send, close, data, open } = useWebSocket(socketInfo.origin, {
  133 + immediate: false,
  134 + autoReconnect: true,
  135 + async onConnected() {
  136 + getSendValue.value.tsSubCmds[0].entityId = socketInfo.entityId;
  137 + send(JSON.stringify(unref(getSendValue)));
  138 + },
  139 + async onMessage() {
  140 + try {
  141 + const value = JSON.parse(unref(data)) as any;
  142 + if (value) {
  143 + const { data } = value;
  144 + const keys = Object.keys(data);
  145 + for (const key of keys) {
  146 + chartInstance.value.forEach((chartItem: ChartInstance) => {
  147 + if (chartItem.type === key) {
  148 + chartItem.xAxisData.push(moment(data[key][0][0]).format('HH:mm'));
  149 + // chartItem.seriesData = [];
  150 + chartItem.seriesData = data[key][0][1] ? [data[key][0][1]] : [];
  151 + chartItem.currentValue = Number(data[key][0][1]);
  152 + }
  153 + if (chartItem.totalKey === key) {
  154 + chartItem.totalCount.cpuTotal =
  155 + key === DeviceInfoOfEdge.CPU_COUNT_OF_EDGE ? data[key][0][1] : 0;
  156 + chartItem.totalCount.totalDisc =
  157 + key === DeviceInfoOfEdge.TOTAL_DISC_SPACE_OF_EDGE
  158 + ? formatSizeUnits(Number(data[key][0][1]))
  159 + : 0;
  160 + chartItem.totalCount.totalMemory =
  161 + key === DeviceInfoOfEdge.TOTAL_MEMORY_OF_EDGE
  162 + ? formatSizeUnits(Number(data[key][0][1]))
  163 + : 0;
  164 + }
  165 + });
  166 + }
  167 + await nextTick();
  168 + handleRenderChartInstance(chartInstance.value);
  169 + }
  170 + } catch (error) {}
  171 + },
  172 + onDisconnected() {},
  173 + onError() {
  174 + createMessage.error('webSocket连接超时,请联系管理员');
  175 + },
  176 + });
  177 +
  178 + function onResize(type) {
  179 + if (!chartsInstance[type]) return;
  180 + chartsInstance[type]?.resize();
  181 + }
  182 +
  183 + const chartOption = {
  184 + series: [
  185 + {
  186 + data: [],
  187 + detail: {
  188 + formatter: `暂无数据`,
  189 + },
  190 + type: 'gauge',
  191 + axisLine: {
  192 + lineStyle: {
  193 + width: 10,
  194 + color: [
  195 + [0.2, '#739ded'],
  196 + [0.8, '#5f89d8'],
  197 + [1, '#377dff'],
  198 + ],
  199 + },
  200 + },
  201 + pointer: {
  202 + itemStyle: {
  203 + color: 'auto',
  204 + },
  205 + },
  206 + axisTick: {
  207 + distance: 0,
  208 + length: 8,
  209 + lineStyle: {
  210 + color: 'auto',
  211 + },
  212 + },
  213 + splitLine: {
  214 + distance: 0,
  215 + length: 10,
  216 + lineStyle: {
  217 + color: 'auto',
  218 + width: 3,
  219 + },
  220 + },
  221 + axisLabel: {
  222 + color: 'inherit',
  223 + distance: 15,
  224 + fontSize: 8,
  225 + },
  226 + },
  227 + ],
  228 + };
  229 + const pieOption = {
  230 + grid: {
  231 + top: 0,
  232 + bottom: 0,
  233 + left: 0,
  234 + right: 0,
  235 + },
  236 + tooltip: {
  237 + trigger: 'item',
  238 + formatter: '{b}<br/>{d}%',
  239 + textStyle: {
  240 + fontSize: 12,
  241 + },
  242 + },
  243 + title: {
  244 + text: '磁盘容量',
  245 + subtext: '暂无数据',
  246 + textStyle: {
  247 + fontSize: 12,
  248 + color: '#72767c',
  249 + },
  250 + subtextStyle: {
  251 + fontSize: 14,
  252 + color: '#000000',
  253 + fontWeight: 500,
  254 + },
  255 + textAlign: 'center',
  256 + left: '48.5%',
  257 + top: '44%',
  258 + },
  259 + legend: {
  260 + show: true,
  261 + icon: 'circle',
  262 +
  263 + bottom: '0%',
  264 + data: [{ name: '磁盘已使用' }, { name: '磁盘剩余空间' }].map((item) => ({
  265 + name: item.name,
  266 + })),
  267 + formatter(name: string) {
  268 + return `{b_style|${name}} `;
  269 + },
  270 + textStyle: {
  271 + color: '#000',
  272 + rich: {
  273 + b_style: {
  274 + color: '#8d8ea0',
  275 + fontSize: 12,
  276 + padding: [0, 5, 0, 0],
  277 + },
  278 + },
  279 + },
  280 + },
  281 + series: [
  282 + {
  283 + type: 'pie',
  284 + // center: ['35%', '50%'],
  285 + radius: ['40%', '67%'],
  286 + startAngle: 30,
  287 + emphasis: {
  288 + scale: false,
  289 + },
  290 + label: {
  291 + position: 'outside',
  292 + alignTo: 'labelLine',
  293 + height: 0,
  294 + width: 0,
  295 + lineHeight: 0,
  296 + distanceToLabelLine: 75,
  297 + borderRadius: 3,
  298 + borderWidth: 1,
  299 + borderColor: 'none',
  300 + padding: [20, -15, 0, -10],
  301 + rich: {
  302 + a: {
  303 + padding: [0, -80, 55, -80],
  304 + fontSize: '12px',
  305 + color: '#000000',
  306 + },
  307 + b: {
  308 + padding: [20, -80, 40, -80],
  309 + fontSize: '12px',
  310 + color: '#72767c',
  311 + },
  312 + },
  313 + // formatter: (params: any) => {
  314 + // const { data } = params;
  315 + // const { value } = data || {};
  316 +
  317 + // const total = Number(totalCount.totalDisc?.split('.')[0] || 0);
  318 + // const totalUnit = totalCount.totalDisc?.split('.')[1] || 0;
  319 +
  320 + // return `{a|${value && total ? value : ''}${value && total ? '%' : ''}} \n {b|${
  321 + // total ? (total * (value / 100)).toFixed(1) : ''
  322 + // }${total ? totalUnit : ''}}`;
  323 + // },
  324 + },
  325 + labelLine: {
  326 + show: false,
  327 + length: 5,
  328 + // align: 'bottom',
  329 + lineStyle: {
  330 + width: 1,
  331 + },
  332 + },
  333 + data: [
  334 + { name: '磁盘已使用', value: 0, itemStyle: { color: '#90b2f1' } },
  335 + { name: '磁盘剩余空间', value: 100, itemStyle: { color: '#377dff' } },
  336 + ],
  337 + },
  338 + ],
  339 + };
  340 +
  341 + const handleRenderChartInstance = async (chartInstance: ChartInstance[]) => {
  342 + await nextTick();
  343 + if (!chartInstance) return;
  344 + if (isArray(chartInstance) && chartInstance.length <= 0) return;
  345 + for (const item of unref(chartInstance)) {
  346 + const { type, seriesData, currentValue, totalCount } = item;
  347 + // chartsInstance[type] = echarts.init(document.getElementById(`chart-${type}`) as HTMLElement);
  348 + if (type !== DeviceInfoOfEdge.DISC_USAGE_OF_EDGE) {
  349 + chartsInstance[type].setOption({
  350 + series: [
  351 + {
  352 + data: seriesData,
  353 + detail: {
  354 + formatter: `${isNumber(currentValue) ? '{value} %' : '暂无数据'}`,
  355 + },
  356 + },
  357 + ],
  358 + });
  359 + } else {
  360 + chartsInstance[type].setOption({
  361 + title: {
  362 + subtext: totalCount.totalDisc,
  363 + },
  364 + series: [
  365 + {
  366 + label: {
  367 + formatter: (params: any) => {
  368 + const { data } = params;
  369 + const { value } = data || {};
  370 +
  371 + const total = Number(totalCount.totalDisc?.split('.')[0] || 0);
  372 + const totalUnit = totalCount.totalDisc?.split('.')[1] || 0;
  373 +
  374 + return `{a|${value && total ? value : ''}${value && total ? '%' : ''}} \n {b|${
  375 + total ? (total * (value / 100)).toFixed(1) : ''
  376 + }${total ? totalUnit : ''}}`;
  377 + },
  378 + },
  379 + data: [
  380 + { name: '磁盘已使用', value: currentValue, itemStyle: { color: '#90b2f1' } },
  381 + {
  382 + name: '磁盘剩余空间',
  383 + value: 100 - currentValue,
  384 + itemStyle: { color: '#377dff' },
  385 + },
  386 + ],
  387 + },
  388 + ],
  389 + });
  390 + }
  391 +
  392 + window.addEventListener('resize', () => onResize(type));
  393 + }
  394 + };
  395 +
  396 + onUnmounted(() => {
  397 + window.removeEventListener('resize', onResize);
  398 + });
  399 +
  400 + onMounted(() => {
  401 + socketInfo.entityId = props.recordData?.id?.id as string;
  402 + if (socketInfo.entityId) {
  403 + open();
  404 + [
  405 + DeviceInfoOfEdge.CPU_USAGE_OF_EDGE,
  406 + DeviceInfoOfEdge.MEMORY_USAGE_OF_EDGE,
  407 + DeviceInfoOfEdge.DISC_USAGE_OF_EDGE,
  408 + ].forEach((item) => {
  409 + chartsInstance[item] = echarts.init(
  410 + document.getElementById(`chart-${item}`) as HTMLElement
  411 + );
  412 + if (item !== DeviceInfoOfEdge.DISC_USAGE_OF_EDGE) {
  413 + chartsInstance[item]?.setOption(chartOption);
  414 + } else {
  415 + chartsInstance[item]?.setOption(pieOption);
  416 + }
  417 + });
  418 + }
  419 + });
  420 +
  421 + onUnmounted(() => close());
  422 +</script>
  423 +
  424 +<template>
  425 + <div>
  426 + <a-row justify="space-around" align="middle" :gutter="{ xs: 8, sm: 16, md: 24, lg: 32 }">
  427 + <a-col
  428 + class="gutter-row"
  429 + style="background: #f5f5f5; border-radius: 20px"
  430 + :span="7"
  431 + v-for="(item, index) in chartInstance"
  432 + :key="index"
  433 + >
  434 + <a-row align="middle">
  435 + <div class="!flex justify-between items-center font-bold fill-dark-900 p-2.5 mt-4">
  436 + <img :src="item.icon" />
  437 + <span class="ml-1">{{ item.text }}</span>
  438 + <!-- <span>{{ item.currentValue }}%</span>
  439 + <span v-if="item.type !== DeviceInfoOfEdge.DISC_USAGE_OF_EDGE"
  440 + >{{
  441 + item.type === DeviceInfoOfEdge.CPU_USAGE_OF_EDGE
  442 + ? item.totalCount.cpuTotal
  443 + : item.type === DeviceInfoOfEdge.MEMORY_USAGE_OF_EDGE
  444 + ? item.totalCount.totalMemory
  445 + : 0
  446 + }}{{ item.type === DeviceInfoOfEdge.CPU_USAGE_OF_EDGE ? 'cores' : '' }}</span
  447 + >
  448 + <span v-if="item.type === DeviceInfoOfEdge.DISC_USAGE_OF_EDGE" style="color: #d46b08"
  449 + >已用{{ cacheUsedSpace(item.totalCount.totalDisc, item.currentValue) }}</span
  450 + >
  451 + <span v-if="item.type === DeviceInfoOfEdge.DISC_USAGE_OF_EDGE" style="color: #1677ff"
  452 + >可用{{ item.totalCount.totalDisc }}</span
  453 + > -->
  454 + </div>
  455 + </a-row>
  456 + <a-row class="mt-8" justify="space-around" align="middle">
  457 + <div class="flex w-full justify-center relative">
  458 + <Empty
  459 + description="暂无数据"
  460 + class="text-dark-50 m-4 absolute"
  461 + :style="{ display: item.seriesData.length == 0 ? 'block' : 'none' }"
  462 + />
  463 + <div
  464 + :id="`chart-${item.type}`"
  465 + class="m-4 w-9/10 h-300px"
  466 + :style="{ opacity: item.seriesData.length > 0 ? 1 : 0 }"
  467 + ></div>
  468 + </div>
  469 + </a-row>
  470 + </a-col>
  471 + </a-row>
  472 + </div>
  473 +</template>
... ...
  1 +export enum PermissionEnum {
  2 + CREATE = 'api:yt:task_center:add:post',
  3 + UPDATE = 'api:yt:task_center:update:update',
  4 + START_TASK = 'api:yt:task_center:update:state',
  5 + DELETE = 'api:yt:task_center:delete',
  6 + ALLOW = 'api:yt:task_center:cancel:allow',
  7 + EXECUTE = 'api:yt:task_center:immediate:execute',
  8 + DETAIL = 'api:yt:task_center:get',
  9 +}
  10 +
  11 +export enum HandleOperationEnum {
  12 + CREATE = 'create',
  13 + VIEW = 'view',
  14 + UPDATE = 'update',
  15 + BATCH_DELETE = 'batchDelete',
  16 + DELETE = 'singleDelete',
  17 +}
  18 +
  19 +export enum HandleOperationNameEnum {
  20 + CREATE = '新增',
  21 + VIEW = '查看',
  22 + UPDATE = '编辑',
  23 + BATCH_DELETE = '批量删除',
  24 + DELETE = '删除',
  25 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { EnumTableCardMode } from '/@/components/Widget';
  3 + import { CardMode } from './components/CardMode';
  4 +</script>
  5 +
  6 +<template>
  7 + <section>
  8 + <CardMode :mode="EnumTableCardMode.CARD" />
  9 + </section>
  10 +</template>
... ...
... ... @@ -3,7 +3,12 @@
3 3 import { BasicForm, useForm } from '/@/components/Form';
4 4 import { ALG, formSchema, PackageField } from '../config/packageDetail.config';
5 5 import { ref } from 'vue';
6   - import { createOtaPackage, uploadOtaPackages, deleteOtaPackage } from '/@/api/ota';
  6 + import {
  7 + createOtaPackage,
  8 + uploadOtaPackages,
  9 + deleteOtaPackage,
  10 + getDeviceProfileInfo,
  11 + } from '/@/api/ota';
7 12 import { CreateOtaPackageParams } from '/@/api/ota/model';
8 13
9 14 interface FieldsValue extends CreateOtaPackageParams {
... ... @@ -14,6 +19,8 @@
14 19
15 20 const emit = defineEmits(['register', 'update:list']);
16 21
  22 + const defaultDeviceProfile = 'default';
  23 +
17 24 const loading = ref(false);
18 25
19 26 const [registerModal, { changeLoading, closeModal }] = useModalInner();
... ... @@ -72,6 +79,10 @@
72 79 try {
73 80 await validate();
74 81 const value = getFieldsValue();
  82 + if (value[PackageField.DEVICE_PROFILE_INFO] === defaultDeviceProfile) {
  83 + const data = await getDeviceProfileInfo(defaultDeviceProfile);
  84 + value[PackageField.DEVICE_PROFILE_INFO] = data.id.id;
  85 + }
75 86 value[PackageField.DEVICE_PROFILE_INFO] = {
76 87 id: value[PackageField.DEVICE_PROFILE_INFO],
77 88 entityType: 'DEVICE_PROFILE',
... ...
1   -import { getDeviceProfileInfo, getDeviceProfileInfos } from '/@/api/ota';
  1 +import { getDeviceProfileInfos } from '/@/api/ota';
2 2
3 3 import { FormSchema } from '/@/components/Form';
4 4 import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
  5 +import { createPickerSearch } from '/@/utils/pickerSearch';
5 6
6 7 export enum PackageField {
7 8 TITLE = 'title',
... ... @@ -107,39 +108,25 @@ export const formSchema: FormSchema[] = [
107 108 {
108 109 field: PackageField.DEVICE_PROFILE_INFO,
109 110 label: '所属产品',
110   - component: 'ApiSearchSelect',
  111 + component: 'ApiSelectScrollLoad',
111 112 helpMessage: ['上传的包仅适用于具有所选配置文件的设备'],
112 113 rules: [{ required: true, message: '所属产品为必填项' }],
113 114 defaultValue: 'default',
114   - componentProps: ({ formModel, formActionType }) => {
  115 + componentProps: () => {
115 116 return {
116 117 placeholder: '请选择所属产品',
117   - showSearch: true,
118 118 labelField: 'name',
119 119 valueField: 'id',
  120 + pagination: { page: 0, pageSize: 10 },
120 121 api: async (params: Recordable) => {
121   - const data = await getDeviceProfileInfos(params);
122   - return data.data.map((item) => ({
  122 + const { totalElements, data } = await getDeviceProfileInfos(params);
  123 + const dataList = data.map((item) => ({
123 124 ...item,
124 125 id: item.id.id,
125 126 }));
  127 + return { items: dataList, total: totalElements };
126 128 },
127   - params: (textSearch: string) => {
128   - return {
129   - page: 0,
130   - pageSize: 10,
131   - textSearch,
132   - };
133   - },
134   - queryApi: async (id = 'default') => {
135   - const data = await getDeviceProfileInfo(id);
136   -
137   - if (formModel[PackageField.DEVICE_PROFILE_INFO] === 'default' && id === 'default') {
138   - formActionType.setFieldsValue({ [PackageField.DEVICE_PROFILE_INFO]: data.id.id });
139   - }
140   -
141   - return { ...data, id: data.id.id };
142   - },
  129 + ...createPickerSearch(),
143 130 };
144 131 },
145 132 },
... ...
... ... @@ -13,7 +13,7 @@
13 13 import { BasicTable, useTable } from '/@/components/Table';
14 14 import { FETCH_SETTING } from '/@/components/Table/src/const';
15 15 import { useDesign } from '/@/hooks/web/useDesign';
16   - import { isFunction } from '/@/utils/is';
  16 + import { isArray, isFunction } from '/@/utils/is';
17 17
18 18 interface DeviceModel extends RawDeviceModal {
19 19 disabled?: boolean;
... ... @@ -225,7 +225,7 @@
225 225 return;
226 226 }
227 227 const { items } = await devicePage({ page: 1, pageSize: 10, ...params, selected: true });
228   - selectedTotalList.value = items;
  228 + selectedTotalList.value = isArray(props.value) && props.value.length === 0 ? [] : items;
229 229 });
230 230 </script>
231 231
... ...
... ... @@ -62,7 +62,7 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
62 62 const flowActionType = useVueFlow({
63 63 id,
64 64 maxZoom: 1,
65   - minZoom: 1,
  65 + minZoom: 0.5,
66 66 panOnScroll: true,
67 67 selectionMode: SelectionMode.Partial,
68 68
... ...
1   -import { h } from 'vue';
  1 +import { h, unref } from 'vue';
2 2 import { BasicColumn } from '/@/components/Table';
3 3 import { dateUtil } from '/@/utils/dateUtil';
4 4 import Icon from '/@/components/Icon';
5 5 import { Modal } from 'ant-design-vue';
6 6 import { JsonPreview } from '/@/components/CodeEditor';
  7 +import { CopyOutlined } from '@ant-design/icons-vue';
  8 +import { useClipboard } from '@vueuse/core';
  9 +import { useMessage } from '/@/hooks/web/useMessage';
7 10
8 11 const handleOpenJsonPreviewModal = (title: string, content: string) => {
9 12 Modal.info({
... ... @@ -13,6 +16,15 @@ const handleOpenJsonPreviewModal = (title: string, content: string) => {
13 16 });
14 17 };
15 18
  19 +const { copy, copied } = useClipboard({ legacy: true });
  20 +const { createMessage } = useMessage();
  21 +
  22 +async function copyText(text: string) {
  23 + await copy(text);
  24 +
  25 + unref(copied) ? createMessage.success('复制成功') : createMessage.error('复制失败');
  26 +}
  27 +
16 28 export const columns: BasicColumn[] = [
17 29 {
18 30 title: '事件时间',
... ... @@ -34,13 +46,30 @@ export const columns: BasicColumn[] = [
34 46 },
35 47 {
36 48 title: '实体类型',
37   - dataIndex: 'body.entityName',
  49 + dataIndex: 'body.entityType',
38 50 ellipsis: true,
39 51 },
40 52 {
41   - title: '消息ID',
  53 + title: '实体ID',
42 54 dataIndex: 'body.entityId',
43 55 ellipsis: true,
  56 + customRender: ({ text }: { text: string }) => {
  57 + return h('span', { class: 'flex items-center' }, [
  58 + h('span', { class: 'truncate' }, text),
  59 + h(CopyOutlined, { class: 'flex-shrink cursor-pointer', onClick: () => copyText(text) }),
  60 + ]);
  61 + },
  62 + },
  63 + {
  64 + title: '消息ID',
  65 + dataIndex: 'body.msgId',
  66 + ellipsis: true,
  67 + customRender: ({ text }: { text: string }) => {
  68 + return h('span', { class: 'flex items-center' }, [
  69 + h('span', { class: 'truncate' }, text),
  70 + h(CopyOutlined, { class: 'flex-shrink cursor-pointer', onClick: () => copyText(text) }),
  71 + ]);
  72 + },
44 73 },
45 74 {
46 75 title: '消息类型',
... ...
... ... @@ -80,7 +80,7 @@
80 80 <template>
81 81 <BasicDrawer
82 82 v-model:visible="visible"
83   - :width="!nodeData?.created ? '55%' : '40%'"
  83 + :width="!nodeData?.created ? '55%' : '50%'"
84 84 showFooter
85 85 showCancelBtn
86 86 showOkBtn
... ...
... ... @@ -21,7 +21,7 @@
21 21
22 22 <template>
23 23 <section>
24   - <ApiSelect v-bind="$attrs">
  24 + <ApiSelect class="!w-56" v-bind="$attrs">
25 25 <template #dropdownRender="{ menuNode }">
26 26 <Options :menuNode="menuNode" />
27 27 <Divider class="!my-2" />
... ...
... ... @@ -217,7 +217,7 @@ export const getFormSchemas = (hasAlarmNotify: Ref<boolean>): FormSchema[] => {
217 217 const { setFieldsValue } = formActionType;
218 218 return {
219 219 api: async () => {
220   - return await getDeviceProfile(deviceType);
  220 + return await getDeviceProfile(deviceType, true);
221 221 },
222 222 labelField: 'name',
223 223 valueField: 'id',
... ... @@ -283,7 +283,10 @@ export const getFormSchemas = (hasAlarmNotify: Ref<boolean>): FormSchema[] => {
283 283 return {
284 284 api: async (params: Record<'organizationId' | 'deviceProfileId', string>) => {
285 285 if (params.deviceProfileId && params.organizationId) {
286   - const result = await byOrganizationIdGetMasterDevice(params);
  286 + const result = await byOrganizationIdGetMasterDevice({
  287 + ...params,
  288 + isSceneLinkage: true,
  289 + });
287 290 if (result) {
288 291 return result.map((item) => ({
289 292 ...item,
... ...
... ... @@ -147,7 +147,7 @@ export const getFormSchemas = (
147 147 const { setFieldsValue } = formActionType;
148 148 return {
149 149 api: async () => {
150   - return await getDeviceProfile(deviceType);
  150 + return await getDeviceProfile(deviceType, true);
151 151 },
152 152 labelField: 'name',
153 153 valueField: 'id',
... ... @@ -251,7 +251,10 @@ export const getFormSchemas = (
251 251 return {
252 252 api: async (params: Record<'organizationId' | 'deviceProfileId', string>) => {
253 253 if (params.deviceProfileId && params.organizationId) {
254   - const result = await byOrganizationIdGetMasterDevice(params);
  254 + const result = await byOrganizationIdGetMasterDevice({
  255 + ...params,
  256 + isSceneLinkage: true,
  257 + });
255 258 if (result) {
256 259 return result.map((item) => ({
257 260 ...item,
... ...
... ... @@ -143,7 +143,12 @@
143 143 </div>
144 144 <Tooltip title="删除">
145 145 <Icon
146   - v-if="!disabledDrawer && flipFlopListElRef.length > 1"
  146 + v-if="
  147 + !disabledDrawer &&
  148 + (type === FlipFlopComponentTypeEnum.FLIP_FLOP
  149 + ? flipFlopListElRef.length > 1
  150 + : true)
  151 + "
147 152 class="ml-2 cursor-pointer"
148 153 icon="fluent:delete-off-20-regular"
149 154 size="20"
... ...
... ... @@ -17,4 +17,5 @@ export interface SceneLinkageDataType {
17 17 name: string;
18 18 description?: string;
19 19 organizationId: string;
  20 + isSceneLinkage?: Boolean;
20 21 }
... ...
  1 +import { FileItem } from '../types';
  2 +import { uploadThumbnail } from '/@/api/configuration/center/configurationCenter';
1 3 import type { FormSchema } from '/@/components/Form/index';
  4 +import { createImgPreview } from '/@/components/Preview';
2 5 export const schemas: FormSchema[] = [
3 6 {
4 7 field: 'name',
... ... @@ -27,21 +30,99 @@ export const schemas: FormSchema[] = [
27 30 },
28 31 {
29 32 field: 'logo',
30   - component: 'Upload',
31 33 label: 'APP Logo',
32   - colProps: {
33   - span: 24,
  34 + // component: 'Upload',
  35 + // colProps: {
  36 + // span: 24,
  37 + // },
  38 + // slot: 'logoUpload',
  39 + helpMessage: ['支持.PNG、.JPG格式,建议尺寸为32*32px,大小不超过5M '],
  40 + component: 'ApiUpload',
  41 + changeEvent: 'update:fileList',
  42 + valueField: 'fileList',
  43 + componentProps: ({ formModel }) => {
  44 + return {
  45 + listType: 'picture-card',
  46 + maxFileLimit: 1,
  47 + accept: '.png,.jpg,.jpeg,.gif',
  48 + api: async (file: File) => {
  49 + try {
  50 + const formData = new FormData();
  51 + formData.set('file', file);
  52 + const { fileStaticUri, fileName } = await uploadThumbnail(formData);
  53 + return {
  54 + uid: fileStaticUri,
  55 + name: fileName,
  56 + url: fileStaticUri,
  57 + } as FileItem;
  58 + } catch (error) {
  59 + return {};
  60 + }
  61 + },
  62 + // showUploadList: true,
  63 + onDownload() {},
  64 + onPreview: (fileList: FileItem) => {
  65 + createImgPreview({ imageList: [fileList.url!] });
  66 + },
  67 + onDelete(url: string) {
  68 + formModel.deleteLogoUrl = url!;
  69 + },
  70 + };
34 71 },
35   - slot: 'logoUpload',
36 72 },
37 73 {
38   - field: 'background',
  74 + field: 'deleteLogoUrl',
  75 + label: '',
39 76 component: 'Input',
  77 + show: false,
  78 + },
  79 + {
  80 + field: 'background',
40 81 label: '登录页背景图片',
41   - colProps: {
42   - span: 24,
  82 + // component: 'Input',
  83 + // colProps: {
  84 + // span: 24,
  85 + // },
  86 + // slot: 'bgUpload',
  87 + helpMessage: ['支持.PNG、.JPG格式,建议尺寸为1920*1080px,大小不超过5M'],
  88 + component: 'ApiUpload',
  89 + changeEvent: 'update:fileList',
  90 + valueField: 'fileList',
  91 + componentProps: ({ formModel }) => {
  92 + return {
  93 + listType: 'picture-card',
  94 + maxFileLimit: 1,
  95 + accept: '.png,.jpg,.jpeg,.gif,',
  96 + api: async (file: File) => {
  97 + try {
  98 + const formData = new FormData();
  99 + formData.set('file', file);
  100 + const { fileStaticUri, fileName } = await uploadThumbnail(formData);
  101 + return {
  102 + uid: fileStaticUri,
  103 + name: fileName,
  104 + url: fileStaticUri,
  105 + } as FileItem;
  106 + } catch (error) {
  107 + return {};
  108 + }
  109 + },
  110 + // showUploadList: true,
  111 + onDownload() {},
  112 + onPreview: (fileList: FileItem) => {
  113 + createImgPreview({ imageList: [fileList.url!] });
  114 + },
  115 + onDelete(url: string) {
  116 + formModel.deleteBackgroundUrl = url!;
  117 + },
  118 + };
43 119 },
44   - slot: 'bgUpload',
  120 + },
  121 + {
  122 + field: 'deleteBackgroundUrl',
  123 + label: '',
  124 + component: 'Input',
  125 + show: false,
45 126 },
46 127 {
47 128 field: 'backgroundColor',
... ...
  1 +import { FileItem } from '../types';
  2 +import { uploadThumbnail } from '/@/api/configuration/center/configurationCenter';
1 3 import type { FormSchema } from '/@/components/Form/index';
  4 +import { createImgPreview } from '/@/components/Preview';
2 5
3 6 export const schemas: FormSchema[] = [
4 7 {
... ... @@ -28,30 +31,148 @@ export const schemas: FormSchema[] = [
28 31 },
29 32 {
30 33 field: 'logo',
31   - component: 'Upload',
32 34 label: '平台Logo',
33   - colProps: {
34   - span: 24,
  35 + // component: 'Upload',
  36 + // colProps: {
  37 + // span: 24,
  38 + // },
  39 + // slot: 'logoUpload',
  40 + helpMessage: ['支持.PNG、.JPG格式,建议尺寸为32*32px,大小不超过5M '],
  41 + component: 'ApiUpload',
  42 + changeEvent: 'update:fileList',
  43 + valueField: 'fileList',
  44 + componentProps: ({ formModel }) => {
  45 + return {
  46 + listType: 'picture-card',
  47 + maxFileLimit: 1,
  48 + accept: '.png,.jpg,.jpeg,.gif',
  49 + api: async (file: File) => {
  50 + try {
  51 + const formData = new FormData();
  52 + formData.set('file', file);
  53 + const { fileStaticUri, fileName } = await uploadThumbnail(formData);
  54 + return {
  55 + uid: fileStaticUri,
  56 + name: fileName,
  57 + url: fileStaticUri,
  58 + } as FileItem;
  59 + } catch (error) {
  60 + return {};
  61 + }
  62 + },
  63 + // showUploadList: true,
  64 + onDownload() {},
  65 + onPreview: (fileList: FileItem) => {
  66 + createImgPreview({ imageList: [fileList.url!] });
  67 + },
  68 + onDelete(url: string) {
  69 + formModel.deleteLogoUrl = url!;
  70 + },
  71 + };
35 72 },
36   - slot: 'logoUpload',
  73 + },
  74 + {
  75 + field: 'deleteLogoUrl',
  76 + label: '',
  77 + component: 'Input',
  78 + show: false,
37 79 },
38 80 {
39 81 field: 'icon',
40   - component: 'Upload',
41 82 label: '浏览器ico图标',
42   - colProps: {
43   - span: 24,
  83 + // component: 'Upload',
  84 + // colProps: {
  85 + // span: 24,
  86 + // },
  87 + // slot: 'iconUpload',
  88 + helpMessage: ['支持.ICO格式,建议尺寸为16*16px '],
  89 + component: 'ApiUpload',
  90 + changeEvent: 'update:fileList',
  91 + valueField: 'fileList',
  92 + componentProps: ({ formModel }) => {
  93 + return {
  94 + listType: 'picture-card',
  95 + maxFileLimit: 1,
  96 + maxSize: 500 * 1024,
  97 + accept: '.icon,.ico',
  98 + api: async (file: File) => {
  99 + try {
  100 + const formData = new FormData();
  101 + formData.set('file', file);
  102 + const { fileStaticUri, fileName } = await uploadThumbnail(formData);
  103 + return {
  104 + uid: fileStaticUri,
  105 + name: fileName,
  106 + url: fileStaticUri,
  107 + } as FileItem;
  108 + } catch (error) {
  109 + return {};
  110 + }
  111 + },
  112 + // showUploadList: true,
  113 + onDownload() {},
  114 + onPreview: (fileList: FileItem) => {
  115 + createImgPreview({ imageList: [fileList.url!] });
  116 + },
  117 + onDelete(url: string) {
  118 + formModel.deleteIconUrl = url!;
  119 + },
  120 + };
44 121 },
45   - slot: 'iconUpload',
46 122 },
47 123 {
48   - field: 'background',
  124 + field: 'deleteIconUrl',
  125 + label: '',
49 126 component: 'Input',
  127 + show: false,
  128 + },
  129 + {
  130 + field: 'background',
50 131 label: '登录页背景图片',
51   - colProps: {
52   - span: 24,
  132 + // component: 'Input',
  133 + // colProps: {
  134 + // span: 24,
  135 + // },
  136 + // slot: 'bgUpload',
  137 + helpMessage: ['支持.PNG、.JPG格式,建议尺寸为1920*1080px以上,大小不超过5M'],
  138 + component: 'ApiUpload',
  139 + changeEvent: 'update:fileList',
  140 + valueField: 'fileList',
  141 + componentProps: ({ formModel }) => {
  142 + return {
  143 + listType: 'picture-card',
  144 + maxFileLimit: 1,
  145 + accept: '.png,.jpg,.jpeg,.gif,.jfif',
  146 + api: async (file: File) => {
  147 + try {
  148 + const formData = new FormData();
  149 + formData.set('file', file);
  150 + const { fileStaticUri, fileName } = await uploadThumbnail(formData);
  151 + return {
  152 + uid: fileStaticUri,
  153 + name: fileName,
  154 + url: fileStaticUri,
  155 + } as FileItem;
  156 + } catch (error) {
  157 + return {};
  158 + }
  159 + },
  160 + // showUploadList: true,
  161 + onDownload() {},
  162 + onPreview: (fileList: FileItem) => {
  163 + createImgPreview({ imageList: [fileList.url!] });
  164 + },
  165 + onDelete(url: string) {
  166 + formModel.deleteBackgroundUrl = url!;
  167 + },
  168 + };
53 169 },
54   - slot: 'bgUpload',
  170 + },
  171 + {
  172 + field: 'deleteBackgroundUrl',
  173 + label: '',
  174 + component: 'Input',
  175 + show: false,
55 176 },
56 177 {
57 178 field: 'backgroundColor',
... ...
1 1 import type { FormSchema } from '/@/components/Form/index';
2 2 import { getAreaList } from '/@/api/oem/index';
3 3 import { emailRule, phoneRule } from '/@/utils/rules';
  4 +import { uploadThumbnail } from '/@/api/configuration/center/configurationCenter';
  5 +import { FileItem } from '../types';
  6 +import { createImgPreview } from '/@/components/Preview';
4 7
5 8 export enum Level {
6 9 PROVINCE = 'PROVINCE',
... ... @@ -179,14 +182,53 @@ export const schemas: FormSchema[] = [
179 182 rules: phoneRule,
180 183 },
181 184 {
182   - field: 'qrcode',
  185 + field: 'qrCode',
183 186 label: '联系我们',
184 187 required: true,
185   - component: 'Select',
  188 + // component: 'Select',
  189 + // slot: 'qrcode',
  190 + component: 'ApiUpload',
  191 + changeEvent: 'update:fileList',
  192 + valueField: 'fileList',
186 193 colProps: {
187 194 span: 24,
188 195 },
189   - slot: 'qrcode',
  196 + helpMessage: ['支持.PNG、.JPG格式,建议尺寸为300*300px,大小不超过5M '],
  197 + componentProps: ({ formModel }) => {
  198 + return {
  199 + listType: 'picture-card',
  200 + maxFileLimit: 1,
  201 + accept: '.png,.jpg,.jpeg,.gif',
  202 + api: async (file: File) => {
  203 + try {
  204 + const formData = new FormData();
  205 + formData.set('file', file);
  206 + const { fileStaticUri, fileName } = await uploadThumbnail(formData);
  207 + return {
  208 + uid: fileStaticUri,
  209 + name: fileName,
  210 + url: fileStaticUri,
  211 + } as FileItem;
  212 + } catch (error) {
  213 + return {};
  214 + }
  215 + },
  216 + // showUploadList: true,
  217 + onDownload() {},
  218 + onPreview: (fileList: FileItem) => {
  219 + createImgPreview({ imageList: [fileList.url!] });
  220 + },
  221 + onDelete(url: string) {
  222 + formModel.deleteUrl = url!;
  223 + },
  224 + };
  225 + },
  226 + },
  227 + {
  228 + field: 'deleteUrl',
  229 + label: '',
  230 + component: 'Input',
  231 + show: false,
190 232 },
191 233 {
192 234 field: 'id',
... ...
... ... @@ -3,7 +3,7 @@
3 3 <Card :bordered="false" class="card">
4 4 <div style="margin-left: -40px">
5 5 <BasicForm @register="registerForm">
6   - <template #logoUpload>
  6 + <!-- <template #logoUpload>
7 7 <ContentUploadText>
8 8 <template #uploadImg>
9 9 <CustomUploadComp :imgUrl="logoPic" @setImg="handleSetLogoImgUrl" />
... ... @@ -24,7 +24,7 @@
24 24 </div>
25 25 </template>
26 26 </ContentUploadText>
27   - </template>
  27 + </template> -->
28 28 <template #colorInput="{ model, field }"
29 29 ><Input disabled v-model:value="model[field]">
30 30 <template #prefix> <input type="color" v-model="model[field]" /> </template
... ... @@ -41,6 +41,7 @@
41 41 :customRequest="customUploadHomeSwiper"
42 42 :before-upload="beforeUploadHomeSwiper"
43 43 @change="handleChange"
  44 + :remove="handleRemove"
44 45 >
45 46 <div v-if="fileList.length < 5">
46 47 <div>
... ... @@ -74,7 +75,7 @@
74 75 </template>
75 76
76 77 <script lang="ts">
77   - import { defineComponent, ref, unref, onMounted } from 'vue';
  78 + import { defineComponent, ref, onMounted, unref } from 'vue';
78 79 import { BasicForm, useForm } from '/@/components/Form/index';
79 80 import { Loading } from '/@/components/Loading/index';
80 81 import { Card, Upload, Input, Modal } from 'ant-design-vue';
... ... @@ -86,7 +87,9 @@
86 87 import { getAppDesign, updateAppDesign } from '/@/api/oem/index';
87 88 import { Authority } from '/@/components/Authority';
88 89 import ContentUploadText from './ContentUploadText.vue';
89   - import { CustomUploadComp } from './customUplaod/index';
  90 + // import { CustomUploadComp } from './customUplaod/index';
  91 + import { buildUUID } from '/@/utils/uuid';
  92 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
90 93
91 94 export default defineComponent({
92 95 components: {
... ... @@ -99,7 +102,7 @@
99 102 Modal,
100 103 Authority,
101 104 ContentUploadText,
102   - CustomUploadComp,
  105 + // CustomUploadComp,
103 106 },
104 107 setup() {
105 108 const loading = ref(false);
... ... @@ -193,19 +196,42 @@
193 196 }
194 197 };
195 198
  199 + const deleteHomeUrl = ref<any[]>([]);
  200 + const handleRemove = (file: FileItem) => {
  201 + deleteHomeUrl.value.push(file.url);
  202 + };
  203 +
196 204 const handleUpdateInfo = async () => {
197 205 try {
198 206 const fieldValue = getFieldsValue();
  207 + if (Reflect.has(fieldValue, 'logo')) {
  208 + const file = (fieldValue.logo || []).at(0) || {};
  209 + fieldValue.logo = file.url || '';
  210 + }
  211 + if (Reflect.has(fieldValue, 'background')) {
  212 + const file = (fieldValue.background || []).at(0) || {};
  213 + fieldValue.background = file.url || '';
  214 + }
199 215 // 做换字段
200 216 const homeSwiper = fileList.value.map((item) => item.url);
201 217 const rotation = homeSwiper.join(',');
202 218
203 219 compState.value.loading = true;
  220 + if (Reflect.has(fieldValue, 'deleteLogoUrl') && fieldValue.deleteLogoUrl) {
  221 + await deleteFilePath(fieldValue?.deleteLogoUrl);
  222 + Reflect.deleteProperty(fieldValue, 'deleteLogoUrl');
  223 + }
  224 + if (Reflect.has(fieldValue, 'deleteBackgroundUrl') && fieldValue.deleteBackgroundUrl) {
  225 + await deleteFilePath(fieldValue?.deleteBackgroundUrl);
  226 + Reflect.deleteProperty(fieldValue, 'deleteBackgroundUrl');
  227 + }
  228 + if (unref(deleteHomeUrl)?.length) {
  229 + await Promise.all(unref(deleteHomeUrl).map((item) => deleteFilePath(item)));
  230 + }
204 231 await updateAppDesign({
205 232 ...fieldValue,
206   - background: unref(bgPic),
207   - icon: unref(bgPic),
208   - logo: unref(logoPic),
  233 + background: fieldValue.background || '',
  234 + logo: fieldValue.logo || '',
209 235 rotation,
210 236 });
211 237 compState.value.loading = false;
... ... @@ -227,9 +253,15 @@
227 253 status: 'done',
228 254 });
229 255 }
  256 + if (res.logo) {
  257 + res.logo = [{ uid: buildUUID(), name: 'name', url: res.logo } as FileItem];
  258 + }
  259 + if (res.background) {
  260 + res.background = [{ uid: buildUUID(), name: 'name', url: res.background } as FileItem];
  261 + }
230 262 setFieldsValue(res);
231   - logoPic.value = res.logo;
232   - bgPic.value = res.background;
  263 + // logoPic.value = res.logo;
  264 + // bgPic.value = res.background;
233 265 if (arr[0]?.url === '') return;
234 266 fileList.value = arr;
235 267 });
... ... @@ -252,8 +284,8 @@
252 284 });
253 285 }
254 286 setFieldsValue(res);
255   - logoPic.value = res.logo;
256   - bgPic.value = res.background;
  287 + // logoPic.value = res.logo;
  288 + // bgPic.value = res.background;
257 289 if (arr[0]?.url === '') return;
258 290 fileList.value = arr;
259 291 } catch (e) {
... ... @@ -271,8 +303,8 @@
271 303 customUploadHomeSwiper,
272 304 beforeUploadHomeSwiper,
273 305 handleChange,
274   - logoPic,
275   - bgPic,
  306 + // logoPic,
  307 + // bgPic,
276 308 previewVisible,
277 309 previewImage,
278 310 loading,
... ... @@ -280,6 +312,7 @@
280 312 handleResetInfo,
281 313 handleSetBgImgUrl,
282 314 handleSetLogoImgUrl,
  315 + handleRemove,
283 316 };
284 317 },
285 318 });
... ...
... ... @@ -74,13 +74,13 @@
74 74 </template>
75 75
76 76 <script lang="ts">
77   - import { defineComponent, ref, onMounted, unref } from 'vue';
  77 + import { defineComponent, ref, onMounted } from 'vue';
78 78 import { Card, Upload, Input, Spin } from 'ant-design-vue';
79 79 import { BasicForm, useForm } from '/@/components/Form/index';
80 80 import { schemas } from '../config/CVIDraw.config';
81 81 import { Loading } from '/@/components/Loading/index';
82 82 import { useMessage } from '/@/hooks/web/useMessage';
83   - import type { FileItem } from '/@/components/Upload/src/typing';
  83 + // import type { FileItem } from '/@/components/Upload/src/typing';
84 84 import { iconUpload, getPlatForm, updatePlatForm, resetPlateInfo } from '/@/api/oem/index';
85 85 import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons-vue';
86 86 import { useUserStore } from '/@/store/modules/user';
... ... @@ -88,6 +88,9 @@
88 88 import { Authority } from '/@/components/Authority';
89 89 import ContentUploadText from './ContentUploadText.vue';
90 90 import { CustomUploadComp } from './customUplaod/index';
  91 + import { buildUUID } from '/@/utils/uuid';
  92 + import { FileItem } from '../types';
  93 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
91 94
92 95 export default defineComponent({
93 96 components: {
... ... @@ -166,18 +169,42 @@
166 169 const handleUpdateInfo = async () => {
167 170 try {
168 171 const fieldValue = getFieldsValue();
  172 + if (Reflect.has(fieldValue, 'logo')) {
  173 + const file = (fieldValue.logo || []).at(0) || {};
  174 + fieldValue.logo = file.url || null;
  175 + }
  176 + if (Reflect.has(fieldValue, 'icon')) {
  177 + const file = (fieldValue.icon || []).at(0) || {};
  178 + fieldValue.icon = file.url || null;
  179 + }
  180 + if (Reflect.has(fieldValue, 'background')) {
  181 + const file = (fieldValue.background || []).at(0) || {};
  182 + fieldValue.background = file.url || null;
  183 + }
169 184 compState.value.loading = true;
170   - const newFieldValue = {
  185 + if (Reflect.has(fieldValue, 'deleteLogoUrl') && fieldValue.deleteLogoUrl) {
  186 + await deleteFilePath(fieldValue?.deleteLogoUrl);
  187 + Reflect.deleteProperty(fieldValue, 'deleteLogoUrl');
  188 + }
  189 + if (Reflect.has(fieldValue, 'deleteIconUrl') && fieldValue.deleteIconUrl) {
  190 + await deleteFilePath(fieldValue?.deleteIconUrl);
  191 + Reflect.deleteProperty(fieldValue, 'deleteIconUrl');
  192 + }
  193 + if (Reflect.has(fieldValue, 'deleteBackgroundUrl') && fieldValue.deleteBackgroundUrl) {
  194 + await deleteFilePath(fieldValue?.deleteBackgroundUrl);
  195 + Reflect.deleteProperty(fieldValue, 'deleteBackgroundUrl');
  196 + }
  197 +
  198 + await updatePlatForm({
171 199 ...fieldValue,
172   - logo: unref(logoPic),
173   - icon: unref(iconPic),
174   - background: unref(bgPic),
175   - };
176   - await updatePlatForm(newFieldValue);
  200 + logo: fieldValue.logo || '',
  201 + icon: fieldValue.icon || '',
  202 + background: fieldValue.background || '',
  203 + });
177 204 compState.value.loading = false;
178 205 createMessage.success('保存信息成功');
179 206
180   - setPlatFormInfo(newFieldValue);
  207 + setPlatFormInfo(fieldValue);
181 208 } catch (e) {
182 209 createMessage.error('保存信息失败');
183 210 }
... ... @@ -197,6 +224,15 @@
197 224
198 225 onMounted(async () => {
199 226 const res = await getPlatForm();
  227 + if (res.logo) {
  228 + res.logo = [{ uid: buildUUID(), name: 'name', url: res.logo }] as any;
  229 + }
  230 + if (res.icon) {
  231 + res.icon = [{ uid: buildUUID(), name: 'name', url: res.icon }] as any;
  232 + }
  233 + if (res.background) {
  234 + res.background = [{ uid: buildUUID(), name: 'name', url: res.background }] as any;
  235 + }
200 236 setFieldsValue(res);
201 237 logoPic.value = res.logo;
202 238 iconPic.value = res.icon;
... ...
... ... @@ -2,7 +2,7 @@
2 2 <div class="card">
3 3 <Card :bordered="false" class="card">
4 4 <BasicForm @register="registerForm">
5   - <template #qrcode>
  5 + <!-- <template #qrcode>
6 6 <ContentUploadText>
7 7 <template #uploadImg>
8 8 <CustomUploadComp :imgUrl="qrcodePic" @setImg="handleSetCodeImgUrl" />
... ... @@ -11,7 +11,7 @@
11 11 <div class="box-outline"> 支持.PNG、.JPG格式,建议尺寸为300*300px,大小不超过5M </div>
12 12 </template>
13 13 </ContentUploadText>
14   - </template>
  14 + </template> -->
15 15 <template #customProv>
16 16 <BasicForm @register="registerCustomForm" />
17 17 </template>
... ... @@ -31,7 +31,7 @@
31 31 </template>
32 32
33 33 <script lang="ts">
34   - import { defineComponent, onMounted, ref, computed, watch } from 'vue';
  34 + import { defineComponent, onMounted, ref, computed } from 'vue';
35 35 import { Card } from 'ant-design-vue';
36 36 import { BasicForm, useForm } from '/@/components/Form/index';
37 37 import { schemas, provSchemas } from '../config/enterPriseInfo.config';
... ... @@ -43,8 +43,11 @@
43 43 import { Authority } from '/@/components/Authority';
44 44 import { USER_INFO_KEY } from '/@/enums/cacheEnum';
45 45 import { getAuthCache } from '/@/utils/auth';
46   - import ContentUploadText from './ContentUploadText.vue';
47   - import { CustomUploadComp } from './customUplaod/index';
  46 + // import ContentUploadText from './ContentUploadText.vue';
  47 + // import { CustomUploadComp } from './customUplaod/index';
  48 + import { buildUUID } from '/@/utils/uuid';
  49 + import { FileItem } from '../types';
  50 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
48 51
49 52 export default defineComponent({
50 53 components: {
... ... @@ -52,8 +55,8 @@
52 55 BasicForm,
53 56 Loading,
54 57 Authority,
55   - ContentUploadText,
56   - CustomUploadComp,
  58 + // ContentUploadText,
  59 + // CustomUploadComp,
57 60 },
58 61 setup() {
59 62 const userInfo: any = getAuthCache(USER_INFO_KEY);
... ... @@ -73,11 +76,8 @@
73 76 loading: false,
74 77 tip: '拼命加载中...',
75 78 });
76   - const [
77   - registerForm,
78   - { getFieldsValue, setFieldsValue, validate, clearValidate, validateFields },
79   - ] = useForm({
80   - labelWidth: 80,
  79 + const [registerForm, { getFieldsValue, setFieldsValue, validate, clearValidate }] = useForm({
  80 + labelWidth: 90,
81 81 schemas,
82 82 showResetButton: false,
83 83 showSubmitButton: false,
... ... @@ -100,26 +100,30 @@
100 100
101 101 const { createMessage } = useMessage();
102 102
103   - const qrcodePic = ref();
104   - const handleSetCodeImgUrl = (d) => {
105   - qrcodePic.value = d;
106   - };
107   - watch(
108   - () => qrcodePic.value,
109   - (newValue) => {
110   - if (!newValue) validateFields(['qrcode']);
111   - else clearValidate('qrcode');
112   - }
113   - );
  103 + // const qrcodePic = ref();
  104 + // const handleSetCodeImgUrl = (d) => {
  105 + // qrcodePic.value = d;
  106 + // };
  107 + // watch(
  108 + // () => qrcodePic.value,
  109 + // (newValue) => {
  110 + // if (!newValue) validateFields(['qrcode']);
  111 + // else clearValidate('qrcode');
  112 + // }
  113 + // );
114 114 // 更新
115 115 const handleUpdateInfo = async () => {
116 116 try {
117 117 const fieldsValue = getFieldsValue();
118 118 const { nameTown } = getNameTown();
  119 + if (Reflect.has(fieldsValue, 'qrCode')) {
  120 + const file = (fieldsValue.qrCode || []).at(0) || {};
  121 + fieldsValue.qrCode = file.url || null;
  122 + }
119 123 const newFieldValue: any = {
120 124 ...fieldsValue,
121 125 codeTown: nameTown,
122   - qrCode: qrcodePic.value,
  126 + // qrCode: qrcodePic.value,
123 127 };
124 128 delete newFieldValue.nameProv;
125 129 delete newFieldValue.nameCity;
... ... @@ -139,9 +143,9 @@
139 143 'id',
140 144 ];
141 145 if (newFieldValue.qrCode == undefined || newFieldValue.qrCode == '') {
142   - validateArray.push('qrcode');
  146 + validateArray.push('qrCode');
143 147 } else {
144   - const findExistIndex = validateArray.findIndex((o) => o == 'qrcode');
  148 + const findExistIndex = validateArray.findIndex((o) => o == 'qrCode');
145 149 if (findExistIndex !== -1) {
146 150 validateArray.splice(findExistIndex, 1);
147 151 }
... ... @@ -158,6 +162,10 @@
158 162 const values = await validate(validateArray);
159 163 if (!values) return;
160 164 compState.value.loading = true;
  165 + if (Reflect.has(newFieldValue, 'deleteUrl') && newFieldValue.deleteUrl) {
  166 + await deleteFilePath(newFieldValue?.deleteUrl);
  167 + Reflect.deleteProperty(newFieldValue, 'deleteUrl');
  168 + }
161 169 await updateEnterPriseDetail(newFieldValue);
162 170 createMessage.success('更新信息成功');
163 171 setEnterPriseInfo(newFieldValue);
... ... @@ -189,19 +197,22 @@
189 197 });
190 198 setFieldsValue({ nameCountry: codeCountry });
191 199 }
192   - setFieldsValue(res);
193   - qrcodePic.value = res.qrCode;
  200 + if (res.qrCode) {
  201 + res.qrCode = [{ uid: buildUUID(), name: 'name', url: res.qrCode } as FileItem];
  202 + }
  203 + setFieldsValue({ ...res, qrcode: res.qrCode });
  204 + // qrcodePic.value = res.qrCode;
194 205 });
195 206
196 207 return {
197 208 registerForm,
198 209 compState,
199   - qrcodePic,
200 210 handleUpdateInfo,
201 211 registerCustomForm,
202 212 loading,
203 213 isWhereAdmin,
204   - handleSetCodeImgUrl,
  214 + // qrcodePic,
  215 + // handleSetCodeImgUrl,
205 216 };
206 217 },
207 218 });
... ...
1 1 export interface FileItem {
  2 + size: number;
2 3 uid: string;
3 4 name?: string;
4 5 status?: string;
... ...
... ... @@ -65,8 +65,22 @@ export const formSchema: FormSchema[] = [
65 65 label: t('routes.common.common.sort'), //排序
66 66 component: 'InputNumber',
67 67 required: true,
  68 + dynamicRules: () => {
  69 + return [
  70 + {
  71 + required: true,
  72 + validator: (_, value) => {
  73 + if (Number(value) < -2147483648 || Number(value) > 2147483647) {
  74 + return Promise.reject('取值范围是-2147483648到2147483647');
  75 + }
  76 + return Promise.resolve();
  77 + },
  78 + },
  79 + ];
  80 + },
68 81 componentProps: {
69   - maxLength: 32,
  82 + min: -2147483648,
  83 + max: 2147483647,
70 84 },
71 85 },
72 86 {
... ...
... ... @@ -9,7 +9,7 @@ import {
9 9 import { PollCommandInput, ModeEnum } from '../PollCommandInput';
10 10 import { DeviceProfileModel } from '/@/api/device/model/deviceModel';
11 11 import { JSONEditorValidator } from '/@/components/CodeEditor/src/JSONEditor';
12   -import { BooleanStringEnum, TimeUnitEnum, TimeUnitNameEnum } from '/@/enums/toolEnum';
  12 +import { TimeUnitEnum, TimeUnitNameEnum } from '/@/enums/toolEnum';
13 13 import { dateUtil } from '/@/utils/dateUtil';
14 14 import { ProductPicker, validateProductPicker } from '../ProductPicker';
15 15 import { useGlobSetting } from '/@/hooks/setting';
... ... @@ -386,16 +386,15 @@ export const formSchemas: FormSchema[] = [
386 386 component: 'RadioGroup',
387 387 label: '时间单位',
388 388 ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.POLL,
389   - defaultValue:
390   - disabledTaskCenterExecuteIntervalUnitSecond === BooleanStringEnum.TRUE
391   - ? TimeUnitEnum.MINUTE
392   - : TimeUnitEnum.SECOND,
  389 + defaultValue: disabledTaskCenterExecuteIntervalUnitSecond
  390 + ? TimeUnitEnum.MINUTE
  391 + : TimeUnitEnum.SECOND,
393 392 componentProps: () => {
394 393 const options = [
395 394 { label: TimeUnitNameEnum.MINUTE, value: TimeUnitEnum.MINUTE },
396 395 { label: TimeUnitNameEnum.HOUR, value: TimeUnitEnum.HOUR },
397 396 ];
398   - if (disabledTaskCenterExecuteIntervalUnitSecond === BooleanStringEnum.FALSE) {
  397 + if (!disabledTaskCenterExecuteIntervalUnitSecond) {
399 398 options.unshift({ label: TimeUnitNameEnum.SECOND, value: TimeUnitEnum.SECOND });
400 399 }
401 400 return {
... ...
... ... @@ -65,7 +65,7 @@
65 65 unref(getRecord).id,
66 66 !unref(getRecord).state ? StateEnum.ENABLE : StateEnum.CLOSE
67 67 );
68   - createMessage.success('更新状态成功');
  68 + createMessage.success(`${unref(getRecord).state ? '禁用' : '启用'}成功`);
69 69 props.reload?.();
70 70 } catch (error) {
71 71 throw error;
... ...
... ... @@ -423,7 +423,7 @@ export const speedSchema: FormSchema[] = [
423 423 label: '传输租户信息',
424 424 colProps: { span: 12 },
425 425 component: 'CreateSpeed',
426   - defaultValue: '0',
  426 + // defaultValue: '0',
427 427 changeEvent: 'update:value',
428 428 componentProps: {
429 429 placeholder: '请输入(请输入数字)',
... ... @@ -432,7 +432,6 @@ export const speedSchema: FormSchema[] = [
432 432 },
433 433 {
434 434 field: 'transportDeviceMsgRateLimit',
435   - defaultValue: '0',
436 435 label: '传输设备信息',
437 436 colProps: { span: 12 },
438 437 component: 'CreateSpeed',
... ... @@ -443,7 +442,6 @@ export const speedSchema: FormSchema[] = [
443 442 },
444 443 {
445 444 field: 'transportTenantTelemetryMsgRateLimit',
446   - defaultValue: '0',
447 445 label: '传输租户遥测消息',
448 446 colProps: { span: 12 },
449 447 component: 'CreateSpeed',
... ... @@ -454,7 +452,6 @@ export const speedSchema: FormSchema[] = [
454 452 },
455 453 {
456 454 field: 'transportDeviceTelemetryMsgRateLimit',
457   - defaultValue: '0',
458 455 label: '传输设备遥测消息',
459 456 colProps: { span: 12 },
460 457 component: 'CreateSpeed',
... ... @@ -492,6 +489,7 @@ export const speedSchema: FormSchema[] = [
492 489 placeholder: '请输入(请输入数字)',
493 490 title: '租户REST请求',
494 491 },
  492 + defaultValue: '',
495 493 },
496 494 {
497 495 field: 'customerServerRestLimitsConfiguration',
... ... @@ -502,6 +500,7 @@ export const speedSchema: FormSchema[] = [
502 500 placeholder: '请输入(请输入数字)',
503 501 title: '客户REST请求',
504 502 },
  503 + defaultValue: '',
505 504 },
506 505 {
507 506 field: 'tenantEntityExportRateLimit',
... ... @@ -532,6 +531,7 @@ export const speedSchema: FormSchema[] = [
532 531 placeholder: '请输入(请输入数字)',
533 532 title: '会话WS更新',
534 533 },
  534 + defaultValue: '',
535 535 },
536 536 {
537 537 field: 'cassandraQueryTenantRateLimitsConfiguration',
... ... @@ -542,6 +542,7 @@ export const speedSchema: FormSchema[] = [
542 542 placeholder: '请输入(请输入数字)',
543 543 title: '租户Cassandra查询',
544 544 },
  545 + defaultValue: '',
545 546 },
546 547 {
547 548 field: 'tenantNotificationRequestsRateLimit',
... ...
... ... @@ -182,9 +182,9 @@
182 182 createdTime: isUpdate ? unref(createTime) : Date.now(),
183 183 };
184 184
185   - const defaultInfo = {
186   - default: unref(isUpdate) ? isDefault.value : false,
187   - };
  185 + // const defaultInfo = {
  186 + // default: unref(isUpdate) ? isDefault.value : false,
  187 + // };
188 188 Object.assign(
189 189 postAllData,
190 190 {
... ... @@ -192,8 +192,8 @@
192 192 },
193 193 getValuesFormData,
194 194 id,
195   - createTime1,
196   - defaultInfo
  195 + createTime1
  196 + // defaultInfo
197 197 );
198 198 if (!unref(isUpdate)) {
199 199 delete postAllData.id;
... ... @@ -206,14 +206,14 @@
206 206 if (!unref(isUpdate)) {
207 207 await getAllFieldsFunc();
208 208 // return;
209   - await saveTenantProfileApi(postAllData);
  209 + await saveTenantProfileApi({ ...postAllData, isolatedTbRuleEngine: null });
210 210 createMessage.success('租户配置新增成功');
211 211 closeDrawer();
212 212 emit('success');
213 213 resetFields();
214 214 } else {
215 215 await getAllFieldsFunc(true);
216   - await saveTenantProfileApi(postAllData);
  216 + await saveTenantProfileApi({ ...postAllData, isolatedTbRuleEngine: null });
217 217 createMessage.success('租户配置编辑成功');
218 218 closeDrawer();
219 219 emit('success');
... ...
... ... @@ -20,6 +20,7 @@
20 20 import { useMessage } from '/@/hooks/web/useMessage';
21 21 import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
22 22 import { buildUUID } from '/@/utils/uuid';
  23 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
23 24
24 25 export default defineComponent({
25 26 name: 'TenantDrawer',
... ... @@ -102,6 +103,10 @@
102 103 entityType: 'TENANT_PROFILE',
103 104 },
104 105 };
  106 + if (Reflect.has(values, 'deleteUrl') && values.deleteUrl) {
  107 + await deleteFilePath(values?.deleteUrl);
  108 + Reflect.deleteProperty(values, 'deleteUrl');
  109 + }
105 110 await updateOrCreateTenant(req);
106 111 emit('success');
107 112 createMessage.success(`${unref(isUpdate) ? '编辑' : '新增'}成功`);
... ...
... ... @@ -83,7 +83,7 @@ export const tenantFormSchema: FormSchema[] = [
83 83 component: 'ApiUpload',
84 84 changeEvent: 'update:fileList',
85 85 valueField: 'fileList',
86   - componentProps: () => {
  86 + componentProps: ({ formModel }) => {
87 87 return {
88 88 listType: 'picture-card',
89 89 maxFileLimit: 1,
... ... @@ -105,10 +105,19 @@ export const tenantFormSchema: FormSchema[] = [
105 105 onPreview: (fileList: FileItem) => {
106 106 createImgPreview({ imageList: [fileList.url!] });
107 107 },
  108 + onDelete(url: string) {
  109 + formModel.deleteUrl = url!;
  110 + },
108 111 };
109 112 },
110 113 },
111 114 {
  115 + field: 'deleteUrl',
  116 + label: '',
  117 + component: 'Input',
  118 + show: false,
  119 + },
  120 + {
112 121 field: 'name',
113 122 label: '租户名称',
114 123 required: true,
... ...
... ... @@ -56,7 +56,7 @@
56 56 </script>
57 57
58 58 <template>
59   - <BasicModal @register="register" title="组件设置" @ok="handleOk">
  59 + <BasicModal @register="register" title="组件设置" @ok="handleOk" :width="700">
60 60 <!-- -->
61 61 <component ref="settingFormEl" :is="getSettingComponent" />
62 62 </BasicModal>
... ...
... ... @@ -29,6 +29,8 @@
29 29 import { MessageAlert } from './components/MessageAlert';
30 30 import { createSelectWidgetKeysContext, createSelectWidgetModeContext } from './useContext';
31 31 import { useGetCategoryByComponentKey } from '../packages/hook/useGetCategoryByComponentKey';
  32 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
  33 + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
32 34
33 35 const props = defineProps<{
34 36 layout: Layout[];
... ... @@ -227,6 +229,20 @@
227 229 return `${category} / ${componentConfig.title}`;
228 230 });
229 231
  232 + const countElementOccurrences = (arr) => {
  233 + const countMap = {};
  234 +
  235 + arr.forEach((element) => {
  236 + if (countMap[element]) {
  237 + countMap[element]++;
  238 + } else {
  239 + countMap[element] = 1;
  240 + }
  241 + });
  242 +
  243 + return countMap;
  244 + };
  245 +
230 246 const handleSubmit = async () => {
231 247 const validateResult = await validate();
232 248 if (validateResult && !validateResult.flag) {
... ... @@ -241,6 +257,59 @@
241 257 }
242 258 }
243 259 const value = getFormValues();
  260 + const { record } = value || {};
  261 + try {
  262 + const currentRecordIconUrl = ref<any>([]);
  263 + // 判断当前自定义组件表单以前的自定义图片呢url
  264 + currentRecord.value?.dataSource.forEach((item) => {
  265 + if (item.componentInfo?.customIcon) {
  266 + item.componentInfo?.customIcon?.forEach((icon: FileItem) => {
  267 + currentRecordIconUrl.value.push(icon.url);
  268 + });
  269 + }
  270 + });
  271 + // 取当前修改过后的自定义图片url
  272 + const dataSourceUrl = record.dataSource?.map(
  273 + (item) => item.componentInfo.customIcon?.[0].url
  274 + );
  275 +
  276 + // 当前自定义组件取出要进行删除的图标url
  277 + const dataSourceDeleteUrl = unref(currentRecordIconUrl).filter(
  278 + (item) => !dataSourceUrl?.includes(item)
  279 + );
  280 +
  281 + //查询外部所有组件的自定义图标的url
  282 + const oldDataSource = props.layout;
  283 + const customIconUrls = ref<any>([]);
  284 + oldDataSource?.forEach((item: any) => {
  285 + item.dataSource?.forEach((dataSource) => {
  286 + if (dataSource.componentInfo?.customIcon) {
  287 + dataSource.componentInfo?.customIcon?.forEach((icon: FileItem) => {
  288 + customIconUrls.value.push(icon.url);
  289 + });
  290 + }
  291 + });
  292 + });
  293 + // const dataSourceDeleteUrl = record.dataSource?.map((item) => item.componentInfo.deleteUrl);
  294 +
  295 + if (unref(customIconUrls) && unref(customIconUrls).length && dataSourceDeleteUrl?.length) {
  296 + // 判断外部所有组件是否有dataSourceDeleteUrl使用中的url
  297 + const deletePromise = unref(customIconUrls)?.filter((item) =>
  298 + dataSourceDeleteUrl?.includes(item)
  299 + );
  300 + const deleteUrlInfo = countElementOccurrences(deletePromise);
  301 + const deleteUrl = deletePromise?.filter((item) => deleteUrlInfo?.[item] == 1);
  302 + Promise.all(
  303 + deleteUrl.map((item) => {
  304 + deleteFilePath(item);
  305 + })
  306 + );
  307 + }
  308 + } catch (err) {
  309 + // eslint-disable-next-line no-console
  310 + console.log(err);
  311 + }
  312 +
244 313 try {
245 314 loading.value = true;
246 315 unref(currentMode) === DataActionModeEnum.UPDATE
... ...
... ... @@ -3,15 +3,68 @@
3 3 import { useForm, BasicForm } from '/@/components/Form';
4 4 import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
5 5 import { option } from './config';
  6 + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
  7 + import { createImgPreview } from '/@/components/Preview';
  8 + import { upload } from '/@/api/oss/ossFileUploader';
6 9
7 10 const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
8 11 schemas: [
9 12 {
  13 + field: ComponentConfigFieldEnum.FONT_SIZE,
  14 + label: '文本字体大小',
  15 + component: 'InputNumber',
  16 + defaultValue: 14,
  17 + componentProps: {
  18 + min: 0,
  19 + max: 100,
  20 + formatter: (e) => {
  21 + const value = e?.toString().replace(/^0/g, '');
  22 + if (value) {
  23 + return value.replace(/^0/g, '');
  24 + } else {
  25 + return 0;
  26 + }
  27 + },
  28 + },
  29 + },
  30 + {
  31 + field: ComponentConfigFieldEnum.PASS_WORD,
  32 + label: '操作密码',
  33 + component: 'InputPassword',
  34 + defaultValue: '',
  35 + },
  36 + {
  37 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  38 + label: '显示设备名称',
  39 + component: 'Checkbox',
  40 + defaultValue: option.showDeviceName,
  41 + },
  42 + {
  43 + field: ComponentConfigFieldEnum.DEFAULT_CUSTOM,
  44 + label: '图标类型',
  45 + component: 'RadioGroup',
  46 + defaultValue: 'default',
  47 + componentProps: ({ formModel }) => {
  48 + return {
  49 + options: [
  50 + { label: '系统默认', value: 'default' },
  51 + { label: '自定义', value: 'custom' },
  52 + ],
  53 + onChange() {
  54 + formModel[ComponentConfigFieldEnum.CUSTOM_ICON] = [];
  55 + },
  56 + };
  57 + },
  58 + },
  59 + {
10 60 field: ComponentConfigFieldEnum.ICON_COLOR,
11 61 label: '图标颜色',
12 62 component: 'ColorPicker',
13 63 changeEvent: 'update:value',
14 64 defaultValue: option.iconColor,
  65 + ifShow: ({ model }) => {
  66 + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom';
  67 + },
15 68 },
16 69 {
17 70 field: ComponentConfigFieldEnum.ICON,
... ... @@ -19,6 +72,9 @@
19 72 component: 'IconDrawer',
20 73 changeEvent: 'update:value',
21 74 defaultValue: option.icon,
  75 + ifShow: ({ model }) => {
  76 + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom';
  77 + },
22 78 componentProps({ formModel }) {
23 79 const color = formModel[ComponentConfigFieldEnum.ICON_COLOR];
24 80 return {
... ... @@ -27,34 +83,51 @@
27 83 },
28 84 },
29 85 {
30   - field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
31   - label: '显示设备名称',
32   - component: 'Checkbox',
33   - defaultValue: option.showDeviceName,
34   - },
35   - {
36   - field: ComponentConfigFieldEnum.FONT_SIZE,
37   - label: '文本字体大小',
38   - component: 'InputNumber',
39   - defaultValue: 14,
40   - componentProps: {
41   - min: 0,
42   - max: 100,
43   - formatter: (e) => {
44   - const value = e?.toString().replace(/^0/g, '');
45   - if (value) {
46   - return value.replace(/^0/g, '');
47   - } else {
48   - return 0;
49   - }
50   - },
  86 + field: ComponentConfigFieldEnum.CUSTOM_ICON,
  87 + label: '图标',
  88 + component: 'ApiUpload',
  89 + ifShow: ({ model }) => model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] === 'custom',
  90 + changeEvent: 'update:fileList',
  91 + valueField: 'fileList',
  92 + helpMessage: ['支持.svg格式,建议尺寸为32*32px,大小不超过50kb '],
  93 + componentProps: ({ formModel }) => {
  94 + return {
  95 + listType: 'picture-card',
  96 + maxFileLimit: 1,
  97 + maxSize: 50 * 1024,
  98 + accept: '.svg',
  99 + api: async (file: File) => {
  100 + try {
  101 + const formData = new FormData();
  102 + const { name } = file;
  103 + formData.set('file', file);
  104 + const { fileStaticUri, fileName } = await upload(formData);
  105 + return {
  106 + uid: fileStaticUri,
  107 + name: name || fileName,
  108 + url: fileStaticUri,
  109 + } as FileItem;
  110 + } catch (error) {
  111 + return {};
  112 + }
  113 + },
  114 + // showUploadList: true,
  115 + onDownload() {},
  116 + onPreview: (fileList: FileItem) => {
  117 + createImgPreview({ imageList: [fileList.url!] });
  118 + },
  119 +
  120 + onDelete(url: string) {
  121 + formModel.deleteUrl = url!;
  122 + },
  123 + };
51 124 },
52 125 },
53 126 {
54   - field: ComponentConfigFieldEnum.PASS_WORD,
55   - label: '操作密码',
56   - component: 'InputPassword',
57   - defaultValue: '',
  127 + field: 'deleteUrl',
  128 + label: '',
  129 + component: 'Input',
  130 + show: false,
58 131 },
59 132 ],
60 133 showActionButtonGroup: false,
... ...
... ... @@ -31,7 +31,7 @@
31 31 fontSize: persetFontSize,
32 32 password: persetPassword,
33 33 } = persetOption || {};
34   - const { icon, iconColor, fontSize, password } = componentInfo || {};
  34 + const { icon, iconColor, fontSize, password, customIcon, defaultCustom } = componentInfo || {};
35 35
36 36 const tsl = getDeviceProfileTslByIdWithIdentifier?.(deviceProfileId, attribute);
37 37 return {
... ... @@ -41,6 +41,8 @@
41 41 fontSize: fontSize || persetFontSize || 14,
42 42 password: password || persetPassword,
43 43 commandType,
  44 + defaultCustom: defaultCustom || 'default',
  45 + customIcon: customIcon || [],
44 46 };
45 47 });
46 48
... ... @@ -81,10 +83,17 @@
81 83 <main class="w-full h-full flex justify-around items-center" :style="getScale">
82 84 <div class="flex flex-col justify-center items-center">
83 85 <SvgIcon
84   - :name="getDesign.icon"
  86 + v-if="getDesign.defaultCustom !== 'custom'"
  87 + :name="getDesign.icon!"
85 88 prefix="iconfont"
86   - :style="{ color: getDesign.iconColor }"
87 89 :size="getRatio ? getRatio * 60 : 60"
  90 + :style="{ color: getDesign.iconColor }"
  91 + />
  92 + <img
  93 + v-else
  94 + :src="getDesign.customIcon[0]?.url"
  95 + :style="{ width: getRatio ? getRatio * 60 + 'px' : '60px' }"
  96 + :alt="getDesign.customIcon[0]?.name"
88 97 />
89 98 <span
90 99 class="mt-3 truncate text-gray-500 text-center"
... ...
... ... @@ -62,29 +62,31 @@
62 62 };
63 63 });
64 64
65   - const sendValue = ref(0);
66   - const handleChange = async (value: number) => {
67   - sendValue.value = value;
68   - };
  65 + // const sendValue = ref(0);
  66 + const isChangeValue = ref<number | string>(); //保留以前的值 报错的时候要取以前的值
  67 + const handleChange = async () => {};
69 68
70 69 const { loading, doControlSendCommand } = useControlComand();
71 70
72   - const handleAfterChange = () => {
  71 + const handleAfterChange = (value) => {
73 72 if (unref(getDesign).password) {
74   - openModal(true, { password: unref(getDesign).password });
  73 + openModal(true, { password: unref(getDesign).password, value: value });
75 74 unref(sliderElRef)?.blur();
76 75 return;
77 76 }
78   - handleSendCommand();
  77 + handleSendCommand(value);
79 78 };
80 79
81   - const handleSendCommand = async () => {
  80 + const handleSendCommand = async (value) => {
82 81 if (props.config.option.mode === ComponentMode.SELECT_PREVIEW) return;
83   - const value = unref(sendValue);
84 82 const { option } = props.config || {};
85 83 const result = await doControlSendCommand(option, value);
86 84 unref(sliderElRef)?.blur();
87   - sliderValue.value = result ? value : unref(sliderValue);
  85 + sliderValue.value = result ? value : unref(isChangeValue);
  86 +
  87 + if (result) {
  88 + isChangeValue.value = sliderValue.value;
  89 + }
88 90 };
89 91
90 92 const { getNumberValue } = useReceiveValue();
... ... @@ -94,6 +96,7 @@
94 96 const [name, value] = latest;
95 97 if (!name && !value) return;
96 98 sliderValue.value = getNumberValue(value);
  99 + isChangeValue.value = getNumberValue(value);
97 100 };
98 101
99 102 useDataFetch(props, updateFn);
... ... @@ -125,7 +128,7 @@
125 128 '--slider-top': -(getRatio ? getRatio * 5 : 5) + 'px',
126 129 }"
127 130 class="no-drag"
128   - :value="sliderValue"
  131 + v-model:value="sliderValue"
129 132 :min="getDesign.minNumber"
130 133 :max="getDesign.maxNumber"
131 134 @change="handleChange"
... ...
... ... @@ -3,30 +3,13 @@
3 3 import { useForm, BasicForm } from '/@/components/Form';
4 4 import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
5 5 import { option } from './config';
  6 + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
  7 + import { createImgPreview } from '/@/components/Preview';
  8 + import { upload } from '/@/api/oss/ossFileUploader';
6 9
7 10 const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
8 11 schemas: [
9 12 {
10   - field: ComponentConfigFieldEnum.ICON_COLOR,
11   - label: '图标颜色',
12   - component: 'ColorPicker',
13   - changeEvent: 'update:value',
14   - defaultValue: option.iconColor,
15   - },
16   - {
17   - field: ComponentConfigFieldEnum.ICON,
18   - label: '图标',
19   - component: 'IconDrawer',
20   - changeEvent: 'update:value',
21   - defaultValue: option.icon,
22   - componentProps({ formModel }) {
23   - const color = formModel[ComponentConfigFieldEnum.ICON_COLOR];
24   - return {
25   - color,
26   - };
27   - },
28   - },
29   - {
30 13 field: ComponentConfigFieldEnum.FONT_SIZE,
31 14 label: '文本字体大小',
32 15 component: 'InputNumber',
... ... @@ -45,16 +28,106 @@
45 28 },
46 29 },
47 30 {
  31 + field: ComponentConfigFieldEnum.PASS_WORD,
  32 + label: '操作密码',
  33 + component: 'InputPassword',
  34 + defaultValue: '',
  35 + },
  36 + {
48 37 field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
49 38 label: '显示设备名称',
50 39 component: 'Checkbox',
51 40 defaultValue: option.showDeviceName,
52 41 },
53 42 {
54   - field: ComponentConfigFieldEnum.PASS_WORD,
55   - label: '操作密码',
56   - component: 'InputPassword',
57   - defaultValue: '',
  43 + field: ComponentConfigFieldEnum.DEFAULT_CUSTOM,
  44 + label: '图标类型',
  45 + component: 'RadioGroup',
  46 + defaultValue: 'default',
  47 + componentProps: ({ formModel }) => {
  48 + return {
  49 + options: [
  50 + { label: '系统默认', value: 'default' },
  51 + { label: '自定义', value: 'custom' },
  52 + ],
  53 + onChange() {
  54 + formModel[ComponentConfigFieldEnum.CUSTOM_ICON] = [];
  55 + },
  56 + };
  57 + },
  58 + },
  59 + {
  60 + field: ComponentConfigFieldEnum.ICON_COLOR,
  61 + label: '图标颜色',
  62 + component: 'ColorPicker',
  63 + changeEvent: 'update:value',
  64 + defaultValue: option.iconColor,
  65 + ifShow: ({ model }) => {
  66 + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom';
  67 + },
  68 + },
  69 + {
  70 + field: ComponentConfigFieldEnum.ICON,
  71 + label: '图标',
  72 + component: 'IconDrawer',
  73 + changeEvent: 'update:value',
  74 + defaultValue: option.icon,
  75 + ifShow: ({ model }) => {
  76 + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom';
  77 + },
  78 + componentProps({ formModel }) {
  79 + const color = formModel[ComponentConfigFieldEnum.ICON_COLOR];
  80 + return {
  81 + color,
  82 + };
  83 + },
  84 + },
  85 + {
  86 + field: ComponentConfigFieldEnum.CUSTOM_ICON,
  87 + label: '图标',
  88 + component: 'ApiUpload',
  89 + ifShow: ({ model }) => model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] === 'custom',
  90 + changeEvent: 'update:fileList',
  91 + valueField: 'fileList',
  92 + helpMessage: ['支持.svg格式,建议尺寸为32*32px,大小不超过50kb '],
  93 + componentProps: ({ formModel }) => {
  94 + return {
  95 + listType: 'picture-card',
  96 + maxSize: 50 * 1024,
  97 + maxFileLimit: 1,
  98 + accept: '.svg',
  99 + api: async (file: File) => {
  100 + try {
  101 + const formData = new FormData();
  102 + const { name } = file;
  103 + formData.set('file', file);
  104 + const { fileStaticUri, fileName } = await upload(formData);
  105 + return {
  106 + uid: fileStaticUri,
  107 + name: name || fileName,
  108 + url: fileStaticUri,
  109 + } as FileItem;
  110 + } catch (error) {
  111 + return {};
  112 + }
  113 + },
  114 + // showUploadList: true,
  115 + onDownload() {},
  116 + onPreview: (fileList: FileItem) => {
  117 + createImgPreview({ imageList: [fileList.url!] });
  118 + },
  119 +
  120 + onDelete(url: string) {
  121 + formModel.deleteUrl = url!;
  122 + },
  123 + };
  124 + },
  125 + },
  126 + {
  127 + field: 'deleteUrl',
  128 + label: '',
  129 + component: 'Input',
  130 + show: false,
58 131 },
59 132 ],
60 133 showActionButtonGroup: false,
... ...
... ... @@ -38,8 +38,17 @@
38 38 } = persetOption || {};
39 39 return {
40 40 dataSource: dataSource.map((item) => {
41   - const { fontColor, icon, iconColor, unit, showDeviceName, password, fontSize } =
42   - item.componentInfo;
  41 + const {
  42 + fontColor,
  43 + icon,
  44 + iconColor,
  45 + unit,
  46 + showDeviceName,
  47 + password,
  48 + fontSize,
  49 + customIcon,
  50 + defaultCustom,
  51 + } = item.componentInfo;
43 52 const {
44 53 attribute,
45 54 attributeRename,
... ... @@ -76,6 +85,8 @@
76 85 closeCommand,
77 86 openService,
78 87 closeService,
  88 + defaultCustom: defaultCustom || 'default',
  89 + customIcon: customIcon || [],
79 90 } as SwitchItemType;
80 91 }),
81 92 };
... ... @@ -128,11 +139,24 @@
128 139 :key="item.id"
129 140 class="flex justify-between items-center w-full px-4"
130 141 >
131   - <SvgIcon
  142 + <!-- <SvgIcon
132 143 :name="item.icon!"
133 144 prefix="iconfont"
134 145 :size="getRatio ? 30 * getRatio : 30"
135 146 :style="{ color: item.iconColor }"
  147 + /> -->
  148 + <SvgIcon
  149 + v-if="item.defaultCustom !== 'custom'"
  150 + :name="item.icon!"
  151 + prefix="iconfont"
  152 + :size="getRatio ? getRatio * 30 : 30"
  153 + :style="{ color: item.iconColor }"
  154 + />
  155 + <img
  156 + v-else
  157 + :src="item.customIcon[0]?.url"
  158 + :style="{ width: getRatio ? getRatio * 30 + 'px' : '30px' }"
  159 + :alt="item.customIcon[0]?.name"
136 160 />
137 161 <div
138 162 class="text-gray-500 truncate mx-2"
... ...
... ... @@ -56,7 +56,7 @@
56 56 createMessage.warning('操作密码不正确');
57 57 return;
58 58 }
59   - emit('success', unref(persetData));
  59 + emit('success', unref(persetData).value);
60 60 closeModal();
61 61 };
62 62 </script>
... ...
... ... @@ -30,16 +30,17 @@
30 30 const { config } = props;
31 31 const { option } = config;
32 32 const { videoConfig, uuid } = option || {};
33   - const { type, url } = await getPlayUrl({
34   - id: videoConfig?.id,
35   - accessMode: videoConfig?.accessMode,
36   - playProtocol: videoConfig?.playProtocol,
37   - videoUrl: videoConfig?.videoUrl,
38   - params: {
39   - deviceId: videoConfig?.deviceId,
40   - channelNo: videoConfig?.channelId,
41   - },
42   - } as unknown as CameraRecord);
  33 + const { type, url } =
  34 + (await getPlayUrl({
  35 + id: videoConfig?.id,
  36 + accessMode: videoConfig?.accessMode,
  37 + playProtocol: videoConfig?.playProtocol,
  38 + videoUrl: videoConfig?.videoUrl,
  39 + params: {
  40 + deviceId: videoConfig?.deviceId,
  41 + channelNo: videoConfig?.channelId,
  42 + },
  43 + } as unknown as CameraRecord)) || {};
43 44 playType.value = type;
44 45 playUrl.value = url;
45 46 if (!uuid) return;
... ...
... ... @@ -3,15 +3,68 @@
3 3 import { useForm, BasicForm } from '/@/components/Form';
4 4 import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
5 5 import { option } from './config';
  6 + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
  7 + import { createImgPreview } from '/@/components/Preview';
  8 + import { upload } from '/@/api/oss/ossFileUploader';
6 9
7 10 const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
8 11 schemas: [
9 12 {
  13 + field: ComponentConfigFieldEnum.FONT_SIZE,
  14 + label: '文本字体大小',
  15 + component: 'InputNumber',
  16 + defaultValue: 14,
  17 + componentProps: {
  18 + min: 0,
  19 + max: 100,
  20 + formatter: (e) => {
  21 + const value = e?.toString().replace(/^0/g, '');
  22 + if (value) {
  23 + return value.replace(/^0/g, '');
  24 + } else {
  25 + return 0;
  26 + }
  27 + },
  28 + },
  29 + },
  30 + {
  31 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  32 + label: '显示设备名称',
  33 + component: 'Checkbox',
  34 + defaultValue: option.showDeviceName,
  35 + },
  36 + {
  37 + field: ComponentConfigFieldEnum.SHOW_TIME,
  38 + label: '显示时间',
  39 + component: 'Checkbox',
  40 + defaultValue: option.showTime,
  41 + },
  42 + {
  43 + field: ComponentConfigFieldEnum.DEFAULT_CUSTOM,
  44 + label: '图标类型',
  45 + component: 'RadioGroup',
  46 + defaultValue: 'default',
  47 + componentProps: ({ formModel }) => {
  48 + return {
  49 + options: [
  50 + { label: '系统默认', value: 'default' },
  51 + { label: '自定义', value: 'custom' },
  52 + ],
  53 + onChange() {
  54 + formModel[ComponentConfigFieldEnum.CUSTOM_ICON] = [];
  55 + },
  56 + };
  57 + },
  58 + },
  59 + {
10 60 field: ComponentConfigFieldEnum.ICON,
11 61 label: '开启状态图标',
12 62 component: 'IconDrawer',
13 63 changeEvent: 'update:value',
14 64 defaultValue: option.icon,
  65 + ifShow: ({ model }) => {
  66 + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom';
  67 + },
15 68 componentProps({ formModel }) {
16 69 const color = formModel[ComponentConfigFieldEnum.ICON_COLOR];
17 70 return {
... ... @@ -24,6 +77,9 @@
24 77 label: '开启图标颜色',
25 78 component: 'ColorPicker',
26 79 changeEvent: 'update:value',
  80 + ifShow: ({ model }) => {
  81 + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom';
  82 + },
27 83 defaultValue: option.iconColor,
28 84 },
29 85 {
... ... @@ -32,6 +88,9 @@
32 88 component: 'IconDrawer',
33 89 changeEvent: 'update:value',
34 90 defaultValue: option.iconClose,
  91 + ifShow: ({ model }) => {
  92 + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom';
  93 + },
35 94 componentProps({ formModel }) {
36 95 const color = formModel[ComponentConfigFieldEnum.ICON_COLOR_CLOSE];
37 96 return {
... ... @@ -44,37 +103,84 @@
44 103 label: '关闭图标颜色',
45 104 component: 'ColorPicker',
46 105 changeEvent: 'update:value',
  106 + ifShow: ({ model }) => {
  107 + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom';
  108 + },
47 109 defaultValue: option.iconColorClose,
48 110 },
49 111 {
50   - field: ComponentConfigFieldEnum.FONT_SIZE,
51   - label: '文本字体大小',
52   - component: 'InputNumber',
53   - defaultValue: 14,
54   - componentProps: {
55   - min: 0,
56   - max: 100,
57   - formatter: (e) => {
58   - const value = e?.toString().replace(/^0/g, '');
59   - if (value) {
60   - return value.replace(/^0/g, '');
61   - } else {
62   - return 0;
63   - }
64   - },
  112 + field: ComponentConfigFieldEnum.CUSTOM_ICON,
  113 + label: '开启状态图标',
  114 + component: 'ApiUpload',
  115 + ifShow: ({ model }) => model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] === 'custom',
  116 + changeEvent: 'update:fileList',
  117 + valueField: 'fileList',
  118 + helpMessage: ['支持.svg格式,建议尺寸为32*32px,大小不超过50kb '],
  119 + componentProps: ({}) => {
  120 + return {
  121 + listType: 'picture-card',
  122 + maxSize: 50 * 1024,
  123 + maxFileLimit: 1,
  124 + accept: '.svg',
  125 + api: async (file: File) => {
  126 + try {
  127 + const formData = new FormData();
  128 + const { name } = file;
  129 + formData.set('file', file);
  130 + const { fileStaticUri, fileName } = await upload(formData);
  131 + return {
  132 + uid: fileStaticUri,
  133 + name: name || fileName,
  134 + url: fileStaticUri,
  135 + } as FileItem;
  136 + } catch (error) {
  137 + return {};
  138 + }
  139 + },
  140 + // showUploadList: true,
  141 + onDownload() {},
  142 + onPreview: (fileList: FileItem) => {
  143 + createImgPreview({ imageList: [fileList.url!] });
  144 + },
  145 + };
65 146 },
66 147 },
67 148 {
68   - field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
69   - label: '显示设备名称',
70   - component: 'Checkbox',
71   - defaultValue: option.showDeviceName,
72   - },
73   - {
74   - field: ComponentConfigFieldEnum.SHOW_TIME,
75   - label: '显示时间',
76   - component: 'Checkbox',
77   - defaultValue: option.showTime,
  149 + field: ComponentConfigFieldEnum.CUSTOM_ICON_CLOSE,
  150 + label: '关闭状态图标',
  151 + component: 'ApiUpload',
  152 + helpMessage: ['支持.svg格式,建议尺寸为32*32px,大小不超过50kb '],
  153 + ifShow: ({ model }) => model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] === 'custom',
  154 + changeEvent: 'update:fileList',
  155 + valueField: 'fileList',
  156 + componentProps: ({}) => {
  157 + return {
  158 + listType: 'picture-card',
  159 + maxSize: 0 * 1024,
  160 + maxFileLimit: 1,
  161 + accept: '.svg',
  162 + api: async (file: File) => {
  163 + try {
  164 + const formData = new FormData();
  165 + const { name } = file;
  166 + formData.set('file', file);
  167 + const { fileStaticUri, fileName } = await upload(formData);
  168 + return {
  169 + uid: fileStaticUri,
  170 + name: name || fileName,
  171 + url: fileStaticUri,
  172 + } as FileItem;
  173 + } catch (error) {
  174 + return {};
  175 + }
  176 + },
  177 + // showUploadList: true,
  178 + onDownload() {},
  179 + onPreview: (fileList: FileItem) => {
  180 + createImgPreview({ imageList: [fileList.url!] });
  181 + },
  182 + };
  183 + },
78 184 },
79 185 ],
80 186 showActionButtonGroup: false,
... ...
... ... @@ -32,8 +32,19 @@
32 32
33 33 const { componentInfo, attributeName, attributeRename } = option;
34 34
35   - const { icon, iconColor, fontColor, unit, iconClose, iconColorClose, showTime, fontSize } =
36   - componentInfo || {};
  35 + const {
  36 + icon,
  37 + iconColor,
  38 + fontColor,
  39 + unit,
  40 + iconClose,
  41 + iconColorClose,
  42 + showTime,
  43 + fontSize,
  44 + customIcon,
  45 + customIconClose,
  46 + defaultCustom,
  47 + } = componentInfo || {};
37 48 return {
38 49 iconColor: iconColor || persetIconColor,
39 50 unit: unit ?? perseUnit,
... ... @@ -44,6 +55,9 @@
44 55 iconColorClose: iconColorClose || persetIconColorClose,
45 56 showTime: showTime ?? persetShowTime,
46 57 fontSize: fontSize || persetFontSize || 14,
  58 + defaultCustom: defaultCustom || 'default',
  59 + customIcon: customIcon || [],
  60 + customIconClose: customIconClose || [],
47 61 };
48 62 });
49 63
... ... @@ -68,11 +82,18 @@
68 82 <DeviceName :config="config" />
69 83 <div class="flex flex-1 flex-col justify-center items-center">
70 84 <SvgIcon
71   - :name="isOpenClose ? getDesign.icon : getDesign.iconClose"
  85 + v-if="getDesign.defaultCustom !== 'custom'"
  86 + :name="getDesign.iconClose"
72 87 prefix="iconfont"
73 88 :size="getRatio ? getRatio * 70 : 70"
74 89 :style="{ color: isOpenClose ? getDesign.iconColor : getDesign.iconColorClose }"
75 90 />
  91 + <img
  92 + v-else
  93 + :src="isOpenClose ? getDesign.customIcon[0]?.url : getDesign.customIconClose[0]?.url"
  94 + :style="{ width: getRatio ? getRatio * 70 + 'px' : '70px' }"
  95 + :alt="getDesign.customIcon[0]?.name"
  96 + />
76 97 <div
77 98 class="text-gray-500 truncate m-2"
78 99 :style="{
... ...
... ... @@ -3,6 +3,9 @@
3 3 import { useForm, BasicForm } from '/@/components/Form';
4 4 import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
5 5 import { option } from './config';
  6 + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
  7 + import { createImgPreview } from '/@/components/Preview';
  8 + import { upload } from '/@/api/oss/ossFileUploader';
6 9
7 10 const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
8 11 schemas: [
... ... @@ -61,11 +64,37 @@
61 64 },
62 65 },
63 66 {
  67 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  68 + label: '显示设备名称',
  69 + component: 'Checkbox',
  70 + defaultValue: option.showDeviceName,
  71 + },
  72 + {
  73 + field: ComponentConfigFieldEnum.DEFAULT_CUSTOM,
  74 + label: '图标类型',
  75 + component: 'RadioGroup',
  76 + defaultValue: 'default',
  77 + componentProps: ({ formModel }) => {
  78 + return {
  79 + options: [
  80 + { label: '系统默认', value: 'default' },
  81 + { label: '自定义', value: 'custom' },
  82 + ],
  83 + onChange() {
  84 + formModel[ComponentConfigFieldEnum.CUSTOM_ICON] = [];
  85 + },
  86 + };
  87 + },
  88 + },
  89 + {
64 90 field: ComponentConfigFieldEnum.ICON_COLOR,
65 91 label: '图标颜色',
66 92 component: 'ColorPicker',
67 93 changeEvent: 'update:value',
68 94 defaultValue: option.iconColor,
  95 + ifShow: ({ model }) => {
  96 + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom';
  97 + },
69 98 },
70 99 {
71 100 field: ComponentConfigFieldEnum.ICON,
... ... @@ -73,6 +102,9 @@
73 102 component: 'IconDrawer',
74 103 changeEvent: 'update:value',
75 104 defaultValue: option.icon,
  105 + ifShow: ({ model }) => {
  106 + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom';
  107 + },
76 108 componentProps({ formModel }) {
77 109 const color = formModel[ComponentConfigFieldEnum.ICON_COLOR];
78 110 return {
... ... @@ -81,10 +113,50 @@
81 113 },
82 114 },
83 115 {
84   - field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
85   - label: '显示设备名称',
86   - component: 'Checkbox',
87   - defaultValue: option.showDeviceName,
  116 + field: ComponentConfigFieldEnum.CUSTOM_ICON,
  117 + label: '图标',
  118 + component: 'ApiUpload',
  119 + ifShow: ({ model }) => model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] === 'custom',
  120 + changeEvent: 'update:fileList',
  121 + valueField: 'fileList',
  122 + helpMessage: ['支持.svg格式,建议尺寸为32*32px,大小不超过50kb '],
  123 + componentProps: ({ formModel }) => {
  124 + return {
  125 + maxSize: 50 * 1024,
  126 + listType: 'picture-card',
  127 + maxFileLimit: 1,
  128 + accept: '.svg',
  129 + api: async (file: File) => {
  130 + try {
  131 + const formData = new FormData();
  132 + const { name } = file;
  133 + formData.set('file', file);
  134 + const { fileStaticUri, fileName } = await upload(formData);
  135 + return {
  136 + uid: fileStaticUri,
  137 + name: name || fileName,
  138 + url: fileStaticUri,
  139 + } as FileItem;
  140 + } catch (error) {
  141 + return {};
  142 + }
  143 + },
  144 + // showUploadList: true,
  145 + onDownload() {},
  146 + onPreview: (fileList: FileItem) => {
  147 + createImgPreview({ imageList: [fileList.url!] });
  148 + },
  149 + onDelete(url: string) {
  150 + formModel.deleteUrl = url!;
  151 + },
  152 + };
  153 + },
  154 + },
  155 + {
  156 + field: 'deleteUrl',
  157 + label: '',
  158 + component: 'Input',
  159 + show: false,
88 160 },
89 161 ],
90 162 showActionButtonGroup: false,
... ...
... ... @@ -31,7 +31,6 @@
31 31
32 32 const getDesign = computed(() => {
33 33 const { persetOption = {}, option } = props.config;
34   -
35 34 const {
36 35 iconColor: persetIconColor,
37 36 unit: perseUnit,
... ... @@ -44,7 +43,8 @@
44 43 const { componentInfo, attributeRename } = option;
45 44 const { functionName } = unref(getThingModelTsl) || {};
46 45
47   - const { icon, iconColor, fontColor, unit, valueSize, fontSize } = componentInfo || {};
  46 + const { icon, iconColor, fontColor, unit, valueSize, fontSize, customIcon, defaultCustom } =
  47 + componentInfo || {};
48 48 return {
49 49 iconColor: iconColor || persetIconColor,
50 50 unit: unit ?? perseUnit,
... ... @@ -53,6 +53,8 @@
53 53 attribute: attributeRename || functionName,
54 54 valueSize: valueSize || persetValueSize || 20,
55 55 fontSize: fontSize || persetFontSize || 14,
  56 + defaultCustom: defaultCustom || 'default',
  57 + customIcon: customIcon || [],
56 58 };
57 59 });
58 60
... ... @@ -78,11 +80,18 @@
78 80 <DeviceName :config="config" />
79 81 <div class="flex-1 flex justify-center items-center flex-col w-full">
80 82 <SvgIcon
  83 + v-if="getDesign.defaultCustom !== 'custom'"
81 84 :name="getDesign.icon!"
82 85 prefix="iconfont"
83 86 :size="getRatio ? getRatio * 70 : 70"
84 87 :style="{ color: getDesign.iconColor }"
85 88 />
  89 + <img
  90 + v-else
  91 + :src="getDesign.customIcon[0]?.url"
  92 + :style="{ width: getRatio ? getRatio * 70 + 'px' : '70px' }"
  93 + :alt="getDesign.customIcon[0]?.name"
  94 + />
86 95 <h1
87 96 class="font-bold m-2 truncate w-full text-center"
88 97 :style="{
... ...
... ... @@ -3,6 +3,9 @@
3 3 import { useForm, BasicForm } from '/@/components/Form';
4 4 import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
5 5 import { option } from './config';
  6 + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
  7 + import { createImgPreview } from '/@/components/Preview';
  8 + import { upload } from '/@/api/oss/ossFileUploader';
6 9
7 10 const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
8 11 schemas: [
... ... @@ -60,11 +63,37 @@
60 63 },
61 64 },
62 65 {
  66 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  67 + label: '显示设备名称',
  68 + component: 'Checkbox',
  69 + defaultValue: option.showDeviceName,
  70 + },
  71 + {
  72 + field: ComponentConfigFieldEnum.DEFAULT_CUSTOM,
  73 + label: '图标类型',
  74 + component: 'RadioGroup',
  75 + defaultValue: 'default',
  76 + componentProps: ({ formModel }) => {
  77 + return {
  78 + options: [
  79 + { label: '系统默认', value: 'default' },
  80 + { label: '自定义', value: 'custom' },
  81 + ],
  82 + onChange() {
  83 + formModel[ComponentConfigFieldEnum.CUSTOM_ICON] = [];
  84 + },
  85 + };
  86 + },
  87 + },
  88 + {
63 89 field: ComponentConfigFieldEnum.ICON_COLOR,
64 90 label: '图标颜色',
65 91 component: 'ColorPicker',
66 92 changeEvent: 'update:value',
67 93 defaultValue: option.iconColor,
  94 + ifShow: ({ model }) => {
  95 + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom';
  96 + },
68 97 },
69 98 {
70 99 field: ComponentConfigFieldEnum.ICON,
... ... @@ -72,6 +101,9 @@
72 101 component: 'IconDrawer',
73 102 changeEvent: 'update:value',
74 103 defaultValue: option.icon,
  104 + ifShow: ({ model }) => {
  105 + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom';
  106 + },
75 107 componentProps({ formModel }) {
76 108 const color = formModel[ComponentConfigFieldEnum.ICON_COLOR];
77 109 return {
... ... @@ -80,10 +112,50 @@
80 112 },
81 113 },
82 114 {
83   - field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
84   - label: '显示设备名称',
85   - component: 'Checkbox',
86   - defaultValue: option.showDeviceName,
  115 + field: ComponentConfigFieldEnum.CUSTOM_ICON,
  116 + label: '图标',
  117 + component: 'ApiUpload',
  118 + ifShow: ({ model }) => model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] === 'custom',
  119 + changeEvent: 'update:fileList',
  120 + valueField: 'fileList',
  121 + helpMessage: ['支持.svg格式,建议尺寸为32*32px,大小不超过50kb '],
  122 + componentProps: ({ formModel }) => {
  123 + return {
  124 + listType: 'picture-card',
  125 + maxSize: 50 * 1024,
  126 + maxFileLimit: 1,
  127 + accept: '.svg',
  128 + api: async (file: File) => {
  129 + try {
  130 + const formData = new FormData();
  131 + const { name } = file;
  132 + formData.set('file', file);
  133 + const { fileStaticUri, fileName } = await upload(formData);
  134 + return {
  135 + uid: fileStaticUri,
  136 + name: name || fileName,
  137 + url: fileStaticUri,
  138 + } as FileItem;
  139 + } catch (error) {
  140 + return {};
  141 + }
  142 + },
  143 + // showUploadList: true,
  144 + onDownload() {},
  145 + onPreview: (fileList: FileItem) => {
  146 + createImgPreview({ imageList: [fileList.url!] });
  147 + },
  148 + onDelete(url: string) {
  149 + formModel.deleteUrl = url!;
  150 + },
  151 + };
  152 + },
  153 + },
  154 + {
  155 + field: 'deleteUrl',
  156 + label: '',
  157 + component: 'Input',
  158 + show: false,
87 159 },
88 160 ],
89 161 showActionButtonGroup: false,
... ...
... ... @@ -40,7 +40,8 @@
40 40
41 41 const { componentInfo, attribute, attributeRename } = option;
42 42
43   - const { icon, iconColor, fontColor, unit, valueSize, fontSize } = componentInfo || {};
  43 + const { icon, iconColor, fontColor, unit, valueSize, fontSize, customIcon, defaultCustom } =
  44 + componentInfo || {};
44 45 return {
45 46 iconColor: iconColor || persetIconColor,
46 47 unit: unit ?? perseUnit,
... ... @@ -49,6 +50,8 @@
49 50 attribute: attributeRename || unref(getThingModelTsl)?.functionName || attribute,
50 51 valueSize: valueSize || persetValueSize || 20,
51 52 fontSize: fontSize || persetFontSize || 14,
  53 + defaultCustom: defaultCustom || 'default',
  54 + customIcon: customIcon || [],
52 55 };
53 56 });
54 57
... ... @@ -73,11 +76,18 @@
73 76 <DeviceName :config="config" />
74 77 <div :style="getScale" class="flex-1 flex justify-center items-center flex-col w-full">
75 78 <SvgIcon
  79 + v-if="getDesign.defaultCustom !== 'custom'"
76 80 :name="getDesign.icon!"
77 81 prefix="iconfont"
78 82 :size="getRatio ? getRatio * 70 : 70"
79 83 :style="{ color: getDesign.iconColor }"
80 84 />
  85 + <img
  86 + v-else
  87 + :src="getDesign.customIcon[0]?.url"
  88 + :style="{ width: getRatio ? getRatio * 70 + 'px' : '70px' }"
  89 + :alt="getDesign.customIcon[0]?.name"
  90 + />
81 91 <h1
82 92 class="my-4 font-bold !my-2 truncate w-full text-center"
83 93 :style="{
... ...
... ... @@ -4,6 +4,10 @@
4 4 import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
5 5 import { option } from './config';
6 6
  7 + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
  8 + import { createImgPreview } from '/@/components/Preview';
  9 + import { upload } from '/@/api/oss/ossFileUploader';
  10 +
7 11 const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
8 12 schemas: [
9 13 {
... ... @@ -60,11 +64,31 @@
60 64 },
61 65 },
62 66 {
  67 + field: ComponentConfigFieldEnum.DEFAULT_CUSTOM,
  68 + label: '图标类型',
  69 + component: 'RadioGroup',
  70 + defaultValue: 'default',
  71 + componentProps: ({ formModel }) => {
  72 + return {
  73 + options: [
  74 + { label: '系统默认', value: 'default' },
  75 + { label: '自定义', value: 'custom' },
  76 + ],
  77 + onChange() {
  78 + formModel[ComponentConfigFieldEnum.CUSTOM_ICON] = [];
  79 + },
  80 + };
  81 + },
  82 + },
  83 + {
63 84 field: ComponentConfigFieldEnum.ICON_COLOR,
64 85 label: '图标颜色',
65 86 component: 'ColorPicker',
66 87 changeEvent: 'update:value',
67 88 defaultValue: option.iconColor,
  89 + ifShow: ({ model }) => {
  90 + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom';
  91 + },
68 92 },
69 93 {
70 94 field: ComponentConfigFieldEnum.ICON,
... ... @@ -73,6 +97,9 @@
73 97 changeEvent: 'update:value',
74 98 valueField: 'value',
75 99 defaultValue: option.icon,
  100 + ifShow: ({ model }) => {
  101 + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom';
  102 + },
76 103 componentProps({ formModel }) {
77 104 const color = formModel[ComponentConfigFieldEnum.ICON_COLOR];
78 105 return {
... ... @@ -80,6 +107,51 @@
80 107 };
81 108 },
82 109 },
  110 + {
  111 + field: ComponentConfigFieldEnum.CUSTOM_ICON,
  112 + label: '图标',
  113 + component: 'ApiUpload',
  114 + ifShow: ({ model }) => model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] === 'custom',
  115 + changeEvent: 'update:fileList',
  116 + valueField: 'fileList',
  117 + helpMessage: ['支持.svg格式,建议尺寸为32*32px,大小不超过50kb '],
  118 + componentProps: ({ formModel }) => {
  119 + return {
  120 + listType: 'picture-card',
  121 + maxSize: 50 * 1024,
  122 + maxFileLimit: 1,
  123 + accept: '.svg',
  124 + api: async (file: File) => {
  125 + try {
  126 + const formData = new FormData();
  127 + const { name } = file;
  128 + formData.set('file', file);
  129 + const { fileStaticUri, fileName } = await upload(formData);
  130 + return {
  131 + uid: fileStaticUri,
  132 + name: name || fileName,
  133 + url: fileStaticUri,
  134 + } as FileItem;
  135 + } catch (error) {
  136 + return {};
  137 + }
  138 + },
  139 + onDownload() {},
  140 + onPreview: (fileList: FileItem) => {
  141 + createImgPreview({ imageList: [fileList.url!] });
  142 + },
  143 + onDelete(url: string) {
  144 + formModel.deleteUrl = url!;
  145 + },
  146 + };
  147 + },
  148 + },
  149 + {
  150 + field: 'deleteUrl',
  151 + label: '',
  152 + component: 'Input',
  153 + show: false,
  154 + },
83 155 ],
84 156 showActionButtonGroup: false,
85 157 labelWidth: 120,
... ...
... ... @@ -35,7 +35,8 @@
35 35
36 36 return {
37 37 dataSource: dataSource.map((item) => {
38   - const { fontColor, icon, iconColor, unit, valueSize, fontSize } = item.componentInfo;
  38 + const { fontColor, icon, iconColor, unit, valueSize, fontSize, customIcon, defaultCustom } =
  39 + item.componentInfo;
39 40 const { attribute, attributeRename, deviceName, deviceRename, deviceId, deviceProfileId } =
40 41 item;
41 42 const tsl = getDeviceProfileTslByIdWithIdentifier?.(deviceProfileId, attribute);
... ... @@ -50,6 +51,8 @@
50 51 id: deviceId,
51 52 valueSize: valueSize || persetValueSize || 20,
52 53 fontSize: fontSize || persetFontSize || 14,
  54 + defaultCustom: defaultCustom || 'default',
  55 + customIcon: customIcon || [],
53 56 };
54 57 }),
55 58 };
... ... @@ -67,6 +70,8 @@
67 70 fontColor: '#357CFB',
68 71 fontSize: 16,
69 72 valueSize: 16,
  73 + defaultCustom: 'default',
  74 + customIcon: [],
70 75 },
71 76 {
72 77 id: buildUUID(),
... ... @@ -79,6 +84,8 @@
79 84 fontColor: '#FFA000',
80 85 fontSize: 16,
81 86 valueSize: 16,
  87 + defaultCustom: 'default',
  88 + customIcon: [],
82 89 },
83 90 ]);
84 91
... ... @@ -114,11 +121,24 @@
114 121 class="flex justify-between items-center mt-2"
115 122 >
116 123 <div class="flex items-center">
117   - <SvgIcon
  124 + <!-- <SvgIcon
118 125 :name="item.icon!"
119 126 prefix="iconfont"
120 127 :size="getRatio ? 30 * getRatio : 30"
121 128 :style="{ color: item.iconColor }"
  129 + /> -->
  130 + <SvgIcon
  131 + v-if="item.defaultCustom !== 'custom'"
  132 + :name="item.icon!"
  133 + prefix="iconfont"
  134 + :size="getRatio ? getRatio * 30 : 30"
  135 + :style="{ color: item.iconColor }"
  136 + />
  137 + <img
  138 + v-else
  139 + :src="item.customIcon[0]?.url"
  140 + :style="{ width: getRatio ? getRatio * 30 + 'px' : '30px' }"
  141 + :alt="item.customIcon[0]?.name"
122 142 />
123 143 <div
124 144 class="text-gray-500 ml-6"
... ...
... ... @@ -36,4 +36,8 @@ export enum ComponentConfigFieldEnum {
36 36 MIN_NUMBER = 'minNumber',
37 37 MAX_NUMBER = 'maxNumber',
38 38 PASS_WORD = 'password', //操作密码
  39 + DEFAULT_CUSTOM = 'defaultCustom',
  40 + DEFAULT_CUSTOM_CLOSE = 'defaultCustomClose',
  41 + CUSTOM_ICON = 'customIcon',
  42 + CUSTOM_ICON_CLOSE = 'customIconClose',
39 43 }
... ...
... ... @@ -20,6 +20,8 @@
20 20 import { useGetComponentConfig } from '../../../packages/hook/useGetComponetConfig';
21 21 import { isBoolean } from '/@/utils/is';
22 22 import { useApp } from '../../hooks/useApp';
  23 + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
  24 + import { deleteFilePath } from '/@/api/oss/ossFileUploader';
23 25
24 26 const props = defineProps<{
25 27 sourceInfo: WidgetDataType;
... ... @@ -74,8 +76,56 @@
74 76 emit('update', toRaw(props.sourceInfo));
75 77 }
76 78
  79 + const countElementOccurrences = (arr) => {
  80 + const countMap = {};
  81 +
  82 + arr.forEach((element) => {
  83 + if (countMap[element]) {
  84 + countMap[element]++;
  85 + } else {
  86 + countMap[element] = 1;
  87 + }
  88 + });
  89 +
  90 + return countMap;
  91 + };
  92 +
77 93 async function handleDelete() {
78 94 try {
  95 + const { componentData: oldDataSource } = props.rawDataSource;
  96 + const customIconUrls = ref<any>([]);
  97 + oldDataSource?.forEach((item: any) => {
  98 + item.dataSource?.forEach((dataSource) => {
  99 + if (dataSource.componentInfo?.customIcon) {
  100 + dataSource.componentInfo?.customIcon.forEach((icon: FileItem) => {
  101 + customIconUrls.value.push(icon.url);
  102 + });
  103 + }
  104 + });
  105 + });
  106 +
  107 + const { dataSource: deleteDataSource } = props.sourceInfo;
  108 + const dataSourceDeleteUrl = deleteDataSource.map(
  109 + (item) => item.componentInfo.customIcon?.[0].url
  110 + );
  111 +
  112 + if (dataSourceDeleteUrl?.length) {
  113 + // 判断外部所有组件是否有dataSourceDeleteUrl使用中的url
  114 + const deletePromise = unref(customIconUrls)?.filter((item) =>
  115 + dataSourceDeleteUrl?.includes(item)
  116 + );
  117 +
  118 + const deleteUrlInfo = countElementOccurrences(deletePromise);
  119 + const deleteUrl = deletePromise?.filter((item) => deleteUrlInfo?.[item] == 1);
  120 + Promise.all(
  121 + deleteUrl.map((item) => {
  122 + deleteFilePath(item);
  123 + })
  124 + );
  125 + }
  126 + } catch (err) {}
  127 +
  128 + try {
79 129 await deleteDataComponent({ dataBoardId: unref(boardId), ids: [props.sourceInfo.id] });
80 130 createMessage.success('删除成功');
81 131 emit('ok');
... ...
... ... @@ -48,6 +48,8 @@ export interface UploadFileParams {
48 48 export interface PaginationResult<T = Recordable> {
49 49 items: T[];
50 50 total: number;
  51 + totalElements?: number;
  52 + data?: T[];
51 53 }
52 54
53 55 export interface TBPaginationResult<T = Recordable> {
... ...