Commit eff0f4473f52d6e29ac22d54989f76a6332d35be

Authored by xp.Huang
2 parents ec78503e 6fdcb5ca

Merge branch 'main_dev'

# Conflicts:
#	src/components/Form/src/types/index.ts
#	src/enums/dictEnum.ts
#	src/views/device/list/config/data.ts
#	src/views/device/list/cpns/step/DeviceStep1.vue
#	src/views/device/list/cpns/tabs/ModelOfMatter.vue
#	src/views/device/list/cpns/tabs/ObjectModelCommandDeliveryModal/index.vue
#	src/views/rule/dataFlow/components/dataflowmodal/DataFlowModal.vue
#	src/views/visual/dataSourceBindPanel/index.vue
#	src/views/visual/packages/components/Alarm/DeviceAlarm/config.ts
#	src/views/visual/packages/components/Alarm/DeviceAlarm/config.vue
#	src/views/visual/packages/components/Alarm/DeviceAlarm/index.vue
#	src/views/visual/packages/components/Alarm/DeviceAlarmHistory/index.vue
#	src/views/visual/packages/components/Control/ControlComponentSlidingSwitch/index.vue
#	src/views/visual/packages/components/Control/ControlComponentSwitchWithIcon/index.vue
#	src/views/visual/packages/components/Control/ControlComponentToggleSwitch/index.vue
#	src/views/visual/packages/components/Control/LateralNumericalControl/config.ts
#	src/views/visual/packages/components/Control/LateralNumericalControl/config.vue
#	src/views/visual/packages/components/Control/LateralNumericalControl/index.vue
#	src/views/visual/packages/components/Control/SwitchList/index.vue
#	src/views/visual/packages/components/Flowmeter/CircleFlowmeter/config.ts
#	src/views/visual/packages/components/Flowmeter/CircleFlowmeter/config.vue
#	src/views/visual/packages/components/Flowmeter/CircleFlowmeter/index.vue
#	src/views/visual/packages/components/Flowmeter/RectFlowmeter/config.ts
#	src/views/visual/packages/components/Flowmeter/RectFlowmeter/config.vue
#	src/views/visual/packages/components/Flowmeter/RectFlowmeter/index.vue
#	src/views/visual/packages/components/Instrument/HumidityComponent/config.vue
#	src/views/visual/packages/components/Instrument/HumidityComponent/index.vue
#	src/views/visual/packages/components/Instrument/HumidityComponent1/config.ts
#	src/views/visual/packages/components/Instrument/HumidityComponent1/config.vue
#	src/views/visual/packages/components/Instrument/HumidityComponent1/index.vue
#	src/views/visual/packages/components/Instrument/HumidityComponent3/config.ts
#	src/views/visual/packages/components/Instrument/HumidityComponent3/config.vue
#	src/views/visual/packages/components/Instrument/HumidityComponent3/index.vue
#	src/views/visual/packages/components/Instrument/InstrumentComponent1/config.vue
#	src/views/visual/packages/components/Instrument/InstrumentComponent1/index.vue
#	src/views/visual/packages/components/Instrument/InstrumentComponent2/config.ts
#	src/views/visual/packages/components/Instrument/InstrumentComponent2/config.vue
#	src/views/visual/packages/components/Instrument/InstrumentComponent2/index.vue
#	src/views/visual/packages/components/Other/SwitchSignalLight/config.ts
#	src/views/visual/packages/components/Other/SwitchSignalLight/config.vue
#	src/views/visual/packages/components/Other/SwitchSignalLight/index.vue
#	src/views/visual/packages/components/Other/SwitchStatus/config.ts
#	src/views/visual/packages/components/Other/SwitchStatus/index.vue
#	src/views/visual/packages/components/Text/ValueList1/index.vue
#	src/views/visual/packages/components/Text/ValueList2/index.vue
#	src/views/visual/palette/components/alarmTimeModal/config.ts
#	src/views/visual/palette/types/index.ts
Showing 77 changed files with 2569 additions and 755 deletions

Too many changes to show.

To preserve performance only 77 of 603 files are displayed.

