Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev
This commit is contained in:
commit
67df928602
|
@ -2,6 +2,7 @@ import { LocalStore } from '@/utils/comm'
|
|||
import server from '@/utils/request'
|
||||
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable'
|
||||
import { DeviceInstance } from '@/views/device/Instance/typings'
|
||||
import { UnitType } from '@/views/device/Product/typings';
|
||||
|
||||
/**
|
||||
* 删除设备物模型
|
||||
|
@ -242,3 +243,74 @@ export const unbindBatchDevice = (deviceId: string, data: Record<string, any>) =
|
|||
* @returns
|
||||
*/
|
||||
export const bindDevice = (deviceId: string, data: Record<string, any>) => server.post(`/device/gateway/${deviceId}/bind`, data)
|
||||
|
||||
/**
|
||||
* 设备接入网关状态
|
||||
* @param id 设备接入网关id
|
||||
* @returns
|
||||
*/
|
||||
export const queryGatewayState = (id: string) => server.get(`/gateway/device/${id}/detail`)
|
||||
|
||||
/**
|
||||
* 网络组件状态
|
||||
* @param id 网络组件id
|
||||
* @returns
|
||||
*/
|
||||
export const queryNetworkState = (id: string) => server.get(`/network/config/${id}`)
|
||||
|
||||
/**
|
||||
* 产品状态
|
||||
* @param id 产品id
|
||||
* @returns
|
||||
*/
|
||||
export const queryProductState = (id: string) => server.get(`/device/product/${id}`)
|
||||
|
||||
/**
|
||||
* 产品配置
|
||||
* @param id 产品id
|
||||
* @returns
|
||||
*/
|
||||
export const queryProductConfig = (id: string) => server.get(`/device/product/${id}/config-metadata`)
|
||||
|
||||
/**
|
||||
* 设备配置
|
||||
* @param id 设备id
|
||||
* @returns
|
||||
*/
|
||||
export const queryDeviceConfig = (id: string) => server.get(`/device-instance/${id}/config-metadata`)
|
||||
|
||||
/**
|
||||
* 查询协议
|
||||
* @param type
|
||||
* @param transport
|
||||
* @returns
|
||||
*/
|
||||
export const queryProtocolDetail = (type: string, transport: string) => server.get(`/protocol/${type}/transport/${transport}`)
|
||||
|
||||
/**
|
||||
* 网络组件启用
|
||||
* @param id 网络组件ID
|
||||
* @returns
|
||||
*/
|
||||
export const startNetwork = (id: string) => server.post(`/network/config/${id}/_start`)
|
||||
|
||||
/**
|
||||
* 启用网关
|
||||
* @param id 网关id
|
||||
* @returns
|
||||
*/
|
||||
export const startGateway = (id: string) => server.post(`/gateway/device/${id}/_startup`)
|
||||
|
||||
/**
|
||||
* 网关详情
|
||||
* @param id 网关id
|
||||
* @returns
|
||||
*/
|
||||
export const getGatewayDetail = (id: string) => server.get(`/gateway/device/${id}`)
|
||||
|
||||
|
||||
/*
|
||||
* 获取单位列表
|
||||
* @returns 单位列表
|
||||
*/
|
||||
export const getUnit = () => server.get<UnitType[]>(`/protocol/units`)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import server from '@/utils/request'
|
||||
import type { ProductType } from '@/views/media/Device/typings';
|
||||
|
||||
export default {
|
||||
// 列表
|
||||
|
@ -10,4 +11,16 @@ export default {
|
|||
// 修改
|
||||
update: (data: any) => server.put(`/media/device/${data.channel}/${data.id}`, data),
|
||||
del: (id: string) => server.remove(`/media/device/${id}`),
|
||||
// 更新通道
|
||||
updateChannels: (id: string) => server.post(`/media/device/${id}/channels/_sync`),
|
||||
// 查询产品列表
|
||||
queryProductList: (data: any) => server.post<ProductType[]>(`/device/product/_query/no-paging`, data),
|
||||
// 快速添加产品
|
||||
saveProduct: (data: any) => server.post<any>(`/device/product`, data),
|
||||
// 产品发布
|
||||
deployProductById: (id: string) => server.post<any>(`/device/product/${id}/deploy`),
|
||||
// 查询设备接入配置
|
||||
queryProvider: (data?: any) => server.post<any>(`/gateway/device/detail/_query`, data),
|
||||
// 查询网关配置
|
||||
getConfiguration: (id: string, transport: string) => server.get<any>(`/protocol/${id}/${transport}/configuration`),
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<a-select v-model:value="_value" mode="tags" :options="options" :size="size" @change="change"></a-select>
|
||||
</template>
|
||||
<script setup lang="ts" name="InputSelect">
|
||||
import { SizeType } from 'ant-design-vue/es/config-provider';
|
||||
import { DefaultOptionType, SelectValue } from 'ant-design-vue/es/select';
|
||||
import { PropType } from 'vue';
|
||||
type valueType = string | number
|
||||
type Emits = {
|
||||
(e: 'update:value', data: valueType | undefined): void;
|
||||
(e: 'change'): void;
|
||||
};
|
||||
const emit = defineEmits<Emits>();
|
||||
const props = defineProps({
|
||||
value: [String, Number],
|
||||
options: {
|
||||
type: Array as PropType<DefaultOptionType[]> | undefined,
|
||||
},
|
||||
size: String as PropType<SizeType>
|
||||
})
|
||||
const _value = ref<valueType[]>();
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
_value.value = val ? [val] : undefined
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const change = (value: SelectValue) => {
|
||||
const _val = (value as valueType[])
|
||||
if (_val.length > 1) {
|
||||
emit('update:value', _val.slice(_val.length - 1)?.[0])
|
||||
} else {
|
||||
emit('update:value', value?.[0])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
</style>
|
|
@ -10,7 +10,7 @@
|
|||
@change="handleChange"
|
||||
:action="FILE_UPLOAD"
|
||||
:headers="{
|
||||
'X-Access-Token': LocalStore.get(TOKEN_KEY)
|
||||
'X-Access-Token': LocalStore.get(TOKEN_KEY),
|
||||
}"
|
||||
v-bind="props"
|
||||
>
|
||||
|
@ -26,8 +26,23 @@
|
|||
<div class="upload-image-mask">点击修改</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<AIcon type="LoadingOutlined" v-if="loading" style="font-size: 20px" />
|
||||
<AIcon v-else type="PlusOutlined" style="font-size: 20px" />
|
||||
<AIcon
|
||||
type="LoadingOutlined"
|
||||
v-if="loading"
|
||||
style="font-size: 20px"
|
||||
/>
|
||||
<template v-else-if="bgImage">
|
||||
<div
|
||||
class="upload-image"
|
||||
:style="`background-image: url(${bgImage});`"
|
||||
></div>
|
||||
<div class="upload-image-mask">点击修改</div>
|
||||
</template>
|
||||
<AIcon
|
||||
v-else
|
||||
type="PlusOutlined"
|
||||
style="font-size: 20px"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</a-upload>
|
||||
|
@ -41,7 +56,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { message, UploadChangeParam, UploadProps } from 'ant-design-vue';
|
||||
import { FILE_UPLOAD } from '@/api/comm'
|
||||
import { FILE_UPLOAD } from '@/api/comm';
|
||||
import { TOKEN_KEY } from '@/utils/variable';
|
||||
import { LocalStore } from '@/utils/comm';
|
||||
import { CSSProperties } from 'vue';
|
||||
|
@ -56,6 +71,7 @@ interface JUploadProps extends UploadProps {
|
|||
errorMessage?: string;
|
||||
size?: number;
|
||||
style?: CSSProperties;
|
||||
bgImage?: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
@ -63,35 +79,42 @@ const emit = defineEmits<Emits>();
|
|||
const props: JUploadProps = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
bgImage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const loading = ref<boolean>(false)
|
||||
const imageUrl = ref<string>(props?.modelValue || '')
|
||||
const loading = ref<boolean>(false);
|
||||
const imageUrl = ref<string>(props?.modelValue || '');
|
||||
const imageTypes = props.types ? props.types : ['image/jpeg', 'image/png'];
|
||||
|
||||
watch(() => props.modelValue,
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
console.log(newValue)
|
||||
imageUrl.value = newValue
|
||||
}, {
|
||||
console.log(newValue);
|
||||
imageUrl.value = newValue;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
})
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
const handleChange = (info: UploadChangeParam) => {
|
||||
if (info.file.status === 'uploading') {
|
||||
loading.value = true;
|
||||
}
|
||||
if (info.file.status === 'done') {
|
||||
imageUrl.value = info.file.response?.result
|
||||
imageUrl.value = info.file.response?.result;
|
||||
loading.value = false;
|
||||
emit('update:modelValue', info.file.response?.result)
|
||||
emit('update:modelValue', info.file.response?.result);
|
||||
}
|
||||
if (info.file.status === 'error') {
|
||||
loading.value = false;
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<a-popover :visible="visible" placement="left">
|
||||
<template #title>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="width: 150px;">配置元素</div>
|
||||
<close-outlined @click="visible = false" />
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div style="max-width: 400px;">
|
||||
<a-form layout="vertical" :model="_value">
|
||||
<value-type-form v-model:value="_value" :name="[]" isSub key="sub"></value-type-form>
|
||||
<a-form-item label="说明" name="description" :rules="[
|
||||
{ max: 200, message: '最多可输入200个字符' },
|
||||
]">
|
||||
<a-textarea v-model:value="_value.description" size="small"></a-textarea>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
<a-button type="dashed" block @click="visible = true">
|
||||
配置元素<edit-outlined class="item-icon" />
|
||||
</a-button>
|
||||
</a-popover>
|
||||
|
||||
</template>
|
||||
<script setup lang="ts" name="ArrayParam">
|
||||
import ValueTypeForm from '@/views/device/components/Metadata/Base/Edit/ValueTypeForm.vue';
|
||||
import { EditOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
type ValueType = Record<any, any>;
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object as PropType<ValueType>,
|
||||
default: () => ({ extends: {} })
|
||||
},
|
||||
name: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
interface Emits {
|
||||
(e: 'update:value', data: ValueType): void;
|
||||
}
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const _value = computed({
|
||||
get: () => props.value,
|
||||
set: val => {
|
||||
emit('update:value', val)
|
||||
}
|
||||
})
|
||||
|
||||
const visible = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
emit('update:value', { extends: {}, ...props.value })
|
||||
})
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.item-icon {
|
||||
color: rgb(136, 136, 136);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,81 @@
|
|||
<template>
|
||||
<div class="boolean-param">
|
||||
<a-row :gutter="4">
|
||||
<a-col :span="12">
|
||||
<a-form-item label=" " :name="name.concat(['trueText'])" :rules="[
|
||||
{ required: true, message: '请输入trueText' },
|
||||
]">
|
||||
<a-input v-model:value="value.trueText" placeholder="trueText" size="small" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="-" :name="name.concat(['trueValue'])" :rules="[
|
||||
{ required: true, message: '请输入trueValue' },
|
||||
]">
|
||||
<a-input v-model:value="value.trueValue" placeholder="trueValue" size="small"/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label=" " :name="name.concat(['falseText'])" :rules="[
|
||||
{ required: true, message: '请输入falseText' },
|
||||
]">
|
||||
<a-input v-model:value="value.falseText" placeholder="falseText" size="small" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="-" :name="name.concat(['falseValue'])" :rules="[
|
||||
{ required: true, message: '请输入falseValue' },
|
||||
]">
|
||||
<a-input v-model:value="value.falseValue" placeholder="falseValue" size="small" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="BooleanParam">
|
||||
import { PropType } from 'vue';
|
||||
type ModelValueType = Record<string, string>
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', data: ModelValueType): void;
|
||||
}
|
||||
const emit = defineEmits<Emits>()
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object as PropType<ModelValueType>,
|
||||
default: () => ({
|
||||
})
|
||||
},
|
||||
name: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
emit('update:value',
|
||||
{
|
||||
trueText: '是',
|
||||
trueValue: 'true',
|
||||
falseText: '否',
|
||||
falseValue: 'false',
|
||||
...props.value
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.boolean-param {
|
||||
:deep(.ant-form-item) {
|
||||
flex-direction: row;
|
||||
|
||||
.ant-form-item-label {
|
||||
>label {
|
||||
margin: 0 10px 0 0;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,160 @@
|
|||
<template>
|
||||
<div class="enum-param">
|
||||
<div class="list-item" v-for="(item, index) in _value" :key="index">
|
||||
<div class="item-left">
|
||||
<menu-outlined class="item-drag item-icon" />
|
||||
</div>
|
||||
<div class="item-middle item-editable">
|
||||
<a-popover :visible="editIndex === index" placement="top">
|
||||
<template #title>
|
||||
<div class="edit-title" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="width: 150px;">枚举项配置</div>
|
||||
<close-outlined @click="handleClose" />
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<a-form :model="_value[index]" layout="vertical">
|
||||
<a-form-item label="Value" name="value" :rules="[
|
||||
{ required: true, message: '请输入Value' },
|
||||
]">
|
||||
<a-input v-model:value="_value[index].value" size="small"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Text" name="text" :rules="[
|
||||
{ required: true, message: '请输入Text' },
|
||||
]">
|
||||
<a-input v-model:value="_value[index].text" size="small"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
<div class="item-edit" @click="handleEdit(index)">
|
||||
{{ item.text || '枚举项配置' }}
|
||||
<edit-outlined class="item-icon" />
|
||||
</div>
|
||||
</a-popover>
|
||||
</div>
|
||||
<div class="item-right">
|
||||
<delete-outlined @click="handleDelete(index)"/>
|
||||
</div>
|
||||
</div>
|
||||
<a-button type="dashed" block @click="handleAdd">
|
||||
<template #icon><plus-outlined class="item-icon" /></template>
|
||||
新增枚举型
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="BooleanParam">
|
||||
import { PropType } from 'vue'
|
||||
import { MenuOutlined, EditOutlined, DeleteOutlined, PlusOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
type EnumType = {
|
||||
text?: string,
|
||||
value?: string,
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', data: EnumType[]): void;
|
||||
}
|
||||
const emit = defineEmits<Emits>()
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object as PropType<EnumType[]>,
|
||||
default: () => ([])
|
||||
}
|
||||
})
|
||||
|
||||
const _value = ref<EnumType[]>([])
|
||||
watchEffect(() => {
|
||||
_value.value = props.value
|
||||
})
|
||||
|
||||
watch(_value,
|
||||
() => {
|
||||
emit('update:value', _value.value)
|
||||
},
|
||||
{ deep: true })
|
||||
|
||||
const editIndex = ref<number>(-1)
|
||||
const handleEdit = (index: number) => {
|
||||
editIndex.value = index
|
||||
}
|
||||
const handleDelete = (index: number) => {
|
||||
editIndex.value = -1
|
||||
_value.value.splice(index, 1)
|
||||
}
|
||||
const handleClose = () => {
|
||||
editIndex.value = -1
|
||||
}
|
||||
const handleAdd = () => {
|
||||
_value.value.push({})
|
||||
emit('update:value', _value.value)
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.enum-param {
|
||||
.list-item {
|
||||
border: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
padding: 3px 6px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #fff;
|
||||
line-height: 26px;
|
||||
font-size: 14px;
|
||||
|
||||
// .item-left {
|
||||
// .item-drag {
|
||||
// cursor: move;
|
||||
// }
|
||||
// }
|
||||
.item-edit {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
color: rgb(136, 136, 136);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label) {
|
||||
line-height: 1;
|
||||
|
||||
>label {
|
||||
font-size: 12px;
|
||||
|
||||
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-explain) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-with-help) {
|
||||
.ant-form-item-explain {
|
||||
min-height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&.ant-form-item-with-help {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-input),
|
||||
:deep(.ant-select) {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,172 @@
|
|||
<template>
|
||||
<div class="json-param">
|
||||
<div class="list-item" v-for="(item, index) in _value" :key="`object_${index}`">
|
||||
<div class="item-left">
|
||||
<menu-outlined class="item-drag item-icon" />
|
||||
</div>
|
||||
<div class="item-middle item-editable">
|
||||
<a-popover :visible="editIndex === index" placement="left">
|
||||
<template #title>
|
||||
<div class="edit-title" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="width: 150px;">配置参数</div>
|
||||
<close-outlined @click="handleClose" />
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div style="max-width: 400px;">
|
||||
<a-form :model="_value[index]" layout="vertical">
|
||||
<a-form-item label="标识" name="id" :rules="[
|
||||
{ required: true, message: '请输入标识' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_]+$/,
|
||||
message: '请输入英文或者数字或者-或者_',
|
||||
},
|
||||
]">
|
||||
<a-input v-model:value="_value[index].id" size="small"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="名称" name="name" :rules="[
|
||||
{ required: true, message: '请输入名称' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
]">
|
||||
<a-input v-model:value="_value[index].name" size="small"></a-input>
|
||||
</a-form-item>
|
||||
<value-type-form v-model:value="_value[index].valueType" :name="['valueType']" isSub
|
||||
key="json_sub"></value-type-form>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
<div class="item-edit" @click="handleEdit(index)">
|
||||
{{ item.name || '配置参数' }}
|
||||
<edit-outlined class="item-icon" />
|
||||
</div>
|
||||
</a-popover>
|
||||
</div>
|
||||
<div class="item-right">
|
||||
<delete-outlined @click="handleDelete(index)" />
|
||||
</div>
|
||||
</div>
|
||||
<a-button type="dashed" block @click="handleAdd">
|
||||
<template #icon><plus-outlined class="item-icon" /></template>
|
||||
添加参数
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="JsonParam">
|
||||
import { PropType } from 'vue'
|
||||
import { MenuOutlined, EditOutlined, DeleteOutlined, PlusOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
||||
import ValueTypeForm from '@/views/device/components/Metadata/Base/Edit/ValueTypeForm.vue';
|
||||
|
||||
type JsonType = Record<any, any>;
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', data: JsonType[]): void;
|
||||
}
|
||||
const emit = defineEmits<Emits>()
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object as PropType<JsonType[]>,
|
||||
default: () => ([])
|
||||
}
|
||||
})
|
||||
|
||||
const _value = ref<JsonType[]>([])
|
||||
watchEffect(() => {
|
||||
_value.value = props.value
|
||||
})
|
||||
|
||||
watch(_value,
|
||||
() => {
|
||||
emit('update:value', _value.value)
|
||||
},
|
||||
{ deep: true })
|
||||
|
||||
const editIndex = ref<number>(-1)
|
||||
const handleEdit = (index: number) => {
|
||||
editIndex.value = index
|
||||
}
|
||||
const handleDelete = (index: number) => {
|
||||
editIndex.value = -1
|
||||
_value.value.slice(index, 1)
|
||||
}
|
||||
const handleClose = () => {
|
||||
editIndex.value = -1
|
||||
}
|
||||
const handleAdd = () => {
|
||||
_value.value.push({
|
||||
valueType: {
|
||||
expands: {}
|
||||
},
|
||||
})
|
||||
emit('update:value', _value.value)
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.json-param {
|
||||
.list-item {
|
||||
border: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
padding: 3px 6px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #fff;
|
||||
line-height: 26px;
|
||||
font-size: 14px;
|
||||
|
||||
// .item-left {
|
||||
// .item-drag {
|
||||
// cursor: move;
|
||||
// }
|
||||
// }
|
||||
.item-edit {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
color: rgb(136, 136, 136);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label) {
|
||||
line-height: 1;
|
||||
|
||||
>label {
|
||||
font-size: 12px;
|
||||
|
||||
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-explain) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-with-help) {
|
||||
.ant-form-item-explain {
|
||||
min-height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&.ant-form-item-with-help {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-input),
|
||||
:deep(.ant-select) {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
|
@ -1,10 +1,11 @@
|
|||
import { UnorderedListOutlined, AppstoreOutlined } from '@ant-design/icons-vue'
|
||||
import styles from './index.module.less'
|
||||
import { Pagination, Table, Empty, Spin, Alert } from 'ant-design-vue'
|
||||
import type { TableProps, ColumnProps } from 'ant-design-vue/es/table'
|
||||
import type { TableProps } from 'ant-design-vue/es/table'
|
||||
import type { TooltipProps } from 'ant-design-vue/es/tooltip'
|
||||
import type { PopconfirmProps } from 'ant-design-vue/es/popconfirm'
|
||||
import { CSSProperties, PropType } from 'vue';
|
||||
import type { JColumnsProps } from './types'
|
||||
|
||||
enum ModelEnum {
|
||||
TABLE = 'TABLE',
|
||||
|
@ -40,14 +41,10 @@ export interface ActionsType {
|
|||
children?: ActionsType[];
|
||||
}
|
||||
|
||||
export interface JColumnProps extends ColumnProps {
|
||||
scopedSlots?: boolean; // 是否为插槽 true: 是 false: 否
|
||||
}
|
||||
|
||||
export interface JTableProps extends TableProps {
|
||||
request?: (params?: Record<string, any>) => Promise<Partial<RequestData>>;
|
||||
cardBodyClass?: string;
|
||||
columns: JColumnProps[];
|
||||
columns: JColumnsProps[];
|
||||
params?: Record<string, any>;
|
||||
model?: keyof typeof ModelEnum | undefined; // 显示table还是card
|
||||
// actions?: ActionsType[];
|
||||
|
@ -156,9 +153,10 @@ const JTable = defineComponent<JTableProps>({
|
|||
const pageIndex = ref<number>(0)
|
||||
const pageSize = ref<number>(6)
|
||||
const total = ref<number>(0)
|
||||
const _columns = ref<JColumnProps[]>(props?.columns || [])
|
||||
const loading = ref<boolean>(true)
|
||||
|
||||
const _columns = computed(() => props.columns.filter(i => !(i?.hideInTable)))
|
||||
|
||||
/**
|
||||
* 监听宽度,计算显示卡片个数
|
||||
*/
|
||||
|
|
|
@ -3,5 +3,6 @@ import { ColumnType } from 'ant-design-vue/es/table'
|
|||
|
||||
export interface JColumnsProps extends ColumnType{
|
||||
scopedSlots?: boolean;
|
||||
search: SearchProps
|
||||
search: SearchProps;
|
||||
hideInTable?: boolean;
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
<template>
|
||||
<div class="dialog-item" :key="data.key" :class="{'dialog-active' : !data?.upstream}">
|
||||
<div class="dialog-card">
|
||||
<div class="dialog-list" v-for="item in data.list" :key="item.key">
|
||||
<div class="dialog-icon">
|
||||
<AIcon :type="visible.includes(item.key) ? 'DownOutlined' : 'RightOutlined'" />
|
||||
</div>
|
||||
<div class="dialog-box">
|
||||
<div class="dialog-header">
|
||||
<div class="dialog-title">
|
||||
<a-badge :color="statusColor.get(item.error ? 'error' : 'success')" style="margin-right: 5px" />
|
||||
{{operationMap.get(item.operation) || item?.operation}}
|
||||
</div>
|
||||
<div class="dialog-item">{{moment(item.endTime).format('YYYY-MM-DD HH:mm:ss')}}</div>
|
||||
</div>
|
||||
<div class="dialog-editor" v-if="visible.includes(item.key)">
|
||||
<a-textarea :bordered="false" :value="item?.detail" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const operationMap = new Map();
|
||||
import moment from 'moment'
|
||||
operationMap.set('connection', '连接');
|
||||
operationMap.set('auth', '权限验证');
|
||||
operationMap.set('decode', '解码');
|
||||
operationMap.set('encode', '编码');
|
||||
operationMap.set('request', '请求');
|
||||
operationMap.set('response', '响应');
|
||||
operationMap.set('downstream', '下行消息');
|
||||
operationMap.set('upstream', '上行消息');
|
||||
|
||||
const statusColor = new Map();
|
||||
statusColor.set('error', '#E50012');
|
||||
statusColor.set('success', '#24B276');
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
const visible = ref<string[]>([])
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import 'ant-design-vue/es/style/themes/default.less';
|
||||
|
||||
:root {
|
||||
--dialog-primary-color: @primary-color;
|
||||
}
|
||||
|
||||
.dialog-item {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
padding-bottom: 12px;
|
||||
|
||||
.dialog-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 60%;
|
||||
padding: 24px;
|
||||
background-color: #fff;
|
||||
|
||||
.dialog-list {
|
||||
display: flex;
|
||||
|
||||
.dialog-icon {
|
||||
margin-right: 10px;
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.dialog-header {
|
||||
.dialog-title {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dialog-time {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-editor {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
|
||||
textarea::-webkit-scrollbar {
|
||||
width: 5px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-active {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
.dialog-card {
|
||||
background-color: @primary-color;
|
||||
|
||||
.dialog-list {
|
||||
.dialog-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
.dialog-header {
|
||||
.dialog-title,
|
||||
.dialog-time {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-editor {
|
||||
textarea {
|
||||
color: #fff !important;
|
||||
background-color: @primary-color !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<a-table
|
||||
rowKey="id"
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
bordered
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, text, record }">
|
||||
<div style="width: 280px">
|
||||
<template v-if="['valueType', 'name'].includes(column.dataIndex)">
|
||||
<span>{{ text }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ValueItem
|
||||
v-model:modelValue="record.value"
|
||||
:itemType="record.type"
|
||||
:options="
|
||||
record.type === 'enum'
|
||||
? (record?.dataType?.elements || []).map(
|
||||
(item) => {
|
||||
return {
|
||||
label: item.text,
|
||||
value: item.value,
|
||||
};
|
||||
},
|
||||
)
|
||||
: record.type === 'boolean'
|
||||
? [
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
]
|
||||
: undefined
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from "vue-demi";
|
||||
|
||||
|
||||
type Emits = {
|
||||
(e: 'update:modelValue', data: Record<string, any>[]): void;
|
||||
};
|
||||
const _emit = defineEmits<Emits>();
|
||||
|
||||
const _props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<Record<string, any>[]>,
|
||||
default: '',
|
||||
}
|
||||
});
|
||||
const columns = [
|
||||
{
|
||||
title: '参数名称',
|
||||
dataIndex: 'name',
|
||||
with: '33%',
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'valueType',
|
||||
with: '33%',
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
dataIndex: 'value',
|
||||
with: '34%',
|
||||
},
|
||||
];
|
||||
|
||||
// const dataSource = ref<Record<any, any>[]>(_props.modelValue || []);
|
||||
|
||||
const dataSource = computed({
|
||||
get: () => {
|
||||
return _props.modelValue || {
|
||||
messageType: undefined,
|
||||
message: {
|
||||
properties: undefined,
|
||||
functionId: undefined,
|
||||
inputs: []
|
||||
}
|
||||
}
|
||||
},
|
||||
set: (val: any) => {
|
||||
_emit('update:modelValue', val);
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
|
@ -0,0 +1,127 @@
|
|||
<template>
|
||||
<div class="function">
|
||||
<a-form
|
||||
:layout="'vertical'"
|
||||
ref="formRef"
|
||||
:model="modelRef"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="6">
|
||||
<a-form-item name="messageType" :rules="{
|
||||
required: true,
|
||||
message: '请选择',
|
||||
}">
|
||||
<a-select placeholder="请选择" v-model:value="modelRef.messageType" show-search :filter-option="filterOption">
|
||||
<a-select-option value="READ_PROPERTY">读取属性</a-select-option>
|
||||
<a-select-option value="WRITE_PROPERTY">修改属性</a-select-option>
|
||||
<a-select-option value="INVOKE_FUNCTION">调用功能</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6" v-if="['READ_PROPERTY','WRITE_PROPERTY'].includes(modelRef.messageType)">
|
||||
<a-form-item :name="['message', 'properties']" :rules="{
|
||||
required: true,
|
||||
message: '请选择属性',
|
||||
}">
|
||||
<a-select placeholder="请选择属性" v-model:value="modelRef.message.properties" show-search :filter-option="filterOption">
|
||||
<a-select-option v-for="i in (metadata?.properties) || []" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6" v-if="modelRef.messageType === 'WRITE_PROPERTY'">
|
||||
<a-form-item :name="['message', 'value']" :rules="{
|
||||
required: true,
|
||||
message: '请输入值',
|
||||
}">
|
||||
<a-input />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6" v-if="modelRef.messageType === 'INVOKE_FUNCTION'">
|
||||
<a-form-item :name="['message', 'functionId']" :rules="{
|
||||
required: true,
|
||||
message: '请选择功能',
|
||||
}">
|
||||
<a-select placeholder="请选择功能" v-model:value="modelRef.message.functionId" show-search :filter-option="filterOption" @change="funcChange">
|
||||
<a-select-option v-for="i in (metadata?.functions) || []" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-button type="primary" @click="saveBtn">发送</a-button>
|
||||
</a-col>
|
||||
<a-col :span="24" v-if="modelRef.messageType === 'INVOKE_FUNCTION' && modelRef.message.functionId">
|
||||
<a-form-item :name="['message', 'inputs']" label="参数列表" :rules="{
|
||||
required: true,
|
||||
message: '请输入参数列表',
|
||||
}">
|
||||
<EditTable v-model="modelRef.message.inputs"/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
import EditTable from './EditTable.vue'
|
||||
|
||||
const instanceStore = useInstanceStore()
|
||||
|
||||
const formRef = ref();
|
||||
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
type Emits = {
|
||||
(e: 'update:modelValue', data: any): void;
|
||||
};
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const modelRef = reactive({
|
||||
messageType: undefined,
|
||||
message: {
|
||||
properties: undefined,
|
||||
functionId: undefined,
|
||||
inputs: []
|
||||
}
|
||||
})
|
||||
|
||||
const metadata = computed(() => {
|
||||
return JSON.parse(instanceStore.current?.metadata || '{}')
|
||||
})
|
||||
|
||||
const funcChange = (val: string) => {
|
||||
if(val){
|
||||
const arr = metadata.value?.functions.find((item: any) => item.id === val)?.inputs || []
|
||||
const list = arr.map((item: any) => {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
value: undefined,
|
||||
valueType: item?.valueType?.type,
|
||||
}
|
||||
})
|
||||
modelRef.message.inputs = list
|
||||
}
|
||||
}
|
||||
|
||||
const saveBtn = () => {
|
||||
formRef.value.validate()
|
||||
.then(() => {
|
||||
console.log(toRaw(modelRef))
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ saveBtn })
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.function {
|
||||
padding: 15px;
|
||||
background-color: #e7eaec;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
log
|
||||
</template>
|
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="16">
|
||||
<a-row :gutter="24" style="margin-bottom: 20px;">
|
||||
<a-col :span="12" v-for="item in messageArr" :key="item">
|
||||
<div :style="messageStyleMap.get(item.status)" class="message-status">
|
||||
<a-badge :status="messageStatusMap.get(item.status)" style="margin-right: 5px;" />
|
||||
<span>{{item.text}}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<div>
|
||||
<TitleComponent data="调试" />
|
||||
<div class="content">
|
||||
<div class="dialog" id="dialog">
|
||||
<template v-for="item in dialogList" :key="item.key">
|
||||
<Dialog :data="item" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div><Function /></div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="right-log">
|
||||
<TitleComponent data="日志" />
|
||||
<div :style="{ marginTop: 10 }">
|
||||
<template v-if="logList.length">
|
||||
<Log v-for="item in logList" :data="item" :key="item.key" />
|
||||
</template>
|
||||
<a-empty v-else />
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MessageType } from './util'
|
||||
import { messageStatusMap, messageStyleMap } from './util'
|
||||
import Dialog from './Dialog/index.vue'
|
||||
import Function from './Function/index.vue'
|
||||
import Log from './Log/index.vue'
|
||||
|
||||
const message = reactive<MessageType>({
|
||||
up: {
|
||||
text: '上行消息诊断中',
|
||||
status: 'loading',
|
||||
},
|
||||
down: {
|
||||
text: '下行消息诊断中',
|
||||
status: 'loading',
|
||||
},
|
||||
})
|
||||
|
||||
const dialogList = ref<Record<string, any>>([])
|
||||
const logList = ref<Record<string, any>>([])
|
||||
|
||||
const messageArr = computed(() => {
|
||||
const arr = Object.keys(message) || []
|
||||
return arr.map(i => { return {...message[i], key: i}})
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.message-status {
|
||||
padding: 8px 24px;
|
||||
}
|
||||
.content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
max-height: 500px;
|
||||
padding: 24px;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: #f2f5f7;
|
||||
}
|
||||
.right-log {
|
||||
padding-left: 20px;
|
||||
border-left: 1px solid rgba(0, 0, 0, .09);
|
||||
overflow: hidden;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
min-height: 400px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,29 @@
|
|||
export type MessageType = {
|
||||
up: {
|
||||
text: string;
|
||||
status: 'loading' | 'success' | 'error';
|
||||
};
|
||||
down: {
|
||||
text: string;
|
||||
status: 'loading' | 'success' | 'error';
|
||||
};
|
||||
}
|
||||
|
||||
export const messageStyleMap = new Map();
|
||||
messageStyleMap.set('loading', {
|
||||
background: 'linear-gradient(0deg, rgba(30, 165, 241, 0.03), rgba(30, 165, 241, 0.03)), #FFFFFF',
|
||||
boxShadow: '-2px 0px 0px #1EA5F1',
|
||||
});
|
||||
messageStyleMap.set('error', {
|
||||
background: 'linear-gradient(0deg, rgba(255, 77, 79, 0.03), rgba(255, 77, 79, 0.03)), #FFFFFF',
|
||||
boxShadow: '-2px 0px 0px #FF4D4F',
|
||||
});
|
||||
messageStyleMap.set('success', {
|
||||
background: 'linear-gradient(0deg, rgba(50, 212, 164, 0.03), rgba(50, 212, 164, 0.03)), #FFFFFF',
|
||||
boxShadow: '-2px 0px 0px #32D4A4',
|
||||
});
|
||||
|
||||
export const messageStatusMap = new Map();
|
||||
messageStatusMap.set('loading', 'processing');
|
||||
messageStatusMap.set('error', 'error');
|
||||
messageStatusMap.set('success', 'success');
|
|
@ -0,0 +1,101 @@
|
|||
import { Badge, Descriptions, Modal, Tooltip } from "ant-design-vue"
|
||||
import TitleComponent from '@/components/TitleComponent/index.vue'
|
||||
import styles from './index.module.less'
|
||||
import AIcon from "@/components/AIcon";
|
||||
import _ from "lodash";
|
||||
|
||||
const DiagnosticAdvice = defineComponent({
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => { }
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
setup(props, { emit }) {
|
||||
const { data } = props
|
||||
return () => <Modal visible title="设备诊断" width={1000} onOk={() => {
|
||||
emit('close')
|
||||
}}
|
||||
onCancel={() => {
|
||||
emit('close')
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<TitleComponent data="诊断建议" />
|
||||
<div class={styles.advice}>
|
||||
<div class={styles.alert}>
|
||||
<span style={{ marginRight: 10 }}><AIcon type="InfoCircleOutlined" /></span>
|
||||
所有诊断均无异常但设备仍未上线,请检查以下内容
|
||||
</div>
|
||||
<div style={{ marginLeft: 10 }}>
|
||||
{
|
||||
(data?.list || []).map((item: any, index: number) => (
|
||||
<div class={styles.infoItem} key={index} style={{ margin: '10px 0' }}>
|
||||
{item}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 15 }}>
|
||||
<TitleComponent data="连接信息" />
|
||||
<Descriptions column={2}>
|
||||
<Descriptions.Item span={1} label="设备ID">
|
||||
{data?.info?.id || ''}
|
||||
</Descriptions.Item>
|
||||
{data?.info?.address?.length > 0 && (
|
||||
<Descriptions.Item span={1} label="连接地址">
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={
|
||||
<div class="serverItem">
|
||||
{(data?.info?.address || []).map((i: any) => (
|
||||
<div key={i.address}>
|
||||
<Badge color={i.health === -1 ? 'red' : 'green'} />
|
||||
{i.address}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="serverItem">
|
||||
{(data?.info?.address || []).slice(0, 1).map((i: any) => (
|
||||
<div key={i.address}>
|
||||
<Badge color={i.health === -1 ? 'red' : 'green'} />
|
||||
{i.address}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
|
||||
{(_.flatten(_.map(data?.info?.config, 'properties')) || []).map((item: any, index: number) => (
|
||||
<Descriptions.Item
|
||||
key={index}
|
||||
span={1}
|
||||
label={
|
||||
item?.description ? (
|
||||
<div>
|
||||
<span style={{ marginRight: '10px' }}>{item.name}</span>
|
||||
<Tooltip title={item.description}>
|
||||
<AIcon type="QuestionCircleOutlined" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
item.name
|
||||
)
|
||||
}
|
||||
>
|
||||
{data?.info?.configValue[item?.property] || ''}
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
})
|
||||
|
||||
export default DiagnosticAdvice
|
|
@ -0,0 +1,217 @@
|
|||
import AIcon from "@/components/AIcon";
|
||||
import { Button, Descriptions, Modal } from "ant-design-vue"
|
||||
import styles from './index.module.less'
|
||||
|
||||
const ManualInspection = defineComponent({
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => { }
|
||||
}
|
||||
},
|
||||
emits: ['close', 'save'],
|
||||
setup(props, { emit }) {
|
||||
|
||||
const { data } = props
|
||||
|
||||
const dataRender = () => {
|
||||
if (data.type === 'device' || data.type === 'product') {
|
||||
return (
|
||||
<>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div class={styles.alert}>
|
||||
<span style={{ marginRight: 10 }}><AIcon type="InfoCircleOutlined" /></span>
|
||||
请检查配置项是否填写正确,若您确定该项无需诊断可
|
||||
<Button type="link" style="padding: 0"
|
||||
onClick={() => {
|
||||
emit('save', data)
|
||||
}}
|
||||
>
|
||||
忽略
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Descriptions title={data?.data?.name} layout="vertical" bordered>
|
||||
{(data?.data?.properties || []).map((item: any) => (
|
||||
<Descriptions.Item
|
||||
key={item.property}
|
||||
label={`${item.name}${item?.description ? `(${item.description})` : ''}`}
|
||||
>
|
||||
{data?.configuration[item.property] || ''}
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
</div>
|
||||
</div>
|
||||
{data?.data?.description ? (
|
||||
<div
|
||||
style={{ width: '50%', border: '1px solid #f0f0f0', padding: 10, borderLeft: 'none' }}
|
||||
>
|
||||
<h4>诊断项说明</h4>
|
||||
<p>{data?.data?.description}</p>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else if (data.type === 'cloud') {
|
||||
return (
|
||||
<>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div class={styles.alert}>
|
||||
<span style={{ marginRight: 10 }}><AIcon type="InfoCircleOutlined" /></span>
|
||||
请检查配置项是否填写正确,若您确定该项无需诊断可
|
||||
<Button type="link" style="padding: 0"
|
||||
onClick={() => {
|
||||
emit('save', data)
|
||||
}}
|
||||
>
|
||||
忽略
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Descriptions title={data?.data?.name} layout="vertical" bordered>
|
||||
{data.configuration?.provider === 'OneNet' ? (
|
||||
<>
|
||||
<Descriptions.Item label={'接口地址'}>
|
||||
{data?.configuration?.configuration?.apiAddress || ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'apiKey'}>
|
||||
{data?.configuration?.configuration?.apiKey || ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'通知Token'}>
|
||||
{data?.configuration?.configuration?.validateToken || ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'aesKey'}>
|
||||
{data?.configuration?.configuration?.aesKey || ''}
|
||||
</Descriptions.Item>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Descriptions.Item label={'接口地址'}>
|
||||
{data?.configuration?.configuration?.apiAddress || ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'appKey'}>
|
||||
{data?.configuration?.configuration?.appKey || ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'appSecret'}>
|
||||
{data?.configuration?.configuration?.appSecret || ''}
|
||||
</Descriptions.Item>
|
||||
</>
|
||||
)}
|
||||
</Descriptions>
|
||||
</div>
|
||||
</div>
|
||||
{data?.configuration?.configuration?.description ? (
|
||||
<div
|
||||
style={{ width: '50%', border: '1px solid #f0f0f0', padding: 10, borderLeft: 'none' }}
|
||||
>
|
||||
<h4>诊断项说明</h4>
|
||||
<p>{data?.configuration?.configuration?.description}</p>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else if (data.type === 'media') {
|
||||
return (
|
||||
<>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div class={styles.alert}>
|
||||
<span style={{ marginRight: 10 }}><AIcon type="InfoCircleOutlined" /></span>
|
||||
请检查配置项是否填写正确,若您确定该项无需诊断可
|
||||
<Button type="link" style="padding: 0"
|
||||
onClick={() => {
|
||||
emit('save', data)
|
||||
}}
|
||||
>
|
||||
忽略
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Descriptions title={data?.data?.name} layout="vertical" bordered>
|
||||
{data?.configuration?.configuration?.shareCluster ? (
|
||||
<>
|
||||
<Descriptions.Item label={'SIP 域'}>
|
||||
{data?.configuration?.configuration?.domain || ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'SIP ID'}>
|
||||
{data?.configuration?.configuration?.sipId || ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'集群'}>
|
||||
{data?.configuration?.configuration?.shareCluster ? '共享配置' : '独立配置'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'SIP 地址'}>
|
||||
{`${data?.configuration?.configuration?.hostPort?.host}:${data?.configuration?.configuration?.hostPort?.port}`}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'公网 Host'}>
|
||||
{`${data?.configuration?.configuration?.hostPort?.publicHost}:${data?.configuration?.configuration?.hostPort?.publicPort}`}
|
||||
</Descriptions.Item>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Descriptions.Item label={'SIP 域'}>
|
||||
{data?.configuration?.configuration?.domain || ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'SIP ID'}>
|
||||
{data?.configuration?.configuration?.sipId || ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'集群'}>
|
||||
{data?.configuration?.configuration?.shareCluster ? '共享配置' : '独立配置'}
|
||||
</Descriptions.Item>
|
||||
{data?.configuration?.configuration?.cluster.map((i: any, it: number) => (
|
||||
<div key={it}>
|
||||
<div>节点{it + 1}</div>
|
||||
<Descriptions.Item label={'节点名称'}>
|
||||
{i?.clusterNodeId || ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'SIP 地址'}>
|
||||
{`${i.host}:${i?.port}`}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'公网 Host'}>
|
||||
{`${i?.publicHost}:${i?.publicPort}`}
|
||||
</Descriptions.Item>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Descriptions>
|
||||
</div>
|
||||
</div>
|
||||
{data?.configuration?.configuration.description ? (
|
||||
<div
|
||||
style={{ width: '50%', border: '1px solid #f0f0f0', padding: 10, borderLeft: 'none' }}
|
||||
>
|
||||
<h4>诊断项说明</h4>
|
||||
<p>{data?.configuration?.description}</p>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return () => <Modal
|
||||
title="人工检查"
|
||||
visible
|
||||
width={1000}
|
||||
cancelText="去修改"
|
||||
okText="确认无误"
|
||||
onOk={() => {
|
||||
emit('save', data)
|
||||
}}
|
||||
onCancel={() => {
|
||||
// TODO 跳转设备和产品
|
||||
}}>
|
||||
<div style={{ display: 'flex' }}>{dataRender()}</div>
|
||||
</Modal>
|
||||
}
|
||||
})
|
||||
|
||||
export default ManualInspection
|
|
@ -0,0 +1,90 @@
|
|||
.statusBox {
|
||||
width: 100%;
|
||||
|
||||
.statusHeader {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.statusContent {
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
border: 1px solid #ececec;
|
||||
border-bottom: none;
|
||||
|
||||
.statusItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #ececec;
|
||||
|
||||
.statusLeft {
|
||||
display: flex;
|
||||
|
||||
.statusImg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 15px 20px 0 0;
|
||||
}
|
||||
|
||||
.statusContext {
|
||||
.statusTitle {
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.statusDesc {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-top: 10px;
|
||||
color: #646464;
|
||||
font-size: 14px;
|
||||
|
||||
.infoItem {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.statusRight {
|
||||
margin-top: 10px;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
animation: loading 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
height: 40px;
|
||||
padding-left: 10px;
|
||||
color: rgba(0, 0, 0, 0.55);
|
||||
line-height: 40px;
|
||||
background-color: #f6f6f6;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,262 @@
|
|||
import { getImage } from '@/utils/comm';
|
||||
import { VNode } from 'vue';
|
||||
|
||||
export type ListProps = {
|
||||
key: string;
|
||||
name: string;
|
||||
desc?: string;
|
||||
status: 'loading' | 'error' | 'success' | 'warning';
|
||||
text?: string;
|
||||
info?: VNode | null;
|
||||
};
|
||||
|
||||
export const TextColorMap = new Map();
|
||||
TextColorMap.set('loading', 'black');
|
||||
TextColorMap.set('error', 'red');
|
||||
TextColorMap.set('success', 'green');
|
||||
TextColorMap.set('warning', '#FAB247');
|
||||
|
||||
export const StatusMap = new Map();
|
||||
StatusMap.set('error', getImage('/diagnose/status/error.png'));
|
||||
StatusMap.set('success', getImage('/diagnose/status/success.png'));
|
||||
StatusMap.set('warning', getImage('/diagnose/status/warning.png'));
|
||||
StatusMap.set('loading', getImage('/diagnose/status/loading.png'));
|
||||
|
||||
export const networkInitList: ListProps[] = [
|
||||
// {
|
||||
// key: 'access',
|
||||
// name: '设备接入配置',
|
||||
// desc: '诊断该设备所属产品是否已配置“设备接入”方式,未配置将导致设备连接失败。',
|
||||
// status: 'loading',
|
||||
// text: '正在诊断中...',
|
||||
// info: null,
|
||||
// },
|
||||
{
|
||||
key: 'network',
|
||||
name: '网络组件',
|
||||
desc: '诊断网络组件配置是否正确,配置错误将导致设备连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
{
|
||||
key: 'gateway',
|
||||
name: '设备接入网关',
|
||||
desc: '诊断设备接入网关状态是否正常,禁用状态将导致连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
{
|
||||
key: 'product',
|
||||
name: '产品状态',
|
||||
desc: '诊断产品状态是否正常,禁用状态将导致设备连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
{
|
||||
key: 'device',
|
||||
name: '设备状态',
|
||||
desc: '诊断设备状态是否正常,禁用状态将导致设备连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const childInitList: ListProps[] = [
|
||||
// {
|
||||
// key: 'access',
|
||||
// name: '设备接入配置',
|
||||
// desc: '诊断该设备所属产品是否已配置“设备接入”方式,未配置将导致设备连接失败。',
|
||||
// status: 'loading',
|
||||
// text: '正在诊断中...',
|
||||
// info: null,
|
||||
// },
|
||||
// {
|
||||
// key: 'network',
|
||||
// name: '网络组件',
|
||||
// desc: '诊断网络组件配置是否正确,配置错误将导致设备连接失败',
|
||||
// status: 'loading',
|
||||
// text: '正在诊断中...',
|
||||
// info: null,
|
||||
// },
|
||||
{
|
||||
key: 'gateway',
|
||||
name: '设备接入网关',
|
||||
desc: '诊断设备接入网关状态是否正常,网关配置是否正确',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
{
|
||||
key: 'parent-device',
|
||||
name: '网关父设备',
|
||||
desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
{
|
||||
key: 'product',
|
||||
name: '产品状态',
|
||||
desc: '诊断产品状态是否正常,禁用状态将导致设备连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
{
|
||||
key: 'device',
|
||||
name: '设备状态',
|
||||
desc: '诊断设备状态是否正常,禁用状态将导致设备连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const cloudInitList: ListProps[] = [
|
||||
// {
|
||||
// key: 'access',
|
||||
// name: '设备接入配置',
|
||||
// desc: '诊断该设备所属产品是否已配置“设备接入”方式,未配置将导致设备连接失败。',
|
||||
// status: 'loading',
|
||||
// text: '正在诊断中...',
|
||||
// info: null,
|
||||
// },
|
||||
{
|
||||
key: 'gateway',
|
||||
name: '设备接入网关',
|
||||
desc: '诊断设备接入网关状态是否正常,网关配置是否正确',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
{
|
||||
key: 'product',
|
||||
name: '产品状态',
|
||||
desc: '诊断产品状态是否正常,禁用状态将导致设备连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
{
|
||||
key: 'device',
|
||||
name: '设备状态',
|
||||
desc: '诊断设备状态是否正常,禁用状态将导致设备连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const channelInitList: ListProps[] = [
|
||||
// {
|
||||
// key: 'access',
|
||||
// name: '设备接入配置',
|
||||
// desc: '诊断该设备所属产品是否已配置“设备接入”方式,未配置将导致设备连接失败。',
|
||||
// status: 'loading',
|
||||
// text: '正在诊断中...',
|
||||
// info: null,
|
||||
// },
|
||||
{
|
||||
key: 'gateway',
|
||||
name: '设备接入网关',
|
||||
desc: '诊断设备接入网关状态是否正常,禁用状态将导致连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
{
|
||||
key: 'product',
|
||||
name: '产品状态',
|
||||
desc: '诊断产品状态是否正常,禁用状态将导致设备连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
{
|
||||
key: 'device',
|
||||
name: '设备状态',
|
||||
desc: '诊断设备状态是否正常,禁用状态将导致设备连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const mediaInitList: ListProps[] = [
|
||||
// {
|
||||
// key: 'access',
|
||||
// name: '设备接入配置',
|
||||
// desc: '诊断该设备所属产品是否已配置“设备接入”方式,未配置将导致设备连接失败。',
|
||||
// status: 'loading',
|
||||
// text: '正在诊断中...',
|
||||
// info: null,
|
||||
// },
|
||||
{
|
||||
key: 'gateway',
|
||||
name: '设备接入网关',
|
||||
desc: '诊断设备接入网关状态是否正常,禁用状态将导致连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
{
|
||||
key: 'product',
|
||||
name: '产品状态',
|
||||
desc: '诊断产品状态是否正常,禁用状态将导致设备连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
{
|
||||
key: 'device',
|
||||
name: '设备状态',
|
||||
desc: '诊断设备状态是否正常,禁用状态将导致设备连接失败',
|
||||
status: 'loading',
|
||||
text: '正在诊断中...',
|
||||
info: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const modifyArrayList = (oldList: ListProps[], item: ListProps, index?: number) => {
|
||||
let newList: ListProps[] = [];
|
||||
if (index !== 0 && !index) {
|
||||
// 添加
|
||||
for (let i = 0; i < oldList.length; i++) {
|
||||
const dt = oldList[i];
|
||||
if (item.key === dt.key) {
|
||||
newList.push(item);
|
||||
} else {
|
||||
newList.push(dt);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 修改
|
||||
oldList.splice(index, 0, item);
|
||||
newList = [...oldList];
|
||||
}
|
||||
return newList;
|
||||
};
|
||||
|
||||
export const isExit = (arr1: any[], arr2: any[]) => {
|
||||
return arr1.find((item) => arr2.includes(item));
|
||||
};
|
||||
|
||||
export const gatewayList = [
|
||||
'websocket-server',
|
||||
'http-server-gateway',
|
||||
'udp-device-gateway',
|
||||
'coap-server-gateway',
|
||||
'mqtt-client-gateway',
|
||||
'tcp-server-gateway',
|
||||
];
|
||||
|
||||
export const urlMap = new Map();
|
||||
urlMap.set('mqtt-client-gateway', 'topic');
|
||||
urlMap.set('http-server-gateway', 'url');
|
||||
urlMap.set('websocket-server', 'url');
|
||||
urlMap.set('coap-server-gateway', 'url');
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
<template>
|
||||
<a-card>
|
||||
<div class="diagnose">
|
||||
<div class="diagnose-header" :style="{background: headerColorMap.get(topState)}">
|
||||
<div class="diagnose-top">
|
||||
<div class="diagnose-img">
|
||||
<div v-if="topState === 'loading'" style="width: 100%; height: 100%; position: relative">
|
||||
<img :src="headerImgMap.get(topState)" style="height: 100%; position: absolute; z-index: 2" />
|
||||
<img :src="getImage('/diagnose/loading-1.png')" style="height: 100%" />
|
||||
</div>
|
||||
<img v-else :src="headerImgMap.get(topState)" style="height: 100%" />
|
||||
</div>
|
||||
<div class="diagnose-text">
|
||||
<div class="diagnose-title">{{headerTitleMap.get(topState)}}</div>
|
||||
<div class="diagnose-desc">
|
||||
<template v-if="topState !== 'loading'">{{headerDescMap.get(topState)}}</template>
|
||||
<template v-else>已诊断{{count}}个</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="diagnose-progress">
|
||||
<a-progress
|
||||
:percent="percent"
|
||||
:showInfo="false"
|
||||
size="small"
|
||||
:strokeColor="progressMap.get(topState)"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="diagnose-radio">
|
||||
<div class="diagnose-radio-item" :class="item.key === 'message' && topState !== 'success' ? 'disabled' : ''" v-for="item in tabList" :key="item.key" :style="activeKey === item.key ? {...activeStyle} : {}" @click="onTabChange(item.key)">
|
||||
{{item.text}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Message v-if="activeKey === 'message'" />
|
||||
<Status v-else :providerType="providerType" @countChange="countChange" @percentChange="percentChange" @stateChange="stateChange" />
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { headerImgMap, headerColorMap, headerTitleMap, headerDescMap, progressMap } from './util'
|
||||
import { getImage } from '@/utils/comm';
|
||||
import Status from './Status/index'
|
||||
import Message from './Message/index.vue'
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
|
||||
type TypeProps = 'network' | 'child-device' | 'media' | 'cloud' | 'channel'
|
||||
|
||||
const instanceStore = useInstanceStore()
|
||||
|
||||
const tabList = [
|
||||
{ key: 'status', text: '连接状态' },
|
||||
{ key: 'message', text: '消息通信' },
|
||||
];
|
||||
|
||||
const activeStyle = {
|
||||
background: '#FFFFFF',
|
||||
border: '1px solid rgba(0, 0, 0, 0.09)',
|
||||
borderRadius: '2px 2px 0px 0px',
|
||||
color: '#000000BF',
|
||||
};
|
||||
|
||||
const topState = ref<'loading' | 'success' | 'error'>('loading')
|
||||
const count = ref<number>(0)
|
||||
const percent = ref<number>(0)
|
||||
const activeKey = ref<'status' | 'message'>('status')
|
||||
const providerType = ref()
|
||||
|
||||
|
||||
const onTabChange = (key: 'status' | 'message') => {
|
||||
if(topState.value === 'success'){
|
||||
activeKey.value = key
|
||||
}
|
||||
}
|
||||
|
||||
const percentChange = (num: number) => {
|
||||
if(num === 0){
|
||||
percent.value = 0
|
||||
} else if( percent.value < 100 && !num) {
|
||||
percent.value += 20
|
||||
} else {
|
||||
percent.value = num
|
||||
}
|
||||
}
|
||||
|
||||
const stateChange = (_type: 'loading' | 'success' | 'error') => {
|
||||
topState.value = _type
|
||||
}
|
||||
|
||||
const countChange = (num: number) => {
|
||||
count.value = num
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const provider = instanceStore.current?.accessProvider;
|
||||
if (provider === 'fixed-media' || provider === 'gb28181-2016') {
|
||||
providerType.value = 'media'
|
||||
} else if (provider === 'OneNet' || provider === 'Ctwing') {
|
||||
providerType.value = 'cloud'
|
||||
} else if (provider === 'modbus-tcp' || provider === 'opc-ua') {
|
||||
providerType.value = 'channel'
|
||||
} else if (provider === 'child-device') {
|
||||
providerType.value = 'child-device'
|
||||
} else {
|
||||
providerType.value = 'network'
|
||||
}
|
||||
topState.value = 'loading';
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.diagnose {
|
||||
.diagnose-header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px 25px;
|
||||
|
||||
.diagnose-top {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.diagnose-img {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.diagnose-text {
|
||||
.diagnose-title {
|
||||
color: #000c;
|
||||
font-weight: 700;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.diagnose-desc {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.diagnose-progress {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.diagnose-radio {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
|
||||
.diagnose-radio-item {
|
||||
width: 150px;
|
||||
height: 35px;
|
||||
margin-right: 8px;
|
||||
color: #00000073;
|
||||
line-height: 35px;
|
||||
text-align: center;
|
||||
background: #f2f2f2;
|
||||
border-radius: 2px 2px 0 0;
|
||||
cursor: pointer;
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.diagnose-loading {
|
||||
animation: diagnose-loading 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes diagnose-loading {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,30 @@
|
|||
import { getImage } from '@/utils/comm';
|
||||
|
||||
export const headerImgMap = new Map();
|
||||
headerImgMap.set('loading', getImage('/diagnose/loading-2.png'));
|
||||
headerImgMap.set('error', getImage('/diagnose/error.png'));
|
||||
headerImgMap.set('success', getImage('/diagnose/success.png'));
|
||||
|
||||
export const headerColorMap = new Map();
|
||||
headerColorMap.set('loading', 'linear-gradient(89.95deg, #E6F5FF 0.03%, #E9EAFF 99.95%)');
|
||||
headerColorMap.set(
|
||||
'error',
|
||||
'linear-gradient(89.95deg, rgba(231, 173, 86, 0.1) 0.03%, rgba(247, 111, 93, 0.1) 99.95%)',
|
||||
);
|
||||
headerColorMap.set('success', 'linear-gradient(89.95deg, #E8F8F7 0.03%, #EBEFFA 99.95%)');
|
||||
|
||||
|
||||
export const headerTitleMap = new Map();
|
||||
headerTitleMap.set('loading', '正在诊断中');
|
||||
headerTitleMap.set('error', '发现连接问题');
|
||||
headerTitleMap.set('success', '连接状态正常');
|
||||
|
||||
export const headerDescMap = new Map();
|
||||
headerDescMap.set('loading', '已诊断XX个');
|
||||
headerDescMap.set('error', '请处理连接异常');
|
||||
headerDescMap.set('success', '现在可调试消息通信');
|
||||
|
||||
export const progressMap = new Map();
|
||||
progressMap.set('loading', '#597EF7');
|
||||
progressMap.set('error', '#FAB247');
|
||||
progressMap.set('success', '#32D4A4');
|
|
@ -44,6 +44,7 @@ import Info from './Info/index.vue';
|
|||
import Running from './Running/index.vue'
|
||||
import Metadata from '../../components/Metadata/index.vue';
|
||||
import ChildDevice from './ChildDevice/index.vue';
|
||||
import Diagnose from './Diagnose/index.vue'
|
||||
import { _deploy, _disconnect } from '@/api/device/instance'
|
||||
import { message } from 'ant-design-vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
|
@ -52,7 +53,7 @@ const route = useRoute();
|
|||
const instanceStore = useInstanceStore()
|
||||
|
||||
const statusMap = new Map();
|
||||
statusMap.set('online', 'processing');
|
||||
statusMap.set('online', 'success');
|
||||
statusMap.set('offline', 'error');
|
||||
statusMap.set('notActive', 'warning');
|
||||
|
||||
|
@ -72,7 +73,11 @@ const list = [
|
|||
{
|
||||
key: 'ChildDevice',
|
||||
tab: '子设备'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'Diagnose',
|
||||
tab: '设备诊断'
|
||||
},
|
||||
]
|
||||
|
||||
const tabs = {
|
||||
|
@ -80,6 +85,7 @@ const tabs = {
|
|||
Metadata,
|
||||
Running,
|
||||
ChildDevice,
|
||||
Diagnose
|
||||
}
|
||||
|
||||
watch(
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<Search :columns="columns" target="device-instance" />
|
||||
<Search
|
||||
:columns="columns"
|
||||
target="device-instance"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<JTable
|
||||
ref="instanceRef"
|
||||
:columns="columns"
|
||||
|
@ -267,6 +271,13 @@ import Export from './Export/index.vue';
|
|||
import Process from './Process/index.vue';
|
||||
import Save from './Save/index.vue';
|
||||
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
|
||||
import {
|
||||
getProviders,
|
||||
queryGatewayList,
|
||||
queryNoPagingPost,
|
||||
queryOrgThree,
|
||||
} from '@/api/device/product';
|
||||
import { queryTree } from '@/api/device/category';
|
||||
|
||||
const router = useRouter();
|
||||
const instanceRef = ref<Record<string, any>>({});
|
||||
|
@ -290,33 +301,172 @@ const columns = [
|
|||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '设备名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '产品名称',
|
||||
dataIndex: 'productName',
|
||||
key: 'productName',
|
||||
search: {
|
||||
type: 'select',
|
||||
options: () =>
|
||||
new Promise((resolve) => {
|
||||
queryNoPagingPost({ paging: false }).then((resp: any) => {
|
||||
resolve(
|
||||
resp.result.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '禁用', value: 'notActive' },
|
||||
{ label: '离线', value: 'offline' },
|
||||
{ label: '在线', value: 'online' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'classifiedId',
|
||||
dataIndex: 'classifiedId',
|
||||
title: '产品分类',
|
||||
hideInTable: true,
|
||||
search: {
|
||||
type: 'treeSelect',
|
||||
options: () =>
|
||||
new Promise((resolve) => {
|
||||
queryTree({ paging: false }).then((resp: any) => {
|
||||
resolve(resp.result);
|
||||
});
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'accessProvider',
|
||||
title: '网关类型',
|
||||
dataIndex: 'accessProvider',
|
||||
valueType: 'select',
|
||||
hideInTable: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: () =>
|
||||
new Promise((resolve) => {
|
||||
getProviders().then((resp: any) => {
|
||||
resolve(
|
||||
resp.result.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: `accessProvider is ${item.id}`,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'productId$product-info',
|
||||
dataIndex: 'productId$product-info',
|
||||
title: '接入方式',
|
||||
hideInTable: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: () =>
|
||||
new Promise((resolve) => {
|
||||
queryGatewayList({}).then((resp: any) => {
|
||||
resolve(
|
||||
resp.result.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: `accessId is ${item.id}`,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'deviceType',
|
||||
title: '设备类型',
|
||||
valueType: 'select',
|
||||
hideInTable: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '直连设备', value: 'device' },
|
||||
{ label: '网关子设备', value: 'childrenDevice' },
|
||||
{ label: '网关设备', value: 'gateway' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'id$dim-assets',
|
||||
title: '所属组织',
|
||||
hideInTable: true,
|
||||
search: {
|
||||
type: 'treeSelect',
|
||||
options: () =>
|
||||
new Promise((resolve) => {
|
||||
queryOrgThree({}).then((resp: any) => {
|
||||
const formatValue = (list: any[]) => {
|
||||
const _list: any[] = [];
|
||||
list.forEach((item) => {
|
||||
if (item.children) {
|
||||
item.children = formatValue(item.children);
|
||||
}
|
||||
_list.push({
|
||||
...item,
|
||||
id: JSON.stringify({
|
||||
assetType: 'device',
|
||||
targets: [
|
||||
{
|
||||
type: 'org',
|
||||
id: item.id,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
return _list;
|
||||
};
|
||||
resolve(formatValue(resp.result));
|
||||
});
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'describe',
|
||||
key: 'describe',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
@ -543,4 +693,9 @@ const saveBtn = () => {
|
|||
visible.value = false;
|
||||
instanceRef.value?.reload();
|
||||
};
|
||||
|
||||
const handleSearch = (_params: any) => {
|
||||
console.log(_params);
|
||||
params.value = _params;
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { MetadataItem } from "../Product/typings";
|
||||
|
||||
export type DeviceInstance = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<a-form ref="addFormRef" :model="form.model" layout="vertical">
|
||||
<a-form-item label="标识" name="id" :rules="[
|
||||
{ required: true, message: '请输入标识' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_]+$/,
|
||||
message: '请输入英文或者数字或者-或者_',
|
||||
},
|
||||
]">
|
||||
<a-input v-model:value="form.model.id" size="small"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="名称" name="name" :rules="[
|
||||
{ required: true, message: '请输入名称' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
]">
|
||||
<a-input v-model:value="form.model.name" size="small"></a-input>
|
||||
</a-form-item>
|
||||
<ValueTypeForm :name="['valueType']" v-model:value="form.model.valueType" key="property"></ValueTypeForm>
|
||||
<a-form-item label="读写类型" :name="['expands', 'type']" :rules="[
|
||||
{ required: true, message: '请选择读写类型' },
|
||||
]">
|
||||
<a-select v-model:value="form.model.expands.type" :options="form.expandsType" mode="multiple" size="small"></a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="说明" name="description" :rules="[
|
||||
{ max: 200, message: '最多可输入200个字符' },
|
||||
]">
|
||||
<a-textarea v-model:value="form.model.description" size="small"></a-textarea>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
<script setup lang="ts" name="PropertyForm">
|
||||
import ValueTypeForm from './ValueTypeForm.vue'
|
||||
|
||||
const form = reactive({
|
||||
model: {
|
||||
valueType: {
|
||||
expands: {}
|
||||
},
|
||||
expands: {}
|
||||
} as any,
|
||||
expandsType: [
|
||||
{
|
||||
label: '读',
|
||||
value: 'read',
|
||||
},
|
||||
{
|
||||
label: '写',
|
||||
value: 'write',
|
||||
},
|
||||
{
|
||||
label: '上报',
|
||||
value: 'report',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
:deep(.ant-form-item-label) {
|
||||
line-height: 1;
|
||||
|
||||
>label {
|
||||
font-size: 12px;
|
||||
|
||||
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-explain) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-with-help) {
|
||||
.ant-form-item-explain {
|
||||
min-height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&.ant-form-item-with-help {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-input),
|
||||
:deep(.ant-select) {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,151 @@
|
|||
<template>
|
||||
<a-form-item label="数据类型" :name="name.concat(['type'])" :rules="[
|
||||
{ required: true, message: '请选择数据类型' },
|
||||
]">
|
||||
<a-select v-model:value="value.type" :options="_dataTypeList" size="small"></a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="单位" :name="name.concat(['unit'])" v-if="['int', 'float', 'long', 'double'].includes(value.type)">
|
||||
<InputSelect v-model:value="value.unit" :options="unit.unitOptions" size="small"></InputSelect>
|
||||
</a-form-item>
|
||||
<a-form-item label="精度" :name="name.concat(['scale'])" v-if="['float', 'double'].includes(value.type)">
|
||||
<a-input-number v-model:value="value.scale" size="small" :min="0" :max="2147483647" :precision="0"
|
||||
:default-value="2" style="width: 100%"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="布尔值" name="booleanConfig" v-if="['boolean'].includes(value.type)">
|
||||
<BooleanParam
|
||||
:name="name"
|
||||
v-model:value="_value"
|
||||
></BooleanParam>
|
||||
</a-form-item>
|
||||
<a-form-item label="枚举项" :name="name.concat(['elements'])" v-if="['enum'].includes(value.type)" :rules="[
|
||||
{ required: true, message: '请配置枚举项' }
|
||||
]">
|
||||
<EnumParam v-model:value="value.elements"></EnumParam>
|
||||
</a-form-item>
|
||||
<a-form-item :name="name.concat(['expands', 'maxLength'])" v-if="['string', 'password'].includes(value.type)">
|
||||
<template #label>
|
||||
<a-space>
|
||||
最大长度
|
||||
<a-tooltip title="字节">
|
||||
<question-circle-outlined style="color: rgb(136, 136, 136); font-size: 12px;" />
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-input-number v-model:value="value.expands.maxLength" size="small" :max="2147483647" :min="1" :precision="0"
|
||||
style="width: 100%;"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="元素配置" :name="name.concat(['elementType'])" v-if="['array'].includes(value.type)">
|
||||
<ArrayParam v-model:value="value.elementType" :name="name.concat(['elementType'])"></ArrayParam>
|
||||
</a-form-item>
|
||||
<a-form-item label="JSON对象" :name="name.concat(['properties'])" v-if="['object'].includes(value.type)">
|
||||
<JsonParam v-model:value="value.jsonConfig" :name="name.concat(['properties'])"></JsonParam>
|
||||
</a-form-item>
|
||||
<a-form-item label="文件类型" :name="name.concat(['fileType'])" v-if="['file'].includes(value.type)" initialValue="url" :rules="[
|
||||
{ required: true, message: '请选择文件类型' },
|
||||
]">
|
||||
<a-select v-model:value="value.fileType" :options="FileTypeList" size="small"></a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<script lang="ts" setup mame="BaseForm">
|
||||
import { DataTypeList, FileTypeList } from '@/views/device/data';
|
||||
import { DefaultOptionType } from 'ant-design-vue/es/select';
|
||||
import { PropType } from 'vue'
|
||||
import { getUnit } from '@/api/device/instance';
|
||||
import { Store } from 'jetlinks-store';
|
||||
import InputSelect from '@/components/InputSelect/index.vue';
|
||||
import BooleanParam from '@/components/Metadata/BooleanParam/index.vue'
|
||||
import EnumParam from '@/components/Metadata/EnumParam/index.vue'
|
||||
import ArrayParam from '@/components/Metadata/ArrayParam/index.vue'
|
||||
import JsonParam from '@/components/Metadata/JsonParam/index.vue'
|
||||
|
||||
type ValueType = Record<any, any>;
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object as PropType<ValueType>,
|
||||
default: () => ({
|
||||
extends: {}
|
||||
})
|
||||
},
|
||||
isSub: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
name: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => ([]),
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', data: ValueType): void;
|
||||
}
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
|
||||
const _value = computed({
|
||||
get: () => props.value,
|
||||
set: val => {
|
||||
emit('update:value', val)
|
||||
}
|
||||
})
|
||||
|
||||
const unit = {
|
||||
unitOptions: [] as DefaultOptionType[],
|
||||
getUnit: () => {
|
||||
getUnit().then(resp => {
|
||||
const _data = resp.result.map(item => ({
|
||||
label: item.description,
|
||||
value: item.id,
|
||||
}));
|
||||
// 缓存单位数据
|
||||
Store.set('units', _data);
|
||||
unit.unitOptions = _data;
|
||||
})
|
||||
},
|
||||
}
|
||||
unit.getUnit()
|
||||
|
||||
const _dataTypeList = computed(() => props.isSub ? DataTypeList.filter(item => item.value !== 'array' && item.value !== 'object') : DataTypeList)
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
:deep(.ant-form-item-label) {
|
||||
line-height: 1;
|
||||
|
||||
>label {
|
||||
font-size: 12px;
|
||||
|
||||
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-explain) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-with-help) {
|
||||
.ant-form-item-explain {
|
||||
min-height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&.ant-form-item-with-help {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-input),
|
||||
:deep(.ant-select) {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<a-drawer :mask-closable="false" width="25vw" visible :title="`新增${typeMapping[metadataStore.model.type]}`"
|
||||
<a-drawer :mask-closable="false" width="25vw" visible :title="`${title}-${typeMapping[metadataStore.model.type]}`"
|
||||
@close="close" destroy-on-close :z-index="1000" placement="right">
|
||||
<template #extra>
|
||||
<a-button :loading="save.loading" type="primary" @click="save.saveMetadata">保存</a-button>
|
||||
</template>
|
||||
<a-form ref="addFormRef" :model="form.model"></a-form>
|
||||
<PropertyForm v-if="metadataStore.model.type === 'properties'"></PropertyForm>
|
||||
</a-drawer>
|
||||
</template>
|
||||
<script lang="ts" setup name="Edit">
|
||||
|
@ -19,6 +19,7 @@ import { Store } from 'jetlinks-store';
|
|||
import { SystemConst } from '@/utils/consts';
|
||||
import { detail } from '@/api/device/instance';
|
||||
import { DeviceInstance } from '@/views/device/Instance/typings';
|
||||
import PropertyForm from './PropertyForm.vue';
|
||||
|
||||
interface Props {
|
||||
type: 'product' | 'device';
|
||||
|
@ -41,6 +42,8 @@ const close = () => {
|
|||
metadataStore.set('item', {})
|
||||
}
|
||||
|
||||
const title = computed(() => metadataStore.model.action === 'add' ? '新增' : '修改')
|
||||
|
||||
const addFormRef = ref<FormInstance>()
|
||||
/**
|
||||
* 保存按钮
|
||||
|
@ -113,7 +116,7 @@ const save = reactive({
|
|||
})
|
||||
|
||||
const form = reactive({
|
||||
model: {}
|
||||
model: {} as Record<string, any>
|
||||
})
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
<JTable :loading="loading" :data-source="data" size="small" :columns="columns" row-key="id" model="TABLE">
|
||||
<template #headerTitle>
|
||||
<a-input-search v-model:value="searchValue" placeholder="请输入名称" @search="handleSearch"></a-input-search>
|
||||
</template>
|
||||
<template #rightExtraRender>
|
||||
<PermissionButton type="primary" :uhas-permission="`${permission}:update`" key="add" @click="handleAddClick"
|
||||
:udisabled="operateLimits('add', type)" :tooltip="{
|
||||
:disabled="operateLimits('add', type)" :tooltip="{
|
||||
title: operateLimits('add', type) ? '当前的存储方式不支持新增' : '新增',
|
||||
}">
|
||||
<template #icon>
|
||||
|
@ -31,13 +33,13 @@
|
|||
</a-tag>
|
||||
</template>
|
||||
<template #action="slotProps">
|
||||
<PermissionButton :has-permission="`${permission}:update`" type="link" key="edit" style="padding: 0"
|
||||
:disabled="operateLimits('updata', type)" @click="handleEditClick(slotProps)" :tooltip="{
|
||||
<PermissionButton :uhas-permission="`${permission}:update`" type="link" key="edit" style="padding: 0"
|
||||
:udisabled="operateLimits('updata', type)" @click="handleEditClick(slotProps)" :tooltip="{
|
||||
title: operateLimits('updata', type) ? '当前的存储方式不支持编辑' : '编辑',
|
||||
}">
|
||||
<EditOutlined />
|
||||
</PermissionButton>,
|
||||
<PermissionButton :has-permission="`${permission}:delete`" type="link" key="delete" style="padding: 0"
|
||||
</PermissionButton>
|
||||
<PermissionButton :uhas-permission="`${permission}:delete`" type="link" key="delete" style="padding: 0"
|
||||
:pop-confirm="{
|
||||
title: '确认删除?', onConfirm: async () => {
|
||||
await removeItem(slotProps);
|
||||
|
@ -58,12 +60,13 @@ import { useInstanceStore } from '@/store/instance'
|
|||
import { useProductStore } from '@/store/product'
|
||||
import { useMetadataStore } from '@/store/metadata'
|
||||
import PermissionButton from '@/components/PermissionButton/index.vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue/es'
|
||||
import { SystemConst } from '@/utils/consts'
|
||||
import { Store } from 'jetlinks-store'
|
||||
import { asyncUpdateMetadata, removeMetadata } from '../metadata'
|
||||
import { detail } from '@/api/device/instance'
|
||||
import Edit from './Edit/index.vue'
|
||||
// import { detail } from '@/api/device/instance'
|
||||
// import { detail as productDetail } from '@/api/device/product'
|
||||
interface Props {
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
import { isNoCommunity } from '@/utils/utils';
|
||||
|
||||
export const DataTypeList: { label: string; value: string }[] = [
|
||||
{
|
||||
value: 'int',
|
||||
label: 'int(整数型)',
|
||||
},
|
||||
{
|
||||
value: 'long',
|
||||
label: 'long(长整数型)',
|
||||
},
|
||||
{
|
||||
value: 'float',
|
||||
label: 'float(单精度浮点型)',
|
||||
},
|
||||
{
|
||||
value: 'double',
|
||||
label: 'double(双精度浮点数)',
|
||||
},
|
||||
{
|
||||
value: 'string',
|
||||
label: 'text(字符串)',
|
||||
},
|
||||
{
|
||||
value: 'boolean',
|
||||
label: 'boolean(布尔型)',
|
||||
},
|
||||
{
|
||||
value: 'date',
|
||||
label: 'date(时间型)',
|
||||
},
|
||||
{
|
||||
value: 'enum',
|
||||
label: 'enum(枚举)',
|
||||
},
|
||||
{
|
||||
value: 'array',
|
||||
label: 'array(数组)',
|
||||
},
|
||||
{
|
||||
value: 'object',
|
||||
label: 'object(结构体)',
|
||||
},
|
||||
{
|
||||
value: 'file',
|
||||
label: 'file(文件)',
|
||||
},
|
||||
{
|
||||
value: 'password',
|
||||
label: 'password(密码)',
|
||||
},
|
||||
{
|
||||
value: 'geoPoint',
|
||||
label: 'geoPoint(地理位置)',
|
||||
},
|
||||
];
|
||||
|
||||
export const PropertySource: { label: string; value: string }[] = isNoCommunity
|
||||
? [
|
||||
{
|
||||
value: 'device',
|
||||
label: '设备',
|
||||
},
|
||||
{
|
||||
value: 'manual',
|
||||
label: '手动',
|
||||
},
|
||||
{
|
||||
value: 'rule',
|
||||
label: '规则',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
value: 'device',
|
||||
label: '设备',
|
||||
},
|
||||
{
|
||||
value: 'manual',
|
||||
label: '手动',
|
||||
},
|
||||
];
|
||||
|
||||
export const FileTypeList: { label: string; value: string }[] = [
|
||||
{
|
||||
label: 'URL(链接)',
|
||||
value: 'url',
|
||||
},
|
||||
{
|
||||
label: 'Base64(Base64编码)',
|
||||
value: 'base64',
|
||||
},
|
||||
{
|
||||
label: 'binary',
|
||||
value: 'Binary(二进制)',
|
||||
},
|
||||
];
|
||||
|
||||
export const EventLevel: { label: string; value: string }[] = [
|
||||
{
|
||||
label: '普通',
|
||||
value: 'ordinary',
|
||||
},
|
||||
{
|
||||
label: '警告',
|
||||
value: 'warn',
|
||||
},
|
||||
{
|
||||
value: 'urgent',
|
||||
label: '紧急',
|
||||
},
|
||||
];
|
||||
|
||||
export const DateTypeList = [
|
||||
// {
|
||||
// label: 'String类型的UTC时间戳 (毫秒)',
|
||||
// value: 'string',
|
||||
// },
|
||||
{
|
||||
label: 'yyyy-MM-dd',
|
||||
value: 'yyyy-MM-dd',
|
||||
},
|
||||
{
|
||||
label: 'yyyy-MM-DD HH:mm:ss',
|
||||
value: 'yyyy-MM-DD HH:mm:ss',
|
||||
},
|
||||
// {
|
||||
// label: 'yyyy-MM-dd HH:mm:ss EE',
|
||||
// value: 'yyyy-MM-dd HH:mm:ss EE',
|
||||
// },
|
||||
// {
|
||||
// label: 'yyyy-MM-dd HH:mm:ss zzz',
|
||||
// value: 'yyyy-MM-dd HH:mm:ss zzz',
|
||||
// },
|
||||
];
|
|
@ -0,0 +1,282 @@
|
|||
<template>
|
||||
<a-modal
|
||||
v-model:visible="_vis"
|
||||
title="快速添加"
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
:confirmLoading="btnLoading"
|
||||
width="660px"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="产品名称" v-bind="validateInfos.name">
|
||||
<a-input
|
||||
v-model:value="formData.name"
|
||||
placeholder="请输入名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
<template v-if="channel === 'gb28181-2016' && formData.accessId">
|
||||
<a-form-item
|
||||
label="接入密码"
|
||||
v-bind="validateInfos['configuration.access_pwd']"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="formData.configuration.access_pwd"
|
||||
placeholder="请输入接入密码"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="流传输模式">
|
||||
<a-select
|
||||
v-model:value="formData.configuration.stream_mode"
|
||||
placeholder="请选择流传输模式"
|
||||
:options="streamMode"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label="接入网关" v-bind="validateInfos.accessId">
|
||||
<div class="gateway-box">
|
||||
<div v-if="!gatewayList.length">
|
||||
暂无数据,请先
|
||||
<a-button type="link">
|
||||
添加{{ providerType[props.channel] }} 接入网关
|
||||
</a-button>
|
||||
</div>
|
||||
<div
|
||||
class="gateway-item"
|
||||
v-for="(item, index) in gatewayList"
|
||||
:key="index"
|
||||
>
|
||||
<CardBox
|
||||
@click="handleClick"
|
||||
:active="_selectedRowKeys.includes(item.id)"
|
||||
:value="item"
|
||||
v-bind="item"
|
||||
:status="item.state?.value"
|
||||
:statusText="item.state?.text"
|
||||
:statusNames="{
|
||||
online: 'enabled',
|
||||
offline: 'disabled',
|
||||
}"
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img
|
||||
:src="getImage('/device-access.png')"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<h3 class="card-item-content-title">
|
||||
{{ item.name }}
|
||||
</h3>
|
||||
<div class="desc">{{ item.description }}</div>
|
||||
<a-row v-if="props.channel === 'gb28181-2016'">
|
||||
<a-col :span="12">
|
||||
{{ item.channelInfo?.name }}
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
{{ item.protocolDetail.name }}
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<p
|
||||
v-for="(i, idx) in item.channelInfo
|
||||
?.addresses"
|
||||
:key="`${i.address}_address${idx}`"
|
||||
>
|
||||
<a-badge
|
||||
:text="i.address"
|
||||
:color="
|
||||
i.health === -1
|
||||
? 'red'
|
||||
: 'green'
|
||||
"
|
||||
/>
|
||||
</p>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-else>
|
||||
<a-col :span="24">
|
||||
<div class="subtitle">
|
||||
{{ item.protocolDetail.name }}
|
||||
</div>
|
||||
<p>
|
||||
{{
|
||||
item.protocolDetail.description
|
||||
}}
|
||||
</p>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</CardBox>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { PropType } from 'vue';
|
||||
import { streamMode } from '@/views/media/Device/const';
|
||||
import DeviceApi from '@/api/media/device';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { gatewayType } from '@/views/media/Device/typings';
|
||||
import { providerType } from '../const';
|
||||
|
||||
const useForm = Form.useForm;
|
||||
|
||||
type Emits = {
|
||||
(e: 'update:visible', data: boolean): void;
|
||||
(e: 'update:productId', data: string): void;
|
||||
(e: 'close'): void;
|
||||
};
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
productId: { type: String, default: '' },
|
||||
channel: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const _vis = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val),
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取接入网关
|
||||
*/
|
||||
const gatewayList = ref<gatewayType[]>([]);
|
||||
const getGatewayList = async () => {
|
||||
const params = {
|
||||
pageSize: 100,
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
terms: [{ column: 'provider', value: props.channel }],
|
||||
};
|
||||
const { result } = await DeviceApi.queryProvider(params);
|
||||
gatewayList.value = result.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* 点击接入网关, 获取对应配置
|
||||
* @param e
|
||||
*/
|
||||
const _selectedRowKeys = ref<string[]>([]);
|
||||
const handleClick = async (e: any) => {
|
||||
_selectedRowKeys.value = [e.id];
|
||||
formData.value.accessId = e.id;
|
||||
formData.value.accessName = e.name;
|
||||
formData.value.accessProvider = e.provider;
|
||||
formData.value.messageProtocol = e.provider;
|
||||
formData.value.protocolName = e.protocolDetail.name;
|
||||
formData.value.transportProtocol = e.transport;
|
||||
|
||||
const { result } = await DeviceApi.getConfiguration(
|
||||
props.channel,
|
||||
e.transport,
|
||||
);
|
||||
console.log('result: ', result);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => _vis.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
getGatewayList();
|
||||
|
||||
formRules.value['configuration.access_pwd'][0].required =
|
||||
props.channel === 'gb28181-2016';
|
||||
validate();
|
||||
} else {
|
||||
emit('close');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
accessId: '',
|
||||
accessName: '',
|
||||
accessProvider: '',
|
||||
configuration: {
|
||||
access_pwd: '',
|
||||
stream_mode: 'UDP',
|
||||
},
|
||||
deviceType: 'device',
|
||||
messageProtocol: '',
|
||||
name: '',
|
||||
protocolName: '',
|
||||
transportProtocol: '',
|
||||
});
|
||||
|
||||
// 验证规则
|
||||
const formRules = ref({
|
||||
name: [{ required: true, message: '请输入产品名称' }],
|
||||
'configuration.access_pwd': [{ required: true, message: '请输入接入密码' }],
|
||||
accessId: [{ required: true, message: '请选择接入网关' }],
|
||||
});
|
||||
|
||||
const { resetFields, validate, validateInfos, clearValidate } = useForm(
|
||||
formData.value,
|
||||
formRules.value,
|
||||
);
|
||||
|
||||
/**
|
||||
* 提交
|
||||
*/
|
||||
const btnLoading = ref(false);
|
||||
const handleOk = () => {
|
||||
// console.log('formData.value: ', formData.value);
|
||||
validate()
|
||||
.then(async () => {
|
||||
btnLoading.value = true;
|
||||
const res = await DeviceApi.saveProduct(formData.value);
|
||||
if (res.success) {
|
||||
emit('update:productId', res.result.id);
|
||||
const deployResp = await DeviceApi.deployProductById(
|
||||
res.result.id,
|
||||
);
|
||||
if (deployResp.success) {
|
||||
message.success('操作成功');
|
||||
handleCancel();
|
||||
}
|
||||
}
|
||||
btnLoading.value = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err: ', err);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
_vis.value = false;
|
||||
resetFields();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.gateway-box {
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
text-align: center;
|
||||
.gateway-item {
|
||||
padding: 16px;
|
||||
.card-item-content-title,
|
||||
.desc,
|
||||
.subtitle {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.desc,
|
||||
.subtitle {
|
||||
margin-top: 10px;
|
||||
color: #666;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -13,82 +13,18 @@
|
|||
layout="horizontal"
|
||||
:options="PROVIDER_OPTIONS"
|
||||
:checkStyle="true"
|
||||
:disabled="!!formData.id"
|
||||
:disabled="!!route.query.id"
|
||||
v-model="formData.channel"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="8">
|
||||
<!-- <div class="upload-image-warp-logo">
|
||||
<div class="upload-image-border-logo">
|
||||
<a-upload
|
||||
name="file"
|
||||
:action="FILE_UPLOAD"
|
||||
:headers="{
|
||||
[TOKEN_KEY]:
|
||||
LocalStore.get(TOKEN_KEY),
|
||||
}"
|
||||
:showUploadList="false"
|
||||
accept="image/jpeg', 'image/png"
|
||||
>
|
||||
<div
|
||||
class="upload-image-content-logo"
|
||||
>
|
||||
<div
|
||||
class="loading-logo"
|
||||
v-if="form.logoLoading"
|
||||
>
|
||||
<LoadingOutlined
|
||||
style="font-size: 28px"
|
||||
<JUpload
|
||||
v-model:modelValue="formData.photoUrl"
|
||||
:bgImage="formData.photoUrl"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="upload-image"
|
||||
style="height: 100%"
|
||||
v-if="formValue.logo"
|
||||
:style="
|
||||
formValue.logo
|
||||
? `background-image: url(${formValue.logo});`
|
||||
: ''
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
v-if="formValue.logo"
|
||||
class="upload-image-mask"
|
||||
>
|
||||
点击修改
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-if="form.logoLoading"
|
||||
>
|
||||
<LoadingOutlined
|
||||
style="
|
||||
font-size: 28px;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<PlusOutlined
|
||||
style="
|
||||
font-size: 28px;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
<div v-if="form.logoLoading">
|
||||
<div class="upload-loading-mask">
|
||||
<LoadingOutlined
|
||||
style="font-size: 28px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-col :span="16">
|
||||
<a-form-item
|
||||
label="ID"
|
||||
v-bind="validateInfos.id"
|
||||
|
@ -96,6 +32,7 @@
|
|||
<a-input
|
||||
v-model:value="formData.id"
|
||||
placeholder="请输入"
|
||||
:disabled="!!route.query.id"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
|
@ -113,31 +50,78 @@
|
|||
label="所属产品"
|
||||
v-bind="validateInfos.productId"
|
||||
>
|
||||
<div>
|
||||
<a-row :gutter="[0, 10]">
|
||||
<a-col :span="22">
|
||||
<a-select
|
||||
v-model:value="formData.productId"
|
||||
placeholder="请选择所属产品"
|
||||
:disabled="!!route.query.id"
|
||||
>
|
||||
<!-- <a-select-option
|
||||
v-for="(item, index) in NOTICE_METHOD"
|
||||
<a-select-option
|
||||
v-for="(item, index) in productList"
|
||||
:key="index"
|
||||
:value="item.value"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-select-option> -->
|
||||
{{ item.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<AIcon type="PlusCircleOutlined" />
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="2">
|
||||
<a-button
|
||||
type="link"
|
||||
@click="saveProductVis = true"
|
||||
>
|
||||
<AIcon type="PlusOutlined" />
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="接入密码"
|
||||
v-bind="validateInfos['others.access_pwd']"
|
||||
v-if="formData.channel === 'gb28181-2016'"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="formData.others.access_pwd"
|
||||
placeholder="请输入接入密码"
|
||||
/>
|
||||
</a-form-item>
|
||||
<template v-if="!!route.query.id">
|
||||
<a-form-item
|
||||
label="流传输模式"
|
||||
v-bind="validateInfos.streamMode"
|
||||
>
|
||||
<a-radio-group
|
||||
button-style="solid"
|
||||
v-model:value="formData.streamMode"
|
||||
>
|
||||
<a-radio-button value="UDP">
|
||||
UDP
|
||||
</a-radio-button>
|
||||
<a-radio-button value="TCP_PASSIVE">
|
||||
TCP被动
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="设备厂商">
|
||||
<a-input
|
||||
v-model:value="formData.manufacturer"
|
||||
placeholder="请输入设备厂商"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="设备型号">
|
||||
<a-input
|
||||
v-model:value="formData.model"
|
||||
placeholder="请输入设备型号"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="固件版本">
|
||||
<a-input
|
||||
v-model:value="formData.firmware"
|
||||
placeholder="请输入固件版本"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<a-form-item label="说明">
|
||||
<a-textarea
|
||||
|
@ -250,6 +234,13 @@
|
|||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<SaveProduct
|
||||
v-model:visible="saveProductVis"
|
||||
v-model:productId="formData.productId"
|
||||
:channel="formData.channel"
|
||||
@close="getProductList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -258,12 +249,11 @@ import { getImage } from '@/utils/comm';
|
|||
import { Form } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import templateApi from '@/api/notice/template';
|
||||
import DeviceApi from '@/api/media/device';
|
||||
|
||||
import { FILE_UPLOAD } from '@/api/comm';
|
||||
import { LocalStore } from '@/utils/comm';
|
||||
import { TOKEN_KEY } from '@/utils/variable';
|
||||
import { PROVIDER_OPTIONS } from '@/views/media/Device/const';
|
||||
import type { ProductType } from '@/views/media/Device/typings';
|
||||
import SaveProduct from './SaveProduct.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
@ -274,18 +264,26 @@ const formData = ref({
|
|||
id: '',
|
||||
name: '',
|
||||
channel: 'gb28181-2016',
|
||||
photoUrl: '',
|
||||
photoUrl: getImage('/device-media.png'),
|
||||
productId: '',
|
||||
others: {
|
||||
access_pwd: '',
|
||||
},
|
||||
description: '',
|
||||
// 编辑字段
|
||||
streamMode: 'UDP',
|
||||
manufacturer: '',
|
||||
model: '',
|
||||
firmware: '',
|
||||
});
|
||||
|
||||
// 验证规则
|
||||
const formRules = ref({
|
||||
id: [
|
||||
{ required: true, message: '请输入ID' },
|
||||
{
|
||||
required: true,
|
||||
message: '请输入ID',
|
||||
},
|
||||
{ max: 64, message: '最多输入64个字符' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_\-]+$/,
|
||||
|
@ -300,8 +298,20 @@ const formRules = ref({
|
|||
channel: [{ required: true, message: '请选择接入方式' }],
|
||||
'others.access_pwd': [{ required: true, message: '请输入接入密码' }],
|
||||
description: [{ max: 200, message: '最多可输入200个字符' }],
|
||||
streamMode: [{ required: true, message: '请选择流传输模式' }],
|
||||
});
|
||||
|
||||
watch(
|
||||
() => formData.value.channel,
|
||||
(val) => {
|
||||
formRules.value['id'][0].required = val === 'gb28181-2016';
|
||||
formRules.value['others.access_pwd'][0].required =
|
||||
val === 'gb28181-2016';
|
||||
validate();
|
||||
getProductList();
|
||||
},
|
||||
);
|
||||
|
||||
const { resetFields, validate, validateInfos, clearValidate } = useForm(
|
||||
formData.value,
|
||||
formRules.value,
|
||||
|
@ -309,36 +319,64 @@ const { resetFields, validate, validateInfos, clearValidate } = useForm(
|
|||
|
||||
const clearValid = () => {
|
||||
setTimeout(() => {
|
||||
formData.value.variableDefinitions = [];
|
||||
clearValidate();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所属产品
|
||||
*/
|
||||
const productList = ref<ProductType[]>([]);
|
||||
const getProductList = async () => {
|
||||
// console.log('formData.productId: ', formData.value.productId);
|
||||
const params = {
|
||||
paging: false,
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
terms: [
|
||||
{ column: 'accessProvider', value: formData.value.channel },
|
||||
{ column: 'state', value: 1 },
|
||||
],
|
||||
};
|
||||
const { result } = await DeviceApi.queryProductList(params);
|
||||
productList.value = result;
|
||||
};
|
||||
getProductList();
|
||||
|
||||
/**
|
||||
* 新增产品
|
||||
*/
|
||||
const saveProductVis = ref(false);
|
||||
|
||||
/**
|
||||
* 获取详情
|
||||
*/
|
||||
const getDetail = async () => {
|
||||
const res = await templateApi.detail(route.params.id as string);
|
||||
const res = await DeviceApi.detail(route.query.id as string);
|
||||
// console.log('res: ', res);
|
||||
formData.value = res.result;
|
||||
// console.log('formData.value: ', formData.value);
|
||||
formData.value.channel = res.result.provider;
|
||||
console.log('formData.value: ', formData.value);
|
||||
// validate();
|
||||
};
|
||||
// getDetail();
|
||||
|
||||
onMounted(() => {
|
||||
getDetail();
|
||||
});
|
||||
|
||||
/**
|
||||
* 表单提交
|
||||
*/
|
||||
const btnLoading = ref<boolean>(false);
|
||||
const handleSubmit = () => {
|
||||
// console.log('formData.value: ', formData.value);
|
||||
console.log('formData.value: ', formData.value);
|
||||
validate()
|
||||
.then(async () => {
|
||||
btnLoading.value = true;
|
||||
let res;
|
||||
if (!formData.value.id) {
|
||||
res = await templateApi.save(formData.value);
|
||||
if (!route.query.id) {
|
||||
res = await DeviceApi.save(formData.value);
|
||||
} else {
|
||||
res = await templateApi.update(formData.value);
|
||||
res = await DeviceApi.update(formData.value);
|
||||
}
|
||||
// console.log('res: ', res);
|
||||
if (res?.success) {
|
||||
|
@ -360,70 +398,5 @@ const handleSubmit = () => {
|
|||
.page-container {
|
||||
background: #f0f2f5;
|
||||
padding: 24px;
|
||||
.upload-image-warp-logo {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
.upload-image-border-logo {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px dashed #d9d9d9;
|
||||
transition: all 0.3s;
|
||||
width: 160px;
|
||||
height: 150px;
|
||||
&:hover {
|
||||
border: 1px dashed #1890ff;
|
||||
display: flex;
|
||||
}
|
||||
.upload-image-content-logo {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 160px;
|
||||
height: 150px;
|
||||
padding: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
cursor: pointer;
|
||||
.loading-logo {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
}
|
||||
.loading-icon {
|
||||
position: absolute;
|
||||
}
|
||||
.upload-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50%;
|
||||
background-size: cover;
|
||||
}
|
||||
.upload-image-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50%;
|
||||
background-size: inherit;
|
||||
}
|
||||
.upload-image-mask {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
background-color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
&:hover .upload-image-mask {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,3 +2,12 @@ export const PROVIDER_OPTIONS = [
|
|||
{ label: '固定地址', value: 'fixed-media' },
|
||||
{ label: 'GB/T28181', value: 'gb28181-2016' },
|
||||
]
|
||||
export const streamMode = [
|
||||
{ label: 'UDP', value: 'UDP' },
|
||||
{ label: 'TCP被动', value: 'TCP_PASSIVE' },
|
||||
]
|
||||
|
||||
export const providerType = {
|
||||
'gb28181-2016': 'GB/T28181',
|
||||
'fixed-media': '固定地址',
|
||||
};
|
|
@ -135,11 +135,7 @@ import type { ActionsType } from '@/components/Table/index.vue';
|
|||
import { message } from 'ant-design-vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { PROVIDER_OPTIONS } from '@/views/media/Device/const';
|
||||
|
||||
const providerType = {
|
||||
'gb28181-2016': 'GB/T28181',
|
||||
'fixed-media': '固定地址',
|
||||
};
|
||||
import { providerType } from './const';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
@ -22,3 +22,64 @@ export type DeviceItem = {
|
|||
streamMode: string;
|
||||
transport: string;
|
||||
} & BaseItem;
|
||||
|
||||
export type ProductType = {
|
||||
accessId: string;
|
||||
accessName: string;
|
||||
accessProvider: string;
|
||||
createTime: number;
|
||||
creatorId: string;
|
||||
deviceType: {
|
||||
text: string;
|
||||
value: string;
|
||||
};
|
||||
id: string;
|
||||
messageProtocol: string;
|
||||
metadata: string;
|
||||
modifierId: string;
|
||||
modifyTime: number;
|
||||
name: string;
|
||||
protocolName: string;
|
||||
state: number;
|
||||
transportProtocol: string;
|
||||
}
|
||||
|
||||
|
||||
type addressesType = {
|
||||
address: string;
|
||||
bad: boolean;
|
||||
disabled: boolean;
|
||||
health: number;
|
||||
ok: boolean;
|
||||
}
|
||||
export type gatewayType = {
|
||||
channel: string;
|
||||
channelId: string;
|
||||
channelInfo: {
|
||||
id: string;
|
||||
name: string;
|
||||
addresses: addressesType[];
|
||||
};
|
||||
id: string;
|
||||
name: string;
|
||||
protocol: string;
|
||||
protocolDetail: {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
provider: string;
|
||||
state: {
|
||||
text: string;
|
||||
value: string;
|
||||
};
|
||||
transport: string;
|
||||
transportDetail: {
|
||||
id: string;
|
||||
name: string;
|
||||
metadata: string;
|
||||
features: string[];
|
||||
routes: string[];
|
||||
};
|
||||
description?: string;
|
||||
}
|
|
@ -11,7 +11,10 @@
|
|||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="名称" name="name" :rules=" [
|
||||
<a-form-item
|
||||
label="名称"
|
||||
name="name"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入名称',
|
||||
|
@ -20,47 +23,87 @@
|
|||
max: 64,
|
||||
message: '最多输入64个字符',
|
||||
},
|
||||
]">
|
||||
<a-input placeholder="请输入名称" v-model:value="modelRef.name" />
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
placeholder="请输入名称"
|
||||
v-model:value="modelRef.name"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item :name="['accessConfig', 'regionId']" :rules="[{
|
||||
<a-form-item
|
||||
:name="['accessConfig', 'regionId']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择服务地址',
|
||||
}]">
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template #label>
|
||||
<span>
|
||||
服务地址
|
||||
<a-tooltip title="阿里云内部给每台机器设置的唯一编号">
|
||||
<a-tooltip
|
||||
title="阿里云内部给每台机器设置的唯一编号"
|
||||
>
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="margin-left: 2px;" />
|
||||
style="margin-left: 2px"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<a-select placeholder="请选择服务地址" v-model:value="modelRef.accessConfig.regionId" show-search :filter-option="filterOption" @blur="productChange">
|
||||
<a-select-option v-for="item in regionsList" :key="item.id" :value="item.id" :label="item.name">{{item.name}}</a-select-option>
|
||||
<a-select
|
||||
placeholder="请选择服务地址"
|
||||
v-model:value="
|
||||
modelRef.accessConfig.regionId
|
||||
"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
@blur="productChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in regionsList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name"
|
||||
>{{ item.name }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item :name="['accessConfig', 'instanceId']">
|
||||
<a-form-item
|
||||
:name="['accessConfig', 'instanceId']"
|
||||
>
|
||||
<template #label>
|
||||
<span>
|
||||
实例ID
|
||||
<a-tooltip title="阿里云物联网平台中的实例ID,没有则不填">
|
||||
<a-tooltip
|
||||
title="阿里云物联网平台中的实例ID,没有则不填"
|
||||
>
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="margin-left: 2px;" />
|
||||
style="margin-left: 2px"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<a-input placeholder="请输入实例ID" v-model:value="modelRef.accessConfig.instanceId" @blur="productChange" />
|
||||
<a-input
|
||||
placeholder="请输入实例ID"
|
||||
v-model:value="
|
||||
modelRef.accessConfig.instanceId
|
||||
"
|
||||
@blur="productChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item :name="['accessConfig', 'accessKeyId']" :rules="[{
|
||||
<a-form-item
|
||||
:name="['accessConfig', 'accessKeyId']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入accessKey',
|
||||
},
|
||||
|
@ -68,22 +111,35 @@
|
|||
max: 64,
|
||||
message: '最多输入64个字符',
|
||||
},
|
||||
]">
|
||||
]"
|
||||
>
|
||||
<template #label>
|
||||
<span>
|
||||
accessKey
|
||||
<a-tooltip title="用于程序通知方式调用云服务API的用户标识">
|
||||
<a-tooltip
|
||||
title="用于程序通知方式调用云服务API的用户标识"
|
||||
>
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="margin-left: 2px;" />
|
||||
style="margin-left: 2px"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<a-input placeholder="请输入accessKey" v-model:value="modelRef.accessConfig.accessKeyId" @blur="productChange" />
|
||||
<a-input
|
||||
placeholder="请输入accessKey"
|
||||
v-model:value="
|
||||
modelRef.accessConfig.accessKeyId
|
||||
"
|
||||
@blur="productChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item :name="['accessConfig', 'accessSecret']" :rules="[{
|
||||
<a-form-item
|
||||
:name="['accessConfig', 'accessSecret']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入accessSecret',
|
||||
},
|
||||
|
@ -91,63 +147,176 @@
|
|||
max: 64,
|
||||
message: '最多输入64个字符',
|
||||
},
|
||||
]">
|
||||
]"
|
||||
>
|
||||
<template #label>
|
||||
<span>
|
||||
accessSecret
|
||||
<a-tooltip title="用于程序通知方式调用云服务费API的秘钥标识">
|
||||
<a-tooltip
|
||||
title="用于程序通知方式调用云服务费API的秘钥标识"
|
||||
>
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="margin-left: 2px;" />
|
||||
style="margin-left: 2px"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<a-input placeholder="请输入accessSecret" v-model:value="modelRef.accessConfig.accessSecret" @blur="productChange" />
|
||||
<a-input
|
||||
placeholder="请输入accessSecret"
|
||||
v-model:value="
|
||||
modelRef.accessConfig.accessSecret
|
||||
"
|
||||
@blur="productChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item name="bridgeProductKey" :rules="{
|
||||
<a-form-item
|
||||
name="bridgeProductKey"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '请选择网桥产品',
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<span>
|
||||
网桥产品
|
||||
<a-tooltip title="物联网平台对应的阿里云产品">
|
||||
<a-tooltip
|
||||
title="物联网平台对应的阿里云产品"
|
||||
>
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="margin-left: 2px;" />
|
||||
style="margin-left: 2px"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<a-select placeholder="请选择网桥产品" v-model:value="modelRef.bridgeProductKey" show-search :filter-option="filterOption">
|
||||
<a-select-option v-for="item in aliyunProductList" :key="item.productKey" :value="item.productKey" :label="item.productName">{{item.productName}}</a-select-option>
|
||||
<a-select
|
||||
placeholder="请选择网桥产品"
|
||||
v-model:value="
|
||||
modelRef.bridgeProductKey
|
||||
"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in aliyunProductList"
|
||||
:key="item.productKey"
|
||||
:value="item.productKey"
|
||||
:label="item.productName"
|
||||
>{{
|
||||
item.productName
|
||||
}}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<p>产品映射</p>
|
||||
<a-collapse v-if="modelRef.mappings.length" :activeKey="modelRef.mappings.map((_, _index) => _index)">
|
||||
<a-collapse-panel v-for="(item, index) in modelRef.mappings" :key="index" :header="item.productKey ? aliyunProductList.find(i => i.productKey === item.productKey)?.productName : `产品映射${index + 1}`">
|
||||
<template #extra><AIcon type="DeleteOutlined" @click="delItem(index)" /></template>
|
||||
<a-collapse
|
||||
v-if="modelRef.mappings.length"
|
||||
:activeKey="activeKey"
|
||||
@change="onCollChange"
|
||||
>
|
||||
<a-collapse-panel
|
||||
v-for="(
|
||||
item, index
|
||||
) in modelRef.mappings"
|
||||
:key="index"
|
||||
:header="
|
||||
item.productKey
|
||||
? aliyunProductList.find(
|
||||
(i) =>
|
||||
i.productKey ===
|
||||
item.productKey,
|
||||
)?.productName
|
||||
: `产品映射${index + 1}`
|
||||
"
|
||||
>
|
||||
<template #extra
|
||||
><AIcon
|
||||
type="DeleteOutlined"
|
||||
@click="delItem(index)"
|
||||
/></template>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="阿里云产品" :name="['mappings', index, 'productKey']" :rules="{
|
||||
<a-form-item
|
||||
label="阿里云产品"
|
||||
:name="[
|
||||
'mappings',
|
||||
index,
|
||||
'productKey',
|
||||
]"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '请选择阿里云产品',
|
||||
}">
|
||||
<a-select placeholder="请选择阿里云产品" v-model:value="item.productKey" show-search :filter-option="filterOption">
|
||||
<a-select-option v-for="i in getAliyunProductList(item.productKey)" :key="i.productKey" :value="i.productKey" :label="i.productName">{{i.productName}}</a-select-option>
|
||||
message:
|
||||
'请选择阿里云产品',
|
||||
}"
|
||||
>
|
||||
<a-select
|
||||
placeholder="请选择阿里云产品"
|
||||
v-model:value="
|
||||
item.productKey
|
||||
"
|
||||
show-search
|
||||
:filter-option="
|
||||
filterOption
|
||||
"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="i in getAliyunProductList(
|
||||
item.productKey,
|
||||
)"
|
||||
:key="i.productKey"
|
||||
:value="
|
||||
i.productKey
|
||||
"
|
||||
:label="
|
||||
i.productName
|
||||
"
|
||||
>{{
|
||||
i.productName
|
||||
}}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="平台产品" :name="['mappings', index, 'productId']" :rules="{
|
||||
<a-form-item
|
||||
label="平台产品"
|
||||
:name="[
|
||||
'mappings',
|
||||
index,
|
||||
'productId',
|
||||
]"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '请选择平台产品',
|
||||
}">
|
||||
<a-select placeholder="请选择平台产品" v-model:value="item.productId" show-search :filter-option="filterOption">
|
||||
<a-select-option v-for="i in getPlatProduct(item.productId)" :key="i.id" :value="item.id" :label="i.name">{{i.name}}</a-select-option>
|
||||
message:
|
||||
'请选择平台产品',
|
||||
}"
|
||||
>
|
||||
<a-select
|
||||
placeholder="请选择平台产品"
|
||||
v-model:value="
|
||||
item.productId
|
||||
"
|
||||
show-search
|
||||
:filter-option="
|
||||
filterOption
|
||||
"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="i in getPlatProduct(
|
||||
item.productId,
|
||||
)"
|
||||
:key="i.id"
|
||||
:value="item.id"
|
||||
:label="i.name"
|
||||
>{{
|
||||
i.name
|
||||
}}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
@ -156,17 +325,26 @@
|
|||
</a-collapse>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-button type="dashed" style="width: 100%; margin-top: 10px" @click="addItem">
|
||||
<a-button
|
||||
type="dashed"
|
||||
style="width: 100%; margin-top: 10px"
|
||||
@click="addItem"
|
||||
>
|
||||
<AIcon
|
||||
type="PlusOutlined"
|
||||
style="margin-left: 2px;" />添加
|
||||
style="margin-left: 2px"
|
||||
/>添加
|
||||
</a-button>
|
||||
</a-col>
|
||||
<a-col :span="24" style="margin-top: 20px">
|
||||
<a-form-item label="说明" name="description" :rules="{
|
||||
<a-form-item
|
||||
label="说明"
|
||||
name="description"
|
||||
:rules="{
|
||||
max: 200,
|
||||
message: '最多输入200个字符',
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="modelRef.description"
|
||||
placeholder="请输入说明"
|
||||
|
@ -178,7 +356,12 @@
|
|||
</a-row>
|
||||
</a-form>
|
||||
<div v-if="type === 'edit'">
|
||||
<a-button :loading="loading" type="primary" @click="saveBtn">保存</a-button>
|
||||
<a-button
|
||||
:loading="loading"
|
||||
type="primary"
|
||||
@click="saveBtn"
|
||||
>保存</a-button
|
||||
>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
|
@ -190,8 +373,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Doc from './doc.vue'
|
||||
import {savePatch, detail, getRegionsList, getAliyunProductsList, queryProductList } from '@/api/northbound/alicloud'
|
||||
import Doc from './doc.vue';
|
||||
import {
|
||||
savePatch,
|
||||
detail,
|
||||
getRegionsList,
|
||||
getAliyunProductsList,
|
||||
queryProductList,
|
||||
} from '@/api/northbound/alicloud';
|
||||
import _ from 'lodash';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
|
@ -207,90 +396,108 @@ const modelRef = reactive({
|
|||
regionId: undefined,
|
||||
instanceId: undefined,
|
||||
accessKeyId: undefined,
|
||||
accessSecret: undefined
|
||||
accessSecret: undefined,
|
||||
},
|
||||
bridgeProductKey: undefined,
|
||||
bridgeProductName: undefined,
|
||||
mappings: [{
|
||||
mappings: [
|
||||
{
|
||||
productKey: undefined,
|
||||
productId: undefined,
|
||||
}],
|
||||
description: undefined
|
||||
},
|
||||
],
|
||||
description: undefined,
|
||||
});
|
||||
|
||||
const addItem = () => {
|
||||
activeKey.value.push(String(modelRef.mappings.length));
|
||||
modelRef.mappings.push({
|
||||
productKey: undefined,
|
||||
productId: undefined,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const delItem = (index: number) => {
|
||||
modelRef.mappings.splice(index, 1)
|
||||
}
|
||||
modelRef.mappings.splice(index, 1);
|
||||
};
|
||||
|
||||
const productList = ref<Record<string, any>[]>([])
|
||||
const regionsList = ref<Record<string, any>[]>([])
|
||||
const aliyunProductList = ref<Record<string, any>[]>([])
|
||||
const loading = ref<boolean>(false)
|
||||
const type = ref<'edit' | 'view'>('view')
|
||||
const productList = ref<Record<string, any>[]>([]);
|
||||
const regionsList = ref<Record<string, any>[]>([]);
|
||||
const aliyunProductList = ref<Record<string, any>[]>([]);
|
||||
const loading = ref<boolean>(false);
|
||||
const type = ref<'edit' | 'view'>('edit');
|
||||
const activeKey = ref<string[]>(['0']);
|
||||
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
const queryRegionsList = async () => {
|
||||
const resp = await getRegionsList()
|
||||
const resp = await getRegionsList();
|
||||
if (resp.status === 200) {
|
||||
regionsList.value = resp.result as Record<string, any>[]
|
||||
}
|
||||
regionsList.value = resp.result as Record<string, any>[];
|
||||
}
|
||||
};
|
||||
const getProduct = async () => {
|
||||
const resp = await queryProductList({
|
||||
paging: false,
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
})
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
productList.value = (resp?.result as Record<string, any>[])
|
||||
}
|
||||
productList.value = resp?.result as Record<string, any>[];
|
||||
}
|
||||
};
|
||||
|
||||
const getAliyunProduct = async (data: any) => {
|
||||
if (data.regionId && data.accessKeyId && data.accessSecret) {
|
||||
const resp: any = await getAliyunProductsList(data)
|
||||
const resp: any = await getAliyunProductsList(data);
|
||||
if (resp.status === 200) {
|
||||
aliyunProductList.value = (resp?.result?.data as Record<string, any>[])
|
||||
}
|
||||
aliyunProductList.value = resp?.result?.data as Record<
|
||||
string,
|
||||
any
|
||||
>[];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const productChange = () => {
|
||||
const data = modelRef.accessConfig
|
||||
getAliyunProduct(data)
|
||||
}
|
||||
const data = modelRef.accessConfig;
|
||||
getAliyunProduct(data);
|
||||
};
|
||||
|
||||
const getPlatProduct = (val: string) => {
|
||||
const arr = modelRef.mappings.map(item => item?.productId) || []
|
||||
const checked = _.cloneDeep(arr)
|
||||
const _index = checked.findIndex(i => i === val)
|
||||
checked.splice(_index, 1)
|
||||
const list = productList.value.filter((i: any) => !checked.includes(i?.id as any))
|
||||
return list || []
|
||||
}
|
||||
const arr = modelRef.mappings.map((item) => item?.productId) || [];
|
||||
const checked = _.cloneDeep(arr);
|
||||
const _index = checked.findIndex((i) => i === val);
|
||||
checked.splice(_index, 1);
|
||||
const list = productList.value.filter(
|
||||
(i: any) => !checked.includes(i?.id as any),
|
||||
);
|
||||
return list || [];
|
||||
};
|
||||
|
||||
const getAliyunProductList = (val: string) => {
|
||||
const items = modelRef.mappings.map((item) => item?.productKey) || []
|
||||
const checked = _.cloneDeep(items)
|
||||
const _index = checked.findIndex(i => i === val)
|
||||
checked.splice(_index, 1)
|
||||
const list = aliyunProductList.value?.filter((i: any) => !checked.includes(i?.productKey as any))
|
||||
return list || []
|
||||
}
|
||||
const items = modelRef.mappings.map((item) => item?.productKey) || [];
|
||||
const checked = _.cloneDeep(items);
|
||||
const _index = checked.findIndex((i) => i === val);
|
||||
checked.splice(_index, 1);
|
||||
const list = aliyunProductList.value?.filter(
|
||||
(i: any) => !checked.includes(i?.productKey as any),
|
||||
);
|
||||
return list || [];
|
||||
};
|
||||
|
||||
const saveBtn = async () => {
|
||||
const data = await formRef.value.validate()
|
||||
const onCollChange = (_key: string[]) => {
|
||||
activeKey.value = _key;
|
||||
};
|
||||
|
||||
const saveBtn = () => {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(async (data: any) => {
|
||||
const product = (aliyunProductList.value || []).find(
|
||||
(item: any) => item?.bridgeProductKey === data?.bridgeProductKey,
|
||||
(item: any) =>
|
||||
item?.bridgeProductKey === data?.bridgeProductKey,
|
||||
);
|
||||
data.bridgeProductName = product?.productName || '';
|
||||
loading.value = true;
|
||||
|
@ -299,35 +506,43 @@ const saveBtn = async () => {
|
|||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
formRef.value.resetFields();
|
||||
router.push('/iot/northbound/AliCloud/');
|
||||
router.push('/iot/northbound/AliCloud');
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
const _arr = err.errorFields.map((i: any) => i.name);
|
||||
_arr.map((item: string | any[]) => {
|
||||
if (item.length === 3 && !activeKey.value.includes(item[1])) {
|
||||
activeKey.value.push(item[1]);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
watch(
|
||||
() => route.params?.id,
|
||||
async (newId) => {
|
||||
if (newId) {
|
||||
queryRegionsList()
|
||||
getProduct()
|
||||
queryRegionsList();
|
||||
getProduct();
|
||||
if (newId === ':id' || !newId) return;
|
||||
const resp = await detail(newId as string)
|
||||
const resp = await detail(newId as string);
|
||||
const _data: any = resp.result;
|
||||
if (_data) {
|
||||
getAliyunProduct(_data?.accessConfig)
|
||||
getAliyunProduct(_data?.accessConfig);
|
||||
}
|
||||
Object.assign(modelRef, _data)
|
||||
Object.assign(modelRef, _data);
|
||||
}
|
||||
},
|
||||
{immediate: true, deep: true}
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
|
||||
watch(
|
||||
() => route.query.type,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
type.value = newVal as 'edit' | 'view'
|
||||
type.value = newVal as 'edit' | 'view';
|
||||
}
|
||||
},
|
||||
{immediate: true, deep: true}
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
</script>
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<Search :columns="columns" target="northbound-dueros" :params="params" />
|
||||
<Search :columns="columns" target="northbound-dueros" @search="handleSearch" />
|
||||
<JTable
|
||||
ref="instanceRef"
|
||||
:columns="columns"
|
||||
|
@ -166,11 +166,17 @@ const columns = [
|
|||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '网桥产品',
|
||||
dataIndex: 'bridgeProductName',
|
||||
key: 'bridgeProductName',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
|
@ -182,6 +188,13 @@ const columns = [
|
|||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '正常', value: 'enabled' },
|
||||
{ label: '禁用', value: 'disabled' }
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
@ -303,4 +316,8 @@ const getActions = (
|
|||
return actions.filter((i: ActionsType) => i.key !== 'view');
|
||||
return actions;
|
||||
};
|
||||
|
||||
const handleSearch = (_params: any) => {
|
||||
params.value = _params
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -127,10 +127,15 @@ const funcChange = (val: string) => {
|
|||
const saveBtn = () => new Promise((resolve) => {
|
||||
formRef.value.validate()
|
||||
.then(() => {
|
||||
const _arr = toRaw(modelRef).value?.message?.inputs || []
|
||||
if(_arr.length && !_arr.every((_a: any) => _a.value)){
|
||||
resolve(false)
|
||||
} else {
|
||||
resolve(toRaw(modelRef))
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
resolve(false)
|
||||
resolve(err)
|
||||
});
|
||||
})
|
||||
|
||||
|
|
|
@ -11,7 +11,10 @@
|
|||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="名称" name="name" :rules=" [
|
||||
<a-form-item
|
||||
label="名称"
|
||||
name="name"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入名称',
|
||||
|
@ -20,89 +23,235 @@
|
|||
max: 64,
|
||||
message: '最多输入64个字符',
|
||||
},
|
||||
]">
|
||||
<a-input placeholder="请输入名称" v-model:value="modelRef.name" />
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
placeholder="请输入名称"
|
||||
v-model:value="modelRef.name"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="产品" name="id" :rules="[{
|
||||
<a-form-item
|
||||
label="产品"
|
||||
name="id"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择产品',
|
||||
}]">
|
||||
<a-select :disabled="modelRef.id !== ':id'" placeholder="请选择产品" v-model:value="modelRef.id" show-search :filter-option="filterOption" @change="productChange">
|
||||
<a-select-option v-for="item in productList" :key="item.id" :value="item.id" :label="item.name">{{item.name}}</a-select-option>
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-select
|
||||
:disabled="type !== 'edit' && modelRef.id && modelRef.id !== ':id'"
|
||||
placeholder="请选择产品"
|
||||
v-model:value="modelRef.id"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
@change="productChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in productList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name"
|
||||
>{{ item.name }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item name="applianceType" :rules="{
|
||||
<a-form-item
|
||||
name="applianceType"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '请选择设备类型',
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<span>
|
||||
设备类型
|
||||
<a-tooltip title="DuerOS平台拟定的规范">
|
||||
<a-tooltip
|
||||
title="DuerOS平台拟定的规范"
|
||||
>
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="margin-left: 2px;" />
|
||||
style="margin-left: 2px"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<a-select placeholder="请选择设备类型" v-model:value="modelRef.applianceType" show-search :filter-option="filterOption" @change="typeChange">
|
||||
<a-select-option v-for="item in typeList" :key="item.id" :value="item.id" :label="item.name">{{item.name}}</a-select-option>
|
||||
<a-select
|
||||
placeholder="请选择设备类型"
|
||||
v-model:value="modelRef.applianceType"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
@change="typeChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in typeList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name"
|
||||
>{{ item.name }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item name="productName" v-show="false" label="产品名称">
|
||||
<a-input v-model:value="modelRef.productName" />
|
||||
<a-form-item
|
||||
name="productName"
|
||||
v-show="false"
|
||||
label="产品名称"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="modelRef.productName"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<p>动作映射</p>
|
||||
<a-collapse v-if="modelRef.actionMappings.length" :activeKey="modelRef.actionMappings.map((_, _index) => _index)">
|
||||
<a-collapse-panel v-for="(item, index) in modelRef.actionMappings" :key="index" :header="item.action ? getTypesActions(item.action).find(i => i.id === item.action)?.name : `动作映射${index + 1}`">
|
||||
<template #extra><AIcon type="DeleteOutlined" @click="delItem(index)" /></template>
|
||||
<a-collapse
|
||||
v-if="modelRef.actionMappings.length"
|
||||
:activeKey="actionActiveKey"
|
||||
@change="onActionCollChange"
|
||||
>
|
||||
<a-collapse-panel
|
||||
v-for="(
|
||||
item, index
|
||||
) in modelRef.actionMappings"
|
||||
:key="index"
|
||||
:header="
|
||||
item.action
|
||||
? getTypesActions(
|
||||
item.action,
|
||||
).find(
|
||||
(i) =>
|
||||
i.id === item.action,
|
||||
)?.name
|
||||
: `动作映射${index + 1}`
|
||||
"
|
||||
>
|
||||
<template #extra
|
||||
><AIcon
|
||||
type="DeleteOutlined"
|
||||
@click="delItem(index)"
|
||||
/></template>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item :name="['actionMappings', index, 'action']" :rules="{
|
||||
<a-form-item
|
||||
:name="[
|
||||
'actionMappings',
|
||||
index,
|
||||
'action',
|
||||
]"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '请选择动作',
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<span>
|
||||
动作
|
||||
<a-tooltip title="DuerOS平台拟定的设备类型具有的相关动作">
|
||||
<AIcon type="QuestionCircleOutlined" />
|
||||
<a-tooltip
|
||||
title="DuerOS平台拟定的设备类型具有的相关动作"
|
||||
>
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<a-select placeholder="请选择动作" v-model:value="item.action" show-search :filter-option="filterOption">
|
||||
<a-select-option v-for="i in getTypesActions(item.action)" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</a-select-option>
|
||||
<a-select
|
||||
placeholder="请选择动作"
|
||||
v-model:value="
|
||||
item.action
|
||||
"
|
||||
show-search
|
||||
:filter-option="
|
||||
filterOption
|
||||
"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="i in getTypesActions(
|
||||
item.action,
|
||||
)"
|
||||
:key="i.id"
|
||||
:value="i.id"
|
||||
:label="i.name"
|
||||
>{{
|
||||
i.name
|
||||
}}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :name="['actionMappings', index, 'actionType']" :rules="{
|
||||
<a-form-item
|
||||
:name="[
|
||||
'actionMappings',
|
||||
index,
|
||||
'actionType',
|
||||
]"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '请选择操作',
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<span>
|
||||
操作
|
||||
<a-tooltip title="映射物联网平台中所选产品具备的动作">
|
||||
<AIcon type="QuestionCircleOutlined" />
|
||||
<a-tooltip
|
||||
title="映射物联网平台中所选产品具备的动作"
|
||||
>
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<a-select placeholder="请选择操作" v-model:value="item.actionType" show-search :filter-option="filterOption">
|
||||
<a-select-option value="command">下发指令</a-select-option>
|
||||
<a-select-option value="latestData">获取历史数据</a-select-option>
|
||||
<a-select
|
||||
placeholder="请选择操作"
|
||||
v-model:value="
|
||||
item.actionType
|
||||
"
|
||||
show-search
|
||||
:filter-option="
|
||||
filterOption
|
||||
"
|
||||
>
|
||||
<a-select-option
|
||||
value="command"
|
||||
>下发指令</a-select-option
|
||||
>
|
||||
<a-select-option
|
||||
value="latestData"
|
||||
>获取历史数据</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24" v-if="item.actionType">
|
||||
<a-form-item :name="['actionMappings', index, 'command']">
|
||||
<Command ref="command" :metadata="findProductMetadata" v-model:modelValue="item.command" :actionType="item.actionType" />
|
||||
<a-col
|
||||
:span="24"
|
||||
v-if="item.actionType"
|
||||
>
|
||||
<a-form-item
|
||||
:name="[
|
||||
'actionMappings',
|
||||
index,
|
||||
'command',
|
||||
]"
|
||||
>
|
||||
<Command
|
||||
ref="command"
|
||||
:metadata="
|
||||
findProductMetadata
|
||||
"
|
||||
v-model:modelValue="
|
||||
item.command
|
||||
"
|
||||
:actionType="
|
||||
item.actionType
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
@ -110,35 +259,118 @@
|
|||
</a-collapse>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-button type="dashed" style="width: 100%; margin-top: 10px" @click="addItem">
|
||||
<a-button
|
||||
type="dashed"
|
||||
style="width: 100%; margin-top: 10px"
|
||||
@click="addItem"
|
||||
>
|
||||
<AIcon
|
||||
type="PlusOutlined"
|
||||
style="margin-left: 2px;" />新增动作
|
||||
style="margin-left: 2px"
|
||||
/>新增动作
|
||||
</a-button>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<p style="margin-top: 20px">属性映射</p>
|
||||
<a-collapse v-if="modelRef.propertyMappings.length" :activeKey="modelRef.propertyMappings.map((_, _index) => _index)">
|
||||
<a-collapse-panel v-for="(item, index) in modelRef.propertyMappings" :key="index" :header="item.source ? getDuerOSProperties(item.source).find(i => i.id === item.source)?.name : `属性映射${index + 1}`">
|
||||
<template #extra><AIcon type="DeleteOutlined" @click="delPropertyItem(index)" /></template>
|
||||
<a-collapse
|
||||
v-if="modelRef.propertyMappings.length"
|
||||
:activeKey="propertyActiveKey"
|
||||
@change="onPropertyCollChange"
|
||||
>
|
||||
<a-collapse-panel
|
||||
v-for="(
|
||||
item, index
|
||||
) in modelRef.propertyMappings"
|
||||
:key="index"
|
||||
:header="
|
||||
item.source
|
||||
? getDuerOSProperties(
|
||||
item.source,
|
||||
).find(
|
||||
(i) =>
|
||||
i.id === item.source,
|
||||
)?.name
|
||||
: `属性映射${index + 1}`
|
||||
"
|
||||
>
|
||||
<template #extra
|
||||
><AIcon
|
||||
type="DeleteOutlined"
|
||||
@click="delPropertyItem(index)"
|
||||
/></template>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="DuerOS属性" :name="['propertyMappings', index, 'source']" :rules="{
|
||||
<a-form-item
|
||||
label="DuerOS属性"
|
||||
:name="[
|
||||
'propertyMappings',
|
||||
index,
|
||||
'source',
|
||||
]"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '请选择DuerOS属性',
|
||||
}">
|
||||
<a-select placeholder="请选择DuerOS属性" v-model:value="item.source" show-search :filter-option="filterOption">
|
||||
<a-select-option v-for="i in getDuerOSProperties(item.source)" :key="i.id" :value="i.id">{{i.name}}</a-select-option>
|
||||
message:
|
||||
'请选择DuerOS属性',
|
||||
}"
|
||||
>
|
||||
<a-select
|
||||
placeholder="请选择DuerOS属性"
|
||||
v-model:value="
|
||||
item.source
|
||||
"
|
||||
show-search
|
||||
:filter-option="
|
||||
filterOption
|
||||
"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="i in getDuerOSProperties(
|
||||
item.source,
|
||||
)"
|
||||
:key="i.id"
|
||||
:value="i.id"
|
||||
>{{
|
||||
i.name
|
||||
}}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="平台属性" :name="['propertyMappings', index, 'target']" :rules="{
|
||||
<a-form-item
|
||||
label="平台属性"
|
||||
:name="[
|
||||
'propertyMappings',
|
||||
index,
|
||||
'target',
|
||||
]"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '请选择平台属性',
|
||||
}">
|
||||
<a-select placeholder="请选择平台属性" v-model:value="item.target" mode="tags" show-search :filter-option="filterOption">
|
||||
<a-select-option v-for="i in getProductProperties(item.target)" :key="i.id" :value="item.id">{{i.name}}</a-select-option>
|
||||
message:
|
||||
'请选择平台属性',
|
||||
}"
|
||||
>
|
||||
<a-select
|
||||
placeholder="请选择平台属性"
|
||||
v-model:value="
|
||||
item.target
|
||||
"
|
||||
mode="tags"
|
||||
show-search
|
||||
:filter-option="
|
||||
filterOption
|
||||
"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="i in getProductProperties(
|
||||
item.target,
|
||||
)"
|
||||
:key="i.id"
|
||||
:value="item.id"
|
||||
>{{
|
||||
i.name
|
||||
}}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
@ -147,17 +379,26 @@
|
|||
</a-collapse>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-button type="dashed" style="width: 100%; margin-top: 10px" @click="addPropertyItem">
|
||||
<a-button
|
||||
type="dashed"
|
||||
style="width: 100%; margin-top: 10px"
|
||||
@click="addPropertyItem"
|
||||
>
|
||||
<AIcon
|
||||
type="PlusOutlined"
|
||||
style="margin-left: 2px;" />新增属性
|
||||
style="margin-left: 2px"
|
||||
/>新增属性
|
||||
</a-button>
|
||||
</a-col>
|
||||
<a-col :span="24" style="margin-top: 20px">
|
||||
<a-form-item label="说明" name="description" :rules="{
|
||||
<a-form-item
|
||||
label="说明"
|
||||
name="description"
|
||||
:rules="{
|
||||
max: 200,
|
||||
message: '最多输入200个字符',
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="modelRef.description"
|
||||
placeholder="请输入说明"
|
||||
|
@ -169,7 +410,12 @@
|
|||
</a-row>
|
||||
</a-form>
|
||||
<div v-if="type === 'edit'">
|
||||
<a-button :loading="loading" type="primary" @click="saveBtn">保存</a-button>
|
||||
<a-button
|
||||
:loading="loading"
|
||||
type="primary"
|
||||
@click="saveBtn"
|
||||
>保存</a-button
|
||||
>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
|
@ -181,9 +427,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Doc from './doc.vue'
|
||||
import Command from './command/index.vue'
|
||||
import { queryProductList, queryTypes, savePatch, detail } from '@/api/northbound/dueros'
|
||||
import Doc from './doc.vue';
|
||||
import Command from './command/index.vue';
|
||||
import {
|
||||
queryProductList,
|
||||
queryTypes,
|
||||
savePatch,
|
||||
detail,
|
||||
} from '@/api/northbound/dueros';
|
||||
import _ from 'lodash';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
|
@ -197,7 +448,8 @@ const modelRef = reactive({
|
|||
name: undefined,
|
||||
applianceType: undefined,
|
||||
productName: undefined,
|
||||
actionMappings: [{
|
||||
actionMappings: [
|
||||
{
|
||||
actionType: undefined,
|
||||
action: undefined,
|
||||
command: {
|
||||
|
@ -205,18 +457,42 @@ const modelRef = reactive({
|
|||
message: {
|
||||
properties: undefined,
|
||||
functionId: undefined,
|
||||
inputs: []
|
||||
}
|
||||
}
|
||||
}],
|
||||
propertyMappings: [{
|
||||
inputs: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
propertyMappings: [
|
||||
{
|
||||
source: undefined,
|
||||
target: []
|
||||
}],
|
||||
description: undefined
|
||||
target: [],
|
||||
},
|
||||
],
|
||||
description: undefined,
|
||||
});
|
||||
|
||||
const productList = ref<Record<string, any>[]>([]);
|
||||
const typeList = ref<Record<string, any>[]>([]);
|
||||
const command = ref([]);
|
||||
const loading = ref<boolean>(false);
|
||||
const type = ref<'edit' | 'view'>('edit');
|
||||
const actionActiveKey = ref<string[]>(['0']);
|
||||
const propertyActiveKey = ref<string[]>(['0']);
|
||||
|
||||
const onPropertyCollChange = (_key: string[]) => {
|
||||
propertyActiveKey.value = _key;
|
||||
};
|
||||
|
||||
const onActionCollChange = (_key: string[]) => {
|
||||
actionActiveKey.value = _key;
|
||||
};
|
||||
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
const addItem = () => {
|
||||
actionActiveKey.value.push(String(modelRef.actionMappings.length));
|
||||
modelRef.actionMappings.push({
|
||||
actionType: undefined,
|
||||
action: undefined,
|
||||
|
@ -225,125 +501,133 @@ const addItem = () => {
|
|||
message: {
|
||||
properties: undefined,
|
||||
functionId: undefined,
|
||||
inputs: []
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const productList = ref<Record<string, any>[]>([])
|
||||
const typeList = ref<Record<string, any>[]>([])
|
||||
const command = ref([])
|
||||
const loading = ref<boolean>(false)
|
||||
const type = ref<'edit' | 'view'>('view')
|
||||
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
inputs: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const delItem = (index: number) => {
|
||||
modelRef.actionMappings.splice(index, 1)
|
||||
}
|
||||
modelRef.actionMappings.splice(index, 1);
|
||||
};
|
||||
|
||||
const addPropertyItem = () => {
|
||||
propertyActiveKey.value.push(String(modelRef.propertyMappings.length));
|
||||
modelRef.propertyMappings.push({
|
||||
source: undefined,
|
||||
target: []
|
||||
})
|
||||
}
|
||||
target: [],
|
||||
});
|
||||
};
|
||||
|
||||
const delPropertyItem = (index: number) => {
|
||||
modelRef.propertyMappings.splice(index, 1)
|
||||
}
|
||||
modelRef.propertyMappings.splice(index, 1);
|
||||
};
|
||||
|
||||
const productChange = (value: string) => {
|
||||
modelRef.propertyMappings = modelRef.propertyMappings.map(item => {
|
||||
return {source: item.source, target: []}
|
||||
})
|
||||
const item = productList.value.find(item => item.id === value)
|
||||
modelRef.propertyMappings = modelRef.propertyMappings.map((item) => {
|
||||
return { source: item.source, target: [] };
|
||||
});
|
||||
const item = productList.value.find((item) => item.id === value);
|
||||
if (item) {
|
||||
modelRef.productName = item.name
|
||||
}
|
||||
modelRef.productName = item.name;
|
||||
}
|
||||
};
|
||||
|
||||
const typeChange = () => {
|
||||
modelRef.propertyMappings = modelRef.propertyMappings.map(item => {
|
||||
return {source: undefined, target: item.target}
|
||||
})
|
||||
modelRef.actionMappings = modelRef.actionMappings.map(item => {
|
||||
return {...item, action: undefined}
|
||||
})
|
||||
}
|
||||
modelRef.propertyMappings = modelRef.propertyMappings.map((item) => {
|
||||
return { source: undefined, target: item.target };
|
||||
});
|
||||
modelRef.actionMappings = modelRef.actionMappings.map((item) => {
|
||||
return { ...item, action: undefined };
|
||||
});
|
||||
};
|
||||
|
||||
const findApplianceType = computed(() => {
|
||||
if(!modelRef.applianceType) return
|
||||
return typeList.value.find(item => item.id === modelRef.applianceType)
|
||||
})
|
||||
if (!modelRef.applianceType) return;
|
||||
return typeList.value.find((item) => item.id === modelRef.applianceType);
|
||||
});
|
||||
|
||||
const findProductMetadata = computed(() => {
|
||||
if(!modelRef.id) return
|
||||
const _product = productList.value?.find((item: any) => item.id === modelRef.id)
|
||||
return _product?.metadata && JSON.parse(_product.metadata || '{}')
|
||||
})
|
||||
if (!modelRef.id) return;
|
||||
const _product = productList.value?.find(
|
||||
(item: any) => item.id === modelRef.id,
|
||||
);
|
||||
return _product?.metadata && JSON.parse(_product.metadata || '{}');
|
||||
});
|
||||
|
||||
// 查询产品列表
|
||||
const getProduct = async (id?: string) => {
|
||||
const resp = await queryProductList(id)
|
||||
const resp = await queryProductList(id);
|
||||
if (resp.status === 200) {
|
||||
productList.value = (resp?.result as Record<string, any>[])
|
||||
}
|
||||
productList.value = resp?.result as Record<string, any>[];
|
||||
}
|
||||
};
|
||||
|
||||
const getTypes = async () => {
|
||||
const resp = await queryTypes()
|
||||
const resp = await queryTypes();
|
||||
if (resp.status === 200) {
|
||||
typeList.value = (resp?.result as Record<string, any>[])
|
||||
}
|
||||
typeList.value = resp?.result as Record<string, any>[];
|
||||
}
|
||||
};
|
||||
|
||||
const getDuerOSProperties = (val: string) => {
|
||||
const arr = modelRef.propertyMappings.map(item => item?.source) || []
|
||||
const checked = _.cloneDeep(arr)
|
||||
const _index = checked.findIndex(i => i === val)
|
||||
const arr = modelRef.propertyMappings.map((item) => item?.source) || [];
|
||||
const checked = _.cloneDeep(arr);
|
||||
const _index = checked.findIndex((i) => i === val);
|
||||
// 去掉重复的
|
||||
checked.splice(_index, 1)
|
||||
checked.splice(_index, 1);
|
||||
const targetList = findApplianceType.value?.properties;
|
||||
const list = targetList?.filter((i: {id: string}) => !checked.includes(i?.id as any))
|
||||
return list || []
|
||||
}
|
||||
const list = targetList?.filter(
|
||||
(i: { id: string }) => !checked.includes(i?.id as any),
|
||||
);
|
||||
return list || [];
|
||||
};
|
||||
|
||||
const getProductProperties = (val: string[]) => {
|
||||
const items = modelRef.propertyMappings.map((item: {target: string[]}) => item?.target.map(j => j)) || []
|
||||
const checked = _.flatMap(items)
|
||||
const _checked: any[] = []
|
||||
checked.map(_item => {
|
||||
const items =
|
||||
modelRef.propertyMappings.map((item: { target: string[] }) =>
|
||||
item?.target.map((j) => j),
|
||||
) || [];
|
||||
const checked = _.flatMap(items);
|
||||
const _checked: any[] = [];
|
||||
checked.map((_item) => {
|
||||
if (!val.includes(_item)) {
|
||||
_checked.push(_item)
|
||||
}
|
||||
})
|
||||
const sourceList = findProductMetadata.value?.properties
|
||||
const list = sourceList?.filter((i: { id: string }) => !_checked.includes(i.id))
|
||||
return list || []
|
||||
_checked.push(_item);
|
||||
}
|
||||
});
|
||||
const sourceList = findProductMetadata.value?.properties;
|
||||
const list = sourceList?.filter(
|
||||
(i: { id: string }) => !_checked.includes(i.id),
|
||||
);
|
||||
return list || [];
|
||||
};
|
||||
|
||||
const getTypesActions = (val: string) => {
|
||||
const items = modelRef.actionMappings.map((item) => item?.action) || []
|
||||
const checked = _.cloneDeep(items)
|
||||
const _index = checked.findIndex(i => i === val)
|
||||
checked.splice(_index, 1)
|
||||
const actionsList = findApplianceType.value?.actions || []
|
||||
const list = actionsList?.filter((i: { id: string, name: string }) => !checked.includes(i?.id as any))
|
||||
return list || []
|
||||
}
|
||||
const items = modelRef.actionMappings.map((item) => item?.action) || [];
|
||||
const checked = _.cloneDeep(items);
|
||||
const _index = checked.findIndex((i) => i === val);
|
||||
checked.splice(_index, 1);
|
||||
const actionsList = findApplianceType.value?.actions || [];
|
||||
const list = actionsList?.filter(
|
||||
(i: { id: string; name: string }) => !checked.includes(i?.id as any),
|
||||
);
|
||||
return list || [];
|
||||
};
|
||||
const saveBtn = async () => {
|
||||
const tasks = []
|
||||
const tasks: any[] = [];
|
||||
for (let i = 0; i < command.value.length; i++) {
|
||||
const res = await (command.value[i] as any)?.saveBtn()
|
||||
tasks.push(res)
|
||||
if(!res) break
|
||||
if(!res || (res?.errorFields && res.errorFields.length)) {
|
||||
actionActiveKey.value.push(String(i));
|
||||
tasks.push(false);
|
||||
} else {
|
||||
tasks.push(res);
|
||||
}
|
||||
const data = await formRef.value.validate()
|
||||
if(tasks.every(item => item) && data){
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(async (data: any) => {
|
||||
if (tasks.every((item) => item) && data) {
|
||||
loading.value = true;
|
||||
const resp = await savePatch(toRaw(modelRef));
|
||||
loading.value = false;
|
||||
|
@ -353,33 +637,46 @@ const saveBtn = async () => {
|
|||
router.push('/iot/northbound/DuerOS/');
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.catch((err: any) => {
|
||||
const _arr = err.errorFields.map((item: any) => item.name);
|
||||
_arr.map((item: string | any[]) => {
|
||||
if (item.length >= 3) {
|
||||
if(item[0] === 'propertyMappings' && !propertyActiveKey.value.includes(item[1])){
|
||||
propertyActiveKey.value.push(item[1]);
|
||||
}
|
||||
if(item[0] === 'actionMappings' && !actionActiveKey.value.includes(item[1])){
|
||||
actionActiveKey.value.push(item[1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
watch(
|
||||
() => route.params?.id,
|
||||
async (newId) => {
|
||||
if (newId) {
|
||||
getProduct(newId as string)
|
||||
getTypes()
|
||||
getProduct(newId as string);
|
||||
getTypes();
|
||||
if (newId === ':id') return;
|
||||
const resp = await detail(newId as string)
|
||||
const resp = await detail(newId as string);
|
||||
const _data: any = resp.result;
|
||||
if (_data) {
|
||||
_data.applianceType = _data?.applianceType?.value;
|
||||
}
|
||||
Object.assign(modelRef, _data)
|
||||
Object.assign(modelRef, _data);
|
||||
}
|
||||
},
|
||||
{immediate: true, deep: true}
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route.query.type,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
type.value = newVal as 'edit' | 'view'
|
||||
type.value = newVal as 'edit' | 'view';
|
||||
}
|
||||
},
|
||||
{immediate: true, deep: true}
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
</script>
|
|
@ -1,6 +1,10 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<Search :columns="columns" target="northbound-dueros" :params="params" />
|
||||
<Search
|
||||
:columns="columns"
|
||||
target="northbound-dueros"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<JTable
|
||||
ref="instanceRef"
|
||||
:columns="columns"
|
||||
|
@ -22,16 +26,12 @@
|
|||
:statusText="slotProps.state?.text"
|
||||
:statusNames="{
|
||||
enabled: 'success',
|
||||
disabled: 'error'
|
||||
disabled: 'error',
|
||||
}"
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img
|
||||
:src="
|
||||
getImage('/cloud/dueros.png')
|
||||
"
|
||||
/>
|
||||
<img :src="getImage('/cloud/dueros.png')" />
|
||||
</slot>
|
||||
</template>
|
||||
<template #content>
|
||||
|
@ -43,9 +43,7 @@
|
|||
</h3>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
产品
|
||||
</div>
|
||||
<div class="card-item-content-text">产品</div>
|
||||
<div>{{ slotProps?.productName }}</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
|
@ -145,12 +143,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
query,
|
||||
_undeploy,
|
||||
_deploy,
|
||||
_delete
|
||||
} from '@/api/northbound/dueros';
|
||||
import { query, _undeploy, _deploy, _delete, queryProductList, queryTypes } from '@/api/northbound/dueros';
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
@ -169,17 +162,48 @@ const columns = [
|
|||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '产品名称',
|
||||
dataIndex: 'productName',
|
||||
key: 'productName',
|
||||
search: {
|
||||
type: 'select',
|
||||
options: () =>
|
||||
new Promise((resolve) => {
|
||||
queryProductList().then((resp: any) => {
|
||||
resolve(
|
||||
resp.result.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '设备类型',
|
||||
dataIndex: 'applianceType',
|
||||
key: 'applianceType',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: () =>
|
||||
new Promise((resolve) => {
|
||||
queryTypes().then((resp: any) => {
|
||||
resolve(
|
||||
resp.result.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
|
@ -191,6 +215,13 @@ const columns = [
|
|||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '正常', value: 'enabled' },
|
||||
{ label: '禁用', value: 'disabled' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
@ -216,8 +247,8 @@ const handleView = (id: string) => {
|
|||
router.push({
|
||||
path: '/iot/northbound/DuerOS/detail/' + id,
|
||||
query: {
|
||||
type: 'view'
|
||||
}
|
||||
type: 'view',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -249,8 +280,8 @@ const getActions = (
|
|||
router.push({
|
||||
path: '/iot/northbound/DuerOS/detail/' + data.id,
|
||||
query: {
|
||||
type: 'edit'
|
||||
}
|
||||
type: 'edit',
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -313,4 +344,8 @@ const getActions = (
|
|||
return actions.filter((i: ActionsType) => i.key !== 'view');
|
||||
return actions;
|
||||
};
|
||||
|
||||
const handleSearch = (_params: any) => {
|
||||
params.value = _params;
|
||||
};
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue