Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev

This commit is contained in:
JiangQiming 2023-03-09 15:16:53 +08:00
commit c5018e631a
56 changed files with 8262 additions and 7276 deletions

View File

@ -2,36 +2,36 @@
<div class="indicator-box">
<template v-if="['int', 'long', 'double', 'float'].includes(type)">
<template v-if="value.range">
<a-input-number v-model:value="value.value[0]" :max="value.value[1]" size="small"
style="width: 100%;"></a-input-number>
<j-input-number v-model:value="value.value[0]" :max="value.value[1]" size="small"
style="width: 100%;"></j-input-number>
~
<a-input-number v-model:value="value.value[1]" :min="value.value[0]" size="small"
style="width: 100%;"></a-input-number>
<j-input-number v-model:value="value.value[1]" :min="value.value[0]" size="small"
style="width: 100%;"></j-input-number>
</template>
<a-input-number v-else v-model:value="value.value" size="small" style="width: 100%;"></a-input-number>
<j-input-number v-else v-model:value="value.value" size="small" style="width: 100%;"></j-input-number>
</template>
<template v-else-if="type === 'date'">
<a-range-picker v-if="value.range" show-time v-model:value="value.value" size="small" />
<a-date-picker v-else show-time v-model:value="value.value" size="small" />
<j-range-picker v-if="value.range" show-time v-model:value="value.value" size="small" />
<j-date-picker v-else show-time v-model:value="value.value" size="small" />
</template>
<template v-else-if="type === 'boolean'">
<a-select v-model:value="value.value[0]" :options="list" size="small" placeholder="请选择"></a-select>
<j-select v-model:value="value.value[0]" :options="list" size="small" placeholder="请选择"></j-select>
</template>
<template v-else-if="type === 'string'">
<a-input v-model:value="value.value" size="small" placeholder="请输入"></a-input>
<j-input v-model:value="value.value" size="small" placeholder="请输入"></j-input>
</template>
<template v-else>
<template v-if="value.range">
<a-input v-model:value="value.value[0]" :max="value.value[1]" size="small" placeholder="请输入"></a-input>
<j-input v-model:value="value.value[0]" :max="value.value[1]" size="small" placeholder="请输入"></j-input>
~
<a-input v-model:value="value.value[1]" :min="value.value[0]" size="small" placeholder="请输入"></a-input>
<j-input v-model:value="value.value[1]" :min="value.value[0]" size="small" placeholder="请输入"></j-input>
</template>
<a-input-number v-else v-model:value="value.value" size="small" placeholder="请输入"></a-input-number>
<j-input-number v-else v-model:value="value.value" size="small" placeholder="请输入"></j-input-number>
</template>
<div v-if="type !== 'boolean' && type !== 'string'">
<a-checkbox style="min-width: 60px; margin-left: 5px;" v-model:checked="value.range" @change="changeChecked">
<j-checkbox style="min-width: 60px; margin-left: 5px;" v-model:checked="value.range" @change="changeChecked">
范围
</a-checkbox>
</j-checkbox>
</div>
</div>
</template>

View File

@ -1,5 +1,5 @@
<template>
<a-popover :visible="visible" placement="left">
<j-popover :visible="visible" placement="left">
<template #title>
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="width: 150px;">配置元素</div>
@ -10,19 +10,19 @@
<div style="max-width: 400px;">
<div class="ant-form-vertical">
<value-type-form v-model:value="_value" :name="name" isSub key="sub"></value-type-form>
<a-form-item label="说明" :name="name.concat(['description'])" :rules="[
<j-form-item label="说明" :name="name.concat(['description'])" :rules="[
{ max: 200, message: '最多可输入200个字符' },
]">
<a-textarea v-model:value="_value.description" size="small"></a-textarea>
</a-form-item>
<j-textarea v-model:value="_value.description" size="small"></j-textarea>
</j-form-item>
</div>
</div>
</template>
<a-button type="dashed" block @click="visible = true">
<j-button type="dashed" block @click="visible = true">
配置元素
<AIcon type="EditOutlined" class="item-icon" />
</a-button>
</a-popover>
</j-button>
</j-popover>
</template>
<script setup lang="ts" name="ArrayParam">
import ValueTypeForm from '@/views/device/components/Metadata/Base/Edit/ValueTypeForm.vue';
@ -33,7 +33,7 @@ type ValueType = Record<any, any>;
const props = defineProps({
value: {
type: Object as PropType<ValueType>,
default: () => ({ extends: {} })
default: () => ({ expands: {} })
},
name: {
type: Array as PropType<(string | number)[]>,

View File

@ -1,35 +1,35 @@
<template>
<div class="boolean-param">
<a-row :gutter="4">
<a-col :span="12">
<a-form-item label=" " :name="name.concat(['trueText'])" :rules="[
<j-row :gutter="4">
<j-col :span="12">
<j-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="[
<j-input v-model:value="value.trueText" placeholder="trueText" size="small" />
</j-form-item>
</j-col>
<j-col :span="12">
<j-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="[
<j-input v-model:value="value.trueValue" placeholder="trueValue" size="small"/>
</j-form-item>
</j-col>
<j-col :span="12">
<j-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="[
<j-input v-model:value="value.falseText" placeholder="falseText" size="small" />
</j-form-item>
</j-col>
<j-col :span="12">
<j-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>
<j-input v-model:value="value.falseValue" placeholder="falseValue" size="small" />
</j-form-item>
</j-col>
</j-row>
</div>
</template>
<script setup lang="ts" name="BooleanParam">

View File

@ -1,5 +1,5 @@
<template>
<a-popover placement="left" trigger="click">
<j-popover placement="left" trigger="click">
<template #title>
<div class="edit-title" style="display: flex; justify-content: space-between; align-items: center;">
<div style="width: 150px;">{{ config.name }}</div>
@ -7,18 +7,18 @@
</template>
<template #content>
<div style="max-width: 400px;" class="ant-form-vertical">
<a-form-item v-for="item in config.properties" :name="name.concat([item.property])" :label="item.name">
<a-select v-model:value="value[item.property]" :options="item.type?.elements?.map((e: { 'text': string, 'value': string }) => ({
<j-form-item v-for="item in config.properties" :name="name.concat([item.property])" :label="item.name">
<j-select v-model:value="value[item.property]" :options="item.type?.elements?.map((e: { 'text': string, 'value': string }) => ({
label: e.text,
value: e.value,
}))" size="small"></a-select>
</a-form-item>
}))" size="small"></j-select>
</j-form-item>
</div>
</template>
<a-button type="dashed" block>
<j-button type="dashed" block>
存储配置<AIcon type="EditOutlined" class="item-icon"/>
</a-button>
</a-popover>
</j-button>
</j-popover>
</template>
<script setup lang="ts" name="ConfigParam">
import { PropType } from 'vue';

View File

@ -5,7 +5,7 @@
<AIcon type="MenuOutlined" class="item-drag item-icon" />
</div>
<div class="item-middle item-editable">
<a-popover :visible="editIndex === index" placement="top">
<j-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>
@ -14,34 +14,34 @@
</template>
<template #content>
<div class="ant-form-vertical">
<a-form-item label="Value" :name="name.concat([index, 'value'])" :rules="[
<j-form-item label="Value" :name="name.concat([index, '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="name.concat([index, 'text'])" :rules="[
<j-input v-model:value="_value[index].value" size="small"></j-input>
</j-form-item>
<j-form-item label="Text" :name="name.concat([index, 'text'])" :rules="[
{ required: true, message: '请输入Text' },
]">
<a-input v-model:value="_value[index].text" size="small"></a-input>
</a-form-item>
<j-input v-model:value="_value[index].text" size="small"></j-input>
</j-form-item>
</div>
</template>
<div class="item-edit" @click="handleEdit(index)">
{{ item.text || '枚举项配置' }}
<AIcon type="EditOutlined" class="item-icon" />
</div>
</a-popover>
</j-popover>
</div>
<div class="item-right">
<AIcon type="DeleteOutlined" @click="handleDelete(index)" />
</div>
</div>
<a-button type="dashed" block @click="handleAdd">
<j-button type="dashed" block @click="handleAdd">
<template #icon>
<AIcon type="PlusOutlined" class="item-icon" />
</template>
新增枚举型
</a-button>
</j-button>
</div>
</template>
<script setup lang="ts" name="BooleanParam">

View File

@ -5,7 +5,7 @@
<AIcon type="MenuOutlined" class="item-drag item-icon" />
</div>
<div class="item-middle item-editable">
<a-popover :visible="editIndex === index" placement="left">
<j-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>
@ -14,7 +14,7 @@
</template>
<template #content>
<div style="max-width: 400px;" class="ant-form-vertical">
<a-form-item label="标识" :name="name.concat([index, 'id'])" :rules="[
<j-form-item label="标识" :name="name.concat([index, 'id'])" :rules="[
{ required: true, message: '请输入标识' },
{ max: 64, message: '最多可输入64个字符' },
{
@ -22,14 +22,14 @@
message: 'ID只能由数字、字母、下划线、中划线组成',
},
]">
<a-input v-model:value="_value[index].id" size="small"></a-input>
</a-form-item>
<a-form-item label="名称" :name="name.concat([index, 'name'])" :rules="[
<j-input v-model:value="_value[index].id" size="small"></j-input>
</j-form-item>
<j-form-item label="名称" :name="name.concat([index, 'name'])" :rules="[
{ required: true, message: '请输入名称' },
{ max: 64, message: '最多可输入64个字符' },
]">
<a-input v-model:value="_value[index].name" size="small"></a-input>
</a-form-item>
<j-input v-model:value="_value[index].name" size="small"></j-input>
</j-form-item>
<value-type-form v-model:value="_value[index].valueType" :name="name.concat([index, 'valueType'])" isSub
key="json_sub"></value-type-form>
</div>
@ -38,18 +38,18 @@
{{ item.name || '配置参数' }}
<AIcon type="EditOutlined" class="item-icon" />
</div>
</a-popover>
</j-popover>
</div>
<div class="item-right">
<AIcon type="DeleteOutlined" @click="handleDelete(index)" />
</div>
</div>
<a-button type="dashed" block @click="handleAdd">
<j-button type="dashed" block @click="handleAdd">
<template #icon>
<AIcon type="PlusOutlined" class="item-icon" />
</template>
添加参数
</a-button>
</j-button>
</div>
</template>
<script setup lang="ts" name="JsonParam">

View File

@ -6,7 +6,7 @@
{{ `#${index + 1}.` }}
</div>
<div class="item-middle item-editable">
<a-popover :visible="editIndex === index" placement="top" @visible-change="change" trigger="click">
<j-popover :visible="editIndex === index" placement="top" @visible-change="change" trigger="click">
<template #title>
<div class="edit-title" style="display: flex; justify-content: space-between; align-items: center;">
<div style="width: 150px;">配置参数</div>
@ -16,7 +16,7 @@
<template #content>
<div>
<div class="ant-form-vertical">
<a-form-item label="标识" :name="name.concat([index, 'id'])" :rules="[
<j-form-item label="标识" :name="name.concat([index, 'id'])" :rules="[
{ required: true, message: '请输入标识' },
{ max: 64, message: '最多可输入64个字符' },
{
@ -24,20 +24,19 @@
message: 'ID只能由数字、字母、下划线、中划线组成',
},
]">
<a-input v-model:value="_value[index].id" size="small"></a-input>
</a-form-item>
<a-form-item label="名称" :name="name.concat([index, 'name'])" :rules="[
<j-input v-model:value="_value[index].id" size="small"></j-input>
</j-form-item>
<j-form-item label="名称" :name="name.concat([index, 'name'])" :rules="[
{ required: true, message: '请输入名称' },
{ max: 64, message: '最多可输入64个字符' },
]">
<a-input v-model:value="_value[index].name" size="small"></a-input>
</a-form-item>
<a-form-item label="指标值" :name="name.concat([index, 'value'])" :rules="[
{ required: true, message: '请输入指标值' },
{ validator: () => validateIndicator(_value[index]), message: '请输入指标值' }
<j-input v-model:value="_value[index].name" size="small"></j-input>
</j-form-item>
<j-form-item label="指标值" :name="name.concat([index, 'value'])" :rules="[
{ required: true, validator: () => validateIndicator(_value[index]), message: '请输入指标值' }
]">
<JIndicators v-model:value="_value[index]" :type="type" size="small" :enum="enum" />
</a-form-item>
</j-form-item>
</div>
</div>
</template>
@ -45,18 +44,18 @@
{{ item.name || '配置参数' }}
<AIcon type="EditOutlined" class="item-icon" />
</div>
</a-popover>
</j-popover>
</div>
<div class="item-right">
<AIcon type="DeleteOutlined" @click="handleDelete(index)" />
</div>
</div>
<a-button type="dashed" block @click="handleAdd">
<j-button type="dashed" block @click="handleAdd">
<template #icon>
<AIcon type="PlusOutlined" class="item-icon" />
</template>
添加指标
</a-button>
</j-button>
</div>
</template>
<script setup lang="ts" name="MetricsParam">

View File

@ -1,33 +1,33 @@
<template>
<a-form-item :name="name.concat(['script'])">
<j-form-item :name="name.concat(['script'])">
<f-rule-editor v-model:value="value.script" :id="id"></f-rule-editor>
</a-form-item>
</j-form-item>
<template v-if="showWindow">
<a-form-item label="规则配置" :name="name.concat(['isVirtualRule'])">
<a-switch v-model:checked="value.isVirtualRule" :checked-value="true" :un-checked-value="false"
@change="changeWindow"></a-switch>
</a-form-item>
<j-form-item label="规则配置" :name="name.concat(['isVirtualRule'])">
<j-switch v-model:checked="value.isVirtualRule" :checked-value="true" :un-checked-value="false"
@change="changeWindow"></j-switch>
</j-form-item>
<template v-if="value.isVirtualRule">
<a-form-item label="窗口" :name="name.concat(['windowType'])" :rules="[
<j-form-item label="窗口" :name="name.concat(['windowType'])" :rules="[
{ required: true, message: '请选择窗口' },
]">
<a-select v-model:value="value.windowType" :options="windowTypeEnum" size="small" allow-clear></a-select>
</a-form-item>
<a-form-item label="聚合函数" :name="name.concat(['aggType'])" :rules="[
<j-select v-model:value="value.windowType" :options="windowTypeEnum" size="small" allow-clear></j-select>
</j-form-item>
<j-form-item label="聚合函数" :name="name.concat(['aggType'])" :rules="[
{ required: true, message: '请选择聚合函数' },
]">
<a-select v-model:value="value.aggType" :options="aggTypeOptions" size="small" allow-clear></a-select>
</a-form-item>
<a-form-item :label="spanLabel" :name="name.concat(['window', 'span'])" :rules="[
<j-select v-model:value="value.aggType" :options="aggTypeOptions" size="small" allow-clear></j-select>
</j-form-item>
<j-form-item :label="spanLabel" :name="name.concat(['window', 'span'])" :rules="[
{ required: true, message: '请输入窗口长度' },
]">
<a-input-number v-model:value="value.window.span" size="small" style="width: 100%;"></a-input-number>
</a-form-item>
<a-form-item :label="everyLabel" :name="name.concat(['window', 'every'])" :rules="[
<j-input-number v-model:value="value.window.span" size="small" style="width: 100%;"></j-input-number>
</j-form-item>
<j-form-item :label="everyLabel" :name="name.concat(['window', 'every'])" :rules="[
{ required: true, message: '请输入步长' },
]">
<a-input-number v-model:value="value.window.every" size="small" style="width: 100%;"></a-input-number>
</a-form-item>
<j-input-number v-model:value="value.window.every" size="small" style="width: 100%;"></j-input-number>
</j-form-item>
</template>
</template>
</template>

View File

@ -29,7 +29,7 @@
v-model:value="myValue"
>
<template #addonAfter>
<form-outlined @click="modalVis = true" />
<AIcon type="FormOutlined" @click="modalVis = true" />
</template>
</j-input>
<GeoComponent
@ -50,7 +50,7 @@
:showUploadList="false"
@change="handleFileChange"
>
<cloud-upload-outlined />
<AIcon type="CloudUploadOutlined" />
</j-upload>
</template>
</j-input>

View File

