Commit fa8080ad9f58db95254f70f2d92f680ba578bdd7

Authored by xp.Huang
2 parents 81a1c4fd 09f45ebb

Merge branch 'ww' into 'main'

fix: BUG in teambition && implement data component map comoponent && implement data component control comoponent

See merge request huang/yun-teng-iot-front!407
Showing 40 changed files with 1114 additions and 656 deletions
... ... @@ -33,3 +33,4 @@ pnpm-debug.log*
33 33 *.sw?
34 34
35 35 yarn.lock
  36 +.vscode
... ...
... ... @@ -5,6 +5,7 @@
5 5 "public/resource/tinymce/langs"
6 6 ],
7 7 "cSpell.words": [
  8 + "inited",
8 9 "unref",
9 10 "VITE"
10 11 ]
... ...
  1 +import { HistoryData } from './model';
1 2 import { defHttp } from '/@/utils/http/axios';
2 3
3 4 // 获取设备配置
... ... @@ -9,7 +10,7 @@ export const getDeviceProfile = () => {
9 10
10 11 // 获取历史数据
11 12 export const getDeviceHistoryInfo = (params) => {
12   - return defHttp.get(
  13 + return defHttp.get<HistoryData>(
13 14 {
14 15 url: `/plugins/telemetry/DEVICE/${params.entityId}/values/timeseries`,
15 16 params: { ...params, entityId: null, orderBy: 'ASC' },
... ...
  1 +export interface HistoryData {
  2 + [key: string]: { ts: number; value: string }[];
  3 +}
... ...
... ... @@ -4,9 +4,11 @@ import {
4 4 ComponentInfoDetail,
5 5 DataBoardList,
6 6 DataComponentRecord,
  7 + DeviceAttributeParams,
  8 + DeviceAttributeRecord,
7 9 GetDataBoardParams,
8   - Layout,
9 10 MasterDeviceList,
  11 + SendCommandParams,
10 12 UpdateDataBoardLayoutParams,
11 13 UpdateDataBoardParams,
12 14 UpdateDataComponentParams,
... ... @@ -32,10 +34,14 @@ enum DataBoardShareUrl {
32 34 GET_DATA_COMPONENT = '/noauth/share/data_board',
33 35 }
34 36
  37 +enum SendCommand {
  38 + ONEWAY = '/plugins/rpc/oneway',
  39 +}
  40 +
35 41 enum DeviceUrl {
36 42 GET_DEVICE = '/device/list/master',
37 43 GET_SLAVE_DEVICE = '/device/list/slave',
38   - GET_DEVICE_ATTRIBUTE = '/plugins/telemetry',
  44 + GET_DEVICE_ATTRIBUTE = '/device/attributes',
39 45 }
40 46
41 47 /**
... ... @@ -184,14 +190,22 @@ export const getGatewaySlaveDevice = (params: { organizationId: string; masterId
184 190 * @param params
185 191 * @returns
186 192 */
187   -export const getDeviceAttributes = (params: { entityType?: string; deviceId: string }) => {
188   - const { entityType = 'DEVICE', deviceId } = params;
189   - return defHttp.get<string[]>(
190   - {
191   - url: `${DeviceUrl.GET_DEVICE_ATTRIBUTE}/${entityType}/${deviceId}/keys/timeseries`,
  193 +export const getDeviceAttributes = (params: DeviceAttributeParams) => {
  194 + const { deviceProfileId, dataType } = params;
  195 + return defHttp.get<DeviceAttributeRecord[]>({
  196 + url: `${DeviceUrl.GET_DEVICE_ATTRIBUTE}/${deviceProfileId}`,
  197 + params: {
  198 + dataType,
192 199 },
  200 + });
  201 +};
  202 +
  203 +export const sendCommandOneway = (params: SendCommandParams) => {
  204 + return defHttp.post(
193 205 {
194   - joinPrefix: false,
195   - }
  206 + url: `${SendCommand.ONEWAY}/${params.deviceId}`,
  207 + params: params.value,
  208 + },
  209 + { joinPrefix: false }
196 210 );
197 211 };
... ...
... ... @@ -133,10 +133,31 @@ export interface UpdateDataComponentParams {
133 133
134 134 export interface MasterDeviceList {
135 135 deviceType: 'DIRECT_CONNECTION' | 'GATEWAY';
  136 + deviceProfileId: string;
136 137 id: string;
137 138 name: string;
  139 + label?: string;
  140 + value?: string;
138 141 }
139 142
140 143 export interface ComponentInfoDetail {
141 144 data: { componentData: DataComponentRecord[]; componentLayout: Layout[] };
142 145 }
  146 +
  147 +export type DataType = 'BOOL' | 'DOUBLE' | 'INT' | 'STRUCT' | 'TExT';
  148 +
  149 +export interface DeviceAttributeParams {
  150 + deviceProfileId: string;
  151 + dataType?: DataType;
  152 +}
  153 +
  154 +export interface DeviceAttributeRecord {
  155 + name: string;
  156 + identifier: string;
  157 + detail: DataType;
  158 +}
  159 +
  160 +export interface SendCommandParams {
  161 + deviceId: string;
  162 + value: any;
  163 +}
... ...
... ... @@ -34,6 +34,7 @@
34 34 let { width = 300, height = 150 } = unref(getOptions);
35 35 width = isNumber(width) ? (`${width}px` as unknown as number) : width;
36 36 height = isNumber(height) ? (`${height}px` as unknown as number) : height;
  37 + console.log({ getOptions });
37 38 return { width, height } as CSSProperties;
38 39 });
39 40
... ...
... ... @@ -10,7 +10,9 @@
10 10 :showOkBtn="false"
11 11 @cancel="handleCancel"
12 12 >
13   - <div class="flex items-center justify-center bg-dark-900 w-full h-full min-h-52">
  13 + <div
  14 + class="flex items-center justify-center bg-dark-900 w-full h-full min-h-52 video-container"
  15 + >
14 16 <BasicVideoPlay v-if="showVideo" :options="options" />
15 17 </div>
16 18 </BasicModal>
... ... @@ -63,3 +65,13 @@
63 65 showVideo.value = false;
64 66 };
65 67 </script>
  68 +
  69 +<style lang="less" scoped>
  70 + .video-container:deep(.vben-basic-video-play) {
  71 + min-height: 13rem;
  72 + }
  73 +
  74 + .video-container:deep(.video-js) {
  75 + min-height: 13rem;
  76 + }
  77 +</style>
... ...
... ... @@ -61,6 +61,9 @@
61 61
62 62 for (const item of items) {
63 63 (item as CameraRecordItem).isTransform = false;
  64 + (item as CameraRecordItem).videoPlayerOptions = {
  65 + ...basicVideoPlayOptions,
  66 + };
64 67 beforeVideoPlay(item);
65 68 }
66 69 if (items.length < pagination.pageSize) {
... ... @@ -117,6 +120,7 @@
117 120 const index = unref(cameraList).findIndex((item) => item.id === record.id);
118 121 if (~index) unref(cameraList)[index].isTransform = true;
119 122 }
  123 + console.log(unref(cameraList));
120 124 }
121 125 };
122 126
... ...
... ... @@ -162,7 +162,7 @@
162 162 } else {
163 163 isNull.value = true;
164 164 }
165   - setChartOptions(res);
  165 + setChartOptions(res, props.attr);
166 166 };
167 167
168 168 onMounted(async () => {
... ...
... ... @@ -479,7 +479,7 @@ export const selectDeviceAttrSchema: FormSchema[] = [
479 479 },
480 480 ];
481 481
482   -export const eChartOptions = (series, keys): EChartsOption => {
  482 +export const eChartOptions = (series: EChartsOption['series'], keys: string[]): EChartsOption => {
483 483 return {
484 484 tooltip: {
485 485 trigger: 'axis',
... ...
... ... @@ -57,7 +57,14 @@
57 57 v-show="!isNull"
58 58 />
59 59 </BasicModal>
60   - <DeviceDetailDrawer @register="registerDetailDrawer" />
  60 + <DeviceDetailDrawer
  61 + @register="registerDetailDrawer"
  62 + @open-tb-device-detail="handleOpenTbDeviceDetail"
  63 + @open-gateway-device-detail="handleOpenGatewayDetail"
  64 + />
  65 + <DeviceDetailDrawer @register="registerTbDetailDrawer" />
  66 +
  67 + <DeviceDetailDrawer @register="registerGatewayDetailDrawer" />
61 68 </div>
62 69 </template>
63 70 <script lang="ts">
... ... @@ -94,6 +101,7 @@
94 101 import { QueryWay, SchemaFiled, AggregateDataEnum } from './cpns/TimePeriodForm/config';
95 102 import { dateUtil } from '/@/utils/dateUtil';
96 103 import { Spin } from 'ant-design-vue';
  104 + import { useAsyncQueue } from './useAsyncQueue';
97 105
98 106 interface DeviceInfo {
99 107 alarmStatus: 0 | 1;
... ... @@ -139,6 +147,8 @@
139 147 const isNull = ref(true);
140 148 const { toPromise } = useScript({ src: BAI_DU_MAP_URL });
141 149 const [registerDetailDrawer, { openDrawer }] = useDrawer();
  150 + const [registerTbDetailDrawer, { openDrawer: openTbDeviceDrawer }] = useDrawer();
  151 + const [registerGatewayDetailDrawer, { openDrawer: openGatewayDetailDrawer }] = useDrawer();
142 152 const [registerModal, { openModal }] = useModal();
143 153 const BMapInstance = ref<Nullable<any>>(null);
144 154
... ... @@ -159,15 +169,31 @@
159 169 showSizeChanger: true,
160 170 },
161 171 afterFetch: async (data: DeviceInfo[]) => {
  172 + if (!(window as any).BMap) {
  173 + clearTask();
  174 + setTask(createMarket.bind(null, data));
  175 + return data;
  176 + }
162 177 createMarket(data);
163 178 return data;
164 179 },
165 180 });
  181 + const { setTask, clearTask, executeFlag } = useAsyncQueue();
  182 +
  183 + let interval: Nullable<NodeJS.Timer> = setInterval(() => {
  184 + if ((window as any).BMap) {
  185 + executeFlag.value = true;
  186 + clearInterval(interval!);
  187 + interval = null;
  188 + }
  189 + }, 1000);
166 190
167 191 async function createMarket(data: DeviceInfo[]) {
168 192 const BMap = (window as any).BMap;
169   - if (!BMap) return;
170   - unref(BMapInstance).clearOverlays();
  193 + if (!BMap) {
  194 + return;
  195 + }
  196 + unref(BMapInstance)?.clearOverlays();
171 197 const markerList: MarkerList[] = [];
172 198 data.forEach((item) => {
173 199 const {
... ... @@ -242,6 +268,7 @@
242 268 };
243 269 const { name, organizationDTO, deviceState, deviceProfile, deviceType } = record;
244 270 const { address, longitude, latitude } = record.deviceInfo;
  271 +
245 272 // 创建信息窗口对象
246 273 const res = await getDeviceActiveTime(entityId);
247 274
... ... @@ -454,6 +481,14 @@
454 481 (window as any).openHistoryModal = null;
455 482 });
456 483
  484 + const handleOpenTbDeviceDetail = (data: { id: string; tbDeviceId: string }) => {
  485 + openTbDeviceDrawer(true, data);
  486 + };
  487 +
  488 + const handleOpenGatewayDetail = (data: { id: string; tbDeviceId: string }) => {
  489 + openGatewayDetailDrawer(true, data);
  490 + };
  491 +
457 492 return {
458 493 wrapRef,
459 494 registerTable,
... ... @@ -467,6 +502,10 @@
467 502 spinning,
468 503 timePeriodRegister,
469 504 handleCancelModal,
  505 + registerTbDetailDrawer,
  506 + registerGatewayDetailDrawer,
  507 + handleOpenTbDeviceDetail,
  508 + handleOpenGatewayDetail,
470 509 };
471 510 },
472 511 });
... ...
  1 +import { ref, watchEffect } from 'vue';
  2 +
  3 +export function useAsyncQueue() {
  4 + const queue: Fn<any, Promise<any>>[] = [];
  5 +
  6 + const executeFlag = ref(false);
  7 +
  8 + const setTask = (fn: Fn<any, Promise<any>>) => {
  9 + queue.push(fn);
  10 + };
  11 +
  12 + const removeTask = (fn: Fn<any, Promise<any>>) => {
  13 + const index = queue.findIndex((item) => item === fn);
  14 + ~index && queue.splice(index, 1);
  15 + };
  16 +
  17 + const clearTask = () => {
  18 + queue.length = 0;
  19 + };
  20 +
  21 + watchEffect(() => {
  22 + if (executeFlag.value) {
  23 + queue.forEach((item) => item());
  24 + executeFlag.value = false;
  25 + }
  26 + });
  27 +
  28 + return { setTask, removeTask, clearTask, executeFlag };
  29 +}
... ...
... ... @@ -28,7 +28,7 @@
28 28 import DeviceConfigurationStep from './step/DeviceConfigurationStep.vue';
29 29 import TransportConfigurationStep from './step/TransportConfigurationStep.vue';
30 30 import PhysicalModelManagementStep from './step/PhysicalModelManagementStep.vue';
31   - import { ref, unref } from 'vue';
  31 + import { nextTick, ref, unref } from 'vue';
32 32 import { deviceConfigGetDetail } from '/@/api/device/deviceConfigApi';
33 33 import { DeviceRecord } from '/@/api/device/model/deviceModel';
34 34 import TopicPanel from './step/TopicPanel.vue';
... ... @@ -49,6 +49,7 @@
49 49 unref(DevConStRef)?.setFormData(res);
50 50 };
51 51 const setTransConfFormData = async (res: Recordable) => {
  52 + await nextTick();
52 53 unref(TransConStRef)?.setFormData(res);
53 54 };
54 55
... ...
... ... @@ -9,6 +9,13 @@ import { FormField, FunctionType } from './step/cpns/physical/cpns/config';
9 9 import { h } from 'vue';
10 10 import { Tag } from 'ant-design-vue';
11 11
  12 +export enum ModelOfMatterPermission {
  13 + CREATE = 'api:yt:things_model:post',
  14 + UPDATE = 'api:yt:things_model:put',
  15 + DELETE = 'api:yt:things_model:delete',
  16 + RELEASE = 'api:yt:things_model:release',
  17 +}
  18 +
12 19 export const steps = [
13 20 {
14 21 title: '产品',
... ...
... ... @@ -27,18 +27,16 @@
27 27 </div>
28 28 <div class="flex justify-between items-end">
29 29 <div class="flex gap-2">
30   - <Authority value="">
  30 + <Authority :value="ModelOfMatterPermission.CREATE">
31 31 <Button v-if="isShowBtn" type="primary" @click="handleCreateOrEdit()">
32 32 新增物模型
33 33 </Button>
34   - <Button type="primary" @click="handleOpenTsl"> 物模型TSL </Button>
35   - <Button v-if="isShowBtn" type="primary" @click="handleImportModel"
36   - >导入物模型</Button
37   - >
38 34 </Authority>
  35 + <Button type="primary" @click="handleOpenTsl"> 物模型TSL </Button>
  36 + <Button v-if="isShowBtn" type="primary" @click="handleImportModel">导入物模型</Button>
39 37 </div>
40 38 <div class="flex gap-2">
41   - <Authority value="">
  39 + <Authority :value="ModelOfMatterPermission.RELEASE">
42 40 <Popconfirm
43 41 title="是否需要发布上线?"
44 42 ok-text="确定"
... ... @@ -49,9 +47,12 @@
49 47 发布上线
50 48 </Button>
51 49 </Popconfirm>
52   - <Button v-if="isShowBtn" class="!bg-gray-200" type="text" @click="handleReturn">
53   - 返回
54   - </Button>
  50 + </Authority>
  51 +
  52 + <Button v-if="isShowBtn" class="!bg-gray-200" type="text" @click="handleReturn">
  53 + 返回
  54 + </Button>
  55 + <Authority :value="ModelOfMatterPermission.DELETE">
55 56 <Popconfirm
56 57 title="您确定要批量删除数据"
57 58 ok-text="确定"
... ... @@ -84,14 +85,14 @@
84 85 {
85 86 label: '编辑',
86 87 icon: 'clarity:note-edit-line',
87   - auth: '',
  88 + auth: ModelOfMatterPermission.UPDATE,
88 89 onClick: handleCreateOrEdit.bind(null, record),
89 90 ifShow: !isShowBtn ? false : true,
90 91 },
91 92 {
92 93 label: '删除',
93 94 icon: 'ant-design:delete-outlined',
94   - auth: '',
  95 + auth: ModelOfMatterPermission.DELETE,
95 96 color: 'error',
96 97 ifShow: !isShowBtn ? false : true,
97 98 popConfirm: {
... ... @@ -114,7 +115,11 @@
114 115 <script lang="ts" setup>
115 116 import { BasicTable, useTable, TableAction } from '/@/components/Table';
116 117 import { useModal } from '/@/components/Modal';
117   - import { modelOfMatterForm, physicalColumn } from '../device.profile.data';
  118 + import {
  119 + modelOfMatterForm,
  120 + ModelOfMatterPermission,
  121 + physicalColumn,
  122 + } from '../device.profile.data';
118 123 import { useBatchDelete } from '/@/hooks/web/useBatchDelete';
119 124 import { Authority } from '/@/components/Authority';
120 125 import PhysicalModelModal from './cpns/physical/PhysicalModelModal.vue';
... ...
... ... @@ -78,11 +78,12 @@
78 78 submitOnReset: false,
79 79 showActionButtonGroup: props.ifShowBtn ? true : false,
80 80 });
81   - const setFormData = (v) => {
  81 + const setFormData = async (v) => {
82 82 setFieldsValue({
83 83 transportType: v?.profileData?.transportConfiguration?.type,
84 84 });
85 85 isMqttType.value = v?.profileData?.transportConfiguration?.type;
  86 + await nextTick();
86 87 mqttRef.value?.setFormData(v?.profileData?.transportConfiguration);
87 88 coapRef.value?.setFormData(v?.profileData?.transportConfiguration);
88 89 lwm2mRef.value?.setFormData(v?.profileData?.transportConfiguration);
... ...
... ... @@ -5,8 +5,10 @@
5 5 </script>
6 6 <script lang="ts" setup>
7 7 import { RadioRecord } from '../../detail/config/util';
  8 + import { ControlComponentDefaultConfig, ControlComponentValue } from './control.config';
  9 + import { useSendCommand } from './useSendCommand';
8 10
9   - interface VisualComponentProps<Layout = Recordable, Value = Recordable> {
  11 + interface VisualComponentProps<Layout = Recordable, Value = ControlComponentValue> {
10 12 value?: Value;
11 13 layout?: Layout;
12 14 radio?: RadioRecord;
... ... @@ -15,29 +17,40 @@
15 17 update?: () => void;
16 18 remove?: (key: string) => void;
17 19 }
  20 +
18 21 const props = defineProps<VisualComponentProps>();
19 22
20 23 const emit = defineEmits(['update:value', 'change']);
21 24
  25 + const { sendCommand } = useSendCommand();
22 26 const handleChange = (event: Event) => {
23 27 const _value = (event.target as HTMLInputElement).checked;
24 28 emit('update:value', _value);
25 29 emit('change', _value);
  30 + sendCommand(props.value?.slaveDeviceId || props.value?.deviceId, _value);
26 31 };
27 32 </script>
28 33
29 34 <template>
30   - <label class="sliding-switch">
31   - <input
32   - :value="props.value?.value"
33   - type="checkbox"
34   - :checked="props.value?.value"
35   - @change="handleChange"
36   - />
37   - <span class="slider"></span>
38   - <span class="on">ON</span>
39   - <span class="off">OFF</span>
40   - </label>
  35 + <div class="flex flex-col justify-center">
  36 + <label class="sliding-switch">
  37 + <input
  38 + :value="props.value?.value"
  39 + type="checkbox"
  40 + :checked="props.value?.value"
  41 + @change="handleChange"
  42 + />
  43 + <span class="slider"></span>
  44 + <span class="on">ON</span>
  45 + <span class="off">OFF</span>
  46 + </label>
  47 + <div
  48 + class="text-center mt-2 text-gray-700"
  49 + :style="{ color: props?.value?.fontColor || ControlComponentDefaultConfig.fontColor }"
  50 + >
  51 + {{ props.value?.attributeRename || props.value?.attribute }}</div
  52 + >
  53 + </div>
41 54 </template>
42 55
43 56 <style scoped lang="less">
... ...
... ... @@ -5,7 +5,7 @@
5 5 </script>
6 6 <script lang="ts" setup>
7 7 import { Switch } from 'ant-design-vue';
8   - import { computed } from 'vue';
  8 + import { computed, ref, watchEffect } from 'vue';
9 9 import { DEFAULT_RADIO_RECORD, fontSize, RadioRecord } from '../../detail/config/util';
10 10 import SvgIcon from '/@/components/Icon/src/SvgIcon.vue';
11 11 import {
... ... @@ -13,6 +13,7 @@
13 13 ControlComponentValue,
14 14 ControlComponentLayout,
15 15 } from './control.config';
  16 + import { useSendCommand } from './useSendCommand';
16 17 const props = withDefaults(
17 18 defineProps<{
18 19 layout?: ControlComponentLayout;
... ... @@ -26,6 +27,18 @@
26 27 const getRadio = computed(() => {
27 28 return props.radio || DEFAULT_RADIO_RECORD;
28 29 });
  30 +
  31 + const checked = ref(!!Number(props.value.value));
  32 +
  33 + const { sendCommand } = useSendCommand();
  34 + const handleChange = (value: boolean) => {
  35 + console.log(props.value);
  36 + sendCommand(props.value.slaveDeviceId! || props.value.deviceId!, value);
  37 + };
  38 +
  39 + watchEffect(() => {
  40 + checked.value = !!Number(props.value.value);
  41 + });
29 42 </script>
30 43
31 44 <template>
... ... @@ -40,8 +53,13 @@
40 53 height: fontSize({ radioRecord: getRadio, basic: 30, min: 16 }),
41 54 }"
42 55 />
43   - <span class="flex-auto mx-4 flex items-center truncate inline-block">属性名</span>
  56 + <span
  57 + class="flex-auto mx-4 flex items-center truncate inline-block text-gray-700"
  58 + :style="{ color: props.value.fontColor || ControlComponentDefaultConfig.fontColor }"
  59 + >
  60 + {{ props.value.attributeRename || props.value.attribute }}
  61 + </span>
44 62 </div>
45   - <Switch />
  63 + <Switch v-model:checked="checked" @change="handleChange" />
46 64 </div>
47 65 </template>
... ...
... ... @@ -6,7 +6,8 @@
6 6 <script lang="ts" setup>
7 7 import { computed } from '@vue/reactivity';
8 8 import { DEFAULT_RADIO_RECORD, fontSize, RadioRecord } from '../../detail/config/util';
9   - import { ControlComponentValue } from './control.config';
  9 + import { ControlComponentDefaultConfig, ControlComponentValue } from './control.config';
  10 + import { useSendCommand } from './useSendCommand';
10 11
11 12 const props = defineProps<{
12 13 value?: ControlComponentValue;
... ... @@ -16,10 +17,12 @@
16 17
17 18 const emit = defineEmits(['update:value', 'change']);
18 19
  20 + const { sendCommand } = useSendCommand();
19 21 const handleChange = (event: Event) => {
20 22 const _value = (event.target as HTMLInputElement).checked;
21 23 emit('update:value', _value);
22 24 emit('change', _value);
  25 + sendCommand(props.value?.slaveDeviceId || props.value?.deviceId, _value);
23 26 };
24 27
25 28 const getRadio = computed(() => {
... ... @@ -28,28 +31,36 @@
28 31 </script>
29 32
30 33 <template>
31   - <div
32   - class="toggle-switch"
33   - :style="{
34   - width: fontSize({ radioRecord: getRadio, basic: 75, max: 75, min: 60 }),
35   - height: fontSize({ radioRecord: getRadio, basic: 97.5, max: 97.5, min: 80 }),
36   - }"
37   - >
38   - <label class="switch">
39   - <input
40   - :value="props.value?.value"
41   - type="checkbox"
42   - :checked="props.value?.value"
43   - @change="handleChange"
44   - />
45   - <div class="button">
46   - <div class="light"></div>
47   - <div class="dots"></div>
48   - <div class="characters"></div>
49   - <div class="shine"></div>
50   - <div class="shadow"></div>
51   - </div>
52   - </label>
  34 + <div class="flex flex-col">
  35 + <div
  36 + class="toggle-switch"
  37 + :style="{
  38 + width: fontSize({ radioRecord: getRadio, basic: 75, max: 75, min: 60 }),
  39 + height: fontSize({ radioRecord: getRadio, basic: 97.5, max: 97.5, min: 80 }),
  40 + }"
  41 + >
  42 + <label class="switch">
  43 + <input
  44 + :value="props.value?.value"
  45 + type="checkbox"
  46 + :checked="props.value?.value"
  47 + @change="handleChange"
  48 + />
  49 + <div class="button">
  50 + <div class="light"></div>
  51 + <div class="dots"></div>
  52 + <div class="characters"></div>
  53 + <div class="shine"></div>
  54 + <div class="shadow"></div>
  55 + </div>
  56 + </label>
  57 + </div>
  58 + <div
  59 + class="text-center mt-2 text-gray-700"
  60 + :style="{ color: props?.value?.fontColor || ControlComponentDefaultConfig.fontColor }"
  61 + >
  62 + {{ props.value?.attributeRename || props.value?.attribute }}</div
  63 + >
53 64 </div>
54 65 </template>
55 66
... ...
... ... @@ -6,14 +6,19 @@ export interface ControlComponentLayout {
6 6
7 7 export interface ControlComponentValue {
8 8 value?: boolean;
9   - name?: string;
  9 + attribute?: string;
  10 + attributeRename?: string;
10 11 icon?: string;
11 12 iconColor?: string;
  13 + deviceId?: string;
  14 + fontColor?: string;
  15 + slaveDeviceId?: string;
12 16 }
13 17
14 18 export const ControlComponentDefaultConfig: ControlComponentValue = {
15 19 icon: 'shuiwen',
16 20 iconColor: '#367BFF',
  21 + fontColor: '#000',
17 22 };
18 23
19 24 export const transformControlConfig = (
... ... @@ -23,8 +28,11 @@ export const transformControlConfig = (
23 28 ) => {
24 29 return {
25 30 value: {
26   - value: dataSourceRecord.componentInfo.value,
27   - icon: dataSourceRecord.componentInfo.icon,
  31 + ...dataSourceRecord.componentInfo,
  32 + attribute: dataSourceRecord.attribute,
  33 + attributeRename: dataSourceRecord.attributeRename,
  34 + deviceId: dataSourceRecord.deviceId,
  35 + slaveDeviceId: dataSourceRecord.slaveDeviceId,
28 36 } as ControlComponentValue,
29 37 };
30 38 };
... ...
  1 +import { sendCommandOneway } from '/@/api/dataBoard';
  2 +import { useMessage } from '/@/hooks/web/useMessage';
  3 +
  4 +const { createMessage } = useMessage();
  5 +export function useSendCommand() {
  6 + const sendCommand = async (deviceId: string, value: any) => {
  7 + if (!deviceId) return;
  8 + try {
  9 + await sendCommandOneway({
  10 + deviceId,
  11 + value: {
  12 + params: Number(value),
  13 + persistent: true,
  14 + additionalInfo: {
  15 + cmdType: 'API',
  16 + },
  17 + method: 'methodThingskit',
  18 + },
  19 + });
  20 + createMessage.success('命令下发成功');
  21 + } catch (error) {}
  22 + };
  23 + return {
  24 + sendCommand,
  25 + };
  26 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { BasicForm, useForm } from '/@/components/Form';
  3 + import { BasicModal, useModalInner } from '/@/components/Modal';
  4 + import {
  5 + formSchema,
  6 + getHistorySearchParams,
  7 + SchemaFiled,
  8 + } from '../../../board/detail/config/historyTrend.config';
  9 + import { HistoryModalOkEmitParams, HistoryModalParams } from './type';
  10 + import { DataSource } from '/@/api/dataBoard/model';
  11 + import { ref } from 'vue';
  12 + import { getAllDeviceByOrg } from '/@/api/dataBoard';
  13 + import { getDeviceHistoryInfo } from '/@/api/alarm/position';
  14 +
  15 + const emit = defineEmits(['register', 'ok']);
  16 +
  17 + const [registerForm, { updateSchema, setFieldsValue, validate, getFieldsValue }] = useForm({
  18 + schemas: formSchema,
  19 + showActionButtonGroup: false,
  20 + fieldMapToTime: [
  21 + [SchemaFiled.DATE_RANGE, [SchemaFiled.START_TS, SchemaFiled.END_TS], 'YYYY-MM-DD HH:ss'],
  22 + ],
  23 + });
  24 +
  25 + const [registerModal, { closeModal }] = useModalInner(async (params: HistoryModalParams) => {
  26 + try {
  27 + const { dataSource = [] } = params;
  28 + const deviceRecord = dataSource?.at(0) || ({} as DataSource);
  29 + if (!deviceRecord.organizationId) return;
  30 + const deviceList = await getAllDeviceByOrg(deviceRecord.organizationId);
  31 + const options = deviceList
  32 + .filter((item) => item.id === deviceRecord.deviceId)
  33 + .map((item) => ({ ...item, label: item.name, value: item.id }));
  34 + const attKey = dataSource.map((item) => ({
  35 + ...item,
  36 + label: item.attribute,
  37 + value: item.attribute,
  38 + }));
  39 + updateSchema([
  40 + {
  41 + field: SchemaFiled.DEVICE_ID,
  42 + componentProps: {
  43 + options,
  44 + },
  45 + },
  46 + {
  47 + field: SchemaFiled.KEYS,
  48 + component: 'Select',
  49 + defaultValue: attKey.map((item) => item.value),
  50 + componentProps: {
  51 + options: attKey,
  52 + mode: 'multiple',
  53 + disabled: true,
  54 + },
  55 + },
  56 + ]);
  57 +
  58 + setFieldsValue({
  59 + [SchemaFiled.DEVICE_ID]: deviceRecord.deviceId,
  60 + [SchemaFiled.KEYS]: attKey.map((item) => item.value),
  61 + });
  62 + } catch (error) {
  63 + throw error;
  64 + }
  65 + });
  66 +
  67 + const loading = ref(false);
  68 + const handleOk = async () => {
  69 + try {
  70 + await validate();
  71 + let value = getFieldsValue();
  72 +
  73 + value = getHistorySearchParams(value);
  74 +
  75 + loading.value = true;
  76 +
  77 + const res = await getDeviceHistoryInfo({
  78 + ...value,
  79 + [SchemaFiled.KEYS]: value[SchemaFiled.KEYS].join(','),
  80 + });
  81 +
  82 + let timespanList = Object.keys(res).reduce((prev, next) => {
  83 + const ts = res[next].map((item) => item.ts);
  84 + return [...prev, ...ts];
  85 + }, [] as number[]);
  86 + timespanList = [...new Set(timespanList)];
  87 +
  88 + const track: Record<'lng' | 'lat', number>[] = [];
  89 + const keys = Object.keys(res);
  90 +
  91 + for (const ts of timespanList) {
  92 + const list: { ts: number; value: number }[] = [];
  93 + for (const key of keys) {
  94 + const record = res[key].find((item) => ts === item.ts);
  95 + list.push(record as any);
  96 + }
  97 + if (list.every(Boolean)) {
  98 + const lng = list.at(0)?.value;
  99 + const lat = list.at(1)?.value;
  100 + if (lng && lat) track.push({ lng, lat });
  101 + }
  102 + }
  103 + emit('ok', { track, value } as HistoryModalOkEmitParams);
  104 + closeModal();
  105 + } catch (error) {
  106 + throw error;
  107 + } finally {
  108 + loading.value = false;
  109 + }
  110 + };
  111 +</script>
  112 +
  113 +<template>
  114 + <BasicModal
  115 + title="历史轨迹"
  116 + @register="registerModal"
  117 + @ok="handleOk"
  118 + :ok-button-props="{ loading }"
  119 + >
  120 + <BasicForm @register="registerForm" />
  121 + </BasicModal>
  122 +</template>
... ...
1 1 <script lang="ts">
2 2 export default {
  3 + components: { Spin },
3 4 inheritAttrs: false,
4 5 };
5 6 </script>
6 7 <script lang="ts" setup>
7   - import { computed, onMounted, ref, unref } from 'vue';
  8 + import { computed, onMounted, reactive, ref, unref, watchEffect } from 'vue';
8 9 import { RadioRecord } from '../../detail/config/util';
9 10 import { MapComponentLayout, MapComponentValue } from './map.config';
10 11 import {
... ... @@ -12,11 +13,18 @@
12 13 PlayCircleOutlined,
13 14 PauseCircleOutlined,
14 15 } from '@ant-design/icons-vue';
15   - import { Button, Tooltip } from 'ant-design-vue';
  16 + import { Button, Spin, Tooltip } from 'ant-design-vue';
16 17 import { FrontComponent } from '../../const/const';
17 18 import { buildUUID } from '/@/utils/uuid';
  19 + import { useModal } from '/@/components/Modal';
  20 + import HistoryDataModel from './HistoryDataModel.vue';
  21 + import { HistoryModalOkEmitParams, HistoryModalParams } from './type';
  22 + import { formatToDateTime } from '/@/utils/dateUtil';
  23 + import { isEqual } from 'lodash-es';
  24 + import { useAsyncQueue } from '/@/views/device/localtion/useAsyncQueue';
18 25
19 26 // useVisualBoardContext();
  27 + type TrackRecord = Record<'lng' | 'lat' | 'ts', number>;
20 28
21 29 const startMethodName = `trackPlayMethod_${buildUUID()}`;
22 30
... ... @@ -44,19 +52,52 @@
44 52 const trackAni = ref<Nullable<any>>(null);
45 53 let mapInstance: Nullable<Recordable> = null;
46 54
  55 + const trackList = ref<TrackRecord[]>([]);
  56 +
  57 + watchEffect(() => {
  58 + if (
  59 + props.layout?.componentType === FrontComponent.MAP_COMPONENT_TRACK_REAL &&
  60 + props.value?.track?.length
  61 + ) {
  62 + const lng = props.value.track.at(0)!;
  63 + const lat = props.value.track.at(1)!;
  64 + const record = {
  65 + lng: lng.value,
  66 + lat: lat.value,
  67 + ts: lng.ts,
  68 + };
  69 + if (unref(trackList).length && isEqual(unref(trackList).at(-1), record)) return;
  70 + trackList.value.push(record);
  71 +
  72 + randomAnimation(unref(trackList));
  73 + // marketPoint(record);
  74 + }
  75 + });
  76 +
  77 + // function marketPoint(params: Record<'lng' | 'lat', number>) {
  78 + // const { lng, lat } = params;
  79 + // const BMap = (window as any).BMapGL;
  80 + // const marker = new BMap.Marker(new BMap.Point(lng, lat));
  81 + // unref(mapInstance)?.centerAndZoom(new BMap.Point(lng, lat));
  82 + // unref(mapInstance)?.addOverlay(marker);
  83 + // }
  84 +
  85 + const prepare = ref(false);
47 86 async function initMap() {
48 87 const wrapEl = unref(wrapRef);
49 88 if (!wrapEl) return;
50 89 const BMapGL = (window as any).BMapGL;
51 90 mapInstance = new BMapGL.Map(wrapId);
52   - const point = new BMapGL.Point(116.404, 39.915);
  91 + const point = new BMapGL.Point(104.09457, 30.53189);
53 92 mapInstance!.centerAndZoom(point, 15);
54 93 mapInstance!.enableScrollWheelZoom(true);
55   - props.layout?.componentType === FrontComponent.MAP_COMPONENT_TRACK_HISTORY && randomAnimation();
  94 + props.layout?.componentType === FrontComponent.MAP_COMPONENT_TRACK_HISTORY &&
  95 + props.random &&
  96 + randomAnimation();
56 97 }
57 98
58   - const randomAnimation = () => {
59   - const path = [
  99 + const randomAnimation = (path?: Record<'lng' | 'lat', number>[], clearOverlays = true) => {
  100 + path = path || [
60 101 {
61 102 lng: 116.297611,
62 103 lat: 40.047363,
... ... @@ -90,6 +131,8 @@
90 131 const point: any[] = [];
91 132 const BMapGL = (window as any).BMapGL;
92 133
  134 + clearOverlays && unref(mapInstance)?.clearOverlays();
  135 +
93 136 for (const { lng, lat } of path) {
94 137 point.push(new BMapGL.Point(lng, lat));
95 138 }
... ... @@ -99,10 +142,10 @@
99 142
100 143 const dynamicPlayMethod = {
101 144 [startMethodName]() {
102   - trackAni.value = new BMapGLLib.TrackAnimation(mapInstance, pl, {
  145 + trackAni.value = new BMapGLLib.TrackAnimation(unref(mapInstance), pl, {
103 146 overallView: true,
104 147 tilt: 30,
105   - duration: 20000,
  148 + duration: 5000,
106 149 delay: 300,
107 150 });
108 151 trackAni.value!.start();
... ... @@ -114,15 +157,43 @@
114 157 setTimeout(`${startMethodName}()`);
115 158 };
116 159
  160 + const { setTask, executeFlag } = useAsyncQueue();
117 161 onMounted(() => {
  162 + let interval: Nullable<NodeJS.Timer> = setInterval(() => {
  163 + if ((window as any).BMapGL) {
  164 + prepare.value = true;
  165 + executeFlag.value = true;
  166 + clearInterval(interval!);
  167 + interval = null;
  168 + }
  169 + }, 1000);
  170 + if (!(window as any).BMapGL) {
  171 + setTask(initMap);
  172 + return;
  173 + }
118 174 initMap();
119 175 });
120 176
  177 + const timeRange = reactive<Record<'start' | 'end', Nullable<number>>>({
  178 + start: null,
  179 + end: null,
  180 + });
121 181 const getTimeRange = computed(() => {
122   - return ` - 从 ${'2020-10-20 10:10:10'} 到 ${'2020-10-20 10:10:10'}`;
  182 + const { start, end } = timeRange;
  183 + if (!start || !end) return `- 请选择`;
  184 + return ` - 从 ${formatToDateTime(start, 'YYYY-MM-DD HH:mm:ss')} 到 ${formatToDateTime(
  185 + end,
  186 + 'YYYY-MM-DD HH:mm:ss'
  187 + )}`;
123 188 });
124 189
125   - const handleTrackSwitch = () => {};
  190 + const [register, { openModal }] = useModal();
  191 +
  192 + const handleTrackSwitch = () => {
  193 + openModal(true, {
  194 + dataSource: props.value?.dataSource || [],
  195 + } as HistoryModalParams);
  196 + };
126 197
127 198 const getTrackPlayStatus = computed(() => {
128 199 return (trackAni.value || {})._status;
... ... @@ -133,10 +204,17 @@
133 204 else if (unref(getTrackPlayStatus) === TrackAnimationStatus.PLAY) unref(trackAni).pause();
134 205 else if (unref(getTrackPlayStatus) === TrackAnimationStatus.PAUSE) unref(trackAni).continue();
135 206 };
  207 +
  208 + const handleRenderHistroyData = (params: HistoryModalOkEmitParams) => {
  209 + const { track, value } = params;
  210 + track.length && randomAnimation(track as unknown as Record<'lng' | 'lat', number>[]);
  211 + timeRange.start = value.startTs as number;
  212 + timeRange.end = value.endTs as number;
  213 + };
136 214 </script>
137 215
138 216 <template>
139   - <div class="w-full h-full flex justify-center items-center flex-col">
  217 + <div class="w-full h-full flex justify-center items-center flex-col p-2 no-drag">
140 218 <div
141 219 class="w-full flex"
142 220 v-if="props.layout?.componentType === FrontComponent.MAP_COMPONENT_TRACK_HISTORY"
... ... @@ -144,7 +222,7 @@
144 222 <Button type="text" class="!px-2 flex-auto !text-left truncate" @click="handleTrackSwitch">
145 223 <div class="w-full truncate text-gray-500 flex items-center">
146 224 <ClockCircleOutlined />
147   - <span class="mx-1">实时</span>
  225 + <span class="mx-1">历史</span>
148 226 <Tooltip :title="getTimeRange.replace('-', '')">
149 227 <span class="truncate">
150 228 {{ getTimeRange }}
... ... @@ -154,12 +232,17 @@
154 232 </Button>
155 233 <Button type="text" class="!px-2 !text-gray-500" @click="handlePlay">
156 234 <PlayCircleOutlined v-show="getTrackPlayStatus !== TrackAnimationStatus.PLAY" />
157   - <PauseCircleOutlined v-show="getTrackPlayStatus === TrackAnimationStatus.PLAY" />
  235 + <PauseCircleOutlined
  236 + class="!ml-0"
  237 + v-show="getTrackPlayStatus === TrackAnimationStatus.PLAY"
  238 + />
158 239 <span>
159 240 {{ getTrackPlayStatus !== TrackAnimationStatus.PLAY ? '播放轨迹' : '暂停播放' }}
160 241 </span>
161 242 </Button>
162 243 </div>
  244 + <Spin class="w-full h-full" :spinning="!prepare" tip="Loading..." />
163 245 <div ref="wrapRef" :id="wrapId" class="w-full h-full"></div>
  246 + <HistoryDataModel @register="register" @ok="handleRenderHistroyData" />
164 247 </div>
165 248 </template>
... ...
1 1 import { FrontComponent } from '../../const/const';
2 2 import { ComponentConfig } from '../../types/type';
  3 +import { DataSource } from '/@/api/dataBoard/model';
3 4
4 5 export interface MapComponentLayout {
5 6 componentType?: FrontComponent;
... ... @@ -7,7 +8,8 @@ export interface MapComponentLayout {
7 8
8 9 export interface MapComponentValue {
9 10 icon?: string;
10   - track?: Recordable[];
  11 + track?: Record<'ts' | 'value', number>[];
  12 + dataSource?: DataSource[];
11 13 }
12 14
13 15 interface Config {
... ... @@ -22,14 +24,39 @@ export const MapRealTrackConfig: Config = {
22 24 componentType: FrontComponent.MAP_COMPONENT_TRACK_REAL,
23 25 };
24 26
  27 +const getTrack = (dataSource: DataSource[], config: Config) => {
  28 + if (dataSource.length >= 2) {
  29 + const trackRecord = dataSource.slice(0, 2);
  30 + if (
  31 + !trackRecord.every((item) => item.componentInfo.value) &&
  32 + config.componentType === FrontComponent.MAP_COMPONENT_TRACK_REAL
  33 + )
  34 + return { track: [], dataSource: [] };
  35 +
  36 + const track = trackRecord.map((item) => {
  37 + return {
  38 + ts: item.componentInfo.updateTime,
  39 + value: item.componentInfo.value || 0,
  40 + };
  41 + });
  42 + return { track, dataSource };
  43 + }
  44 + return { track: [], dataSource: [] };
  45 +};
  46 +
25 47 export const transfromMapComponentConfig: ComponentConfig['transformConfig'] = (
26 48 componentConfig: Config,
27 49 _record,
28   - _dataSourceRecord
  50 + dataSourceRecord
29 51 ) => {
  52 + const { track, dataSource } = getTrack(dataSourceRecord as DataSource[], componentConfig);
30 53 return {
31 54 layout: {
32 55 ...componentConfig,
33   - },
  56 + } as MapComponentLayout,
  57 + value: {
  58 + track,
  59 + dataSource,
  60 + } as MapComponentValue,
34 61 };
35 62 };
... ...
  1 +import { SchemaFiled } from '../../detail/config/historyTrend.config';
  2 +import { DataSource } from '/@/api/dataBoard/model';
  3 +
  4 +export interface HistoryModalParams {
  5 + dataSource?: DataSource[];
  6 +}
  7 +
  8 +export interface HistoryModalOkEmitParams {
  9 + track: {
  10 + lng: number | string;
  11 + lat: number | string;
  12 + }[];
  13 + value: Record<SchemaFiled, string | number>;
  14 +}
... ...
... ... @@ -2,11 +2,22 @@
2 2 import { useUpdateCenter } from '../../hook/useUpdateCenter';
3 3 import { FrontDataSourceRecord } from '../../types/type';
4 4 import { createVisualBoardContext } from '../../hook/useVisualBoardContext';
  5 + import { DataComponentRecord } from '/@/api/dataBoard/model';
  6 + import { computed } from 'vue';
  7 + import { frontComponentMap } from '../help';
  8 + import { FrontComponent } from '../../const/const';
5 9
6 10 const props = defineProps<{
  11 + record: DataComponentRecord;
7 12 dataSource: FrontDataSourceRecord[];
8 13 }>();
9 14
  15 + const isMultipleDataSource = computed(() => {
  16 + const { record } = props;
  17 + const componentInfo = frontComponentMap.get(record.frontId as FrontComponent);
  18 + return componentInfo?.isMultipleDataSource;
  19 + });
  20 +
10 21 const { update, add, remove } = useUpdateCenter();
11 22
12 23 createVisualBoardContext({ update, add, remove });
... ... @@ -19,24 +30,41 @@
19 30 <slot name="header"></slot>
20 31
21 32 <div class="widget-content">
22   - <div
23   - v-for="item in props.dataSource"
24   - :key="item.id"
25   - :style="{ width: `${item.width || 100}%`, height: `${item.height || 100}%` }"
26   - class="widget-item"
27   - >
28   - <div class="widget-box">
29   - <div class="widget-controls-container">
30   - <slot
31   - name="controls"
32   - :record="item"
33   - :add="add"
34   - :remove="remove"
35   - :update="update"
36   - ></slot>
  33 + <template v-if="!isMultipleDataSource">
  34 + <div
  35 + v-for="item in props.dataSource"
  36 + :key="item.id"
  37 + :style="{ width: `${item.width || 100}%`, height: `${item.height || 100}%` }"
  38 + class="widget-item"
  39 + >
  40 + <div class="widget-box">
  41 + <div class="widget-controls-container">
  42 + <slot
  43 + name="controls"
  44 + :record="item"
  45 + :add="add"
  46 + :remove="remove"
  47 + :update="update"
  48 + ></slot>
  49 + </div>
  50 + </div>
  51 + </div>
  52 + </template>
  53 + <template v-if="isMultipleDataSource">
  54 + <div :style="{ width: `${100}%`, height: `${100}%` }" class="widget-item">
  55 + <div class="widget-box">
  56 + <div class="widget-controls-container">
  57 + <slot
  58 + name="controls"
  59 + :record="props.dataSource"
  60 + :add="add"
  61 + :remove="remove"
  62 + :update="update"
  63 + ></slot>
  64 + </div>
37 65 </div>
38 66 </div>
39   - </div>
  67 + </template>
40 68 </div>
41 69 <slot name="footer"></slot>
42 70 </section>
... ...
... ... @@ -45,6 +45,9 @@ frontComponentMap.set(FrontComponent.TEXT_COMPONENT_1, {
45 45 ComponentKey: FrontComponent.TEXT_COMPONENT_1,
46 46 ComponentConfig: TextComponent1Config,
47 47 ComponentCategory: FrontComponentCategory.TEXT,
  48 + isMultipleDataSource: false,
  49 + hasHistoryTrend: true,
  50 + hasSetting: true,
48 51 transformConfig: transformTextComponentConfig,
49 52 });
50 53
... ... @@ -54,6 +57,9 @@ frontComponentMap.set(FrontComponent.TEXT_COMPONENT_3, {
54 57 ComponentKey: FrontComponent.TEXT_COMPONENT_3,
55 58 ComponentConfig: TextComponent3Config,
56 59 ComponentCategory: FrontComponentCategory.TEXT,
  60 + isMultipleDataSource: false,
  61 + hasHistoryTrend: true,
  62 + hasSetting: true,
57 63 transformConfig: transformTextComponentConfig,
58 64 });
59 65
... ... @@ -63,6 +69,9 @@ frontComponentMap.set(FrontComponent.TEXT_COMPONENT_4, {
63 69 ComponentKey: FrontComponent.TEXT_COMPONENT_4,
64 70 ComponentConfig: TextComponent4Config,
65 71 ComponentCategory: FrontComponentCategory.TEXT,
  72 + isMultipleDataSource: false,
  73 + hasHistoryTrend: true,
  74 + hasSetting: true,
66 75 transformConfig: transformTextComponentConfig,
67 76 });
68 77
... ... @@ -72,6 +81,9 @@ frontComponentMap.set(FrontComponent.TEXT_COMPONENT_5, {
72 81 ComponentKey: FrontComponent.TEXT_COMPONENT_5,
73 82 ComponentConfig: TextComponent5Config,
74 83 ComponentCategory: FrontComponentCategory.TEXT,
  84 + isMultipleDataSource: false,
  85 + hasHistoryTrend: true,
  86 + hasSetting: true,
75 87 transformConfig: transformTextComponentConfig,
76 88 });
77 89
... ... @@ -84,6 +96,9 @@ frontComponentMap.set(FrontComponent.INSTRUMENT_COMPONENT_1, {
84 96 componentType: FrontComponent.INSTRUMENT_COMPONENT_1,
85 97 } as DashboardComponentLayout,
86 98 ComponentCategory: FrontComponentCategory.INSTRUMENT,
  99 + isMultipleDataSource: false,
  100 + hasHistoryTrend: true,
  101 + hasSetting: true,
87 102 transformConfig: transformDashboardComponentConfig,
88 103 });
89 104
... ... @@ -96,6 +111,9 @@ frontComponentMap.set(FrontComponent.INSTRUMENT_COMPONENT_2, {
96 111 componentType: FrontComponent.INSTRUMENT_COMPONENT_2,
97 112 } as DashboardComponentLayout,
98 113 ComponentCategory: FrontComponentCategory.INSTRUMENT,
  114 + isMultipleDataSource: false,
  115 + hasHistoryTrend: true,
  116 + hasSetting: true,
99 117 transformConfig: transformDashboardComponentConfig,
100 118 });
101 119
... ... @@ -104,6 +122,9 @@ frontComponentMap.set(FrontComponent.DIGITAL_DASHBOARD_COMPONENT, {
104 122 ComponentName: '数字仪表盘',
105 123 ComponentKey: FrontComponent.DIGITAL_DASHBOARD_COMPONENT,
106 124 ComponentCategory: FrontComponentCategory.INSTRUMENT,
  125 + isMultipleDataSource: false,
  126 + hasHistoryTrend: true,
  127 + hasSetting: true,
107 128 transformConfig: transformDashboardComponentConfig,
108 129 });
109 130
... ... @@ -112,6 +133,9 @@ frontComponentMap.set(FrontComponent.PICTURE_COMPONENT_1, {
112 133 ComponentName: '图片组件',
113 134 ComponentKey: FrontComponent.PICTURE_COMPONENT_1,
114 135 ComponentCategory: FrontComponentCategory.PICTURE,
  136 + isMultipleDataSource: false,
  137 + hasHistoryTrend: true,
  138 + hasSetting: false,
115 139 transformConfig: transformPictureConfig,
116 140 });
117 141
... ... @@ -120,6 +144,9 @@ frontComponentMap.set(FrontComponent.CONTROL_COMPONENT_SWITCH_WITH_ICON, {
120 144 ComponentName: '控制按钮1',
121 145 ComponentKey: FrontComponent.CONTROL_COMPONENT_SWITCH_WITH_ICON,
122 146 ComponentCategory: FrontComponentCategory.CONTROL,
  147 + isMultipleDataSource: false,
  148 + hasHistoryTrend: true,
  149 + hasSetting: true,
123 150 transformConfig: transformControlConfig,
124 151 });
125 152
... ... @@ -128,6 +155,9 @@ frontComponentMap.set(FrontComponent.CONTROL_COMPONENT_SLIDING_SWITCH, {
128 155 ComponentName: '控制按钮2',
129 156 ComponentKey: FrontComponent.CONTROL_COMPONENT_SLIDING_SWITCH,
130 157 ComponentCategory: FrontComponentCategory.CONTROL,
  158 + isMultipleDataSource: false,
  159 + hasHistoryTrend: true,
  160 + hasSetting: false,
131 161 transformConfig: transformControlConfig,
132 162 });
133 163
... ... @@ -136,6 +166,9 @@ frontComponentMap.set(FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH, {
136 166 ComponentName: '控制按钮3',
137 167 ComponentKey: FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH,
138 168 ComponentCategory: FrontComponentCategory.CONTROL,
  169 + isMultipleDataSource: false,
  170 + hasHistoryTrend: true,
  171 + hasSetting: false,
139 172 transformConfig: transformControlConfig,
140 173 });
141 174
... ... @@ -145,6 +178,9 @@ frontComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK_REAL, {
145 178 ComponentKey: FrontComponent.MAP_COMPONENT_TRACK_REAL,
146 179 ComponentConfig: MapRealTrackConfig,
147 180 ComponentCategory: FrontComponentCategory.MAP,
  181 + isMultipleDataSource: true,
  182 + hasHistoryTrend: false,
  183 + hasSetting: false,
148 184 transformConfig: transfromMapComponentConfig,
149 185 });
150 186
... ... @@ -154,6 +190,9 @@ frontComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK_HISTORY, {
154 190 ComponentKey: FrontComponent.MAP_COMPONENT_TRACK_HISTORY,
155 191 ComponentConfig: MaphistoryTrackConfig,
156 192 ComponentCategory: FrontComponentCategory.MAP,
  193 + isMultipleDataSource: true,
  194 + hasHistoryTrend: false,
  195 + hasSetting: false,
157 196 transformConfig: transfromMapComponentConfig,
158 197 });
159 198
... ...
1 1 <script lang="ts" setup>
2   - import { CopyOutlined, DeleteOutlined, SettingOutlined } from '@ant-design/icons-vue';
  2 + import {
  3 + CopyOutlined,
  4 + DeleteOutlined,
  5 + SettingOutlined,
  6 + SwapOutlined,
  7 + } from '@ant-design/icons-vue';
3 8 import { Tooltip, Button } from 'ant-design-vue';
4 9 import { FormActionType, useForm } from '/@/components/Form';
5   - import { basicSchema } from '../config/basicConfiguration';
  10 + import { basicSchema, DataSourceField } from '../config/basicConfiguration';
6 11 import BasicForm from '/@/components/Form/src/BasicForm.vue';
7   - import { ref, shallowReactive, unref, nextTick, watch, computed } from 'vue';
  12 + import { ref, shallowReactive, unref, nextTick, watch, computed, onMounted } from 'vue';
8 13 import VisualOptionsModal from './VisualOptionsModal.vue';
9 14 import { useModal } from '/@/components/Modal';
10 15 import { buildUUID } from '/@/utils/uuid';
... ... @@ -13,6 +18,10 @@
13 18 import { DataBoardLayoutInfo } from '../../types/type';
14 19 import { getDataSourceComponent } from './DataSourceForm/help';
15 20 import { FrontComponent } from '../../const/const';
  21 + import { isNullAndUnDef } from '/@/utils/is';
  22 + import { useSortable } from '/@/hooks/web/useSortable';
  23 + import { cloneDeep } from 'lodash-es';
  24 + import { frontComponentMap } from '../../components/help';
16 25
17 26 type DataSourceFormEL = { [key: string]: Nullable<FormActionType> };
18 27
... ... @@ -61,27 +70,55 @@
61 70
62 71 const validateDataSourceField = async () => {
63 72 const hasExistEl = Object.keys(dataSourceEl).filter((key) => dataSourceEl[key]);
64   - const _dataSource: boolean[] = [];
  73 + const _dataSource: Record<DataSourceField, string>[] = [];
65 74 for (const id of hasExistEl) {
66   - const flag = (await (dataSourceEl[id] as FormActionType).validate()) as boolean;
67   - _dataSource.push(flag);
  75 + const flag = (await (dataSourceEl[id] as FormActionType).validate()) as Record<
  76 + DataSourceField,
  77 + string
  78 + >;
  79 + flag && _dataSource.push(flag);
  80 + }
  81 +
  82 + if (
  83 + [
  84 + FrontComponent.MAP_COMPONENT_TRACK_HISTORY,
  85 + FrontComponent.MAP_COMPONENT_TRACK_REAL,
  86 + ].includes(props.frontId!)
  87 + ) {
  88 + await validateMapComponent(_dataSource);
68 89 }
69 90 return _dataSource;
70 91 };
71 92
  93 + const validateMapComponent = async (dataSource: Record<DataSourceField, string>[]) => {
  94 + if (dataSource.length) {
  95 + const firstRecord = dataSource.at(0)!;
  96 + const { deviceId, slaveDeviceId } = firstRecord;
  97 + const flag = dataSource.every(
  98 + (item) => item.deviceId === deviceId && item.slaveDeviceId === slaveDeviceId
  99 + );
  100 + if (!flag) {
  101 + createMessage.warning('地图组件绑定的数据源应该一致');
  102 + return Promise.reject(false);
  103 + }
  104 + }
  105 + };
  106 +
72 107 const getDataSourceField = () => {
73 108 const hasExistEl = Object.keys(dataSourceEl).filter((key) => dataSourceEl[key]);
74 109 const _dataSource: DataSource[] = [];
  110 +
75 111 for (const id of hasExistEl) {
76 112 const index = unref(dataSource).findIndex((item) => item.id === id);
77 113 const value = (dataSourceEl[id] as FormActionType).getFieldsValue() as DataSource;
78 114 if (!~index) continue;
79 115 const componentInfo = unref(dataSource)[index].componentInfo || {};
80   - _dataSource.unshift({
  116 + _dataSource[index] = {
81 117 ...value,
82 118 componentInfo: { ...(props.defaultConfig || {}), ...componentInfo },
83   - });
  119 + };
84 120 }
  121 +
85 122 return _dataSource;
86 123 };
87 124
... ... @@ -160,7 +197,8 @@
160 197 };
161 198
162 199 const showSettingButton = computed(() => {
163   - return props.frontId !== FrontComponent.PICTURE_COMPONENT_1;
  200 + const flag = frontComponentMap.get(props.frontId!)?.hasSetting;
  201 + return flag;
164 202 });
165 203
166 204 watch(
... ... @@ -179,6 +217,41 @@
179 217 return getDataSourceComponent(props.frontId as FrontComponent);
180 218 });
181 219
  220 + let inited = false;
  221 + const formListEl = ref<HTMLElement>();
  222 + async function handleSort() {
  223 + if (inited) return;
  224 + await nextTick();
  225 + const formList = unref(formListEl);
  226 + if (!formList) return;
  227 +
  228 + const { initSortable } = useSortable(unref(formList), {
  229 + handle: '.sort-icon',
  230 + onEnd: (evt) => {
  231 + const { oldIndex, newIndex } = evt;
  232 + if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) {
  233 + return;
  234 + }
  235 +
  236 + const _dataSource = cloneDeep(unref(dataSource));
  237 +
  238 + if (oldIndex > newIndex) {
  239 + _dataSource.splice(newIndex, 0, _dataSource[oldIndex]);
  240 + _dataSource.splice(oldIndex + 1, 1);
  241 + } else {
  242 + _dataSource.splice(newIndex + 1, 0, _dataSource[oldIndex]);
  243 + _dataSource.splice(oldIndex, 1);
  244 + }
  245 +
  246 + dataSource.value = _dataSource;
  247 + },
  248 + });
  249 + initSortable();
  250 + inited = true;
  251 + }
  252 +
  253 + onMounted(() => handleSort());
  254 +
182 255 defineExpose({
183 256 getAllDataSourceFieldValue,
184 257 validate,
... ... @@ -192,36 +265,47 @@
192 265 <BasicForm @register="basicRegister" class="w-full" />
193 266 </div>
194 267 <h3 class="w-24 flex-shrink-0 text-right pr-2 my-4">选择数据源</h3>
195   - <div v-for="item in dataSource" :key="item.id" class="flex">
196   - <div
197   - class="w-24 text-right leading-30px pr-8px flex right"
198   - style="flex: 0 0 96px; justify-content: right"
199   - >
200   - 选择设备
201   - </div>
202   - <div class="pl-2 flex-auto">
203   - <!-- <BasicDataSourceForm /> -->
204   - <component :is="dataSourceComponent" :ref="(el) => setFormEl(el, item.id)" />
205   - </div>
206   - <div class="flex justify-center gap-3 w-24">
207   - <Tooltip title="复制">
208   - <CopyOutlined @click="handleCopy(item)" class="cursor-pointer text-lg !leading-32px" />
209   - </Tooltip>
210   - <Tooltip title="设置">
211   - <SettingOutlined
212   - v-show="showSettingButton"
213   - @click="handleSetting(item)"
214   - class="cursor-pointer text-lg !leading-32px"
215   - />
216   - </Tooltip>
217   - <Tooltip title="删除">
218   - <DeleteOutlined
219   - @click="handleDelete(item)"
220   - class="cursor-pointer text-lg !leading-32px"
  268 + <section ref="formListEl">
  269 + <div v-for="item in dataSource" :data-id="item.id" :key="item.id" class="flex bg-light-50">
  270 + <div
  271 + class="w-24 text-right leading-30px pr-8px flex right"
  272 + style="flex: 0 0 96px; justify-content: right"
  273 + >
  274 + 选择设备
  275 + </div>
  276 + <div class="pl-2 flex-auto">
  277 + <component
  278 + :frontId="$props.frontId"
  279 + :is="dataSourceComponent"
  280 + :ref="(el) => setFormEl(el, item.id)"
221 281 />
222   - </Tooltip>
  282 + </div>
  283 + <div class="flex justify-center gap-3 w-28">
  284 + <Tooltip title="复制">
  285 + <CopyOutlined @click="handleCopy(item)" class="cursor-pointer text-lg !leading-32px" />
  286 + </Tooltip>
  287 + <Tooltip title="设置">
  288 + <SettingOutlined
  289 + v-show="showSettingButton"
  290 + @click="handleSetting(item)"
  291 + class="cursor-pointer text-lg !leading-32px"
  292 + />
  293 + </Tooltip>
  294 + <Tooltip title="拖拽排序">
  295 + <SwapOutlined
  296 + class="cursor-pointer text-lg !leading-32px svg:transform svg:rotate-90 sort-icon"
  297 + />
  298 + </Tooltip>
  299 + <Tooltip title="删除">
  300 + <DeleteOutlined
  301 + @click="handleDelete(item)"
  302 + class="cursor-pointer text-lg !leading-32px"
  303 + />
  304 + </Tooltip>
  305 + </div>
223 306 </div>
224   - </div>
  307 + </section>
  308 +
225 309 <div class="text-center">
226 310 <Button type="primary" @click="handleAdd">添加数据源</Button>
227 311 </div>
... ...
... ... @@ -66,6 +66,7 @@
66 66 const { getAllDataSourceFieldValue, validate } = unref(basicConfigurationEl)!;
67 67 await validate();
68 68 const value = getAllDataSourceFieldValue();
  69 +
69 70 unref(isEdit) ? handleUpdateComponent(value) : handleAddComponent(value);
70 71 resetForm();
71 72 } catch (error: unknown) {
... ...
1 1 <script lang="ts" setup>
2 2 import { ref } from 'vue';
  3 + import { FrontComponent } from '../../../const/const';
3 4 import { dataSourceSchema } from '../../config/basicConfiguration';
4 5 import { FormActionType } from '/@/components/Form';
5 6 import BasicForm from '/@/components/Form/src/BasicForm.vue';
6 7 const formEl = ref<Nullable<FormActionType>>(null);
7 8
  9 + defineProps<{
  10 + frontId?: FrontComponent;
  11 + }>();
  12 +
8 13 defineExpose({ formActionType: formEl });
9 14 </script>
10 15
11 16 <template>
12 17 <BasicForm
13 18 ref="formEl"
14   - :schemas="dataSourceSchema"
  19 + :schemas="dataSourceSchema($props.frontId)"
15 20 class="w-full flex-1 data-source-form"
16 21 :show-action-button-group="false"
17 22 :row-props="{
... ...
1 1 <script lang="ts" setup>
2 2 import { ref, unref } from 'vue';
3 3 import { BasicForm, FormActionType } from '/@/components/Form';
4   - import { controlFormSchema } from '../../config/basicConfiguration';
  4 + import { dataSourceSchema } from '../../config/basicConfiguration';
  5 + import { FrontComponent } from '../../../const/const';
  6 +
  7 + defineProps<{
  8 + frontId?: FrontComponent;
  9 + }>();
5 10
6 11 const formEl = ref<Nullable<FormActionType>>();
7 12
... ... @@ -33,7 +38,7 @@
33 38 <div class="w-full flex-1">
34 39 <BasicForm
35 40 :ref="(el) => setFormEl(el)"
36   - :schemas="controlFormSchema"
  41 + :schemas="dataSourceSchema($props.frontId)"
37 42 class="w-full flex-1 data-source-form"
38 43 :show-action-button-group="false"
39 44 :row-props="{
... ...
1   -<script lang="ts" setup>
2   - import { ref, unref } from 'vue';
3   - import { BasicForm, FormActionType } from '/@/components/Form';
4   - import { mapFormSchema } from '../../config/basicConfiguration';
5   -
6   - const formEl = ref<Nullable<FormActionType>>();
7   -
8   - const setFormEl = (el: any) => {
9   - formEl.value = el;
10   - };
11   -
12   - const getFieldsValue = () => {
13   - return unref(formEl)!.getFieldsValue();
14   - };
15   -
16   - const validate = async () => {
17   - await unref(formEl)!.validate();
18   - };
19   -
20   - const setFieldsValue = async (record: Recordable) => {
21   - await unref(formEl)!.setFieldsValue(record);
22   - };
23   -
24   - const clearValidate = async (name?: string | string[]) => {
25   - await unref(formEl)!.clearValidate(name);
26   - };
27   - defineExpose({
28   - formActionType: { getFieldsValue, validate, setFieldsValue, clearValidate },
29   - });
30   -</script>
31   -
32   -<template>
33   - <div class="w-full flex-1">
34   - <BasicForm
35   - :ref="(el) => setFormEl(el)"
36   - :schemas="mapFormSchema"
37   - class="w-full flex-1 data-source-form"
38   - :show-action-button-group="false"
39   - :row-props="{
40   - gutter: 10,
41   - }"
42   - layout="horizontal"
43   - :label-col="{ span: 0 }"
44   - />
45   - </div>
46   -</template>
1 1 import { Component } from 'vue';
2 2 import { FrontComponent } from '../../../const/const';
3 3 import BasicDataSourceForm from './BasicDataSourceForm.vue';
4   -import ControlDataSourceForm from './ControlDataSourceForm.vue';
5   -import MapDataSourceForm from './MapDataSourceForm.vue';
  4 +// import ControlDataSourceForm from './ControlDataSourceForm.vue';
6 5
7 6 const dataSourceComponentMap = new Map<FrontComponent, Component>();
8 7
9   -dataSourceComponentMap.set(FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH, ControlDataSourceForm);
10   -dataSourceComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK_REAL, MapDataSourceForm);
11   -dataSourceComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK_HISTORY, MapDataSourceForm);
12   -
13 8 export const getDataSourceComponent = (frontId: FrontComponent) => {
14 9 if (dataSourceComponentMap.has(frontId)) return dataSourceComponentMap.get(frontId)!;
15 10 return BasicDataSourceForm;
... ...
... ... @@ -8,20 +8,24 @@
8 8 import { AggregateDataEnum, eChartOptions } from '/@/views/device/localtion/config.data';
9 9 import { useGridLayout } from '/@/hooks/component/useGridLayout';
10 10 import { ColEx } from '/@/components/Form/src/types';
11   - import { DataSource } from '/@/api/dataBoard/model';
  11 + import { DataSource, DeviceAttributeRecord } from '/@/api/dataBoard/model';
12 12 import { useForm, BasicForm } from '/@/components/Form';
13 13 import { formSchema, QueryWay, SchemaFiled } from '../config/historyTrend.config';
14 14 import { DEFAULT_DATE_FORMAT } from '../config/util';
15 15 import { Loading } from '/@/components/Loading';
16 16 import BasicModal from '/@/components/Modal/src/BasicModal.vue';
17 17 import { useModalInner } from '/@/components/Modal';
18   - import { getDeviceAttributes } from '/@/api/dataBoard';
  18 + import { getAllDeviceByOrg, getDeviceAttributes } from '/@/api/dataBoard';
  19 + import { HistoryData } from '/@/api/alarm/position/model';
  20 + import { EChartsOption } from 'echarts';
  21 +
  22 + type DeviceOption = Record<'label' | 'value' | 'organizationId', string>;
19 23
20 24 defineEmits(['register']);
21 25
22 26 const chartRef = ref();
23 27
24   - const deviceAttrs = ref<string[]>([]);
  28 + const deviceAttrs = ref<DeviceAttributeRecord[]>([]);
25 29
26 30 const loading = ref(false);
27 31
... ... @@ -29,26 +33,27 @@
29 33
30 34 function getSearchParams(value: Recordable) {
31 35 const { startTs, endTs, interval, agg, limit, keys, way, deviceId } = value;
  36 + const basicRecord = {
  37 + entityId: deviceId,
  38 + keys: keys
  39 + ? keys
  40 + : unref(deviceAttrs)
  41 + .map((item) => item.identifier)
  42 + .join(),
  43 + interval,
  44 + agg,
  45 + limit,
  46 + };
32 47 if (way === QueryWay.LATEST) {
33   - return {
34   - entityId: deviceId,
35   - keys: keys ? keys : unref(deviceAttrs).join(),
  48 + return Object.assign(basicRecord, {
36 49 startTs: moment().subtract(startTs, 'ms').valueOf(),
37 50 endTs: Date.now(),
38   - interval,
39   - agg,
40   - limit,
41   - };
  51 + });
42 52 } else {
43   - return {
44   - entityId: deviceId,
45   - keys: keys ? keys : unref(deviceAttrs).join(),
  53 + return Object.assign(basicRecord, {
46 54 startTs: moment(startTs).valueOf(),
47 55 endTs: moment(endTs).valueOf(),
48   - interval,
49   - agg,
50   - limit,
51   - };
  56 + });
52 57 }
53 58 }
54 59
... ... @@ -60,24 +65,24 @@
60 65 }
61 66 }
62 67
63   - function setChartOptions(data, keys?) {
  68 + function setChartOptions(data: HistoryData, keys?: string | string[]) {
64 69 const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
65 70
66   - const dataArray: any[] = [];
  71 + const dataArray: [string, string, string][] = [];
67 72 for (const key in data) {
68 73 for (const item1 of data[key]) {
69 74 let { ts, value } = item1;
70 75 const time = dateUtil(ts).format(DEFAULT_DATE_FORMAT);
71 76 value = Number(value).toFixed(2);
72   - dataArray.push([time, value, key]);
  77 + dataArray.push([time, value, key as string]);
73 78 }
74 79 }
75   - keys = keys ? [keys] : unref(deviceAttrs);
76   - const series: any = keys.map((item) => {
  80 + keys = keys ? [keys as string] : unref(deviceAttrs).map((item) => item.identifier);
  81 + const series: EChartsOption['series'] = (keys as string[]).map((item) => {
77 82 return {
78 83 name: item,
79 84 type: 'line',
80   - data: dataArray.filter((item1) => item1[2] === item),
  85 + data: dataArray.filter((temp) => temp[2] === item),
81 86 };
82 87 });
83 88 // 设置数据
... ... @@ -118,18 +123,23 @@
118 123 },
119 124 });
120 125
121   - const getDeviceDataKey = async (deviceId: string) => {
122   - if (!deviceId) return;
  126 + const getDeviceDataKey = async (record: DeviceOption) => {
  127 + const { organizationId, value } = record;
123 128 try {
124   - deviceAttrs.value = (await getDeviceAttributes({ deviceId })) || [];
  129 + const options = await getAllDeviceByOrg(organizationId);
  130 + const record = options.find((item) => item.id === value);
  131 + const { deviceProfileId } = record!;
  132 + deviceAttrs.value = (await getDeviceAttributes({ deviceProfileId })) || [];
125 133 await nextTick();
126 134 method.updateSchema({
127 135 field: SchemaFiled.KEYS,
128 136 componentProps: {
129   - options: unref(deviceAttrs).map((item) => ({ label: item, value: item })),
  137 + options: unref(deviceAttrs).map((item) => ({ label: item.name, value: item.identifier })),
130 138 },
131 139 });
132   - } catch (error) {}
  140 + } catch (error) {
  141 + throw error;
  142 + }
133 143 };
134 144
135 145 const handleModalOpen = async () => {
... ... @@ -147,7 +157,9 @@
147 157
148 158 const res = await getDeviceHistoryInfo({
149 159 entityId: deviceId,
150   - keys: unref(deviceAttrs).join(),
  160 + keys: unref(deviceAttrs)
  161 + .map((item) => item.identifier)
  162 + .join(),
151 163 startTs: Date.now() - 1 * 24 * 60 * 60 * 1000,
152 164 endTs: Date.now(),
153 165 agg: AggregateDataEnum.NONE,
... ... @@ -166,9 +178,10 @@
166 178
167 179 const generateDeviceOptions = (dataSource: DataSource[]) => {
168 180 const record: { [key: string]: boolean } = {};
169   - const options: Record<'label' | 'value', string>[] = [];
  181 +
  182 + const options: DeviceOption[] = [];
170 183 for (const item of dataSource) {
171   - const { deviceName, gatewayDevice, slaveDeviceId } = item;
  184 + const { deviceName, gatewayDevice, slaveDeviceId, organizationId } = item;
172 185 let { deviceId } = item;
173 186 if (gatewayDevice) {
174 187 deviceId = slaveDeviceId;
... ... @@ -177,6 +190,7 @@
177 190 options.push({
178 191 label: deviceName,
179 192 value: deviceId,
  193 + organizationId,
180 194 });
181 195 record[deviceId] = true;
182 196 }
... ... @@ -195,8 +209,8 @@
195 209 const { setFieldsValue } = formActionType;
196 210 return {
197 211 options,
198   - onChange(value: string) {
199   - getDeviceDataKey(value);
  212 + onChange(_, record: DeviceOption) {
  213 + getDeviceDataKey(record);
200 214 setFieldsValue({ [SchemaFiled.KEYS]: null });
201 215 },
202 216 };
... ... @@ -204,17 +218,10 @@
204 218 });
205 219
206 220 if (options.length && options.at(0)?.value) {
207   - const value = options.at(0)!.value;
208   - getDeviceDataKey(value);
  221 + const record = options.at(0)!;
  222 + await getDeviceDataKey(record);
209 223 try {
210   - deviceAttrs.value = (await getDeviceAttributes({ deviceId: value })) || [];
211   - method.updateSchema({
212   - field: SchemaFiled.KEYS,
213   - componentProps: {
214   - options: unref(deviceAttrs).map((item) => ({ label: item, value: item })),
215   - },
216   - });
217   - await method.setFieldsValue({ [SchemaFiled.DEVICE_ID]: value });
  224 + await method.setFieldsValue({ [SchemaFiled.DEVICE_ID]: record.value });
218 225 } catch (error) {}
219 226 }
220 227
... ... @@ -231,7 +238,7 @@
231 238 <section class="bg-white my-3 p-2">
232 239 <BasicForm @register="register" />
233 240 </section>
234   - <section class="bg-white p-3" style="min-hight: 350px">
  241 + <section class="bg-white p-3" style="min-height: 350px">
235 242 <div v-show="isNull" ref="chartRef" :style="{ height: '350px', width: '100%' }">
236 243 <Loading :loading="loading" :absolute="true" />
237 244 </div>
... ...
... ... @@ -2,18 +2,18 @@ import { getAllDeviceByOrg, getDeviceAttributes, getGatewaySlaveDevice } from '/
2 2 import { getOrganizationList } from '/@/api/system/system';
3 3 import { FormSchema } from '/@/components/Form';
4 4 import { copyTransFun } from '/@/utils/fnUtils';
5   -import { OnChangeHookParams } from '/@/components/Form/src/components/ApiSearchSelect.vue';
6   -import { unref } from 'vue';
  5 +import { DeviceAttributeParams, MasterDeviceList } from '/@/api/dataBoard/model';
  6 +import { FrontComponent } from '../../const/const';
7 7
8 8 export enum BasicConfigField {
9 9 NAME = 'name',
10 10 REMARK = 'remark',
11 11 }
12 12
13   -const getDeviceAttribute = async (deviceId: string) => {
  13 +const getDeviceAttribute = async (params: DeviceAttributeParams) => {
14 14 try {
15   - const data = await getDeviceAttributes({ deviceId });
16   - if (data) return data.map((item) => ({ label: item, value: item }));
  15 + const data = await getDeviceAttributes(params);
  16 + if (data) return data.map((item) => ({ label: item.name, value: item.identifier }));
17 17 } catch (error) {}
18 18 return [];
19 19 };
... ... @@ -23,6 +23,8 @@ export enum DataSourceField {
23 23 ORIGINATION_ID = 'organizationId',
24 24 DEVICE_ID = 'deviceId',
25 25 SLAVE_DEVICE_ID = 'slaveDeviceId',
  26 + DEVICE_PROFILE_ID = 'deviceProfileId',
  27 + SLAVE_DEVICE_PROFILE_ID = 'slaveDeviceProfileId',
26 28 ATTRIBUTE = 'attribute',
27 29 ATTRIBUTE_RENAME = 'attributeRename',
28 30 DEVICE_NAME = 'deviceName',
... ... @@ -31,6 +33,18 @@ export enum DataSourceField {
31 33 LATITUDE_ATTRIBUTE = 'latitudeAttribute',
32 34 }
33 35
  36 +const isControlComponent = (frontId: FrontComponent) => {
  37 + const list = [
  38 + FrontComponent.CONTROL_COMPONENT_SLIDING_SWITCH,
  39 + FrontComponent.CONTROL_COMPONENT_SWITCH_WITH_ICON,
  40 + FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH,
  41 + ];
  42 + if (list.includes(frontId)) {
  43 + return true;
  44 + }
  45 + return false;
  46 +};
  47 +
34 48 export const basicSchema: FormSchema[] = [
35 49 {
36 50 field: BasicConfigField.NAME,
... ... @@ -52,388 +66,214 @@ export const basicSchema: FormSchema[] = [
52 66 },
53 67 ];
54 68
55   -export const dataSourceSchema: FormSchema[] = [
56   - {
57   - field: DataSourceField.IS_GATEWAY_DEVICE,
58   - component: 'Switch',
59   - label: '是否是网关设备',
60   - show: false,
61   - },
62   - {
63   - field: DataSourceField.DEVICE_NAME,
64   - component: 'Input',
65   - label: '设备名',
66   - show: false,
67   - },
68   - {
69   - field: DataSourceField.ORIGINATION_ID,
70   - component: 'ApiTreeSelect',
71   - label: '组织',
72   - colProps: { span: 8 },
73   - rules: [{ required: true, message: '组织为必填项' }],
74   - componentProps({ formActionType }) {
75   - const { setFieldsValue } = formActionType;
76   - return {
77   - placeholder: '请选择组织',
78   - api: async () => {
79   - const data = await getOrganizationList();
80   - copyTransFun(data as any as any[]);
81   - return data;
82   - },
83   - onChange() {
84   - setFieldsValue({
85   - [DataSourceField.DEVICE_ID]: null,
86   - [DataSourceField.ATTRIBUTE]: null,
87   - [DataSourceField.SLAVE_DEVICE_ID]: null,
88   - [DataSourceField.IS_GATEWAY_DEVICE]: false,
89   - });
90   - },
91   - getPopupContainer: () => document.body,
92   - };
93   - },
94   - },
95   - {
96   - field: DataSourceField.DEVICE_ID,
97   - component: 'ApiSelect',
98   - label: '设备',
99   - colProps: { span: 8 },
100   - rules: [{ required: true, message: '设备名称为必填项' }],
101   - componentProps({ formModel, formActionType }) {
102   - const { setFieldsValue } = formActionType;
103   - const organizationId = formModel[DataSourceField.ORIGINATION_ID];
104   - return {
105   - api: async () => {
106   - if (organizationId) {
107   - try {
108   - const data = await getAllDeviceByOrg(organizationId);
109   - if (data)
110   - return data.map((item) => ({
111   - label: item.name,
112   - value: item.id,
113   - deviceType: item.deviceType,
114   - }));
115   - } catch (error) {}
116   - }
117   - return [];
118   - },
119   - onChange(_value, record: Record<'value' | 'label' | 'deviceType', string>) {
120   - setFieldsValue({
121   - [DataSourceField.ATTRIBUTE]: null,
122   - [DataSourceField.IS_GATEWAY_DEVICE]: record?.deviceType === 'GATEWAY',
123   - [DataSourceField.SLAVE_DEVICE_ID]: null,
124   - [DataSourceField.DEVICE_NAME]: record?.label,
125   - });
126   - },
127   - placeholder: '请选择设备',
128   - getPopupContainer: () => document.body,
129   - };
130   - },
131   - },
132   - {
133   - field: DataSourceField.SLAVE_DEVICE_ID,
134   - label: '网关子设备',
135   - component: 'ApiSelect',
136   - colProps: { span: 8 },
137   - rules: [{ required: true, message: '网关子设备为必填项' }],
138   - ifShow({ model }) {
139   - return model[DataSourceField.IS_GATEWAY_DEVICE];
140   - },
141   - dynamicRules({ model }) {
142   - return [{ required: model[DataSourceField.IS_GATEWAY_DEVICE], message: '请选择网关子设备' }];
  69 +export const dataSourceSchema = (frontId?: FrontComponent): FormSchema[] => {
  70 + return [
  71 + {
  72 + field: DataSourceField.IS_GATEWAY_DEVICE,
  73 + component: 'Switch',
  74 + label: '是否是网关设备',
  75 + show: false,
143 76 },
144   - componentProps({ formModel, formActionType }) {
145   - const { setFieldsValue } = formActionType;
146   - const organizationId = formModel[DataSourceField.ORIGINATION_ID];
147   - const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE];
148   - const deviceId = formModel[DataSourceField.DEVICE_ID];
149   - return {
150   - api: async () => {
151   - if (organizationId && isGatewayDevice) {
152   - try {
153   - const data = await getGatewaySlaveDevice({ organizationId, masterId: deviceId });
154   - if (data)
155   - return data.map((item) => ({
156   - label: item.name,
157   - value: item.id,
158   - deviceType: item.deviceType,
159   - }));
160   - } catch (error) {}
161   - }
162   - return [];
163   - },
164   - onChange(_value, record: Record<'value' | 'label' | 'deviceType', string>) {
165   - setFieldsValue({
166   - [DataSourceField.ATTRIBUTE]: null,
167   - [DataSourceField.DEVICE_NAME]: record?.label,
168   - });
169   - },
170   - placeholder: '请选择网关子设备',
171   - getPopupContainer: () => document.body,
172   - };
  77 + {
  78 + field: DataSourceField.DEVICE_NAME,
  79 + component: 'Input',
  80 + label: '设备名',
  81 + show: false,
173 82 },
174   - },
175   - {
176   - field: DataSourceField.ATTRIBUTE,
177   - component: 'ApiSelect',
178   - label: '属性',
179   - colProps: { span: 8 },
180   - rules: [{ required: true, message: '属性为必填项' }],
181   - componentProps({ formModel }) {
182   - const organizationId = formModel[DataSourceField.ORIGINATION_ID];
183   - const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE];
184   - const deviceId = formModel[DataSourceField.DEVICE_ID];
185   - const slaveDeviceId = formModel[DataSourceField.SLAVE_DEVICE_ID];
186   - return {
187   - api: async () => {
188   - if (organizationId && deviceId) {
189   - try {
190   - if (isGatewayDevice && slaveDeviceId) {
191   - return await getDeviceAttribute(slaveDeviceId);
192   - }
193   - if (!isGatewayDevice) {
194   - return await getDeviceAttribute(deviceId);
195   - }
196   - } catch (error) {}
197   - }
198   - return [];
199   - },
200   - placeholder: '请选择属性',
201   - getPopupContainer: () => document.body,
202   - };
  83 + {
  84 + field: DataSourceField.ORIGINATION_ID,
  85 + component: 'ApiTreeSelect',
  86 + label: '组织',
  87 + colProps: { span: 8 },
  88 + rules: [{ required: true, message: '组织为必填项' }],
  89 + componentProps({ formActionType }) {
  90 + const { setFieldsValue } = formActionType;
  91 + return {
  92 + placeholder: '请选择组织',
  93 + api: async () => {
  94 + const data = await getOrganizationList();
  95 + copyTransFun(data as any as any[]);
  96 + return data;
  97 + },
  98 + onChange() {
  99 + setFieldsValue({
  100 + [DataSourceField.DEVICE_ID]: null,
  101 + [DataSourceField.ATTRIBUTE]: null,
  102 + [DataSourceField.SLAVE_DEVICE_ID]: null,
  103 + [DataSourceField.IS_GATEWAY_DEVICE]: false,
  104 + [DataSourceField.DEVICE_PROFILE_ID]: null,
  105 + [DataSourceField.SLAVE_DEVICE_PROFILE_ID]: null,
  106 + });
  107 + },
  108 + getPopupContainer: () => document.body,
  109 + };
  110 + },
203 111 },
204   - },
205   - {
206   - field: DataSourceField.DEVICE_RENAME,
207   - component: 'Input',
208   - label: '设备',
209   - colProps: { span: 8 },
210   - componentProps: {
211   - placeholder: '设备重命名',
  112 + {
  113 + field: DataSourceField.DEVICE_PROFILE_ID,
  114 + component: 'Input',
  115 + label: '',
  116 + show: false,
212 117 },
213   - },
214   - {
215   - field: DataSourceField.ATTRIBUTE_RENAME,
216   - component: 'Input',
217   - label: '属性',
218   - colProps: { span: 8 },
219   - componentProps: {
220   - placeholder: '属性重命名',
221   - },
222   - },
223   -];
  118 + {
  119 + field: DataSourceField.DEVICE_ID,
  120 + component: 'ApiSelect',
  121 + label: '设备',
  122 + colProps: { span: 8 },
  123 + rules: [{ required: true, message: '设备名称为必填项' }],
  124 + componentProps({ formModel, formActionType }) {
  125 + const { setFieldsValue } = formActionType;
  126 + const organizationId = formModel[DataSourceField.ORIGINATION_ID];
  127 + const deviceId = formModel[DataSourceField.DEVICE_ID];
  128 + return {
  129 + api: async () => {
  130 + if (organizationId) {
  131 + try {
  132 + const data = await getAllDeviceByOrg(organizationId);
  133 + if (deviceId) {
  134 + const record = data.find((item) => item.id === deviceId);
  135 + setFieldsValue({ [DataSourceField.DEVICE_PROFILE_ID]: record?.deviceProfileId });
  136 + }
  137 + if (data)
  138 + return data.map((item) => ({
  139 + ...item,
  140 + label: item.name,
  141 + value: item.id,
  142 + deviceType: item.deviceType,
  143 + }));
  144 + } catch (error) {}
  145 + }
  146 + return [];
  147 + },
224 148
225   -export const controlFormSchema: FormSchema[] = [
226   - {
227   - field: DataSourceField.DEVICE_RENAME,
228   - component: 'Input',
229   - label: '设备',
230   - colProps: { span: 8 },
231   - componentProps: {
232   - placeholder: '设备重命名',
  149 + onChange(_value, record: MasterDeviceList) {
  150 + setFieldsValue({
  151 + [DataSourceField.ATTRIBUTE]: null,
  152 + [DataSourceField.IS_GATEWAY_DEVICE]: record?.deviceType === 'GATEWAY',
  153 + [DataSourceField.DEVICE_PROFILE_ID]: record?.deviceProfileId,
  154 + [DataSourceField.SLAVE_DEVICE_ID]: null,
  155 + [DataSourceField.SLAVE_DEVICE_PROFILE_ID]: null,
  156 + [DataSourceField.DEVICE_NAME]: record?.label,
  157 + });
  158 + },
  159 + placeholder: '请选择设备',
  160 + getPopupContainer: () => document.body,
  161 + };
  162 + },
233 163 },
234   - },
235   - {
236   - field: DataSourceField.ATTRIBUTE_RENAME,
237   - component: 'Input',
238   - label: '属性',
239   - colProps: { span: 8 },
240   - componentProps: {
241   - placeholder: '属性重命名',
  164 + {
  165 + field: DataSourceField.SLAVE_DEVICE_PROFILE_ID,
  166 + component: 'Input',
  167 + label: '',
  168 + show: false,
242 169 },
243   - },
244   -];
245   -
246   -export const mapFormSchema: FormSchema[] = [
247   - {
248   - field: DataSourceField.IS_GATEWAY_DEVICE,
249   - component: 'Switch',
250   - label: '是否是网关设备',
251   - show: false,
252   - },
253   - {
254   - field: DataSourceField.DEVICE_NAME,
255   - component: 'Input',
256   - label: '设备名',
257   - show: false,
258   - },
259   - {
260   - field: DataSourceField.ORIGINATION_ID,
261   - component: 'ApiTreeSelect',
262   - label: '组织',
263   - colProps: { span: 8 },
264   - rules: [{ required: true, message: '组织为必填项' }],
265   - componentProps({ formActionType }) {
266   - const { setFieldsValue } = formActionType;
267   - return {
268   - placeholder: '请选择组织',
269   - api: async () => {
270   - const data = await getOrganizationList();
271   - copyTransFun(data as any as any[]);
272   - return data;
273   - },
274   - onChange() {
275   - setFieldsValue({
276   - [DataSourceField.DEVICE_ID]: null,
277   - [DataSourceField.LATITUDE_ATTRIBUTE]: null,
278   - [DataSourceField.LONGITUDE_ATTRIBUTE]: null,
279   - [DataSourceField.SLAVE_DEVICE_ID]: null,
280   - [DataSourceField.IS_GATEWAY_DEVICE]: false,
281   - });
282   - },
283   - getPopupContainer: () => document.body,
284   - };
  170 + {
  171 + field: DataSourceField.SLAVE_DEVICE_ID,
  172 + label: '网关子设备',
  173 + component: 'ApiSelect',
  174 + colProps: { span: 8 },
  175 + rules: [{ required: true, message: '网关子设备为必填项' }],
  176 + ifShow({ model }) {
  177 + return model[DataSourceField.IS_GATEWAY_DEVICE];
  178 + },
  179 + dynamicRules({ model }) {
  180 + return [
  181 + { required: model[DataSourceField.IS_GATEWAY_DEVICE], message: '请选择网关子设备' },
  182 + ];
  183 + },
  184 + componentProps({ formModel, formActionType }) {
  185 + const { setFieldsValue } = formActionType;
  186 + const organizationId = formModel[DataSourceField.ORIGINATION_ID];
  187 + const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE];
  188 + const deviceId = formModel[DataSourceField.DEVICE_ID];
  189 + const slaveDeviceId = formModel[DataSourceField.SLAVE_DEVICE_ID];
  190 + return {
  191 + api: async () => {
  192 + if (organizationId && isGatewayDevice) {
  193 + try {
  194 + const data = await getGatewaySlaveDevice({ organizationId, masterId: deviceId });
  195 + if (slaveDeviceId) {
  196 + const record = data.find((item) => item.id === slaveDeviceId);
  197 + setFieldsValue({ [DataSourceField.DEVICE_PROFILE_ID]: record?.deviceProfileId });
  198 + }
  199 + if (data)
  200 + return data.map((item) => ({
  201 + ...item,
  202 + label: item.name,
  203 + value: item.id,
  204 + deviceType: item.deviceType,
  205 + }));
  206 + } catch (error) {}
  207 + }
  208 + return [];
  209 + },
  210 + onChange(_value, record: MasterDeviceList) {
  211 + setFieldsValue({
  212 + [DataSourceField.ATTRIBUTE]: null,
  213 + [DataSourceField.SLAVE_DEVICE_PROFILE_ID]: record.deviceProfileId,
  214 + [DataSourceField.DEVICE_NAME]: record?.label,
  215 + });
  216 + },
  217 + placeholder: '请选择网关子设备',
  218 + getPopupContainer: () => document.body,
  219 + };
  220 + },
285 221 },
286   - },
287   - {
288   - field: DataSourceField.DEVICE_ID,
289   - component: 'ApiSelect',
290   - label: '设备',
291   - colProps: { span: 8 },
292   - rules: [{ required: true, message: '设备名称为必填项' }],
293   - componentProps({ formModel, formActionType }) {
294   - const { setFieldsValue } = formActionType;
295   - const organizationId = formModel[DataSourceField.ORIGINATION_ID];
296   - return {
297   - api: async () => {
298   - if (organizationId) {
299   - try {
300   - const data = await getAllDeviceByOrg(organizationId);
301   - if (data)
302   - return data.map((item) => ({
303   - label: item.name,
304   - value: item.id,
305   - deviceType: item.deviceType,
306   - }));
307   - } catch (error) {}
308   - }
309   - return [];
310   - },
311   - onChange(_value, record: Record<'value' | 'label' | 'deviceType', string>) {
312   - setFieldsValue({
313   - [DataSourceField.LONGITUDE_ATTRIBUTE]: null,
314   - [DataSourceField.LATITUDE_ATTRIBUTE]: null,
315   - [DataSourceField.IS_GATEWAY_DEVICE]: record?.deviceType === 'GATEWAY',
316   - [DataSourceField.SLAVE_DEVICE_ID]: null,
317   - [DataSourceField.DEVICE_NAME]: record?.label,
318   - });
319   - },
320   - placeholder: '请选择设备',
321   - getPopupContainer: () => document.body,
322   - };
323   - },
324   - },
325   - {
326   - field: DataSourceField.SLAVE_DEVICE_ID,
327   - label: '网关子设备',
328   - component: 'ApiSelect',
329   - colProps: { span: 8 },
330   - rules: [{ required: true, message: '网关子设备为必填项' }],
331   - ifShow({ model }) {
332   - return model[DataSourceField.IS_GATEWAY_DEVICE];
333   - },
334   - dynamicRules({ model }) {
335   - return [{ required: model[DataSourceField.IS_GATEWAY_DEVICE], message: '请选择网关子设备' }];
336   - },
337   - componentProps({ formModel, formActionType }) {
338   - const { setFieldsValue } = formActionType;
339   - const organizationId = formModel[DataSourceField.ORIGINATION_ID];
340   - const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE];
341   - const deviceId = formModel[DataSourceField.DEVICE_ID];
342   - return {
343   - api: async () => {
344   - if (organizationId && isGatewayDevice) {
345   - try {
346   - const data = await getGatewaySlaveDevice({ organizationId, masterId: deviceId });
347   - if (data)
348   - return data.map((item) => ({
349   - label: item.name,
350   - value: item.id,
351   - deviceType: item.deviceType,
352   - }));
353   - } catch (error) {}
354   - }
355   - return [];
356   - },
357   - onChange(_value, record: Record<'value' | 'label' | 'deviceType', string>) {
358   - setFieldsValue({
359   - [DataSourceField.LATITUDE_ATTRIBUTE]: null,
360   - [DataSourceField.LONGITUDE_ATTRIBUTE]: null,
361   - [DataSourceField.DEVICE_NAME]: record?.label,
362   - });
363   - },
364   - placeholder: '请选择网关子设备',
365   - getPopupContainer: () => document.body,
366   - };
  222 + {
  223 + field: DataSourceField.ATTRIBUTE,
  224 + component: 'ApiSelect',
  225 + label: '属性',
  226 + colProps: { span: 8 },
  227 + rules: [{ required: true, message: '属性为必填项' }],
  228 + componentProps({ formModel }) {
  229 + const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE];
  230 + const deviceId = formModel[DataSourceField.DEVICE_ID];
  231 + const deviceProfileId = formModel[DataSourceField.DEVICE_PROFILE_ID];
  232 + const slaveDeviceId = formModel[DataSourceField.SLAVE_DEVICE_ID];
  233 + const slaveDeviceProfileId = formModel[DataSourceField.SLAVE_DEVICE_PROFILE_ID];
  234 +
  235 + return {
  236 + api: async () => {
  237 + if (deviceId) {
  238 + try {
  239 + if (isGatewayDevice && slaveDeviceId && slaveDeviceProfileId) {
  240 + return await getDeviceAttribute({
  241 + deviceProfileId: slaveDeviceProfileId,
  242 + dataType: isControlComponent(frontId!) ? 'BOOL' : undefined,
  243 + });
  244 + }
  245 + if (!isGatewayDevice && deviceProfileId) {
  246 + return await getDeviceAttribute({
  247 + deviceProfileId,
  248 + dataType: isControlComponent(frontId!) ? 'BOOL' : undefined,
  249 + });
  250 + }
  251 + } catch (error) {}
  252 + }
  253 + return [];
  254 + },
  255 + placeholder: '请选择属性',
  256 + getPopupContainer: () => document.body,
  257 + };
  258 + },
367 259 },
368   - },
369   - {
370   - field: DataSourceField.LONGITUDE_ATTRIBUTE,
371   - component: 'ApiSearchSelect',
372   - label: '经度属性',
373   - colProps: { span: 8 },
374   - rules: [{ required: true, message: '属性为必填项' }],
375   - componentProps({ formModel }) {
376   - const organizationId = formModel[DataSourceField.ORIGINATION_ID];
377   - const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE];
378   - const deviceId = formModel[DataSourceField.DEVICE_ID];
379   - const slaveDeviceId = formModel[DataSourceField.SLAVE_DEVICE_ID];
380   - return {
381   - api: async () => {
382   - if (organizationId && deviceId) {
383   - try {
384   - if (isGatewayDevice && slaveDeviceId) {
385   - return await getDeviceAttribute(slaveDeviceId);
386   - }
387   - if (!isGatewayDevice) {
388   - return await getDeviceAttribute(deviceId);
389   - }
390   - } catch (error) {}
391   - }
392   - return [];
393   - },
394   - placeholder: '请选择经度属性',
395   - dropdownVisibleChangeHook: ({ options }: OnChangeHookParams) => {
396   - options.value = unref(options).filter(
397   - (item) => item.value !== formModel[DataSourceField.LATITUDE_ATTRIBUTE]
398   - );
399   - },
400   - getPopupContainer: () => document.body,
401   - };
  260 + {
  261 + field: DataSourceField.DEVICE_RENAME,
  262 + component: 'Input',
  263 + label: '设备',
  264 + colProps: { span: 8 },
  265 + componentProps: {
  266 + placeholder: '设备重命名',
  267 + },
402 268 },
403   - },
404   - {
405   - field: DataSourceField.LATITUDE_ATTRIBUTE,
406   - component: 'ApiSearchSelect',
407   - label: '纬度属性',
408   - colProps: { span: 8 },
409   - rules: [{ required: true, message: '属性为必填项' }],
410   - componentProps({ formModel }) {
411   - const organizationId = formModel[DataSourceField.ORIGINATION_ID];
412   - const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE];
413   - const deviceId = formModel[DataSourceField.DEVICE_ID];
414   - const slaveDeviceId = formModel[DataSourceField.SLAVE_DEVICE_ID];
415   - return {
416   - api: async () => {
417   - if (organizationId && deviceId) {
418   - try {
419   - if (isGatewayDevice && slaveDeviceId) {
420   - return getDeviceAttribute(slaveDeviceId);
421   - }
422   - if (!isGatewayDevice) {
423   - return await getDeviceAttribute(deviceId);
424   - }
425   - } catch (error) {}
426   - }
427   - return [];
428   - },
429   - dropdownVisibleChangeHook: ({ options }: OnChangeHookParams) => {
430   - options.value = unref(options).filter(
431   - (item) => item.value !== formModel[DataSourceField.LONGITUDE_ATTRIBUTE]
432   - );
433   - },
434   - placeholder: '请选择纬度属性',
435   - getPopupContainer: () => document.body,
436   - };
  269 + {
  270 + field: DataSourceField.ATTRIBUTE_RENAME,
  271 + component: 'Input',
  272 + label: '属性',
  273 + colProps: { span: 8 },
  274 + componentProps: {
  275 + placeholder: '属性重命名',
  276 + },
437 277 },
438   - },
439   -];
  278 + ];
  279 +};
... ...
1 1 import moment from 'moment';
2 2 import { Moment } from 'moment';
3   -import { getDeviceAttributes } from '/@/api/dataBoard';
4 3 import { FormSchema } from '/@/components/Form';
5 4 import { ColEx } from '/@/components/Form/src/types';
6 5 import { useGridLayout } from '/@/hooks/component/useGridLayout';
... ... @@ -134,7 +133,6 @@ export const formSchema: FormSchema[] = [
134 133 field: SchemaFiled.AGG,
135 134 label: '数据聚合功能',
136 135 component: 'Select',
137   - // defaultValue: AggregateDataEnum.NONE,
138 136 componentProps: {
139 137 getPopupContainer: () => document.body,
140 138 options: [
... ... @@ -181,7 +179,6 @@ export const formSchema: FormSchema[] = [
181 179 field: SchemaFiled.LIMIT,
182 180 label: '最大条数',
183 181 component: 'InputNumber',
184   - // defaultValue: 7,
185 182 ifShow({ values }) {
186 183 return values[SchemaFiled.AGG] === AggregateDataEnum.NONE;
187 184 },
... ... @@ -201,34 +198,30 @@ export const formSchema: FormSchema[] = [
201 198 componentProps: {
202 199 getPopupContainer: () => document.body,
203 200 },
204   - // componentProps({ formModel }) {
205   - // const deviceId = formModel[SchemaFiled.DEVICE_ID];
206   - // return {
207   - // api: async () => {
208   - // if (deviceId) {
209   - // try {
210   - // try {
211   - // const data = await getDeviceAttributes({ deviceId });
212   - // if (data) return data.map((item) => ({ label: item, value: item }));
213   - // } catch (error) {}
214   - // return [];
215   - // } catch (error) {}
216   - // }
217   - // return [];
218   - // },
219   - // getPopupContainer: () => document.body,
220   - // };
221   - // },
222 201 },
223 202 ];
224 203
225   -// export const selectDeviceAttrSchema: FormSchema[] = [
226   -// {
227   -// field: 'keys',
228   -// label: '设备属性',
229   -// component: 'Select',
230   -// componentProps: {
231   -// getPopupContainer: () => document.body,
232   -// },
233   -// },
234   -// ];
  204 +export function getHistorySearchParams(value: Recordable) {
  205 + const { startTs, endTs, interval, agg, limit, way, keys, deviceId } = value;
  206 + if (way === QueryWay.LATEST) {
  207 + return {
  208 + keys,
  209 + entityId: deviceId,
  210 + startTs: moment().subtract(startTs, 'ms').valueOf(),
  211 + endTs: Date.now(),
  212 + interval,
  213 + agg,
  214 + limit,
  215 + };
  216 + } else {
  217 + return {
  218 + keys,
  219 + entityId: deviceId,
  220 + startTs: moment(startTs).valueOf(),
  221 + endTs: moment(endTs).valueOf(),
  222 + interval,
  223 + agg,
  224 + limit,
  225 + };
  226 + }
  227 +}
... ...
... ... @@ -178,6 +178,39 @@ export const modeFour: FormSchema[] = [
178 178 },
179 179 ];
180 180
  181 +export const modeFive: FormSchema[] = [
  182 + {
  183 + field: visualOptionField.FONT_COLOR,
  184 + label: '数值字体颜色',
  185 + component: 'ColorPicker',
  186 + changeEvent: 'update:value',
  187 + componentProps: {
  188 + defaultValue: '#000',
  189 + },
  190 + },
  191 + {
  192 + field: visualOptionField.ICON_COLOR,
  193 + label: '图标颜色',
  194 + component: 'ColorPicker',
  195 + changeEvent: 'update:value',
  196 + componentProps: {
  197 + defaultValue: '#367BFF',
  198 + },
  199 + },
  200 + {
  201 + field: visualOptionField.ICON,
  202 + label: '图标',
  203 + component: 'IconDrawer',
  204 + changeEvent: 'update:value',
  205 + componentProps({ formModel }) {
  206 + const color = formModel[visualOptionField.ICON_COLOR];
  207 + return {
  208 + color,
  209 + };
  210 + },
  211 + },
  212 +];
  213 +
181 214 export const schemasMap = new Map<FrontComponent, FormSchema[]>();
182 215
183 216 schemasMap.set(FrontComponent.TEXT_COMPONENT_1, modeOne);
... ... @@ -188,3 +221,6 @@ schemasMap.set(FrontComponent.TEXT_COMPONENT_5, modeTwo);
188 221 schemasMap.set(FrontComponent.INSTRUMENT_COMPONENT_1, modeOne);
189 222 schemasMap.set(FrontComponent.INSTRUMENT_COMPONENT_2, modeThree);
190 223 schemasMap.set(FrontComponent.DIGITAL_DASHBOARD_COMPONENT, modeFour);
  224 +schemasMap.set(FrontComponent.CONTROL_COMPONENT_SWITCH_WITH_ICON, modeFive);
  225 +schemasMap.set(FrontComponent.CONTROL_COMPONENT_SLIDING_SWITCH, modeOne);
  226 +schemasMap.set(FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH, modeOne);
... ...
... ... @@ -119,7 +119,7 @@
119 119 data.width = newWPx;
120 120 data.height = newHPx;
121 121
122   - data.record.dataSource = data?.record.dataSource.map((item) => {
  122 + data.record.dataSource = data.record.dataSource.map((item) => {
123 123 if (!item.uuid) item.uuid = buildUUID();
124 124 return {
125 125 ...item,
... ... @@ -265,9 +265,9 @@
265 265 record: item,
266 266 };
267 267 });
268   -
269 268 beginSendMessage();
270 269 } catch (error) {
  270 + throw error;
271 271 } finally {
272 272 loading.value = false;
273 273 }
... ... @@ -310,7 +310,7 @@
310 310
311 311 const getComponentConfig = (
312 312 record: DataBoardLayoutInfo['record'],
313   - dataSourceRecord: DataSource
  313 + dataSourceRecord: DataSource | DataSource[]
314 314 ) => {
315 315 const frontComponent = record.frontId;
316 316 const component = frontComponentMap.get(frontComponent as FrontComponent);
... ... @@ -375,9 +375,13 @@
375 375 historyDataModalMethod.openModal(true, record);
376 376 };
377 377
  378 + const hasHistoryTrend = (item: DataBoardLayoutInfo) => {
  379 + return frontComponentMap.get(item.record.frontId as FrontComponent)?.hasHistoryTrend;
  380 + };
  381 +
378 382 onMounted(async () => {
379   - await injectBaiDuMapLib();
380   - await injectBaiDuMapTrackAniMationLib();
  383 + injectBaiDuMapLib();
  384 + injectBaiDuMapTrackAniMationLib();
381 385 getDataBoardComponent();
382 386 });
383 387 </script>
... ... @@ -438,10 +442,12 @@
438 442 @resize="itemResize"
439 443 @moved="itemMoved"
440 444 @container-resized="itemContainerResized"
  445 + drag-ignore-from=".no-drag"
441 446 >
442 447 <WidgetWrapper
443 448 :key="item.i"
444 449 :ref="(el: Element) => setComponentRef(el, item)"
  450 + :record="item.record"
445 451 :data-source="item.record.dataSource"
446 452 >
447 453 <template #header>
... ... @@ -455,7 +461,7 @@
455 461 <Tooltip title="趋势">
456 462 <img
457 463 :src="trendIcon"
458   - v-if="!getIsSharePage"
  464 + v-if="!getIsSharePage && hasHistoryTrend(item)"
459 465 class="cursor-pointer w-4.5 h-4.5"
460 466 @click="handleOpenHistroyDataModal(item.record.dataSource)"
461 467 />
... ...
... ... @@ -18,10 +18,13 @@ export interface ComponentConfig {
18 18 ComponentKey: FrontComponent;
19 19 ComponentConfig?: Recordable;
20 20 ComponentCategory: FrontComponentCategory;
  21 + isMultipleDataSource?: boolean;
  22 + hasHistoryTrend?: boolean;
  23 + hasSetting?: boolean;
21 24 transformConfig: (
22 25 componentConfig: Recordable,
23 26 record: DataComponentRecord,
24   - dataSourceRecord: DataSource
  27 + dataSourceRecord: DataSource | DataSource[]
25 28 ) => Recordable;
26 29 }
27 30
... ...