Commit afde031f90ddbab1e1b958c78a4668a84f509831

Authored by xp.Huang
2 parents 6715c23b 0109c9c5

Merge branch 'main_dev'

# Conflicts:
#	src/views/message/template/TemplateDrawer.vue
Showing 79 changed files with 1186 additions and 310 deletions
... ... @@ -13,7 +13,7 @@
13 13 name="viewport"
14 14 content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
15 15 />
16   - <title></title>
  16 + <title>&lrm;</title>
17 17 <link rel="icon" href="/favicon.ico" />
18 18 </head>
19 19
... ...
... ... @@ -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 {
... ...
... ... @@ -187,6 +187,7 @@ export interface DeviceRecord {
187 187 brand?: string;
188 188 deviceProfileId: string;
189 189 organizationId: string;
  190 + alarmStatus: number;
190 191 deviceProfile: {
191 192 default: boolean;
192 193 name: string;
... ...
... ... @@ -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 +};
... ...
  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
... ...
  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
... ...
  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));
... ...
... ... @@ -144,4 +144,5 @@ export type ComponentType =
144 144 | 'TimeRangePicker'
145 145 | 'TriggerDurationInput'
146 146 | 'AlarmProfileSelect'
147   - | 'LockControlGroup';
  147 + | 'LockControlGroup'
  148 + | 'EnumList';
... ...
... ... @@ -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;
... ...
... ... @@ -24,6 +24,9 @@ export enum DictEnum {
24 24 // 实体类型 规则节点 Filter originator types switch
25 25 ORIGINATOR_TYPES = 'originator_types',
26 26
  27 + // 流媒体平台
  28 + STREAMING_MEDIA_TYPE = 'streaming_media_type',
  29 +
27 30 // 产品品类领域
28 31 CATEGORY_FIELD = 'category_field',
29 32 }
... ...
... ... @@ -4,4 +4,5 @@ export enum DataTypeEnum {
4 4 STRING = 'TEXT',
5 5 STRUCT = 'STRUCT',
6 6 BOOL = 'BOOL',
  7 + ENUM = 'ENUM',
7 8 }
... ...
... ... @@ -16,6 +16,7 @@ export const PageEnum = {
16 16 DEVICE_LIST: '/device/list',
17 17
18 18 SHARE_PAGE: '/share/:viewType/:id/:publicId',
  19 + APP_PAGE: '/appPage/:boardId/:userId',
19 20
20 21 RULE_CHAIN_DETAIL: '/rule/chain/:id',
21 22
... ...
... ... @@ -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
... ...
  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) {
... ...
  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(
... ...
... ... @@ -144,6 +144,7 @@ export const formSchema: FormSchema[] = [
144 144 { label: '移动端', value: Platform.PHONE },
145 145 ],
146 146 },
  147 + dynamicDisabled: ({ model }) => !!model?.enableTemplate,
147 148 },
148 149 {
149 150 field: 'enableTemplate', //前端控制
... ...
... ... @@ -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>
... ...
... ... @@ -28,6 +28,7 @@ export enum FormField {
28 28 REGISTER_ADDRESS = 'registerAddress',
29 29 EXTENSION_DESC = 'extensionDesc',
30 30 STRUCT = 'struct',
  31 + ENUM_LIST = 'enumList',
31 32
32 33 HAS_STRUCT_FROM = 'hasStructForm',
33 34 }
... ...
... ... @@ -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 },
... ...
  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 +};
... ...
  1 +export const isShareMode = () => {
  2 + const sharePageReg = /^\/share\/[^/]+\/[^/]+\/[^/]+$/;
  3 + const { pathname } = location;
  4 + return sharePageReg.test(pathname);
  5 +};
... ...
  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>
... ...
... ... @@ -88,6 +88,7 @@ export const usePlatform = async () => {
88 88 };
89 89
90 90 const setTitle = () => {
  91 + if (!platformInfo.name) return;
91 92 document.title = platformInfo.name || '';
92 93 };
93 94
... ...
... ... @@ -34,6 +34,7 @@
34 34 if (!Reflect.get(value, 'accessCredentials')) {
35 35 Reflect.deleteProperty(value, 'accessCredentials');
36 36 }
  37 +
37 38 return value as any;
38 39 };
39 40
... ...
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
... ...
... ... @@ -4,7 +4,7 @@ export const formSchema: FormSchema[] = [
4 4 {
5 5 field: 'password',
6 6 label: '操作密码',
7   - colProps: { span: 16 },
  7 + colProps: { span: 18 },
8 8 component: 'InputPassword',
9 9 required: true,
10 10 componentProps: {
... ...
... ... @@ -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
... ...
  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>
... ...
... ... @@ -91,6 +91,7 @@ export interface ComponentLayoutType {
91 91 export interface ApiDataBoardDataType {
92 92 componentData: ComponentDataType[];
93 93 componentLayout: ComponentLayoutType[];
  94 + phoneModel?: string;
94 95 }
95 96
96 97 export interface ApiDataBoardInfoType {
... ...