Commit afde031f90ddbab1e1b958c78a4668a84f509831
Merge branch 'main_dev'
# Conflicts: # src/views/message/template/TemplateDrawer.vue
Showing
79 changed files
with
1186 additions
and
310 deletions
... | ... | @@ -53,6 +53,7 @@ export interface DataBoardRecord { |
53 | 53 | publicId: string; |
54 | 54 | organizationId?: string; |
55 | 55 | accessCredentials?: string; |
56 | + platform?: string; | |
56 | 57 | } |
57 | 58 | |
58 | 59 | export interface DataBoardList { |
... | ... | @@ -137,7 +138,12 @@ export interface MasterDeviceList { |
137 | 138 | } |
138 | 139 | |
139 | 140 | export interface ComponentInfoDetail { |
140 | - data: { componentData: DataComponentRecord[]; componentLayout: Layout[] }; | |
141 | + data: { | |
142 | + componentData: DataComponentRecord[]; | |
143 | + componentLayout: Layout[]; | |
144 | + phoneModel?: string | object; | |
145 | + platform: string; | |
146 | + }; | |
141 | 147 | } |
142 | 148 | |
143 | 149 | export interface DeviceAttributeParams { | ... | ... |
... | ... | @@ -7,6 +7,9 @@ export interface Specs { |
7 | 7 | unit: string; |
8 | 8 | unitName: string; |
9 | 9 | |
10 | + dataType?: string; | |
11 | + name?: string; | |
12 | + value?: string; | |
10 | 13 | step: string; |
11 | 14 | length: string; |
12 | 15 | boolOpen: string; |
... | ... | @@ -20,6 +23,7 @@ export interface Specs { |
20 | 23 | export interface DataType { |
21 | 24 | type: DataTypeEnum; |
22 | 25 | specs?: Partial<Specs> | StructJSON[]; |
26 | + specsList?: Specs[]; | |
23 | 27 | } |
24 | 28 | |
25 | 29 | export interface StructJSON { | ... | ... |
... | ... | @@ -21,6 +21,7 @@ enum Api { |
21 | 21 | SendLoginSmsCode = '/noauth/send_login_code/', |
22 | 22 | ResetCode = '/noauth/reset_code/', |
23 | 23 | ResetPassword = '/noauth/reset/', |
24 | + APP_GET_TOKEN = '/third/login/id/', | |
24 | 25 | } |
25 | 26 | |
26 | 27 | /** |
... | ... | @@ -100,3 +101,9 @@ export const getUserToken = (id: string) => { |
100 | 101 | url: `/third/login/id/${id}`, |
101 | 102 | }); |
102 | 103 | }; |
104 | + | |
105 | +export const doAppLogin = (userId: string) => { | |
106 | + return defHttp.get<Record<'refreshToken' | 'token', string>>({ | |
107 | + url: `${Api.APP_GET_TOKEN}${userId}`, | |
108 | + }); | |
109 | +}; | ... | ... |
src/assets/images/sn-step4.png
0 → 100644
197 KB
src/assets/svg/battery.svg
0 → 100644
1 | +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1702374950759" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1997" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M144.700101 684.994006l535.580045 0L680.280146 359.237781 144.700101 359.237781 144.700101 684.994006 144.700101 684.994006zM918.373823 440.680675l0-81.442894c0-44.791136-36.649711-81.437777-81.437777-81.437777l-692.235944 0c-44.791136 0-81.437777 36.646642-81.437777 81.437777L63.262324 684.994006c0 44.791136 36.646642 81.442894 81.437777 81.442894l692.235944 0c44.788066 0 81.437777-36.650735 81.437777-81.442894l0-81.437777c22.396079 0 40.7194-18.322297 40.7194-40.7194l0-81.436754C959.093223 459.003995 940.769902 440.680675 918.373823 440.680675L918.373823 440.680675zM877.655446 481.400075l0 81.436754L877.655446 684.994006c0 22.395056-18.323321 40.718377-40.7194 40.718377l-692.235944 0c-22.396079 0-40.7194-18.323321-40.7194-40.718377L103.980701 359.237781c0-22.396079 18.323321-40.7194 40.7194-40.7194l692.235944 0c22.396079 0 40.7194 18.323321 40.7194 40.7194L877.655446 481.400075 877.655446 481.400075zM877.655446 481.400075" fill="#272636" p-id="1998"></path></svg> | |
\ No newline at end of file | ... | ... |
src/assets/svg/signal.svg
0 → 100644
1 | +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1702374946029" class="icon" viewBox="0 0 1294 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1850" xmlns:xlink="http://www.w3.org/1999/xlink" width="252.734375" height="200"><path d="M0 727.578947l188.631579 0 0 296.421053-188.631579 0 0-296.421053Z" p-id="1851"></path><path d="M269.473684 565.894737l188.631579 0 0 458.105263-188.631579 0 0-458.105263Z" p-id="1852"></path><path d="M565.894737 377.263158l161.684211 0 0 646.736842-161.684211 0 0-646.736842Z" p-id="1853"></path><path d="M835.368421 188.631579l188.631579 0 0 835.368421-188.631579 0 0-835.368421Z" p-id="1854"></path><path d="M1104.842105 0l188.631579 0 0 1024-188.631579 0 0-1024Z" p-id="1855"></path></svg> | |
\ No newline at end of file | ... | ... |
src/assets/svg/wifi.svg
0 → 100644
1 | +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1702374954963" class="icon" viewBox="0 0 1280 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2277" xmlns:xlink="http://www.w3.org/1999/xlink" width="250" height="200"><path d="M1269.82 309.76C915.48-17.98 364.38-17.86 10.18 309.76c-13.32 12.32-13.58 33.18-0.7 45.96l68.48 67.94c12.28 12.2 32.04 12.46 44.8 0.76 291.84-267.36 742.6-267.42 1034.5 0 12.76 11.7 32.52 11.42 44.8-0.76l68.48-67.94c12.86-12.78 12.6-33.64-0.72-45.96zM640 704c-70.7 0-128 57.3-128 128s57.3 128 128 128 128-57.3 128-128-57.3-128-128-128z m405.34-167.18c-230.52-203.86-580.42-203.64-810.68 0-13.8 12.2-14.24 33.38-1.14 46.3l68.88 67.98c12 11.84 31.32 12.64 44.1 1.6 167.9-145.14 419.48-144.82 586.98 0 12.78 11.04 32.1 10.26 44.1-1.6l68.88-67.98c13.12-12.92 12.66-34.12-1.12-46.3z" p-id="2278"></path></svg> | |
\ No newline at end of file | ... | ... |
... | ... | @@ -42,6 +42,7 @@ import InputGroup from './components/InputGroup.vue'; |
42 | 42 | import RegisterAddressInput from '/@/views/task/center/components/PollCommandInput/RegisterAddressInput.vue'; |
43 | 43 | import ExtendDesc from '/@/components/Form/src/externalCompns/components/ExtendDesc/index.vue'; |
44 | 44 | import DeviceProfileForm from '/@/components/Form/src/externalCompns/components/DeviceProfileForm/index.vue'; |
45 | +import EnumList from './externalCompns/components/StructForm/EnumList.vue'; | |
45 | 46 | |
46 | 47 | const componentMap = new Map<ComponentType, Component>(); |
47 | 48 | |
... | ... | @@ -90,6 +91,7 @@ componentMap.set('ApiSelectScrollLoad', ApiSelectScrollLoad); |
90 | 91 | componentMap.set('InputGroup', InputGroup); |
91 | 92 | componentMap.set('RegisterAddressInput', RegisterAddressInput); |
92 | 93 | componentMap.set('ExtendDesc', ExtendDesc); |
94 | +componentMap.set('EnumList', EnumList); | |
93 | 95 | componentMap.set('DeviceProfileForm', DeviceProfileForm); |
94 | 96 | |
95 | 97 | export function add(compName: ComponentType, component: Component) { | ... | ... |
1 | +import { FormSchema } from '/@/components/Table'; | |
2 | + | |
3 | +export enum FormFieldsEnum { | |
4 | + VALUE = 'value', | |
5 | + NAME = 'name', | |
6 | + DATA_TYPE = 'dataType', | |
7 | +} | |
8 | + | |
9 | +export const getFormSchemas = (): FormSchema[] => { | |
10 | + return [ | |
11 | + { | |
12 | + field: FormFieldsEnum.VALUE, | |
13 | + label: '', | |
14 | + component: 'InputNumber', | |
15 | + rules: [ | |
16 | + { required: true, message: `支持整型,取值范围:-2147483648 ~ 2147483647`, type: 'number' }, | |
17 | + ], | |
18 | + componentProps: () => { | |
19 | + return { | |
20 | + placeholder: '编号如"0"', | |
21 | + min: -2147483648, | |
22 | + max: 2147483647, | |
23 | + step: 1, | |
24 | + precision: 0, | |
25 | + }; | |
26 | + }, | |
27 | + colProps: { | |
28 | + span: 11, | |
29 | + }, | |
30 | + }, | |
31 | + { | |
32 | + field: 'division', | |
33 | + label: '', | |
34 | + component: 'Input', | |
35 | + slot: 'division', | |
36 | + colProps: { | |
37 | + span: 1, | |
38 | + }, | |
39 | + }, | |
40 | + { | |
41 | + field: FormFieldsEnum.NAME, | |
42 | + label: '', | |
43 | + component: 'Input', | |
44 | + rules: [ | |
45 | + { | |
46 | + required: true, | |
47 | + message: `支持中文、英文大小写、数字、下划线和短划线,必须以中文、英文或数字开头,不超过20个字符`, | |
48 | + type: 'string', | |
49 | + pattern: /^[a-zA-Z0-9\u4e00-\u9fa5a][\u4e00-\u9fa5a-zA-Z0-9_-]*$/, | |
50 | + }, | |
51 | + ], | |
52 | + componentProps: () => { | |
53 | + return { | |
54 | + placeholder: '对该枚举项的描述', | |
55 | + maxLength: 20, | |
56 | + }; | |
57 | + }, | |
58 | + colProps: { | |
59 | + span: 11, | |
60 | + }, | |
61 | + }, | |
62 | + ]; | |
63 | +}; | ... | ... |
1 | +<script setup lang="ts"> | |
2 | + import { Button, Tooltip } from 'ant-design-vue'; | |
3 | + import { computed, nextTick, ref, unref, watch } from 'vue'; | |
4 | + import { useForm, BasicForm } from '/@/components/Form'; | |
5 | + import { Specs } from '/@/api/device/model/modelOfMatterModel'; | |
6 | + import { Icon } from '/@/components/Icon'; | |
7 | + import { getFormSchemas } from './EnumList.config'; | |
8 | + import { FormActionType } from '../../../types/form'; | |
9 | + import { buildUUID } from '/@/utils/uuid'; | |
10 | + import { DataTypeEnum } from '/@/enums/objectModelEnum'; | |
11 | + import { isNullOrUnDef } from '/@/utils/is'; | |
12 | + | |
13 | + const props = defineProps<{ disabled?: boolean; value?: Specs[] }>(); | |
14 | + | |
15 | + interface EnumElItemType { | |
16 | + uuid: string; | |
17 | + formActionType?: FormActionType; | |
18 | + dataSource?: Recordable; | |
19 | + } | |
20 | + | |
21 | + const [registerForm] = useForm({ | |
22 | + schemas: getFormSchemas(), | |
23 | + showActionButtonGroup: false, | |
24 | + layout: 'inline', | |
25 | + }); | |
26 | + | |
27 | + const enumsListElRef = ref<EnumElItemType[]>([{ uuid: buildUUID() }]); | |
28 | + | |
29 | + const setFormActionType = (item: EnumElItemType, el: any) => { | |
30 | + item.formActionType = el as unknown as FormActionType; | |
31 | + }; | |
32 | + | |
33 | + const getEnumsLimit = computed(() => unref(enumsListElRef).length >= 100); | |
34 | + | |
35 | + const hasSameEnum = ref(false); | |
36 | + | |
37 | + const validateSameEnum = () => { | |
38 | + const value = getFieldsValue(); | |
39 | + hasSameEnum.value = false; | |
40 | + const values = value.map((item) => item.value).filter((value) => !isNullOrUnDef(value)); | |
41 | + const names = value.map((item) => item.name).filter((value) => !isNullOrUnDef(value)); | |
42 | + | |
43 | + if (values.length !== new Set(values).size || names.length !== new Set(names).size) { | |
44 | + hasSameEnum.value = true; | |
45 | + } | |
46 | + }; | |
47 | + | |
48 | + const validate = async () => { | |
49 | + if (unref(hasSameEnum)) throw Error('存在相同的枚举'); | |
50 | + for (const enumElItem of unref(enumsListElRef)) { | |
51 | + await enumElItem.formActionType?.validate?.(); | |
52 | + } | |
53 | + validateSameEnum(); | |
54 | + }; | |
55 | + | |
56 | + const getFieldsValue = () => { | |
57 | + return unref(enumsListElRef).map( | |
58 | + (item) => | |
59 | + ({ | |
60 | + ...(item.formActionType?.getFieldsValue?.() || {}), | |
61 | + dataType: DataTypeEnum.ENUM, | |
62 | + } as Specs) | |
63 | + ); | |
64 | + }; | |
65 | + | |
66 | + const setFieldsValue = (spaceList: Specs[]) => { | |
67 | + enumsListElRef.value = spaceList.map((item) => ({ uuid: buildUUID(), dataSource: item })); | |
68 | + | |
69 | + nextTick(() => { | |
70 | + unref(enumsListElRef).forEach((item) => | |
71 | + item.formActionType?.setFieldsValue?.(item.dataSource) | |
72 | + ); | |
73 | + }); | |
74 | + }; | |
75 | + | |
76 | + const handleDeleteEnums = (item: EnumElItemType) => { | |
77 | + const index = unref(enumsListElRef).findIndex((temp) => item.uuid === temp.uuid); | |
78 | + | |
79 | + if (~index) { | |
80 | + enumsListElRef.value.splice(index, 1); | |
81 | + validateSameEnum(); | |
82 | + } | |
83 | + }; | |
84 | + | |
85 | + const handleAddEnums = () => { | |
86 | + unref(enumsListElRef).push({ uuid: buildUUID() }); | |
87 | + }; | |
88 | + | |
89 | + watch( | |
90 | + () => props.value, | |
91 | + (target) => { | |
92 | + setFieldsValue(target || [{} as Specs]); | |
93 | + }, | |
94 | + { | |
95 | + immediate: true, | |
96 | + } | |
97 | + ); | |
98 | + | |
99 | + defineExpose({ | |
100 | + validate, | |
101 | + getFieldsValue, | |
102 | + setFieldsValue, | |
103 | + }); | |
104 | +</script> | |
105 | + | |
106 | +<template> | |
107 | + <section class="w-full"> | |
108 | + <header class="flex h-8 items-center"> | |
109 | + <div class="w-1/2"> | |
110 | + <span>参考值</span> | |
111 | + <Tooltip title="支持整型,取值范围:-2147483648 ~ 2147483647"> | |
112 | + <Icon icon="ant-design:question-circle-outlined" class="cursor-pointer ml-1" /> | |
113 | + </Tooltip> | |
114 | + </div> | |
115 | + <div class="w-1/2"> | |
116 | + <span>参考描述</span> | |
117 | + <Tooltip | |
118 | + title="支持中文、英文大小写、数字、下划线和短划线,必须以中文、英文或数字开头,不超过20个字符" | |
119 | + > | |
120 | + <Icon icon="ant-design:question-circle-outlined" class="cursor-pointer ml-1" /> | |
121 | + </Tooltip> | |
122 | + </div> | |
123 | + </header> | |
124 | + <main class="w-full"> | |
125 | + <section class="w-full flex" v-for="item in enumsListElRef" :key="item.uuid"> | |
126 | + <BasicForm | |
127 | + :ref="(el) => setFormActionType(item, el)" | |
128 | + @register="registerForm" | |
129 | + class="enums-form" | |
130 | + :disabled="disabled" | |
131 | + @field-value-change="validateSameEnum" | |
132 | + > | |
133 | + <template #division> | |
134 | + <div>~</div> | |
135 | + </template> | |
136 | + </BasicForm> | |
137 | + <Button | |
138 | + type="link" | |
139 | + class="relative -left-6" | |
140 | + :disabled="disabled" | |
141 | + @click="handleDeleteEnums(item)" | |
142 | + > | |
143 | + 删除 | |
144 | + </Button> | |
145 | + </section> | |
146 | + </main> | |
147 | + <div v-if="hasSameEnum" class="text-red-400">枚举项中存在相同的参数值或参数描述</div> | |
148 | + <Tooltip title="枚举项最多创建 100 个"> | |
149 | + <Button type="link" @click="handleAddEnums" :disabled="disabled || getEnumsLimit"> | |
150 | + +添加枚举项 | |
151 | + </Button> | |
152 | + </Tooltip> | |
153 | + </section> | |
154 | +</template> | |
155 | + | |
156 | +<style scoped lang="less"> | |
157 | + .enums-form { | |
158 | + @apply w-full; | |
159 | + | |
160 | + > :deep(.ant-row) { | |
161 | + @apply w-full; | |
162 | + | |
163 | + .ant-input-number { | |
164 | + width: 100%; | |
165 | + } | |
166 | + } | |
167 | + } | |
168 | +</style> | ... | ... |
... | ... | @@ -14,6 +14,8 @@ |
14 | 14 | import { DataType, StructJSON } from '/@/api/device/model/modelOfMatterModel'; |
15 | 15 | import { isArray } from '/@/utils/is'; |
16 | 16 | import { useMessage } from '/@/hooks/web/useMessage'; |
17 | + import EnumList from './EnumList.vue'; | |
18 | + import { DataTypeEnum } from '/@/enums/objectModelEnum'; | |
17 | 19 | |
18 | 20 | const modalReceiveRecord = ref<OpenModalParams>({ |
19 | 21 | mode: OpenModalMode.CREATE, |
... | ... | @@ -26,6 +28,8 @@ |
26 | 28 | hiddenAccessMode: boolean; |
27 | 29 | }>(); |
28 | 30 | |
31 | + const enumListRef = ref<InstanceType<typeof EnumList>>(); | |
32 | + | |
29 | 33 | const emit = defineEmits(['register', 'submit']); |
30 | 34 | |
31 | 35 | const { createMessage } = useMessage(); |
... | ... | @@ -53,13 +57,14 @@ |
53 | 57 | modalReceiveRecord.value = record; |
54 | 58 | const data = record.record || {}; |
55 | 59 | const { dataType = {} } = data! as StructJSON; |
56 | - const { specs = {}, type } = dataType as DataType; | |
60 | + const { specs = {}, type, specsList } = dataType as DataType; | |
57 | 61 | |
58 | 62 | if (record.record) { |
59 | 63 | const value = { |
60 | 64 | type, |
61 | 65 | ...data, |
62 | 66 | ...(isArray(specs) ? { specs } : { ...specs }), |
67 | + enumList: type === DataTypeEnum.ENUM ? specsList : [], | |
63 | 68 | }; |
64 | 69 | |
65 | 70 | setFieldsValue(value); |
... | ... | @@ -74,7 +79,8 @@ |
74 | 79 | const handleSubmit = async () => { |
75 | 80 | try { |
76 | 81 | const _value = await validate(); |
77 | - let structJSON = transfromToStructJSON(_value); | |
82 | + await unref(enumListRef)?.validate?.(); | |
83 | + let structJSON = transfromToStructJSON(_value, unref(enumListRef)?.getFieldsValue?.() || []); | |
78 | 84 | const value = { |
79 | 85 | ...structJSON, |
80 | 86 | ...(unref(modalReceiveRecord)?.record?.id |
... | ... | @@ -104,7 +110,11 @@ |
104 | 110 | destroy-on-close |
105 | 111 | :show-ok-btn="!$props.disabled" |
106 | 112 | > |
107 | - <BasicForm @register="register" :schemas="getFormSchemas" /> | |
113 | + <BasicForm @register="register" :schemas="getFormSchemas"> | |
114 | + <template #EnumList="{ field, model }"> | |
115 | + <EnumList ref="enumListRef" :value="model[field]" :disabled="disabled" /> | |
116 | + </template> | |
117 | + </BasicForm> | |
108 | 118 | </BasicModal> |
109 | 119 | </template> |
110 | 120 | ... | ... |
... | ... | @@ -37,7 +37,7 @@ const validateExcludeComma = (field: string, errorName: string): Rule[] => { |
37 | 37 | validator: () => { |
38 | 38 | const reg = /[,,]+/; |
39 | 39 | if (reg.test(field)) { |
40 | - return Promise.reject(errorName); | |
40 | + return Promise.reject(`${errorName}不能包含逗号`); | |
41 | 41 | } |
42 | 42 | return Promise.resolve(); |
43 | 43 | }, |
... | ... | @@ -64,7 +64,10 @@ export const formSchemas = ({ |
64 | 64 | placeholder: '请输入功能名称', |
65 | 65 | }, |
66 | 66 | dynamicRules: ({ values }) => { |
67 | - return validateExcludeComma(values[FormField.FUNCTION_NAME], '功能名称不能包含逗号'); | |
67 | + return [ | |
68 | + { required: true, message: '请输入功能名称' }, | |
69 | + ...validateExcludeComma(values[FormField.FUNCTION_NAME], '功能名称'), | |
70 | + ]; | |
68 | 71 | }, |
69 | 72 | }, |
70 | 73 | { |
... | ... | @@ -80,7 +83,10 @@ export const formSchemas = ({ |
80 | 83 | placeholder: '请输入标识符', |
81 | 84 | }, |
82 | 85 | dynamicRules: ({ values }) => { |
83 | - return validateExcludeComma(values[FormField.IDENTIFIER], '标识符不能包含逗号'); | |
86 | + return [ | |
87 | + { required: true, message: '请输入标识符' }, | |
88 | + ...validateExcludeComma(values[FormField.IDENTIFIER], '标识符'), | |
89 | + ]; | |
84 | 90 | }, |
85 | 91 | }, |
86 | 92 | { |
... | ... | @@ -105,6 +111,19 @@ export const formSchemas = ({ |
105 | 111 | api: async (params: Recordable) => { |
106 | 112 | try { |
107 | 113 | const record = await findDictItemByCode(params); |
114 | + | |
115 | + if (isTcp) { | |
116 | + // TCP 产品 属性可创建范围 | |
117 | + return record.filter((item) => | |
118 | + [ | |
119 | + DataTypeEnum.BOOL, | |
120 | + DataTypeEnum.NUMBER_DOUBLE, | |
121 | + DataTypeEnum.NUMBER_INT, | |
122 | + DataTypeEnum.STRING, | |
123 | + ].includes(item.itemValue as DataTypeEnum) | |
124 | + ); | |
125 | + } | |
126 | + | |
108 | 127 | return hasStructForm |
109 | 128 | ? record.filter((item) => item.itemValue !== DataTypeEnum.STRUCT) |
110 | 129 | : record; |
... | ... | @@ -127,6 +146,16 @@ export const formSchemas = ({ |
127 | 146 | }, |
128 | 147 | }, |
129 | 148 | { |
149 | + field: FormField.ENUM_LIST, | |
150 | + component: 'Input', | |
151 | + label: '枚举', | |
152 | + ifShow: ({ values }) => values[FormField.TYPE] === DataTypeEnum.ENUM, | |
153 | + slot: 'EnumList', | |
154 | + colProps: { | |
155 | + span: 24, | |
156 | + }, | |
157 | + }, | |
158 | + { | |
130 | 159 | field: FormField.VALUE_RANGE, |
131 | 160 | label: '取值范围', |
132 | 161 | component: 'CustomMinMaxInput', | ... | ... |
1 | 1 | import { cloneDeep } from 'lodash-es'; |
2 | 2 | import { StructFormValue } from './type'; |
3 | -import { DataType, ModelOfMatterParams, StructJSON } from '/@/api/device/model/modelOfMatterModel'; | |
3 | +import { | |
4 | + DataType, | |
5 | + ModelOfMatterParams, | |
6 | + Specs, | |
7 | + StructJSON, | |
8 | +} from '/@/api/device/model/modelOfMatterModel'; | |
4 | 9 | import { isArray } from '/@/utils/is'; |
5 | 10 | import { DataTypeEnum } from '/@/enums/objectModelEnum'; |
6 | 11 | |
7 | -export function transfromToStructJSON(value: StructFormValue): StructJSON { | |
12 | +export function transfromToStructJSON(value: StructFormValue, enumList: Specs[] = []): StructJSON { | |
8 | 13 | const { |
9 | 14 | type, |
10 | 15 | valueRange, |
... | ... | @@ -55,6 +60,13 @@ export function transfromToStructJSON(value: StructFormValue): StructJSON { |
55 | 60 | }; |
56 | 61 | break; |
57 | 62 | |
63 | + case DataTypeEnum.ENUM: | |
64 | + dataType = { | |
65 | + type, | |
66 | + specsList: enumList, | |
67 | + }; | |
68 | + break; | |
69 | + | |
58 | 70 | case DataTypeEnum.STRUCT: |
59 | 71 | dataType = { |
60 | 72 | type, |
... | ... | @@ -62,12 +74,13 @@ export function transfromToStructJSON(value: StructFormValue): StructJSON { |
62 | 74 | }; |
63 | 75 | break; |
64 | 76 | } |
65 | - return { ...basic, dataType }; | |
77 | + return { ...basic, dataType } as StructJSON; | |
66 | 78 | } |
67 | 79 | |
68 | 80 | export const excludeIdInStructJSON = (struct: DataType) => { |
69 | 81 | const _value = cloneDeep(struct); |
70 | 82 | const { specs } = _value; |
83 | + if (!specs) return _value; | |
71 | 84 | const list = [specs]; |
72 | 85 | |
73 | 86 | while (list.length) { |
... | ... | @@ -77,10 +90,10 @@ export const excludeIdInStructJSON = (struct: DataType) => { |
77 | 90 | if (temp.dataType?.specs) { |
78 | 91 | list.push(temp.dataType.specs); |
79 | 92 | } |
80 | - Reflect.deleteProperty(temp, 'id'); | |
93 | + Reflect.has(temp, 'id') && Reflect.deleteProperty(temp, 'id'); | |
81 | 94 | }); |
82 | 95 | } else { |
83 | - Reflect.deleteProperty(item as Recordable, 'id'); | |
96 | + Reflect.has(item as Recordable, 'id') && Reflect.deleteProperty(item as Recordable, 'id'); | |
84 | 97 | } |
85 | 98 | list.shift(); |
86 | 99 | } | ... | ... |
... | ... | @@ -101,6 +101,26 @@ export const getFormSchemas = ({ |
101 | 101 | }; |
102 | 102 | }; |
103 | 103 | |
104 | + const createEnumsSelect = ({ identifier, functionName, dataType }: StructJSON): FormSchema => { | |
105 | + const { specsList } = dataType || {}; | |
106 | + return { | |
107 | + field: identifier, | |
108 | + label: functionName!, | |
109 | + component: 'Select', | |
110 | + rules: [ | |
111 | + { | |
112 | + required, | |
113 | + message: `${functionName}是必填项`, | |
114 | + type: 'number', | |
115 | + }, | |
116 | + ], | |
117 | + componentProps: { | |
118 | + options: specsList?.map((item) => ({ label: item.name, value: item.value })), | |
119 | + placeholder: `请选择${functionName}`, | |
120 | + }, | |
121 | + }; | |
122 | + }; | |
123 | + | |
104 | 124 | const createStructJson = ({ identifier, functionName, dataType }: StructJSON): FormSchema => { |
105 | 125 | return { |
106 | 126 | field: identifier, |
... | ... | @@ -140,6 +160,7 @@ export const getFormSchemas = ({ |
140 | 160 | } |
141 | 161 | |
142 | 162 | if (type === DataTypeEnum.BOOL) schemas.push(createSelect(item)); |
163 | + else if (type === DataTypeEnum.ENUM) schemas.push(createEnumsSelect(item)); | |
143 | 164 | else if (type === DataTypeEnum.NUMBER_INT) schemas.push(createInputNumber(item)); |
144 | 165 | else if (type === DataTypeEnum.NUMBER_DOUBLE) schemas.push(createInputNumber(item)); |
145 | 166 | else if (type === DataTypeEnum.STRING) schemas.push(createInput(item)); | ... | ... |
... | ... | @@ -4,12 +4,12 @@ |
4 | 4 | <Tooltip v-if="action.tooltip" v-bind="getTooltip(action.tooltip)"> |
5 | 5 | <PopConfirmButton v-bind="action"> |
6 | 6 | <Icon :icon="action.icon" :class="{ 'mr-1': !!action.label }" v-if="action.icon" /> |
7 | - <template v-if="action.label">{{ action.label }}</template> | |
7 | + <template v-if="action.label"><Label :label="action.label" /></template> | |
8 | 8 | </PopConfirmButton> |
9 | 9 | </Tooltip> |
10 | 10 | <PopConfirmButton v-else v-bind="action"> |
11 | 11 | <Icon :icon="action.icon" :class="{ 'mr-1': !!action.label }" v-if="action.icon" /> |
12 | - <template v-if="action.label">{{ action.label }}</template> | |
12 | + <template v-if="action.label"><Label :label="action.label" /> </template> | |
13 | 13 | </PopConfirmButton> |
14 | 14 | <Divider |
15 | 15 | type="vertical" |
... | ... | @@ -36,7 +36,7 @@ |
36 | 36 | </div> |
37 | 37 | </template> |
38 | 38 | <script lang="ts"> |
39 | - import { defineComponent, PropType, computed, toRaw, unref } from 'vue'; | |
39 | + import { defineComponent, PropType, computed, toRaw, unref, VNode } from 'vue'; | |
40 | 40 | // import { MoreOutlined } from '@ant-design/icons-vue'; |
41 | 41 | import { Divider, Tooltip, TooltipProps } from 'ant-design-vue'; |
42 | 42 | import Icon from '/@/components/Icon/index'; |
... | ... | @@ -52,7 +52,14 @@ |
52 | 52 | |
53 | 53 | export default defineComponent({ |
54 | 54 | name: 'TableAction', |
55 | - components: { Icon, PopConfirmButton, Divider, Dropdown, Tooltip }, | |
55 | + components: { | |
56 | + Icon, | |
57 | + PopConfirmButton, | |
58 | + Divider, | |
59 | + Dropdown, | |
60 | + Tooltip, | |
61 | + Label: (props: { label: VNode | string }) => props.label, | |
62 | + }, | |
56 | 63 | props: { |
57 | 64 | actions: { |
58 | 65 | type: Array as PropType<ActionItem[]>, | ... | ... |
1 | 1 | import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes'; |
2 | 2 | import { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip'; |
3 | 3 | import { RoleEnum } from '/@/enums/roleEnum'; |
4 | +import { VNode } from 'vue'; | |
4 | 5 | export interface ActionItem extends ButtonProps { |
5 | 6 | onClick?: Fn; |
6 | - label?: string; | |
7 | + label?: string | VNode; | |
7 | 8 | color?: 'success' | 'error' | 'warning'; |
8 | 9 | icon?: string; |
9 | 10 | popConfirm?: PopConfirm; | ... | ... |
... | ... | @@ -2,7 +2,7 @@ import { RouteLocationNormalizedLoaded } from 'vue-router'; |
2 | 2 | |
3 | 3 | const menuMap = new Map(); |
4 | 4 | |
5 | -menuMap.set('/visual/board/detail/:boardId/:boardName/:organizationId?', '/visual/board'); | |
5 | +menuMap.set('/visual/board/detail/:boardId/:boardName/:platform/:organizationId?', '/visual/board'); | |
6 | 6 | menuMap.set('/rule/chain/:id', '/rule/chain'); |
7 | 7 | |
8 | 8 | export const useMenuActiveFix = (route: RouteLocationNormalizedLoaded) => { | ... | ... |
... | ... | @@ -11,7 +11,8 @@ import { getAuthCache } from '/@/utils/auth'; |
11 | 11 | const LOGIN_PATH = PageEnum.BASE_LOGIN; |
12 | 12 | const ROOT_PATH = RootRoute.path; |
13 | 13 | const SHARE_PATH = PageEnum.SHARE_PAGE; |
14 | -const whitePathList: string[] = [LOGIN_PATH, SHARE_PATH]; | |
14 | +const APP_PATH = PageEnum.APP_PAGE; | |
15 | +const whitePathList: string[] = [LOGIN_PATH, SHARE_PATH, APP_PATH]; | |
15 | 16 | // const userInfo1 = getAuthCache(USER_INFO_KEY); |
16 | 17 | // const userInfo = ref(userInfo1); |
17 | 18 | ... | ... |
src/router/routes/appPage.ts
0 → 100644
1 | +import { AppRouteRecordRaw } from '../types'; | |
2 | +import { PageEnum } from '/@/enums/pageEnum'; | |
3 | + | |
4 | +export const APP_PAGE_ROUTER: AppRouteRecordRaw = { | |
5 | + path: PageEnum.APP_PAGE, | |
6 | + name: 'appPage', | |
7 | + component: () => import('/@/views/sys/appPage/index.vue'), | |
8 | + meta: { | |
9 | + title: '公开', | |
10 | + hideBreadcrumb: true, | |
11 | + hideChildrenInMenu: true, | |
12 | + }, | |
13 | +}; | ... | ... |
... | ... | @@ -5,6 +5,7 @@ import { PageEnum } from '/@/enums/pageEnum'; |
5 | 5 | import { t } from '/@/hooks/web/useI18n'; |
6 | 6 | import { LAYOUT } from '../constant'; |
7 | 7 | import { PUBLIC_PAGE_ROUTER } from './public'; |
8 | +import { APP_PAGE_ROUTER } from './appPage'; | |
8 | 9 | |
9 | 10 | const modules = import.meta.globEager('./modules/**/*.ts'); |
10 | 11 | const routeModuleList: AppRouteModule[] = []; |
... | ... | @@ -87,4 +88,5 @@ export const basicRoutes = [ |
87 | 88 | REDIRECT_ROUTE, |
88 | 89 | PAGE_NOT_FOUND_ROUTE, |
89 | 90 | PUBLIC_PAGE_ROUTER, |
91 | + APP_PAGE_ROUTER, | |
90 | 92 | ]; | ... | ... |
... | ... | @@ -12,7 +12,16 @@ |
12 | 12 | <a-select |
13 | 13 | placeholder="请选择流媒体配置" |
14 | 14 | v-model:value="model[field]" |
15 | - :options="streamConfigOptions.map((item) => ({ value: item.value, label: item.label }))" | |
15 | + :options=" | |
16 | + streamConfigOptions | |
17 | + .filter((item) => item.type === model.videoType) | |
18 | + .map((item) => ({ | |
19 | + value: item.value, | |
20 | + label: item.label, | |
21 | + type: item.type, | |
22 | + })) | |
23 | + " | |
24 | + @change="handleChange" | |
16 | 25 | > |
17 | 26 | <template #dropdownRender="{ menuNode: menu }"> |
18 | 27 | <v-nodes :vnodes="menu" /> |
... | ... | @@ -31,7 +40,7 @@ |
31 | 40 | <script lang="ts"> |
32 | 41 | import { defineComponent, ref, computed, unref, nextTick, onMounted } from 'vue'; |
33 | 42 | import { BasicForm, useForm } from '/@/components/Form'; |
34 | - import { formSchema } from './config.data'; | |
43 | + import { AccessMode, formSchema, VideoPlatformEnum } from './config.data'; | |
35 | 44 | import { BasicDrawer, useDrawerInner } from '/@/components/Drawer'; |
36 | 45 | import { createOrEditCameraManage } from '/@/api/camera/cameraManager'; |
37 | 46 | import { useMessage } from '/@/hooks/web/useMessage'; |
... | ... | @@ -70,6 +79,7 @@ |
70 | 79 | return { |
71 | 80 | label: m.host, |
72 | 81 | value: m.id, |
82 | + type: m.type, | |
73 | 83 | }; |
74 | 84 | }); |
75 | 85 | }); |
... | ... | @@ -96,14 +106,26 @@ |
96 | 106 | isUpdate.value = !!data?.isUpdate; |
97 | 107 | if (unref(isUpdate)) { |
98 | 108 | await nextTick(); |
109 | + const { record } = data || {}; | |
99 | 110 | editId.value = data.record.id; |
111 | + | |
100 | 112 | if (data.record.avatar) { |
101 | - setFieldsValue({ | |
102 | - avatar: [{ uid: buildUUID(), name: 'name', url: data.record.avatar } as FileItem], | |
113 | + Object.assign(record, { | |
114 | + avatar: [{ uid: buildUUID(), name: 'name', url: record.avatar } as FileItem], | |
103 | 115 | }); |
104 | 116 | } |
105 | - const { ...params } = data.record; | |
106 | - await setFieldsValue({ ...params }); | |
117 | + | |
118 | + if ( | |
119 | + record?.accessMode === AccessMode.Streaming && | |
120 | + record.videoPlatformDTO?.type === VideoPlatformEnum.FLUORITE | |
121 | + ) { | |
122 | + Object.assign(record, { | |
123 | + articulation: record.streamType, | |
124 | + videoFormat: record.playProtocol, | |
125 | + }); | |
126 | + } | |
127 | + | |
128 | + setFieldsValue({ ...record, videoType: record.videoPlatformDTO?.type }); | |
107 | 129 | } else { |
108 | 130 | editId.value = ''; |
109 | 131 | } |
... | ... | @@ -128,6 +150,15 @@ |
128 | 150 | } |
129 | 151 | let saveMessage = '添加成功'; |
130 | 152 | let updateMessage = '修改成功'; |
153 | + | |
154 | + if ( | |
155 | + values?.accessMode === AccessMode.Streaming && | |
156 | + values.videoType === VideoPlatformEnum.FLUORITE | |
157 | + ) { | |
158 | + values.streamType = values.articulation; | |
159 | + values.playProtocol = values.videoFormat; | |
160 | + } | |
161 | + | |
131 | 162 | await createOrEditCameraManage(values); |
132 | 163 | closeDrawer(); |
133 | 164 | emit('success'); |
... | ... | @@ -139,6 +170,11 @@ |
139 | 170 | } |
140 | 171 | } |
141 | 172 | |
173 | + const handleChange = (e, options) => { | |
174 | + //流媒体配置 | |
175 | + setFieldsValue({ videoType: e ? options.type : null }); | |
176 | + }; | |
177 | + | |
142 | 178 | return { |
143 | 179 | getTitle, |
144 | 180 | registerDrawer, |
... | ... | @@ -149,6 +185,7 @@ |
149 | 185 | registerSteramingDrawer, |
150 | 186 | handleOpenStreamConfig, |
151 | 187 | handleSuccess, |
188 | + handleChange, | |
152 | 189 | }; |
153 | 190 | }, |
154 | 191 | }); | ... | ... |
... | ... | @@ -42,6 +42,8 @@ |
42 | 42 | |
43 | 43 | const withToken = ref(false); |
44 | 44 | |
45 | + const videoId = ref<string>(); | |
46 | + | |
45 | 47 | const fingerprintResult = ref<Nullable<GetResult>>(null); |
46 | 48 | |
47 | 49 | const options = reactive<VideoJsPlayerOptions>({ |
... | ... | @@ -64,6 +66,7 @@ |
64 | 66 | const [register] = useModalInner( |
65 | 67 | async (data: { record: CameraModel | StreamingManageRecord }) => { |
66 | 68 | const { record } = data; |
69 | + videoId.value = record.id || ''; | |
67 | 70 | const result = await getResult(); |
68 | 71 | fingerprintResult.value = result; |
69 | 72 | if (record.accessMode === AccessMode.ManuallyEnter) { | ... | ... |
src/views/camera/manage/SnHelpMessage1.vue
0 → 100644
1 | +<script lang="ts" setup> | |
2 | + import { BasicHelp } from '/@/components/Basic'; | |
3 | + import { createImgPreview } from '/@/components/Preview/index'; | |
4 | + import snStep4 from '/@/assets/images/sn-step4.png'; | |
5 | + const imgList: string[] = [snStep4]; | |
6 | + function handlePreview() { | |
7 | + createImgPreview({ imageList: imgList, defaultWidth: 1000 }); | |
8 | + } | |
9 | +</script> | |
10 | + | |
11 | +<template> | |
12 | + <div>监控点编号</div> | |
13 | + <BasicHelp | |
14 | + placement="top" | |
15 | + @click="handlePreview" | |
16 | + class="mx-1" | |
17 | + text='点击查看如何获取"监控点编号"' | |
18 | + /> | |
19 | +</template> | ... | ... |
... | ... | @@ -306,7 +306,7 @@ |
306 | 306 | @on-unmounted="handleCloseFlvPlayUrl(item)" |
307 | 307 | /> |
308 | 308 | <div |
309 | - class="video-container-mask absolute top-0 left-0 z-50 text-lg w-full text-light-50 flex justify-center items-center" | |
309 | + class="video-container-mask absolute top-0 left-0 z-50 text-lg w-full text-light-50 flex justify-center" | |
310 | 310 | style="height: 100%; background-color: rgba(0, 0, 0, 0.5)" |
311 | 311 | > |
312 | 312 | <span>{{ item.name }}</span> | ... | ... |
... | ... | @@ -4,10 +4,13 @@ import { FormSchema as QFormSchema, useComponentRegister } from '/@/components/F |
4 | 4 | import { CameraVideoUrl, CameraMaxLength } from '/@/utils/rules'; |
5 | 5 | import { h } from 'vue'; |
6 | 6 | import SnHelpMessage from './SnHelpMessage.vue'; |
7 | +import SnHelpMessage1 from './SnHelpMessage1.vue'; | |
7 | 8 | import { OrgTreeSelect } from '../../common/OrgTreeSelect'; |
8 | 9 | import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; |
9 | 10 | import { createImgPreview } from '/@/components/Preview'; |
10 | 11 | import { uploadThumbnail } from '/@/api/configuration/center/configurationCenter'; |
12 | +import { findDictItemByCode } from '/@/api/system/dict'; | |
13 | +import { DictEnum } from '/@/enums/dictEnum'; | |
11 | 14 | |
12 | 15 | useComponentRegister('OrgTreeSelect', OrgTreeSelect); |
13 | 16 | |
... | ... | @@ -45,6 +48,19 @@ export enum MediaType { |
45 | 48 | M3U8 = 'm3u8', |
46 | 49 | } |
47 | 50 | |
51 | +export enum FluoriteMideaProtocolEnum { | |
52 | + HLS = 2, | |
53 | + FLV = 4, | |
54 | +} | |
55 | + | |
56 | +export enum ArticulationEnumType { | |
57 | + HIGH_DEFINITION = 1, | |
58 | + SMOOTH = 2, | |
59 | +} | |
60 | +export enum ArticulationEnumNameType { | |
61 | + HIGH_DEFINITION = '高清', | |
62 | + SMOOTH = '流畅', | |
63 | +} | |
48 | 64 | // 表格列数据 |
49 | 65 | export const columns: BasicColumn[] = [ |
50 | 66 | { |
... | ... | @@ -100,6 +116,13 @@ export const searchFormSchema: FormSchema[] = [ |
100 | 116 | }, |
101 | 117 | ]; |
102 | 118 | |
119 | +export enum VideoPlatformEnum { | |
120 | + // 海康 | |
121 | + SCI = 0, | |
122 | + // 萤石云 | |
123 | + FLUORITE = 1, | |
124 | +} | |
125 | + | |
103 | 126 | // 弹框配置项 |
104 | 127 | export const formSchema: QFormSchema[] = [ |
105 | 128 | { |
... | ... | @@ -215,7 +238,25 @@ export const formSchema: QFormSchema[] = [ |
215 | 238 | }, |
216 | 239 | rules: [{ required: true, message: '视频流是必填项' }, ...CameraVideoUrl], |
217 | 240 | }, |
218 | - | |
241 | + { | |
242 | + field: 'videoType', | |
243 | + label: '流媒体平台', | |
244 | + component: 'ApiRadioGroup', | |
245 | + required: true, | |
246 | + defaultValue: VideoPlatformEnum.SCI, | |
247 | + ifShow: ({ values }) => values.accessMode === AccessMode.Streaming, | |
248 | + componentProps: { | |
249 | + api: async (params) => { | |
250 | + const values = await findDictItemByCode(params); | |
251 | + return values.map((item) => ({ label: item.itemText, value: Number(item.itemValue) })); | |
252 | + }, | |
253 | + params: { | |
254 | + dictCode: DictEnum.STREAMING_MEDIA_TYPE, | |
255 | + }, | |
256 | + getPopupContainer: () => document.body, | |
257 | + placeholder: `请选择平台类型`, | |
258 | + }, | |
259 | + }, | |
219 | 260 | { |
220 | 261 | field: 'videoPlatformId', |
221 | 262 | label: '流媒体配置', |
... | ... | @@ -234,7 +275,10 @@ export const formSchema: QFormSchema[] = [ |
234 | 275 | component: 'RadioGroup', |
235 | 276 | defaultValue: StreamType.MASTER, |
236 | 277 | ifShow({ values }) { |
237 | - return values.accessMode === AccessMode.Streaming; | |
278 | + return ( | |
279 | + values.accessMode === AccessMode.Streaming && | |
280 | + values.videoType !== VideoPlatformEnum.FLUORITE | |
281 | + ); | |
238 | 282 | }, |
239 | 283 | componentProps: { |
240 | 284 | placeholder: '请选择码流', |
... | ... | @@ -247,12 +291,53 @@ export const formSchema: QFormSchema[] = [ |
247 | 291 | }, |
248 | 292 | }, |
249 | 293 | { |
294 | + field: 'articulation', | |
295 | + label: '清晰度', | |
296 | + component: 'RadioGroup', | |
297 | + defaultValue: ArticulationEnumType.HIGH_DEFINITION, | |
298 | + ifShow: ({ model }) => | |
299 | + model.accessMode === AccessMode.Streaming && model.videoType === VideoPlatformEnum.FLUORITE, | |
300 | + componentProps: () => { | |
301 | + return { | |
302 | + options: [ | |
303 | + { | |
304 | + label: ArticulationEnumNameType.HIGH_DEFINITION, | |
305 | + value: ArticulationEnumType.HIGH_DEFINITION, | |
306 | + }, | |
307 | + { | |
308 | + label: ArticulationEnumNameType.SMOOTH, | |
309 | + value: ArticulationEnumType.SMOOTH, | |
310 | + }, | |
311 | + ], | |
312 | + }; | |
313 | + }, | |
314 | + }, | |
315 | + { | |
316 | + field: 'videoFormat', | |
317 | + label: '视频格式', | |
318 | + component: 'Select', | |
319 | + ifShow: ({ model }) => | |
320 | + model.accessMode === AccessMode.Streaming && model.videoType === VideoPlatformEnum.FLUORITE, | |
321 | + defaultValue: FluoriteMideaProtocolEnum.FLV, | |
322 | + required: true, | |
323 | + componentProps: { | |
324 | + options: [ | |
325 | + { label: 'FLV', value: FluoriteMideaProtocolEnum.FLV }, | |
326 | + { label: 'HLS', value: FluoriteMideaProtocolEnum.HLS }, | |
327 | + ], | |
328 | + allowClear: false, | |
329 | + }, | |
330 | + }, | |
331 | + { | |
250 | 332 | field: 'playProtocol', |
251 | 333 | label: '播放协议', |
252 | 334 | component: 'RadioGroup', |
253 | 335 | defaultValue: PlayProtocol.HTTP, |
254 | 336 | ifShow({ values }) { |
255 | - return values.accessMode === AccessMode.Streaming; | |
337 | + return ( | |
338 | + values.accessMode === AccessMode.Streaming && | |
339 | + values.videoType !== VideoPlatformEnum.FLUORITE | |
340 | + ); | |
256 | 341 | }, |
257 | 342 | helpMessage: ['平台使用https的hls协议,需联系海康开放平台专家支持。'], |
258 | 343 | componentProps: { |
... | ... | @@ -270,7 +355,25 @@ export const formSchema: QFormSchema[] = [ |
270 | 355 | component: 'Input', |
271 | 356 | rules: [...CameraVideoUrl, { required: true, message: '摄像头编号是必填项' }], |
272 | 357 | ifShow({ values }) { |
273 | - return values.accessMode === AccessMode.Streaming; | |
358 | + return ( | |
359 | + values.accessMode === AccessMode.Streaming && | |
360 | + values.videoType !== VideoPlatformEnum.FLUORITE | |
361 | + ); | |
362 | + }, | |
363 | + componentProps: { | |
364 | + placeholder: '请输入监控点编号', | |
365 | + }, | |
366 | + }, | |
367 | + { | |
368 | + field: 'sn', | |
369 | + label: h(SnHelpMessage1) as any, | |
370 | + component: 'Input', | |
371 | + rules: [...CameraVideoUrl, { required: true, message: '摄像头编号是必填项' }], | |
372 | + ifShow({ values }) { | |
373 | + return ( | |
374 | + values.accessMode === AccessMode.Streaming && | |
375 | + values.videoType === VideoPlatformEnum.FLUORITE | |
376 | + ); | |
274 | 377 | }, |
275 | 378 | componentProps: { |
276 | 379 | placeholder: '请输入监控点编号', | ... | ... |
1 | 1 | import { PlayProtocol } from '../manage/config.data'; |
2 | 2 | import type { StreamingMediaModel } from '/@/api/camera/model/cameraModel'; |
3 | +import { findDictItemByCode } from '/@/api/system/dict'; | |
4 | +import { DictEnum } from '/@/enums/dictEnum'; | |
3 | 5 | import { BasicColumn, FormSchema } from '/@/components/Table'; |
4 | 6 | |
5 | 7 | export interface DrawerParams { |
... | ... | @@ -9,6 +11,7 @@ export interface DrawerParams { |
9 | 11 | |
10 | 12 | export const streamingMediaTypeMapping = { |
11 | 13 | 0: '海康ISC平台', |
14 | + 1: '萤石平台', | |
12 | 15 | }; |
13 | 16 | |
14 | 17 | export const streamingMediaSSLMapping = { |
... | ... | @@ -37,6 +40,9 @@ export const columnSchema: BasicColumn[] = [ |
37 | 40 | title: '用户Key', |
38 | 41 | dataIndex: 'appKey', |
39 | 42 | width: 80, |
43 | + format(text) { | |
44 | + return formatSecret(text); | |
45 | + }, | |
40 | 46 | }, |
41 | 47 | { |
42 | 48 | title: '用户密钥', |
... | ... | @@ -73,26 +79,27 @@ export const formDetailSchema: FormSchema[] = [ |
73 | 79 | { |
74 | 80 | label: '平台类型', |
75 | 81 | field: 'type', |
76 | - component: 'Select', | |
77 | - rules: [{ required: true, message: '平台类型为必填项', type: 'number' }], | |
82 | + component: 'ApiSelect', | |
83 | + required: true, | |
78 | 84 | componentProps: { |
79 | - options: [{ label: '海康ISC平台', value: 0 }], | |
80 | - placeholder: '请输入选择平台类型', | |
85 | + api: async (params) => { | |
86 | + const values = await findDictItemByCode(params); | |
87 | + return values.map((item) => ({ label: item.itemText, value: Number(item.itemValue) })); | |
88 | + }, | |
89 | + params: { | |
90 | + dictCode: DictEnum.STREAMING_MEDIA_TYPE, | |
91 | + }, | |
92 | + getPopupContainer: () => document.body, | |
93 | + placeholder: `请选择平台类型`, | |
81 | 94 | }, |
82 | 95 | }, |
83 | 96 | { |
84 | 97 | label: '部署环境', |
85 | 98 | field: 'ssl', |
86 | 99 | component: 'RadioGroup', |
100 | + ifShow: false, | |
87 | 101 | rules: [{ required: true, message: '流媒体部署环境为必填项', type: 'number' }], |
88 | - defaultValue: PlayProtocol.HTTP, | |
89 | - componentProps: { | |
90 | - defaultValue: PlayProtocol.HTTP, | |
91 | - options: [ | |
92 | - { label: 'http', value: PlayProtocol.HTTP }, | |
93 | - { label: 'https', value: PlayProtocol.HTTPS }, | |
94 | - ], | |
95 | - }, | |
102 | + defaultValue: PlayProtocol.HTTPS, | |
96 | 103 | }, |
97 | 104 | { |
98 | 105 | label: '平台地址', |
... | ... | @@ -108,7 +115,7 @@ export const formDetailSchema: FormSchema[] = [ |
108 | 115 | { |
109 | 116 | label: '用户Key', |
110 | 117 | field: 'appKey', |
111 | - component: 'Input', | |
118 | + component: 'InputPassword', | |
112 | 119 | rules: [{ required: true, message: '用户Key为必填项' }], |
113 | 120 | componentProps: { |
114 | 121 | maxLength: 36, |
... | ... | @@ -118,7 +125,7 @@ export const formDetailSchema: FormSchema[] = [ |
118 | 125 | { |
119 | 126 | label: '用户密钥', |
120 | 127 | field: 'appSecret', |
121 | - component: 'Input', | |
128 | + component: 'InputPassword', | |
122 | 129 | rules: [ |
123 | 130 | { required: true, message: '用户密钥为必填项' }, |
124 | 131 | { required: true, min: 20, message: '用户密钥不能少于20位字符' }, | ... | ... |
... | ... | @@ -14,7 +14,9 @@ |
14 | 14 | v-model:value="model['templateId']" |
15 | 15 | placeholder="请选择模板" |
16 | 16 | style="width: 100%" |
17 | - :options="selectTemplateOptions" | |
17 | + :options=" | |
18 | + selectTemplateOptions?.filter((item) => item.platform === model['platform']) || [] | |
19 | + " | |
18 | 20 | @change="handleTemplateChange" |
19 | 21 | v-bind="createPickerSearch()" |
20 | 22 | :disabled="templateDisabled" |
... | ... | @@ -140,6 +142,7 @@ |
140 | 142 | }); |
141 | 143 | |
142 | 144 | const selectTemplateOptions: Ref<any[]> = ref([]); |
145 | + | |
143 | 146 | const getTemplate = async (params: queryPageParams) => { |
144 | 147 | const { items } = await getPage({ ...params, isTemplate: 1 }); |
145 | 148 | selectTemplateOptions.value = items.map((item) => ({ |
... | ... | @@ -152,11 +155,7 @@ |
152 | 155 | |
153 | 156 | const handleTemplateChange = async (_, option) => { |
154 | 157 | const { productAndDevice } = option; |
155 | - // if (!productAndDevice) return; | |
156 | - // selectOptions.value = productAndDevice?.map((item) => ({ | |
157 | - // label: item.profileName || item.name, | |
158 | - // value: item.profileId, | |
159 | - // })); | |
158 | + setFieldsValue({ platform: option?.platform }); | |
160 | 159 | await nextTick(); |
161 | 160 | // 赋值 |
162 | 161 | selectDeviceProfileRef.value?.setFieldsValue( | ... | ... |
... | ... | @@ -101,10 +101,6 @@ export const formSchema: FormSchema[] = [ |
101 | 101 | onPreview: (fileList: FileItem) => { |
102 | 102 | createImgPreview({ imageList: [fileList.url!] }); |
103 | 103 | }, |
104 | - // showUploadList: { | |
105 | - // showDownloadIcon: true, | |
106 | - // showRemoveIcon: true, | |
107 | - // }, | |
108 | 104 | }; |
109 | 105 | }, |
110 | 106 | }, | ... | ... |
... | ... | @@ -348,7 +348,7 @@ export const step1Schemas: FormSchema[] = [ |
348 | 348 | popconfirmTitle: () => |
349 | 349 | updateOrgHelpMessage.map((text) => h('div', { style: { maxWidth: '240px' } }, text)), |
350 | 350 | }), |
351 | - componentProps: ({ formModel }) => { | |
351 | + componentProps: ({ formModel, formActionType }) => { | |
352 | 352 | return { |
353 | 353 | component: 'OrgTreeSelect', |
354 | 354 | defaultLockStatus: !!formModel?.isUpdate, |
... | ... | @@ -358,6 +358,17 @@ export const step1Schemas: FormSchema[] = [ |
358 | 358 | organizationId: formModel?.sensorOrganizationId, |
359 | 359 | }, |
360 | 360 | }, |
361 | + onOptionsChange: (options: Recordable[]) => { | |
362 | + if (!formModel?.organizationId && formModel?.deviceType === DeviceTypeEnum.SENSOR) { | |
363 | + const firstItem = options?.[0]; | |
364 | + | |
365 | + if (firstItem && firstItem?.id) { | |
366 | + const { setFieldsValue, clearValidate } = formActionType; | |
367 | + setFieldsValue({ organizationId: firstItem.id }); | |
368 | + clearValidate('organizationId'); | |
369 | + } | |
370 | + } | |
371 | + }, | |
361 | 372 | }, |
362 | 373 | }; |
363 | 374 | }, | ... | ... |
... | ... | @@ -70,6 +70,7 @@ export const columns: BasicColumn[] = [ |
70 | 70 | title: '状态', |
71 | 71 | dataIndex: 'deviceState', |
72 | 72 | width: 110, |
73 | + className: 'device-status', | |
73 | 74 | slots: { customRender: 'deviceState' }, |
74 | 75 | }, |
75 | 76 | { |
... | ... | @@ -237,4 +238,17 @@ export const searchFormSchema: FormSchema[] = [ |
237 | 238 | placeholder: '请选择', |
238 | 239 | }, |
239 | 240 | }, |
241 | + { | |
242 | + field: 'alarmStatus', | |
243 | + label: '是否告警', | |
244 | + component: 'Select', | |
245 | + colProps: { span: 6 }, | |
246 | + componentProps: { | |
247 | + options: [ | |
248 | + { label: '是', value: 1 }, | |
249 | + { label: '否', value: 0 }, | |
250 | + ], | |
251 | + placeholder: '请选择设备是否存在告警', | |
252 | + }, | |
253 | + }, | |
240 | 254 | ]; | ... | ... |
... | ... | @@ -19,7 +19,20 @@ |
19 | 19 | <Tabs.TabPane key="modelOfMatter" tab="物模型数据"> |
20 | 20 | <ModelOfMatter :deviceDetail="deviceDetail" /> |
21 | 21 | </Tabs.TabPane> |
22 | - <Tabs.TabPane key="3" tab="告警"> | |
22 | + <Tabs.TabPane key="3"> | |
23 | + <template #tab> | |
24 | + <Badge :offset="[2, -5]" style="color: inherit"> | |
25 | + <span>告警</span> | |
26 | + <template #count> | |
27 | + <div | |
28 | + :style="{ visibility: deviceDetail.alarmStatus ? 'visible' : 'hidden' }" | |
29 | + class="w-3.5 h-3.5 !flex justify-center items-center rounded-1 border-red-400 border" | |
30 | + > | |
31 | + <Icon icon="mdi:bell-warning" color="#f46161" :size="12" class="!mr-0" /> | |
32 | + </div> | |
33 | + </template> | |
34 | + </Badge> | |
35 | + </template> | |
23 | 36 | <AlarmLog :device-id="deviceDetail.id" class="bg-gray-100" /> |
24 | 37 | </Tabs.TabPane> |
25 | 38 | <Tabs.TabPane key="4" tab="子设备" v-if="deviceDetail?.deviceType === 'GATEWAY'"> |
... | ... | @@ -51,7 +64,7 @@ |
51 | 64 | <script lang="ts" setup> |
52 | 65 | import { ref, computed } from 'vue'; |
53 | 66 | import { BasicDrawer, useDrawerInner } from '/@/components/Drawer'; |
54 | - import { Tabs } from 'ant-design-vue'; | |
67 | + import { Tabs, Badge } from 'ant-design-vue'; | |
55 | 68 | import Detail from '../tabs/Detail.vue'; |
56 | 69 | import ChildDevice from '../tabs/ChildDevice.vue'; |
57 | 70 | import TBoxDetail from '../tabs/TBoxDetail.vue'; |
... | ... | @@ -62,6 +75,7 @@ |
62 | 75 | import { DeviceRecord } from '/@/api/device/model/deviceModel'; |
63 | 76 | import Task from '../tabs/Task.vue'; |
64 | 77 | import AlarmLog from '/@/views/alarm/log/index.vue'; |
78 | + import { Icon } from '/@/components/Icon'; | |
65 | 79 | |
66 | 80 | const emit = defineEmits(['reload', 'register', 'openTbDeviceDetail', 'openGatewayDeviceDetail']); |
67 | 81 | ... | ... |
... | ... | @@ -27,16 +27,26 @@ export interface SocketInfoDataSourceItemType extends BaseAdditionalInfo { |
27 | 27 | expand?: boolean; |
28 | 28 | showHistoryDataButton?: boolean; |
29 | 29 | rawValue?: any; |
30 | + enum?: Record<string, string>; | |
30 | 31 | } |
31 | 32 | |
32 | 33 | export function buildTableDataSourceByObjectModel( |
33 | 34 | models: DeviceModelOfMatterAttrs[] |
34 | 35 | ): SocketInfoDataSourceItemType[] { |
35 | 36 | function getAdditionalInfoByDataType(dataType?: DataType) { |
36 | - const { specs } = dataType || {}; | |
37 | + const { specs, specsList, type } = dataType || {}; | |
37 | 38 | if (isArray(specs)) return {}; |
38 | 39 | const { unit, boolClose, boolOpen, unitName } = (specs as Partial<Specs>) || {}; |
39 | - return { unit, boolClose, boolOpen, unitName }; | |
40 | + const result = { unit, boolClose, boolOpen, unitName }; | |
41 | + if (type == DataTypeEnum.ENUM && specsList && specsList.length) { | |
42 | + Reflect.set( | |
43 | + result, | |
44 | + 'enum', | |
45 | + specsList.reduce((prev, next) => ({ ...prev, [next.value!]: next.name }), {}) | |
46 | + ); | |
47 | + } | |
48 | + | |
49 | + return result; | |
40 | 50 | } |
41 | 51 | |
42 | 52 | return models.map((item) => { |
... | ... | @@ -72,7 +82,6 @@ export function buildTableDataSourceByObjectModel( |
72 | 82 | } else { |
73 | 83 | Object.assign(res, getAdditionalInfoByDataType(dataType)); |
74 | 84 | } |
75 | - | |
76 | 85 | return res; |
77 | 86 | }); |
78 | 87 | } | ... | ... |
... | ... | @@ -284,13 +284,15 @@ |
284 | 284 | }); |
285 | 285 | |
286 | 286 | const formatValue = (item: SocketInfoDataSourceItemType) => { |
287 | - return item.type === DataTypeEnum.BOOL | |
288 | - ? !isNullOrUnDef(item.value) | |
289 | - ? !!Number(item.value) | |
290 | - ? item.boolOpen | |
291 | - : item.boolClose | |
292 | - : '--' | |
293 | - : (item.value as string) || '--'; | |
287 | + if (isNullOrUnDef(item)) return '--'; | |
288 | + switch (item.type) { | |
289 | + case DataTypeEnum.BOOL: | |
290 | + return !!Number(item.value) ? item.boolOpen : item.boolClose; | |
291 | + case DataTypeEnum.ENUM: | |
292 | + return item.enum?.[item.value as string]; | |
293 | + default: | |
294 | + return item.value || '--'; | |
295 | + } | |
294 | 296 | }; |
295 | 297 | |
296 | 298 | const [register, { openModal: openSendCommandModal }] = useModal(); | ... | ... |
1 | +import { StructJSON } from '/@/api/device/model/modelOfMatterModel'; | |
1 | 2 | import { FormSchema } from '/@/components/Form'; |
3 | +import { validateTCPCustomCommand } from '/@/components/Form/src/externalCompns/components/ThingsModelForm'; | |
4 | +import { DataTypeEnum } from '/@/enums/objectModelEnum'; | |
2 | 5 | |
3 | 6 | const InsertString = (t, c, n) => { |
4 | 7 | const r: string | number[] = []; |
... | ... | @@ -127,21 +130,33 @@ const SingleToHexBatch = (t) => { |
127 | 130 | return r.join('\r\n'); |
128 | 131 | }; |
129 | 132 | |
130 | -const formSchemasConfig = (schemas, actionType): FormSchema[] => { | |
131 | - const { identifier, functionName } = schemas; | |
133 | +const formSchemasConfig = (schemas: StructJSON, actionType: string): FormSchema[] => { | |
134 | + const { identifier, functionName, dataType } = schemas; | |
135 | + | |
136 | + if (dataType?.type === DataTypeEnum.STRING) { | |
137 | + return [ | |
138 | + { | |
139 | + field: identifier, | |
140 | + label: functionName!, | |
141 | + component: 'Input', | |
142 | + rules: [{ required: true, validator: validateTCPCustomCommand }], | |
143 | + componentProps: { | |
144 | + placeholder: `请输入${functionName}`, | |
145 | + }, | |
146 | + }, | |
147 | + ]; | |
148 | + } | |
149 | + | |
132 | 150 | if (actionType == '06') { |
133 | 151 | return [ |
134 | 152 | { |
135 | 153 | field: identifier, |
136 | - label: functionName, | |
154 | + label: functionName!, | |
137 | 155 | component: 'InputNumber', |
138 | 156 | rules: [{ required: true, message: '请输入正数' }], |
139 | 157 | componentProps: { |
140 | 158 | min: 0, |
141 | - formatter: (e) => { | |
142 | - const value = `${e}`.replace('-', '').replace(/^(-)*(\d+)\.(\d\d).*$/, '$1$2.$3'); | |
143 | - return value; | |
144 | - }, | |
159 | + precision: 2, | |
145 | 160 | placeholder: `请输入正数`, |
146 | 161 | }, |
147 | 162 | }, |
... | ... | @@ -150,7 +165,7 @@ const formSchemasConfig = (schemas, actionType): FormSchema[] => { |
150 | 165 | return [ |
151 | 166 | { |
152 | 167 | field: identifier, |
153 | - label: functionName, | |
168 | + label: functionName!, | |
154 | 169 | component: 'InputNumber', |
155 | 170 | rules: [{ required: true, message: '请输入值' }], |
156 | 171 | componentProps: { |
... | ... | @@ -165,13 +180,12 @@ const formSchemasConfig = (schemas, actionType): FormSchema[] => { |
165 | 180 | return [ |
166 | 181 | { |
167 | 182 | field: identifier, |
168 | - label: functionName, | |
183 | + label: functionName!, | |
169 | 184 | component: 'InputNumber', |
170 | 185 | rules: [{ required: true, message: '请输入值' }], |
171 | 186 | componentProps: { |
172 | 187 | placeholder: `请输入数字`, |
173 | - formatter: (e) => | |
174 | - `${e}`.replace(/\B(?=(\d{3})+(?!\d))/g, '').replace(/^(-)*(\d+)\.(\d\d).*$/, '$1$2.$3'), | |
188 | + precision: 2, | |
175 | 189 | }, |
176 | 190 | }, |
177 | 191 | ]; | ... | ... |
... | ... | @@ -35,6 +35,7 @@ |
35 | 35 | const zoomFactorValue = ref<number>(1); //缩放因子 |
36 | 36 | const isShowMultiply = ref<Boolean>(false); // 只有tcp --> int和double类型才相乘缩放因子 |
37 | 37 | const deviceTransportType = ref<string>(); |
38 | + const objectDataType = ref<DataTypeEnum>(); | |
38 | 39 | |
39 | 40 | const [register] = useModalInner(async (params: ModalParamsType<DeviceModelOfMatterAttrs>) => { |
40 | 41 | const { record } = params; |
... | ... | @@ -48,6 +49,7 @@ |
48 | 49 | zoomFactorValue.value = zoomFactor ? Number(zoomFactor) : 1; |
49 | 50 | isShowMultiply.value = type == 'INT' || type == 'DOUBLE' ? true : false; |
50 | 51 | deviceTransportType.value = transportType; |
52 | + objectDataType.value = type; | |
51 | 53 | |
52 | 54 | let schemas = [{ dataType: dataType, identifier, functionName: name } as StructJSON]; |
53 | 55 | |
... | ... | @@ -136,7 +138,12 @@ |
136 | 138 | |
137 | 139 | const sendValue = ref({}); |
138 | 140 | //判断tcp类型 标识符是自定义还是ModBus |
139 | - if (unref(isShowModBUS)) { | |
141 | + if (unref(objectDataType) === DataTypeEnum.STRING) { | |
142 | + const flag = await validate(); | |
143 | + if (!flag) return; | |
144 | + const value = getFieldsValue()[unref(formField)]; | |
145 | + sendValue.value = value; | |
146 | + } else if (unref(isShowModBUS)) { | |
140 | 147 | if (!unref(isShowActionType)) { |
141 | 148 | createMessage.warning('当前物模型扩展描述没有填写'); |
142 | 149 | return; | ... | ... |
... | ... | @@ -12,16 +12,9 @@ export interface BasicCreateFormParams { |
12 | 12 | |
13 | 13 | useComponentRegister('JSONEditor', JSONEditor); |
14 | 14 | |
15 | -const validateDouble = ( | |
16 | - value: number, | |
17 | - type: DataTypeEnum, | |
18 | - min?: number | string, | |
19 | - max?: number | string | |
20 | -) => { | |
21 | - min = | |
22 | - Number(min) || type === DataTypeEnum.NUMBER_INT ? Number.MIN_SAFE_INTEGER : Number.MIN_VALUE; | |
23 | - max = | |
24 | - Number(max) || type === DataTypeEnum.NUMBER_INT ? Number.MAX_SAFE_INTEGER : Number.MAX_VALUE; | |
15 | +const validateDouble = (value: number, min?: number | string, max?: number | string) => { | |
16 | + min = Number(min) || Number.MIN_SAFE_INTEGER; | |
17 | + max = Number(max) || Number.MAX_SAFE_INTEGER; | |
25 | 18 | |
26 | 19 | return { |
27 | 20 | flag: value < min || value > max, |
... | ... | @@ -47,7 +40,7 @@ export const useGenDynamicForm = () => { |
47 | 40 | type: 'number', |
48 | 41 | trigger: 'change', |
49 | 42 | validator: (_rule, value) => { |
50 | - const { flag, message } = validateDouble(value, type, min, max); | |
43 | + const { flag, message } = validateDouble(value, min, max); | |
51 | 44 | if (flag) { |
52 | 45 | return Promise.reject(`${functionName}${message}`); |
53 | 46 | } |
... | ... | @@ -56,8 +49,8 @@ export const useGenDynamicForm = () => { |
56 | 49 | }, |
57 | 50 | ], |
58 | 51 | componentProps: { |
59 | - max: max ?? type === DataTypeEnum.NUMBER_INT ? Number.MAX_SAFE_INTEGER : Number.MAX_VALUE, | |
60 | - min: min ?? type === DataTypeEnum.NUMBER_INT ? Number.MIN_SAFE_INTEGER : Number.MIN_VALUE, | |
52 | + max: max ?? Number.MAX_SAFE_INTEGER, | |
53 | + min: min ?? Number.MIN_SAFE_INTEGER, | |
61 | 54 | step, |
62 | 55 | placeholder: `请输入${functionName}`, |
63 | 56 | precision: type === DataTypeEnum.NUMBER_INT ? 0 : 2, |
... | ... | @@ -81,7 +74,7 @@ export const useGenDynamicForm = () => { |
81 | 74 | type: 'string', |
82 | 75 | trigger: 'change', |
83 | 76 | validator: (_rule, value) => { |
84 | - if (value.length > length) { | |
77 | + if (value?.length > length) { | |
85 | 78 | return Promise.reject(`${functionName}数据长度应该小于${length}`); |
86 | 79 | } |
87 | 80 | return Promise.resolve(value); |
... | ... | @@ -117,6 +110,24 @@ export const useGenDynamicForm = () => { |
117 | 110 | }; |
118 | 111 | }; |
119 | 112 | |
113 | + const createEnumSelect = ({ | |
114 | + identifier, | |
115 | + functionName, | |
116 | + dataType, | |
117 | + }: BasicCreateFormParams): FormSchema => { | |
118 | + const { specsList } = dataType; | |
119 | + return { | |
120 | + field: identifier, | |
121 | + label: functionName, | |
122 | + component: 'Select', | |
123 | + componentProps: { | |
124 | + options: specsList?.map((item) => ({ label: item.name, value: item.value })), | |
125 | + placeholder: `请选择${functionName}`, | |
126 | + getPopupContainer: () => document.body, | |
127 | + }, | |
128 | + }; | |
129 | + }; | |
130 | + | |
120 | 131 | const createInputJson = ({ identifier, functionName }: BasicCreateFormParams): FormSchema => { |
121 | 132 | return { |
122 | 133 | field: identifier, |
... | ... | @@ -144,6 +155,7 @@ export const useGenDynamicForm = () => { |
144 | 155 | [DataTypeEnum.NUMBER_INT]: createInputNumber, |
145 | 156 | [DataTypeEnum.STRING]: createInput, |
146 | 157 | [DataTypeEnum.STRUCT]: createInputJson, |
158 | + [DataTypeEnum.ENUM]: createEnumSelect, | |
147 | 159 | }; |
148 | 160 | |
149 | 161 | const fieldTypeMap = new Map<string, DataTypeEnum>(); | ... | ... |
... | ... | @@ -39,6 +39,8 @@ |
39 | 39 | |
40 | 40 | const playUrl = ref(''); |
41 | 41 | |
42 | + const videoId = ref<string>(); | |
43 | + | |
42 | 44 | const withToken = ref(false); |
43 | 45 | |
44 | 46 | const fingerprintResult = ref<Nullable<GetResult>>(null); |
... | ... | @@ -64,6 +66,7 @@ |
64 | 66 | const [register] = useModalInner( |
65 | 67 | async (data: { record: CameraModel | StreamingManageRecord }) => { |
66 | 68 | const { record } = data; |
69 | + videoId.value = record.id || ''; | |
67 | 70 | const result = await getResult(); |
68 | 71 | fingerprintResult.value = result; |
69 | 72 | if (record.accessMode === AccessMode.ManuallyEnter) { | ... | ... |
... | ... | @@ -2,7 +2,7 @@ |
2 | 2 | <div> |
3 | 3 | <PageWrapper dense contentFullHeight contentClass="flex"> |
4 | 4 | <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" /> |
5 | - <BasicTable style="flex: auto" @register="registerTable" class="w-5/6 xl:w-4/5"> | |
5 | + <BasicTable style="flex: auto" @register="registerTable" class="w-5/6 xl:w-4/5 device-table"> | |
6 | 6 | <template #toolbar> |
7 | 7 | <Authority :value="DeviceListAuthEnum.CREATE"> |
8 | 8 | <a-button type="primary" @click="handleCreate" v-if="authBtn(role)"> |
... | ... | @@ -106,13 +106,17 @@ |
106 | 106 | </Tag> |
107 | 107 | </template> |
108 | 108 | <template #deviceState="{ record }"> |
109 | - <!-- <HeartOutlined v-if="!record.isCollect" class="mr-1" style="color: red" /> --> | |
110 | - <Tooltip> | |
111 | - <template #title> 我的收藏</template> | |
112 | - <HeartTwoTone v-if="record.isCollect" class="mr-1" twoToneColor="#3B82F6" /> | |
113 | - </Tooltip> | |
109 | + <div v-if="record.isCollect"> | |
110 | + <div class="absolute top-0 left-0 device-collect"> </div> | |
111 | + <Icon | |
112 | + icon="ph:star-fill" | |
113 | + class="fill-light-50 absolute top-0.5 left-0.5" | |
114 | + color="#fff" | |
115 | + :size="12" | |
116 | + /> | |
117 | + </div> | |
118 | + | |
114 | 119 | <Tag |
115 | - :style="{ marginLeft: !record.isCollect ? '17px' : '' }" | |
116 | 120 | :color=" |
117 | 121 | record.deviceState == DeviceState.INACTIVE |
118 | 122 | ? 'warning' |
... | ... | @@ -135,7 +139,7 @@ |
135 | 139 | <TableAction |
136 | 140 | :actions="[ |
137 | 141 | { |
138 | - label: '详情', | |
142 | + label: AlarmDetailActionButton({ hasAlarm: !!record.alarmStatus }), | |
139 | 143 | icon: 'ant-design:eye-outlined', |
140 | 144 | auth: DeviceListAuthEnum.DETAIL, |
141 | 145 | onClick: handleDetail.bind(null, record), |
... | ... | @@ -232,7 +236,7 @@ |
232 | 236 | </div> |
233 | 237 | </template> |
234 | 238 | <script lang="ts" setup> |
235 | - import { reactive, onMounted, ref } from 'vue'; | |
239 | + import { reactive, onMounted, ref, h, CSSProperties } from 'vue'; | |
236 | 240 | import { |
237 | 241 | DeviceModel, |
238 | 242 | DeviceRecord, |
... | ... | @@ -241,8 +245,7 @@ |
241 | 245 | } from '/@/api/device/model/deviceModel'; |
242 | 246 | import { BasicTable, useTable, TableAction, TableImg } from '/@/components/Table'; |
243 | 247 | import { columns, DeviceListAuthEnum, searchFormSchema } from './config/device.data'; |
244 | - import { Tag, Popover, Button, Tooltip } from 'ant-design-vue'; | |
245 | - import { HeartTwoTone } from '@ant-design/icons-vue'; | |
248 | + import { Tag, Popover, Button, Badge } from 'ant-design-vue'; | |
246 | 249 | import { |
247 | 250 | deleteDevice, |
248 | 251 | devicePage, |
... | ... | @@ -278,6 +281,7 @@ |
278 | 281 | } from './cpns/modal/BatchUpdateProductModal'; |
279 | 282 | import { DataActionModeEnum } from '/@/enums/toolEnum'; |
280 | 283 | import { AuthDropDown } from '/@/components/Widget'; |
284 | + import Icon from '/@/components/Icon'; | |
281 | 285 | |
282 | 286 | const { isCustomer } = useAuthDeviceDetail(); |
283 | 287 | const { createMessage } = useMessage(); |
... | ... | @@ -294,6 +298,32 @@ |
294 | 298 | const [registerImportModal, { openModal: openImportModal }] = useModal(); |
295 | 299 | const [registerBatchUpdateProductModal, { openModal: openBatchUpdateProductModal }] = useModal(); |
296 | 300 | |
301 | + const AlarmDetailActionButton = ({ hasAlarm }: { hasAlarm?: boolean }) => | |
302 | + h( | |
303 | + Badge, | |
304 | + { offset: [0, -5] }, | |
305 | + { | |
306 | + default: () => h('span', { style: { color: '#377dff' } }, '详情'), | |
307 | + count: () => | |
308 | + h( | |
309 | + 'div', | |
310 | + { | |
311 | + style: { | |
312 | + visibility: hasAlarm ? 'visible' : 'hidden', | |
313 | + width: '14px', | |
314 | + height: '14px', | |
315 | + display: 'flex', | |
316 | + justifyContent: 'center', | |
317 | + alignItems: 'center', | |
318 | + border: '1px solid #f46161', | |
319 | + borderRadius: '50%', | |
320 | + } as CSSProperties, | |
321 | + }, | |
322 | + h(Icon, { icon: 'mdi:bell-warning', color: '#f46161', size: 12 }) | |
323 | + ), | |
324 | + } | |
325 | + ); | |
326 | + | |
297 | 327 | const batchUpdateProductFlag = ref(true); |
298 | 328 | |
299 | 329 | const [ |
... | ... | @@ -336,6 +366,7 @@ |
336 | 366 | rowKey: 'id', |
337 | 367 | searchInfo: searchInfo, |
338 | 368 | clickToRowSelect: false, |
369 | + rowClassName: (record) => ((record as DeviceRecord).alarmStatus ? 'device-alarm-badge' : ''), | |
339 | 370 | actionColumn: { |
340 | 371 | width: 200, |
341 | 372 | title: '操作', |
... | ... | @@ -557,4 +588,25 @@ |
557 | 588 | }; |
558 | 589 | </script> |
559 | 590 | |
560 | -<style scoped lang="css"></style> | |
591 | +<style scoped lang="less"> | |
592 | + .device-table { | |
593 | + :deep(.ant-form-item-control-input-content) { | |
594 | + & > div > div { | |
595 | + width: 100%; | |
596 | + } | |
597 | + } | |
598 | + } | |
599 | +</style> | |
600 | + | |
601 | +<style lang="less"> | |
602 | + .device-status { | |
603 | + position: relative; | |
604 | + | |
605 | + .device-collect { | |
606 | + width: 0; | |
607 | + height: 0; | |
608 | + border-top: 30px solid #377dff; | |
609 | + border-right: 30px solid transparent; | |
610 | + } | |
611 | + } | |
612 | +</style> | ... | ... |
... | ... | @@ -144,7 +144,7 @@ |
144 | 144 | <div class="h-full w-full !flex justify-center items-center text-center p-1"> |
145 | 145 | <Image |
146 | 146 | @click.stop |
147 | - wrapper-class-name="!w-32 !h-32 !flex !items-center" | |
147 | + wrapper-class-name="!w-32 !h-32 !flex !items-center overflow-hidden" | |
148 | 148 | :src="item.image || IMAGE_FALLBACK" |
149 | 149 | placeholder |
150 | 150 | :fallback="IMAGE_FALLBACK" | ... | ... |
1 | -<template> | |
2 | - <BasicForm @register="register" /> | |
3 | -</template> | |
4 | 1 | <script lang="ts" setup> |
5 | 2 | import { BasicForm, useForm } from '/@/components/Form'; |
6 | 3 | import { DataType, ModelOfMatterParams } from '/@/api/device/model/modelOfMatterModel'; |
... | ... | @@ -15,34 +12,38 @@ |
15 | 12 | import { formSchemas } from '/@/components/Form/src/externalCompns/components/StructForm/config'; |
16 | 13 | import { TransportTypeEnum } from '../../../../components/TransportDescript/const'; |
17 | 14 | import { DataTypeEnum } from '/@/enums/objectModelEnum'; |
15 | + import { ref, unref } from 'vue'; | |
16 | + import EnumList from '/@/components/Form/src/externalCompns/components/StructForm/EnumList.vue'; | |
18 | 17 | |
19 | 18 | const props = defineProps<{ openModalMode: OpenModelMode; transportType?: string | undefined }>(); |
20 | 19 | |
21 | - const [register, { validate, resetFields, setFieldsValue, setProps }] = useForm({ | |
20 | + const enumListRef = ref<InstanceType<typeof EnumList>>(); | |
21 | + | |
22 | + const [register, { validate, getFieldsValue, resetFields, setFieldsValue }] = useForm({ | |
22 | 23 | labelWidth: 100, |
23 | 24 | schemas: formSchemas({ |
24 | 25 | hasStructForm: false, |
25 | 26 | hiddenAccessMode: false, |
26 | 27 | isTcp: props.transportType === TransportTypeEnum.TCP, |
27 | 28 | }), |
28 | - actionColOptions: { | |
29 | - span: 14, | |
30 | - }, | |
31 | - showResetButton: false, | |
32 | - submitOnReset: false, | |
33 | 29 | showActionButtonGroup: false, |
34 | 30 | }); |
35 | 31 | |
32 | + const disabled = ref(false); | |
36 | 33 | const setDisable = (flag: boolean) => { |
37 | - setProps({ disabled: flag }); | |
34 | + disabled.value = flag; | |
38 | 35 | }; |
39 | 36 | |
40 | 37 | async function getFormData(): Promise<Partial<ModelOfMatterParams>> { |
41 | - const _values = (await validate()) as StructFormValue; | |
42 | - if (!_values) return {}; | |
38 | + await validate(); | |
39 | + await unref(enumListRef)?.validate?.(); | |
40 | + | |
41 | + const _values = getFieldsValue() as StructFormValue; | |
43 | 42 | const { functionName, remark, identifier, accessMode } = _values; |
44 | - const structJSON = transfromToStructJSON(_values); | |
43 | + const structJSON = transfromToStructJSON(_values, unref(enumListRef)?.getFieldsValue?.()); | |
44 | + | |
45 | 45 | const dataType = excludeIdInStructJSON(structJSON.dataType!); |
46 | + | |
46 | 47 | const value = { |
47 | 48 | functionName, |
48 | 49 | functionType: FunctionType.PROPERTIES, |
... | ... | @@ -54,6 +55,7 @@ |
54 | 55 | dataType: dataType, |
55 | 56 | }, |
56 | 57 | } as ModelOfMatterParams; |
58 | + | |
57 | 59 | return value; |
58 | 60 | } |
59 | 61 | |
... | ... | @@ -63,18 +65,21 @@ |
63 | 65 | |
64 | 66 | const setFormData = (record: ModelOfMatterParams) => { |
65 | 67 | const { functionJson } = record; |
66 | - const { dataType = {} } = functionJson!; | |
68 | + const { dataType } = functionJson!; | |
67 | 69 | |
68 | - const { specs } = dataType! as DataType; | |
70 | + const { specs } = (dataType! || {}) as DataType; | |
69 | 71 | |
70 | 72 | const value = { |
71 | 73 | ...record, |
72 | 74 | ...functionJson, |
73 | 75 | ...dataType, |
74 | 76 | ...(isArray(specs) ? specs : { ...specs }), |
75 | - hasStructForm: (record?.functionJson?.dataType as DataType)?.type === DataTypeEnum.STRUCT, | |
77 | + hasStructForm: (dataType as DataType)?.type === DataTypeEnum.STRUCT, | |
78 | + enumList: | |
79 | + (dataType as DataType)?.type === DataTypeEnum.ENUM | |
80 | + ? unref((dataType as DataType).specsList) | |
81 | + : [], | |
76 | 82 | }; |
77 | - | |
78 | 83 | setFieldsValue(value); |
79 | 84 | }; |
80 | 85 | |
... | ... | @@ -85,4 +90,13 @@ |
85 | 90 | setDisable, |
86 | 91 | }); |
87 | 92 | </script> |
93 | + | |
94 | +<template> | |
95 | + <BasicForm @register="register" :disabled="disabled"> | |
96 | + <template #EnumList="{ field, model }"> | |
97 | + <EnumList ref="enumListRef" :value="model[field]" :disabled="disabled" /> | |
98 | + </template> | |
99 | + </BasicForm> | |
100 | +</template> | |
101 | + | |
88 | 102 | <style lang="less" scoped></style> | ... | ... |
... | ... | @@ -185,8 +185,8 @@ export const list = [ |
185 | 185 | { |
186 | 186 | deviceType: '网关/直连/网关子设备', |
187 | 187 | function: '事件上报', |
188 | - release: 'v1/devices/event/${deviceId}||${deviceName}/${identifier}', | |
189 | - subscribe: 'v1/devices/event/${deviceId}||${deviceName}/${identifier}', | |
188 | + release: 'v1/devices/event/${deviceId}或${deviceName}/${identifier}', | |
189 | + subscribe: 'v1/devices/event/${deviceId}或${deviceName}/${identifier}', | |
190 | 190 | platform: '订阅', |
191 | 191 | device: '发布', |
192 | 192 | }, | ... | ... |
... | ... | @@ -132,10 +132,7 @@ |
132 | 132 | }; |
133 | 133 | } |
134 | 134 | Reflect.set(values, 'config', config); |
135 | - if ( | |
136 | - Reflect.get(values, 'messageType') === MessageEnum.IS_VOICE && | |
137 | - Reflect.get(values, 'voiceSignName') | |
138 | - ) { | |
135 | + if (Reflect.get(values, 'messageType') === MessageEnum.IS_VOICE) { | |
139 | 136 | Reflect.set(values, 'signName', values['voiceSignName']); |
140 | 137 | } |
141 | 138 | let saveMessage = '添加成功'; | ... | ... |
... | ... | @@ -102,36 +102,36 @@ export const getFormSchemas = (hasAlarmNotify: Ref<boolean>): FormSchema[] => { |
102 | 102 | }, |
103 | 103 | }, |
104 | 104 | { |
105 | - field: FormFieldsEnum.ALARM_PROFILED, | |
105 | + field: FormFieldsEnum.ALARM_LEVEL, | |
106 | 106 | label: '', |
107 | - component: 'AlarmProfileSelect', | |
108 | - rules: [{ required: true, message: `请选择${FormFieldsEnum.ALARM_PROFILED}` }], | |
107 | + component: 'Select', | |
109 | 108 | ifShow: ({ model }) => model[FormFieldsEnum.OUT_TARGET] === ExecutionActionEnum.MSG_NOTIFY, |
109 | + rules: [{ required: true, message: `请选择${FormFieldsNameEnum.ALARM_LEVEL}` }], | |
110 | 110 | componentProps: () => { |
111 | 111 | return { |
112 | - api: async () => { | |
113 | - if (!unref(organizationId)) return []; | |
114 | - return await getOrganizationAlarmConfig({ organizationId: unref(organizationId) }); | |
115 | - }, | |
116 | - labelField: 'name', | |
117 | - valueField: 'id', | |
118 | - placeholder: `请选择${FormFieldsNameEnum.ALARM_PROFILED}`, | |
112 | + options: Object.keys(AlarmLevelEnum).map((value) => ({ | |
113 | + label: AlarmLevelNameEnum[value], | |
114 | + value, | |
115 | + })), | |
116 | + placeholder: `请选择${FormFieldsNameEnum.ALARM_LEVEL}`, | |
119 | 117 | }; |
120 | 118 | }, |
121 | 119 | }, |
122 | 120 | { |
123 | - field: FormFieldsEnum.ALARM_LEVEL, | |
121 | + field: FormFieldsEnum.ALARM_PROFILED, | |
124 | 122 | label: '', |
125 | - component: 'Select', | |
123 | + component: 'AlarmProfileSelect', | |
124 | + // rules: [{ required: true, message: `请选择${FormFieldsEnum.ALARM_PROFILED}` }], | |
126 | 125 | ifShow: ({ model }) => model[FormFieldsEnum.OUT_TARGET] === ExecutionActionEnum.MSG_NOTIFY, |
127 | - rules: [{ required: true, message: `请选择${FormFieldsEnum.ALARM_LEVEL}` }], | |
128 | 126 | componentProps: () => { |
129 | 127 | return { |
130 | - options: Object.keys(AlarmLevelEnum).map((value) => ({ | |
131 | - label: AlarmLevelNameEnum[value], | |
132 | - value, | |
133 | - })), | |
134 | - placeholder: `请选择${FormFieldsNameEnum.ALARM_LEVEL}`, | |
128 | + api: async () => { | |
129 | + if (!unref(organizationId)) return []; | |
130 | + return await getOrganizationAlarmConfig({ organizationId: unref(organizationId) }); | |
131 | + }, | |
132 | + labelField: 'name', | |
133 | + valueField: 'id', | |
134 | + placeholder: `请选择${FormFieldsNameEnum.ALARM_PROFILED}(可选项)`, | |
135 | 135 | }; |
136 | 136 | }, |
137 | 137 | }, | ... | ... |
src/views/sys/appPage/config/config.ts
0 → 100644
1 | +import { useRouter } from 'vue-router'; | |
2 | + | |
3 | +export enum ViewTypeEnum { | |
4 | + DATA_BOARD = 'DATA_BOARD', | |
5 | + LARGE_SCREEN = 'LARGE_SCREEN', | |
6 | + SCADA = 'SCADA', | |
7 | +} | |
8 | + | |
9 | +export const goShareUrl = (options: { type: ViewTypeEnum; id: string }, openNew?: false) => { | |
10 | + const { type, id } = options; | |
11 | + const ROUTER = useRouter(); | |
12 | + const { origin } = location; | |
13 | + const path = `/share/${type}/${id}`; | |
14 | + openNew ? ROUTER.push(path) : open(`${origin}${path}`); | |
15 | +}; | ... | ... |
src/views/sys/appPage/hook/index.ts
0 → 100644
src/views/sys/appPage/index.vue
0 → 100644
1 | +<script lang="ts" setup> | |
2 | + import { onMounted } from 'vue'; | |
3 | + import { Spin } from 'ant-design-vue'; | |
4 | + import { ref } from 'vue'; | |
5 | + import { useRoute } from 'vue-router'; | |
6 | + import { useUserStore } from '/@/store/modules/user'; | |
7 | + import BoardDetail from '/@/views/visual/board/detail/index.vue'; | |
8 | + import { doAppLogin } from '/@/api/sys/user'; | |
9 | + | |
10 | + const loading = ref(true); | |
11 | + const ROUTE = useRoute(); | |
12 | + const contentData = ref<any>(); | |
13 | + const canLoadComponent = ref(false); | |
14 | + | |
15 | + const modelVisable = ref(false); | |
16 | + | |
17 | + const userStore = useUserStore(); | |
18 | + | |
19 | + const getShareToken = async () => { | |
20 | + const { params } = ROUTE; | |
21 | + const { userId }: any = params; | |
22 | + const { token, refreshToken } = await doAppLogin(userId); | |
23 | + userStore.storeToken(token, refreshToken); | |
24 | + }; | |
25 | + | |
26 | + const getContentLoading = ref(false); | |
27 | + const getContentData = async () => { | |
28 | + try { | |
29 | + getContentLoading.value = true; | |
30 | + loading.value = false; | |
31 | + canLoadComponent.value = true; | |
32 | + modelVisable.value = false; | |
33 | + } catch (error) { | |
34 | + } finally { | |
35 | + getContentLoading.value = false; | |
36 | + } | |
37 | + }; | |
38 | + | |
39 | + onMounted(async () => { | |
40 | + await getShareToken(); | |
41 | + await getContentData(); | |
42 | + }); | |
43 | +</script> | |
44 | + | |
45 | +<template> | |
46 | + <Spin | |
47 | + :spinning="loading" | |
48 | + tip="正在加载中..." | |
49 | + size="large" | |
50 | + class="!flex justify-center items-center w-full h-full share-full-loading" | |
51 | + > | |
52 | + <BoardDetail v-if="canLoadComponent" :value="contentData" /> | |
53 | + </Spin> | |
54 | +</template> | |
55 | + | |
56 | +<style lang="less"> | |
57 | + .share-page-token-modal { | |
58 | + .ant-modal-close { | |
59 | + display: none; | |
60 | + } | |
61 | + } | |
62 | +</style> | ... | ... |
src/views/visual/board/components/TextComponent/config.ts
deleted
100644 → 0
1 | -import { ComponentInfo, DataComponentRecord, DataSource } from '/@/api/dataBoard/model'; | |
2 | -export interface TextComponentLayout { | |
3 | - id: string; | |
4 | - base?: boolean; | |
5 | - showUpdate?: boolean; | |
6 | - showIcon?: boolean; | |
7 | - showUnit?: boolean; | |
8 | -} | |
9 | - | |
10 | -export interface TextComponentValue { | |
11 | - name: string; | |
12 | - value: number; | |
13 | - icon?: string; | |
14 | - unit?: string; | |
15 | - updateTime?: string; | |
16 | - fontColor?: string; | |
17 | - iconColor?: string; | |
18 | - deviceName?: string; | |
19 | -} | |
20 | - | |
21 | -type TextComponentDefault = TextComponentLayout; | |
22 | - | |
23 | -export const TextComponent1Config: TextComponentDefault = { | |
24 | - id: 'text-component-1', | |
25 | - base: true, | |
26 | -}; | |
27 | - | |
28 | -export const TextComponent3Config: TextComponentDefault = { | |
29 | - id: 'text-component-3', | |
30 | - base: false, | |
31 | - showUpdate: true, | |
32 | -}; | |
33 | -export const TextComponent4Config: TextComponentDefault = { | |
34 | - id: 'text-component-4', | |
35 | - base: false, | |
36 | - showIcon: true, | |
37 | - showUpdate: true, | |
38 | - showUnit: true, | |
39 | -}; | |
40 | -export const TextComponent5Config: TextComponentDefault = { | |
41 | - id: 'text-component-5', | |
42 | - base: false, | |
43 | - showIcon: true, | |
44 | - showUnit: true, | |
45 | -}; | |
46 | - | |
47 | -export const TextComponentDefaultConfig: Partial<ComponentInfo> = { | |
48 | - fontColor: '#000', | |
49 | - unit: '℃', | |
50 | - iconColor: '#367BFF', | |
51 | - icon: 'shuiwen', | |
52 | -}; | |
53 | - | |
54 | -export const transformTextComponentConfig = ( | |
55 | - config: TextComponentDefault, | |
56 | - _record: DataComponentRecord, | |
57 | - dataSourceRecord: DataSource | |
58 | -) => { | |
59 | - return { | |
60 | - layout: { | |
61 | - ...config, | |
62 | - } as TextComponentLayout, | |
63 | - value: { | |
64 | - name: dataSourceRecord.attributeRename || dataSourceRecord.attribute, | |
65 | - deviceName: dataSourceRecord.deviceRename || dataSourceRecord.deviceId, | |
66 | - value: dataSourceRecord.componentInfo.value, | |
67 | - icon: dataSourceRecord.componentInfo.icon, | |
68 | - unit: dataSourceRecord.componentInfo.unit, | |
69 | - updateTime: dataSourceRecord.componentInfo.updateTime, | |
70 | - fontColor: dataSourceRecord.componentInfo.fontColor, | |
71 | - iconColor: dataSourceRecord.componentInfo.iconColor, | |
72 | - } as TextComponentValue, | |
73 | - }; | |
74 | -}; |
... | ... | @@ -4,7 +4,13 @@ export enum ViewType { |
4 | 4 | PRIVATE_VIEW = 'PRIVATE_VIEW', |
5 | 5 | PUBLIC_VIEW = 'PUBLIC_VIEW', |
6 | 6 | } |
7 | +import { Platform } from '../../palette/components/PagerHeader/config'; | |
8 | + | |
7 | 9 | useComponentRegister('OrgTreeSelect', OrgTreeSelect); |
10 | +export enum PlatformType { | |
11 | + PHONE = 'phone', | |
12 | + PC = 'pc', | |
13 | +} | |
8 | 14 | |
9 | 15 | export const formSchema: FormSchema[] = [ |
10 | 16 | { |
... | ... | @@ -24,6 +30,32 @@ export const formSchema: FormSchema[] = [ |
24 | 30 | rules: [{ required: true, message: '组织为必填项' }], |
25 | 31 | }, |
26 | 32 | { |
33 | + field: 'platform', | |
34 | + label: '平台', | |
35 | + required: true, | |
36 | + component: 'RadioGroup', | |
37 | + defaultValue: PlatformType.PC, | |
38 | + componentProps({ formModel }) { | |
39 | + return { | |
40 | + defaultValue: PlatformType.PC, | |
41 | + options: [ | |
42 | + { label: 'PC端', value: PlatformType.PC }, | |
43 | + { label: '移动端', value: PlatformType.PHONE }, | |
44 | + ], | |
45 | + onChange(e) { | |
46 | + formModel.phoneModel = | |
47 | + e?.target?.value === PlatformType.PHONE ? JSON.stringify(Platform[1]) : []; | |
48 | + }, | |
49 | + }; | |
50 | + }, | |
51 | + }, | |
52 | + { | |
53 | + field: 'phoneModel', | |
54 | + label: '手机端尺寸', | |
55 | + component: 'Input', | |
56 | + ifShow: false, | |
57 | + }, | |
58 | + { | |
27 | 59 | field: 'remark', |
28 | 60 | label: '备注', |
29 | 61 | component: 'InputTextArea', | ... | ... |
1 | +import { PlatformType } from './panelDetail'; | |
1 | 2 | import { FormSchema } from '/@/components/Form'; |
2 | 3 | import { ColEx } from '/@/components/Form/src/types'; |
3 | 4 | import { useGridLayout } from '/@/hooks/component/useGridLayout'; |
... | ... | @@ -13,4 +14,16 @@ export const formSchema: FormSchema[] = [ |
13 | 14 | placeholder: '请输入看板名称', |
14 | 15 | }, |
15 | 16 | }, |
17 | + { | |
18 | + field: 'platform', | |
19 | + label: '平台', | |
20 | + component: 'Select', | |
21 | + colProps: useGridLayout(2, 3, 4) as unknown as ColEx, | |
22 | + componentProps: { | |
23 | + options: [ | |
24 | + { label: 'PC端', value: PlatformType.PC }, | |
25 | + { label: '移动端', value: PlatformType.PHONE }, | |
26 | + ], | |
27 | + }, | |
28 | + }, | |
16 | 29 | ]; | ... | ... |
... | ... | @@ -135,12 +135,13 @@ |
135 | 135 | |
136 | 136 | const handleViewBoard = (record: DataBoardRecord) => { |
137 | 137 | const hasDetailPermission = hasPermission(VisualBoardPermission.DETAIL); |
138 | + const { platform = 'pc' } = record || {}; | |
138 | 139 | if (hasDetailPermission) { |
139 | 140 | const boardId = encode(record.id); |
140 | 141 | const boardName = encode(record.name); |
141 | 142 | const organizationId = encode(record!.organizationId!); |
142 | 143 | |
143 | - router.push(`/visual/board/detail/${boardId}/${boardName}/${organizationId}`); | |
144 | + router.push(`/visual/board/detail/${boardId}/${boardName}/${platform}/${organizationId}`); | |
144 | 145 | } else createMessage.warning('没有权限'); |
145 | 146 | }; |
146 | 147 | </script> |
... | ... | @@ -170,7 +171,7 @@ |
170 | 171 | </Dropdown> |
171 | 172 | </template> |
172 | 173 | <section @click="handleViewBoard(item)"> |
173 | - <div class="flex data-card__info"> | |
174 | + <div class="flex data-card__info relative"> | |
174 | 175 | <div> |
175 | 176 | <div>组件数量</div> |
176 | 177 | <Statistic class="text-2xl" :value="item.componentNum"> |
... | ... | @@ -179,6 +180,12 @@ |
179 | 180 | </template> |
180 | 181 | </Statistic> |
181 | 182 | </div> |
183 | + <div class="absolute" style="right: 1%"> | |
184 | + <Icon | |
185 | + :icon="item.platform === 'pc' ? 'ri:computer-line' : 'clarity:mobile-phone-solid'" | |
186 | + style="font-size: 32px" | |
187 | + /> | |
188 | + </div> | |
182 | 189 | </div> |
183 | 190 | <div class="flex justify-between mt-4 text-sm" style="color: #999"> |
184 | 191 | <div class="flex min-w-20 mr-3"> | ... | ... |
... | ... | @@ -107,6 +107,9 @@ export const formSchemas = (): FormSchema[] => { |
107 | 107 | [DataSourceField.DEVICE_ID]: null, |
108 | 108 | }); |
109 | 109 | }, |
110 | + apiTreeSelectProps: { | |
111 | + params: { organizationId: location?.pathname?.split('/')?.pop() || '' }, | |
112 | + }, | |
110 | 113 | showCreate: false, |
111 | 114 | getPopupContainer: () => document.body, |
112 | 115 | }; |
... | ... | @@ -157,7 +160,6 @@ export const formSchemas = (): FormSchema[] => { |
157 | 160 | }); |
158 | 161 | }, |
159 | 162 | placeholder: '请选择设备', |
160 | - getPopupContainer: () => document.body, | |
161 | 163 | ...createPickerSearch(), |
162 | 164 | }; |
163 | 165 | }, | ... | ... |
... | ... | @@ -58,24 +58,24 @@ |
58 | 58 | |
59 | 59 | const alarmColumns: BasicColumn[] = [ |
60 | 60 | { |
61 | - title: '状态', | |
62 | - dataIndex: 'severity', | |
61 | + title: '告警等级', | |
62 | + dataIndex: 'status', | |
63 | 63 | ellipsis: true, |
64 | 64 | width: 80, |
65 | - customRender: ({ record }) => { | |
66 | - const { severity } = record; | |
67 | - const { text, color } = alarmLevel(severity); | |
68 | - return h(Tag, { color }, () => text); | |
69 | - }, | |
65 | + format: (text) => statusType(text), | |
70 | 66 | }, |
71 | 67 | { title: '设备', dataIndex: 'device', ellipsis: true, width: 120 }, |
72 | 68 | { title: '告警场景', dataIndex: 'type', ellipsis: true, width: 80 }, |
73 | 69 | { |
74 | - title: '状态', | |
75 | - dataIndex: 'status', | |
70 | + title: '告警级别', | |
71 | + dataIndex: 'severity', | |
76 | 72 | ellipsis: true, |
77 | 73 | width: 80, |
78 | - format: (text) => statusType(text), | |
74 | + customRender: ({ record }) => { | |
75 | + const { severity } = record; | |
76 | + const { text, color } = alarmLevel(severity); | |
77 | + return h(Tag, { color }, () => text); | |
78 | + }, | |
79 | 79 | }, |
80 | 80 | { |
81 | 81 | title: '时间', | ... | ... |
... | ... | @@ -4,8 +4,8 @@ |
4 | 4 | destroyOnClose |
5 | 5 | v-bind="$attrs" |
6 | 6 | width="30rem" |
7 | - :height="50" | |
8 | - :minHeight="50" | |
7 | + :height="75" | |
8 | + :minHeight="75" | |
9 | 9 | @register="register" |
10 | 10 | title="操作密码" |
11 | 11 | @ok="handleSuccess" |
... | ... | @@ -33,6 +33,8 @@ |
33 | 33 | const [registerForm, { getFieldsValue, validate }] = useForm({ |
34 | 34 | labelWidth: 70, |
35 | 35 | schemas: formSchema, |
36 | + | |
37 | + wrapperCol: { span: 12 }, | |
36 | 38 | showActionButtonGroup: false, |
37 | 39 | }); |
38 | 40 | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | import { useBaiduMapSDK } from '../../../hook/useBaiduMapSDK'; |
15 | 15 | import { HistoryModalOkEmitParams, TrackAnimationStatus } from './type'; |
16 | 16 | import { useMapTrackPlayback } from './useMapTrackPlayback'; |
17 | - import { shallowRef } from 'vue'; | |
17 | + // import { shallowRef } from 'vue'; | |
18 | 18 | import { formatToDateTime } from '/@/utils/dateUtil'; |
19 | 19 | |
20 | 20 | const props = defineProps<{ |
... | ... | @@ -25,7 +25,7 @@ |
25 | 25 | |
26 | 26 | const wrapId = `bai-map-${buildUUID()}`; |
27 | 27 | |
28 | - const mapInstance = shallowRef<Nullable<Recordable>>(null); | |
28 | + const mapInstance = ref<Nullable<Recordable>>(null); | |
29 | 29 | |
30 | 30 | const rangString = ref<Nullable<string>>(null); |
31 | 31 | |
... | ... | @@ -35,6 +35,9 @@ |
35 | 35 | openModal(true, toRaw(props.config.option.dataSource)); |
36 | 36 | }; |
37 | 37 | |
38 | + const { genTrackPlaybackAnimation, playStatus, playFn, continueFn, pauseFn } = | |
39 | + useMapTrackPlayback(mapInstance); | |
40 | + | |
38 | 41 | const getIsWidgetLibSelectMode = computed(() => { |
39 | 42 | return !props.config.option.dataSource; |
40 | 43 | }); |
... | ... | @@ -68,8 +71,8 @@ |
68 | 71 | async function initMap() { |
69 | 72 | const wrapEl = unref(wrapRef); |
70 | 73 | if (!wrapEl) return; |
71 | - const BMapGL = (window as any).BMapGL; | |
72 | 74 | if (!Reflect.has(window, 'BMapGL')) return; |
75 | + const BMapGL = (window as any).BMapGL; | |
73 | 76 | mapInstance.value = new BMapGL.Map(wrapId); |
74 | 77 | |
75 | 78 | // 定位当前城市 |
... | ... | @@ -89,13 +92,10 @@ |
89 | 92 | } |
90 | 93 | |
91 | 94 | const { loading } = useBaiduMapSDK(initMap); |
92 | - | |
93 | - const { genTrackPlaybackAnimation, playStatus, playFn, continueFn, pauseFn } = | |
94 | - useMapTrackPlayback(mapInstance); | |
95 | 95 | </script> |
96 | 96 | |
97 | 97 | <template> |
98 | - <main class="w-full h-full flex flex-col justify-center items-center"> | |
98 | + <main class="w-full h-full flex flex-col p-2 justify-center items-center"> | |
99 | 99 | <div class="w-full flex justify-end"> |
100 | 100 | <Button |
101 | 101 | type="text" |
... | ... | @@ -126,9 +126,8 @@ |
126 | 126 | :spinning="loading" |
127 | 127 | wrapper-class-name="map-spin-wrapper !w-full !h-full !flex justify-center items-center pointer-events-none" |
128 | 128 | tip="地图加载中..." |
129 | - > | |
130 | - <div ref="wrapRef" :id="wrapId" class="w-full h-full no-drag"> </div> | |
131 | - </Spin> | |
129 | + /> | |
130 | + <div ref="wrapRef" :id="wrapId" class="w-full h-full no-drag"> </div> | |
132 | 131 | |
133 | 132 | <HistoryDataModel @register="register" @ok="handleRenderHistroyData" /> |
134 | 133 | </main> | ... | ... |
... | ... | @@ -88,6 +88,9 @@ export const formSchemas: FormSchema[] = [ |
88 | 88 | return { |
89 | 89 | showCreate: false, |
90 | 90 | allowClean: true, |
91 | + apiTreeSelectProps: { | |
92 | + params: { organizationId: location?.pathname?.split('/')?.pop() || '' }, | |
93 | + }, | |
91 | 94 | onChange() { |
92 | 95 | setFieldsValue({ |
93 | 96 | [FormFieldEnum.ACCESS_MODE]: null, | ... | ... |
... | ... | @@ -68,8 +68,6 @@ |
68 | 68 | }; |
69 | 69 | |
70 | 70 | const instance = unref(basicVideoPlayEl)?.customInit((options) => { |
71 | - withToken.value = true; | |
72 | - | |
73 | 71 | if (unref(withToken)) { |
74 | 72 | (options as any).flvjs.config.headers = { |
75 | 73 | 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`, | ... | ... |
... | ... | @@ -5,6 +5,7 @@ |
5 | 5 | import { ComponentPropsConfigType } from '../../../index.type'; |
6 | 6 | import { useDataFetch } from '../../../hook/socket/useSocket'; |
7 | 7 | import { DataFetchUpdateFn } from '../../../hook/socket/useSocket.type'; |
8 | + import { isNullOrUnDef } from '/@/utils/is'; | |
8 | 9 | |
9 | 10 | const props = defineProps<{ |
10 | 11 | config: ComponentPropsConfigType; |
... | ... | @@ -42,6 +43,7 @@ |
42 | 43 | const { data = {} } = message; |
43 | 44 | const [latest] = data[attribute] || []; |
44 | 45 | const [timespan, value] = latest; |
46 | + if (isNullOrUnDef(value)) return; | |
45 | 47 | time.value = timespan; |
46 | 48 | url.value = `${value}?timespan=${timespan}`; |
47 | 49 | }; | ... | ... |
... | ... | @@ -23,6 +23,7 @@ |
23 | 23 | import { WidgetDataType } from '../../hooks/useDataSource'; |
24 | 24 | import { ExtraDataSource } from '../../types'; |
25 | 25 | import { OrderByEnum } from '/@/views/device/localtion/cpns/TimePeriodForm/config'; |
26 | + import { useApp } from '../../hooks/useApp'; | |
26 | 27 | |
27 | 28 | type DeviceOption = Record<'label' | 'value' | 'organizationId', string>; |
28 | 29 | |
... | ... | @@ -38,6 +39,8 @@ |
38 | 39 | |
39 | 40 | const historyData = ref<{ ts: number; value: string; name: string }[]>([]); |
40 | 41 | |
42 | + const { getIsAppPage } = useApp(); | |
43 | + | |
41 | 44 | const { deviceAttrs, getDeviceKeys, getSearchParams, setChartOptions, getDeviceAttribute } = |
42 | 45 | useHistoryData(); |
43 | 46 | |
... | ... | @@ -294,7 +297,7 @@ |
294 | 297 | :destroy-on-close="true" |
295 | 298 | :show-ok-btn="false" |
296 | 299 | cancel-text="关闭" |
297 | - width="70%" | |
300 | + :width="getIsAppPage ? '100%' : '75%'" | |
298 | 301 | title="历史趋势" |
299 | 302 | > |
300 | 303 | <section | ... | ... |
1 | +export const Platform = [ | |
2 | + { key: 'iPhone 8', title: 'iPhone 8', width: 375, height: 667 }, | |
3 | + { key: 'iPhone 8 Plus', title: 'iPhone 8 Plus', width: 415, height: 737 }, | |
4 | + { key: 'iPhone X/XS', title: 'iPhone X/XS', width: 376, height: 813 }, | |
5 | + { key: 'iPad 4', title: 'iPad 4', width: 709, height: 1025 }, | |
6 | + { key: 'Galaxy S9', title: 'Galaxy S9', width: 361, height: 741 }, | |
7 | + { key: 'Galaxy S10/S10+', title: 'Galaxy S10/S10+', width: 413, height: 870 }, | |
8 | + { key: 'Pixel 2', title: 'Pixel 2', width: 413, height: 732 }, | |
9 | + { key: 'custom', title: '自定义', width: '', height: '' }, | |
10 | +]; | ... | ... |
... | ... | @@ -9,6 +9,7 @@ |
9 | 9 | |
10 | 10 | const ROUTE = useRoute(); |
11 | 11 | const ROUTER = useRouter(); |
12 | + | |
12 | 13 | const getIsSharePage = computed(() => { |
13 | 14 | return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId'); |
14 | 15 | }); |
... | ... | @@ -21,8 +22,6 @@ |
21 | 22 | if (unref(getIsSharePage)) return; |
22 | 23 | ROUTER.go(-1); |
23 | 24 | }; |
24 | - | |
25 | - // const handleOpenCreatePanel = () => {}; | |
26 | 25 | </script> |
27 | 26 | |
28 | 27 | <template> | ... | ... |
... | ... | @@ -19,6 +19,7 @@ |
19 | 19 | import { computed } from 'vue'; |
20 | 20 | import { useGetComponentConfig } from '../../../packages/hook/useGetComponetConfig'; |
21 | 21 | import { isBoolean } from '/@/utils/is'; |
22 | + import { useApp } from '../../hooks/useApp'; | |
22 | 23 | |
23 | 24 | const props = defineProps<{ |
24 | 25 | sourceInfo: WidgetDataType; |
... | ... | @@ -38,6 +39,8 @@ |
38 | 39 | |
39 | 40 | const { createMessage } = useMessage(); |
40 | 41 | |
42 | + const { getIsAppPage } = useApp(); | |
43 | + | |
41 | 44 | const { boardId } = useBoardId(); |
42 | 45 | |
43 | 46 | const dropMenuList = ref<AuthDropMenuList[]>([ |
... | ... | @@ -171,7 +174,7 @@ |
171 | 174 | <AreaChartOutlined v-else class="text-lg" @click="handleOpenTrendModal" /> |
172 | 175 | </Tooltip> |
173 | 176 | <AuthDropDown |
174 | - v-if="!isCustomerUser && dropMenuList.length" | |
177 | + v-if="!isCustomerUser && dropMenuList.length && !getIsAppPage" | |
175 | 178 | :drop-menu-list="dropMenuList" |
176 | 179 | :trigger="['click']" |
177 | 180 | > | ... | ... |
... | ... | @@ -7,6 +7,7 @@ |
7 | 7 | import { BasicForm } from '/@/components/Form'; |
8 | 8 | import { BasicModal } from '/@/components/Modal'; |
9 | 9 | import { nextTick } from 'vue'; |
10 | + import { useApp } from '../../hooks/useApp'; | |
10 | 11 | const emit = defineEmits(['register', 'getAlarmForm', 'getHistoryForm']); |
11 | 12 | // const emit = defineEmits<{ |
12 | 13 | // (event: 'getAlarmForm', data: WidgetDataType): void; |
... | ... | @@ -15,6 +16,8 @@ |
15 | 16 | // const fontId = ref(''); |
16 | 17 | const [registerModal, { closeModal }] = useModalInner(async () => {}); |
17 | 18 | |
19 | + const { getIsAppPage } = useApp(); | |
20 | + | |
18 | 21 | const [register, method] = useForm({ |
19 | 22 | schemas: formSchema(), |
20 | 23 | baseColProps: useGridLayout(1) as unknown as ColEx, |
... | ... | @@ -53,7 +56,7 @@ |
53 | 56 | :destroy-on-close="true" |
54 | 57 | :show-ok-btn="true" |
55 | 58 | cancel-text="关闭" |
56 | - width="40%" | |
59 | + :width="getIsAppPage ? '80%' : '40%'" | |
57 | 60 | title="历史趋势" |
58 | 61 | > |
59 | 62 | <section | ... | ... |
src/views/visual/palette/hooks/useApp.ts
0 → 100644
1 | +import { computed } from 'vue'; | |
2 | +import { useRoute } from 'vue-router'; | |
3 | + | |
4 | +export const useApp = () => { | |
5 | + const ROUTE = useRoute(); | |
6 | + const getIsAppPage = computed(() => { | |
7 | + return ROUTE.matched.find((item) => item.path === '/appPage/:boardId/:userId'); | |
8 | + }); | |
9 | + | |
10 | + const isPhone = () => { | |
11 | + const values = location?.pathname.split('/') || []; | |
12 | + return values[values?.length - 2] === 'phone' ? true : false; | |
13 | + }; | |
14 | + | |
15 | + return { getIsAppPage, isPhone }; | |
16 | +}; | ... | ... |
1 | 1 | import { unref } from 'vue'; |
2 | 2 | import { Layout } from 'vue3-grid-layout'; |
3 | -import { DEFAULT_MAX_COL, DEFAULT_WIDGET_HEIGHT, DEFAULT_WIDGET_WIDTH } from '..'; | |
3 | +import { DEFAULT_MAX_COL, DEFAULT_WIDGET_HEIGHT, DEFAULT_WIDGET_WIDTH, PHONE_SIZE } from '..'; | |
4 | +import { useApp } from './useApp'; | |
4 | 5 | |
5 | 6 | interface GapRecord { |
6 | 7 | maxGap: number; |
... | ... | @@ -8,9 +9,14 @@ interface GapRecord { |
8 | 9 | endIndex: Nullable<number>; |
9 | 10 | } |
10 | 11 | |
12 | +const { isPhone } = useApp(); | |
13 | + | |
11 | 14 | export const useCalcNewWidgetPosition = ( |
12 | 15 | layoutInfo: Layout[], |
13 | - randomLayout = { width: DEFAULT_WIDGET_WIDTH, height: DEFAULT_WIDGET_HEIGHT } | |
16 | + randomLayout = { | |
17 | + width: isPhone() ? PHONE_SIZE.DEFAULT_WIDGET_WIDTH : DEFAULT_WIDGET_WIDTH, | |
18 | + height: isPhone() ? PHONE_SIZE.DEFAULT_WIDGET_HEIGHT : DEFAULT_WIDGET_HEIGHT, | |
19 | + } | |
14 | 20 | ) => { |
15 | 21 | let maxWidth = 0; |
16 | 22 | let maxHeight = 0; | ... | ... |
... | ... | @@ -30,6 +30,10 @@ export const useDataSource = (propsRef: ComputedRef<Recordable>) => { |
30 | 30 | const getIsSharePage = computed(() => { |
31 | 31 | return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId'); |
32 | 32 | }); |
33 | + //小程序打开的页面 | |
34 | + const getIsAppPage = computed(() => { | |
35 | + return ROUTE.matched.find((item) => item.path === '/appPage/:boardId/:userId'); | |
36 | + }); | |
33 | 37 | |
34 | 38 | const getBoardId = computed(() => { |
35 | 39 | return decode((ROUTE.params as { boardId: string }).boardId); |
... | ... | @@ -131,8 +135,8 @@ export const useDataSource = (propsRef: ComputedRef<Recordable>) => { |
131 | 135 | |
132 | 136 | return { |
133 | 137 | loading, |
134 | - draggable: !unref(getIsSharePage), | |
135 | - resizable: !unref(getIsSharePage), | |
138 | + draggable: !unref(getIsSharePage) && !unref(getIsAppPage), | |
139 | + resizable: !unref(getIsSharePage) && !unref(getIsAppPage), | |
136 | 140 | dataSource, |
137 | 141 | rawDataSource, |
138 | 142 | getDataSource, | ... | ... |
... | ... | @@ -11,6 +11,12 @@ export const DEFAULT_WIDGET_HEIGHT = 6; |
11 | 11 | export const DEFAULT_MIN_HEIGHT = 5; |
12 | 12 | export const DEFAULT_MIN_WIDTH = 3; |
13 | 13 | export const DEFAULT_ITEM_MARIGN = 20; |
14 | +export const PHONE_SIZE = { | |
15 | + DEFAULT_WIDGET_WIDTH: 24, | |
16 | + DEFAULT_WIDGET_HEIGHT: 5, | |
17 | + DEFAULT_MIN_HEIGHT: 5, | |
18 | + DEFAULT_MIN_WIDTH: 24, | |
19 | +}; | |
14 | 20 | |
15 | 21 | import { ViewTypeEnum } from '/@/views/sys/share/config/config'; |
16 | 22 | ... | ... |
... | ... | @@ -10,6 +10,7 @@ |
10 | 10 | DEFAULT_MIN_WIDTH, |
11 | 11 | DEFAULT_ITEM_MARIGN, |
12 | 12 | VisualComponentPermission, |
13 | + PHONE_SIZE, | |
13 | 14 | } from './index'; |
14 | 15 | import { useDragGridLayout } from './hooks/useDragGridLayout'; |
15 | 16 | import { WidgetHeader, WidgetWrapper } from './components/WidgetWrapper'; |
... | ... | @@ -19,6 +20,7 @@ |
19 | 20 | import { DataSourceBindPanel } from '../dataSourceBindPanel'; |
20 | 21 | import { PageHeader } from './components/PagerHeader'; |
21 | 22 | import { useShare } from './hooks/useShare'; |
23 | + import { useApp } from './hooks/useApp'; | |
22 | 24 | import { useRole } from '/@/hooks/business/useRole'; |
23 | 25 | import { Authority } from '/@/components/Authority'; |
24 | 26 | import { useModal } from '/@/components/Modal'; |
... | ... | @@ -33,6 +35,11 @@ |
33 | 35 | import { useSocket } from '/@/views/visual/packages/hook/socket/useSocket'; |
34 | 36 | import { createAlarmContext } from './hooks/useAlarmTime'; |
35 | 37 | import { createHistoryContext } from './hooks/useHistoryForm'; |
38 | + import { MoreOutlined, LeftOutlined } from '@ant-design/icons-vue'; | |
39 | + import WIFISVG from '/@/assets/svg/wifi.svg'; | |
40 | + import SIGNALSVG from '/@/assets/svg/signal.svg'; | |
41 | + import BATTERYSVG from '/@/assets/svg/battery.svg'; | |
42 | + import { useRoute } from 'vue-router'; | |
36 | 43 | |
37 | 44 | const props = defineProps<{ |
38 | 45 | value?: Recordable; |
... | ... | @@ -44,6 +51,8 @@ |
44 | 51 | |
45 | 52 | const containerRectRef = ref<DOMRect>({} as unknown as DOMRect); |
46 | 53 | |
54 | + const ROUTE = useRoute(); | |
55 | + | |
47 | 56 | const { loading, draggable, resizable, dataSource, rawDataSource, setLayoutInfo, getDataSource } = |
48 | 57 | useDataSource(getProps); |
49 | 58 | |
... | ... | @@ -63,6 +72,9 @@ |
63 | 72 | } |
64 | 73 | }); |
65 | 74 | const { getIsSharePage } = useShare(); |
75 | + | |
76 | + // getIsAppPage 是否是小程序进入的页面 isPhone 是否是创建的手机端 | |
77 | + const { getIsAppPage, isPhone } = useApp(); | |
66 | 78 | const { isCustomerUser } = useRole(); |
67 | 79 | const handleOpenCreatePanel = () => { |
68 | 80 | openModal(true, { mode: DataActionModeEnum.CREATE } as ModalParamsType); |
... | ... | @@ -157,6 +169,25 @@ |
157 | 169 | }, |
158 | 170 | { immediate: true } |
159 | 171 | ); |
172 | + watch( | |
173 | + getIsAppPage, | |
174 | + (value) => { | |
175 | + if (value) { | |
176 | + const root = document.querySelector('#app'); | |
177 | + (root as HTMLDivElement).style.backgroundColor = | |
178 | + unref(getDarkMode) === ThemeEnum.LIGHT ? '#F5F5F5' : '#1b1b1b'; | |
179 | + } | |
180 | + }, | |
181 | + { immediate: true } | |
182 | + ); | |
183 | + const getDataBoardName = computed(() => { | |
184 | + return decodeURIComponent((ROUTE.params as { boardName: string }).boardName || ''); | |
185 | + }); | |
186 | + | |
187 | + const phoneSize = ref({ | |
188 | + width: 375, | |
189 | + height: 667, | |
190 | + }); | |
160 | 191 | </script> |
161 | 192 | |
162 | 193 | <template> |
... | ... | @@ -164,7 +195,7 @@ |
164 | 195 | ref="containerRefEl" |
165 | 196 | class="palette w-full h-full flex-col bg-neutral-100 flex dark:bg-dark-700 dark:text-light-50" |
166 | 197 | > |
167 | - <PageHeader :widget-number="dataSource.length"> | |
198 | + <PageHeader v-if="!getIsAppPage" :widget-number="dataSource.length"> | |
168 | 199 | <Authority :value="VisualComponentPermission.CREATE"> |
169 | 200 | <Button |
170 | 201 | v-if="!getIsSharePage && !isCustomerUser" |
... | ... | @@ -177,55 +208,104 @@ |
177 | 208 | </PageHeader> |
178 | 209 | |
179 | 210 | <Spin :spinning="loading"> |
180 | - <GridLayout | |
181 | - v-model:layout="dataSource" | |
182 | - :col-num="DEFAULT_MAX_COL" | |
183 | - :row-height="30" | |
184 | - :margin="[DEFAULT_ITEM_MARIGN, DEFAULT_ITEM_MARIGN]" | |
185 | - :is-draggable="draggable" | |
186 | - :is-resizable="resizable" | |
187 | - :vertical-compact="true" | |
188 | - :use-css-transforms="true" | |
189 | - style="width: 100%" | |
190 | - > | |
191 | - <GridItem | |
192 | - v-for="item in dataSource" | |
193 | - :key="item.i" | |
194 | - :static="item.static" | |
195 | - :x="item.x" | |
196 | - :y="item.y" | |
197 | - :w="item.w" | |
198 | - :h="item.h" | |
199 | - :i="item.i" | |
200 | - :min-h="DEFAULT_MIN_HEIGHT" | |
201 | - :min-w="DEFAULT_MIN_WIDTH" | |
202 | - :style="{ display: 'flex', flexWrap: 'wrap' }" | |
203 | - class="grid-item-layout" | |
204 | - @resized="resized" | |
205 | - @resize="resize" | |
206 | - @moved="moved" | |
207 | - @container-resized="containerResized" | |
208 | - drag-ignore-from=".no-drag" | |
211 | + <div class="w-full h-full flex justify-center items-center mb-3"> | |
212 | + <div | |
213 | + :style=" | |
214 | + !getIsAppPage && isPhone() | |
215 | + ? { | |
216 | + width: phoneSize.width + 'px', | |
217 | + height: phoneSize.height + 'px', | |
218 | + border: '2px solid #e5e7eb', | |
219 | + } | |
220 | + : { width: '100%', height: '100%' } | |
221 | + " | |
222 | + style="border-radius: 1%" | |
209 | 223 | > |
210 | - <WidgetWrapper> | |
211 | - <template #header> | |
212 | - <WidgetHeader | |
213 | - :raw-data-source="rawDataSource" | |
214 | - :source-info="item" | |
215 | - @update="handleUpdateWidget" | |
216 | - @open-trend="handleOpenTrend" | |
217 | - @open-alarm="handleOpenAlarm" | |
218 | - @ok="getDataSource" | |
219 | - /> | |
220 | - </template> | |
221 | - <WidgetDistribute :source-info="item" /> | |
222 | - </WidgetWrapper> | |
223 | - </GridItem> | |
224 | - </GridLayout> | |
225 | - <Empty | |
226 | - v-if="!dataSource.length" | |
227 | - class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" | |
228 | - /> | |
224 | + <!-- 手机端的模拟样式 --> | |
225 | + <div | |
226 | + v-if="!getIsAppPage && isPhone()" | |
227 | + style="height: 60px; background: white" | |
228 | + class="px-1 py-1 relative" | |
229 | + > | |
230 | + <div class="flex justify-between"> | |
231 | + <div>thingskit</div> | |
232 | + <div class="flex items-center"> | |
233 | + <img :src="WIFISVG" alt="" class="w-3 h-3" /> | |
234 | + <img :src="SIGNALSVG" alt="" class="w-3 h-3 mx-1" /> | |
235 | + <img :src="BATTERYSVG" alt="" class="w-4 h-4 rotate-45" /> | |
236 | + <h1 class="mb-0">18:13</h1> | |
237 | + </div> | |
238 | + </div> | |
239 | + <div class="flex items-center justify-between"> | |
240 | + <LeftOutlined class="transform cursor-pointer text-lg" /> | |
241 | + <h1 class="font-bold">{{ getDataBoardName }}</h1> | |
242 | + <MoreOutlined class="transform rotate-90 cursor-pointer text-lg" /> | |
243 | + </div> | |
244 | + </div> | |
245 | + | |
246 | + <div | |
247 | + id="appLayoutId" | |
248 | + :style=" | |
249 | + !getIsAppPage && isPhone() ? { height: `calc(${phoneSize.height}px - 67px)` } : {} | |
250 | + " | |
251 | + class="overflow-y-scroll" | |
252 | + > | |
253 | + <GridLayout | |
254 | + v-model:layout="dataSource" | |
255 | + :col-num="DEFAULT_MAX_COL" | |
256 | + :row-height="30" | |
257 | + :margin="[DEFAULT_ITEM_MARIGN, DEFAULT_ITEM_MARIGN]" | |
258 | + :is-draggable="draggable" | |
259 | + :is-resizable="resizable" | |
260 | + :vertical-compact="true" | |
261 | + :use-css-transforms="true" | |
262 | + style="width: 100%" | |
263 | + :style="{ | |
264 | + '--is-app': getIsAppPage ? 'auto' : 'none', | |
265 | + }" | |
266 | + > | |
267 | + <GridItem | |
268 | + v-for="item in dataSource" | |
269 | + :key="item.i" | |
270 | + :static="item.static" | |
271 | + :x="item.x" | |
272 | + :y="item.y" | |
273 | + :w="item.w" | |
274 | + :h="item.h" | |
275 | + :i="item.i" | |
276 | + :min-h="isPhone() ? PHONE_SIZE.DEFAULT_MIN_HEIGHT : DEFAULT_MIN_HEIGHT" | |
277 | + :min-w="isPhone() ? PHONE_SIZE.DEFAULT_MIN_WIDTH : DEFAULT_MIN_WIDTH" | |
278 | + :style="{ display: 'flex', flexWrap: 'wrap' }" | |
279 | + class="grid-item-layout" | |
280 | + @resized="resized" | |
281 | + @resize="resize" | |
282 | + @moved="moved" | |
283 | + @container-resized="containerResized" | |
284 | + drag-ignore-from=".no-drag" | |
285 | + > | |
286 | + <WidgetWrapper> | |
287 | + <template #header> | |
288 | + <WidgetHeader | |
289 | + :raw-data-source="rawDataSource" | |
290 | + :source-info="item" | |
291 | + @update="handleUpdateWidget" | |
292 | + @open-trend="handleOpenTrend" | |
293 | + @open-alarm="handleOpenAlarm" | |
294 | + @ok="getDataSource" | |
295 | + /> | |
296 | + </template> | |
297 | + <WidgetDistribute :source-info="item" /> | |
298 | + </WidgetWrapper> | |
299 | + </GridItem> | |
300 | + </GridLayout> | |
301 | + </div> | |
302 | + <Empty | |
303 | + v-if="!dataSource.length" | |
304 | + :class="isPhone() ? 'absolute' : 'fixed'" | |
305 | + class="top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" | |
306 | + /> | |
307 | + </div> | |
308 | + </div> | |
229 | 309 | </Spin> |
230 | 310 | |
231 | 311 | <DataSourceBindPanel @register="register" :layout="dataSource" @ok="getDataSource" /> |
... | ... | @@ -238,4 +318,9 @@ |
238 | 318 | </section> |
239 | 319 | </template> |
240 | 320 | |
241 | -<style lang="less" scoped></style> | |
321 | +<style lang="less"> | |
322 | + .vue-grid-item { | |
323 | + pointer-events: painted; | |
324 | + touch-action: var(--is-app) !important; | |
325 | + } | |
326 | +</style> | ... | ... |