@ -15,6 +15,7 @@ import Ellipsis from './Ellipsis/index.vue'
import JEmpty from './Empty/index.vue'
import AMapComponent from './AMapComponent/index.vue'
import PathSimplifier from './AMapComponent/PathSimplifier.vue'
import ValueItem from './ValueItem/index.vue'
export default {
install(app: App) {
@ -35,5 +36,6 @@ export default {
.component('JEmpty', JEmpty)
.component('AMapComponent', AMapComponent)
.component('PathSimplifier', PathSimplifier)
.component('ValueItem', ValueItem)
}
}

3
src/global.d.ts vendored
View File

@ -11,4 +11,5 @@ declare module '*.bmp';
declare module '*.js';
declare module '*.ts';
declare module 'js-cookie';
declare module 'jetlinks-ui-components';
declare module 'jetlinks-ui-components';
declare module 'vue3-json-viewer';

View File

@ -67,10 +67,10 @@ export default [
path: '/form',
component: () => import('@/views/demo/Form.vue')
},
{
path: '/system/Api',
component: () => import('@/views/system/Platforms/index.vue')
},
// {
// path: '/system/Api',
// component: () => import('@/views/system/Platforms/index.vue')
// },
// end: 测试用, 可删除
// 初始化

View File

@ -16,7 +16,7 @@
在特定场景下设备无法直接接入阿里云物联网平台时您可先将设备接入物联网平台再使用阿里云云云对接SDK快速构建桥接服务搭建物联网平台与阿里云物联网平台的双向数据通道
</div>
<div class="image">
<a-image width="100%" :src="getImage('/northbound/aliyun2.png')" />
<j-image width="100%" :src="getImage('/northbound/aliyun2.png')" />
</div>
<h1>2.配置说明</h1>
<div>
@ -26,14 +26,14 @@
</div>
<div>获取路径阿里云物联网平台--服务地址</div>
<div class="image">
<a-image width="100%" :src="getImage('/northbound/aliyun3.png')" />
<j-image width="100%" :src="getImage('/northbound/aliyun3.png')" />
</div>
<h2> 2AccesskeyID/Secret</h2>
<div>
用于程序通知方式调用云服务费API的用户标识和秘钥获取路径阿里云管理控制台--用户头像----AccessKey管理--查看
</div>
<div class="image">
<a-image width="100%" :src="getImage('/northbound/aliyun1.jpg')" />
<j-image width="100%" :src="getImage('/northbound/aliyun1.jpg')" />
</div>
<h2> 3. 网桥产品</h2>
<div>
@ -44,7 +44,7 @@
将阿里云物联网平台中的产品实例与物联网平台的产品实例进行关联关联后需要进入该产品下的每一个设备的实例信息页填入对应的阿里云物联网平台设备的DeviceNameDeviceSecret进行一对一绑定
</div>
<div class="image">
<a-image width="100%" :src="getImage('/northbound/aliyun4.png')" />
<j-image width="100%" :src="getImage('/northbound/aliyun4.png')" />
</div>
</div>
</div>

View File

@ -1,17 +1,17 @@
<template>
<page-container>
<a-card>
<a-row :gutter="24">
<a-col :span="16">
<j-card>
<j-row :gutter="24">
<j-col :span="16">
<TitleComponent data="基本信息" />
<a-form
<j-form
:layout="'vertical'"
ref="formRef"
:model="modelRef"
>
<a-row :gutter="24">
<a-col :span="24">
<a-form-item
<j-row :gutter="24">
<j-col :span="24">
<j-form-item
label="名称"
name="name"
:rules="[
@ -25,14 +25,14 @@
},
]"
>
<a-input
<j-input
placeholder="请输入名称"
v-model:value="modelRef.name"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item
:name="['accessConfig', 'regionId']"
:rules="[
{
@ -44,63 +44,62 @@
<template #label>
<span>
服务地址
<a-tooltip
<j-tooltip
title="阿里云内部给每台机器设置的唯一编号"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</j-tooltip>
</span>
</template>
<a-select
<j-select
placeholder="请选择服务地址"
v-model:value="
modelRef.accessConfig.regionId
"
show-search
:filter-option="filterOption"
@blur="productChange"
>
<a-select-option
<j-select-option
v-for="item in regionsList"
:key="item.id"
:value="item.id"
:label="item.name"
>{{ item.name }}</a-select-option
>{{ item.name }}</j-select-option
>
</a-select>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
</j-select>
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item
:name="['accessConfig', 'instanceId']"
>
<template #label>
<span>
实例ID
<a-tooltip
<j-tooltip
title="阿里云物联网平台中的实例ID,没有则不填"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</j-tooltip>
</span>
</template>
<a-input
<j-input
placeholder="请输入实例ID"
v-model:value="
modelRef.accessConfig.instanceId
"
@blur="productChange"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item
:name="['accessConfig', 'accessKeyId']"
:rules="[
{
@ -116,27 +115,27 @@
<template #label>
<span>
accessKey
<a-tooltip
<j-tooltip
title="用于程序通知方式调用云服务API的用户标识"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</j-tooltip>
</span>
</template>
<a-input
<j-input
placeholder="请输入accessKey"
v-model:value="
modelRef.accessConfig.accessKeyId
"
@blur="productChange"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item
:name="['accessConfig', 'accessSecret']"
:rules="[
{
@ -152,27 +151,27 @@
<template #label>
<span>
accessSecret
<a-tooltip
<j-tooltip
title="用于程序通知方式调用云服务费API的秘钥标识"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</j-tooltip>
</span>
</template>
<a-input
<j-input
placeholder="请输入accessSecret"
v-model:value="
modelRef.accessConfig.accessSecret
"
@blur="productChange"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item
name="bridgeProductKey"
:rules="{
required: true,
@ -182,44 +181,43 @@
<template #label>
<span>
网桥产品
<a-tooltip
<j-tooltip
title="物联网平台对应的阿里云产品"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</j-tooltip>
</span>
</template>
<a-select
<j-select
placeholder="请选择网桥产品"
v-model:value="
modelRef.bridgeProductKey
"
show-search
:filter-option="filterOption"
>
<a-select-option
<j-select-option
v-for="item in aliyunProductList"
:key="item.productKey"
:value="item.productKey"
:label="item.productName"
>{{
item.productName
}}</a-select-option
}}</j-select-option
>
</a-select>
</a-form-item>
</a-col>
<a-col :span="24">
</j-select>
</j-form-item>
</j-col>
<j-col :span="24">
<p>产品映射</p>
<a-collapse
<j-collapse
v-if="modelRef.mappings.length"
:activeKey="activeKey"
@change="onCollChange"
>
<a-collapse-panel
<j-collapse-panel
v-for="(
item, index
) in modelRef.mappings"
@ -239,9 +237,9 @@
type="DeleteOutlined"
@click="delItem(index)"
/></template>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item
<j-row :gutter="24">
<j-col :span="12">
<j-form-item
label="阿里云产品"
:name="[
'mappings',
@ -254,19 +252,16 @@
'请选择阿里云产品',
}"
>
<a-select
<j-select
placeholder="请选择阿里云产品"
v-model:value="
item.productKey
"
show-search
:filter-option="
filterOption
"
>
<a-select-option
<j-select-option
v-for="i in getAliyunProductList(
item.productKey,
item?.productKey || ''
)"
:key="i.productKey"
:value="
@ -277,13 +272,13 @@
"
>{{
i.productName
}}</a-select-option
}}</j-select-option
>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
</j-select>
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="平台产品"
:name="[
'mappings',
@ -296,36 +291,36 @@
'请选择平台产品',
}"
>
<a-select
<j-select
placeholder="请选择平台产品"
v-model:value="
item.productId
"
show-search
:filter-option="
filterOption
"
>
<a-select-option
<j-select-option
v-for="i in getPlatProduct(
item.productId,
item.productId || ''
)"
:key="i.id"
:value="item.id"
:value="i?.id"
:label="i.name"
>{{
i.name
}}</a-select-option
}}</j-select-option
>
</a-select>
</a-form-item>
</a-col>
</a-row>
</a-collapse-panel>
</a-collapse>
</a-col>
<a-col :span="24">
<a-button
</j-select>
</j-form-item>
</j-col>
</j-row>
</j-collapse-panel>
</j-collapse>
<j-card v-else>
<j-empty />
</j-card>
</j-col>
<j-col :span="24">
<j-button
type="dashed"
style="width: 100%; margin-top: 10px"
@click="addItem"
@ -334,10 +329,10 @@
type="PlusOutlined"
style="margin-left: 2px"
/>
</a-button>
</a-col>
<a-col :span="24" style="margin-top: 20px">
<a-form-item
</j-button>
</j-col>
<j-col :span="24" style="margin-top: 20px">
<j-form-item
label="说明"
name="description"
:rules="{
@ -345,16 +340,16 @@
message: '最多输入200个字符',
}"
>
<a-textarea
<j-textarea
v-model:value="modelRef.description"
placeholder="请输入说明"
showCount
:maxlength="200"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</j-form-item>
</j-col>
</j-row>
</j-form>
<div v-if="type === 'edit'">
<PermissionButton
type="primary"
@ -365,12 +360,12 @@
保存
</PermissionButton>
</div>
</a-col>
<a-col :span="8">
</j-col>
<j-col :span="8">
<Doc />
</a-col>
</a-row>
</a-card>
</j-col>
</j-row>
</j-card>
</page-container>
</template>
@ -384,7 +379,7 @@ import {
queryProductList,
} from '@/api/northbound/alicloud';
import _ from 'lodash';
import { message } from 'ant-design-vue';
import { message } from 'jetlinks-ui-components';
const router = useRouter();
const route = useRoute();
@ -430,10 +425,6 @@ 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();
if (resp.status === 200) {
@ -503,8 +494,9 @@ const saveBtn = () => {
);
data.bridgeProductName = product?.productName || '';
loading.value = true;
const resp = await savePatch({...toRaw(modelRef), ...data});
loading.value = false;
const resp = await savePatch({...toRaw(modelRef), ...data}).finally(() => {
loading.value = false;
})
if (resp.status === 200) {
message.success('操作成功!');
formRef.value.resetFields();

View File

@ -1,11 +1,11 @@
<template>
<page-container>
<Search
<j-advanced-search
:columns="columns"
target="northbound-aliyun"
@search="handleSearch"
/>
<JTable
<JProTable
ref="instanceRef"
:columns="columns"
:request="query"
@ -13,7 +13,7 @@
:params="params"
>
<template #headerTitle>
<a-space>
<j-space>
<PermissionButton
type="primary"
@click="handleAdd"
@ -22,7 +22,7 @@
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
</a-space>
</j-space>
</template>
<template #card="slotProps">
<CardBox
@ -45,20 +45,20 @@
>
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<j-row>
<j-col :span="12">
<div class="card-item-content-text">
网桥产品
</div>
<div>{{ slotProps?.bridgeProductName }}</div>
</a-col>
<a-col :span="12">
</j-col>
<j-col :span="12">
<div class="card-item-content-text">
<label>说明</label>
</div>
<div>{{ slotProps?.description }}</div>
</a-col>
</a-row>
</j-col>
</j-row>
</template>
<template #actions="item">
<PermissionButton
@ -81,13 +81,13 @@
</CardBox>
</template>
<template #state="slotProps">
<a-badge
<j-badge
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
/>
</template>
<template #action="slotProps">
<a-space>
<j-space>
<template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
@ -104,23 +104,21 @@
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</template>
</a-space>
</j-space>
</template>
</JTable>
</JProTable>
</page-container>
</template>
<script setup lang="ts">
import { query, _undeploy, _deploy, _delete } from '@/api/northbound/alicloud';
import type { ActionsType } from '@/components/Table/index.vue';
import type { ActionsType } from '@/views/device/Instance/typings'
import { getImage } from '@/utils/comm';
import { message } from 'ant-design-vue';
import { message } from 'jetlinks-ui-components';
import { useMenuStore } from 'store/menu';
const router = useRouter();
const instanceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const current = ref<Record<string, any>>({});
const menuStory = useMenuStore();
@ -149,6 +147,9 @@ const columns = [
title: '说明',
dataIndex: 'describe',
key: 'describe',
search: {
type: 'string',
},
},
{
title: '状态',

View File

@ -40,7 +40,7 @@
</template>
<script lang="ts" setup>
import { PropType } from "vue-demi";
import { PropType } from "vue";
type Emits = {

View File

@ -9,8 +9,8 @@
<j-form-item name="messageType" label="指令类型" :rules="{
required: true,
message: '请选择指令类型',
}">
<j-select placeholder="请选择指令类型" v-model:value="modelRef.messageType" show-search :filter-option="filterOption">
}" class="other">
<j-select placeholder="请选择指令类型" v-model:value="modelRef.messageType" show-search>
<j-select-option value="READ_PROPERTY">读取属性</j-select-option>
<j-select-option value="WRITE_PROPERTY">修改属性</j-select-option>
<j-select-option value="INVOKE_FUNCTION">调用功能</j-select-option>
@ -22,7 +22,7 @@
required: true,
message: '请选择属性',
}">
<j-select placeholder="请选择属性" v-model:value="modelRef.message.properties" show-search :filter-option="filterOption">
<j-select placeholder="请选择属性" v-model:value="modelRef.message.properties" show-search @change="onPropertyChange">
<j-select-option v-for="i in (metadata?.properties) || []" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</j-select-option>
</j-select>
</j-form-item>
@ -32,7 +32,27 @@
required: true,
message: '请输入值',
}">
<j-input />
<ValueItem
v-model:modelValue="modelRef.message.value"
:itemType="property.type || property.valueType?.type || 'int'"
:options="
property.valueType?.type === 'enum'
? (property?.dataType?.elements || []).map(
(item) => {
return {
label: item?.text,
value: item?.value,
};
},
)
: property.valueType?.type === 'boolean'
? [
{ label: '是', value: true },
{ label: '否', value: false },
]
: undefined
"
/>
</j-form-item>
</j-col>
<j-col :span="24" v-if="modelRef.messageType === 'INVOKE_FUNCTION'">
@ -40,7 +60,7 @@
required: true,
message: '请选择功能',
}">
<j-select placeholder="请选择功能" v-model:value="modelRef.message.functionId" show-search :filter-option="filterOption" @change="funcChange">
<j-select placeholder="请选择功能" v-model:value="modelRef.message.functionId" show-search @change="funcChange">
<j-select-option v-for="i in (metadata?.functions) || []" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</j-select-option>
</j-select>
</j-form-item>
@ -62,10 +82,6 @@ import EditTable from './EditTable.vue'
const formRef = ref();
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const props = defineProps({
actionType: {
type: String,
@ -89,10 +105,12 @@ const props = defineProps({
type Emits = {
(e: 'update:modelValue', data: any): void;
};
const emit = defineEmits<Emits>();
const modelRef = computed({
get: () => {
onPropertyChange(props.modelValue?.message?.properties)
return props.modelValue || {
messageType: undefined,
message: {
@ -107,6 +125,8 @@ const modelRef = computed({
}
})
const property = ref<any>({})
const funcChange = (val: string) => {
if(val){
const arr = props.metadata?.functions.find((item: any) => item.id === val)?.inputs || []
@ -122,6 +142,13 @@ const funcChange = (val: string) => {
}
}
const onPropertyChange = (val: string) => {
if(val){
const _item = props.metadata?.properties.find((item: any) => item.id === val)
property.value = _item?.[0] || {}
}
}
const saveBtn = () => new Promise((resolve) => {
formRef.value.validate()
.then(() => {
@ -139,4 +166,13 @@ const saveBtn = () => new Promise((resolve) => {
defineExpose({ saveBtn })
</script>
</script>
<style lang="less" scoped>
:deep(.ant-form-item){
margin-bottom: 0;
}
.other {
margin-bottom: 24px;
}
</style>

View File

@ -51,7 +51,6 @@
placeholder="请选择产品"
v-model:value="modelRef.id"
show-search
:filter-option="filterOption"
@change="productChange"
>
<j-select-option
@ -89,7 +88,6 @@
placeholder="请选择设备类型"
v-model:value="modelRef.applianceType"
show-search
:filter-option="filterOption"
@change="typeChange"
>
<j-select-option
@ -170,13 +168,10 @@
item.action
"
show-search
:filter-option="
filterOption
"
>
<j-select-option
v-for="i in getTypesActions(
item.action,
item.action || ''
)"
:key="i.id"
:value="i.id"
@ -218,9 +213,6 @@
item.actionType
"
show-search
:filter-option="
filterOption
"
>
<j-select-option
value="command"
@ -261,6 +253,9 @@
</j-row>
</j-collapse-panel>
</j-collapse>
<j-card v-else>
<j-empty />
</j-card>
</j-col>
<j-col :span="24">
<j-button
@ -323,13 +318,10 @@
item.source
"
show-search
:filter-option="
filterOption
"
>
<j-select-option
v-for="i in getDuerOSProperties(
item.source,
item.source || '',
)"
:key="i.id"
:value="i.id"
@ -361,16 +353,13 @@
"
mode="tags"
show-search
:filter-option="
filterOption
"
>
<j-select-option
v-for="i in getProductProperties(
item.target,
)"
:key="i.id"
:value="item.id"
:value="i.id"
>{{
i.name
}}</j-select-option
@ -381,6 +370,9 @@
</j-row>
</j-collapse-panel>
</j-collapse>
<j-card v-else>
<j-empty />
</j-card>
</j-col>
<j-col :span="24">
<j-button
@ -494,10 +486,6 @@ 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({
@ -636,8 +624,9 @@ const saveBtn = async () => {
.then(async (data: any) => {
if (tasks.every((item) => item) && data) {
loading.value = true;
const resp = await savePatch(data);
loading.value = false;
const resp = await savePatch(data).finally(() => {
loading.value = false;
})
if (resp.status === 200) {
message.success('操作成功!');
formRef.value.resetFields();

View File

@ -14,7 +14,6 @@
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange,
}"
@cancelSelect="cancelSelect"
:params="params"
>
<template #headerTitle>
@ -643,10 +642,6 @@ const onSelectChange = (keys: string[]) => {
_selectedRowKeys.value = [...keys];
};
const cancelSelect = () => {
_selectedRowKeys.value = [];
};
const handleClick = (dt: any) => {
if (_selectedRowKeys.value.includes(dt.id)) {
const _index = _selectedRowKeys.value.findIndex((i) => i === dt.id);

View File

@ -1,5 +1,5 @@
<template>
<a-form-item label="标识" name="id" :rules="[
<j-form-item label="标识" name="id" :rules="[
{ required: true, message: '请输入标识' },
{ max: 64, message: '最多可输入64个字符' },
{
@ -7,14 +7,14 @@
message: 'ID只能由数字、字母、下划线、中划线组成',
},
]">
<a-input v-model:value="value.id" size="small" @change="asyncOtherConfig" :disabled="metadataStore.model.action === 'edit'"></a-input>
</a-form-item>
<a-form-item label="名称" name="name" :rules="[
<j-input v-model:value="value.id" size="small" @change="asyncOtherConfig" :disabled="metadataStore.model.action === 'edit'"></j-input>
</j-form-item>
<j-form-item label="名称" name="name" :rules="[
{ required: true, message: '请输入名称' },
{ max: 64, message: '最多可输入64个字符' },
]">
<a-input v-model:value="value.name" size="small"></a-input>
</a-form-item>
<j-input v-model:value="value.name" size="small"></j-input>
</j-form-item>
<template v-if="modelType === 'properties'">
<value-type-form :name="['valueType']" v-model:value="value.valueType" key="property" title="数据类型"
@change-type="changeValueType"></value-type-form>
@ -22,42 +22,42 @@
:valueType="value.valueType"></expands-form>
</template>
<template v-if="modelType === 'functions'">
<a-form-item label="是否异步" name="async" :rules="[
<j-form-item label="是否异步" name="async" :rules="[
{ required: true, message: '请选择是否异步' },
]">
<a-radio-group v-model:value="value.async">
<a-radio :value="true"></a-radio>
<a-radio :value="false"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="输入参数" name="inputs" :rules="[
<j-radio-group v-model:value="value.async">
<j-radio :value="true"></j-radio>
<j-radio :value="false"></j-radio>
</j-radio-group>
</j-form-item>
<j-form-item label="输入参数" name="inputs" :rules="[
{ required: true, message: '请输入输入参数' },
]">
<JsonParam v-model:value="value.inputs" :name="['inputs']"></JsonParam>
</a-form-item>
</j-form-item>
<value-type-form :name="['output']" v-model:value="value.output" key="function" title="输出参数"></value-type-form>
</template>
<template v-if="modelType === 'events'">
<a-form-item label="级别" :name="['expands', 'level']" :rules="[
<j-form-item label="级别" :name="['expands', 'level']" :rules="[
{ required: true, message: '请选择级别' },
]">
<a-select v-model:value="value.expands.level" :options="EventLevel" size="small"></a-select>
</a-form-item>
<j-select v-model:value="value.expands.level" :options="EventLevel" size="small"></j-select>
</j-form-item>
<value-type-form :name="['valueType']" v-model:value="value.valueType" key="function" title="输出参数"></value-type-form>
</template>
<template v-if="modelType === 'tags'">
<value-type-form :name="['valueType']" v-model:value="value.valueType" key="property" title="数据类型"></value-type-form>
<a-form-item label="读写类型" :name="['expands', 'type']" :rules="[
<j-form-item label="读写类型" :name="['expands', 'type']" :rules="[
{ required: true, message: '请选择读写类型' },
]">
<a-select v-model:value="value.expands.type" :options="ExpandsTypeList" mode="multiple" size="small"></a-select>
</a-form-item>
<j-select v-model:value="value.expands.type" :options="ExpandsTypeList" mode="multiple" size="small"></j-select>
</j-form-item>
</template>
<a-form-item label="说明" name="description" :rules="[
<j-form-item label="说明" name="description" :rules="[
{ max: 200, message: '最多可输入200个字符' },
]">
<a-textarea v-model:value="value.description" size="small"></a-textarea>
</a-form-item>
<j-textarea v-model:value="value.description" size="small"></j-textarea>
</j-form-item>
</template>
<script setup lang="ts" name="BaseForm">
import { PropType } from 'vue';

View File

@ -1,26 +1,30 @@
<template>
<a-form-item label="来源" :name="name.concat(['source'])" v-if="type === 'product'" :rules="[
<j-form-item label="来源" :name="name.concat(['source'])" v-if="type === 'product'" :rules="[
{ required: true, message: '请选择来源' },
]">
<a-select v-model:value="_value.source" :options="PropertySource" size="small"
:disabled="metadataStore.model.action === 'edit'"></a-select>
</a-form-item>
<j-select v-model:value="_value.source" :options="PropertySource" size="small"
:disabled="metadataStore.model.action === 'edit'"></j-select>
</j-form-item>
<virtual-rule-param v-if="_value.source === 'rule'" v-model:value="_value.virtualRule"
:name="name.concat(['virtualRule'])" :id="id" :showWindow="_value.source === 'rule'"></virtual-rule-param>
<a-form-item label="读写类型" :name="name.concat(['type'])" :rules="[
<j-form-item label="读写类型" :name="name.concat(['type'])" :rules="[
{ required: true, message: '请选择读写类型' },
]">
<a-select v-model:value="_value.type" :options="ExpandsTypeList" mode="multiple" size="small"></a-select>
</a-form-item>
<a-form-item label="其他配置" v-if="config.length > 0">
<a-form-item v-for="(item, index) in config" :key="index">
<j-select v-model:value="_value.type" :options="ExpandsTypeList" mode="multiple" size="small"></j-select>
</j-form-item>
<j-form-item label="其他配置" v-if="config.length > 0">
<j-form-item v-for="(item, index) in config" :key="index">
<config-param v-model:value="_value" :config="item" :name="name"></config-param>
</a-form-item>
</a-form-item>
<a-form-item v-if="type === 'product' && ['int', 'float', 'double', 'long', 'date', 'string', 'boolean'].includes(valueType.type)"
label="指标配置" :name="name.concat(['metrics'])">
<metrics-param v-model:value="_value.metrics" :type="valueType.type" :enum="valueType" :name="name.concat(['metrics'])"></metrics-param>
</a-form-item>
</j-form-item>
</j-form-item>
<j-form-item
v-if="type === 'product' && ['int', 'float', 'double', 'long', 'date', 'string', 'boolean'].includes(valueType.type)"
label="指标配置" :name="name.concat(['metrics'])" :rules="[
{ validator: () => validateMetrics(_value.metrics), message: '请输入指标配置' }
]">
<metrics-param v-model:value="_value.metrics" :type="valueType.type" :enum="valueType"
:name="name.concat(['metrics'])"></metrics-param>
</j-form-item>
</template>
<script setup lang="ts" name="ExpandsForm">
import { useMetadataStore } from '@/store/metadata';
@ -86,5 +90,15 @@ onMounted(() => {
}
})
const validateMetrics = (value: Record<any, any>[]) => {
const flag = value.every((item) => {
return item.id && item.name && item.value;
});
if (!flag) {
return Promise.reject(new Error('请输入指标配置'));
}
return Promise.resolve();
}
</script>
<style lang="less" scoped></style>

View File

@ -1,48 +1,54 @@
<template>
<a-form-item :label="title" :name="name.concat(['type'])" :rules="[
<j-form-item :label="title" :name="name.concat(['type'])" :rules="[
metadataStore.model.type !== 'functions' ? { required: true, message: `请选择${title}` } : {},
]">
<a-select v-model:value="_value.type" :options="metadataStore.model.type === 'events' ? eventDataTypeList : _dataTypeList" size="small" @change="changeType"></a-select>
</a-form-item>
<a-form-item label="单位" :name="name.concat(['unit'])" v-if="['int', 'float', 'long', 'double'].includes(_value.type)">
<j-select v-model:value="_value.type"
:options="metadataStore.model.type === 'events' ? eventDataTypeList : _dataTypeList" size="small"
@change="changeType"></j-select>
</j-form-item>
<j-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)">
</j-form-item>
<j-form-item label="精度" :name="name.concat(['scale'])" v-if="['float', 'double'].includes(_value.type)">
<j-input-number v-model:value="_value.scale" size="small" :min="0" :max="2147483647" :precision="0" :default-value="2"
style="width: 100%"></j-input-number>
</j-form-item>
<j-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: '请配置枚举项' }
</j-form-item>
<j-form-item label="枚举项" :name="name.concat(['elements'])" v-if="['enum'].includes(_value.type)" :rules="[
{ required: true, validator: validateEnum, message: '请配置枚举项' }
]">
<EnumParam v-model:value="_value.elements" :name="name.concat(['elements'])"></EnumParam>
</a-form-item>
<a-form-item :name="name.concat(['expands', 'maxLength'])" v-if="['string', 'password'].includes(_value.type)">
</j-form-item>
<j-form-item :name="name.concat(['expands', 'maxLength'])" v-if="['string', 'password'].includes(_value.type)">
<template #label>
<a-space>
<j-space>
最大长度
<a-tooltip title="字节">
<j-tooltip title="字节">
<question-circle-outlined style="color: rgb(136, 136, 136); font-size: 12px;" />
</a-tooltip>
</a-space>
</j-tooltip>
</j-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)">
<j-input-number v-model:value="_value.expands.maxLength" size="small" :max="2147483647" :min="1" :precision="0"
style="width: 100%;"></j-input-number>
</j-form-item>
<j-form-item label="元素配置" :name="name.concat(['elementType'])" v-if="['array'].includes(_value.type)" :rules="[
{ validator: validateArray }
]">
<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)" :rules="[]">
</j-form-item>
<j-form-item label="JSON对象" :name="name.concat(['properties'])" v-if="['object'].includes(_value.type)" :rules="[
{ validator: validateJson }
]">
<JsonParam v-model:value="_value.properties" :name="name.concat(['properties'])"></JsonParam>
</a-form-item>
<a-form-item label="文件类型" :name="name.concat(['fileType'])" v-if="['file'].includes(_value.type)" initialValue="url"
</j-form-item>
<j-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>
<j-select v-model:value="_value.fileType" :options="FileTypeList" size="small"></j-select>
</j-form-item>
</template>
<script lang="ts" setup mame="BaseForm">
import { DataTypeList, FileTypeList } from '@/views/device/data';
@ -56,6 +62,8 @@ import EnumParam from '@/components/Metadata/EnumParam/index.vue'
import ArrayParam from '@/components/Metadata/ArrayParam/index.vue'
import JsonParam from '@/components/Metadata/JsonParam/index.vue'
import { useMetadataStore } from '@/store/metadata';
import { Rule } from 'ant-design-vue/es/form';
import { Form } from 'ant-design-vue/es';
type ValueType = Record<any, any>;
const props = defineProps({
@ -84,15 +92,8 @@ interface Emits {
}
const emit = defineEmits<Emits>()
// emit('update:value', { extends: {}, ...props.value })
const metadataStore = useMetadataStore()
// const _value = computed({
// get: () => props.value,
// set: val => {
// emit('update:value', val)
// }
// })
const _value = ref<ValueType>({})
watchEffect(() => {
_value.value = props.value || {
@ -142,6 +143,79 @@ const eventDataTypeList = [
const changeType = (val: SelectValue) => {
emit('changeType', val as string)
}
const validateEnum = async (_rule: Rule, val: Record<any, any>[]) => {
if (val.length === 0) return Promise.reject(new Error('请配置枚举项'));
const flag = val.every((item) => {
return item.value && item.text;
});
if (!flag) {
return Promise.reject(new Error('请配置枚举项'));
}
return Promise.resolve();
}
const validateArray = async (_rule: Rule, val: Record<any, any>) => {
if (!val) return Promise.reject(new Error('请输入元素配置'));
await validateValueType(_rule, val)
return Promise.resolve();
}
const validateJson = async (_rule: Rule, val: Record<any, any>[]) => {
if (!val || val.length === 0) {
return Promise.reject(new Error('请输入配置参数'));
}
for (let item of val) {
if (!item) return Promise.reject(new Error('请输入配置参数'));
await validateValueType(_rule, item)
}
return Promise.resolve();
}
const validateValueType = async (_rule: Rule, val: Record<any, any>) => {
if (!val) return Promise.reject(new Error('请输入元素配置'));
if (!val.id) {
return Promise.reject(new Error('请输入标识'))
}
if (!val.name) {
return Promise.reject(new Error('请输入名称'))
}
if (metadataStore.model.type !== 'functions' && !val.valueType?.type) {
return Promise.reject(new Error(`请选择${props.title}`))
}
if (['enum'].includes(val.valueType.type)) {
await validateEnum(_rule, val.valueType.elements)
}
if (['array'].includes(val.valueType.type)) {
await validateArray(_rule, val.valueType.elementType)
}
if (['object'].includes(val.valueType.type)) {
await validateJson(_rule, val.valueType.properties)
}
if (['file'].includes(val.valueType.type) && !val.valueType.fileType) {
return Promise.reject(new Error('请选择文件类型'))
}
return Promise.resolve();
}
// const rules = ref({
// type: [
// metadataStore.model.type !== 'functions' ? { required: true, message: `${props.title}` } : {},
// ],
// elements: [
// { required: true, validator: validateEnum, message: '' }
// ],
// elementType: [
// { validator: validateArray, message: '' }
// ],
// properties: [
// { validator: validateJson, message: '' }
// ],
// fileType: [
// { required: true, message: '' },
// ]
// })
</script>
<style lang="less" scoped>
:deep(.ant-form-item-label) {

View File

@ -1,13 +1,13 @@
<template>
<a-drawer :mask-closable="false" width="25vw" visible :title="`${title}-${typeMapping[metadataStore.model.type]}`"
<j-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>
<j-button :loading="save.loading" type="primary" @click="save.saveMetadata">保存</j-button>
</template>
<a-form ref="formRef" :model="form.model" layout="vertical">
<j-form ref="formRef" :model="form.model" layout="vertical">
<BaseForm :model-type="metadataStore.model.type" :type="type" v-model:value="form.model"></BaseForm>
</a-form>
</a-drawer>
</j-form>
</j-drawer>
</template>
<script lang="ts" setup name="Edit">
import { useInstanceStore } from '@/store/instance';
@ -22,6 +22,7 @@ import { DeviceInstance } from '@/views/device/Instance/typings';
import BaseForm from './BaseForm.vue';
import { PropType } from 'vue';
import { _deploy } from '@/api/device/product';
import { cloneDeep } from 'lodash';
const props = defineProps({
type: {
@ -60,7 +61,7 @@ const form = reactive({
model: {} as any,
})
if (metadataStore.model.action === 'edit') {
form.model = metadataStore.model.item
form.model = cloneDeep(metadataStore.model.item)
}
const formRef = ref<FormInstance>()
@ -75,7 +76,9 @@ const save = reactive({
const type = metadataStore.model.type
const _detail: ProductItem | DeviceInstance = props.type === 'device' ? instanceStore.detail : productStore.current
const _metadata = JSON.parse(_detail?.metadata || '{}')
const list = _metadata[type] as any[]
console.log(_metadata)
console.log(type)
const list = (_metadata[type] as any[]) || []
if (formValue.id) {
if (metadataStore.model.action === 'add' && list.some(item => item.id === formValue.id)) {
message.error('标识已存在')

View File

@ -1,7 +1,7 @@
<template>
<j-pro-table :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>
<j-input-search v-model:value="searchValue" placeholder="请输入名称" @search="handleSearch"></j-input-search>
</template>
<template #rightExtraRender>
<PermissionButton type="primary" :uhas-permission="`${permission}:update`" key="add" @click="handleAddClick"
@ -135,6 +135,13 @@ const refreshMetadata = () => {
watch([route.params.id, type], refreshMetadata, { immediate: true })
const metadataStore = useMetadataStore()
watch(() => metadataStore.model.importMetadata,
(val: boolean) => {
if (!!val) {
refreshMetadata()
metadataStore.set('importMetadata', false)
}
})
const handleAddClick = () => {
metadataStore.set('edit', true)
metadataStore.set('item', undefined)
@ -189,6 +196,4 @@ const removeItem = async (record: MetadataItem) => {
}
};
</script>
<style scoped lang="less">
</style>
<style scoped lang="less"></style>

View File

@ -1,13 +1,13 @@
<template>
<a-drawer :mask-closable="false" title="查看物模型" width="700" v-model:visible="_visible" destroy-on-close @close="close">
<j-drawer :mask-closable="false" title="查看物模型" width="700" v-model:visible="_visible" destroy-on-close @close="close">
<template #extra>
<a-space>
<a-button type="primary" @click="handleExport">
<j-space>
<j-button type="primary" @click="handleExport">
导出
</a-button>
</a-space>
</j-button>
</j-space>
</template>
<a-spin :spinning="loading">
<j-spin :spinning="loading">
<div class="cat-content">
<p class="cat-tip">
物模型是对设备在云端的功能描述包括设备的属性服务和事件物联网平台通过定义一种物的描述语言来描述物模型称之为
@ -15,15 +15,15 @@
组装上报设备的数据您可以导出完整物模型用于云端应用开发
</p>
</div>
<a-tabs @change="handleConvertMetadata" destroy-inactive-tab-pane>
<a-tab-pane v-for="item in codecs" :key="item.id" :tab="item.name">
<j-tabs @change="handleConvertMetadata" destroy-inactive-tab-pane>
<j-tab-pane v-for="item in codecs" :key="item.id" :tab="item.name">
<div class="cat-panel">
<MonacoEditor v-model="value" theme="vs" style="height: 100%"></MonacoEditor>
</div>
</a-tab-pane>
</a-tabs>
</a-spin>
</a-drawer>
</j-tab-pane>
</j-tabs>
</j-spin>
</j-drawer>
</template>
<script setup lang="ts" name="Cat">
import { message } from 'ant-design-vue/es';

View File

@ -1,5 +1,5 @@
<template>
<a-modal :mask-closable="false" title="导入物模型" destroy-on-close v-model:visible="_visible" @cancel="close"
<j-modal :mask-closable="false" title="导入物模型" destroy-on-close v-model:visible="_visible" @cancel="close"
@ok="handleImport" :confirm-loading="loading">
<div class="import-content">
<p class="import-tip">
@ -7,46 +7,46 @@
导入的物模型会覆盖原来的属性功能事件标签请谨慎操作
</p>
</div>
<a-form layout="vertical" v-model="formModel">
<a-form-item v-if="type === 'product'" label="导入方式" v-bind="validateInfos.type">
<a-select v-model:value="formModel.type">
<a-select-option value="copy">拷贝产品</a-select-option>
<a-select-option value="import">导入物模型</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="选择产品" v-bind="validateInfos.copy" v-if="formModel.type === 'copy'">
<a-select :options="productList" v-model:value="formModel.copy" option-filter-prop="label"></a-select>
</a-form-item>
<a-form-item label="物模型类型" v-bind="validateInfos.metadata"
<j-form layout="vertical" v-model="formModel">
<j-form-item v-if="type === 'product'" label="导入方式" v-bind="validateInfos.type">
<j-select v-model:value="formModel.type">
<j-select-option value="copy">拷贝产品</j-select-option>
<j-select-option value="import">导入物模型</j-select-option>
</j-select>
</j-form-item>
<j-form-item label="选择产品" v-bind="validateInfos.copy" v-if="formModel.type === 'copy'">
<j-select :options="productList" v-model:value="formModel.copy" option-filter-prop="label"></j-select>
</j-form-item>
<j-form-item label="物模型类型" v-bind="validateInfos.metadata"
v-if="type === 'device' || formModel.type === 'import'">
<a-select v-model:value="formModel.metadata">
<a-select-option value="jetlinks">Jetlinks物模型</a-select-option>
<a-select-option value="alink">阿里云物模型TSL</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="导入类型" v-bind="validateInfos.metadataType"
<j-select v-model:value="formModel.metadata">
<j-select-option value="jetlinks">Jetlinks物模型</j-select-option>
<j-select-option value="alink">阿里云物模型TSL</j-select-option>
</j-select>
</j-form-item>
<j-form-item label="导入类型" v-bind="validateInfos.metadataType"
v-if="type === 'device' || formModel.type === 'import'">
<a-select v-model:value="formModel.metadataType">
<a-select-option value="file">文件上传</a-select-option>
<a-select-option value="script">脚本</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="文件上传" v-bind="validateInfos.upload" v-if="formModel.metadataType === 'file'">
<a-input v-model:value="formModel.upload">
<j-select v-model:value="formModel.metadataType">
<j-select-option value="file">文件上传</j-select-option>
<j-select-option value="script">脚本</j-select-option>
</j-select>
</j-form-item>
<j-form-item label="文件上传" v-bind="validateInfos.upload" v-if="formModel.metadataType === 'file'">
<j-input v-model:value="formModel.upload">
<template #addonAfter>
<a-upload v-model:file-list="fileList" :before-upload="beforeUpload" accept=".json"
<j-upload v-model:file-list="fileList" :before-upload="beforeUpload" accept=".json"
:show-upload-list="false" :action="FILE_UPLOAD" @change="fileChange"
:headers="{ 'X-Access-Token': getToken()}">
<AIcon type="UploadOutlined" class="upload-button" />
</a-upload>
</j-upload>
</template>
</a-input>
</a-form-item>
<a-form-item label="物模型" v-bind="validateInfos.import" v-if="formModel.metadataType === 'script'">
</j-input>
</j-form-item>
<j-form-item label="物模型" v-bind="validateInfos.import" v-if="formModel.metadataType === 'script'">
<MonacoEditor v-model="formModel.import" theme="vs" style="height: 300px"></MonacoEditor>
</a-form-item>
</a-form>
</a-modal>
</j-form-item>
</j-form>
</j-modal>
</template>
<script setup lang="ts" name="Import">
import { useForm } from 'ant-design-vue/es/form';
@ -54,13 +54,14 @@ import { saveMetadata } from '@/api/device/instance'
import { queryNoPagingPost, convertMetadata, modify } from '@/api/device/product'
import type { DefaultOptionType } from 'ant-design-vue/es/select';
import type { UploadProps, UploadFile, UploadChangeParam } from 'ant-design-vue/es';
import type { DeviceMetadata, ProductItem } from '@/views/device/Product/typings'
import { message } from 'ant-design-vue/es';
import type { DeviceMetadata } from '@/views/device/Product/typings'
import { message } from 'jetlinks-ui-components';
import { useInstanceStore } from '@/store/instance'
import { useProductStore } from '@/store/product';
import { FILE_UPLOAD } from '@/api/comm';
import { getToken } from '@/utils/comm';
import MonacoEditor from '@/components/MonacoEditor/index.vue'
import { useMetadataStore } from '@/store/metadata';
const route = useRoute()
const instanceStore = useInstanceStore()
@ -87,7 +88,6 @@ const _visible = computed({
})
const close = () => {
console.log(1)
emits('update:visible', false);
}
@ -196,7 +196,7 @@ const operateLimits = (mdata: DeviceMetadata) => {
});
return obj;
};
const metadataStore = useMetadataStore()
const handleImport = async () => {
validate().then(async (data) => {
loading.value = true
@ -224,6 +224,7 @@ const handleImport = async () => {
} else {
productStore.refresh(id as string)
}
metadataStore.set('importMetadata', true)
// Store.set(SystemConst.GET_METADATA, true)
// Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
close()
@ -263,13 +264,14 @@ const handleImport = async () => {
message.success('导入成功')
}
}
// Store.set(SystemConst.GET_METADATA, true)
// Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
if (props?.type === 'device') {
instanceStore.refresh(id as string)
} else {
productStore.refresh(id as string)
}
metadataStore.set('importMetadata', true)
// Store.set(SystemConst.GET_METADATA, true)
// Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
close();
} catch (e) {
loading.value = false

View File

@ -18,10 +18,25 @@
:dataList="deviceStepDetails"
/>
</j-row>
</div>
<div class="dialog">
<ProductChooseDialog
v-if="productDialogVisible"
v-model:visible="productDialogVisible"
@confirm="(id:string)=>jumpPage('device/Product/Detail', { id })"
/>
<DeviceChooseDialog
v-if="deviceDialogVisible"
v-model:visible="deviceDialogVisible"
@confirm="(id:string)=>jumpPage('device/Instance/Detail', { id })"
/>
</div>
</div>
</template>
<script setup lang="ts" name="deviceHome">
import ProductChooseDialog from '../dialogs/ProductChooseDialog.vue';
import DeviceChooseDialog from '../dialogs/DeviceChooseDialog.vue';
import BootCard from '../BootCard.vue';
import DeviceCountCard from '../DeviceCountCard.vue';
import PlatformPicCard from '../PlatformPicCard.vue';
@ -29,6 +44,7 @@ import StepCard from '../StepCard.vue';
import { usePermissionStore } from '@/store/permission';
import { bootConfig, recommendList } from '../../typing';
import { useMenuStore } from '@/store/menu';
//
const hasPermission = usePermissionStore().hasPermission;
@ -39,6 +55,11 @@ const devicePermission = (action: string) =>
const rulePermission = (action: string) =>
hasPermission(`rule-engine/Instance:${action}`);
const { jumpPage } = useMenuStore();
const productDialogVisible = ref(false);
const deviceDialogVisible = ref(false);
const deviceBootConfig: bootConfig[] = [
{
english: 'STEP1',
@ -87,7 +108,9 @@ const deviceStepDetails: recommendList[] = [
iconUrl: '/images/home/bottom-1.png',
linkUrl: 'device/Product/Detail',
auth: productPermission('update'),
dialogTag: 'accessMethod',
onClick: () => {
productDialogVisible.value = true;
},
},
{
title: '添加测试设备',
@ -105,7 +128,9 @@ const deviceStepDetails: recommendList[] = [
'对添加的测试设备进行功能调试,验证能否连接到平台,设备功能是否配置正确。',
iconUrl: '/images/home/bottom-2.png',
linkUrl: 'device/Instance/Detail',
dialogTag: 'funcTest',
onClick: () => {
deviceDialogVisible.value = true;
},
},
{
title: '批量添加设备',

View File

@ -3,34 +3,34 @@
<div class="title">请选择首页视图</div>
<div class="choose-view">
<a-row class="view-content" :gutter="24">
<a-col
<j-row class="view-content" :gutter="24">
<j-col
:span="8"
class="select-item"
:class="{ selected: selectValue === 'device' }"
@click="selectValue = 'device'"
>
<img :src="getImage('/home/device.png')" alt="" />
</a-col>
<a-col
</j-col>
<j-col
:span="8"
class="select-item"
:class="{ selected: selectValue === 'ops' }"
@click="selectValue = 'ops'"
>
<img :src="getImage('/home/ops.png')" alt="" />
</a-col>
<a-col
</j-col>
<j-col
:span="8"
class="select-item"
:class="{ selected: selectValue === 'comprehensive' }"
@click="selectValue = 'comprehensive'"
>
<img :src="getImage('/home/comprehensive.png')" alt="" />
</a-col>
</a-row>
<a-button type="primary" class="btn" @click="confirm"
>确定</a-button
</j-col>
</j-row>
<j-button type="primary" class="btn" @click="confirm"
>确定</j-button
>
</div>
</div>

View File

@ -1,261 +1,278 @@
<template>
<Search
:columns="columns"
type='simple'
@search="handleSearch"
class='search'
target="scene-triggrt-device-device"
<j-advanced-search
:columns="columns"
type="simple"
@search="handleSearch"
class="search"
target="scene-trigger-device-product"
/>
<a-divider style='margin: 0' />
<a-divider style="margin: 0" />
<j-pro-table
ref='actionRef'
model='CARD'
:columns='columns'
:params='params'
:request='productQuery'
:gridColumn='2'
:gridColumns='[2,2,2]'
:bodyStyle='{
paddingRight: 0,
paddingLeft: 0
}'
ref="actionRef"
model="CARD"
:columns="columns"
:params="params"
:request="productQuery"
:gridColumn="2"
:bodyStyle="{
paddingRight: 0,
paddingLeft: 0,
}"
>
<template #card="slotProps">
<CardBox
:value='slotProps'
:active="rowKey === slotProps.id"
:status="slotProps.state"
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
:statusNames="{ 1: 'success', 0: 'error', }"
@click="handleClick"
>
<template #img>
<slot name="img">
<img width='88' height='88' :src="slotProps.photoUrl || getImage('/device-product.png')" />
</slot>
</template>
<template #content>
<div style='width: calc(100% - 100px)'>
<Ellipsis>
<span style="font-size: 16px;font-weight: 600" >
{{ slotProps.name }}
</span>
</Ellipsis>
</div>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>直连设备</div>
</a-col>
</a-row>
</template>
</CardBox>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
:active="rowKey === slotProps.id"
:status="String(slotProps.state)"
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
:statusNames="{ '1': 'success', '0': 'error' }"
@click="handleClick(slotProps)"
>
<template #img>
<slot name="img">
<img
:width="88"
:height="88"
:src="
slotProps.photoUrl ||
getImage('/device-product.png')
"
/>
</slot>
</template>
<template #content>
<div style="width: calc(100% - 100px)">
<Ellipsis>
<span style="font-size: 16px; font-weight: 600">
{{ slotProps.name }}
</span>
</Ellipsis>
</div>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">设备类型</div>
<Ellipsis>{{ slotProps.deviceType?.text }}</Ellipsis>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">接入方式</div>
<Ellipsis>{{ slotProps?.accessName || '未接入' }}</Ellipsis>
</a-col>
</a-row>
</template>
</CardBox>
</template>
</j-pro-table>
</template>
</template>
<script setup lang='ts' name='Product'>
import { getProviders, queryGatewayList, queryProductList } from '@/api/device/product'
import { queryTree } from '@/api/device/category'
import { getTreeData_api } from '@/api/system/department'
import { isNoCommunity } from '@/utils/utils'
import { getImage } from '@/utils/comm'
type Emit = {
(e: 'update:rowKey', data: string): void
(e: 'update:detail', data: string): void
(e: 'change', data: string): void
}
const actionRef = ref()
const params = ref({})
const props = defineProps({
import {
getProviders,
queryGatewayList,
queryProductList,
} from '@/api/device/product';
import { queryTree } from '@/api/device/category';
import { getTreeData_api } from '@/api/system/department';
import { isNoCommunity } from '@/utils/utils';
import { getImage } from '@/utils/comm';
type Emit = {
(e: 'update:rowKey', data: string): void;
(e: 'update:detail', data: string): void;
(e: 'change', data: string): void;
};
const actionRef = ref();
const params = ref({});
const props = defineProps({
rowKey: {
type: String,
default: ''
type: String,
default: '',
},
detail: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits<Emit>()
const columns = [
type: Object,
default: () => ({}),
},
});
const emit = defineEmits<Emit>();
const columns = [
{
title: 'ID',
dataIndex: 'id',
width: 300,
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
title: 'ID',
dataIndex: 'id',
width: 300,
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '名称',
dataIndex: 'name',
width: 200,
ellipsis: true,
search: {
type: 'string',
first: true
}
title: '名称',
dataIndex: 'name',
width: 200,
ellipsis: true,
search: {
type: 'string',
first: true,
},
},
{
title: '网关类型',
dataIndex: 'accessProvider',
width: 150,
ellipsis: true,
hideInTable: true,
search: {
type: 'select',
options: () => getProviders().then((resp: any) => {
if (isNoCommunity) {
return (resp?.result || []).map((item: any) => ({
label: item.name,
value: item.id
}))
} else {
return (resp?.result || []).filter((item: any) => [
'mqtt-server-gateway',
'http-server-gateway',
'mqtt-client-gateway',
'tcp-server-gateway',
].includes(item.id))
.map((item: any) => ({
label: item.name,
value: item.id,
}))
}
})
}
},
{
title: '接入方式',
dataIndex: 'accessName',
width: 150,
ellipsis: true,
search: {
type: 'select',
options: () => queryGatewayList().then((resp: any) =>
resp.result.map((item: any) => ({
label: item.name, value: item.id
}))
)
}
},
{
title: '设备类型',
dataIndex: 'deviceType',
width: 150,
search: {
type: 'select',
options: [
{ label: '直连设备', value: 'device' },
{ label: '网关子设备', value: 'childrenDevice' },
{ label: '网关设备', value: 'gateway' },
]
}
},
{
title: '状态',
dataIndex: 'state',
width: '90px',
search: {
type: 'select',
options: [
{ label: '禁用', value: 0 },
{ label: '正常', value: 1 },
]
}
},
{
title: '说明',
dataIndex: 'describe',
ellipsis: true,
width: 300,
},
{
dataIndex: 'classifiedId',
title: '分类',
hideInTable: true,
search: {
type: 'treeSelect',
options: queryTree({ paging: false }).then(resp => resp.result),
componentProps: {
fieldNames: {
label: 'name',
value: 'id',
}
}
}
},
{
dataIndex: 'id$dim-assets',
title: '所属组织',
hideInTable: true,
search: {
type: 'treeSelect',
options: getTreeData_api({ paging: false }).then((resp: any) => {
const formatValue = (list: any[]) => {
return list.map((item: any) => {
if (item.children) {
item.children = formatValue(item.children);
}
return {
...item,
value: JSON.stringify({
assetType: 'product',
targets: [
{
type: 'org',
id: item.id,
},
],
title: '网关类型',
dataIndex: 'accessProvider',
width: 150,
ellipsis: true,
hideInTable: true,
search: {
type: 'select',
options: () =>
getProviders().then((resp: any) => {
if (isNoCommunity) {
return (resp?.result || []).map((item: any) => ({
label: item.name,
value: item.id,
}));
} else {
return (resp?.result || [])
.filter((item: any) =>
[
'mqtt-server-gateway',
'http-server-gateway',
'mqtt-client-gateway',
'tcp-server-gateway',
].includes(item.id),
)
.map((item: any) => ({
label: item.name,
value: item.id,
}));
}
}),
}
})
}
return formatValue(resp.result)
}),
}
}
]
const handleSearch = (p: any) => {
params.value = p
}
const productQuery = (p: any) => {
},
},
{
title: '接入方式',
dataIndex: 'accessName',
width: 150,
ellipsis: true,
search: {
type: 'select',
options: () =>
queryGatewayList().then((resp: any) =>
resp.result.map((item: any) => ({
label: item.name,
value: item.id,
})),
),
},
},
{
title: '设备类型',
dataIndex: 'deviceType',
width: 150,
search: {
type: 'select',
options: [
{ label: '直连设备', value: 'device' },
{ label: '网关子设备', value: 'childrenDevice' },
{ label: '网关设备', value: 'gateway' },
],
},
},
{
title: '状态',
dataIndex: 'state',
width: '90px',
search: {
type: 'select',
options: [
{ label: '禁用', value: 0 },
{ label: '正常', value: 1 },
],
},
},
{
title: '说明',
dataIndex: 'describe',
ellipsis: true,
width: 300,
},
{
dataIndex: 'classifiedId',
title: '分类',
hideInTable: true,
search: {
type: 'treeSelect',
options: queryTree({ paging: false }).then((resp) => resp.result),
componentProps: {
fieldNames: {
label: 'name',
value: 'id',
},
},
},
},
{
dataIndex: 'id$dim-assets',
title: '所属组织',
hideInTable: true,
search: {
type: 'treeSelect',
options: getTreeData_api({ paging: false }).then((resp: any) => {
const formatValue = (list: any[]) => {
return list.map((item: any) => {
if (item.children) {
item.children = formatValue(item.children);
}
return {
...item,
value: JSON.stringify({
assetType: 'product',
targets: [
{
type: 'org',
id: item.id,
},
],
}),
};
});
};
return formatValue(resp.result);
}),
},
},
];
const handleSearch = (p: any) => {
params.value = p;
};
const productQuery = (p: any) => {
const sorts: any = [];
if (props.rowKey) {
sorts.push({
name: 'id',
value: props.rowKey,
});
sorts.push({
name: 'id',
value: props.rowKey,
});
}
sorts.push({ name: 'createTime', order: 'desc' });
p.sorts = sorts
return queryProductList(p)
}
const handleClick = (detail: any) => {
emit('update:rowKey', detail.id)
emit('update:detail', detail)
emit('change', detail)
}
</script>
p.sorts = sorts;
return queryProductList(p);
};
const handleClick = (detail: any) => {
emit('update:rowKey', detail.id);
emit('update:detail', detail);
emit('change', detail);
};
</script>
<style scoped lang='less'>
.search {
.search {
margin-bottom: 0;
padding-right: 0px;
padding-left: 0px;
}
</style>
}
</style>

View File

@ -0,0 +1,64 @@
<template>
<j-table
rowKey="id"
:columns="columns"
:data-source="dataSource"
bordered
:pagination="false"
>
<template #bodyCell="{ column, text, record }">
<div>
<template v-if="['valueType', 'name'].includes(column.dataIndex)">
<span>{{ text }}</span>
</template>
<template v-else>
<j-input />
<!-- TODO -->
</template>
</div>
</template>
</j-table>
</template>
<script lang="ts" setup>
import { PropType } from "vue";
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 = computed({
get: () => {
return _props.modelValue || []
},
set: (val: any) => {
_emit('update:modelValue', val);
}
})
</script>

View File

@ -0,0 +1,215 @@
<template>
<div>
<a-form :layout="'vertical'" ref="formRef" :model="modelRef">
<j-form-item
:name="['message', 'messageType']"
label="动作类型"
:rules="[{ required: true, message: '请选择动作类型' }]"
>
<TopCard
:typeList="TypeList"
v-model:value="modelRef.message.messageType"
/>
</j-form-item>
<template v-if="deviceMessageType === 'INVOKE_FUNCTION'">
<j-form-item
:name="['message', 'functionId']"
label="功能调用"
:rules="[{ required: true, message: '请选择功能' }]"
>
<j-select
showSearch
placeholder="请选择功能"
v-model:value="modelRef.message.functionId"
@change="onFunctionChange"
>
<j-select-option
v-for="item in metadata?.functions || []"
:value="item?.id"
:key="item?.id"
>{{ item?.name }}</j-select-option
>
</j-select>
</j-form-item>
<j-form-item
v-if="modelRef.message.functionId"
:name="['message', 'inputs']"
:rules="[{ required: true, message: '请输入功能值' }]"
>
<EditTable v-model="modelRef.message.inputs" />
</j-form-item>
</template>
<template v-else-if="deviceMessageType === 'READ_PROPERTY'">
<j-form-item
:name="['message', 'properties']"
label="读取属性"
:rules="[{ required: true, message: '请选择读取属性' }]"
>
<j-select
showSearch
placeholder="请选择属性"
v-model:value="modelRef.message.properties"
>
<j-select-option
v-for="item in metadata?.properties || []"
:value="item?.id"
:key="item?.id"
>{{ item?.name }}</j-select-option
>
</j-select>
</j-form-item>
</template>
<template v-else-if="deviceMessageType === 'WRITE_PROPERTY'">
<j-row>
<j-col :span="11">
<j-form-item
:name="['message', 'properties']"
label="读取属性"
:rules="[
{ required: true, message: '请选择读取属性' },
]"
>
<j-select
showSearch
placeholder="请选择属性"
v-model:value="modelRef.message.properties"
>
<j-select-option
v-for="item in metadata?.properties || []"
:value="item?.id"
:key="item?.id"
>{{ item?.name }}</j-select-option
>
</j-select>
</j-form-item>
</j-col>
<j-col :span="2"></j-col>
<j-col :span="11">
<j-form-item
:name="['message', 'properties']"
label="读取属性"
:rules="[
{ required: true, message: '请选择读取属性' },
]"
>
<j-select placeholder="请选择">
<!-- TODO -->
</j-select>
</j-form-item>
</j-col>
</j-row>
</template>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { getImage } from '@/utils/comm';
import TopCard from '../device/TopCard.vue';
import { detail } from '@/api/device/instance';
import EditTable from './EditTable.vue';
const TypeList = [
{
label: '功能调用',
value: 'INVOKE_FUNCTION',
image: getImage('/scene/invoke-function.png'),
tip: '',
},
{
label: '读取属性',
value: 'READ_PROPERTY',
image: getImage('/scene/read-property.png'),
tip: '',
},
{
label: '设置属性',
value: 'WRITE_PROPERTY',
image: getImage('/scene/write-property.png'),
tip: '',
},
];
const props = defineProps({
values: {
type: Object,
default: () => {},
},
name: {
type: Number,
default: 0,
},
thenName: {
type: Number,
default: 0,
},
branchGroup: {
type: Number,
default: 0,
},
});
const formRef = ref();
const modelRef = reactive({
message: {
messageType: undefined,
functionId: undefined,
properties: undefined,
inputs: [],
},
});
const metadata = ref<{
functions: any[];
properties: any[];
}>({
functions: [],
properties: [],
});
const deviceMessageType = computed(() => {
return modelRef.message.messageType;
});
const onFunctionChange = (val: string) => {
const _item = (metadata.value?.functions || []).find((item: any) => {
return val === item.id;
});
const list = (_item?.inputs || []).map((item: any) => {
return {
id: item.id,
name: item.name,
value: undefined,
valueType: item?.valueType?.type,
};
});
modelRef.message.inputs = list;
};
watchEffect(() => {
// console.log(props.values)
// console.log(metadata.value)
// Object.assign()
});
watch(
() => [props.values?.productDetail, props.values.deviceDetail],
([newVal1, newVal2]) => {
if (newVal1) {
if (props.values?.selector === 'fixed') {
detail(newVal2.id).then((resp) => {
if (resp.status === 200) {
metadata.value = JSON.parse(
resp.result?.metadata || '{}',
);
}
});
} else {
metadata.value = JSON.parse(newVal1?.metadata || '{}');
}
}
},
{ deep: true, immediate: true },
);
</script>

View File

@ -0,0 +1,186 @@
<template>
<!-- <j-advanced-search
:columns="columns"
type="simple"
@search="handleSearch"
class="search"
target="scene-trigger-device-device"
/> -->
<a-divider style="margin: 0" />
<j-pro-table
ref="actionRef"
model="CARD"
:columns="columns"
:params="params"
:request="deviceQuery"
:gridColumn="2"
:bodyStyle="{
paddingRight: 0,
paddingLeft: 0,
}"
>
<template #card="slotProps">
<CardBox
:value="slotProps"
@click="handleClick"
:active="value[0]?.value === slotProps.id"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
online: 'success',
offline: 'error',
notActive: 'warning',
}"
>
<template #img>
<slot name="img">
<img
:src="getImage('/device/instance/device-card.png')"
/>
</slot>
</template>
<template #content>
<div style="width: calc(100% - 100px)">
<Ellipsis>
<span style="font-size: 16px; font-weight: 600">
{{ slotProps.name }}
</span>
</Ellipsis>
</div>
<j-row style="margin-top: 20px">
<j-col :span="12">
<div class="card-item-content-text">设备类型</div>
<div>{{ slotProps.deviceType?.text }}</div>
</j-col>
<j-col :span="12">
<div class="card-item-content-text">产品名称</div>
<Ellipsis style="width: 100%">
{{ slotProps.productName }}
</Ellipsis>
</j-col>
</j-row>
</template>
</CardBox>
</template>
</j-pro-table>
</template>
<script setup lang='ts' name='Product'>
import { query } from '@/api/device/instance';
import { getImage } from '@/utils/comm';
import { PropType } from 'vue';
type Emit = {
(e: 'update:value', data: any): void;
(e: 'change', data: any): void;
};
const actionRef = ref();
const params = ref({
terms: []
});
const props = defineProps({
value: {
type: Array as PropType<any>,
default: []
},
detail: {
type: Object,
default: () => ({}),
},
productId: {
type: String,
default: ''
}
});
const emit = defineEmits<Emit>();
const columns = [
{
title: 'ID',
dataIndex: 'id',
width: 300,
search: {
type: 'string',
},
},
{
title: '设备名称',
dataIndex: 'name',
search: {
type: 'string',
first: true,
},
},
{
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' },
],
},
},
];
const handleSearch = (p: any) => {
params.value = {
...p,
terms: [
...p.terms,
{terms: [{ column: 'productId', value: props?.productId }]}
]
}
};
const deviceQuery = (p: any) => {
const sorts: any = [];
if (props.value) {
sorts.push({
name: 'id',
value: props.value,
});
}
sorts.push({ name: 'createTime', order: 'desc' });
p.sorts = sorts;
return query(p);
};
const handleClick = (detail: any) => {
emit('update:value', [{ value: detail.id, name: detail.name }]);
emit('change', detail);
};
watchEffect(() => {
params.value = {
...params.value,
terms: params.value?.terms ? [
...(params.value.terms || []),
{terms: [{ column: 'productId', value: props?.productId }]}
] : [{terms: [{ column: 'productId', value: props?.productId }]}]
}
})
</script>
<style scoped lang='less'>
.search {
margin-bottom: 0;
padding-right: 0px;
padding-left: 0px;
}
</style>

View File

@ -0,0 +1,206 @@
<template>
<div>
<template v-for="(item, index) in tagList" :key="item.id">
<j-row :gutter="24" style="margin-bottom: 12px">
<j-col :span="4">
<span v-if="index === 0" class="tagName">标签选择</span>
<j-select
:options="[
{ label: '并且', value: 'and' },
{ label: '或者', value: 'or' },
]"
v-else
:value="item?.type"
style="width: 100%"
@select="(key) => onTypeSelect(key, index)"
/>
</j-col>
<j-col :span="16">
<j-row :gutter="24">
<j-col flex="120px">
<j-select
style="width: 120px"
:value="item.id"
placeholder="请选择标签"
:options="options"
@select="
(_, _data) => onTagSelect(_data, index)
"
/>
</j-col>
<j-col flex="auto">
<ValueItem
v-model:modelValue="item.value"
:itemType="item.valueType"
@change="onValueChange"
:options="
item.valueType === 'enum'
? (item?.dataType?.elements || []).map(
(i) => {
return {
label: i.text,
value: i.value,
};
},
)
: item.valueType === 'boolean'
? [
{ label: '是', value: true },
{ label: '否', value: false },
]
: undefined
"
/>
</j-col>
</j-row>
</j-col>
<j-col :span="4">
<j-space>
<j-button style="padding: 0 8px" @click="addItem">
<AIcon type="PlusOutlined" />
</j-button>
<j-button
danger
v-if="tagData.length > 1"
style="padding: 0 8px"
@click="deleteItem(index)"
>
<AIcon type="DeleteOutlined" />
</j-button>
</j-space>
</j-col>
</j-row>
</template>
</div>
</template>
<script lang="ts" setup>
import { PropType } from 'vue';
const props = defineProps({
value: {
type: Array as PropType<any>,
default: () => [],
},
tagData: {
type: Array as PropType<any[]>,
default: () => [],
},
});
const emits = defineEmits(['update:value', 'change']);
const options = ref<any[]>([]);
const tagList = ref<any[]>([]);
const handleItem = (data: any) => {
return {
...data,
valueType: data.valueType ? data.valueType.type : '-',
format: data.valueType ? data.valueType.format : undefined,
options: data.valueType ? data.valueType.elements : undefined,
value: data.value,
};
};
const addItem = () => {
tagList.value.push({ type: 'and' });
};
const deleteItem = (_index: number) => {
tagList.value.splice(_index, 1);
};
const onTypeSelect = (key: any, _index: number) => {
const indexItem = tagList[_index];
indexItem.type = key;
tagList.value[_index] = indexItem;
};
const onTagSelect = (_data: any, _index: number) => {
const newList = [...unref(tagList)];
const indexType = newList[_index].type;
newList.splice(
_index,
1,
handleItem({ ..._data, value: undefined, type: indexType }),
);
tagList.value = newList;
};
watch(
() => [props.tagData, props.value],
([newTag, newVal]) => {
options.value = newTag.map((item: any) => {
return { label: item.name, value: item.id, ...item };
});
if (newVal && newVal[0] && newVal[0].name && newTag && newTag.length) {
const names: string[] = [];
const newTagList = newVal[0].value
.filter((valueItem: any) => {
return newTag.some(
(item: any) => valueItem.column === item.id,
);
})
.map((valueItem: any) => {
const oldItem = newTag.find(
(item: any) => item.id === valueItem.column,
);
if (oldItem) {
names.push(oldItem.name);
return {
...handleItem(oldItem),
value: valueItem.value,
type: valueItem.type,
};
}
return valueItem;
});
tagList.value = newTagList;
} else {
tagList.value = [{}];
}
},
{
immediate: true,
deep: true,
},
);
const onValueChange = () => {
const newValue = tagList.value
.filter((item) => !!item.value)
.map((item: any) => {
return {
column: item.id,
type: item.type,
value: item.value,
};
});
const arr = newValue
.filter((item) => !!item.value)
.map((item: any) => {
return {
column: item.name,
type: item.type,
value: item.value,
};
});
emits('update:value', [{ value: newValue, name: '标签' }]);
emits('change', [{ value: newValue, name: '标签' }], undefined);
};
</script>
<style lang="less" scoped>
.tagName::before {
position: relative;
left: 70px;
display: inline-block;
margin-right: 4px;
color: #ff4d4f;
font-size: 14px;
font-family: SimSun, sans-serif;
line-height: 1;
content: '*';
}
</style>

View File

@ -0,0 +1,149 @@
<template>
<div class="trigger-way-warp" :class="{ disabled: disabled }">
<div
v-for="item in typeList"
:key="item.value"
class="trigger-way-item"
:class="{
active: _value === item.value,
labelBottom: labelBottom,
}"
@click="onSelect(item.value)"
>
<div class="'way-item-title">
<span class="way-item-label">{{ item.label }}</span>
<j-popover v-if="item.tip">
<AIcon
type="QuestionCircleOutlined"
class="way-item-icon"
/>
</j-popover>
</div>
<div class="way-item-image">
<img :width="48" :src="item.image" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { PropType } from 'vue';
const props = defineProps({
typeList: {
type: Array as PropType<any>,
default: () => [],
},
value: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
labelBottom: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['update:value', 'change']);
const _value = ref(props?.value || '');
watch(
() => props.value,
(newValue) => {
_value.value = newValue || ""
},
{ immediate: true, deep: true },
);
const onSelect = (_type: string) => {
if (!props.disabled) {
_value.value = _type;
emits('update:value', _type);
emits('change', _type)
}
};
</script>
<style lang="less" scoped>
.trigger-way-warp {
display: flex;
flex-wrap: wrap;
gap: 16px 24px;
width: 100%;
.trigger-way-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 237px;
padding: 12px 16px;
border: 1px solid #e0e4e8;
border-radius: 2px;
cursor: pointer;
transition: all 0.3s;
.way-item-title {
span {
font-size: 16px;
}
.way-item-label {
padding-right: 6px;
color: rgba(#000, 0.64);
}
.way-item-icon {
color: rgba(#000, 0.5);
}
}
.way-item-image {
margin: 0 !important;
opacity: 0.6;
}
&:hover {
.way-item-image {
opacity: 0.8;
}
}
&.active {
border-color: @primary-color-active;
.way-item-image {
opacity: 1;
}
}
&.labelBottom {
flex-direction: column-reverse;
grid-gap: 16px;
gap: 0;
align-items: center;
width: auto;
padding: 8px 16px;
p {
margin: 0;
}
}
}
&.disabled {
.trigger-way-item {
cursor: not-allowed;
&:hover {
color: initial;
opacity: 0.6;
}
&.active {
opacity: 1;
}
}
}
}
</style>

View File

@ -1,14 +1,354 @@
<template>
<div>
</div>
<div>
<a-form :layout="'vertical'" ref="formRef" :model="modelRef">
<j-form-item
name="selector"
label="选择方式"
v-show="!(list.length === 1)"
:rules="[{ required: true, message: '请选择方式' }]"
>
<TopCard
:typeList="list"
v-model:value="modelRef.selector"
@change="onSelectorChange"
/>
</j-form-item>
<j-form-item
v-if="modelRef.selector === 'fixed'"
name="selectorValues"
:rules="[{ required: true, message: '请选择设备' }]"
>
<Device
:productId="values.productDetail.id"
v-model:value="modelRef.selectorValues"
@change="onDeviceChange"
/>
</j-form-item>
<j-form-item
v-else-if="modelRef.selector === 'relation'"
label="关系"
name="selectorValues"
:rules="[{ required: true, message: '请选择关系' }]"
>
<j-select
placeholder="请选择关系"
v-model:value="modelRef.selectorValues"
:options="relationList"
show-search
@change="onRelationChange"
/>
</j-form-item>
<j-form-item
v-else-if="modelRef.selector === 'tag'"
name="selectorValues"
:rules="[{ required: true, message: '请选择标签' }]"
>
<Tag
v-model:value="modelRef.selectorValues"
:tagData="tagList"
@change="onTagChange"
/>
</j-form-item>
<j-form-item
v-else
name="selectorValues"
label="变量"
:rules="[{ required: true, message: '请选择' }]"
>
<j-tree-select
style="width: 100%; height: 100%"
:tree-data="builtInList"
v-model:value="modelRef.selectorValues"
placeholder="请选择参数"
@select="onVariableChange"
>
<template #title="{ name, description }">
<a-space>
{{ name }}
<span style="color: grey; margin-left: 5px">{{
description
}}</span>
</a-space>
</template>
</j-tree-select>
</j-form-item>
</a-form>
</div>
</template>
<script setup lang='ts' name="Device">
import { useSceneStore } from '@/store/scene';
import TopCard from './TopCard.vue';
import { storeToRefs } from 'pinia';
import { queryBuiltInParams } from '@/api/rule-engine/scene';
import { getImage } from '@/utils/comm';
import NoticeApi from '@/api/notice/config';
import Device from './Device.vue';
import Tag from './Tag.vue';
const props = defineProps({
values: {
type: Object,
default: () => {},
},
name: {
type: Number,
default: 0,
},
thenName: {
type: Number,
default: 0,
},
branchGroup: {
type: Number,
default: 0,
},
parallel: {
type: Boolean,
},
});
const emits = defineEmits(['save', 'cancel']);
const sceneStore = useSceneStore();
const { data } = storeToRefs(sceneStore);
const formRef = ref();
const modelRef = reactive({
selector: 'fixed',
selectorValues: undefined,
deviceId: '',
source: '',
relationName: '',
upperKey: '',
});
const list = ref<any[]>([]);
const builtInList = ref<any[]>([]);
const tagList = ref<any[]>([]);
const relationList = ref<any[]>([]);
const TypeList = [
{
label: '自定义',
value: 'fixed',
image: getImage('/scene/device-custom.png'),
tip: '自定义选择当前产品下的任意设备',
},
{
label: '按关系',
value: 'relation',
image: getImage('/scene/device-relation.png'),
tip: '选择与触发设备具有相同关系的设备',
},
{
label: '按标签',
value: 'tag',
image: getImage('/scene/device-tag.png'),
tip: '按标签选择产品下具有特定标签的设备',
},
{
label: '按变量',
value: 'variable',
image: getImage('/scene/device-variable.png'),
tip: '选择设备ID为上游变量值的设备',
},
];
const filterTree = (nodes: any[]) => {
if (!nodes?.length) {
return nodes;
}
return nodes.filter((it) => {
if (
it.children.find(
(item: any) =>
item.id.indexOf('deviceId' || 'device_id' || 'device_Id') >
-1,
) &&
!it.children.find((item: any) => item.id.indexOf('boolean') > -1)
) {
return true;
}
return false;
});
};
const treeDataFilter = (arr: any[]) => {
if (Array.isArray(arr) && arr.length) {
const list: any[] = [];
arr.map((item: any) => {
if (item.children) {
const children = treeDataFilter(item.children);
if (children.length) {
list.push({
...item,
title: item.name,
value: item.id,
disabled: true,
children,
});
}
} else {
if (
item.children.find(
(item: any) =>
item.id.indexOf(
'deviceId' || 'device_id' || 'device_Id',
) > -1,
) &&
!item.children.find(
(item: any) => item.id.indexOf('bolaen') > -1,
)
) {
list.push(item);
}
}
});
return list;
} else {
return [];
}
};
const sourceChangeEvent = async () => {
const _params = {
branch: props.thenName,
branchGroup: props.branchGroup,
action: props.name - 1,
};
const resp = await queryBuiltInParams(unref(data), _params);
if (resp.status === 200) {
// const array = filterTree(resp.result as any[]);
//
// if (props.formProductId === DeviceModel.productId)// TODO
console.log(array);
const arr = treeDataFilter(resp.result as any[]);
builtInList.value = array;
}
};
const queryRelationList = () => {
NoticeApi.getRelationUsers({
paging: false,
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [{ termType: 'eq', column: 'objectTypeName', value: '设备' }],
}).then((resp) => {
if (resp.status === 200) {
relationList.value = (resp.result as any[]).map((item) => {
return {
label: item.name,
value: item.relation,
};
});
}
});
};
const filterType = async () => {
const _list = TypeList.filter((item) => item.value === 'fixed');
if (unref(data)?.trigger?.type === 'device') {
//
const res = await NoticeApi.getRelationUsers({
paging: false,
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [
{ termType: 'eq', column: 'objectTypeName', value: '设备' },
],
});
if (res.status === 200 && res.result.length !== 0) {
const array = TypeList.filter((item) => item.value === 'relation');
_list.push(...array);
}
//
const tag = JSON.parse(
props.values.productDetail?.metadata || '{}',
)?.tags;
if (tag && tag.length !== 0) {
const array = TypeList.filter((item) => item.value === 'tag');
_list.push(...array);
}
//
if (
builtInList.value.length !== 0 &&
!props.parallel &&
props.name !== 0
) {
const array = TypeList.filter((item) => item.value === 'variable');
_list.push(...array);
}
list.value = _list;
} else {
if (
builtInList.value.length !== 0 &&
!props.parallel &&
props.name !== 0
) {
const array = TypeList.filter((item) => item.value === 'variable');
_list.push(...array);
}
list.value = _list;
}
};
const onSelectorChange = (val: string) => {
if (val === 'relation') {
queryRelationList();
}
};
const onDeviceChange = (_detail: any) => {
if (_detail) {
emits('save', modelRef, _detail);
}
};
const onRelationChange = (val: any, options: any) => {
modelRef.deviceId = 'deviceId';
modelRef.source = 'upper';
modelRef.selectorValues = val;
modelRef.upperKey = 'scene.deviceId';
modelRef.relationName = options.label;
emits('save', modelRef, {});
};
const onTagChange = (val: any[], arr: any[]) => {
if (val) {
modelRef.deviceId = 'deviceId';
modelRef.source = 'fixed';
}
if (arr) {
tagList.value = arr;
}
};
const onVariableChange = (val: any, node: any) => {
modelRef.deviceId = val;
// modelRef.deviceDetail = node;
modelRef.selectorValues = [{ value: val, name: node.description }] as any;
};
watchEffect(() => {
Object.assign(modelRef, props.values);
});
watch(
() => props.values.productDetail,
async (newVal) => {
await sourceChangeEvent();
if (newVal) {
const metadata = JSON.parse(newVal?.metadata || '{}');
tagList.value = metadata?.tags || [];
filterType();
}
},
{
immediate: true,
deep: true,
},
);
</script>
<style scoped lang='less'>
</style>

View File

@ -1,6 +1,13 @@
<template>
<j-modal title="执行动作" visible :width="860" @cancel="onCancel" @ok="save" :maskClosable="false">
<j-steps :current='DeviceModel.current' @change='stepChange'>
<j-modal
title="执行动作"
visible
:width="860"
@cancel="onCancel"
@ok="save"
:maskClosable="false"
>
<j-steps :current="current" @change="stepChange">
<j-step>
<template #title>选择产品</template>
</j-step>
@ -11,36 +18,82 @@
<template #title>执行动作</template>
</j-step>
</j-steps>
<j-divider style='margin-bottom: 10px;' />
<div class='steps-content'>
<Product v-if='DeviceModel.current === 0' v-model:rowKey='DeviceModel.productId'
v-model:detail='DeviceModel.productDetail' />
<j-divider style="margin-bottom: 10px" />
<div class="steps-content">
<Product
v-if="current === 0"
v-model:rowKey="DeviceModel.productId"
v-model:detail="DeviceModel.productDetail"
/>
<Device
v-else-if="current === 1"
:name="name"
:parallel="parallel"
:branchGroup="branchGroup"
:thenName="thenName"
:values="DeviceModel"
@save="onDeviceSave"
/>
<Action v-else-if="current === 2"
:name="name"
:branchGroup="branchGroup"
:thenName="thenName"
:values="DeviceModel"
/>
</div>
<template #footer>
<div class='steps-action'>
<j-button v-if='DeviceModel.current === 0' @click='cancel'>取消</j-button>
<j-button v-else @click='prev'>上一步</j-button>
<j-button type='primary' v-if='DeviceModel.current < 2' @click='saveClick'>下一步</j-button>
<j-button type='primary' v-else @click='saveClick'>确定</j-button>
</div>
</template>
<div class="steps-action">
<j-button v-if="current === 0" @click="onCancel">取消</j-button>
<j-button v-else @click="prev">上一步</j-button>
<j-button type="primary" v-if="current < 2" @click="saveClick"
>下一步</j-button
>
<j-button type="primary" v-else @click="saveClick"
>确定</j-button
>
</div>
</template>
</j-modal>
</template>
<script lang="ts" setup>
import { DeviceModelType } from './typings';
import Product from './Product.vue';
import Device from './device/index.vue';
import Action from './actions/index.vue';
import { onlyMessage } from '@/utils/comm';
import { detail } from '@/api/device/product'
type Emit = {
(e: 'cancel'): void
(e: 'save', data: any, options: Record<string, any>): void
}
(e: 'cancel'): void;
(e: 'save', data: any, options: Record<string, any>): void;
};
const props = defineProps({
value: {
type: Object,
default: () => {},
},
name: {
type: Number,
default: 0,
},
thenName: {
type: Number,
default: 0,
},
branchGroup: {
type: Number,
default: 0,
},
parallel: {
type: Boolean,
},
});
const current = ref<number>(0);
const DeviceModel = reactive<DeviceModelType>({
steps: [],
current: 0,
productId: '',
deviceId: '',
productDetail: {},
@ -58,38 +111,64 @@ const DeviceModel = reactive<DeviceModelType>({
columns: [],
actionName: '',
tagList: [],
})
});
const emit = defineEmits<Emit>()
const cancel = () => {
const emit = defineEmits<Emit>();
const onCancel = () => {
emit('cancel');
};
const save = async(step?: number) => {
let _step = step !== undefined ? step : DeviceModel.current
if (_step === 0) {
DeviceModel.productId ? DeviceModel.current = 1 : onlyMessage('请选择产品', 'error')
} else if (_step === 1) {
} else {
const save = async (step?: number) => {
let _step = step !== undefined ? step : current.value;
if (_step === 0) {
DeviceModel.productId
? (current.value = 1)
: onlyMessage('请选择产品', 'error');
} else if (_step === 1) {
DeviceModel.deviceId
? (current.value = 2)
: onlyMessage('请选择设备', 'error');
} else {
}
};
const stepChange = (step: number) => {
if (step !== 0) {
save(step - 1)
save(step - 1);
} else {
DeviceModel.current = 0
current.value = 0;
}
}
};
const prev = () => {
DeviceModel.current = DeviceModel.current - 1
}
const saveClick = () => save()
current.value -= 1;
};
const saveClick = () => save();
const onDeviceSave = (_data: any, _detail: any) => {
Object.assign(DeviceModel, _data)
DeviceModel.deviceId = _detail.id
DeviceModel.deviceDetail = _detail
}
watch(
() => props.value,
(newValue) => {
Object.assign(DeviceModel, newValue);
if(newValue?.productId){
detail(newValue.productId).then(resp => {
if(resp.status === 200){
DeviceModel.productDetail = resp.result
}
})
}
},
{ immediate: true, deep: true },
);
</script>
<style lang="less" scoped>
.steps-steps {
width: 100%;
@ -100,5 +179,8 @@ const saveClick = () => save()
.steps-content {
width: 100%;
max-height: 500px;
overflow-y: auto;
overflow-x: hidden;
}
</style>

View File

@ -2,12 +2,12 @@ import { ProductItem } from '@/views/device/Product/typings';
import { ActionsDeviceProps } from '../../../typings';
type DeviceModelType = {
steps: {
key: string;
title: string;
content: React.ReactNode;
}[];
current: number;
// steps: {
// key: string;
// title: string;
// content: React.ReactNode;
// }[];
// current: number;
productId: string;
deviceId: string;
productDetail: ProductItem | any;

View File

@ -1,7 +1,7 @@
<template>
<div>
<template v-if="actionType === 'device'">
<Device v-bind="props" :value="data?.device" @cancel="onCancel" @save="onPropsOk" />
<Device v-bind="props" :value="data?.device" @cancel="onCancel" @save="onPropsOk" :thenName="branchesName" />
</template>
<template v-else-if="actionType === 'notify'">
<Notify v-bind="props" :value="data?.notify" @cancel="onCancel" @save="onPropsOk" />

View File

@ -19,7 +19,7 @@
<template #title="{ name, description }">
<a-space>
{{ name }}
<spn style="color: grey; margin-left: 5px">{{ description }}</spn>
<span style="color: grey; margin-left: 5px">{{ description }}</span>
</a-space>
</template>
</a-tree-select>

View File

@ -12,7 +12,7 @@
内部独立应用的<span>后端服务</span>相互<span>独立运行</span>互不影响
</div>
<div class="image">
<a-image width="100%" :src="img1" />
<j-image width="100%" :src="img1" />
</div>
<h1>2.接入方式说明</h1>
@ -58,7 +58,7 @@
</div>
<div>内部独立应用的<span>后端服务在同一个环境运行</span></div>
<div class="image">
<a-image width="100%" :src="img2" />
<j-image width="100%" :src="img2" />
</div>
<h1>2.接入方式说明</h1>
<div>1页面集成</div>
@ -86,7 +86,7 @@
<h1>1.概述</h1>
<div>钉钉企业内部应用适用于通过钉钉登录<span>物联网平台</span></div>
<div class="image">
<a-image width="100%" :src="img4" />
<j-image width="100%" :src="img4" />
</div>
<h1>2.接入方式说明</h1>
<div>1单点登录</div>
@ -106,7 +106,7 @@
<h1>1.概述</h1>
<div>微信网站应用适用于通过微信授权登录<span>物联网平台</span></div>
<div class="image">
<a-image width="100%" :src="img3" />
<j-image width="100%" :src="img3" />
</div>
<h1>2.接入方式说明</h1>
<div>1单点登录</div>
@ -121,7 +121,7 @@
例如将公司业务管理系统集成至物联网平台或者将物联网平台集成至业务管理系统以实现多处访问集中管控的业务场景
</div>
<div class="image">
<a-image width="100%" :src="img5" />
<j-image width="100%" :src="img5" />
</div>
<h1>2.接入方式说明</h1>
<div>1页面集成</div>

File diff suppressed because it is too large Load Diff

View File

@ -2,10 +2,10 @@
<div class="form-label-container">
<span class="text">{{ props.text }}</span>
<span class="required" v-show="props.required">*</span>
<a-tooltip>
<j-tooltip>
<template #title>{{ props.tooltip }}</template>
<AIcon type="QuestionCircleOutlined" class="icon" />
</a-tooltip>
</j-tooltip>
</div>
</template>

View File

@ -1,52 +1,52 @@
<template>
<div class="request-table-container">
<a-table
<j-table
:columns="columns"
:data-source="tableData"
:datj-source="tableData"
:pagination="false"
size="small"
bordered
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'key'">
<a-input v-model:value="record.label" />
<j-input v-model:value="record.label" />
</template>
<template v-else-if="column.dataIndex === 'value'">
<a-input
<j-input
v-model:value="record.value"
v-if="props.valueType === 'input'"
/>
<a-select
<j-select
v-else-if="props.valueType === 'select'"
v-model:value="record.value"
>
<a-select-option
<j-select-option
v-for="item in props.valueOptions"
:value="item.value"
>{{ item.label }}</a-select-option
>{{ item.label }}</j-select-option
>
</a-select>
</j-select>
</template>
<template v-else-if="column.dataIndex === 'action'">
<a-button
<j-button
type="link"
@click="removeRow((current - 1) * 10 + index)"
>
<AIcon type="DeleteOutlined" />
</a-button>
</j-button>
</template>
</template>
</a-table>
<a-pagination
</j-table>
<j-pagination
v-show="props.value.length > 10"
v-model:current="current"
:page-size="10"
:total="props.value.length"
show-less-items
/>
<a-button type="dashed" @click="addRow" class="add-btn">
<j-button type="dashed" @click="addRow" class="add-btn">
<AIcon type="PlusOutlined" />新增
</a-button>
</j-button>
</div>
</template>
@ -120,7 +120,6 @@ function addRow() {
label: '',
value: '',
};
console.log(111);
emits('update:value', [...props.value, newRow]);
}

View File

@ -1,13 +1,13 @@
<template>
<page-container>
<a-card class="save-container">
<a-row :gutter="24">
<a-col :span="14">
<div class="save-container">
<j-row :gutter="24">
<j-col :span="14">
<EditForm @change-apply-type="chengeType" />
</a-col>
<a-col :span="10"><Does :type="rightType" /></a-col>
</a-row>
</a-card>
</j-col>
<j-col :span="10"><Does :type="rightType" /></j-col>
</j-row>
</div>
</page-container>
</template>
@ -21,3 +21,10 @@ const chengeType = (newType: applyType) => {
rightType.value = newType;
};
</script>
<style lang="less" scoped>
.save-container {
background-color: #fff;
padding: 24px;
}
</style>

View File

@ -1,30 +1,30 @@
<template>
<a-modal
v-model:visible="dialog.visible"
<j-modal
visible
title="集成菜单"
width="600px"
@ok="dialog.handleOk"
@cancel="dialog.cancel"
@ok="handleOk"
@cancel="cancel"
class="edit-dialog-container"
:confirmLoading="dialog.loading"
cancelText="取消"
okText="确定"
:confirmLoading="loading"
>
<a-select
<j-select
v-model:value="form.checkedSystem"
@change="(value) => value && getTree(value as string)"
@change="(value:string) => value && getTree(value)"
style="width: 200px"
placeholder="请选择集成系统"
>
<a-select-option
<j-select-option
v-for="item in form.systemList"
:value="item.value"
>{{ item.label }}</a-select-option
>{{ item.label }}</j-select-option
>
</a-select>
</j-select>
<p style="margin: 20px 0 0 0" v-show="form.menuTree.length > 0">当前集成菜单</p>
<a-tree
<p style="margin: 20px 0 0 0" v-show="form.menuTree.length > 0">
当前集成菜单
</p>
<j-tree
v-model:checkedKeys="form.checkedMenu"
v-model:expandedKeys="form.expandedKeys"
checkable
@ -35,8 +35,8 @@
<template #title="{ name }">
<span>{{ name }}</span>
</template>
</a-tree>
</a-modal>
</j-tree>
</j-modal>
</template>
<script setup lang="ts">
@ -55,69 +55,60 @@ import { message } from 'ant-design-vue';
import { getMenuTree_api } from '@/api/system/menu';
const menuStory = useMenuStore();
const emits = defineEmits(['update:visible']);
const props = defineProps<{
mode: 'add' | 'edit';
visible: boolean;
id: string;
provider: applyType;
}>();
//
const dialog = reactive({
visible: false,
loading: false,
handleOk: () => {
const items = filterTree(form.menuTree, [
...form.checkedMenu,
...form.half,
]);
if (form.checkedSystem) {
if (items && items.length !== 0) {
saveOwnerMenu_api('iot', form.id, items).then((resp) => {
const loading = ref(false);
const handleOk = () => {
const items = filterTree(form.menuTree, [
...form.checkedMenu,
...form.half,
]);
if (form.checkedSystem) {
if (items && items.length !== 0) {
loading.value = true;
saveOwnerMenu_api('iot', form.id, items)
.then((resp) => {
if (resp.status === 200) {
message.success('操作成功');
dialog.visible = false;
emits('update:visible', false);
}
});
} else {
message.warning('请勾选配置菜单');
}
})
.finally(() => (loading.value = false));
} else {
message.warning('请选择所属系统');
message.warning('请勾选配置菜单');
}
},
cancel: () => {
if (props.mode === 'add')
menuStory.jumpPage('system/Apply/Save', {}, { id: form.id });
dialog.visible = false;
},
changeVisible: (id: string, provider: applyType) => {
form.id = id;
form.provider = provider;
form.checkedSystem = undefined;
form.checkedMenu = [];
dialog.visible = true;
if (id) {
getSystemList();
getMenus();
}
},
});
//
defineExpose({
openDialog: dialog.changeVisible,
});
} else {
message.warning('请选择所属系统');
}
};
const cancel = () => {
if (props.mode === 'add')
menuStory.jumpPage('system/Apply/Save', {}, { id: form.id });
emits('update:visible', false);
};
const form = reactive({
id: '',
checkedSystem: '' as undefined | string,
id: props.id,
checkedSystem: undefined as undefined | string,
checkedMenu: [] as string[],
expandedKeys: [] as string[],
half: [] as string[],
provider: '' as applyType,
provider: props.provider,
systemList: [] as optionItemType[],
menuTree: [] as any[],
});
if (props.id) {
getSystemList();
getMenus();
}
/**
* 与集成系统关联的菜单
* @param params

View File

@ -1,7 +1,10 @@
<template>
<page-container>
<div class="apply-container">
<Search :columns="columns" @search="search" />
<j-advanced-search
:columns="columns"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table
ref="tableRef"
@ -10,7 +13,7 @@
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:params="params"
:params="queryParams"
:gridColumn="3"
>
<template #headerTitle>
@ -53,8 +56,8 @@
<h3 class="card-item-content-title">
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<j-row>
<j-col :span="12">
<div class="card-item-content-text">
类型
</div>
@ -65,45 +68,45 @@
)
}}
</div>
</a-col>
<a-col :span="12">
</j-col>
<j-col :span="12">
<div class="card-item-content-text">
说明
</div>
<div>{{ slotProps.description }}</div>
</a-col>
</a-row>
</j-col>
</j-row>
</template>
<template #actions="item">
<a-tooltip
<j-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-dropdown
<j-dropdown
placement="bottomRight"
v-if="item.key === 'others'"
>
<a-button>
<j-button>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</a-button>
</j-button>
<template #overlay>
<a-menu>
<a-menu-item
<j-menu>
<j-menu-item
v-for="(o, i) in item.children"
:key="i"
>
<a-button
<j-button
type="link"
@click="o.onClick"
>
<AIcon :type="o.icon" />
<span>{{ o.text }}</span>
</a-button>
</a-menu-item>
</a-menu>
</j-button>
</j-menu-item>
</j-menu>
</template>
</a-dropdown>
</j-dropdown>
<PermissionButton
v-else
:uhasPermission="item.permission"
@ -117,7 +120,7 @@
item.text
}}</span>
</PermissionButton>
</a-tooltip>
</j-tooltip>
</template>
<template #mark>
@ -144,7 +147,7 @@
></BadgeStatus>
</template>
<template #action="slotProps">
<a-space :size="16">
<j-space :size="16">
<PermissionButton
v-for="i in table.getActions(slotProps, 'table')"
:uhasPermission="i.permission"
@ -156,12 +159,18 @@
>
<AIcon :type="i.icon" />
</PermissionButton>
</a-space>
</j-space>
</template>
</j-pro-table>
</div>
<div class="dialogs">
<MenuDialog ref="dialogRef" mode="edit" />
<MenuDialog
v-if="dialogVisible"
v-model:visible="dialogVisible"
:id="selectId"
:provider="selectProvider"
mode="edit"
/>
</div>
</page-container>
</template>
@ -263,11 +272,9 @@ const columns = [
scopedSlots: true,
},
];
const params = ref({});
const search = (newParams: any) => (params.value = { ...newParams });
const queryParams = ref({});
const tableRef = ref();
const dialogRef = ref();
const table = {
refresh: () => {
tableRef.value.reload();
@ -359,8 +366,9 @@ const table = {
},
icon: 'MenuUnfoldOutlined',
onClick: () => {
dialogRef.value &&
dialogRef.value.openDialog(data.id, data.provider);
selectId.value = data.id;
selectProvider.value = data.provider;
dialogVisible.value = true;
},
});
// api
@ -415,6 +423,9 @@ const table = {
return typeOptions.find((item) => item.value === val)?.label;
},
};
const dialogVisible = ref(false);
const selectId = ref<string>('');
const selectProvider = ref<any>('');
</script>
<style lang="less" scoped>

View File

@ -17,10 +17,17 @@
{ max: 64, message: '最多可输入64个字符' },
]"
>
<j-input
<j-auto-complete
v-model:value="form.data.id"
:options="codeOptions"
placeholder="请输入编码"
:disabled="props.mode !== '新增'"
/>
>
<template #option="{ value: val, message }">
{{ val }}
<span class="message">{{ message }}</span>
</template>
</j-auto-complete>
</j-form-item>
<j-form-item
label="名称"
@ -122,6 +129,11 @@ const form = reactive({
return Promise.resolve();
},
});
const codeOptions = [
{ label: 'add', value: 'add', message: '新增' },
{ label: 'delete', value: 'delete', message: '删除' },
{ label: 'update', value: 'update', message: '更新' },
];
type formType = {
name: string;
@ -141,6 +153,11 @@ type formType = {
display: none;
}
}
.message {
font-size: 12px;
color: rgba(0, 0, 0, 0.4);
}
}
}
</style>

View File

@ -4,7 +4,7 @@
<h5>{{ selectApi.summary }}</h5>
<div class="input">
<InputCard :value="selectApi.method" />
<a-input :value="selectApi?.url" disabled />
<j-input :value="selectApi?.url" disabled />
</div>
</div>
@ -18,6 +18,14 @@
<span>{{ `["/"]` }}</span>
</p>
<div class="api-card" v-if="props.selectApi.description">
<h5>接口描述</h5>
<div>{{ props.selectApi.description }}</div>
</div>
<div class="api-card" v-if="requestCard.codeText">
<h5>请求示例</h5>
<JsonViewer :value="requestCard.codeText" copyable />
</div>
<div class="api-card">
<h5>请求参数</h5>
<div class="content">
@ -26,12 +34,13 @@
:dataSource="requestCard.tableData"
noPagination
model="TABLE"
size="small"
>
<template #required="slotProps">
<span>{{ Boolean(slotProps.required) + '' }}</span>
</template>
<template #type="slotProps">
<span>{{ slotProps.schema.type }}</span>
<span>{{ slotProps?.schema.type }}</span>
</template>
</j-pro-table>
</div>
@ -44,16 +53,17 @@
:dataSource="responseStatusCard.tableData"
noPagination
model="TABLE"
size="small"
>
</j-pro-table>
<a-tabs v-model:activeKey="responseStatusCard.activeKey">
<a-tab-pane
<j-tabs v-model:activeKey="responseStatusCard.activeKey">
<j-tab-pane
:key="key"
:tab="key"
v-for="key in tabs"
></a-tab-pane>
</a-tabs>
></j-tab-pane>
</j-tabs>
</div>
</div>
@ -65,21 +75,19 @@
:dataSource="respParamsCard.tableData"
noPagination
model="TABLE"
size="small"
>
</j-pro-table>
</div>
<MonacoEditor
v-model:modelValue="codeText"
style="height: 300px; width: 100%"
theme="vs"
/>
<JsonViewer :value="respParamsCard.codeText" copyable />
</div>
</div>
</template>
<script setup lang="ts">
import MonacoEditor from '@/components/MonacoEditor/index.vue';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
import type { apiDetailsType } from '../typing';
import InputCard from './InputCard.vue';
import { PropType } from 'vue';
@ -98,7 +106,7 @@ const { selectApi } = toRefs(props);
type tableCardType = {
columns: object[];
tableData: object[];
tableData: any[];
codeText?: any;
activeKey?: any;
getData?: any;
@ -134,8 +142,36 @@ const requestCard = reactive<tableCardType>({
},
],
tableData: [],
codeText: undefined,
getData: () => {
requestCard.tableData = props.selectApi.parameters;
if (!props.selectApi.requestBody)
return (requestCard.tableData = props.selectApi.parameters);
const schema =
props.selectApi.requestBody.content['application/json'].schema;
const schemaName = (schema.$ref || schema.items.$ref)?.split('/').pop();
const type = schema.type || '';
const tableData = findData(schemaName);
if (type === 'array') {
requestCard.codeText = [getCodeText(tableData, 3)];
} else requestCard.codeText = getCodeText(tableData, 3);
// console.clear();
// console.log(schemaName, tableData);
requestCard.tableData = [
{
name: schemaName[0].toLowerCase() + schemaName.substring(1),
description: schemaName,
in: 'body',
required: true,
schema: { type: type || schemaName },
children: tableData.map((item) => ({
name: item.paramsName,
description: item.desc,
required: false,
schema: { type: item.paramsType },
})),
},
];
},
});
const responseStatusCard = reactive<tableCardType>({
@ -200,102 +236,17 @@ const respParamsCard = reactive<tableCardType>({
tableData: [],
codeText: '',
getData: (code: string) => {
type schemaObjType = {
paramsName: string;
paramsType: string;
desc?: string;
children?: schemaObjType[];
};
const schemaName = responseStatusCard.tableData.find(
(item: any) => item.code === code,
)?.schema;
const schemas = toRaw(props.schemas);
const basicType = ['string', 'integer', 'boolean'];
const tableData = findData(schemaName);
const codeText = getCodeText(tableData, 3);
respParamsCard.codeText = getCodeText(tableData, 3);
respParamsCard.tableData = tableData;
respParamsCard.codeText = JSON.stringify(codeText);
function findData(schemaName: string) {
if (!schemaName || !schemas[schemaName]) {
return [];
}
const result: schemaObjType[] = [];
const schema = schemas[schemaName];
Object.entries(schema.properties).forEach((item: [string, any]) => {
const paramsType =
item[1].type ||
(item[1].$ref && item[1].$ref.split('/').pop()) ||
(item[1].items && item[1].items.$ref.split('/').pop()) ||
'';
const schemaObj: schemaObjType = {
paramsName: item[0],
paramsType,
desc: item[1].description || '',
};
if (!basicType.includes(paramsType))
schemaObj.children = findData(paramsType);
result.push(schemaObj);
});
return result;
}
function getCodeText(arr: schemaObjType[], level: number): object {
const result = {};
arr.forEach((item) => {
switch (item.paramsType) {
case 'string':
result[item.paramsName] = '';
break;
case 'integer':
result[item.paramsName] = 0;
break;
case 'boolean':
result[item.paramsName] = true;
break;
case 'array':
result[item.paramsName] = [];
break;
case 'object':
result[item.paramsName] = {};
break;
default: {
const properties = schemas[item.paramsType]
.properties as object;
const newArr = Object.entries(properties).map(
(item: [string, any]) => ({
paramsName: item[0],
paramsType: level
? (item[1].$ref &&
item[1].$ref.split('/').pop()) ||
(item[1].items &&
item[1].items.$ref
.split('/')
.pop()) ||
item[1].type ||
''
: item[1].type,
}),
);
result[item.paramsName] = getCodeText(
newArr,
level - 1,
);
}
}
});
return result;
}
},
});
const { codeText } = toRefs(requestCard);
const getContent = (data: any) => {
if (data && data.content) {
return Object.keys(data.content || {})[0];
@ -317,6 +268,83 @@ watch(
watch([() => responseStatusCard.activeKey, () => props.selectApi], (n) => {
n[0] && respParamsCard.getData(n[0]);
});
function findData(schemaName: string) {
const schemas = toRaw(props.schemas);
const basicType = ['string', 'integer', 'boolean'];
if (!schemaName || !schemas[schemaName]) {
return [];
}
const result: schemaObjType[] = [];
const schema = schemas[schemaName];
Object.entries(schema.properties).forEach((item: [string, any]) => {
const paramsType =
item[1].type ||
(item[1].$ref && item[1].$ref.split('/').pop()) ||
(item[1].items && item[1].items.$ref.split('/').pop()) ||
'';
const schemaObj: schemaObjType = {
paramsName: item[0],
paramsType,
desc: item[1].description || '',
};
if (!basicType.includes(paramsType))
schemaObj.children = findData(paramsType);
result.push(schemaObj);
});
return result;
}
function getCodeText(arr: schemaObjType[], level: number): object {
const result = {};
const schemas = toRaw(props.schemas);
arr.forEach((item) => {
switch (item.paramsType) {
case 'string':
result[item.paramsName] = '';
break;
case 'integer':
result[item.paramsName] = 0;
break;
case 'boolean':
result[item.paramsName] = true;
break;
case 'array':
result[item.paramsName] = [];
break;
case 'object':
result[item.paramsName] = '';
break;
default: {
const properties = schemas[item.paramsType]
.properties as object;
const newArr = Object.entries(properties).map(
(item: [string, any]) => ({
paramsName: item[0],
paramsType: level
? (item[1].$ref && item[1].$ref.split('/').pop()) ||
(item[1].items &&
item[1].items.$ref.split('/').pop()) ||
item[1].type ||
''
: item[1].type,
}),
);
result[item.paramsName] = getCodeText(newArr, level - 1);
}
}
});
return result;
}
type schemaObjType = {
paramsName: string;
paramsType: string;
desc?: string;
children?: schemaObjType[];
};
</script>
<style lang="less" scoped>

View File

@ -4,7 +4,7 @@
<h5>{{ props.selectApi.summary }}</h5>
<div class="input">
<InputCard :value="props.selectApi.method" />
<a-input :value="props.selectApi?.url" disabled />
<j-input :value="props.selectApi?.url" disabled />
<span class="send" @click="send">发送</span>
</div>
</div>
@ -12,14 +12,9 @@
<div class="api-card">
<h5>请求参数</h5>
<div class="content">
<!-- <VueJsoneditor
height="400"
mode="tree"
v-model:text="requestBody.paramsText"
/> -->
<div class="table" v-if="paramsTable.length">
<a-form :model="requestBody.params" ref="formRef" >
<a-table
<j-form :model="requestBody.params" ref="formRef">
<j-table
:columns="requestBody.tableColumns"
:dataSource="paramsTable"
:pagination="false"
@ -27,7 +22,7 @@
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'name'">
<a-form-item
<j-form-item
:name="[
'paramsTable',
index +
@ -42,13 +37,13 @@
},
]"
>
<a-input
<j-input
v-model:value="record.name"
></a-input>
</a-form-item>
></j-input>
</j-form-item>
</template>
<template v-else-if="column.key === 'value'">
<a-form-item
<j-form-item
:name="[
'paramsTable',
index +
@ -63,10 +58,10 @@
},
]"
>
<a-input
<j-input
v-model:value="record.value"
></a-input>
</a-form-item>
></j-input>
</j-form-item>
</template>
<template v-else-if="column.key === 'action'">
<PermissionButton
@ -82,60 +77,53 @@
</PermissionButton>
</template>
</template>
</a-table>
</a-form>
</j-table>
</j-form>
<a-pagination
<j-pagination
:pageSize="requestBody.pageSize"
v-model:current="requestBody.pageNum"
:total="requestBody.params.paramsTable.length"
hideOnSinglePage
style="text-align: center"
/>
<a-button
<j-button
@click="requestBody.addRow"
style="width: 100%; text-align: center"
>
<AIcon type="PlusOutlined" />新增
</a-button>
</j-button>
</div>
<MonacoEditor
v-model:modelValue="requestBody.paramsText"
v-model:modelValue="requestBody.code"
style="height: 300px; width: 100%"
theme="vs"
ref="editorRef"
/>
</div>
</div>
<div class="api-card">
<h5>响应参数</h5>
<div class="content">
<VueJsoneditor
height="400"
mode="tree"
v-model:text="responsesContent"
:disabled="true"
/>
<!-- <MonacoEditor
v-model:modelValue="responsesContent"
style="height: 300px; width: 100%"
theme="vs"
/> -->
<JsonViewer :value="responsesContent" copyable />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import VueJsoneditor from 'vue3-ts-jsoneditor';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
import MonacoEditor from '@/components/MonacoEditor/index.vue';
import type { apiDetailsType } from '../typing';
import InputCard from './InputCard.vue';
import { cloneDeep, toLower } from 'lodash';
import { FormInstance } from 'ant-design-vue';
import server from '@/utils/request'
import server from '@/utils/request';
const props = defineProps<{
selectApi: apiDetailsType;
schemas: any;
}>();
const formRef = ref<FormInstance>();
const requestBody = reactive({
@ -163,10 +151,12 @@ const requestBody = reactive({
pageSize: 10,
pageNum: 1,
params: {
paramsTable: cloneDeep(props.selectApi.parameters || []) as requestObj[],
paramsTable: cloneDeep(
props.selectApi.parameters || [],
) as requestObj[],
},
paramsText: '',
code: '',
addRow: () => {
if (paramsTable.value.length === 10)
@ -188,49 +178,142 @@ const paramsTable = computed(() => {
return requestBody.params.paramsTable.slice(startIndex, endIndex);
});
const responsesContent = ref('{"a":123}');
const responsesContent = ref({});
const editorRef = ref()
const send = () => {
formRef.value &&
formRef.value.validate().then(() => {
const methodName = toLower(props.selectApi.method)
const methodObj = {
get: 'get',
post: 'post',
patch: 'patch',
put: 'put',
delete: 'remove'
}
let url = props.selectApi?.url;
const urlParams = {}
requestBody.params.paramsTable.forEach(item=>{
if(methodName === 'get')
urlParams[item.name] = item.value
if(url.includes(`{${item.name}}`))
url = url.replace(`{${item.name}}`, item.value)
})
const params = {
...JSON.parse(requestBody.paramsText || '{}'),
...urlParams
}
server[methodObj[methodName]](url,params).then((resp:any)=>{
responsesContent.value = JSON.stringify(resp)
})
});
if (paramsTable.value.length)
formRef.value &&
formRef.value.validate().then(() => {
_send();
});
else _send();
};
const _send = () => {
const methodName = toLower(props.selectApi.method);
const methodObj = {
get: 'get',
post: 'post',
patch: 'patch',
put: 'put',
delete: 'remove',
};
let url = props.selectApi?.url;
const urlParams = {};
requestBody.params.paramsTable.forEach((item) => {
if (methodName === 'get') urlParams[item.name] = item.value;
if (url.includes(`{${item.name}}`))
url = url.replace(`{${item.name}}`, item.value);
});
const params = {
...JSON.parse(requestBody.code || '{}'),
...urlParams,
};
server[methodObj[methodName]](url, params).then((resp: any) => {
if (Object.keys(params).length === 0){
requestBody.code = JSON.stringify(getDefaultParams());
editorRef.value?.editorFormat()
}
responsesContent.value = resp;
});
};
/**
* 获取默认参数
*/
function getDefaultParams() {
if (!props.selectApi.requestBody) return {};
const schema =
props.selectApi.requestBody.content['application/json'].schema;
const schemaName = (schema.$ref || schema.items.$ref)?.split('/').pop();
const type = schema.type || '';
const tableData = findData(schemaName);
if (type === 'array') {
return [getCodeText(tableData, 3)];
} else return getCodeText(tableData, 3);
}
function findData(schemaName: string) {
const schemas = toRaw(props.schemas);
const basicType = ['string', 'integer', 'boolean'];
if (!schemaName || !schemas[schemaName]) {
return [];
}
const result: schemaObjType[] = [];
const schema = schemas[schemaName];
Object.entries(schema.properties).forEach((item: [string, any]) => {
const paramsType =
item[1].type ||
(item[1].$ref && item[1].$ref.split('/').pop()) ||
(item[1].items && item[1].items.$ref.split('/').pop()) ||
'';
const schemaObj: schemaObjType = {
paramsName: item[0],
paramsType,
desc: item[1].description || '',
};
if (!basicType.includes(paramsType))
schemaObj.children = findData(paramsType);
result.push(schemaObj);
});
return result;
}
function getCodeText(arr: schemaObjType[], level: number): object {
const result = {};
const schemas = toRaw(props.schemas);
arr.forEach((item) => {
switch (item.paramsType) {
case 'string':
result[item.paramsName] = '';
break;
case 'integer':
result[item.paramsName] = 0;
break;
case 'boolean':
result[item.paramsName] = true;
break;
case 'array':
result[item.paramsName] = [];
break;
case 'object':
result[item.paramsName] = '';
break;
default: {
const properties = schemas[item.paramsType]
.properties as object;
const newArr = Object.entries(properties).map(
(item: [string, any]) => ({
paramsName: item[0],
paramsType: level
? (item[1].$ref && item[1].$ref.split('/').pop()) ||
(item[1].items &&
item[1].items.$ref.split('/').pop()) ||
item[1].type ||
''
: item[1].type,
}),
);
result[item.paramsName] = getCodeText(newArr, level - 1);
}
}
});
return result;
}
type requestObj = {
name: string;
value: string;
};
type schemaObjType = {
paramsName: string;
paramsType: string;
desc?: string;
children?: schemaObjType[];
};
</script>
<style lang="less" scoped>
@ -284,8 +367,6 @@ type requestObj = {
.jtable-body-header {
display: none;
}
}
.table {
:deep(.ant-table-cell) {

View File

@ -1,23 +1,25 @@
<template>
<div class="choose-api-container">
<j-pro-table
:columns="columns"
:dataSource="props.tableData"
:rowSelection="props.mode !== 'home' ? rowSelection : undefined"
noPagination
model="TABLE"
>
<template #url="slotProps">
<span
style="color: #1d39c4; cursor: pointer"
@click="jump(slotProps)"
>{{ slotProps.url }}</span
>
</template>
</j-pro-table>
<div class="table">
<j-pro-table
:columns="columns"
:dataSource="props.tableData"
:rowSelection="props.mode !== 'home' ? rowSelection : undefined"
noPagination
model="TABLE"
>
<template #url="slotProps">
<span
style="color: #1d39c4; cursor: pointer"
@click="jump(slotProps)"
>{{ slotProps.url }}</span
>
</template>
</j-pro-table>
</div>
<a-button type="primary" @click="save" v-if="props.mode !== 'home'"
>保存</a-button
<j-button type="primary" @click="save" v-if="props.mode !== 'home'"
>保存</j-button
>
</div>
</template>
@ -88,8 +90,10 @@ watch(
<style lang="less" scoped>
.choose-api-container {
height: 100%;
.table {
max-height: calc(100vh - 260px);
overflow-y: auto;
}
:deep(.jtable-body-header) {
display: none !important;
}

View File

@ -1,5 +1,5 @@
<template>
<a-tree
<j-tree
:tree-data="treeData"
@select="clickSelectItem"
v-model:selected-keys="selectedKeys"
@ -9,7 +9,7 @@
<template #title="{ name }">
{{ name }}
</template>
</a-tree>
</j-tree>
</template>
<script setup lang="ts">
@ -141,9 +141,6 @@ const filterPath = (path: object, filterArr: string[]) => {
<style lang="less">
.left-tree-container {
border-right: 1px solid #e9e9e9;
height: calc(100vh - 150px);
overflow-y: auto;
.ant-tree-list {
.ant-tree-list-holder-inner {
.ant-tree-switcher-noop {

View File

@ -3,14 +3,15 @@
<div class="top">
<slot name="top" />
</div>
<a-row :gutter="24" style="background-color: #fff; padding: 20px;margin: 0;">
<a-col
<j-row :gutter="24">
<j-col
:span="24"
v-if="props.showTitle"
style="font-size: 16px; margin-bottom: 48px"
>API文档</a-col
>
<a-col :span="5">
API文档
</j-col>
<j-col :span="5" class="tree-content">
<LeftTree
@select="treeSelect"
:mode="props.mode"
@ -18,8 +19,8 @@
:filter-array="treeFilter"
:code="props.code"
/>
</a-col>
<a-col :span="19">
</j-col>
<j-col :span="19">
<HomePage v-show="showHome" />
<div class="url-page" v-show="!showHome">
<ChooseApi
@ -35,26 +36,29 @@
class="api-details"
v-if="selectedApi.url && tableData.length > 0"
>
<a-button
<j-button
@click="selectedApi = initSelectedApi"
style="margin-bottom: 24px"
>返回</a-button
>返回</j-button
>
<a-tabs v-model:activeKey="activeKey" type="card">
<a-tab-pane key="does" tab="文档">
<j-tabs v-model:activeKey="activeKey" type="card">
<j-tab-pane key="does" tab="文档">
<ApiDoes
:select-api="selectedApi"
:schemas="schemas"
/>
</a-tab-pane>
<a-tab-pane key="test" tab="调试">
<ApiTest :select-api="selectedApi" />
</a-tab-pane>
</a-tabs>
</j-tab-pane>
<j-tab-pane key="test" tab="调试">
<ApiTest
:select-api="selectedApi"
:schemas="schemas"
/>
</j-tab-pane>
</j-tabs>
</div>
</div>
</a-col>
</a-row>
</j-col>
</j-row>
</div>
</template>
@ -76,7 +80,7 @@ const props = defineProps<{
mode: modeType;
showTitle?: boolean;
hasHome?: boolean;
code?: string
code?: string;
}>();
const showHome = ref<boolean>(Boolean(props.hasHome));
const tableData = ref([]);
@ -127,7 +131,7 @@ function init() {
getApiGranted_api(props.code as string).then((resp) => {
selectedKeys.value = resp.result as string[];
selectSourceKeys.value = [...(resp.result as string[])];
})
});
} else if (props.mode === 'api') {
apiOperations_api().then((resp) => {
selectedKeys.value = resp.result as string[];
@ -138,12 +142,20 @@ function init() {
activeKey.value = 'does';
selectedApi.value = initSelectedApi;
});
watch(
() => selectedApi.value.url,
() => (activeKey.value = 'does'),
);
}
</script>
<style scoped>
<style lang="less" scoped>
.api-page-container {
height: 100%;
background-color: transparent;
.tree-content {
padding-bottom: 30px;
height: calc(100vh - 230px);
overflow-y: auto;
border-right: 1px solid #e9e9e9;
}
}
</style>

View File

@ -22,6 +22,7 @@ export type apiDetailsType = {
parameters: any[];
requestBody?: any;
responses:object;
description?:string;
}
export type modeType = 'api'| 'appManger' | 'home'

View File

@ -0,0 +1,28 @@
<template>
<page-container>
<div class="api-container">
<Api mode="api">
<template #top>
<p>
<AIcon
type="ExclamationCircleOutlined"
style="margin-right: 12px; font-size: 14px"
/>API
</p>
</template>
</Api>
</div>
</page-container>
</template>
<script setup lang="ts" name="Api">
import Api from '../Api/index.vue';
</script>
<style lang="less" scoped>
.api-container {
background-color: #fff;
padding: 24px;
}
</style>

View File

@ -16,5 +16,3 @@
<script setup lang="ts" name="Platforms">
import Api from './Api/index.vue';
</script>
<style scoped></style>

11427
yarn.lock

File diff suppressed because it is too large Load Diff