... ... @@ -5,22 +5,28 @@
5 5 "public/resource/tinymce/langs"
6 6 ],
7 7 "cSpell.words": [
  8 + "ACKS",
  9 + "clazz",
8 10 "Cmds",
9 11 "COAP",
10 12 "echarts",
11 13 "edrx",
12   - "EFENTO",
  14 + "EFENTO",
13 15 "fingerprintjs",
14 16 "flvjs",
15   - "flvjs",
16 17 "inited",
17 18 "liveui",
18 19 "MQTT",
  20 + "noconflict",
19 21 "notif",
20 22 "PROTOBUF",
  23 + "Rabbitmq",
21 24 "rtsp",
22 25 "SCADA",
  26 + "SMTPS",
23 27 "SNMP",
  28 + "TSLV",
  29 + "UNACK",
24 30 "unref",
25 31 "vben",
26 32 "videojs",
... ...
  1 +import { NodeItemConfigType } from '/@/views/rule/designer/types/node';
  2 +
  3 +export const components: Partial<
  4 + NodeItemConfigType & { id: { id: string }; createdTime: number; actions: any }
  5 +>[] = [];
... ...
  1 +import { join } from 'path';
  2 +import { pathExists, ensureFile, writeFile } from 'fs-extra';
  3 +import { NodeItemConfigType } from '/@/views/rule/designer/types/node';
  4 +import { camelCase, upperFirst, snakeCase } from 'lodash-es';
  5 +import { components } from './components';
  6 +type GroupNodeType = { [key: string]: NodeItemConfigType[] };
  7 +
  8 +const RULE_CHAIN_FILE_PATH = join(process.cwd(), '/src/views/rule/designer');
  9 +
  10 +const list: NodeItemConfigType[] = components;
  11 +
  12 +const getCategoryConfigName = (name: string) => {
  13 + return `${upperFirst(camelCase(name))}CategoryConfig`;
  14 +};
  15 +
  16 +const getNodeConfigName = (name: string) => {
  17 + return `${upperFirst(camelCase(name))}Config`;
  18 +};
  19 +
  20 +const getCagegoryEnumName = (name: string) => {
  21 + return `${upperFirst(name.toLowerCase())}CategoryComponentEnum`;
  22 +};
  23 +
  24 +const getEnumKeyName = (name: string) => {
  25 + return snakeCase(name).toUpperCase();
  26 +};
  27 +
  28 +const createFile = async (fileName: string, fileContent?: string, replace = false) => {
  29 + const path = join(RULE_CHAIN_FILE_PATH, './packages', fileName);
  30 +
  31 + const flag = await pathExists(path);
  32 +
  33 + if (flag && !replace) return false;
  34 +
  35 + await ensureFile(path);
  36 +
  37 + fileContent && (await writeFile(path, fileContent, { encoding: 'utf-8' }));
  38 +};
  39 +
  40 +const groupByType = () => {
  41 + const group: { [key: string]: NodeItemConfigType[] } = {};
  42 +
  43 + list.forEach((item) => {
  44 + if (!group[item.type]) group[item.type] = [];
  45 + group[item.type].push(item);
  46 + });
  47 +
  48 + return group;
  49 +};
  50 +
  51 +const generateCategoryEnumFile = async (data: GroupNodeType) => {
  52 + const defaultContent = `
  53 +export enum EntryCategoryComponentEnum {
  54 + INPUT = 'Input',
  55 +}
  56 + `;
  57 +
  58 + const fileContent = Object.keys(data).reduce((prev, next) => {
  59 + const enumName = getCagegoryEnumName(next);
  60 +
  61 + const enumKeys = data[next].map((item) => getEnumKeyName(item.name));
  62 +
  63 + const content = `export enum ${enumName} {
  64 + ${enumKeys.map((name) => `${name} = '${upperFirst(camelCase(name))}'`)}
  65 + }`;
  66 +
  67 + return `${prev} \n ${content}`;
  68 + }, defaultContent);
  69 +
  70 + createFile('../enum/category.ts', fileContent, true);
  71 + return fileContent;
  72 +};
  73 +
  74 +const generateRuleNodeEnumFile = async (data: GroupNodeType) => {
  75 + const categoryKeys = Object.keys(data).map((type) => type.toUpperCase());
  76 + const filePath = join(RULE_CHAIN_FILE_PATH, './packages/index.type.ts');
  77 + const fileContent = `
  78 +export enum RuleNodeTypeEnum {
  79 + ${categoryKeys.map((item) => `${item} = '${item}'`).join(',\n')}
  80 +}
  81 + `;
  82 +
  83 + await writeFile(filePath, fileContent, {
  84 + encoding: 'utf-8',
  85 + });
  86 +
  87 + return fileContent;
  88 +};
  89 +
  90 +const generateCategoryIndexFile = async (type: string, data: NodeItemConfigType[]) => {
  91 + const getComponentsName = data.map((temp) => `${upperFirst(camelCase(temp.name))}`);
  92 + const importContent = getComponentsName.map(
  93 + (name) => `import { ${name}Config } from './${name}';\n`
  94 + );
  95 +
  96 + const components = getComponentsName.map((item) => `${item}Config`);
  97 +
  98 + const content = `import type { CategoryConfigType, NodeItemConfigType } from '../../types/node';
  99 +import { RuleNodeTypeEnum } from '../index.type';
  100 +${importContent.join('')}
  101 +
  102 +export const ${getCategoryConfigName(type)}: CategoryConfigType = {
  103 + category: RuleNodeTypeEnum.${type.toUpperCase()},
  104 + title: '${type}',
  105 + icon: 'tabler:circuit-ground',
  106 + description: '使用配置条件筛选传入消息',
  107 +};
  108 +
  109 +export const ${upperFirst(type.toLowerCase())}Components: NodeItemConfigType[] = [${components}];
  110 +`;
  111 +
  112 + createFile(`./${upperFirst(type.toLowerCase())}/index.ts`, content, true);
  113 +
  114 + return content;
  115 +};
  116 +
  117 +const generateNodeIndexFile = async (type: string, data: NodeItemConfigType) => {
  118 + const categoryEnumName = getCagegoryEnumName(type);
  119 +
  120 + const nodeConfigName = getNodeConfigName(data.name);
  121 +
  122 + const content = `
  123 + import { ${categoryEnumName} } from '../../../enum/category';
  124 + import { useCreateNodeKey } from '../../../hook/useCreateNodeKey';
  125 + import type { NodeItemConfigType } from '../../../types/node';
  126 + import { RuleNodeTypeEnum } from '../../index.type';
  127 +
  128 + const keys = useCreateNodeKey(${categoryEnumName}.${getEnumKeyName(data.name)});
  129 +
  130 + export interface ${upperFirst(camelCase(data.name))}DataType {
  131 + someConfiguration?: Recordable
  132 + }
  133 +
  134 + export const ${nodeConfigName}: NodeItemConfigType = {
  135 + ...keys,
  136 + clazz: '${data.clazz}',
  137 + categoryType: RuleNodeTypeEnum.${type.toUpperCase()},
  138 + name: '${data.name}',
  139 + configurationDescriptor: ${JSON.stringify(data.configurationDescriptor, null, 2)}
  140 + };
  141 + `;
  142 +
  143 + createFile(
  144 + `./${upperFirst(type.toLowerCase())}/${upperFirst(camelCase(data.name))}/index.ts`,
  145 + content
  146 + );
  147 +
  148 + return content;
  149 +};
  150 +
  151 +const generateNodeVueTemplateFile = async (type: string, data: NodeItemConfigType) => {
  152 + const content = `
  153 + <script lang="ts" setup>
  154 + import type { CreateModalDefineExposeType } from '../../../types';
  155 + import { BasicForm, useForm } from '/@/components/Form';
  156 + import { formSchemas } from './create.config';
  157 + import { NodeData } from '../../../types/node';
  158 +
  159 + defineProps<{
  160 + config: NodeData;
  161 + }>();
  162 +
  163 + const [register, { validate, getFieldsValue, setFieldsValue, resetFields }] = useForm({
  164 + schemas: formSchemas,
  165 + showActionButtonGroup: false,
  166 + });
  167 +
  168 + const getValue: CreateModalDefineExposeType['getFieldsValue'] = async () => {
  169 + await validate();
  170 + const value = getFieldsValue() || {};
  171 + return value;
  172 + };
  173 +
  174 + const setValue: CreateModalDefineExposeType['setFieldsValue'] = (value) => {
  175 + resetFields();
  176 + setFieldsValue(value);
  177 + };
  178 +
  179 + defineExpose({
  180 + setFieldsValue: setValue,
  181 + getFieldsValue: getValue,
  182 + } as CreateModalDefineExposeType);
  183 + </script>
  184 +
  185 + <template>
  186 + <BasicForm @register="register" />
  187 + </template>
  188 + `;
  189 +
  190 + createFile(
  191 + `./${upperFirst(type.toLowerCase())}/${upperFirst(camelCase(data.name))}/create.vue`,
  192 + content
  193 + );
  194 + return content;
  195 +};
  196 +
  197 +const generateNodeVueConfigFile = async (type: string, data: NodeItemConfigType) => {
  198 + const content = `
  199 + import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node';
  200 + import { FormSchema } from '/@/components/Form';
  201 +
  202 + export const formSchemas: FormSchema[] = [
  203 + {
  204 + field: NodeBindDataFieldEnum.NAME,
  205 + component: 'Input',
  206 + label: NodeBindDataFieldNameEnum.NAME,
  207 + }
  208 + ];
  209 + `;
  210 +
  211 + createFile(
  212 + `./${upperFirst(type.toLowerCase())}/${upperFirst(camelCase(data.name))}/create.config.ts`,
  213 + content
  214 + );
  215 + return content;
  216 +};
  217 +
  218 +const generateNodeConfigFile = async (type: string, data: NodeItemConfigType) => {
  219 + const nodeConfigName = getNodeConfigName(data.name);
  220 + const categoryConfigName = getCategoryConfigName(type);
  221 + const content = `
  222 + import { cloneDeep } from 'lodash-es';
  223 + import { PublicNodeItemClass } from '../../../types/node';
  224 + import type {
  225 + CategoryConfigType,
  226 + CreateComponentType,
  227 + NodeItemConfigType,
  228 + } from '../../../types/node';
  229 + import { ${categoryConfigName} } from '..';
  230 + import { ${nodeConfigName} } from '.';
  231 +
  232 + export class Config extends PublicNodeItemClass implements CreateComponentType {
  233 + public config: NodeItemConfigType = cloneDeep(${nodeConfigName});
  234 +
  235 + public categoryConfig: CategoryConfigType = cloneDeep(${categoryConfigName});
  236 +
  237 + constructor() {
  238 + super();
  239 + }
  240 + }
  241 + `;
  242 +
  243 + createFile(
  244 + `./${upperFirst(type.toLowerCase())}/${upperFirst(camelCase(data.name))}/config.ts`,
  245 + content
  246 + );
  247 + return content;
  248 +};
  249 +
  250 +const bootstrap = async () => {
  251 + const groupData = groupByType();
  252 + await generateRuleNodeEnumFile(groupData);
  253 + await generateCategoryEnumFile(groupData);
  254 + for (const type of Object.keys(groupData)) {
  255 + const item = groupData[type];
  256 + await generateCategoryIndexFile(type, item);
  257 + for (const temp of item) {
  258 + await generateNodeConfigFile(type, temp);
  259 + await generateNodeIndexFile(type, temp);
  260 + await generateNodeVueConfigFile(type, temp);
  261 + await generateNodeVueTemplateFile(type, temp);
  262 + }
  263 + }
  264 +};
  265 +
  266 +bootstrap();
... ...
... ... @@ -32,13 +32,18 @@
32 32 "reinstall": "rimraf yarn.lock && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",
33 33 "prepare": "husky install",
34 34 "gen:icon": "esno ./build/generate/icon/index.ts",
35   - "gen:iconfont": "esno ./build/generate/iconfont/index.ts"
  35 + "gen:iconfont": "esno ./build/generate/iconfont/index.ts",
  36 + "gen:rule": "esno ./build/generate/ruleChain/index.ts && npx eslint \"src/views/rule/designer/**/*.{vue,ts,tsx}\" --fix"
36 37 },
37 38 "dependencies": {
38 39 "@fingerprintjs/fingerprintjs": "^3.4.1",
39 40 "@iconify/iconify": "^2.0.3",
40 41 "@logicflow/core": "^0.6.9",
41 42 "@logicflow/extension": "^0.6.9",
  43 + "@vue-flow/background": "^1.2.0",
  44 + "@vue-flow/controls": "^1.1.0",
  45 + "@vue-flow/core": "^1.22.1",
  46 + "@vue-flow/node-toolbar": "^1.1.0",
42 47 "@vueuse/core": "^10.1.0",
43 48 "@zxcvbn-ts/core": "^1.0.0-beta.0",
44 49 "ace-builds": "^1.4.14",
... ... @@ -53,6 +58,7 @@
53 58 "flv.js": "^1.6.2",
54 59 "hls.js": "^1.0.10",
55 60 "intro.js": "^4.1.0",
  61 + "js-beautify": "^1.14.9",
56 62 "jsoneditor": "^9.7.2",
57 63 "jwt-decode": "^3.1.2",
58 64 "lodash-es": "^4.17.21",
... ... @@ -68,7 +74,7 @@
68 74 "vditor": "^3.8.6",
69 75 "video.js": "^7.20.3",
70 76 "videojs-flvjs-es6": "^1.0.1",
71   - "vue": "3.2.31",
  77 + "vue": "3.3.4",
72 78 "vue-i18n": "9.1.7",
73 79 "vue-json-pretty": "^2.0.4",
74 80 "vue-router": "^4.0.11",
... ...
... ... @@ -12,11 +12,11 @@ export const getDeviceProfile = (deviceType?: string) => {
12 12 };
13 13
14 14 // 获取历史数据
15   -export const getDeviceHistoryInfo = (params: Recordable) => {
  15 +export const getDeviceHistoryInfo = (params: Recordable, orderBy?: string) => {
16 16 return defHttp.get<HistoryData>(
17 17 {
18 18 url: `/plugins/telemetry/DEVICE/${params.entityId}/values/timeseries`,
19   - params: { ...params, entityId: null, orderBy: 'ASC' },
  19 + params: { ...params, entityId: null, orderBy: orderBy || 'DESC' },
20 20 },
21 21 {
22 22 joinPrefix: false,
... ...
... ... @@ -8,6 +8,7 @@ export interface BigScreenCenterItemsModel {
8 8 remark: string;
9 9 state: number;
10 10 publicId: string;
  11 + organizationId?: string;
11 12 }
12 13 export type queryPageParams = BasicPageParams & {
13 14 name?: Nullable<string>;
... ...
... ... @@ -7,6 +7,7 @@ export interface ConfigurationCenterItemsModal {
7 7 creator: string;
8 8 remark: string;
9 9 publicId?: string;
  10 + organizationId?: string;
10 11 }
11 12 export type queryPageParams = BasicPageParams & {
12 13 name?: Nullable<string>;
... ...
... ... @@ -85,7 +85,7 @@ interface TrendParamsType {
85 85 }
86 86 // 获取租户趋势或者客户趋势数据
87 87 export const getTrendData = (params: TrendParamsType) => {
88   - return defHttp.get({
  88 + return defHttp.get<Record<'date' | 'value', string>[]>({
89 89 url: HomeEnum.TrendAPI,
90 90 params,
91 91 });
... ...
1   -import { defHttp } from '/@/utils/http/axios';
2   -import {
3   - TDeviceConfigParams,
4   - IDeviceConfigAddOrEditModel,
5   - ProfileRecord,
6   - RuleChainRecord,
7   -} from '/@/api/device/model/deviceConfigModel';
8   -import { PaginationResult } from '/#/axios';
9   -
10   -enum EDeviceConfigApi {
11   - /**
12   - * 设备配置URL
13   - */
14   - DEVICE_CONFIG_GET_PAGE = '/device_profile',
15   - DEVICE_CONFIG_POST_ADD_OR_EDIT = '/device_profile',
16   - DEVICE_CONFIG_GET_DETAIL = '/device_profile/',
17   - DEVICE_CONFIG_DELETE = '/device_profile',
18   - DEVICE_CONFIG_GET_RULECHAIN = '/rule_chain/me/list',
19   - ALARM_CONTACT_GET_PAGE = '/alarm_contact',
20   - DEVICE_CONFIG_EXPORT = '/device_profile/export',
21   - DEVICE_CONFIG_IMPORT = '/device_profile/import',
22   - SET_DEVICE_ISDEFAULT = '/deviceProfile',
23   - FRP_API = '/frp/',
24   - GET_TB_DEVICE_ID = '/device/get_subset',
25   - COMMODRECORD = '/rpc',
26   -}
27   -
28   -/**
29   - * 设备配置详情
30   - */
31   -export const deviceConfigGetDetail = (id: string) => {
32   - return defHttp.get({
33   - url: `${EDeviceConfigApi.DEVICE_CONFIG_GET_DETAIL}${id}`,
34   - });
35   -};
36   -
37   -/**
38   - * 获取规则链
39   - */
40   -export const deviceConfigGetRuleChain = () => {
41   - return defHttp.get<RuleChainRecord[]>({
42   - url: EDeviceConfigApi.DEVICE_CONFIG_GET_RULECHAIN,
43   - });
44   -};
45   -
46   -/**
47   - * 获取告警联系人
48   - */
49   -export const alarmContactGetPage = () => {
50   - return defHttp.get({
51   - url: `${EDeviceConfigApi.ALARM_CONTACT_GET_PAGE}?page=1&pageSize=10`,
52   - });
53   -};
54   -
55   -/**
56   - * 分页查询设备配置页面
57   - */
58   -export const deviceConfigGetQuery = (params?: TDeviceConfigParams) => {
59   - return defHttp.get<PaginationResult<ProfileRecord>>({
60   - url: EDeviceConfigApi.DEVICE_CONFIG_GET_PAGE,
61   - params,
62   - });
63   -};
64   -
65   -/**
66   - * 新增或者编辑设备配置
67   -
68   - */
69   -export const deviceConfigAddOrEdit = (params: IDeviceConfigAddOrEditModel) => {
70   - return defHttp.post<IDeviceConfigAddOrEditModel>({
71   - url: EDeviceConfigApi.DEVICE_CONFIG_POST_ADD_OR_EDIT,
72   - params,
73   - });
74   -};
75   -
76   -/**
77   - * 删除设备配置
78   - */
79   -export const deviceConfigDelete = (ids: string[]) => {
80   - return defHttp.delete({
81   - url: EDeviceConfigApi.DEVICE_CONFIG_DELETE,
82   - data: {
83   - ids,
84   - },
85   - });
86   -};
87   -
88   -/**
89   - * 导出设备配置
90   - */
91   -export const deviceConfigExport = (params: IDeviceConfigAddOrEditModel) => {
92   - return defHttp.post<IDeviceConfigAddOrEditModel>({
93   - url: EDeviceConfigApi.DEVICE_CONFIG_EXPORT,
94   - params,
95   - });
96   -};
97   -
98   -/**
99   - * 导入设备配置
100   - */
101   -export const deviceConfigImport = (params: IDeviceConfigAddOrEditModel) => {
102   - return defHttp.post<IDeviceConfigAddOrEditModel>({
103   - url: EDeviceConfigApi.DEVICE_CONFIG_IMPORT,
104   - params,
105   - });
106   -};
107   -
108   -/**
109   - *
110   - * 设置该设备配置为默认
111   - */
112   -export const setDeviceProfileIsDefaultApi = (id: string, v, params?: {}) => {
113   - return defHttp.post(
114   - {
115   - url: EDeviceConfigApi.SET_DEVICE_ISDEFAULT + '/' + id + '/' + v,
116   - params,
117   - },
118   - {
119   - joinPrefix: false,
120   - }
121   - );
122   -};
123   -
124   -/**
125   - * Frp内网穿透信息API
126   - */
127   -export const frpGetInfoApi = (proxyName: string) => {
128   - return defHttp.get({
129   - url: `${EDeviceConfigApi.FRP_API}${proxyName}`,
130   - });
131   -};
132   -
133   -export const frpPutInfoApi = (proxyName: string, enableRemote: number) => {
134   - return defHttp.put({
135   - url: `${EDeviceConfigApi.FRP_API}${proxyName}/${enableRemote}`,
136   - });
137   -};
138   -
139   -export const getTbDeviceId = (params: string) => {
140   - return defHttp.get({
141   - url: `${EDeviceConfigApi.GET_TB_DEVICE_ID}/${params}`,
142   - });
143   -};
144   -
145   -/**
146   - * 命令下发记录
147   - */
148   -export const deviceCommandRecordGetQuery = (params?: TDeviceConfigParams) => {
149   - return defHttp.get({
150   - url: EDeviceConfigApi.COMMODRECORD,
151   - params,
152   - });
153   -};
  1 +import { defHttp } from '/@/utils/http/axios';
  2 +import {
  3 + TDeviceConfigParams,
  4 + IDeviceConfigAddOrEditModel,
  5 + ProfileRecord,
  6 + RuleChainRecord,
  7 + DeviceProfileDetail,
  8 +} from '/@/api/device/model/deviceConfigModel';
  9 +import { PaginationResult } from '/#/axios';
  10 +
  11 +enum EDeviceConfigApi {
  12 + /**
  13 + * 设备配置URL
  14 + */
  15 + DEVICE_CONFIG_GET_PAGE = '/device_profile',
  16 + DEVICE_CONFIG_POST_ADD_OR_EDIT = '/device_profile',
  17 + DEVICE_CONFIG_GET_DETAIL = '/device_profile/',
  18 + DEVICE_CONFIG_DELETE = '/device_profile',
  19 + DEVICE_CONFIG_GET_RULECHAIN = '/rule_chain/me/list',
  20 + ALARM_CONTACT_GET_PAGE = '/alarm_contact',
  21 + DEVICE_CONFIG_EXPORT = '/device_profile/export',
  22 + DEVICE_CONFIG_IMPORT = '/device_profile/import',
  23 + SET_DEVICE_ISDEFAULT = '/deviceProfile',
  24 + FRP_API = '/frp/',
  25 + GET_TB_DEVICE_ID = '/device/get_subset',
  26 + COMMODRECORD = '/rpc',
  27 +}
  28 +
  29 +/**
  30 + * 设备配置详情
  31 + */
  32 +export const deviceConfigGetDetail = (id: string) => {
  33 + return defHttp.get<DeviceProfileDetail>({
  34 + url: `${EDeviceConfigApi.DEVICE_CONFIG_GET_DETAIL}${id}`,
  35 + });
  36 +};
  37 +
  38 +/**
  39 + * 获取规则链
  40 + */
  41 +export const deviceConfigGetRuleChain = () => {
  42 + return defHttp.get<RuleChainRecord[]>({
  43 + url: EDeviceConfigApi.DEVICE_CONFIG_GET_RULECHAIN,
  44 + });
  45 +};
  46 +
  47 +/**
  48 + * 获取告警联系人
  49 + */
  50 +export const alarmContactGetPage = () => {
  51 + return defHttp.get({
  52 + url: `${EDeviceConfigApi.ALARM_CONTACT_GET_PAGE}?page=1&pageSize=10`,
  53 + });
  54 +};
  55 +
  56 +/**
  57 + * 分页查询设备配置页面
  58 + */
  59 +export const deviceConfigGetQuery = (params?: TDeviceConfigParams) => {
  60 + return defHttp.get<PaginationResult<ProfileRecord>>({
  61 + url: EDeviceConfigApi.DEVICE_CONFIG_GET_PAGE,
  62 + params,
  63 + });
  64 +};
  65 +
  66 +/**
  67 + * 新增或者编辑设备配置
  68 +
  69 + */
  70 +export const deviceConfigAddOrEdit = (params: IDeviceConfigAddOrEditModel) => {
  71 + return defHttp.post<IDeviceConfigAddOrEditModel>({
  72 + url: EDeviceConfigApi.DEVICE_CONFIG_POST_ADD_OR_EDIT,
  73 + params,
  74 + });
  75 +};
  76 +
  77 +/**
  78 + * 删除设备配置
  79 + */
  80 +export const deviceConfigDelete = (ids: string[]) => {
  81 + return defHttp.delete({
  82 + url: EDeviceConfigApi.DEVICE_CONFIG_DELETE,
  83 + data: {
  84 + ids,
  85 + },
  86 + });
  87 +};
  88 +
  89 +/**
  90 + * 导出设备配置
  91 + */
  92 +export const deviceConfigExport = (params: IDeviceConfigAddOrEditModel) => {
  93 + return defHttp.post<IDeviceConfigAddOrEditModel>({
  94 + url: EDeviceConfigApi.DEVICE_CONFIG_EXPORT,
  95 + params,
  96 + });
  97 +};
  98 +
  99 +/**
  100 + * 导入设备配置
  101 + */
  102 +export const deviceConfigImport = (params: IDeviceConfigAddOrEditModel) => {
  103 + return defHttp.post<IDeviceConfigAddOrEditModel>({
  104 + url: EDeviceConfigApi.DEVICE_CONFIG_IMPORT,
  105 + params,
  106 + });
  107 +};
  108 +
  109 +/**
  110 + *
  111 + * 设置该设备配置为默认
  112 + */
  113 +export const setDeviceProfileIsDefaultApi = (id: string, v, params?: {}) => {
  114 + return defHttp.post(
  115 + {
  116 + url: EDeviceConfigApi.SET_DEVICE_ISDEFAULT + '/' + id + '/' + v,
  117 + params,
  118 + },
  119 + {
  120 + joinPrefix: false,
  121 + }
  122 + );
  123 +};
  124 +
  125 +/**
  126 + * Frp内网穿透信息API
  127 + */
  128 +export const frpGetInfoApi = (proxyName: string) => {
  129 + return defHttp.get({
  130 + url: `${EDeviceConfigApi.FRP_API}${proxyName}`,
  131 + });
  132 +};
  133 +
  134 +export const frpPutInfoApi = (proxyName: string, enableRemote: number) => {
  135 + return defHttp.put({
  136 + url: `${EDeviceConfigApi.FRP_API}${proxyName}/${enableRemote}`,
  137 + });
  138 +};
  139 +
  140 +export const getTbDeviceId = (params: string) => {
  141 + return defHttp.get({
  142 + url: `${EDeviceConfigApi.GET_TB_DEVICE_ID}/${params}`,
  143 + });
  144 +};
  145 +
  146 +/**
  147 + * 命令下发记录
  148 + */
  149 +export const deviceCommandRecordGetQuery = (params?: TDeviceConfigParams) => {
  150 + return defHttp.get({
  151 + url: EDeviceConfigApi.COMMODRECORD,
  152 + params,
  153 + });
  154 +};
... ...
... ... @@ -28,6 +28,10 @@ enum DeviceManagerApi {
28 28
29 29 DEVICE_ALARM_URL = '/alarm',
30 30
  31 + ALARM_BATCH_ACK = '/alarm/batch/ack',
  32 +
  33 + ALARM_BATCH_CLEAR = '/alarm/batch/clear',
  34 +
31 35 DEVICE_CREDENTIALS = '/device/credentials',
32 36
33 37 COMMAND_ISSUANCE = '/rpc',
... ... @@ -346,3 +350,23 @@ export const getDevicesByDeviceIds = (ids: string[]) => {
346 350 data: ids,
347 351 });
348 352 };
  353 +
  354 +export const doBatchAckAlarm = (ids: string[]) => {
  355 + return defHttp.post(
  356 + {
  357 + url: DeviceManagerApi.ALARM_BATCH_ACK,
  358 + data: { alarmIds: ids },
  359 + },
  360 + { joinPrefix: false }
  361 + );
  362 +};
  363 +
  364 +export const doBatchClearAlarm = (ids: string[]) => {
  365 + return defHttp.post(
  366 + {
  367 + url: DeviceManagerApi.ALARM_BATCH_CLEAR,
  368 + data: { alarmIds: ids },
  369 + },
  370 + { joinPrefix: false }
  371 + );
  372 +};
... ...
1   -import { BasicPageParams } from '/@/api/model/baseModel';
2   -
3   -export type TDeviceConfigPageQueryParam = BasicPageParams & TDeviceConfigParams;
4   -
5   -export type TDeviceConfigParams = {
6   - page?: any;
7   - pageSize?: any;
8   - name?: string;
9   - transportType?: string;
10   - orderFiled?: string;
11   - orderType?: string;
12   - tbDeviceId?: string;
13   -};
14   -
15   -export interface IDeviceConfigAddOrEditModel {
16   - defaultQueueName?: string; //处理队列
17   - alarmProfile?: {
18   - alarmContactId: string;
19   - createTime: '2021-12-15T02:17:26.644Z';
20   - creator: string;
21   - defaultConfig: string;
22   - description: string;
23   - deviceProfileId: string;
24   - enabled: true;
25   - icon: string;
26   - id: string;
27   - messageMode: string;
28   - name: string;
29   - roleIds: [string];
30   - tenantExpireTime: '2021-12-15T02:17:26.644Z';
31   - tenantId: string;
32   - tenantStatus: 'DISABLED';
33   - updateTime: '2021-12-15T02:17:26.644Z';
34   - updater: string;
35   - };
36   - convertJs?: string;
37   - createTime?: '2021-12-15T02:17:26.644Z';
38   - creator?: string;
39   - defaultConfig?: string;
40   - defaultRuleChainId?: string;
41   - description?: string;
42   - enabled?: true;
43   - icon?: string;
44   - id?: string;
45   - name?: string;
46   - profileData?: {
47   - configuration: {};
48   - transportConfiguration: {};
49   - provisionConfiguration: {
50   - provisionDeviceSecret: string;
51   - };
52   - //报警类型字段
53   - alarms: [
54   - {
55   - id: 'highTemperatureAlarmID';
56   - alarmType: 'High Temperature Alarm';
57   - createRules: {
58   - additionalProp1: {
59   - condition: {
60   - condition: [
61   - {
62   - key: {
63   - type: 'TIME_SERIES';
64   - key: 'temp';
65   - };
66   - valueType: 'NUMERIC';
67   - value: {};
68   - predicate: {};
69   - }
70   - ];
71   - spec: {};
72   - };
73   - schedule: {
74   - type: 'ANY_TIME';
75   - };
76   - alarmDetails: string;
77   - dashboardId: {
78   - id: '784f394c-42b6-435a-983c-b7beff2784f9';
79   - entityType: 'DASHBOARD';
80   - };
81   - };
82   - additionalProp2: {
83   - condition: {
84   - condition: [
85   - {
86   - key: {
87   - type: 'TIME_SERIES';
88   - key: 'temp';
89   - };
90   - valueType: 'NUMERIC';
91   - value: {};
92   - predicate: {};
93   - }
94   - ];
95   - spec: {};
96   - };
97   - schedule: {
98   - type: 'ANY_TIME';
99   - };
100   - alarmDetails: string;
101   - dashboardId: {
102   - id: '784f394c-42b6-435a-983c-b7beff2784f9';
103   - entityType: 'DASHBOARD';
104   - };
105   - };
106   - additionalProp3: {
107   - condition: {
108   - condition: [
109   - {
110   - key: {
111   - type: 'TIME_SERIES';
112   - key: 'temp';
113   - };
114   - valueType: 'NUMERIC';
115   - value: {};
116   - predicate: {};
117   - }
118   - ];
119   - spec: {};
120   - };
121   - schedule: {
122   - type: 'ANY_TIME';
123   - };
124   - alarmDetails: string;
125   - dashboardId: {
126   - id: '784f394c-42b6-435a-983c-b7beff2784f9';
127   - entityType: 'DASHBOARD';
128   - };
129   - };
130   - };
131   - clearRule: {
132   - condition: {
133   - condition: [
134   - {
135   - key: {
136   - type: 'TIME_SERIES';
137   - key: 'temp';
138   - };
139   - valueType: 'NUMERIC';
140   - value: {};
141   - predicate: {};
142   - }
143   - ];
144   - spec: {};
145   - };
146   - schedule: {
147   - type: 'ANY_TIME';
148   - };
149   - alarmDetails: string;
150   - dashboardId: {
151   - id: '784f394c-42b6-435a-983c-b7beff2784f9';
152   - entityType: 'DASHBOARD';
153   - };
154   - };
155   - propagate: true;
156   - propagateRelationTypes: [string];
157   - }
158   - ];
159   - };
160   - roleIds?: [string];
161   - tbProfileId?: string;
162   - tenantExpireTime?: '2021-12-15T02:17:26.645Z';
163   - tenantId?: string;
164   - tenantStatus?: 'DISABLED';
165   - transportType?: string;
166   - updateTime?: '2021-12-15T02:17:26.645Z';
167   - updater?: string;
168   -}
169   -
170   -export interface Data {
171   - CO: number;
172   -}
173   -
174   -export interface Details {
175   - data: Data;
176   -}
177   -
178   -export interface AlarmLogItem {
179   - id: string;
180   - tenantId: string;
181   - creator?: any;
182   - updater?: any;
183   - createdTime: string;
184   - updatedTime: string;
185   - customerId: string;
186   - tbDeviceId: string;
187   - originatorType: number;
188   - deviceId: string;
189   - deviceName: string;
190   - type: string;
191   - severity: string;
192   - status: string;
193   - startTs: string;
194   - endTs: string;
195   - ackTs: string;
196   - clearTs: string;
197   - details: Details;
198   - propagate: boolean;
199   - propagateRelationTypes?: any;
200   - organizationId: string;
201   - organizationName: string;
202   -}
203   -
204   -export interface Configuration {
205   - type: string;
206   -}
207   -
208   -export interface TransportConfiguration {
209   - type: string;
210   -}
211   -
212   -export interface ProvisionConfiguration {
213   - type: string;
214   - provisionDeviceSecret?: any;
215   -}
216   -
217   -export interface ProfileData {
218   - configuration: Configuration;
219   - transportConfiguration: TransportConfiguration;
220   - provisionConfiguration: ProvisionConfiguration;
221   - alarms?: any;
222   -}
223   -
224   -export interface ProfileRecord {
225   - id: string;
226   - creator: string;
227   - createTime: string;
228   - updater: string;
229   - updateTime: string;
230   - name: string;
231   - tenantId: string;
232   - transportType: string;
233   - provisionType: string;
234   - deviceType: string;
235   - tbProfileId: string;
236   - profileData: ProfileData;
237   - defaultRuleChainId: string;
238   - defaultQueueName: string;
239   - image: string;
240   - type: string;
241   - default: boolean;
242   -
243   - checked?: boolean;
244   -}
245   -
246   -export interface IDRecord {
247   - entityType: string;
248   - id: string;
249   -}
250   -
251   -export interface RuleChainRecord {
252   - id: IDRecord;
253   - createdTime: number;
254   - additionalInfo?: any;
255   - tenantId: IDRecord;
256   - name: string;
257   - type: string;
258   - firstRuleNodeId: IDRecord;
259   - root: boolean;
260   - debugMode: boolean;
261   - configuration?: any;
262   -}
  1 +import { BasicPageParams } from '/@/api/model/baseModel';
  2 +
  3 +export type TDeviceConfigPageQueryParam = BasicPageParams & TDeviceConfigParams;
  4 +
  5 +export type TDeviceConfigParams = {
  6 + page?: any;
  7 + pageSize?: any;
  8 + name?: string;
  9 + transportType?: string;
  10 + orderFiled?: string;
  11 + orderType?: string;
  12 + tbDeviceId?: string;
  13 +};
  14 +
  15 +export interface IDeviceConfigAddOrEditModel {
  16 + defaultQueueName?: string; //处理队列
  17 + alarmProfile?: {
  18 + alarmContactId: string;
  19 + createTime: '2021-12-15T02:17:26.644Z';
  20 + creator: string;
  21 + defaultConfig: string;
  22 + description: string;
  23 + deviceProfileId: string;
  24 + enabled: true;
  25 + icon: string;
  26 + id: string;
  27 + messageMode: string;
  28 + name: string;
  29 + roleIds: [string];
  30 + tenantExpireTime: '2021-12-15T02:17:26.644Z';
  31 + tenantId: string;
  32 + tenantStatus: 'DISABLED';
  33 + updateTime: '2021-12-15T02:17:26.644Z';
  34 + updater: string;
  35 + };
  36 + convertJs?: string;
  37 + createTime?: '2021-12-15T02:17:26.644Z';
  38 + creator?: string;
  39 + defaultConfig?: string;
  40 + defaultRuleChainId?: string;
  41 + description?: string;
  42 + enabled?: true;
  43 + icon?: string;
  44 + id?: string;
  45 + name?: string;
  46 + profileData?: {
  47 + configuration: {};
  48 + transportConfiguration: {};
  49 + provisionConfiguration: {
  50 + provisionDeviceSecret: string;
  51 + };
  52 + //报警类型字段
  53 + alarms: [
  54 + {
  55 + id: 'highTemperatureAlarmID';
  56 + alarmType: 'High Temperature Alarm';
  57 + createRules: {
  58 + additionalProp1: {
  59 + condition: {
  60 + condition: [
  61 + {
  62 + key: {
  63 + type: 'TIME_SERIES';
  64 + key: 'temp';
  65 + };
  66 + valueType: 'NUMERIC';
  67 + value: {};
  68 + predicate: {};
  69 + }
  70 + ];
  71 + spec: {};
  72 + };
  73 + schedule: {
  74 + type: 'ANY_TIME';
  75 + };
  76 + alarmDetails: string;
  77 + dashboardId: {
  78 + id: '784f394c-42b6-435a-983c-b7beff2784f9';
  79 + entityType: 'DASHBOARD';
  80 + };
  81 + };
  82 + additionalProp2: {
  83 + condition: {
  84 + condition: [
  85 + {
  86 + key: {
  87 + type: 'TIME_SERIES';
  88 + key: 'temp';
  89 + };
  90 + valueType: 'NUMERIC';
  91 + value: {};
  92 + predicate: {};
  93 + }
  94 + ];
  95 + spec: {};
  96 + };
  97 + schedule: {
  98 + type: 'ANY_TIME';
  99 + };
  100 + alarmDetails: string;
  101 + dashboardId: {
  102 + id: '784f394c-42b6-435a-983c-b7beff2784f9';
  103 + entityType: 'DASHBOARD';
  104 + };
  105 + };
  106 + additionalProp3: {
  107 + condition: {
  108 + condition: [
  109 + {
  110 + key: {
  111 + type: 'TIME_SERIES';
  112 + key: 'temp';
  113 + };
  114 + valueType: 'NUMERIC';
  115 + value: {};
  116 + predicate: {};
  117 + }
  118 + ];
  119 + spec: {};
  120 + };
  121 + schedule: {
  122 + type: 'ANY_TIME';
  123 + };
  124 + alarmDetails: string;
  125 + dashboardId: {
  126 + id: '784f394c-42b6-435a-983c-b7beff2784f9';
  127 + entityType: 'DASHBOARD';
  128 + };
  129 + };
  130 + };
  131 + clearRule: {
  132 + condition: {
  133 + condition: [
  134 + {
  135 + key: {
  136 + type: 'TIME_SERIES';
  137 + key: 'temp';
  138 + };
  139 + valueType: 'NUMERIC';
  140 + value: {};
  141 + predicate: {};
  142 + }
  143 + ];
  144 + spec: {};
  145 + };
  146 + schedule: {
  147 + type: 'ANY_TIME';
  148 + };
  149 + alarmDetails: string;
  150 + dashboardId: {
  151 + id: '784f394c-42b6-435a-983c-b7beff2784f9';
  152 + entityType: 'DASHBOARD';
  153 + };
  154 + };
  155 + propagate: true;
  156 + propagateRelationTypes: [string];
  157 + }
  158 + ];
  159 + };
  160 + roleIds?: [string];
  161 + tbProfileId?: string;
  162 + tenantExpireTime?: '2021-12-15T02:17:26.645Z';
  163 + tenantId?: string;
  164 + tenantStatus?: 'DISABLED';
  165 + transportType?: string;
  166 + updateTime?: '2021-12-15T02:17:26.645Z';
  167 + updater?: string;
  168 +}
  169 +
  170 +export interface Data {
  171 + CO: number;
  172 +}
  173 +
  174 +export interface Details {
  175 + data: Data;
  176 +}
  177 +
  178 +export interface AlarmLogItem {
  179 + id: string;
  180 + tenantId: string;
  181 + creator?: any;
  182 + updater?: any;
  183 + createdTime: string;
  184 + updatedTime: string;
  185 + customerId: string;
  186 + tbDeviceId: string;
  187 + originatorType: number;
  188 + deviceId: string;
  189 + deviceName: string;
  190 + type: string;
  191 + severity: string;
  192 + status: string;
  193 + startTs: string;
  194 + endTs: string;
  195 + ackTs: string;
  196 + clearTs: string;
  197 + details: Details;
  198 + propagate: boolean;
  199 + propagateRelationTypes?: any;
  200 + organizationId: string;
  201 + organizationName: string;
  202 +}
  203 +
  204 +export interface Configuration {
  205 + type: string;
  206 +}
  207 +
  208 +export interface TransportConfiguration {
  209 + type: string;
  210 +}
  211 +
  212 +export interface ProvisionConfiguration {
  213 + type: string;
  214 + provisionDeviceSecret?: any;
  215 +}
  216 +
  217 +export interface ProfileData {
  218 + configuration: Configuration;
  219 + transportConfiguration: TransportConfiguration;
  220 + provisionConfiguration: ProvisionConfiguration;
  221 + alarms?: any;
  222 +}
  223 +
  224 +export interface ProfileRecord {
  225 + id: string;
  226 + creator: string;
  227 + createTime: string;
  228 + updater: string;
  229 + updateTime: string;
  230 + name: string;
  231 + tenantId: string;
  232 + transportType: string;
  233 + provisionType: string;
  234 + deviceType: string;
  235 + tbProfileId: string;
  236 + profileData: ProfileData;
  237 + defaultRuleChainId: string;
  238 + defaultQueueName: string;
  239 + image: string;
  240 + type: string;
  241 + default: boolean;
  242 +
  243 + checked?: boolean;
  244 +}
  245 +
  246 +export interface IDRecord {
  247 + entityType: string;
  248 + id: string;
  249 +}
  250 +
  251 +export interface RuleChainRecord {
  252 + id: IDRecord;
  253 + createdTime: number;
  254 + additionalInfo?: any;
  255 + tenantId: IDRecord;
  256 + name: string;
  257 + type: string;
  258 + firstRuleNodeId: IDRecord;
  259 + root: boolean;
  260 + debugMode: boolean;
  261 + configuration?: any;
  262 +}
  263 +
  264 +export interface DeviceProfileDetail {
  265 + id: string;
  266 + creator: string;
  267 + createTime: string;
  268 + updater: string;
  269 + updateTime: string;
  270 + name: string;
  271 + description: string;
  272 + tenantId: string;
  273 + transportType: string;
  274 + provisionType: string;
  275 + deviceType: string;
  276 + tbProfileId: string;
  277 + profileData: ProfileData;
  278 + defaultQueueName: string;
  279 + type: string;
  280 + deviceCount: number;
  281 + default: boolean;
  282 +}
  283 +
  284 +export interface ProfileData {
  285 + configuration: Configuration;
  286 + transportConfiguration: TransportConfiguration;
  287 + provisionConfiguration: ProvisionConfiguration;
  288 + alarms: any;
  289 + thingsModel: any;
  290 +}
  291 +
  292 +export interface Configuration {
  293 + type: string;
  294 +}
  295 +
  296 +export interface TransportConfiguration {
  297 + type: string;
  298 +}
  299 +
  300 +export interface ProvisionConfiguration {
  301 + type: string;
  302 + provisionDeviceSecret: any;
  303 +}
... ...
  1 +import { DeviceInfoItemType, DeviceTypeItem, PageParams, ScriptTestParams } from './model';
  2 +import { TBPaginationResult } from '/#/axios';
  3 +import { defHttp } from '/@/utils/http/axios';
  4 +
  5 +enum Api {
  6 + GET_DEVICE_INFOS = '/tenant/deviceInfos',
  7 + GET_DEVICE_TYPE = '/device/types',
  8 + TENANT_QUEUE = '/tenant/queues',
  9 + TEST_SCRIPT = '/ruleChain/testScript',
  10 +}
  11 +
  12 +enum Entity {
  13 + DEVICES = '/tenant/devices',
  14 + ASSETS = '/tenant/assets',
  15 + ENTITY_VIEW = '/tenant/entityViews',
  16 + TENANT = '/tenant',
  17 + CUSTOMER = '/customers',
  18 + DASHBOARD = '/tenant/dashboards',
  19 + USER = '/users',
  20 + EDGE = '/tenant/edges',
  21 +}
  22 +
  23 +export const getDeviceInfos = () => {
  24 + return defHttp.get<TBPaginationResult<DeviceInfoItemType>>(
  25 + {
  26 + url: Api.GET_DEVICE_INFOS,
  27 + },
  28 + { joinPrefix: false }
  29 + );
  30 +};
  31 +
  32 +export const getDeviceTypes = () => {
  33 + return defHttp.get<TBPaginationResult<DeviceTypeItem[]>>(
  34 + {
  35 + url: Api.GET_DEVICE_TYPE,
  36 + },
  37 + { joinPrefix: false }
  38 + );
  39 +};
  40 +
  41 +export const getTenantQueue = (params: Recordable) => {
  42 + return defHttp.get<string[]>(
  43 + {
  44 + url: Api.TENANT_QUEUE,
  45 + params,
  46 + },
  47 + { joinPrefix: false }
  48 + );
  49 +};
  50 +
  51 +export const getEntityDevice = (params: PageParams) => {
  52 + return defHttp.get(
  53 + {
  54 + url: Entity.DEVICES,
  55 + params,
  56 + },
  57 + {
  58 + joinPrefix: false,
  59 + }
  60 + );
  61 +};
  62 +
  63 +export const getEntityAssets = (params: PageParams) => {
  64 + return defHttp.get(
  65 + {
  66 + url: Entity.ASSETS,
  67 + params,
  68 + },
  69 + {
  70 + joinPrefix: false,
  71 + }
  72 + );
  73 +};
  74 +
  75 +export const getEntityViews = (params: PageParams) => {
  76 + return defHttp.get(
  77 + {
  78 + url: Entity.ENTITY_VIEW,
  79 + params,
  80 + },
  81 + {
  82 + joinPrefix: false,
  83 + }
  84 + );
  85 +};
  86 +
  87 +export const getEntityTenant = (params: Record<'tenantId', string>) => {
  88 + return defHttp.get(
  89 + {
  90 + url: `${Entity.TENANT}/${params.tenantId}`,
  91 + params,
  92 + },
  93 + {
  94 + joinPrefix: false,
  95 + }
  96 + );
  97 +};
  98 +
  99 +export const getEntityCustomer = (params: PageParams) => {
  100 + return defHttp.get(
  101 + {
  102 + url: Entity.CUSTOMER,
  103 + params,
  104 + },
  105 + {
  106 + joinPrefix: false,
  107 + }
  108 + );
  109 +};
  110 +
  111 +export const getEntityUser = (params: PageParams) => {
  112 + return defHttp.get(
  113 + {
  114 + url: Entity.USER,
  115 + params,
  116 + },
  117 + {
  118 + joinPrefix: false,
  119 + }
  120 + );
  121 +};
  122 +
  123 +export const getEntityDashboard = (params: PageParams) => {
  124 + return defHttp.get(
  125 + {
  126 + url: Entity.DASHBOARD,
  127 + params,
  128 + },
  129 + {
  130 + joinPrefix: false,
  131 + }
  132 + );
  133 +};
  134 +
  135 +export const getEntityEdge = (params: PageParams) => {
  136 + return defHttp.get(
  137 + {
  138 + url: Entity.EDGE,
  139 + params,
  140 + },
  141 + {
  142 + joinPrefix: false,
  143 + }
  144 + );
  145 +};
  146 +
  147 +export const testScript = (data: ScriptTestParams) => {
  148 + return defHttp.post<Record<'error' | 'output', string>>(
  149 + {
  150 + url: Api.TEST_SCRIPT,
  151 + data,
  152 + },
  153 + { joinPrefix: false }
  154 + );
  155 +};
... ...
  1 +export interface DeviceInfoItemType {
  2 + id: Id;
  3 + createdTime: number;
  4 + additionalInfo: AdditionalInfo;
  5 + tenantId: Id;
  6 + customerId: Id;
  7 + name: string;
  8 + type: string;
  9 + label: string;
  10 + deviceProfileId: Id;
  11 + deviceData: DeviceData;
  12 + firmwareId: any;
  13 + softwareId: any;
  14 + customerTitle: any;
  15 + customerIsPublic: boolean;
  16 + deviceProfileName: string;
  17 +}
  18 +
  19 +export interface Id {
  20 + entityType: string;
  21 + id: string;
  22 +}
  23 +
  24 +export interface DeviceData {
  25 + configuration: Configuration;
  26 + transportConfiguration: TransportConfiguration;
  27 +}
  28 +
  29 +export interface AdditionalInfo {
  30 + gateway: boolean;
  31 + description: string;
  32 + overwriteActivityTime: boolean;
  33 +}
  34 +export interface Configuration {
  35 + type: string;
  36 +}
  37 +
  38 +export interface TransportConfiguration {
  39 + type: string;
  40 +}
  41 +
  42 +export interface DeviceTypeItem {
  43 + tenantId: Id;
  44 + entityType: string;
  45 + type: string;
  46 +}
  47 +
  48 +export interface PageParams {
  49 + pageSize?: number;
  50 + page?: number;
  51 + textSearch?: string;
  52 + sortProperty?: string;
  53 + sortOrder?: string;
  54 +}
  55 +
  56 +export interface ScriptTestParams {
  57 + argNames?: string[];
  58 + metadata?: Recordable;
  59 + msg?: string;
  60 + msgType?: string;
  61 + script?: string;
  62 + scriptType?: string;
  63 +}
... ...
  1 +import { RuleChainPaginationItemType } from './model/type';
  2 +import { TBPaginationResult } from '/#/axios';
  3 +import { defHttp } from '/@/utils/http/axios';
  4 +import { RuleChainDetail, RuleChainType } from '/@/views/rule/designer/types/ruleNode';
  5 +
  6 +enum Api {
  7 + SAVE_RULE_CHAINS = '/ruleChain',
  8 + GET_RULE_CHAINS_DETAIL = '/ruleChain',
  9 + SAVE = '/ruleChain/metadata',
  10 + GET_RULE_CHAINES = '/ruleChains',
  11 + GET_RULE_NODE_EVENTS = '/events/RULE_NODE',
  12 +}
  13 +
  14 +export const saveRuleChainDetail = (params: Partial<RuleChainDetail>) => {
  15 + return defHttp.post<RuleChainDetail>(
  16 + {
  17 + url: Api.SAVE_RULE_CHAINS,
  18 + data: params,
  19 + },
  20 + { joinPrefix: false }
  21 + );
  22 +};
  23 +
  24 +export const getRuleChainDetail = (id: string) => {
  25 + return defHttp.get<RuleChainDetail>(
  26 + {
  27 + url: `${Api.GET_RULE_CHAINS_DETAIL}/${id}`,
  28 + },
  29 + { joinPrefix: false }
  30 + );
  31 +};
  32 +
  33 +export const getRuleChainData = (id: string) => {
  34 + return defHttp.get<RuleChainType>(
  35 + {
  36 + url: `/ruleChain/${id}/metadata`,
  37 + },
  38 + { joinPrefix: false }
  39 + );
  40 +};
  41 +
  42 +export const saveRuleChainData = (data: RuleChainType) => {
  43 + return defHttp.post(
  44 + {
  45 + url: Api.SAVE,
  46 + data,
  47 + },
  48 + { joinPrefix: false }
  49 + );
  50 +};
  51 +
  52 +export const getRuleChains = (params: Recordable) => {
  53 + return defHttp.get<TBPaginationResult<RuleChainPaginationItemType>>(
  54 + {
  55 + url: Api.GET_RULE_CHAINES,
  56 + params,
  57 + },
  58 + {
  59 + joinPrefix: false,
  60 + }
  61 + );
  62 +};
  63 +
  64 +export const getRuleNodeEventList = (
  65 + ruleNodeId: string,
  66 + params: Recordable,
  67 + data: Recordable & Record<'eventType', string>
  68 +) => {
  69 + return defHttp.post<TBPaginationResult>(
  70 + {
  71 + url: `${Api.GET_RULE_NODE_EVENTS}/${ruleNodeId}`,
  72 + params,
  73 + data,
  74 + },
  75 + { joinPrefix: false }
  76 + );
  77 +};
... ...
  1 +export interface RuleChainPaginationItemType {
  2 + id: Id;
  3 + createdTime: number;
  4 + additionalInfo?: AdditionalInfo;
  5 + tenantId: Id;
  6 + name: string;
  7 + type: string;
  8 + firstRuleNodeId: Id;
  9 + root: boolean;
  10 + debugMode: boolean;
  11 + configuration: any;
  12 +}
  13 +
  14 +export interface Id {
  15 + entityType: string;
  16 + id: string;
  17 +}
  18 +
  19 +export interface AdditionalInfo {
  20 + description: string;
  21 +}
... ...
... ... @@ -22,6 +22,8 @@ enum ScreenManagerApi {
22 22 GET_ATTRBUTELIST = '/device/attributes/',
23 23 ALARM_PROFILE = '/alarm/profile/',
24 24 MASTER_GET_DEVICE = '/device/list',
  25 + RULE_CHAINS = '/ruleChains',
  26 + RULE_CHAIN = '/ruleChain',
25 27 }
26 28
27 29 /**
... ... @@ -130,3 +132,83 @@ export const byOrganizationIdGetMasterDevice = (params: {
130 132 });
131 133 };
132 134 //TODO-fengtao
  135 +
  136 +/**
  137 + * 分页查询规则链库
  138 + */
  139 +
  140 +export const getRuleChinsList = (params) => {
  141 + return defHttp.get(
  142 + {
  143 + url: `${ScreenManagerApi.RULE_CHAINS}`,
  144 + params,
  145 + },
  146 + { joinPrefix: false }
  147 + );
  148 +};
  149 +
  150 +/**
  151 + * 新增规则链
  152 + */
  153 +
  154 +export const createRuleChine = (params) => {
  155 + return defHttp.post(
  156 + {
  157 + url: `${ScreenManagerApi.RULE_CHAIN}`,
  158 + params,
  159 + },
  160 + { joinPrefix: false }
  161 + );
  162 +};
  163 +
  164 +export const importRuleChine = (params) => {
  165 + return defHttp.post(
  166 + {
  167 + url: `${ScreenManagerApi.RULE_CHAIN}/metadata`,
  168 + params,
  169 + },
  170 + { joinPrefix: false }
  171 + );
  172 +};
  173 +
  174 +/**
  175 + * 删除规则链
  176 + */
  177 +
  178 +export const deleteRuleChine = (id) => {
  179 + return defHttp.delete(
  180 + {
  181 + url: `${ScreenManagerApi.RULE_CHAIN}/`,
  182 + params: id,
  183 + },
  184 + { joinPrefix: false }
  185 + );
  186 +};
  187 +
  188 +/**
  189 + * 导出规则链
  190 + */
  191 +
  192 +export const exportRuleChine = (id) => {
  193 + return defHttp.get(
  194 + {
  195 + url: `${ScreenManagerApi.RULE_CHAIN}/`,
  196 + params: id + '/metadata',
  197 + },
  198 + { joinPrefix: false }
  199 + );
  200 +};
  201 +
  202 +/**
  203 + * 设置根规则链接
  204 + */
  205 +
  206 +export const settingRootChine = (id) => {
  207 + return defHttp.post(
  208 + {
  209 + url: `${ScreenManagerApi.RULE_CHAIN}/`,
  210 + params: id + '/root',
  211 + },
  212 + { joinPrefix: false }
  213 + );
  214 +};
... ...
... ... @@ -66,7 +66,7 @@ export interface RoleReqDTO {
66 66 name?: string;
67 67 remark?: string;
68 68 status: number;
69   - menu: Array<string>;
  69 + menu: Array<string | number>;
70 70 }
71 71
72 72 export interface ChangeAccountParams {
... ...
... ... @@ -34,6 +34,7 @@ enum Api {
34 34 GetAllRoleList = '/role/find/list',
35 35 BaseUserUrl = '/user',
36 36 BaseOrganization = '/organization',
  37 + RESET_USER_PASSWORD = '/user/reset_password/',
37 38 }
38 39
39 40 export const getAccountInfo = (userId: string) =>
... ... @@ -172,3 +173,12 @@ export const resetPassword = (params: ChangeAccountParams) =>
172 173 url: Api.BaseUserUrl + '/reset',
173 174 params: params,
174 175 });
  176 +
  177 +/**
  178 + * 清除密码
  179 + * @param params
  180 + */
  181 +export const clearUserPassword = (userId: string) =>
  182 + defHttp.post({
  183 + url: Api.RESET_USER_PASSWORD + userId,
  184 + });
... ...
  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="1694516709762" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18843" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M515.1 513.6m-429 0a429 429 0 1 0 858 0 429 429 0 1 0-858 0Z" fill="#0B6AE2" p-id="18844"></path><path d="M794.8 773.8C745.7 674.6 670.7 562.3 581 556.7c64.7-25.5 110.5-88.5 110.5-162.3 0-96.3-78.1-174.4-174.4-174.4s-174.4 78.1-174.4 174.4c0 73.7 45.8 136.8 110.5 162.3-90.6 5.7-166.1 120-215.2 220 69.6 73.3 168 119 277 119 110.5-0.1 210-46.9 279.8-121.9z" fill="#ffffff" p-id="18845"></path></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="1694516489379" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6077" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 1024Q0 1024 0 512T512 0q512 0 512 512T512 1024z" fill="#0B6AE2" p-id="6078"></path><path d="M672.814545 802.909091H350.72a81.454545 81.454545 0 0 1-81.454545-81.454546V302.545455a81.454545 81.454545 0 0 1 81.454545-81.454546h322.094545a81.454545 81.454545 0 0 1 81.454546 81.454546v418.90909a81.454545 81.454545 0 0 1-81.454546 81.454546z m-322.094545-512a11.636364 11.636364 0 0 0-11.636364 11.636364v418.90909a11.636364 11.636364 0 0 0 11.636364 11.636364h322.094545a11.636364 11.636364 0 0 0 11.636364-11.636364V302.545455a11.636364 11.636364 0 0 0-11.636364-11.636364z" fill="#FFFFFF" p-id="6079"></path><path d="M376.785455 560.407273m29.090909 0l212.247272 0q29.090909 0 29.090909 29.090909l0-0.232727q0 29.090909-29.090909 29.090909l-212.247272 0q-29.090909 0-29.090909-29.090909l0 0.232727q0-29.090909 29.090909-29.090909Z" fill="#FFFFFF" p-id="6080"></path><path d="M512 676.305455m-29.090909 0a29.090909 29.090909 0 1 0 58.181818 0 29.090909 29.090909 0 1 0-58.181818 0Z" fill="#FFFFFF" p-id="6081"></path></svg>
\ No newline at end of file
... ...
1 1 <script lang="ts" setup>
2   - import { ref, watch } from 'vue';
3   - import JSONEditor, { JSONEditorOptions } from 'jsoneditor';
4   - import 'jsoneditor/dist/jsoneditor.min.css';
5   - import { unref } from 'vue';
6   - import { onMounted } from 'vue';
7   - import { computed } from '@vue/reactivity';
8   - import { onUnmounted } from 'vue';
  2 + import { onMounted, computed, onUnmounted, unref, ref, watch } from 'vue';
  3 + import { Tooltip } from 'ant-design-vue';
  4 + import { Icon } from '/@/components/Icon';
  5 + import { useFullscreen } from '@vueuse/core';
  6 + import { isNumber, isObject, isString } from '/@/utils/is';
  7 + import AceEditor, { Ace } from 'ace-builds';
  8 + import workerJsonUrl from 'ace-builds/src-noconflict/worker-json?url';
  9 + import githubTheme from 'ace-builds/src-noconflict/theme-github?url';
  10 + import 'ace-builds/src-noconflict/mode-json';
9 11
10 12 enum EventEnum {
11 13 UPDATE_VALUE = 'update:value',
... ... @@ -16,89 +18,134 @@
16 18
17 19 const props = withDefaults(
18 20 defineProps<{
19   - value?: string;
20   - options?: JSONEditorOptions;
21   - height?: number;
  21 + value?: string | Recordable;
  22 + height?: number | string;
  23 + title?: string;
  24 + disabled?: boolean;
22 25 }>(),
23 26 {
24   - options: () =>
25   - ({
26   - mode: 'code',
27   - mainMenuBar: false,
28   - statusBar: false,
29   - } as JSONEditorOptions),
30 27 height: 150,
31 28 }
32 29 );
33 30
34 31 const emit = defineEmits<{
35   - (e: EventEnum.UPDATE_VALUE, value: any, instance?: JSONEditor): void;
36   - (e: EventEnum.CHANGE, value: any, instance?: JSONEditor): void;
37   - (e: EventEnum.BLUR, event: Event, instance?: JSONEditor): void;
38   - (e: EventEnum.FOCUS, event: Event, instance?: JSONEditor): void;
  32 + (e: EventEnum.UPDATE_VALUE, value: any, instance?: Ace.Editor): void;
  33 + (e: EventEnum.CHANGE, value: any, instance?: Ace.Editor): void;
  34 + (e: EventEnum.BLUR, event: Event, instance?: Ace.Editor): void;
  35 + (e: EventEnum.FOCUS, event: Event, instance?: Ace.Editor): void;
39 36 }>();
40 37
41 38 const jsonEditorElRef = ref<Nullable<any>>();
42 39
43   - const editoreRef = ref<JSONEditor>();
  40 + const editoreRef = ref<Ace.Editor>();
44 41
45 42 const isFocus = ref(false);
46 43
47   - const handleChange = (value: any) => {
  44 + const handleOnChange = () => {
  45 + const value = get();
48 46 emit(EventEnum.UPDATE_VALUE, value, unref(editoreRef));
49 47 emit(EventEnum.CHANGE, value, unref(editoreRef));
50 48 };
51 49
52   - const handleEmit = (event: Event, key: EventEnum) => {
53   - emit(key as EventEnum[keyof EventEnum], event, unref(editoreRef));
  50 + const handleOnBlur = (event: Event) => {
  51 + isFocus.value = false;
  52 + emit(EventEnum.BLUR, event, unref(editoreRef));
54 53 };
55 54
56   - const getOptions = computed(() => {
57   - const { options } = props;
58   - return {
59   - ...options,
60   - onChangeText: handleChange,
61   - onBlur: (event: Event) => {
62   - isFocus.value = false;
63   - handleEmit(event, EventEnum.BLUR);
64   - },
65   - onFocus: (event: Event) => {
66   - isFocus.value = true;
67   - handleEmit(event, EventEnum.FOCUS);
68   - },
69   - } as JSONEditorOptions;
70   - });
  55 + const handleOnFocus = (event: Event) => {
  56 + isFocus.value = true;
  57 + emit(EventEnum.FOCUS, event, unref(editoreRef));
  58 + };
  59 +
  60 + const getFormatValue = (value: Recordable | string = '') => {
  61 + return isObject(value) ? JSON.stringify(value, null, 2) : value;
  62 + };
71 63
72 64 const initialize = () => {
73   - editoreRef.value = new JSONEditor(unref(jsonEditorElRef), unref(getOptions));
  65 + AceEditor.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl);
  66 + AceEditor.config.setModuleUrl('ace/theme/github', githubTheme);
  67 + const editor = AceEditor.edit(unref(jsonEditorElRef)!, {
  68 + mode: 'ace/mode/json',
  69 + });
  70 + editor.setTheme('ace/theme/github');
  71 + editor.setOptions({
  72 + fontSize: 14,
  73 + });
  74 +
  75 + editor.on('change', handleOnChange);
  76 + editor.on('blur', handleOnBlur);
  77 + editor.on('focus', handleOnFocus);
  78 +
  79 + editoreRef.value = editor;
  80 + unref(editoreRef)?.setValue(getFormatValue(props.value), 1);
  81 + unref(editoreRef)?.setReadOnly(props.disabled);
74 82 };
75 83
76 84 watch(
77 85 () => props.value,
78 86 (target) => {
  87 + // const position = unref(editoreRef)?.getCursorPosition();
79 88 if (unref(isFocus)) return;
80   - unref(editoreRef)?.setText(target || '');
  89 + unref(editoreRef)?.setValue(getFormatValue(target));
  90 + unref(editoreRef)?.clearSelection();
  91 + // position && unref(editoreRef)?.moveCursorToPosition(position!);
81 92 },
82 93 {
83 94 immediate: true,
84 95 }
85 96 );
86 97
  98 + watch(
  99 + () => props.disabled,
  100 + (value) => {
  101 + unref(editoreRef)?.setReadOnly(value);
  102 + }
  103 + );
  104 +
87 105 const get = (): string => {
88   - return unref(editoreRef)?.getText() || '';
  106 + return unref(editoreRef)?.getValue() || '';
89 107 };
90 108
91 109 const set = (data: any) => {
92   - return unref(editoreRef)?.set(data);
  110 + return unref(editoreRef)?.setValue(getFormatValue(data));
93 111 };
94 112
95 113 onMounted(() => {
96 114 initialize();
97   - unref(editoreRef)?.setText(props.value || '');
  115 + unref(editoreRef)?.setValue(getFormatValue(props.value));
98 116 });
99 117
100 118 onUnmounted(() => {
  119 + unref(editoreRef)?.off('change', handleOnChange);
  120 + unref(editoreRef)?.off('blur', handleOnBlur);
  121 + unref(editoreRef)?.off('focus', handleOnFocus);
101 122 unref(editoreRef)?.destroy();
  123 + unref(editoreRef)?.container.remove();
  124 + });
  125 +
  126 + const handleFormat = () => {
  127 + const value = get();
  128 + if (isString(value) && !value) return;
  129 + unref(editoreRef)?.setValue(JSON.stringify(JSON.parse(value), null, 2));
  130 + unref(editoreRef)?.clearSelection();
  131 + };
  132 +
  133 + const handleCompress = () => {
  134 + const value = get();
  135 + if (isString(value) && !value) return;
  136 + unref(editoreRef)?.setValue(JSON.stringify(JSON.parse(value)));
  137 + unref(editoreRef)?.clearSelection();
  138 + };
  139 +
  140 + const jsonEditorContainerElRef = ref<Nullable<HTMLDivElement>>();
  141 + const { isFullscreen, isSupported, toggle } = useFullscreen(jsonEditorContainerElRef);
  142 + const handleFullScreen = () => {
  143 + toggle();
  144 + };
  145 +
  146 + const getHeight = computed(() => {
  147 + const { height } = props;
  148 + return isNumber(height) ? `${height}px` : height;
102 149 });
103 150
104 151 defineExpose({
... ... @@ -108,22 +155,41 @@
108 155 </script>
109 156
110 157 <template>
111   - <div class="p-2 bg-gray-200" :style="{ height: `${height}px` }">
112   - <div ref="jsonEditorElRef" class="jsoneditor"></div>
  158 + <div
  159 + ref="jsonEditorContainerElRef"
  160 + class="p-2 bg-gray-200 flex flex-col"
  161 + :style="{ height: getHeight }"
  162 + >
  163 + <div class="w-full h-8 flex justify-between items-center">
  164 + <div>
  165 + {{ title }}
  166 + </div>
  167 + <slot name="header"></slot>
  168 + <div class="flex h-8 gap-3 justify-end items-center text-dark-500 svg:text-2xl">
  169 + <slot name="beforeFormat"></slot>
  170 + <Tooltip title="整洁">
  171 + <Icon @click="handleFormat" class="cursor-pointer" icon="gg:format-left" />
  172 + </Tooltip>
  173 + <slot name="beforeCompress"></slot>
  174 +
  175 + <Tooltip title="迷你">
  176 + <Icon @click="handleCompress" class="cursor-pointer" icon="material-symbols:compress" />
  177 + </Tooltip>
  178 + <slot name="beforeFullScreen"></slot>
  179 +
  180 + <Tooltip title="全屏">
  181 + <Icon
  182 + v-if="isSupported"
  183 + class="cursor-pointer"
  184 + :icon="
  185 + isFullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'
  186 + "
  187 + @click="handleFullScreen"
  188 + />
  189 + </Tooltip>
  190 + <slot name="afterFullScreen"></slot>
  191 + </div>
  192 + </div>
  193 + <div ref="jsonEditorElRef" class="flex-auto"></div>
113 194 </div>
114 195 </template>
115   -
116   -<style lang="less" scoped>
117   - .jsoneditor {
118   - border: none !important;
119   -
120   - :deep(.jsoneditor) {
121   - border: none !important;
122   -
123   - .ace-jsoneditor,
124   - textarea.jsoneditor-text {
125   - min-height: auto;
126   - }
127   - }
128   - }
129   -</style>
... ...
... ... @@ -13,6 +13,7 @@ export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
13 13 export { default as ApiUpload } from './src/components/ApiUpload.vue';
14 14
15 15 export { default as StructForm } from './src/externalCompns/components/StructForm/StructForm.vue';
  16 +export { default as JavaScriptFunctionEditor } from './src/components/JavaScriptFunctionEditor.vue';
16 17
17 18 //注册自定义组件
18 19 export {
... ...
... ... @@ -16,6 +16,7 @@
16 16 :formProps="getProps"
17 17 :allDefaultValues="defaultValueRef"
18 18 :formModel="formModel"
  19 + :validateFields="validateFields"
19 20 :setFormModel="setFormModel"
20 21 >
21 22 <template #[item]="data" v-for="item in Object.keys($slots)">
... ...
... ... @@ -14,6 +14,7 @@
14 14 import { get, omit } from 'lodash-es';
15 15 import { LoadingOutlined } from '@ant-design/icons-vue';
16 16 import { useI18n } from '/@/hooks/web/useI18n';
  17 + import { useDebounceFn } from '@vueuse/shared';
17 18
18 19 const emit = defineEmits(['options-change', 'change']);
19 20 const props = withDefaults(
... ... @@ -27,6 +28,7 @@
27 28 labelField?: string;
28 29 valueField?: string;
29 30 immediate?: boolean;
  31 + searchField?: string;
30 32 queryEmptyDataAgin?: boolean;
31 33 onChangeHook?: ({ options }: OnChangeHookParams) => void;
32 34 dropdownVisibleChangeHook?: ({ options }: OnChangeHookParams) => void;
... ... @@ -35,6 +37,7 @@
35 37 resultField: '',
36 38 labelField: 'label',
37 39 valueField: 'value',
  40 + searchField: 'text',
38 41 immediate: true,
39 42 queryEmptyDataAgin: true,
40 43 }
... ... @@ -53,17 +56,27 @@
53 56 const { labelField, valueField = 'value', numberToString } = props;
54 57 return unref(options).reduce((prev, next: Recordable) => {
55 58 if (next) {
56   - const value = next[valueField];
  59 + const value = get(next, valueField);
  60 + const label = get(next, labelField);
57 61 prev.push({
58   - label: next[labelField],
59   - value: numberToString ? `${value}` : value,
60 62 ...omit(next, [labelField, valueField]),
  63 + label,
  64 + value: numberToString ? `${value}` : value,
61 65 });
62 66 }
63 67 return prev;
64 68 }, [] as OptionsItem[]);
65 69 });
66 70
  71 + const getBindProps = computed(() => {
  72 + const { searchApi } = props;
  73 + return {
  74 + ...attrs,
  75 + showSearch: true,
  76 + filterOption: !searchApi,
  77 + };
  78 + });
  79 +
67 80 watchEffect(() => {
68 81 props.immediate && fetch();
69 82 });
... ... @@ -122,8 +135,9 @@
122 135 onChangeHook({ options });
123 136 }
124 137
  138 + const debounceSearchFunction = useDebounceFn(handleSearch, 300);
125 139 async function handleSearch(params?: string) {
126   - let { searchApi, api } = props;
  140 + let { searchApi, api, searchField } = props;
127 141 if (!searchApi || !isFunction(searchApi)) {
128 142 if (!api || !isFunction(api)) return;
129 143 searchApi = api;
... ... @@ -131,7 +145,7 @@
131 145 options.value = [];
132 146 try {
133 147 loading.value = true;
134   - const res = await searchApi({ ...props.params, text: params });
  148 + const res = await searchApi({ ...props.params, [searchField]: params });
135 149 if (Array.isArray(res)) {
136 150 options.value = res;
137 151 emitChange();
... ... @@ -152,11 +166,10 @@
152 166 <template>
153 167 <Select
154 168 @dropdownVisibleChange="handleFetch"
155   - v-bind="attrs"
156   - show-search
  169 + v-bind="getBindProps"
157 170 @change="handleChange"
158 171 :options="getOptions"
159   - @search="handleSearch"
  172 + @search="debounceSearchFunction"
160 173 v-model:value="state"
161 174 >
162 175 <template #[item]="data" v-for="item in Object.keys($slots)">
... ...
... ... @@ -6,7 +6,7 @@
6 6 <script lang="ts" setup>
7 7 import { ref, watchEffect, computed, unref, watch, reactive } from 'vue';
8 8 import { Select, Spin } from 'ant-design-vue';
9   - import { isFunction } from '/@/utils/is';
  9 + import { isFunction, isNullAndUnDef } from '/@/utils/is';
10 10 import { useRuleFormItem } from '/@/hooks/component/useFormItem';
11 11 import { useAttrs } from '/@/hooks/core/useAttrs';
12 12 import { get, omit } from 'lodash-es';
... ... @@ -30,8 +30,11 @@
30 30 labelField?: string;
31 31 valueField?: string;
32 32 immediate?: boolean;
33   - pagenation?: Pagination;
  33 + searchField?: string;
  34 + pagination?: Pagination;
34 35 queryEmptyDataAgin?: boolean;
  36 + fetchSearch?: boolean;
  37 + filterOption?: (inputValue: string, options: Recordable) => boolean;
35 38 }>(),
36 39 {
37 40 resultField: '',
... ... @@ -39,14 +42,15 @@
39 42 valueField: 'value',
40 43 immediate: true,
41 44 queryEmptyDataAgin: true,
42   - pagenation: () => ({ page: 1, pageSize: 10 }),
  45 + pagination: () => ({ page: 1, pageSize: 10 }),
  46 + fetchSearch: false,
43 47 }
44 48 );
45 49
46 50 const OptionsItem = (_, { attrs }: { attrs: { vNode: any } }) => attrs.vNode;
47 51
48 52 const options = ref<OptionsItem[]>([]);
49   - const pagination = reactive(Object.assign({ total: 0, page: 1, pageSize: 10 }, props.pagenation));
  53 + const pagination = reactive<Record<'total' | 'page' | 'pageSize', number>>({} as any);
50 54 const scrollLoading = ref(false);
51 55 const lock = ref(false);
52 56 const loading = ref(false);
... ... @@ -55,6 +59,13 @@
55 59 const attrs = useAttrs();
56 60 const { t } = useI18n();
57 61
  62 + const getPagination = computed(() => {
  63 + return {
  64 + ...props.pagination,
  65 + ...unref(pagination),
  66 + };
  67 + });
  68 +
58 69 // Embedded in the form, just use the hook binding to perform form verification
59 70 const [state] = useRuleFormItem(props, 'value', 'change', emitData);
60 71
... ... @@ -86,16 +97,18 @@
86 97 { deep: true }
87 98 );
88 99
89   - async function fetch() {
90   - const api = props.api;
  100 + async function fetch(searchText?: string) {
  101 + const { api, searchField, fetchSearch } = props;
91 102 if (!api || !isFunction(api)) return;
  103 + const isFetchSearchFlag = fetchSearch && !isNullAndUnDef(searchText) && searchField;
92 104 try {
93 105 !unref(getOptions).length ? (loading.value = true) : (scrollLoading.value = true);
94 106 lock.value = true;
95 107 const { total, items } = await api({
96 108 ...props.params,
97   - page: pagination.page,
98   - pageSize: pagination.pageSize,
  109 + page: unref(getPagination).page,
  110 + pageSize: unref(getPagination).pageSize,
  111 + ...(isFetchSearchFlag ? { [searchField!]: searchText } : {}),
99 112 });
100 113
101 114 pagination.total = total;
... ... @@ -105,11 +118,13 @@
105 118 return;
106 119 }
107 120 if (props.resultField) {
108   - options.value = [...options.value, ...(get(items, props.resultField) || [])];
  121 + options.value = isFetchSearchFlag
  122 + ? get(items, props.resultField) || []
  123 + : [...options.value, ...(get(items, props.resultField) || [])];
109 124 }
110 125 emitChange();
111 126 } catch (error) {
112   - pagination.page = Math.ceil(unref(getOptions).length / pagination.pageSize);
  127 + pagination.page = Math.ceil(unref(getOptions).length / unref(getPagination).pageSize);
113 128 console.warn(error);
114 129 } finally {
115 130 isFirstLoad.value = false;
... ... @@ -134,17 +149,39 @@
134 149 emitData.value = args;
135 150 }
136 151
  152 + const sleep = async (number: number) => {
  153 + return new Promise((resolve) => {
  154 + setTimeout(() => {
  155 + resolve(number);
  156 + }, number);
  157 + });
  158 + };
  159 +
137 160 async function handlePopupScroll(event: MouseEvent) {
138 161 const { scrollHeight, scrollTop, clientHeight } = event.target as HTMLDivElement;
139 162 if (scrollTop + clientHeight >= scrollHeight) {
140   - if (unref(getOptions).length < pagination.total && !unref(lock)) {
141   - pagination.page = pagination.page + 1;
  163 + if (unref(getOptions).length < unref(getPagination).total && !unref(lock)) {
  164 + pagination.page = unref(getPagination).page + 1;
  165 + scrollLoading.value = true;
  166 + await sleep(500);
142 167 await fetch();
143 168 }
144 169 }
145 170 }
146 171
147 172 const debounceHandlePopupScroll = useDebounceFn(handlePopupScroll, 100);
  173 +
  174 + const handleFilterOption = async (inputValue: string, option: Recordable) => {
  175 + const { filterOption, fetchSearch } = props;
  176 + if (filterOption && isFunction(filterOption)) {
  177 + filterOption?.(inputValue, option);
  178 + return;
  179 + }
  180 +
  181 + if (fetchSearch) {
  182 + await fetch(inputValue);
  183 + }
  184 + };
148 185 </script>
149 186
150 187 <template>
... ... @@ -153,6 +190,8 @@
153 190 v-bind="attrs"
154 191 @change="handleChange"
155 192 :options="getOptions"
  193 + :filterOption="handleFilterOption"
  194 + :showSearch="true"
156 195 v-model:value="state"
157 196 @popup-scroll="debounceHandlePopupScroll"
158 197 >
... ...
... ... @@ -9,7 +9,7 @@
9 9 import { useMessage } from '/@/hooks/web/useMessage';
10 10 import { computed, ref, unref } from 'vue';
11 11 import { cloneDeep } from 'lodash-es';
12   - import { isFunction, isNumber, isObject } from '/@/utils/is';
  12 + import { isFunction, isNumber, isObject, isString } from '/@/utils/is';
13 13 import { useDesign } from '/@/hooks/web/useDesign';
14 14
15 15 export interface FileItem {
... ... @@ -40,21 +40,38 @@
40 40 accept?: string;
41 41 maxSize?: number;
42 42 disabled?: boolean;
43   - listType?: string;
  43 + listType?: 'text' | 'picture-card' | 'picture';
44 44 multiple?: boolean;
45 45 maxFileLimit?: number;
46   - showUploadList?: boolean | { showPreviewIcon?: boolean; showRemoveIcon?: boolean };
  46 + showUploadList?: InstanceType<typeof Upload>['$props']['showUploadList'];
47 47 transformFile?: (file: File) => string | Blob | Promise<string | Blob | File>;
48 48 api: (file: string | Blob | Promise<string | Blob | File>) => Promise<FileItem>;
  49 + overFileLimitHiddenUploadEntry?: boolean;
  50 + beforeUpload?: (file: File, fileList: File[]) => boolean;
49 51 }>(),
50 52 {
51 53 fileList: () => [],
52 54 maxSize: 5 * 1024 * 1024,
  55 + overFileLimitHiddenUploadEntry: true,
  56 + listType: 'text',
53 57 showUploadList: () => ({ showPreviewIcon: true, showRemoveIcon: true }),
54 58 }
55 59 );
56 60
57   - const handleBeforeUpload = (file: File) => {
  61 + const handleBeforeUpload = (file: File, fileList: File[]) => {
  62 + const { beforeUpload, accept } = props;
  63 +
  64 + if (beforeUpload && isFunction(beforeUpload)) return beforeUpload?.(file, fileList);
  65 +
  66 + if (accept && isString(accept)) {
  67 + const limitFileSuffix = accept.split(',');
  68 +
  69 + if (limitFileSuffix.length && !limitFileSuffix.some((suffix) => file.name.includes(suffix))) {
  70 + createMessage.warning(`允许上传的文件类型包括${accept}`);
  71 + return false;
  72 + }
  73 + }
  74 +
58 75 if (file.size > props.maxSize) {
59 76 createMessage.warning(`文件大小超过${Math.floor(props.maxSize / 1024 / 1024)}mb`);
60 77 return false;
... ... @@ -75,7 +92,7 @@
75 92
76 93 const getMaxFileLimit = computed(() => {
77 94 const { maxFileLimit } = props;
78   - return isPictureCard.value ? 1 : maxFileLimit;
  95 + return isPictureCard.value ? 1 : maxFileLimit || 1;
79 96 });
80 97
81 98 const handleUpload = async (file: File | string | Blob | Promise<string | Blob | File>) => {
... ... @@ -126,16 +143,21 @@
126 143 <template>
127 144 <Upload
128 145 class="block"
  146 + :accept="accept"
129 147 :class="prefixCls"
130 148 :file-list="props.fileList"
131 149 :list-type="props.listType"
132 150 :disabled="getDisabled"
133 151 :before-upload="handleBeforeUpload"
  152 + :show-upload-list="showUploadList"
134 153 @preview="handlePreview"
135 154 @download="handleDownload"
136 155 :remove="handleRemove"
137 156 >
138   - <Spin v-if="!fileList.length" :spinning="loading">
  157 + <Spin
  158 + v-if="!(fileList.length >= getMaxFileLimit && overFileLimitHiddenUploadEntry)"
  159 + :spinning="loading"
  160 + >
139 161 <div class="w-full h-full flex flex-col justify-center content-center">
140 162 <Tooltip title="点击上传或拖拽上传">
141 163 <InboxOutlined class="text-[3rem] !text-blue-500" />
... ...
... ... @@ -14,6 +14,7 @@
14 14 import { upperFirst, cloneDeep } from 'lodash-es';
15 15 import { useItemLabelWidth } from '../hooks/useLabelWidth';
16 16 import { useI18n } from '/@/hooks/web/useI18n';
  17 + import { NamePath, ValidateOptions } from 'ant-design-vue/lib/form/interface';
17 18
18 19 export default defineComponent({
19 20 name: 'BasicFormItem',
... ... @@ -39,6 +40,12 @@
39 40 type: Function as PropType<(key: string, value: any) => void>,
40 41 default: null,
41 42 },
  43 + validateFields: {
  44 + type: Function as PropType<
  45 + (nameList?: NamePath[], options?: ValidateOptions) => Promise<any>
  46 + >,
  47 + default: null,
  48 + },
42 49 tableAction: {
43 50 type: Object as PropType<TableActionType>,
44 51 },
... ... @@ -208,6 +215,7 @@
208 215 rules[characterInx].message ||
209 216 t('component.form.maxTip', [rules[characterInx].max] as Recordable);
210 217 }
  218 + rules.forEach((item) => !item.trigger && (item.trigger = 'change'));
211 219 return rules;
212 220 }
213 221
... ... @@ -234,6 +242,10 @@
234 242 const value = target ? (isCheck ? target.checked : target.value) : e;
235 243 props.setFormModel(field, value);
236 244 },
  245 + onBlur: (...args: any[]) => {
  246 + unref(getComponentsProps)?.onBlur?.(...args);
  247 + props.validateFields([field], { triggerName: 'blur' }).catch((_) => {});
  248 + },
237 249 };
238 250 const Comp = componentMap.get(component) as ReturnType<typeof defineComponent>;
239 251
... ...
  1 +<script lang="ts" setup>
  2 + import { Icon } from '/@/components/Icon';
  3 + import { Tooltip } from 'ant-design-vue';
  4 + import { computed, onMounted, onUnmounted, ref, shallowRef, unref, watch } from 'vue';
  5 + import AceEditor, { Ace } from 'ace-builds';
  6 + import workerJavascriptUrl from 'ace-builds/src-noconflict/worker-javascript?url';
  7 + import githubTheme from 'ace-builds/src-noconflict/theme-github?url';
  8 + import 'ace-builds/src-noconflict/mode-javascript';
  9 + import 'ace-builds/src-noconflict/ext-searchbox';
  10 + import 'ace-builds/src-noconflict/ext-language_tools';
  11 + import 'ace-builds/src-noconflict/snippets/javascript';
  12 + import { useBeautify } from '/@/hooks/business/useBeautify';
  13 + import { useFullscreen } from '@vueuse/core';
  14 + import { isNumber } from '/@/utils/is';
  15 +
  16 + const emit = defineEmits(['update:value', 'focus', 'blur']);
  17 + const props = withDefaults(
  18 + defineProps<{
  19 + functionName?: string;
  20 + paramsName?: string[];
  21 + height?: number | string;
  22 + value?: string;
  23 + disabled?: boolean;
  24 + validateStatus?: boolean;
  25 + }>(),
  26 + {
  27 + functionName: 'method',
  28 + paramsName: () => [],
  29 + height: 200,
  30 + value: '',
  31 + }
  32 + );
  33 +
  34 + const getHeight = computed(() => {
  35 + const { height } = props;
  36 + return isNumber(height) ? `${height}px` : height;
  37 + });
  38 +
  39 + const javaScriptEditorElRef = ref<Nullable<HTMLDivElement>>();
  40 + const editorInstance = shallowRef<Ace.Editor>();
  41 + const isFocus = ref(false);
  42 +
  43 + const handleFocus = () => {
  44 + isFocus.value = true;
  45 + emit('focus', unref(editorInstance));
  46 + };
  47 +
  48 + const handleBlur = () => {
  49 + isFocus.value = false;
  50 + emit('update:value', get());
  51 + emit('blur', unref(editorInstance));
  52 + };
  53 +
  54 + const handleChange = () => {
  55 + emit('update:value', get());
  56 + };
  57 +
  58 + const initEditor = () => {
  59 + AceEditor.config.setModuleUrl('ace/mode/javascript_worker', workerJavascriptUrl);
  60 + AceEditor.config.setModuleUrl('ace/theme/github', githubTheme);
  61 + const editor = AceEditor.edit(unref(javaScriptEditorElRef)!, {
  62 + mode: 'ace/mode/javascript',
  63 + });
  64 + editor.setTheme('ace/theme/github');
  65 + editor.setOptions({
  66 + fontSize: 14,
  67 + enableBasicAutocompletion: true,
  68 + enableSnippets: true,
  69 + enableLiveAutocompletion: true,
  70 + });
  71 +
  72 + editor.on('focus', handleFocus);
  73 + editor.on('blur', handleBlur);
  74 + editor.on('change', handleChange);
  75 + editorInstance.value = editor;
  76 + editor.setValue(props.value);
  77 + editor?.clearSelection();
  78 + unref(editorInstance)?.setReadOnly(props.disabled);
  79 + };
  80 +
  81 + const get = () => {
  82 + return unref(editorInstance)?.getValue();
  83 + };
  84 +
  85 + const set = (val: string, cursorPos?: number) => {
  86 + return unref(editorInstance)?.setValue(val, cursorPos);
  87 + };
  88 +
  89 + onMounted(() => {
  90 + initEditor();
  91 + });
  92 +
  93 + onUnmounted(() => {
  94 + unref(editorInstance)?.off('change', handleChange);
  95 + unref(editorInstance)?.off('focus', handleFocus);
  96 + unref(editorInstance)?.off('blur', handleBlur);
  97 + unref(editorInstance)?.destroy();
  98 + unref(editorInstance)?.container.remove();
  99 + });
  100 +
  101 + const { beautifyJs } = useBeautify();
  102 + const handleFormatCode = async () => {
  103 + const res = await beautifyJs(get() || '', { indent_size: 4, wrap_line_length: 60 });
  104 + set(res || '', -1);
  105 + };
  106 +
  107 + const jsFunctionContainerElRef = ref<Nullable<HTMLDivElement>>();
  108 + const { isFullscreen, isSupported, toggle } = useFullscreen(jsFunctionContainerElRef);
  109 + const handleFullScreen = () => {
  110 + toggle();
  111 + };
  112 +
  113 + watch(
  114 + () => props.value,
  115 + (value) => {
  116 + // const position = unref(editorInstance)?.getCursorPosition();
  117 + if (unref(isFocus)) return;
  118 + set(value);
  119 + unref(editorInstance)?.clearSelection();
  120 + // position && unref(editorInstance)?.moveCursorToPosition(position!);
  121 + },
  122 + {
  123 + immediate: true,
  124 + }
  125 + );
  126 +
  127 + watch(
  128 + () => props.disabled,
  129 + (value) => {
  130 + unref(editorInstance)?.setReadOnly(value);
  131 + }
  132 + );
  133 +
  134 + defineExpose({
  135 + get,
  136 + set,
  137 + });
  138 +</script>
  139 +
  140 +<template>
  141 + <section
  142 + ref="jsFunctionContainerElRef"
  143 + class="p-2 bg-gray-200 flex flex-col"
  144 + :style="{ height: getHeight }"
  145 + >
  146 + <head class="flex justify-between h-8 items-center">
  147 + <div class="font-bold">
  148 + <span>function</span>
  149 + <span class="ml-1">{{ functionName }}</span>
  150 + <span>({{ paramsName.join(',') }})</span>
  151 + <span class="ml-1">{</span>
  152 + </div>
  153 + <div class="flex gap-3 items-center svg:text-2xl">
  154 + <slot name="beforeFormat"></slot>
  155 +
  156 + <Tooltip title="整洁">
  157 + <Icon class="cursor-pointer" icon="gg:format-left" @click="handleFormatCode" />
  158 + </Tooltip>
  159 + <slot name="beforeFullScreen"></slot>
  160 + <Tooltip title="全屏">
  161 + <Icon
  162 + v-if="isSupported"
  163 + class="cursor-pointer"
  164 + :icon="
  165 + isFullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'
  166 + "
  167 + @click="handleFullScreen"
  168 + />
  169 + </Tooltip>
  170 + <slot name="afterFullScreen"></slot>
  171 + </div>
  172 + </head>
  173 + <main ref="javaScriptEditorElRef" class="flex-auto"> </main>
  174 + <footer class="font-bold">}</footer>
  175 + </section>
  176 +</template>
... ...
1 1 import type { FormProps, FormActionType, UseFormReturnType, FormSchema } from '../types/form';
2   -import type { NamePath } from 'ant-design-vue/lib/form/interface';
  2 +import type { NamePath, ValidateOptions } from 'ant-design-vue/lib/form/interface';
3 3 import type { DynamicProps } from '/#/utils';
4 4 import { ref, onUnmounted, unref, nextTick, watch } from 'vue';
5 5 import { isProdMode } from '/@/utils/env';
... ... @@ -112,9 +112,12 @@ export function useForm(props?: Props): UseFormReturnType {
112 112 return form.validate(nameList);
113 113 },
114 114
115   - validateFields: async (nameList?: NamePath[]): Promise<Recordable> => {
  115 + validateFields: async (
  116 + nameList?: NamePath[],
  117 + options?: ValidateOptions
  118 + ): Promise<Recordable> => {
116 119 const form = await getForm();
117   - return form.validateFields(nameList);
  120 + return form.validateFields(nameList, options);
118 121 },
119 122 };
120 123
... ...
1 1 import type { ComputedRef, Ref } from 'vue';
2 2 import type { FormProps, FormSchema, FormActionType } from '../types/form';
3   -import type { NamePath } from 'ant-design-vue/lib/form/interface';
  3 +import type { NamePath, ValidateOptions } from 'ant-design-vue/lib/form/interface';
4 4 import { unref, toRaw } from 'vue';
5 5 import { isArray, isFunction, isObject, isString } from '/@/utils/is';
6 6 import { deepMerge } from '/@/utils';
... ... @@ -206,8 +206,8 @@ export function useFormEvents({
206 206 });
207 207 }
208 208
209   - async function validateFields(nameList?: NamePath[] | undefined) {
210   - return unref(formElRef)?.validateFields(nameList);
  209 + async function validateFields(nameList?: NamePath[] | undefined, options?: ValidateOptions) {
  210 + return unref(formElRef)?.validateFields(nameList, options);
211 211 }
212 212
213 213 async function validate(nameList?: NamePath[] | undefined) {
... ...
... ... @@ -38,7 +38,7 @@ export const basicProps = {
38 38 },
39 39 autoSetPlaceHolder: propTypes.bool.def(true),
40 40 // 在INPUT组件上单击回车时,是否自动提交
41   - autoSubmitOnEnter: propTypes.bool.def(false),
  41 + autoSubmitOnEnter: propTypes.bool.def(true),
42 42 submitOnReset: propTypes.bool,
43 43 size: propTypes.oneOf(['default', 'small', 'large']).def('default'),
44 44 // 禁用表单
... ...
1   -import type { NamePath, RuleObject } from 'ant-design-vue/lib/form/interface';
  1 +import type { NamePath, RuleObject, ValidateOptions } from 'ant-design-vue/lib/form/interface';
2 2 import type { VNode } from 'vue';
3 3 import type { ButtonProps as AntdButtonProps } from '/@/components/Button';
4 4 import type { FormItem } from './formItem';
... ... @@ -39,7 +39,7 @@ export interface FormActionType {
39 39 prefixField: string | undefined,
40 40 first?: boolean | undefined
41 41 ) => Promise<void>;
42   - validateFields: (nameList?: NamePath[]) => Promise<any>;
  42 + validateFields: (nameList?: NamePath[], options?: ValidateOptions) => Promise<any>;
43 43 validate: (nameList?: NamePath[]) => Promise<any>;
44 44 scrollToField: (name: NamePath, options?: ScrollOptions) => Promise<void>;
45 45 }
... ...
... ... @@ -130,4 +130,11 @@ export type ComponentType =
130 130 | 'ControlGroup'
131 131 | 'JSONEditor'
132 132 | 'OrgTreeSelect'
133   - | 'ExtendDesc';
  133 + | 'ExtendDesc'
  134 + | 'JavaScriptFunctionEditor'
  135 + | 'JavascriptEditorWithTestModal'
  136 + | 'AttributeConfiguration'
  137 + | 'CorrelationFilters'
  138 + | 'RelationsQuery'
  139 + | 'CredentialsCard'
  140 + | 'ApiComplete';
... ...
... ... @@ -33,6 +33,7 @@
33 33 import { openWindow } from '/@/utils';
34 34
35 35 import { useOpenKeys } from './useOpenKeys';
  36 + import { useMenuActiveFix } from '/@/hooks/business/useMenuActiveFix';
36 37 export default defineComponent({
37 38 name: 'SimpleMenu',
38 39 components: {
... ... @@ -120,7 +121,9 @@
120 121 isClickGo.value = false;
121 122 return;
122 123 }
123   - const path = (route || unref(currentRoute)).path;
  124 +
  125 + let { flag, path } = useMenuActiveFix(route || unref(currentRoute));
  126 + path = flag ? path : (route || unref(currentRoute)).path;
124 127
125 128 menuState.activeName = path;
126 129
... ...
... ... @@ -29,7 +29,6 @@ export function useOpenKeys(
29 29 return;
30 30 }
31 31 const keys = getAllParentPath(menuList, path);
32   -
33 32 if (!unref(accordion)) {
34 33 menuState.openNames = uniq([...menuState.openNames, ...keys]);
35 34 } else {
... ...
... ... @@ -48,6 +48,7 @@
48 48 'update:value',
49 49 'change',
50 50 'check',
  51 + 'unSelectAll',
51 52 'update:searchValue',
52 53 ],
53 54 setup(props, { attrs, slots, emit, expose }) {
... ... @@ -188,6 +189,7 @@
188 189 }
189 190
190 191 function checkAll(checkAll: boolean) {
  192 + if (!checkAll) emit('unSelectAll');
191 193 state.checkedKeys = checkAll ? getEnabledKeys() : ([] as Keys);
192 194 }
193 195
... ...
... ... @@ -2,17 +2,23 @@
2 2 import { isNumber } from 'lodash';
3 3 import videoJs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js';
4 4 import 'video.js/dist/video-js.css';
5   - import { computed, CSSProperties, onMounted, onUnmounted, ref, unref } from 'vue';
  5 + import { computed, CSSProperties, onMounted, onUnmounted, ref, toRaw, unref } from 'vue';
6 6 import { useDesign } from '/@/hooks/web/useDesign';
7 7 import { getJwtToken, getShareJwtToken } from '/@/utils/auth';
8 8 import { isShareMode } from '/@/views/sys/share/hook';
9 9 import 'videojs-flvjs-es6';
10 10 const { prefixCls } = useDesign('basic-video-play');
11 11
12   - const props = defineProps<{
13   - options?: VideoJsPlayerOptions;
14   - withToken?: boolean;
15   - }>();
  12 + const props = withDefaults(
  13 + defineProps<{
  14 + options?: VideoJsPlayerOptions;
  15 + withToken?: boolean;
  16 + immediateInitOnMounted?: boolean;
  17 + }>(),
  18 + {
  19 + immediateInitOnMounted: true,
  20 + }
  21 + );
16 22
17 23 const emit = defineEmits<{
18 24 (event: 'ready', instance?: Nullable<VideoJsPlayer>): void;
... ... @@ -68,8 +74,18 @@
68 74 });
69 75 };
70 76
  77 + const customInit = (getOptionsFn: (optios: VideoJsPlayerOptions) => VideoJsPlayerOptions) => {
  78 + return (videoPlayInstance.value = videoJs(
  79 + unref(videoPlayEl)!,
  80 + getOptionsFn(toRaw(unref(getOptions))),
  81 + () => {
  82 + emit('ready', unref(videoPlayInstance));
  83 + }
  84 + ));
  85 + };
  86 +
71 87 onMounted(() => {
72   - init();
  88 + props.immediateInitOnMounted && init();
73 89 });
74 90
75 91 onUnmounted(() => {
... ... @@ -79,6 +95,7 @@
79 95 });
80 96
81 97 defineExpose({
  98 + customInit,
82 99 reloadPlayer: init,
83 100 getInstance: () => unref(videoPlayInstance),
84 101 });
... ...
  1 +export enum AlarmStatus {
  2 + CLEARED_UN_ACK = 'CLEARED_UNACK',
  3 + ACTIVE_UN_ACK = 'ACTIVE_UNACK',
  4 + CLEARED_ACK = 'CLEARED_ACK',
  5 + ACTIVE_ACK = 'ACTIVE_ACK',
  6 +}
  7 +
  8 +export enum AlarmStatusMean {
  9 + CLEARED_UNACK = '清除未确认',
  10 + ACTIVE_UNACK = '激活未确认',
  11 + CLEARED_ACK = '清除已确认',
  12 + ACTIVE_ACK = '激活已确认',
  13 +}
... ...
... ... @@ -38,6 +38,15 @@ export const PLATFORM = 'PLATFORM';
38 38 export const PLATFORM_INFO_CACHE_KEY = 'PLATFORM_INFO';
39 39
40 40 export const MENU_LIST = 'MENU_LIST';
  41 +
  42 +export const RULE_NODE_LOCAL_CACHE_KEY = 'RULE__NODE__KEY__';
  43 +
  44 +export const RULE_CHAIN_IMPORT_LOCAL_CACHE_KEY = 'RULE__CHAIN__IMPORT__KEY__';
  45 +
  46 +export const RULE_NODE_KEY = 'RULE_NODE';
  47 +
  48 +export const RULE_CHAIN_KEY = 'RULE_CHAIN';
  49 +
41 50 export enum CacheTypeEnum {
42 51 SESSION,
43 52 LOCAL,
... ...
... ... @@ -19,4 +19,8 @@ export enum DictEnum {
19 19 DISABLE_CUSTOMER_AUTH = 'disabled_customer_auth',
20 20 // 寄存器数据格式
21 21 REGISTER_DATA_FORMAT = 'register_data_format',
  22 + // 消息类型 规则节点 Filter message type switch
  23 + MESSAGE_TYPES_FILTER = 'message_types_filter',
  24 + // 实体类型 规则节点 Filter originator types switch
  25 + ORIGINATOR_TYPES = 'originator_types',
22 26 }
... ...
... ... @@ -16,4 +16,8 @@ export const PageEnum = {
16 16 DEVICE_LIST: '/device/list',
17 17
18 18 SHARE_PAGE: '/share/:viewType/:id/:publicId',
  19 +
  20 + RULE_CHAIN_DETAIL: '/rule/chain/:id',
  21 +
  22 + RULE_CHAIN_DETAIL_IMPORT: '/rule/chain/import',
19 23 };
... ...
  1 +import { saveOrEditDictItem } from '/@/api/system/dict';
  2 +import { SysDictItem } from '/@/api/system/model/dictModel';
  3 +
  4 +interface BatchSettingParamsType {
  5 + dictId: string;
  6 + list: any[];
  7 + transformText: (item: any) => any;
  8 + transformValue: (item: any) => any;
  9 + update?: boolean;
  10 +}
  11 +
  12 +export function useBatchSettingDict() {
  13 + const batchSetting = async (options: BatchSettingParamsType) => {
  14 + const { dictId, list, transformText, transformValue, update = false } = options;
  15 + if (!transformText || !transformValue) return;
  16 + let index = 1;
  17 + for (const item of list) {
  18 + const params: Partial<SysDictItem> & { dictId: string } = {
  19 + dictId,
  20 + sort: index++,
  21 + status: 1,
  22 + itemText: transformText(item),
  23 + itemValue: transformValue(item),
  24 + };
  25 +
  26 + await saveOrEditDictItem(params as SysDictItem, update);
  27 + }
  28 + };
  29 +
  30 + const batchDelete = () => {};
  31 +
  32 + return {
  33 + batchSetting,
  34 + batchDelete,
  35 + };
  36 +}
... ...
  1 +export function useBeautify() {
  2 + let jsBeautifyModule: any;
  3 +
  4 + async function loadJsBeautify() {
  5 + if (jsBeautifyModule) return jsBeautifyModule;
  6 + jsBeautifyModule = await import('js-beautify/js/lib/beautifier');
  7 + return jsBeautifyModule;
  8 + }
  9 +
  10 + async function beautifyJs(source: string, options?: any) {
  11 + const module = await loadJsBeautify();
  12 + return module.js(source, options);
  13 + }
  14 + return { beautifyJs };
  15 +}
... ...
  1 +import { isObject, isString } from '/@/utils/is';
  2 +
  3 +export function useJsonParse(value: any, defaultValue = {}) {
  4 + let flag = false;
  5 + try {
  6 + value = JSON.parse(value);
  7 + if (isObject(value) || (isString(value) && value.trim() === '')) flag = true;
  8 + } catch (error) {
  9 + value = defaultValue;
  10 + }
  11 + return { flag, value };
  12 +}
... ...
  1 +import { RouteLocationNormalizedLoaded } from 'vue-router';
  2 +
  3 +const menuMap = new Map();
  4 +
  5 +menuMap.set('/visual/board/detail/:boardId/:boardName/:organizationId?', '/visual/board');
  6 +menuMap.set('/rule/chain/:id', '/rule/chain');
  7 +
  8 +export const useMenuActiveFix = (route: RouteLocationNormalizedLoaded) => {
  9 + let flag = false;
  10 + let path;
  11 +
  12 + const matchPath = route.matched.map((item) => item.path);
  13 + const needFixMenus: string[] = Array.from(menuMap.keys());
  14 +
  15 + for (const item of matchPath) {
  16 + for (const menu of needFixMenus) {
  17 + if (menu === item) {
  18 + flag = true;
  19 + path = menuMap.get(menu);
  20 + break;
  21 + }
  22 + }
  23 + }
  24 +
  25 + return {
  26 + flag,
  27 + path,
  28 + };
  29 +};
... ...
... ... @@ -52,6 +52,7 @@ export function useBatchDelete(
52 52 if (record) {
53 53 await deleteFn([record.id]);
54 54 createMessage.success('删除成功');
  55 + selectedRowIds.value = [];
55 56 } else {
56 57 await deleteFn(selectedRowIds.value);
57 58 createMessage.success('批量删除成功');
... ...
... ... @@ -19,6 +19,7 @@
19 19 import { useRootSetting } from '/@/hooks/setting/useRootSetting';
20 20 import { useAppInject } from '/@/hooks/web/useAppInject';
21 21 import { useDesign } from '/@/hooks/web/useDesign';
  22 + import { OUTSIDE_LINK_PREFIX } from '/@/router/helper/routeHelper';
22 23
23 24 export default defineComponent({
24 25 name: 'LayoutMenu',
... ... @@ -113,6 +114,11 @@
113 114 */
114 115
115 116 function handleMenuClick(path: string) {
  117 + if (path.startsWith(OUTSIDE_LINK_PREFIX)) {
  118 + path = path.replace(OUTSIDE_LINK_PREFIX, '');
  119 + window.open(path);
  120 + return;
  121 + }
116 122 go(path);
117 123 }
118 124
... ... @@ -183,7 +189,6 @@
183 189 padding: 10px 4px 10px 10px;
184 190
185 191 img {
186   - width: @logo-width;
187 192 height: @logo-width;
188 193 }
189 194 }
... ...
... ... @@ -6,6 +6,7 @@ import { cloneDeep, omit } from 'lodash-es';
6 6 import { warn } from '/@/utils/log';
7 7 import { createRouter, createWebHashHistory } from 'vue-router';
8 8
  9 +export const OUTSIDE_LINK_PREFIX = '/outside/link';
9 10 export type LayoutMapKey = 'LAYOUT';
10 11 const IFRAME = () => import('/@/views/sys/iframe/FrameBlank.vue');
11 12
... ... @@ -81,6 +82,9 @@ export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModul
81 82 route.meta = meta;
82 83 }
83 84 }
  85 + if (!route.path.startsWith('/') && route.meta.isLink) {
  86 + route.path = `${OUTSIDE_LINK_PREFIX}${route.path}`;
  87 + }
84 88 route.children && asyncImportRoute(route.children);
85 89 });
86 90 return routeList as unknown as T[];
... ...
... ... @@ -16,6 +16,7 @@
16 16 mode="multiple"
17 17 v-model:value="model[field]"
18 18 :options="alarmContactOptions.map((item) => ({ value: item.value, label: item.label }))"
  19 + @change="handleChange"
19 20 >
20 21 <template #dropdownRender="{ menuNode: menu }">
21 22 <v-nodes :vnodes="menu" />
... ... @@ -53,8 +54,14 @@
53 54 return attrs.vnodes;
54 55 },
55 56 },
  57 + props: {
  58 + defaultEnable: {
  59 + type: Boolean,
  60 + default: false,
  61 + },
  62 + },
56 63 emits: ['success', 'register'],
57   - setup(_, { emit }) {
  64 + setup(props, { emit }) {
58 65 const alarmContactOptions: any = ref([]);
59 66 const orgId = ref('');
60 67 const orgFunc = (e) => {
... ... @@ -82,6 +89,7 @@
82 89 const [registerAlarmContactDrawer, { openDrawer }] = useDrawer();
83 90 async function handleSuccess() {
84 91 //获取告警联系人
  92 + if (!unref(orgId)) return;
85 93 const res = await byOrgIdGetAlarmContact(orgId.value);
86 94 if (res) {
87 95 alarmContactOptions.value = res.map((m) => {
... ... @@ -91,6 +99,10 @@
91 99 alarmContactOptions.value = [];
92 100 }
93 101 }
  102 +
  103 + const handleChange = () => {
  104 + validateFields(['alarmContactId']);
  105 + };
94 106 // 新增或编辑
95 107 const handleOpenAlarmContact = () => {
96 108 openDrawer(true, {
... ... @@ -100,7 +112,10 @@
100 112 const isUpdate = ref(true);
101 113 let allData: any = reactive({});
102 114 const editId = ref('');
103   - const [registerForm, { validate, setFieldsValue, resetFields, updateSchema }] = useForm({
  115 + const [
  116 + registerForm,
  117 + { validate, setFieldsValue, resetFields, updateSchema, validateFields },
  118 + ] = useForm({
104 119 labelWidth: 120,
105 120 schemas: formSchema,
106 121 showActionButtonGroup: false,
... ... @@ -165,6 +180,9 @@
165 180 ...alarmContactIdD,
166 181 ...messageModeD,
167 182 };
  183 +
  184 + props.defaultEnable && Object.assign(allData, { status: 1 });
  185 +
168 186 if (!unref(isUpdate)) {
169 187 delete allData.id;
170 188 }
... ... @@ -191,6 +209,7 @@
191 209 handleOpenAlarmContact,
192 210 registerAlarmContactDrawer,
193 211 handleSuccess,
  212 + handleChange,
194 213 };
195 214 },
196 215 });
... ...
... ... @@ -37,13 +37,21 @@
37 37 </a-button>
38 38 </template>
39 39 <template #status="{ record }">
40   - <Switch
41   - :checked="record.status === 1"
42   - :loading="record.pendingStatus"
43   - checkedChildren="启用"
44   - unCheckedChildren="禁用"
45   - @change="(checked:boolean)=>statusChange(checked,record)"
46   - />
  40 + <Authority value="api:yt:alarm:profile:update">
  41 + <Switch
  42 + :checked="record.status === 1"
  43 + :loading="record.pendingStatus"
  44 + checkedChildren="启用"
  45 + unCheckedChildren="禁用"
  46 + @change="(checked:boolean)=>statusChange(checked,record)"
  47 + />
  48 + </Authority>
  49 + <Tag
  50 + v-if="!hasPermission('api:yt:alarm:profile:update')"
  51 + :color="record.status ? 'green' : 'red'"
  52 + >
  53 + {{ record.status ? '启用' : '禁用' }}
  54 + </Tag>
47 55 </template>
48 56 <template #action="{ record }">
49 57 <TableAction
... ... @@ -88,7 +96,7 @@
88 96 import { useResetOrganizationTree, OrganizationIdTree } from '/@/views/common/organizationIdTree';
89 97 import { deleteAlarmConfig, queryAlarmConfig } from '/@/api/alarm/config/alarmConfig';
90 98 import { searchFormSchema, columns } from './config.data';
91   - import { Modal, Popconfirm } from 'ant-design-vue';
  99 + import { Modal, Popconfirm, Tag } from 'ant-design-vue';
92 100 import { JsonPreview } from '/@/components/CodeEditor';
93 101 import { findDictItemByCode } from '/@/api/system/dict';
94 102 import { alarmContactGetPage } from '/@/api/device/deviceConfigApi';
... ... @@ -97,6 +105,7 @@
97 105 import { putAlarmConfigStatus } from '/@/api/alarm/config/alarmConfig';
98 106 import { useMessage } from '/@/hooks/web/useMessage';
99 107 import { Authority } from '/@/components/Authority';
  108 + import { usePermission } from '/@/hooks/web/usePermission';
100 109
101 110 export default defineComponent({
102 111 components: {
... ... @@ -108,8 +117,10 @@
108 117 Switch,
109 118 Authority,
110 119 Popconfirm,
  120 + Tag,
111 121 },
112 122 setup() {
  123 + const { hasPermission } = usePermission();
113 124 const searchInfo = reactive<Recordable>({});
114 125 const { organizationIdTreeRef, resetFn } = useResetOrganizationTree(searchInfo);
115 126 // 刷新
... ... @@ -250,6 +261,7 @@
250 261 showAlarmContact,
251 262 showMessageMode,
252 263 statusChange,
  264 + hasPermission,
253 265 };
254 266 },
255 267 });
... ...
... ... @@ -3,20 +3,7 @@ import { FormSchema } from '/@/components/Form';
3 3 import { BasicColumn } from '/@/components/Table';
4 4 import moment from 'moment';
5 5 import { findDictItemByCode } from '/@/api/system/dict';
6   -
7   -export enum AlarmStatus {
8   - CLEARED_UN_ACK = 'CLEARED_UNACK',
9   - ACTIVE_UN_ACK = 'ACTIVE_UNACK',
10   - CLEARED_ACK = 'CLEARED_ACK',
11   - ACTIVE_ACK = 'ACTIVE_ACK',
12   -}
13   -
14   -export enum AlarmStatusMean {
15   - CLEARED_UNACK = '清除未确认',
16   - ACTIVE_UNACK = '激活未确认',
17   - CLEARED_ACK = '清除已确认',
18   - ACTIVE_ACK = '激活已确认',
19   -}
  6 +import { AlarmStatus, AlarmStatusMean } from '/@/enums/alarmEnum';
20 7
21 8 export const alarmSearchSchemas: FormSchema[] = [
22 9 {
... ... @@ -126,6 +113,10 @@ export const alarmColumns: BasicColumn[] = [
126 113 title: '告警设备',
127 114 dataIndex: 'deviceName',
128 115 width: 100,
  116 + customRender: ({ record }) => {
  117 + const { deviceAlias, deviceName } = record || {};
  118 + return deviceAlias || deviceName;
  119 + },
129 120 },
130 121 {
131 122 title: '告警场景',
... ... @@ -139,6 +130,12 @@ export const alarmColumns: BasicColumn[] = [
139 130 format: (text) => alarmLevel(text),
140 131 },
141 132 {
  133 + title: '告警详情',
  134 + dataIndex: 'details',
  135 + slots: { customRender: 'details' },
  136 + width: 160,
  137 + },
  138 + {
142 139 title: '状态',
143 140 dataIndex: 'status',
144 141 format: (text) => statusType(text),
... ...
... ... @@ -46,14 +46,16 @@
46 46 const alarmStatus = ref('');
47 47 const [registerDrawer, { closeDrawer }] = useDrawerInner(async (data) => {
48 48 await resetFields();
  49 + const { deviceAlias, deviceName, severity, status, details, id } = data || {};
49 50 await setFieldsValue({
50 51 ...data,
51   - severity: alarmLevel(data.severity),
52   - status: statusType(data.status),
53   - details: JSON.stringify(data.details),
  52 + deviceName: deviceAlias || deviceName,
  53 + severity: alarmLevel(severity),
  54 + status: statusType(status),
  55 + details: JSON.stringify(details),
54 56 });
55   - alarmStatus.value = data.status;
56   - alarmId.value = data.id;
  57 + alarmStatus.value = status;
  58 + alarmId.value = id;
57 59 });
58 60 // 处理报警
59 61 const handleAlarm = async () => {
... ...
1   -import { AlarmStatus, AlarmStatusMean } from '../config/detail.config';
2 1 import { clearOrAckAlarm, getDeviceAlarm } from '/@/api/device/deviceManager';
3 2 import { notification, Button, Tag } from 'ant-design-vue';
4 3 import { h, onMounted, onUnmounted } from 'vue';
... ... @@ -8,6 +7,7 @@ import { RoleEnum } from '/@/enums/roleEnum';
8 7 import { usePermission } from '/@/hooks/web/usePermission';
9 8 import { useUserStore } from '/@/store/modules/user';
10 9 import { useGlobSetting } from '/@/hooks/setting';
  10 +import { AlarmStatus, AlarmStatusMean } from '/@/enums/alarmEnum';
11 11
12 12 interface UseAlarmNotifyParams {
13 13 alarmNotifyStatus?: AlarmStatus;
... ...
... ... @@ -13,47 +13,105 @@
13 13 ]"
14 14 />
15 15 </template>
  16 + <template #details="{ record }">
  17 + <a-button type="link" class="ml-2" @click="handleViewAlarmDetails(record)">
  18 + 查看告警详情
  19 + </a-button>
  20 + </template>
  21 + <template #toolbar>
  22 + <Tooltip>
  23 + <template #title>
  24 + <div>激活未确认: 可以处理,清除</div>
  25 + <div>激活已确认: 只可清除,已经处理</div>
  26 + <div>清除未确认: 只可处理,已经清除</div>
  27 + <div>清除已确认: 不需要做处理和清除</div>
  28 + </template>
  29 + <Button @click="handleBatchClear" type="primary" :disabled="getCanBatchClear">
  30 + <QuestionCircleOutlined class="cursor-pointer" />
  31 + <span>批量清除</span>
  32 + </Button>
  33 + </Tooltip>
  34 + <Tooltip>
  35 + <template #title>
  36 + <div>激活未确认: 可以处理,清除</div>
  37 + <div>激活已确认: 只可清除,已经处理</div>
  38 + <div>清除未确认: 只可处理,已经清除</div>
  39 + <div>清除已确认: 不需要做处理和清除</div>
  40 + </template>
  41 + <Button @click="handleBatchAck" type="primary" :disabled="getCanBatchAck">
  42 + <QuestionCircleOutlined class="cursor-pointer" />
  43 + <span>批量处理</span>
  44 + </Button>
  45 + </Tooltip>
  46 + </template>
16 47 </BasicTable>
17 48 <AlarmDetailDrawer @register="registerDetailDrawer" @success="handleSuccess" />
18 49 </div>
19 50 </template>
20 51 <script lang="ts">
21   - import { defineComponent } from 'vue';
  52 + import { defineComponent, nextTick, h, computed } from 'vue';
22 53 import { BasicTable, useTable, TableAction } from '/@/components/Table';
23 54 import { alarmColumns, alarmSearchSchemas } from './config/detail.config';
24   - import { getDeviceAlarm } from '/@/api/device/deviceManager';
  55 + import { doBatchAckAlarm, doBatchClearAlarm, getDeviceAlarm } from '/@/api/device/deviceManager';
25 56 import { useDrawer } from '/@/components/Drawer';
26 57 import AlarmDetailDrawer from './cpns/AlarmDetailDrawer.vue';
  58 + import { Modal, Button, Tooltip } from 'ant-design-vue';
  59 + import { JsonPreview } from '/@/components/CodeEditor';
  60 + import { getDeviceDetail } from '/@/api/device/deviceManager';
  61 + import { getAttribute } from '/@/api/ruleengine/ruleengineApi';
  62 + import {
  63 + operationNumber_OR_TIME,
  64 + operationString,
  65 + operationBoolean,
  66 + } from '/@/views/rule/linkedge/config/formatData';
  67 + import { AlarmLogItem } from '/@/api/device/model/deviceConfigModel';
  68 + import { AlarmStatus } from '/@/enums/alarmEnum';
  69 + import { useMessage } from '/@/hooks/web/useMessage';
  70 + import { QuestionCircleOutlined } from '@ant-design/icons-vue';
27 71
28 72 export default defineComponent({
29 73 name: 'AlarmCenter',
30 74 components: {
  75 + Button,
31 76 BasicTable,
32 77 TableAction,
33 78 AlarmDetailDrawer,
  79 + QuestionCircleOutlined,
  80 + Tooltip,
34 81 },
35 82
36 83 setup() {
37   - const [registerTable, { reload }] = useTable({
38   - title: '告警记录列表',
39   - api: getDeviceAlarm,
40   - columns: alarmColumns,
41   - useSearchForm: true,
42   - formConfig: {
43   - labelWidth: 120,
44   - schemas: alarmSearchSchemas,
45   - fieldMapToTime: [['alarmTime', ['startTime', 'endTime'], 'x']],
46   - },
47   - bordered: true,
48   - showIndexColumn: false,
49   - showTableSetting: true,
50   - actionColumn: {
51   - width: 200,
52   - title: '操作',
53   - slots: { customRender: 'action' },
54   - fixed: 'right',
55   - },
56   - });
  84 + const [registerTable, { reload, getSelectRows, clearSelectedRowKeys, getRowSelection }] =
  85 + useTable({
  86 + title: '告警记录列表',
  87 + api: getDeviceAlarm,
  88 + columns: alarmColumns,
  89 + rowKey: 'id',
  90 + useSearchForm: true,
  91 + formConfig: {
  92 + labelWidth: 120,
  93 + schemas: alarmSearchSchemas,
  94 + fieldMapToTime: [['alarmTime', ['startTime', 'endTime'], 'x']],
  95 + },
  96 + bordered: true,
  97 + showIndexColumn: false,
  98 + showTableSetting: true,
  99 + clickToRowSelect: false,
  100 + rowSelection: {
  101 + type: 'checkbox',
  102 + getCheckboxProps: (record: AlarmLogItem) => {
  103 + return {
  104 + disabled: record.status === AlarmStatus.CLEARED_ACK,
  105 + };
  106 + },
  107 + },
  108 + actionColumn: {
  109 + width: 200,
  110 + title: '操作',
  111 + slots: { customRender: 'action' },
  112 + fixed: 'right',
  113 + },
  114 + });
57 115 const [registerDetailDrawer, { openDrawer }] = useDrawer();
58 116 const handleDetail = (record: Recordable) => {
59 117 openDrawer(true, record);
... ... @@ -61,11 +119,137 @@
61 119 const handleSuccess = () => {
62 120 reload();
63 121 };
  122 + const handleViewAlarmDetails = async (record: Recordable) => {
  123 + await nextTick();
  124 + const { details } = record;
  125 + const deviceIdKeys = Object.keys(details);
  126 + const detailObject = deviceIdKeys.map((key) => ({ label: key, value: details[key] }));
  127 + const dataFormat = await handleAlarmDetailFormat(deviceIdKeys);
  128 + const dataFormats = detailObject.reduce((acc: any, curr: any) => {
  129 + dataFormat.forEach((item) => {
  130 + if (item.tbDeviceId === curr.label) {
  131 + const findName = item.attribute.find(
  132 + (item) => item.identifier === curr.value.key
  133 + )?.name;
  134 + const findLogin = [
  135 + ...operationNumber_OR_TIME,
  136 + ...operationString,
  137 + ...operationBoolean,
  138 + ].find((item) => item.value === curr.value.logic)?.symbol;
  139 + const findAttribute = item.attribute.find(
  140 + (findItem) => findItem.identifier === curr.value.key
  141 + );
  142 + const value = {
  143 + ['触发属性']: findName,
  144 + ['触发条件']: `${findLogin}${curr.value.logicValue}`,
  145 + ['触发值']: `${curr.value.realValue}${
  146 + findAttribute.detail?.dataType?.specs?.unit?.key ?? ''
  147 + }`,
  148 + };
  149 + const data = {
  150 + [item.name]: value,
  151 + };
  152 + acc.push(data);
  153 + }
  154 + });
  155 + return [...acc];
  156 + }, []);
  157 + const objectFormat = dataFormats.reduce((acc: any, curr: any) => {
  158 + return {
  159 + ...acc,
  160 + ...curr,
  161 + };
  162 + }, {});
  163 + Modal.info({
  164 + title: '告警详情',
  165 + width: 600,
  166 + centered: true,
  167 + maskClosable: true,
  168 + content: h(JsonPreview, { data: JSON.parse(JSON.stringify(objectFormat)) }),
  169 + });
  170 + };
  171 + const handleAlarmDetailFormat = async (keys: string[]) => {
  172 + const temp: any = [];
  173 + for (let item of keys) {
  174 + if (item === 'key' || item === 'data') return []; //旧数据则终止
  175 + const deviceDetailRes = await getDeviceDetail(item);
  176 + const { deviceProfileId } = deviceDetailRes;
  177 + if (!deviceProfileId) return [];
  178 + const attributeRes = await getAttribute(deviceProfileId);
  179 + const dataFormat: any = handleDataFormat(deviceDetailRes, attributeRes);
  180 + temp.push(dataFormat);
  181 + }
  182 + return temp;
  183 + };
  184 + const handleDataFormat = (deviceDetail: any, attributes: any) => {
  185 + const { name, tbDeviceId, alias } = deviceDetail;
  186 + const attribute = attributes.map((item) => ({
  187 + identifier: item.identifier,
  188 + name: item.name,
  189 + detail: item.detail,
  190 + }));
  191 + return {
  192 + name: alias || name,
  193 + tbDeviceId,
  194 + attribute,
  195 + };
  196 + };
  197 +
  198 + const getCanBatchClear = computed(() => {
  199 + const rowSelection = getRowSelection();
  200 + const getRows: AlarmLogItem[] = getSelectRows();
  201 +
  202 + return (
  203 + !rowSelection.selectedRowKeys?.length ||
  204 + getRows.some(
  205 + (item) =>
  206 + item.status === AlarmStatus.CLEARED_ACK || item.status === AlarmStatus.CLEARED_UN_ACK
  207 + )
  208 + );
  209 + });
  210 +
  211 + const getCanBatchAck = computed(() => {
  212 + const rowSelection = getRowSelection();
  213 + const getRows: AlarmLogItem[] = getSelectRows();
  214 +
  215 + return (
  216 + !rowSelection.selectedRowKeys?.length ||
  217 + getRows.some(
  218 + (item) =>
  219 + item.status === AlarmStatus.CLEARED_ACK || item.status === AlarmStatus.ACTIVE_ACK
  220 + )
  221 + );
  222 + });
  223 +
  224 + const { createMessage } = useMessage();
  225 + const handleBatchAck = async () => {
  226 + const ids = getSelectRows<AlarmLogItem>().map((item) => item.id);
  227 + if (!ids.length) return;
  228 + await doBatchAckAlarm(ids);
  229 + createMessage.success('操作成功');
  230 + clearSelectedRowKeys();
  231 + reload();
  232 + };
  233 +
  234 + const handleBatchClear = async () => {
  235 + const ids = getSelectRows<AlarmLogItem>().map((item) => item.id);
  236 + if (!ids.length) return;
  237 + await doBatchClearAlarm(ids);
  238 + createMessage.success('操作成功');
  239 + clearSelectedRowKeys();
  240 + reload();
  241 + };
  242 +
64 243 return {
65 244 registerTable,
66 245 registerDetailDrawer,
67 246 handleDetail,
68 247 handleSuccess,
  248 + handleViewAlarmDetails,
  249 + handleBatchAck,
  250 + handleBatchClear,
  251 + getCanBatchAck,
  252 + getCanBatchClear,
69 253 };
70 254 },
71 255 });
... ...
... ... @@ -11,6 +11,7 @@
11 11 <template #iconSelect>
12 12 <Upload
13 13 name="avatar"
  14 + accept=".png,.jpg,.jpeg,.gif"
14 15 list-type="picture-card"
15 16 class="avatar-uploader"
16 17 :show-upload-list="false"
... ...
... ... @@ -116,7 +116,7 @@
116 116 const { organizationIdTreeRef, resetFn } = useResetOrganizationTree(searchInfo);
117 117 const [registerModal, { openModal }] = useModal();
118 118 // 表格hooks
119   - const [registerTable, { reload, setProps }] = useTable({
  119 + const [registerTable, { reload, setProps, clearSelectedRowKeys }] = useTable({
120 120 title: '视频列表',
121 121 api: cameraPage,
122 122 columns,
... ... @@ -145,6 +145,7 @@
145 145 // 刷新
146 146 const handleSuccess = () => {
147 147 reload();
  148 + clearSelectedRowKeys();
148 149 };
149 150 const { hasBatchDelete, handleDeleteOrBatchDelete, selectionOptions } = useBatchDelete(
150 151 deleteCameraManage,
... ...
... ... @@ -56,7 +56,10 @@
56 56 return unref(orgList);
57 57 }
58 58 },
59   - params: { ...params, _t: unref(timespan) },
  59 + params: {
  60 + ...params,
  61 + _t: unref(timespan),
  62 + },
60 63 onChange: (...args: any[]) => {
61 64 emit('change', ...args);
62 65 },
... ...
... ... @@ -93,6 +93,7 @@ export const formSchema: FormSchema[] = [
93 93 return {
94 94 listType: 'picture-card',
95 95 maxFileLimit: 1,
  96 + accept: '.png,.jpg,.jpeg,.gif',
96 97 api: async (file: File) => {
97 98 try {
98 99 const formData = new FormData();
... ... @@ -110,6 +111,10 @@ export const formSchema: FormSchema[] = [
110 111 onPreview: (fileList: FileItem) => {
111 112 createImgPreview({ imageList: [fileList.url!] });
112 113 },
  114 + // showUploadList: {
  115 + // showDownloadIcon: true,
  116 + // showRemoveIcon: true,
  117 + // },
113 118 };
114 119 },
115 120 },
... ...
... ... @@ -11,7 +11,7 @@
11 11 import { ConfigurationCenterItemsModal } from '/@/api/configuration/center/model/configurationCenterModal';
12 12 import { PageWrapper } from '/@/components/Page';
13 13 import { BasicForm, useForm } from '/@/components/Form';
14   - import { ConfigurationPermission, searchFormSchema } from './center.data';
  14 + import { ConfigurationPermission, Platform, searchFormSchema } from './center.data';
15 15 import { useMessage } from '/@/hooks/web/useMessage';
16 16 import { Authority } from '/@/components/Authority';
17 17 import { isDevMode } from '/@/utils/env';
... ... @@ -30,6 +30,7 @@
30 30 import { ViewType } from '../../visual/board/config/panelDetail';
31 31 import { useRole } from '/@/hooks/business/useRole';
32 32 import { useClipboard } from '@vueuse/core';
  33 + import { Icon } from '/@/components/Icon';
33 34
34 35 const listColumn = ref(5);
35 36
... ... @@ -135,13 +136,19 @@
135 136 const handlePreview = (record: ConfigurationCenterItemsModal) => {
136 137 if (!unref(getPreviewFlag)) return;
137 138 window.open(
138   - `${configurationPrefix}/${isDev ? '?dev=1&' : '?'}configurationId=${record!.id}&lightbox=1`
  139 + `${configurationPrefix}/${isDev ? '?dev=1&' : '?'}configurationId=${
  140 + record!.id
  141 + }&lightbox=1&organizationId=${record.organizationId}`
139 142 );
140 143 };
141 144
142 145 const handleDesign = (record: ConfigurationCenterItemsModal) => {
143 146 if (!unref(getDesignFlag)) return;
144   - window.open(`${configurationPrefix}/${isDev ? '?dev=1&' : '?'}configurationId=${record!.id}`);
  147 + window.open(
  148 + `${configurationPrefix}/${isDev ? '?dev=1&' : '?'}configurationId=${
  149 + record!.id
  150 + }&organizationId=${record.organizationId}`
  151 + );
145 152 };
146 153
147 154 const handleDelete = async (record: ConfigurationCenterItemsModal) => {
... ... @@ -164,6 +171,7 @@
164 171 searchParams.set('configurationId', record.id);
165 172 searchParams.set('publicId', record.publicId || '');
166 173 searchParams.set('lightbox', '1');
  174 + searchParams.set('organizationId', record!.organizationId || '');
167 175 return `${origin}${configurationPrefix}/?${searchParams.toString()}`;
168 176 };
169 177
... ... @@ -187,7 +195,7 @@
187 195
188 196 onMounted(() => {
189 197 const clientHeight = document.documentElement.clientHeight;
190   - const rect = getBoundingClientRect(unref(listEl)!.$el!) as DOMRect;
  198 + const rect = getBoundingClientRect(unref(listEl)!.$el! as HTMLElement) as DOMRect;
191 199 // margin-top 24 height 24
192 200 const paginationHeight = 24 + 24 + 8;
193 201 // list pading top 8 maring-top 8 extra slot 56
... ... @@ -322,7 +330,16 @@
322 330 </template>
323 331 <template #description>
324 332 <div class="truncate h-11">
325   - <div class="truncate">{{ item.organizationDTO.name }}</div>
  333 + <div class="truncate flex justify-between items-center">
  334 + <div>{{ item.organizationDTO.name }}</div>
  335 + <Icon
  336 + :icon="
  337 + item.platform === Platform.PC
  338 + ? 'ri:computer-line'
  339 + : 'clarity:mobile-phone-solid'
  340 + "
  341 + />
  342 + </div>
326 343 <div class="truncate">{{ item.remark || '' }} </div>
327 344 </div>
328 345 </template>
... ...
... ... @@ -22,7 +22,7 @@
22 22 watch(
23 23 () => props.customerList,
24 24 (newValue) => {
25   - const transferResult = newValue.map((item) => [item.ts, item.value]);
  25 + const transferResult = newValue.map((item) => [item.date, item.value]);
26 26
27 27 setOptions({
28 28 tooltip: {
... ... @@ -35,7 +35,7 @@
35 35 },
36 36 },
37 37 xAxis: {
38   - type: 'time',
  38 + type: 'category',
39 39 splitLine: {
40 40 show: true,
41 41 lineStyle: {
... ... @@ -90,7 +90,7 @@
90 90 trend: props.type,
91 91 });
92 92
93   - const transferResult = res.map((item) => [item.ts, item.value]);
  93 + const transferResult = res.map((item) => [item.date, item.value]);
94 94 setOptions({
95 95 tooltip: {
96 96 trigger: 'axis',
... ...
... ... @@ -2,9 +2,10 @@
2 2 <div ref="chartRef" :style="{ height, width }"></div>
3 3 </template>
4 4 <script lang="ts" setup>
5   - import { ref, Ref, withDefaults, onMounted, watch } from 'vue';
  5 + import { ref, Ref, onMounted, watch } from 'vue';
6 6 import { useECharts } from '/@/hooks/web/useECharts';
7 7 import { getTrendData } from '/@/api/dashboard';
  8 + import { getDateByShortcutQueryKey, ShortcutQueryKeyEnum } from '../hooks/useDate';
8 9
9 10 interface Props {
10 11 width?: string;
... ... @@ -30,7 +31,7 @@
30 31 },
31 32 },
32 33 xAxis: {
33   - type: 'time',
  34 + type: 'category',
34 35 splitLine: {
35 36 show: true,
36 37 lineStyle: {
... ... @@ -77,15 +78,12 @@
77 78 const chartRef = ref<HTMLDivElement | null>(null);
78 79 const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
79 80 onMounted(async () => {
80   - const endTs = Date.now();
81 81 const res = await getTrendData({
82   - startTs: endTs - 2592000000,
83   - endTs,
84   - interval: 86400000,
  82 + ...getDateByShortcutQueryKey(ShortcutQueryKeyEnum.LATEST_1_MONTH),
85 83 trend: 'CUSTOMER_TREND',
86 84 });
87 85
88   - const transferResult = res.map((item) => [item.ts, item.value]);
  86 + const transferResult = res.map((item) => [item.date, item.value]);
89 87 setOptions({
90 88 tooltip: {
91 89 trigger: 'axis',
... ... @@ -97,7 +95,7 @@
97 95 },
98 96 },
99 97 xAxis: {
100   - type: 'time',
  98 + type: 'category',
101 99 splitLine: {
102 100 show: true,
103 101 lineStyle: {
... ...
... ... @@ -19,7 +19,8 @@
19 19 <div>1小时: 查询最近1小时的数据,间隔时间为5分钟.</div>
20 20 <div>
21 21 间隔时间:
22   - 以当前时间作为结束时间,往前推移对应天数或小时的时间作为开始时间,然后在此时间区间内进行分组聚合查询.
  22 + <span class="font-bold underline">以当前时间作为结束时间</span>
  23 + ,往前推移对应天数或小时的时间作为开始时间,然后在此时间区间内进行分组聚合查询.
23 24 </div>
24 25 </template>
25 26 <QuestionCircleOutlined class="!mr-1" />
... ... @@ -36,7 +37,15 @@
36 37 <DatePicker
37 38 @change="(_, DateString) => onDateChange(_, DateString, role === RoleEnum.CUSTOMER_USER)"
38 39 v-model:value="dateValue"
39   - />
  40 + >
  41 + <Button
  42 + type="link"
  43 + class="!px-0"
  44 + :style="dateValue ? { color: '#0960bd', fontWeight: 500 } : { color: '#000000d9' }"
  45 + >
  46 + {{ dateValue ? (dateValue as moment.Moment).format('YYYY-MM-DD') : '自定义' }}
  47 + </Button>
  48 + </DatePicker>
40 49 </div>
41 50 </template>
42 51 <div v-if="activeKey === '1'">
... ... @@ -95,6 +104,21 @@
95 104 >
96 105 <template #extra>
97 106 <div class="extra-date">
  107 + <Tooltip :overlayStyle="{ maxWidth: '340px' }">
  108 + <template #title>
  109 + <section>
  110 + <div>最近一个月: 查询最近一个月的数据,间隔时间为1天.</div>
  111 + <div>最近三个月: 查询最近三个月的数据,间隔时间为1天.</div>
  112 + <div>最近一年: 查询最近一年的数据,间隔时间为1月.</div>
  113 + <div>
  114 + 间隔时间:
  115 + <span class="font-bold underline"> 以当天的(最后时间)作为结束时间 </span>
  116 + ,往前推移对应天数或月的时间作为开始时间,然后在此时间区间内进行分组聚合查询.
  117 + </div>
  118 + </section>
  119 + </template>
  120 + <QuestionCircleOutlined class="!mr-2 cursor-pointer" />
  121 + </Tooltip>
98 122 <template v-for="(item, index) in TenantOrCustomerDateList" :key="item.value">
99 123 <span
100 124 @click="quickQueryTenantOrCustomerTime(index, item.value)"
... ... @@ -104,9 +128,31 @@
104 128 </template>
105 129 <DatePicker.RangePicker
106 130 @change="onDateCustomerChange"
  131 + :disabledDate="handleDisableDate"
  132 + @calendarChange="handleCalendarChange"
107 133 size="small"
108 134 v-model:value="customerDateValue"
109   - />
  135 + :showTime="{
  136 + defaultValue: [dateUtil('00:00:00', 'HH:mm:ss'), dateUtil('23:59:59', 'HH:mm:ss')],
  137 + }"
  138 + >
  139 + <Button
  140 + type="link"
  141 + class="!px-0"
  142 + :style="
  143 + customerDateValue && customerDateValue.length
  144 + ? { color: '#0960bd', fontWeight: 500 }
  145 + : { color: '#000000d9' }
  146 + "
  147 + >{{
  148 + customerDateValue && customerDateValue.length
  149 + ? `${(customerDateValue?.[0] as moment.Moment)?.format('YYYY-MM-DD')}
  150 + ~
  151 + ${(customerDateValue?.[1] as moment.Moment)?.format('YYYY-MM-DD')}`
  152 + : '自定义'
  153 + }}</Button
  154 + >
  155 + </DatePicker.RangePicker>
110 156 </div>
111 157 </template>
112 158 <CustomerTrend :customerTrendList="customerTrendList" />
... ... @@ -114,15 +160,15 @@
114 160 </div>
115 161 </template>
116 162 <script lang="ts" setup>
117   - import { ref, reactive } from 'vue';
118   - import { Card, DatePicker, Tooltip } from 'ant-design-vue';
  163 + import { ref, reactive, unref } from 'vue';
  164 + import { Card, DatePicker, Tooltip, Button } from 'ant-design-vue';
119 165 import VisitAnalysis from './VisitAnalysis.vue';
120 166 import VisitAnalysisBar from './VisitAnalysisBar.vue';
121 167 import { RoleEnum, isAdmin } from '/@/enums/roleEnum';
122 168 import { useWebSocket } from '@vueuse/core';
123 169 import { getAuthCache } from '/@/utils/auth';
124 170 import { JWT_TOKEN_KEY } from '/@/enums/cacheEnum';
125   - import { formatToDateTime } from '/@/utils/dateUtil';
  171 + import { dateUtil, formatToDateTime } from '/@/utils/dateUtil';
126 172 import CustomerTrend from './CustomerTrend.vue';
127 173 // import TenantTrend from './TenantTrend.vue';
128 174 import CustomerAlarmMessage from './CustomerAlarmMessage.vue';
... ... @@ -130,6 +176,7 @@
130 176 import { getTrendData } from '/@/api/dashboard';
131 177 import { useGlobSetting } from '/@/hooks/setting';
132 178 import { QuestionCircleOutlined } from '@ant-design/icons-vue';
  179 + import { RangePickerValue } from 'ant-design-vue/lib/date-picker/interface';
133 180
134 181 defineExpose({
135 182 isAdmin,
... ... @@ -480,6 +527,19 @@
480 527 // onDateTenantChange,
481 528 onDateCustomerChange,
482 529 } = useDate();
  530 +
  531 + const handleCalendarChange = (val: RangePickerValue) => {
  532 + customerDateValue.value = val as any;
  533 + };
  534 +
  535 + const handleDisableDate = (current: moment.Moment) => {
  536 + if (!current) return true;
  537 + if (!unref(customerDateValue) || unref(customerDateValue).length === 0) {
  538 + return false;
  539 + }
  540 + const diffDate = current.diff(unref(customerDateValue)[0], 'days');
  541 + return Math.abs(diffDate) > 30;
  542 + };
483 543 </script>
484 544
485 545 <style lang="less">
... ...
... ... @@ -29,7 +29,8 @@
29 29 <div>1小时: 查询最近1小时的数据,间隔时间为5分钟.</div>
30 30 <div>
31 31 间隔时间:
32   - 以当前时间作为结束时间,往前推移对应天数或小时的时间作为开始时间,然后在此时间区间内进行分组聚合查询.
  32 + <span class="font-bold underline">以当前时间作为结束时间</span>
  33 + ,往前推移对应天数或小时的时间作为开始时间,然后在此时间区间内进行分组聚合查询.
33 34 </div>
34 35 </template>
35 36 <QuestionCircleOutlined class="!mr-1" />
... ... @@ -46,7 +47,15 @@
46 47 <DatePicker
47 48 @change="(_, DateString) => onDateChange(_, DateString, role === RoleEnum.CUSTOMER_USER)"
48 49 v-model:value="dateValue"
49   - />
  50 + >
  51 + <Button
  52 + type="link"
  53 + class="!px-0"
  54 + :style="dateValue ? { color: '#0960bd', fontWeight: 500 } : { color: '#000000d9' }"
  55 + >
  56 + {{ dateValue ? (dateValue as moment.Moment).format('YYYY-MM-DD') : '自定义' }}
  57 + </Button>
  58 + </DatePicker>
50 59 </div>
51 60 </template>
52 61 <!-- <div v-if="activeKey === '1'">
... ... @@ -80,6 +89,21 @@
80 89 >
81 90 <template #extra>
82 91 <div class="extra-date">
  92 + <Tooltip :overlayStyle="{ maxWidth: '340px' }">
  93 + <template #title>
  94 + <section>
  95 + <div>最近一个月: 查询最近一个月的数据,间隔时间为1天.</div>
  96 + <div>最近三个月: 查询最近三个月的数据,间隔时间为1天.</div>
  97 + <div>最近一年: 查询最近一年的数据,间隔时间为1月.</div>
  98 + <div>
  99 + 间隔时间:
  100 + <span class="font-bold underline"> 以当天的(最后时间)作为结束时间 </span>
  101 + ,往前推移对应天数或月的时间作为开始时间,然后在此时间区间内进行分组聚合查询.
  102 + </div>
  103 + </section>
  104 + </template>
  105 + <QuestionCircleOutlined class="!mr-2 cursor-pointer" />
  106 + </Tooltip>
83 107 <template v-for="(item, index) in TenantOrCustomerDateList" :key="item.value">
84 108 <span
85 109 @click="quickQueryTenantOrCustomerTime(index, item.value, 'tenant')"
... ... @@ -90,8 +114,30 @@
90 114 <DatePicker.RangePicker
91 115 @change="onDateTenantChange"
92 116 size="small"
  117 + @calendarChange="handleCalendarChange"
  118 + :disabledDate="handleDisableDate"
93 119 v-model:value="tenantDateValue"
94   - />
  120 + :showTime="{
  121 + defaultValue: [dateUtil('00:00:00', 'HH:mm:ss'), dateUtil('23:59:59', 'HH:mm:ss')],
  122 + }"
  123 + >
  124 + <Button
  125 + type="link"
  126 + class="!px-0"
  127 + :style="
  128 + tenantDateValue && tenantDateValue.length
  129 + ? { color: '#0960bd', fontWeight: 500 }
  130 + : { color: '#000000d9' }
  131 + "
  132 + >{{
  133 + tenantDateValue && tenantDateValue.length
  134 + ? `${(tenantDateValue?.[0] as moment.Moment)?.format('YYYY-MM-DD')}
  135 + ~
  136 + ${(tenantDateValue?.[1] as moment.Moment)?.format('YYYY-MM-DD')}`
  137 + : '自定义'
  138 + }}</Button
  139 + >
  140 + </DatePicker.RangePicker>
95 141 </div>
96 142 </template>
97 143 <TenantTrend :tenantTrendList="tenantTrendList" />
... ... @@ -123,8 +169,8 @@
123 169 </div>
124 170 </template>
125 171 <script lang="ts" setup>
126   - import { ref, reactive } from 'vue';
127   - import { Card, DatePicker, Tooltip } from 'ant-design-vue';
  172 + import { ref, reactive, unref } from 'vue';
  173 + import { Card, DatePicker, Tooltip, Button } from 'ant-design-vue';
128 174 import { QuestionCircleOutlined } from '@ant-design/icons-vue';
129 175 // import VisitAnalysis from './VisitAnalysis.vue';
130 176 import VisitAnalysisBar from './VisitAnalysisBar.vue';
... ... @@ -132,13 +178,14 @@
132 178 import { useWebSocket } from '@vueuse/core';
133 179 import { getAuthCache } from '/@/utils/auth';
134 180 import { JWT_TOKEN_KEY } from '/@/enums/cacheEnum';
135   - import { formatToDateTime } from '/@/utils/dateUtil';
  181 + import { dateUtil, formatToDateTime } from '/@/utils/dateUtil';
136 182 // import CustomerTrend from './CustomerTrend.vue';
137 183 import TenantTrend from './TenantTrend.vue';
138 184 import CustomerAlarmMessage from './CustomerAlarmMessage.vue';
139 185 import { useDate } from '../hooks/useDate';
140 186 import { getTrendData } from '/@/api/dashboard';
141 187 import { useGlobSetting } from '/@/hooks/setting';
  188 + import { RangePickerValue } from 'ant-design-vue/lib/date-picker/interface';
142 189
143 190 defineExpose({
144 191 isAdmin,
... ... @@ -445,7 +492,7 @@
445 492 if (activeIndex.value === index) return;
446 493 activeIndex.value = index;
447 494 dateValue.value = '';
448   - console.log(interval);
  495 +
449 496 if (isCustomer) {
450 497 if (activeKey.value === '1') {
451 498 const data = await getTrendData({
... ... @@ -498,6 +545,19 @@
498 545 onDateTenantChange,
499 546 // onDateCustomerChange,
500 547 } = useDate();
  548 +
  549 + const handleCalendarChange = (val: RangePickerValue) => {
  550 + tenantDateValue.value = val as any;
  551 + };
  552 +
  553 + const handleDisableDate = (current: moment.Moment) => {
  554 + if (!current) return true;
  555 + if (!unref(tenantDateValue) || unref(tenantDateValue).length === 0) {
  556 + return false;
  557 + }
  558 + const diffDate = current.diff(unref(tenantDateValue)[0], 'days');
  559 + return Math.abs(diffDate) > 30;
  560 + };
501 561 </script>
502 562
503 563 <style lang="less">
... ...
... ... @@ -2,9 +2,10 @@
2 2 <div ref="chartRef" :style="{ height, width }"></div>
3 3 </template>
4 4 <script lang="ts" setup>
5   - import { ref, Ref, withDefaults, onMounted, watch } from 'vue';
  5 + import { ref, Ref, onMounted, watch } from 'vue';
6 6 import { useECharts } from '/@/hooks/web/useECharts';
7 7 import { getTrendData } from '/@/api/dashboard/index';
  8 + import { getDateByShortcutQueryKey, ShortcutQueryKeyEnum } from '../hooks/useDate';
8 9 interface Props {
9 10 width?: string;
10 11 height?: string;
... ... @@ -30,7 +31,7 @@
30 31 },
31 32 },
32 33 xAxis: {
33   - type: 'time',
  34 + type: 'category',
34 35 splitLine: {
35 36 show: true,
36 37 lineStyle: {
... ... @@ -77,14 +78,11 @@
77 78 const chartRef = ref<HTMLDivElement | null>(null);
78 79 const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
79 80 onMounted(async () => {
80   - const endTs = Date.now();
81 81 const res = await getTrendData({
82   - startTs: endTs - 2592000000,
83   - endTs,
84   - interval: 86400000,
  82 + ...getDateByShortcutQueryKey(ShortcutQueryKeyEnum.LATEST_1_MONTH),
85 83 trend: 'TENANT_TREND',
86 84 });
87   - const transferResult = res.map((item) => [item.ts, item.value]);
  85 + const transferResult = res.map((item) => [item.date, item.value]);
88 86 setOptions({
89 87 tooltip: {
90 88 trigger: 'axis',
... ... @@ -96,7 +94,7 @@
96 94 },
97 95 },
98 96 xAxis: {
99   - type: 'time',
  97 + type: 'category',
100 98 splitLine: {
101 99 show: true,
102 100 lineStyle: {
... ...
1   -import { formatToDateTime } from '/@/utils/dateUtil';
2   -import { ref } from 'vue';
  1 +import { dateUtil } from '/@/utils/dateUtil';
  2 +import { Ref, ref } from 'vue';
3 3 import { getTrendData } from '/@/api/dashboard';
  4 +import { RangePickerValue } from 'ant-design-vue/lib/date-picker/interface';
  5 +import moment from 'moment';
  6 +
  7 +export enum ShortcutQueryKeyEnum {
  8 + LATEST_1_MONTH = 'LATEST_1_MONTH',
  9 + LATEST_3_MONTH = 'LATEST_3_MONTH',
  10 + LATEST_1_YEAR = 'LATEST_1_YEAR',
  11 +}
  12 +
  13 +export function getDateByShortcutQueryKey(value: ShortcutQueryKeyEnum) {
  14 + const mapping = {
  15 + [ShortcutQueryKeyEnum.LATEST_1_MONTH]: () => {
  16 + return {
  17 + startTs: dateUtil().subtract(1, 'month').startOf('day').valueOf(),
  18 + interval: 24 * 60 * 60 * 1000,
  19 + };
  20 + },
  21 + [ShortcutQueryKeyEnum.LATEST_3_MONTH]: () => {
  22 + return {
  23 + startTs: dateUtil().subtract(3, 'month').startOf('day').valueOf(),
  24 + interval: 24 * 60 * 60 * 1000,
  25 + };
  26 + },
  27 + [ShortcutQueryKeyEnum.LATEST_1_YEAR]: () => {
  28 + return {
  29 + startTs: dateUtil().subtract(1, 'year').startOf('day').valueOf(),
  30 + interval: 30 * 24 * 60 * 60 * 1000,
  31 + };
  32 + },
  33 + };
  34 +
  35 + const result = mapping?.[value]?.();
  36 +
  37 + return {
  38 + ...result,
  39 + endTs: dateUtil().add(1, 'day').startOf('day').valueOf(),
  40 + };
  41 +}
4 42
5 43 export function useDate() {
6 44 const tenantDateValue = ref([]);
7   - const customerDateValue = ref([]);
8   - const tenantTrendList = ref([]);
9   - const customerTrendList = ref([]);
  45 + const customerDateValue = ref<RangePickerValue>([]);
  46 + const tenantTrendList = ref<[string, string][]>([]);
  47 + const customerTrendList = ref<[string, string][]>([]);
10 48 const activeTenantIndex = ref(0);
11 49 const activeCustomerIndex = ref(0);
12 50 const TenantOrCustomerDateList = ref([
13   - { label: '30天', value: 2592000000 },
14   - { label: '最近三个月', value: 7776000000 },
15   - { label: '最近一年', value: 31536000000 },
  51 + { label: '最近一个月', value: ShortcutQueryKeyEnum.LATEST_1_MONTH },
  52 + { label: '最近三个月', value: ShortcutQueryKeyEnum.LATEST_3_MONTH },
  53 + { label: '最近一年', value: ShortcutQueryKeyEnum.LATEST_1_YEAR },
16 54 ]);
17 55
18 56 // 租户趋势和客户趋势快速选择时间
19 57 async function quickQueryTenantOrCustomerTime(
20 58 index: number,
21   - value: number,
  59 + value: ShortcutQueryKeyEnum,
22 60 flag: 'tenant' | 'customer'
23 61 ) {
24   - const endTs = Date.now();
25 62 if (flag === 'tenant') {
26 63 if (activeTenantIndex.value === index) return;
27 64 activeTenantIndex.value = index;
28 65 tenantDateValue.value = [];
29 66 const res = await getTrendData({
30   - startTs: endTs - value,
31   - endTs,
32   - interval: value === 2592000000 ? 86400000 : value === 7776000000 ? 172800000 : 2592000000,
  67 + ...getDateByShortcutQueryKey(value),
33 68 trend: 'TENANT_TREND',
34 69 });
35   - tenantTrendList.value = res.map((item) => [item.ts, item.value]);
  70 + tenantTrendList.value = res.map((item) => [item.date, item.value]);
36 71 } else {
37 72 if (activeCustomerIndex.value === index) return;
38 73 activeCustomerIndex.value = index;
39 74 customerDateValue.value = [];
40 75 const res = await getTrendData({
41   - startTs: endTs - value,
42   - endTs,
43   - interval: value === 2592000000 ? 86400000 : value === 7776000000 ? 172800000 : 2592000000,
  76 + ...getDateByShortcutQueryKey(value),
44 77 trend: 'CUSTOMER_TREND',
45 78 });
46   - customerTrendList.value = res.map((item) => [item.ts, item.value]);
  79 + customerTrendList.value = res.map((item) => [item.date, item.value]);
47 80 }
48 81 }
49 82
50 83 // 获取选中的时间范围内的数据
51   - async function getDateData(startTs, endTs, trend: 'CUSTOMER_TREND' | 'TENANT_TREND', list) {
52   - // 计算时间间隔
53   - function computedInterval(startTs: number, endTs: number) {
54   - /**
55   - * 选择的时间间隔
56   - * <=1 2h
57   - * <=30 1day
58   - * >30<90 2day
59   - * >90 1month
60   - */
61   - let interval = 86400000;
62   - if (endTs - startTs <= 86400000) {
63   - interval = 7200000;
64   - } else if (endTs - startTs <= 2592000000) {
65   - interval = 86400000;
66   - } else if (endTs - startTs > 2592000000 && endTs - startTs < 7776000000) {
67   - interval = 172800000;
68   - } else if (endTs - startTs > 7776000000) {
69   - interval = 2592000000;
70   - }
71   - return interval;
72   - }
73   - startTs = parseInt(formatToDateTime(startTs, 'x')) - 86400000;
74   - endTs = parseInt(formatToDateTime(endTs, 'x'));
  84 + async function getDateData(
  85 + startTs: moment.Moment,
  86 + endTs: moment.Moment,
  87 + trend: 'CUSTOMER_TREND' | 'TENANT_TREND',
  88 + list: Ref<[string, string][]>
  89 + ) {
75 90 const res = await getTrendData({
76   - startTs,
77   - endTs,
78   - interval: computedInterval(startTs, endTs),
  91 + startTs: startTs.valueOf(),
  92 + endTs: endTs.valueOf(),
  93 + interval: 24 * 60 * 60 * 1000,
79 94 trend,
80 95 });
81   - list.value = res.map((item) => [item.ts, item.value]);
  96 + list.value = res.map((item) => [item.date, item.value]);
82 97 }
83 98
84 99 // 租户选择日期
85   - function onDateTenantChange(_, dateString) {
86   - if (!_.length) return;
87   - const [startTs, endTs] = dateString;
  100 + function onDateTenantChange(range: RangePickerValue) {
  101 + if (!range.length) return;
  102 + const [startTs, endTs] = range;
88 103 activeTenantIndex.value = -1;
89   - getDateData(startTs, endTs, 'TENANT_TREND', tenantTrendList);
  104 + getDateData(startTs as moment.Moment, endTs as moment.Moment, 'TENANT_TREND', tenantTrendList);
90 105 }
91 106 // 客户趋势选择日期
92   - function onDateCustomerChange(_, dateString) {
93   - if (!_.length) return;
94   - const [startTs, endTs] = dateString;
  107 + function onDateCustomerChange(range: RangePickerValue) {
  108 + if (!range.length) return;
  109 + const [startTs, endTs] = range;
95 110 activeCustomerIndex.value = -1;
96   - getDateData(startTs, endTs, 'CUSTOMER_TREND', customerTrendList);
  111 + getDateData(
  112 + startTs as moment.Moment,
  113 + endTs as moment.Moment,
  114 + 'CUSTOMER_TREND',
  115 + customerTrendList
  116 + );
97 117 }
98 118 return {
99 119 tenantDateValue,
... ...
... ... @@ -17,10 +17,21 @@ export enum ConfigurationPermission {
17 17 UPDATE = 'api:yt:dataview:center:update',
18 18 DELETE = 'api:yt:dataview:center:delete',
19 19 SHARE = 'api:yt:dataview:center:share',
20   - DESIGN = 'api:yt:dataview:center:get_configuration_info:design',
  20 + // DESIGN = 'api:yt:dataview:center:get_configuration_info:design',
  21 + DESIGN = 'api:yt:dataview:center:get_dataview_info:design',
21 22 PREVIEW = 'api:yt:dataview:center:get_configuration_info:preview',
22 23 PUBLISH = 'api:yt:dataview:center:publish',
23 24 // CANCEL_PUBLISH = 'api:yt:dataview:center:cancel_publish',
  25 + PUBLISH_INTERFACE = 'api:yt:dataview:center:public_interface',
  26 +}
  27 +
  28 +export enum PublicInterface {
  29 + CREATE = 'api:yt:dataview:center:public_interface:post',
  30 + LIST = 'api:yt:dataview:center:public_interface:list',
  31 + UPDATE = 'api:yt:dataview:center:public_interface:update',
  32 + DELETE = 'api:yt:dataview:center:public_interface:delete',
  33 + PUBLISH = 'api:yt:dataview:center:public_interface:publish',
  34 + CANCEL_PUBLISH = 'api:yt:dataview:center:public_interface:cancel',
24 35 }
25 36
26 37 // 查询字段
... ... @@ -48,6 +59,7 @@ export const formSchema: FormSchema[] = [
48 59 return {
49 60 listType: 'picture-card',
50 61 maxFileLimit: 1,
  62 + accept: '.png,.jpg,.jpeg,.gif',
51 63 api: async (file: File) => {
52 64 try {
53 65 const formData = new FormData();
... ... @@ -62,6 +74,10 @@ export const formSchema: FormSchema[] = [
62 74 return {};
63 75 }
64 76 },
  77 + // showUploadList: true,
  78 + onDownload(file) {
  79 + console.log(file);
  80 + },
65 81 onPreview: (fileList: FileItem) => {
66 82 createImgPreview({ imageList: [fileList.url!] });
67 83 },
... ... @@ -76,7 +92,7 @@ export const formSchema: FormSchema[] = [
76 92 component: 'Input',
77 93 componentProps: {
78 94 placeholder: '请输入大屏名称',
79   - maxLength: 255,
  95 + maxLength: 32,
80 96 },
81 97 },
82 98 {
... ...
... ... @@ -124,12 +124,16 @@
124 124 const { largeDesignerPrefix } = useGlobSetting();
125 125
126 126 const handlePreview = (record: BigScreenCenterItemsModel) => {
127   - window.open(`${largeDesignerPrefix}/#/chart/preview/${record.id}`);
  127 + window.open(
  128 + `${largeDesignerPrefix}/#/chart/preview/${record.id}?organizationId=${record.organizationId}`
  129 + );
128 130 };
129 131
130 132 const handleDesign = (record: BigScreenCenterItemsModel) => {
131 133 if (record.state === 1) return;
132   - window.open(`${largeDesignerPrefix}/#/chart/home/${record.id}`);
  134 + window.open(
  135 + `${largeDesignerPrefix}/#/chart/home/${record.id}?organizationId=${record.organizationId}`
  136 + );
133 137 };
134 138
135 139 const handleDelete = async (record: BigScreenCenterItemsModel) => {
... ... @@ -218,7 +222,10 @@
218 222 <Authority v-if="!isCustomerUser" :value="ConfigurationPermission.CREATE">
219 223 <Button type="primary" @click="handleCreateOrUpdate()"> 新增大屏 </Button>
220 224 </Authority>
221   - <Authority v-if="hasPublicInterfacePermission" :value="ConfigurationPermission.CREATE">
  225 + <Authority
  226 + v-if="hasPublicInterfacePermission"
  227 + :value="ConfigurationPermission.PUBLISH_INTERFACE"
  228 + >
222 229 <Button type="primary" @click="handleCreateOrUpdatePublicApi()">公共接口管理</Button>
223 230 </Authority>
224 231 <CardLayoutButton v-model:value="listColumn" @change="handleCardLayoutChange" />
... ... @@ -280,6 +287,7 @@
280 287 </Tooltip>
281 288 <Tooltip v-if="!isCustomerUser" title="设计">
282 289 <AuthIcon
  290 + :auth="ConfigurationPermission.DESIGN"
283 291 :disabled="item.state === 1"
284 292 icon="ant-design:edit-outlined"
285 293 @click="handleDesign(item)"
... ...
... ... @@ -9,23 +9,30 @@
9 9 <a-button type="link" class="ml-2" @click="handleRecordContent(record)"> 查看 </a-button>
10 10 </template>
11 11 <template #toolbar>
12   - <a-button type="primary" @click="handleCreateOrEdit(null)"> 新增公共接口 </a-button>
13   - <Popconfirm
14   - title="您确定要批量删除数据"
15   - ok-text="确定"
16   - cancel-text="取消"
17   - @confirm="handleDeleteOrBatchDelete(null)"
18   - >
19   - <a-button color="error" :disabled="hasBatchDelete"> 批量删除 </a-button>
20   - </Popconfirm>
21   - <Popconfirm
22   - title="您确定要批量发布"
23   - ok-text="确定"
24   - cancel-text="取消"
25   - @confirm="handleBatchPublish('batchPublish')"
26   - >
27   - <a-button color="error" :disabled="hasBatchPublish"> 批量发布 </a-button>
28   - </Popconfirm>
  12 + <Authority :value="PublicInterface.CREATE">
  13 + <a-button type="primary" @click="handleCreateOrEdit(null)"> 新增公共接口 </a-button>
  14 + </Authority>
  15 +
  16 + <Authority :value="PublicInterface.DELETE">
  17 + <Popconfirm
  18 + title="您确定要批量删除数据"
  19 + ok-text="确定"
  20 + cancel-text="取消"
  21 + @confirm="handleDeleteOrBatchDelete(null)"
  22 + >
  23 + <a-button color="error" :disabled="hasBatchDelete"> 批量删除 </a-button>
  24 + </Popconfirm>
  25 + </Authority>
  26 + <Authority :value="PublicInterface.PUBLISH">
  27 + <Popconfirm
  28 + title="您确定要批量发布"
  29 + ok-text="确定"
  30 + cancel-text="取消"
  31 + @confirm="handleBatchPublish('batchPublish')"
  32 + >
  33 + <a-button color="error" :disabled="hasBatchPublish"> 批量发布 </a-button>
  34 + </Popconfirm>
  35 + </Authority>
29 36 <!-- <Popconfirm
30 37 title="您确定要批量取消发布"
31 38 ok-text="确定"
... ... @@ -41,6 +48,7 @@
41 48 {
42 49 label: '发布',
43 50 icon: 'ant-design:node-expand-outlined',
  51 + auth: PublicInterface.PUBLISH,
44 52 onClick: handlePublish.bind(null, 'publish', record),
45 53 ifShow: () => {
46 54 return record.state === 0 && record.creator === userId;
... ... @@ -49,6 +57,7 @@
49 57 {
50 58 label: '取消发布',
51 59 icon: 'ant-design:node-collapse-outlined',
  60 + auth: PublicInterface.CANCEL_PUBLISH,
52 61 onClick: handlePublish.bind(null, 'canelPublish', record),
53 62 ifShow: () => {
54 63 return record.state === 1 && record.creator === userId;
... ... @@ -57,6 +66,7 @@
57 66 {
58 67 label: '修改',
59 68 icon: 'clarity:note-edit-line',
  69 + auth: PublicInterface.UPDATE,
60 70 onClick: handleCreateOrEdit.bind(null, record),
61 71 ifShow: () => {
62 72 return record.state === 0 && record.creator === userId;
... ... @@ -65,6 +75,7 @@
65 75 {
66 76 label: '删除',
67 77 icon: 'ant-design:delete-outlined',
  78 + auth: PublicInterface.DELETE,
68 79 color: 'error',
69 80 ifShow: () => {
70 81 return record.state === 0 && record.creator === userId;
... ... @@ -99,6 +110,8 @@
99 110 import { useMessage } from '/@/hooks/web/useMessage';
100 111 import { USER_INFO_KEY } from '/@/enums/cacheEnum';
101 112 import { getAuthCache } from '/@/utils/auth';
  113 + import { PublicInterface } from '../config';
  114 + import { Authority } from '/@/components/Authority';
102 115
103 116 const userInfo = getAuthCache(USER_INFO_KEY) as any;
104 117
... ... @@ -119,7 +132,7 @@
119 132 },
120 133 useSearchForm: true,
121 134 actionColumn: {
122   - width: 150,
  135 + width: 180,
123 136 title: '操作',
124 137 dataIndex: 'action',
125 138 slots: { customRender: 'action' },
... ...
... ... @@ -7,7 +7,7 @@ import { JSONEditor } from '/@/components/CodeEditor';
7 7 import { DeviceTypeEnum } from '/@/api/device/model/deviceModel';
8 8 import { getModelServices } from '/@/api/device/modelOfMatter';
9 9 import { ModelOfMatterParams } from '/@/api/device/model/modelOfMatterModel';
10   -import { toRaw, unref } from 'vue';
  10 +import { nextTick, toRaw, unref } from 'vue';
11 11 import ObjectModelValidateForm from '/@/components/Form/src/externalCompns/components/ObjectModelValidateForm/ObjectModelValidateForm.vue';
12 12 import { CommandDeliveryWayEnum, ServiceCallTypeEnum } from '/@/enums/toolEnum';
13 13 import { TaskTypeEnum } from '/@/views/task/center/config';
... ... @@ -121,7 +121,7 @@ export const step1Schemas: FormSchema[] = [
121 121 {
122 122 field: 'codeType',
123 123 label: '标识符类型',
124   - component: 'Select',
  124 + component: 'RadioGroup',
125 125 dynamicRules({ values }) {
126 126 return [
127 127 {
... ... @@ -246,8 +246,9 @@ export const step1Schemas: FormSchema[] = [
246 246 required: true,
247 247 component: 'ApiSelect',
248 248 ifShow: ({ values }) => values.deviceType === 'SENSOR' && values.organizationId,
249   - componentProps: ({ formModel }) => {
  249 + componentProps: ({ formModel, formActionType }) => {
250 250 const { organizationId, transportType } = formModel;
  251 + const { validateFields } = formActionType;
251 252 if (![organizationId, transportType].every(Boolean)) return {};
252 253 return {
253 254 api: async (params: Recordable) => {
... ... @@ -266,6 +267,10 @@ export const step1Schemas: FormSchema[] = [
266 267 },
267 268 valueField: 'tbDeviceId',
268 269 labelField: 'alias',
  270 + onChange: async () => {
  271 + await nextTick();
  272 + validateFields(['gatewayId']);
  273 + },
269 274 };
270 275 },
271 276 },
... ...
... ... @@ -169,12 +169,16 @@ export const alarmColumns: BasicColumn[] = [
169 169 {
170 170 title: '告警时间',
171 171 dataIndex: 'createdTime',
172   - width: 120,
  172 + width: 180,
173 173 },
174 174 {
175 175 title: '告警设备',
176 176 dataIndex: 'deviceName',
177   - width: 100,
  177 + width: 120,
  178 + customRender: ({ record }) => {
  179 + const { deviceAlias, deviceName } = record || {};
  180 + return deviceAlias || deviceName;
  181 + },
178 182 },
179 183 {
180 184 title: '告警场景',
... ... @@ -184,14 +188,20 @@ export const alarmColumns: BasicColumn[] = [
184 188 {
185 189 title: '告警级别',
186 190 dataIndex: 'severity',
187   - width: 160,
  191 + width: 90,
188 192 format: (text) => alarmLevel(text),
189 193 },
190 194 {
  195 + title: '告警详情',
  196 + dataIndex: 'details',
  197 + slots: { customRender: 'details' },
  198 + width: 160,
  199 + },
  200 + {
191 201 title: '状态',
192 202 dataIndex: 'status',
193 203 format: (text) => statusType(text),
194   - width: 160,
  204 + width: 100,
195 205 },
196 206 ];
197 207
... ... @@ -263,14 +273,6 @@ export const alarmSchemasForm: FormSchema[] = [
263 273 disabled: true,
264 274 },
265 275 },
266   - {
267   - field: 'details',
268   - label: '详情',
269   - component: 'InputTextArea',
270   - componentProps: {
271   - maxLength: 255,
272   - },
273   - },
274 276 ];
275 277 // 子设备
276 278 export const childDeviceSchemas: FormSchema[] = [
... ...
... ... @@ -52,17 +52,15 @@
52 52 const alarmStatus = ref('');
53 53 const [registerModal, { closeModal }] = useModalInner(async (data) => {
54 54 await resetFields();
  55 + const { deviceAlias, deviceName, severity, status, id } = data || {};
55 56 await setFieldsValue({
56 57 ...data,
57   - details: JSON.stringify(data?.details?.data).slice(
58   - 1,
59   - JSON.stringify(data?.details?.data).length - 1
60   - ),
61   - severity: alarmLevel(data.severity),
62   - status: statusType(data.status),
  58 + deviceName: deviceAlias || deviceName,
  59 + severity: alarmLevel(severity),
  60 + status: statusType(status),
63 61 });
64   - alarmId.value = data.id;
65   - alarmStatus.value = data.status;
  62 + alarmId.value = id;
  63 + alarmStatus.value = status;
66 64 });
67 65 // 处理报警
68 66 const handleAlarm = async () => {
... ...
1 1 <script lang="ts" setup>
2 2 import { Upload, Button } from 'ant-design-vue';
3 3 import { InboxOutlined } from '@ant-design/icons-vue';
4   - import { computed, ref } from 'vue';
  4 + import { computed } from 'vue';
5 5 import StepContainer from './StepContainer.vue';
6 6 import XLSX, { CellObject } from 'xlsx';
7 7 import { basicProps } from './props';
8 8 import { UploadFileParseValue } from './type';
  9 + import { useMessage } from '/@/hooks/web/useMessage';
9 10
10 11 const props = defineProps({
11 12 ...basicProps,
  13 + fileList: {
  14 + required: true,
  15 + type: Array as PropType<(Record<'uid' | 'name', string> & File)[]>,
  16 + },
12 17 value: {
13 18 require: true,
14 19 type: Object as PropType<UploadFileParseValue>,
15 20 },
16 21 });
17 22
18   - const emit = defineEmits(['update:value']);
19   -
20   - const fileList = ref<Record<'uid' | 'name', string>[]>([]);
  23 + const emit = defineEmits(['update:value', 'update:fileList']);
21 24
22 25 interface FileRequestParams {
23 26 file: File & { uid: string };
... ... @@ -48,23 +51,30 @@
48 51 return new Promise((resolve, reject) => {
49 52 const fileReader = new FileReader();
50 53 fileReader.onload = (event: ProgressEvent) => {
51   - const data = (event.target as FileReader).result as string;
52   -
53   - const result = XLSX.read(data, { type: 'string' });
54   -
55   - const sheetName = result.SheetNames.at(0);
56   - const workbook = result.Sheets;
57   - const sheet = workbook[sheetName as string];
58   - const sheetRange = sheet['!ref'];
59   -
60   - const {
61   - s: { c: startColumn },
62   - e: { c: endColumn },
63   - } = XLSX.utils.decode_range(sheetRange!);
64   -
65   - const header = getTableHeader(sheet, [startColumn, endColumn]);
66   - const content = XLSX.utils.sheet_to_json(sheet, { range: sheetRange }) as Recordable[];
67   - resolve({ header, content });
  54 + try {
  55 + const data = (event.target as FileReader).result as string;
  56 +
  57 + const result = XLSX.read(data, { type: 'string' });
  58 +
  59 + const sheetName = result.SheetNames.at(0);
  60 + const workbook = result.Sheets;
  61 + const sheet = workbook[sheetName as string];
  62 + const sheetRange = sheet['!ref'];
  63 +
  64 + const {
  65 + s: { c: startColumn },
  66 + e: { c: endColumn },
  67 + } = XLSX.utils.decode_range(sheetRange!);
  68 +
  69 + const header = getTableHeader(sheet, [startColumn, endColumn]);
  70 + const content = XLSX.utils.sheet_to_json(sheet, { range: sheetRange }) as Recordable[];
  71 +
  72 + resolve({ header, content });
  73 + } catch (error) {
  74 + const { createMessage } = useMessage();
  75 + createMessage.error('请检查csv文件是否正确');
  76 + throw error;
  77 + }
68 78 };
69 79
70 80 fileReader.onerror = () => {
... ... @@ -75,19 +85,19 @@
75 85 };
76 86
77 87 const handleParseFile = async ({ file, onSuccess, onError }: FileRequestParams) => {
78   - fileList.value = [];
79 88 const value = await readFile(file);
80 89 if (!value) {
81 90 onError();
82 91 return;
83 92 }
84   - fileList.value = [file];
  93 +
  94 + emit('update:fileList', [file]);
85 95 emit('update:value', value);
86 96 onSuccess({}, file);
87 97 };
88 98
89 99 const canGoNextStep = computed(() => {
90   - return !!fileList.value.length;
  100 + return !!props.fileList.length;
91 101 });
92 102
93 103 const handlePreviousStep = () => {
... ... @@ -97,12 +107,23 @@
97 107 const handleNextStep = () => {
98 108 props.goNextStep?.();
99 109 };
  110 +
  111 + const handleRemove = () => {
  112 + emit('update:fileList', []);
  113 + return true;
  114 + };
100 115 </script>
101 116
102 117 <template>
103 118 <StepContainer>
104 119 <div class="">设备文件</div>
105   - <Upload.Dragger :fileList="fileList" :customRequest="handleParseFile" accept=".csv" name="file">
  120 + <Upload.Dragger
  121 + :fileList="fileList"
  122 + :customRequest="handleParseFile"
  123 + accept=".csv"
  124 + name="file"
  125 + :remove="handleRemove"
  126 + >
106 127 <section class="cursor-pointer flex flex-col justify-center items-center">
107 128 <InboxOutlined class="text-[4rem] !text-blue-400" />
108 129 <div class="text-gray-500">点击上传或拖拽上传</div>
... ...
... ... @@ -17,6 +17,8 @@
17 17
18 18 const basicInfo = ref({});
19 19
  20 + const fileList = ref<(File & Record<'uid' | 'name', string>)[]>([]);
  21 +
20 22 const fileParseValue = ref<UploadFileParseValue>({ header: [], content: [] });
21 23
22 24 const columnConfiguration = ref<Record<'type', string>[]>([]);
... ... @@ -61,6 +63,7 @@
61 63
62 64 const reset = () => {
63 65 basicInfo.value = {};
  66 + fileList.value = [];
64 67 fileParseValue.value = { header: [], content: [] };
65 68 columnConfiguration.value = [];
66 69 importResult.value = {} as unknown as ImportDeviceResponse;
... ... @@ -109,6 +112,7 @@
109 112 />
110 113 <ImportCsv
111 114 v-if="StepsEnum[item] === StepsEnum.IMPORT_FILE && StepsEnum[item] === currentStep"
  115 + v-model:fileList="fileList"
112 116 v-model:value="fileParseValue"
113 117 :go-next-step="goNextStep"
114 118 :go-previous-step="goPreviousStep"
... ...
... ... @@ -145,6 +145,8 @@
145 145 // !!!此处需要删除地图的属性,否则会报堆栈溢出的错误 Uncaught RangeError: Maximum call stack size exceeded
146 146 Reflect.deleteProperty(DeviceStep1Ref.value.positionState, 'map');
147 147 Reflect.deleteProperty(DeviceStep1Ref.value.positionState, 'marker');
  148 + Reflect.deleteProperty(DeviceStep1Ref.value.devicePositionState, 'map');
  149 + Reflect.deleteProperty(DeviceStep1Ref.value.devicePositionState, 'marker');
148 150 setModalProps({
149 151 confirmLoading: true,
150 152 });
... ... @@ -156,7 +158,7 @@
156 158 customerId: currentDeviceData.customerId,
157 159 deviceInfo: {
158 160 avatar: DeviceStep1Ref.value?.devicePic,
159   - ...DeviceStep1Ref.value?.positionState,
  161 + ...DeviceStep1Ref.value?.devicePositionState,
160 162 },
161 163 };
162 164 validateNameLength(stepRecord.name);
... ... @@ -168,7 +170,7 @@
168 170 sn: stepRecord.name,
169 171 deviceInfo: {
170 172 avatar: DeviceStep1Ref.value?.devicePic,
171   - ...DeviceStep1Ref.value?.positionState,
  173 + ...DeviceStep1Ref.value?.devicePositionState,
172 174 },
173 175 deviceToken:
174 176 unref(current) === 0 || !unref(stepState).addAgree
... ...
... ... @@ -25,6 +25,7 @@
25 25 <template #iconSelect>
26 26 <Upload
27 27 name="avatar"
  28 + accept=".png,.jpg,.jpeg,.gif"
28 29 :show-upload-list="false"
29 30 list-type="picture-card"
30 31 class="avatar-uploader"
... ... @@ -46,7 +47,7 @@
46 47 </div>
47 48 </template>
48 49 <template #deviceAddress>
49   - <Input disabled v-model:value="positionState.address">
  50 + <Input disabled v-model:value="devicePositionState.address">
50 51 <template #addonAfter>
51 52 <EnvironmentTwoTone @click="selectPosition" />
52 53 </template>
... ... @@ -128,6 +129,7 @@
128 129 import { useDrawer } from '/@/components/Drawer';
129 130 import DeptDrawer from '/@/views/system/organization/OrganizationDrawer.vue';
130 131 import { TaskTypeEnum } from '/@/views/task/center/config';
  132 + import { toRaw } from 'vue';
131 133
132 134 export default defineComponent({
133 135 components: {
... ... @@ -239,8 +241,14 @@
239 241 const selectPosition = () => {
240 242 visible.value = true;
241 243 if (!positionState.longitude) {
242   - positionState.longitude = '104.04666605565338';
243   - positionState.latitude = '30.543516387560476';
  244 + positionState.longitude = '104.05326410962411';
  245 + positionState.latitude = '30.54855093076791';
  246 +
  247 + // 根据经纬度获取详细位置
  248 + if (positionState.longitude && positionState.latitude) {
  249 + var pt = new BMap.Point(positionState.longitude, positionState.latitude);
  250 + getAddrByPoint(pt);
  251 + }
244 252 initMap(positionState.longitude, positionState.latitude);
245 253 } else {
246 254 initMap(positionState.longitude, positionState.latitude);
... ... @@ -261,6 +269,15 @@
261 269 map: null,
262 270 marker: null,
263 271 });
  272 +
  273 + const devicePositionState = ref({
  274 + longitude: '',
  275 + latitude: '',
  276 + address: '',
  277 + map: null,
  278 + marker: null,
  279 + });
  280 +
264 281 /**
265 282 * 逆地址解析函数(根据坐标点获取详细地址)
266 283 * @param {Object} point 百度地图坐标点,必传
... ... @@ -371,6 +388,7 @@
371 388 // 确定选择的位置
372 389 const handleOk = () => {
373 390 visible.value = false;
  391 + devicePositionState.value = { ...toRaw(positionState) };
374 392 };
375 393 // 取消选择位置
376 394 const handleCancel = () => {
... ... @@ -385,6 +403,7 @@
385 403 positionState.longitude = deviceInfo.longitude;
386 404 positionState.latitude = deviceInfo.latitude;
387 405 positionState.address = deviceInfo.address;
  406 + devicePositionState.value = { ...toRaw(positionState) };
388 407 devicePic.value = deviceInfo.avatar;
389 408 setFieldsValue({
390 409 ...data,
... ... @@ -418,11 +437,13 @@
418 437 function parentResetDevicePic(deviceInfo) {
419 438 devicePic.value = deviceInfo.avatar;
420 439 }
  440 +
421 441 // 父组件重置位置
422 442 function parentResetPositionState() {
423 443 for (let key in positionState) {
424 444 positionState[key] = '';
425 445 }
  446 + devicePositionState.value = { ...toRaw(positionState) };
426 447 }
427 448 // 禁用设备类型
428 449 function disabledDeviceType(disabled: boolean) {
... ... @@ -474,6 +495,7 @@
474 495 handleOpenOrgDrawer,
475 496 handleSuccess,
476 497 handleTreeOrg,
  498 + devicePositionState,
477 499 };
478 500 },
479 501 });
... ...
... ... @@ -12,17 +12,31 @@
12 12 ]"
13 13 />
14 14 </template>
  15 + <template #details="{ record }">
  16 + <a-button type="link" class="ml-2" @click="handleViewAlarmDetails(record)">
  17 + 查看告警详情
  18 + </a-button>
  19 + </template>
15 20 </BasicTable>
16 21 <AlarmDetailModal @register="registerDetailModal" @success="handleSuccess" />
17 22 </div>
18 23 </template>
19 24 <script lang="ts">
20   - import { defineComponent } from 'vue';
  25 + import { defineComponent, h, nextTick } from 'vue';
21 26 import { BasicTable, useTable, TableAction } from '/@/components/Table';
22 27 import { alarmColumns, alarmSearchSchemas } from '../../config/detail.config';
23 28 import { getDeviceAlarm } from '/@/api/device/deviceManager';
24 29 import AlarmDetailModal from '../modal/AlarmDetailModal.vue';
25 30 import { useModal } from '/@/components/Modal';
  31 + import { Modal } from 'ant-design-vue';
  32 + import { JsonPreview } from '/@/components/CodeEditor';
  33 + import { getDeviceDetail } from '/@/api/device/deviceManager';
  34 + import { getAttribute } from '/@/api/ruleengine/ruleengineApi';
  35 + import {
  36 + operationNumber_OR_TIME,
  37 + operationString,
  38 + operationBoolean,
  39 + } from '/@/views/rule/linkedge/config/formatData';
26 40 export default defineComponent({
27 41 name: 'DeviceManagement',
28 42 components: {
... ... @@ -66,11 +80,87 @@
66 80 const handleSuccess = () => {
67 81 reload();
68 82 };
  83 + const handleViewAlarmDetails = async (record: Recordable) => {
  84 + await nextTick();
  85 + const { details } = record;
  86 + const deviceIdKeys = Object.keys(details);
  87 + const detailObject = deviceIdKeys.map((key) => ({ label: key, value: details[key] }));
  88 + const dataFormat = await handleAlarmDetailFormat(deviceIdKeys);
  89 + const dataFormats = detailObject.reduce((acc: any, curr: any) => {
  90 + dataFormat.forEach((item) => {
  91 + if (item.tbDeviceId === curr.label) {
  92 + const findName = item.attribute.find(
  93 + (item) => item.identifier === curr.value.key
  94 + )?.name;
  95 + const findLogin = [
  96 + ...operationNumber_OR_TIME,
  97 + ...operationString,
  98 + ...operationBoolean,
  99 + ].find((item) => item.value === curr.value.logic)?.symbol;
  100 + const findAttribute = item.attribute.find(
  101 + (findItem) => findItem.identifier === curr.value.key
  102 + );
  103 + const value = {
  104 + ['触发属性']: findName,
  105 + ['触发条件']: `${findLogin}${curr.value.logicValue}`,
  106 + ['触发值']: `${curr.value.realValue}${
  107 + findAttribute.detail?.dataType?.specs?.unit?.key ?? ''
  108 + }`,
  109 + };
  110 + const data = {
  111 + [item.name]: value,
  112 + };
  113 + acc.push(data);
  114 + }
  115 + });
  116 + return [...acc];
  117 + }, []);
  118 + const objectFormat = dataFormats.reduce((acc: any, curr: any) => {
  119 + return {
  120 + ...acc,
  121 + ...curr,
  122 + };
  123 + }, {});
  124 + Modal.info({
  125 + title: '告警详情',
  126 + width: 600,
  127 + centered: true,
  128 + maskClosable: true,
  129 + content: h(JsonPreview, { data: JSON.parse(JSON.stringify(objectFormat)) }),
  130 + });
  131 + };
  132 + const handleAlarmDetailFormat = async (keys: string[]) => {
  133 + const temp: any = [];
  134 + for (let item of keys) {
  135 + if (item === 'key' || item === 'data') return []; //旧数据则终止
  136 + const deviceDetailRes = await getDeviceDetail(item);
  137 + const { deviceProfileId } = deviceDetailRes;
  138 + if (!deviceProfileId) return [];
  139 + const attributeRes = await getAttribute(deviceProfileId);
  140 + const dataFormat: any = handleDataFormat(deviceDetailRes, attributeRes);
  141 + temp.push(dataFormat);
  142 + }
  143 + return temp;
  144 + };
  145 + const handleDataFormat = (deviceDetail: any, attributes: any) => {
  146 + const { name, alias, tbDeviceId } = deviceDetail;
  147 + const attribute = attributes.map((item) => ({
  148 + identifier: item.identifier,
  149 + name: item.name,
  150 + detail: item.detail,
  151 + }));
  152 + return {
  153 + name: alias || name,
  154 + tbDeviceId,
  155 + attribute,
  156 + };
  157 + };
69 158 return {
70 159 registerTable,
71 160 registerDetailModal,
72 161 handleDetail,
73 162 handleSuccess,
  163 + handleViewAlarmDetails,
74 164 };
75 165 },
76 166 });
... ...
... ... @@ -10,7 +10,7 @@
10 10 </div>
11 11 </template>
12 12 <script lang="ts">
13   - import { defineComponent, ref } from 'vue';
  13 + import { defineComponent, nextTick, ref } from 'vue';
14 14 import { BasicForm, useForm } from '/@/components/Form';
15 15 import { CommandFieldsEnum, CommandSchemas, CommandType, ValueType } from '../../config/data';
16 16 import { commandIssuanceApi } from '/@/api/device/deviceManager';
... ... @@ -35,7 +35,7 @@
35 35 const { createMessage } = useMessage();
36 36 const loading = ref(false);
37 37
38   - const [registerForm, { getFieldsValue, validate, resetFields }] = useForm({
  38 + const [registerForm, { getFieldsValue, validate, resetFields, clearValidate }] = useForm({
39 39 labelWidth: 120,
40 40 schemas: CommandSchemas(
41 41 props.deviceDetail.deviceProfile.transportType as TransportTypeEnum,
... ... @@ -47,8 +47,10 @@
47 47 showResetButton: false,
48 48 });
49 49
50   - const handleCancel = () => {
51   - resetFields();
  50 + const handleCancel = async () => {
  51 + await resetFields();
  52 + await nextTick();
  53 + await clearValidate();
52 54 };
53 55
54 56 const handleOk = async () => {
... ...