fix: 修复公网host多次校验提示

* fix: 处理额外子路由

* fix: 优化物模型-全屏编辑

* fix: 优化物模型-枚举校验

* fix: 修改设备透传默认脚本

* fix: 优化个人中心菜单

* fix: 修改物模型-指标值float,double值问题

* fix: bug#16782;修改数采值为COLLECTOR_GATEWAY;

* fix: 修改bug

* fix: 新增通道添加circuitBreaker属性

* fix: 16802

* fix: 修改运维管理的dashboard颜色

* fix: 修改通道状态

* fix: 16719

* fix: 修改首页视图图片

* feat: 视频预置点位

* refactor: 预置点位

* fix: 修改预置点位

* fix: 修改物模型页面路由跳转无法触发修改未保存提示

* fix: bug#16796

* fix: bug#16963

* fix: 修改auth页面获取参数

* fix: 修改设备物模型-属性来源可以选择规则

* fix: 优化设备物模型-属性来源可以修改产品规则

* fix: 优化设备物模型-规则调试

* fix: 修改设备物模型-属性来源可以选择规则

* feat: 修改bug优化ui

* fix: 修改物模型导入bug

* fix: 优化设备物模型-规则调试

* fix: 优化设备物模型-属性来源可以修改产品规则

* fix: 修改设备物模型-属性来源可以选择规则

* fix: 优化设备管理-设备功能无物模型时兼容处理

* fix: 优化设备管理-设备功能无物模型时兼容处理

* fix: 修复设备无物模型时兼容问题

* fix: 修复第三方应用集成无法显示

* fix: bug#16997

* fix: bug#17001

* fix: bug#16990

* fix: 优化初始化菜单协议查询;修改产品保存接口请求类型为put

* fix: bug#16969

* fix: 修复公网host多次校验提示

* fix: 修改bug

* feat: 修改bug优化ui

* fix: 修改bug

* fix: 修改bug

* fix: 修改bug

* fix: 修改物模型导入bug

* fix: 修改bug

* fix: 修改bug

* fix: 修改bug

* fix: 设备功能

* fix: 设备功能删除console.log

* fix: 修改onenet和ctwing

* fix: 修改bug

* fix: 修改首页视图

* fix: 修复产品跳转设备接口异常

* fix: 17008、16996

* fix: 修复Iframe页面无法访问

* fix: bug#17006

* fix: 修改bug

* feat: 17019

* fix: 修改文档

* fix: ctwing右侧提示

* fix: bug#16975

* fix: 修改17021

* fix: bug#17018

* fix: 优化oauth页面跳转逻辑

* fix: 产品导入提示bug

* fix: 产品导入bug

* fix: bug#17035

* fix: bug#17041

* fix: bug#17041

* fix: bug#17041

* fix: bug#17051

* fix: 初始化页面添加菜单控制

* fix: 修复公网host多次校验提示
This commit is contained in:
XieYongHong 2023-08-07 10:42:33 +08:00 committed by GitHub
parent 749c36bf48
commit b1affdb19b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
138 changed files with 2693 additions and 1074 deletions

View File

@ -25,7 +25,7 @@
"event-source-polyfill": "^1.0.31",
"global": "^4.4.0",
"jetlinks-store": "^0.0.3",
"jetlinks-ui-components": "^1.0.27",
"jetlinks-ui-components": "^1.0.28",
"js-cookie": "^3.0.1",
"jsencrypt": "^3.3.2",
"less": "^4.1.3",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -10,7 +10,7 @@ export const updateMeInfo_api = (data:object) => server.put(`/user/detail`,data)
// 修改登录用户密码
export const updateMepsd_api = (data:object) => server.put(`/user/passwd`,data);
// 第三方账号解绑
export const unBind_api = (appId: string) => server.request.post(`/application/sso/${appId}/unbind/me`);
export const unBind_api = (appId: string) => server.post(`/application/sso/${appId}/unbind/me`);
/**
*
* @param type

View File

@ -11,7 +11,7 @@ export const save = (data: any) => server.post(`/data-collect/channel`, data);
export const update = (id: string, data: any) =>
server.put(`/data-collect/channel/${id}`, data);
export const getProviders = () => server.get(`/gateway/device/providers`);
export const getProviders = () => server.get(`/data-collect/channel/providers`);
export const queryOptionsList = (type: string) =>
server.get(`/data-collect/opc/${type}`);

View File

@ -61,3 +61,6 @@ export const scanOpcUAList = (data: any) =>
export const queryTypeList = () => server.get(`/data-collect/opc/data-types`);
export const getProviders = () => server.get('/data-collect/channel/gateway/codec/providers')
export const getStates = () => server.get('/dictionary/running-state/items')

View File

@ -106,7 +106,7 @@ export const addProduct = (data:any) => server.post('/device-product',data)
* @param data
* @returns
*/
export const editProduct = (data: any) => server.patch('/device-product', data)
export const editProduct = (data: any) => server.put(`/device-product/${data.id}`, data)
/**
*

View File

@ -67,4 +67,7 @@ export default {
// 播放云端回放
playbackStart: (recordId: string) => server.get(`/media/record/${recordId}.mp4`),
// 设备预置位相关接口
opFunction: (deviceId: string, functionId: string, data?: any) => server.post(`/device/invoked/${deviceId}/function/${functionId}`, data)
}

View File

@ -3,7 +3,9 @@
<j-input-search @search="search" allow-clear placeholder="搜索关键字" />
<div class="tree">
<j-tree @select="selectTree" :field-names="{ title: 'name', key: 'id', }" auto-expand-parent
:tree-data="data">
:tree-data="data"
:showLine="{ showLeafIcon: false }"
:show-icon="true">
<template #title="node">
<div class="node">
<div style="max-width: 180px"><Ellipsis>{{ node.name }}</Ellipsis></div>

View File

@ -15,6 +15,8 @@
:field-names="{ title: 'name', key: 'id' }"
auto-expand-parent
:tree-data="data"
:showLine="{ showLeafIcon: false }"
:show-icon="true"
>
<template #title="node">
<div class="node">
@ -86,6 +88,7 @@ import Markdown from 'vue3-markdown-it';
const props = defineProps({
id: String,
propertiesOptions: Array
});
interface Emits {
@ -121,16 +124,14 @@ const lastClick = (node: OperatorItem) => {
emit('addOperatorValue', `$lastState("${node.id}")`);
};
const addClick = (node: OperatorItem) => {
console.log(node)
emit('addOperatorValue', node.code);
};
const productStore = useProductStore();
const getData = async (id?: string) => {
const metadata = productStore.current.metadata || '{}';
const _properties =
JSON.parse(metadata).properties || ([] as PropertyMetadata[]);
// const metadata = productStore.current.metadata || '{}';
const _properties = props.propertiesOptions as PropertyMetadata[]
const properties = {
id: 'property',
name: '属性',

View File

@ -24,7 +24,7 @@
<div class="box">
<div class="left">
<div>
<Operator :id="id" @add-operator-value="addOperatorValue" />
<Operator :id="id" :propertiesOptions="propertiesOptions" @add-operator-value="addOperatorValue" />
</div>
<div style="margin-top: 10px;">
<Editor
@ -70,7 +70,8 @@ const props = defineProps({
value: String,
id: String,
virtualRule: Object,
aggList: Array
aggList: Array,
propertiesOptions: Array
});
const _value = ref<string | undefined>(props.value);

View File

@ -0,0 +1,80 @@
<template>
<div class="view-content">
<div
class="select-item"
v-for="item in list"
:key="item.id"
@click="onChange(item.id)"
:class="{
active: currentView === item.id,
}"
>
<img :src="getImage(`/home/home-view/${item.id}${currentView === item.id ? '-active' : ''}.png`)" alt="" />
</div>
</div>
</template>
<script lang="ts" setup>
import { getImage } from '@/utils/comm';
const list = [
{
id: 'device',
name: '设备接入视图',
},
{
id: 'ops',
name: '运营管理视图',
},
{
id: 'comprehensive',
name: '综合管理视图',
},
];
const props = defineProps({
value: {
type: String,
default: ''
},
})
const emits = defineEmits(['update:value', 'change'])
const currentView = ref<string>('');
const onChange = (id: string) => {
emits('change', id);
emits('update:value', id)
}
watchEffect(() => {
currentView.value = (props.value || '') as string
})
</script>
<style lang="less" scoped>
.view-content {
display: flex;
justify-content: space-between;
.select-item {
cursor: pointer;
width: 30%;
border-radius: 14px;
color: #333333;
overflow: hidden;
img {
width: 100%;
height: 100%;
background-size: cover;
}
&:hover {
box-shadow: 0px 3px 6px -4px rgba(0, 0, 0, 0.12),
0px 6px 16px 0px rgba(0, 0, 0, 0.08),
0px 9px 16px 8px rgba(0, 0, 0, 0.1);
}
}
}
</style>

View File

@ -19,7 +19,7 @@
{ max: 64, message: '最多可输入64个字符' },
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: 'ID只能由数字、字母、下划线、中划线组成',
message: '标识只能由数字、字母、下划线、中划线组成',
},
]">
<j-input v-model:value="_value[index].id" size="small" placeholder="请输入标识"></j-input>

View File

@ -21,7 +21,7 @@
{ max: 64, message: '最多可输入64个字符' },
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: 'ID只能由数字、字母、下划线、中划线组成',
message: '标识只能由数字、字母、下划线、中划线组成',
},
]">
<j-input v-model:value="_value[index].id" size="small" placeholder="请输入标识"></j-input>

View File

@ -1,5 +1,10 @@
<template>
<j-form-item name="type" label="读写类型" required>
<j-form-item name="type" label="读写类型" :rules="[
{
required: true,
message: '请选择读写类型'
}
]">
<j-select
v-model:value="myValue"
mode="multiple"

View File

@ -40,7 +40,7 @@
</j-button>
</template>
</template>
<j-tooltip v-else title="暂无权限,请联系管理员">
<j-tooltip v-else title="暂无权限,请联系管理员" :placement="placement">
<slot v-if="noButton"></slot>
<j-button v-else v-bind="props" :disabled="_isPermission" :style="props.style">
<slot></slot>
@ -89,6 +89,10 @@ const props = defineProps({
style: {
type: Object as PropType<CSSProperties>
},
placement:{
type: String,
default: 'top'
},
...omit(buttonProps(), 'icon')
})

View File

@ -44,7 +44,7 @@
justify-content: center;
width: 45%;
height: 45%;
font-size: 30px;
// font-size: 30px;
background-color: #fff;
border-radius: 50%;
transform: translate(-50%, -50%) rotateZ(-45deg);

View File

@ -39,7 +39,7 @@
</div>
</div>
<div class="direction-audio">
<!-- <AIcon type="AudioOutlined" /> -->
<slot name="center"><!-- <AIcon type="AudioOutlined" /> --></slot>
</div>
</div>
<div class="zoom">

View File

@ -1,5 +1,5 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import menus, { AccountCenterBindPath, InitHomePath, InitLicense, LoginPath, OauthPath } from './menu'
import menus, { AccountCenterBindPath, InitHomePath, InitLicense, LoginPath, OauthPath, VideoSharePath } from './menu'
import { cleanToken, getToken } from '@/utils/comm'
import { useUserInfo } from '@/store/userInfo'
import { useSystem } from '@/store/system'
@ -15,7 +15,7 @@ const router = createRouter({
})
const filterPath = [ InitHomePath ]
const noTokenPath = [ AccountCenterBindPath, OauthPath, InitLicense ]
const noTokenPath = [ AccountCenterBindPath, OauthPath, InitLicense, VideoSharePath ]
router.beforeEach((to, from, next) => {
// TODO 切换路由取消请求

View File

@ -5,6 +5,7 @@ export const InitLicense = '/init-license'
export const NotificationSubscriptionCode = 'message-subscribe'
export const NotificationRecordCode = 'account/NotificationRecord'
export const OauthPath = '/oauth'
export const VideoSharePath = '/media/device/Share'
export const AccountMenu = {
path: '/account',
@ -86,5 +87,9 @@ export default [
},
component: () => import('@/views/oauth/WeChat.vue')
},
{
path: VideoSharePath,
component: () => import('@/views/media/Device/Channel/Share/index.vue')
},
AccountMenu
]

View File

@ -212,3 +212,7 @@ body {
margin: 16px 0;
}
}
.ant-notification {
z-index: 1100;
}

View File

@ -53,6 +53,7 @@ export const USER_CENTER_MENU_BUTTON_CODE = 'user-center-passwd-update'
/**协议列表 */
export const protocolList = [
{ label: 'OPC-UA', value: 'OPC_UA', alias: 'opc-ua' },
{ label: 'Modbus/TCP', value: 'MODBUS_TCP', alias: 'modbus-tcp' },
{ label: 'OPC_UA', value: 'OPC_UA', alias: 'opc-ua' },
{ label: 'MODBUS_TCP', value: 'MODBUS_TCP', alias: 'Modbus/TCP' },
{ label: 'COLLECTOR_GATEWAY', value: 'COLLECTOR_GATEWAY', alias: 'GATEWAY' },
]

View File

@ -368,10 +368,10 @@ const findComponents = (code: string, level: number, isApp: boolean, components:
const myComponents = components[code]
if (level === 1) { // BasicLayoutPage
return myComponents ? () => myComponents() : BasicLayoutPage
} else if (isApp){ // iframe
return Iframe
} else if (level === 2) { // BlankLayoutPage or components
return myComponents ? () => myComponents() : BlankLayoutPage
} else if (isApp){ // iframe
return () => Iframe
} else if(myComponents) { // components
return () => myComponents()
}
@ -417,6 +417,21 @@ const findDetailRouteItem = (item: any, components: any) => {
return []
}
const findSaveRouteItem = (item: any, components: any) => {
const { code, url } = item
const Component = components[`${item.code}/Save`]
if (Component) {
return [{
url: `${url}/detail/:id`,
code: `${code}/Save`,
component: Component,
name: '详情信息',
isShow: false
}]
}
return []
}
export const handleMenus = (menuData: any[], components: any, level: number = 1) => {
if (menuData && menuData.length) {
return menuData.map(item => {
@ -434,7 +449,6 @@ export const handleMenus = (menuData: any[], components: any, level: number = 1)
const extraRoute = hasExtraChildren(item, extraRouteObj)
const detail_components = findDetailRouteItem(item, components)
if (extraRoute && !isApp) { // 包含额外的子路由
route.children = route.children ? [...route.children, ...extraRoute] : extraRoute
}

View File

@ -43,9 +43,7 @@ export const initWebSocket = () => {
const data = JSON.parse(msg.data)
if (data.type === 'error') {
notification.error({ key: 'wserr', message: data.message, style: {
zIndex: 1040
} })
notification.error({ key: 'error', message: data.message })
}
if (subs[data.requestId]) {

View File

@ -0,0 +1,190 @@
<template>
<j-modal
:maskClosable="false"
:width='820'
title="选择网关设备"
visible
@cancel="cancel"
@ok="confirm"
>
<div>
<j-advanced-search
:columns="columns"
class="scene-search"
target="scene-triggrt-device-device"
type='simple'
@search="handleSearch"
/>
<j-divider style='margin: 0' />
<j-pro-table
ref='actionRef'
:bodyStyle='{
paddingRight: 0,
paddingLeft: 0
}'
:columns='columns'
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [{value: 'gateway', termType: 'eq', column: 'deviceType'}]
}"
:gridColumn='2'
:params='params'
:request='query'
:scroll="{y: 350}"
model='CARD'
>
<template #card="slotProps">
<CardBox
:active="selectKey.id === slotProps.id"
:status="slotProps.state?.value"
:statusNames="{
online: 'processing',
offline: 'error',
notActive: 'warning',
}"
:statusText="slotProps.state?.text"
:value='slotProps'
@click="handleClick"
>
<template #img>
<slot name="img">
<img :src="slotProps.photoUrl || getImage('/device/instance/device-card.png')" height='80' width='80' />
</slot>
</template>
<template #content>
<Ellipsis style='width: calc(100% - 100px)'>
<span style="font-size: 16px;font-weight: 600" >
{{ slotProps.name }}
</span>
</Ellipsis>
<j-row>
<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>
<div>{{ slotProps.productName }}</div>
</j-col>
</j-row>
</template>
</CardBox>
</template>
</j-pro-table>
</div>
</j-modal>
</template>
<script name="GateWayDevice" setup>
import { getImage } from '@/utils/comm'
import {query} from "@/api/device/instance";
const props = defineProps({
value: {
type: String,
default: undefined
},
name: {
type: String,
default: undefined
}
})
const emit = defineEmits(['cancel', 'confirm'])
const selectKey = reactive({
id: props.value,
name: props.name
})
const params = ref()
const columns = [
{
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: 'createTime',
width: 200,
search: {
type: 'date'
}
},
{
title: '状态',
dataIndex: 'state',
width: 90,
search: {
type: 'select',
options: [
{ label: '禁用', value: 'notActive' },
{ label: '离线', value: 'offline' },
{ label: '在线', value: 'online' },
]
}
},
]
// const deviceQuery = (p) => {
// const sorts = [];
//
// if (props.rowKeys) {
// props.rowKeys.forEach(rowKey => {
// sorts.push({
// name: 'id',
// value: rowKey,
// });
// })
// }
// sorts.push({ name: 'createTime', order: 'desc' });
// const terms = [
// ...p.terms,
// { terms: [{ column: "productId", value: props.productId }]}
// ]
// return query({ ...p, terms, sorts })
// }
const handleClick = (detail) => {
console.log(detail)
selectKey.id = detail.id
selectKey.name = detail.name
}
const handleSearch = (p) => {
params.value = p
}
const cancel = () => {
emit('cancel')
}
const confirm = () => {
emit('confirm', selectKey)
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,71 @@
<template>
<j-button
v-if="!value"
style="width: 100%"
@click="showModal"
>
选择网关设备
</j-button>
<div v-else class="gateway-form-item">
<span>
<j-ellipsis>
{{ name }}
</j-ellipsis>
</span>
<j-button type="link" @click="showModal">
重新选择
</j-button>
</div>
<DeviceModal
v-if="visible"
:name="name"
:value="value"
@cancel="cancel"
@confirm="confirm"
/>
</template>
<script name="GateWayFormItem" setup>
import DeviceModal from './GateWayDeviceModal.vue'
const props = defineProps({
value: {
type: String,
default: undefined
},
name: {
type: String,
default: undefined
}
})
const emit = defineEmits(['update:value', 'update:name'])
const visible = ref(false)
const showModal = () =>{
visible.value = true
}
const cancel = () => {
visible.value = false
}
const confirm = (select) => {
console.log(select)
emit('update:value', select.id)
emit('update:name', select.name)
cancel()
}
</script>
<style lang="less" scoped>
.gateway-form-item {
display: flex;
>span {
line-height: 32px;
max-width: calc(100% - 88px);
}
}
</style>

View File

@ -92,6 +92,17 @@
:filter-option="filterOption"
/>
</j-form-item>
<j-form-item
v-if="formData.provider === 'COLLECTOR_GATEWAY'"
:name="['configuration','deviceId']"
:rules="[{ required: true, message: '请选择网关设备'}]"
label="选择网关设备"
>
<GateWayFormItem
v-model:name="formData.configuration.deviceName"
v-model:value="formData.configuration.deviceId"
/>
</j-form-item>
<j-form-item
v-if="formData.provider === 'OPC_UA'"
label="安全模式"
@ -198,6 +209,7 @@ import type { FormInstance } from 'ant-design-vue';
import type { FormDataType } from '../type.d';
import { cloneDeep, isArray } from 'lodash-es';
import { protocolList } from '@/utils/consts';
import GateWayFormItem from "@/views/DataCollect/Channel/Save/GateWayFormItem.vue";
const props = defineProps({
data: {
@ -222,7 +234,15 @@ const Options = ref({
const formData = ref<FormDataType>(cloneDeep(FormState));
const handleOk = async () => {
const params = await formRef.value?.validate();
const params: any = await formRef.value?.validate();
if (params?.provider === 'COLLECTOR_GATEWAY') {
params.configuration.deviceName = formData.value.configuration.deviceName
}
params.circuitBreaker = {
type: 'Ignore'
}
loading.value = true;
const response = !id
? await save(params).catch(() => {})
@ -284,9 +304,9 @@ const getProvidersList = async () => {
if (resp.status === 200) {
const arr = resp.result
.filter(
(item: any) => item.id === 'modbus-tcp' || item.id === 'opc-ua',
(item: any) => ['GATEWAY', 'Modbus/TCP', 'opc-ua'].includes(item.name),
)
.map((it: any) => it.id);
.map((it: any) => it.name);
const providers: any = protocolList.filter((item: any) =>
arr.includes(item.alias),
);
@ -302,7 +322,9 @@ getCertificateList();
watch(
() => props.data,
(value) => {
if (value.id) formData.value = value as FormDataType;
if (value.id) {
formData.value = value as FormDataType;
}
},
{ immediate: true, deep: true },
);

View File

@ -15,6 +15,8 @@ export const FormState: FormDataType = {
authType: 'anonymous',
username: '',
password: '',
deviceId: undefined,
deviceName: undefined,
},
description: '',
};

View File

@ -143,11 +143,12 @@
<script lang="ts" setup name="DataCollectPage">
import type { ActionsType } from '@/components/Table/index';
import { getImage } from '@/utils/comm';
import { query, remove, update } from '@/api/data-collect/channel';
import { query, remove, update ,getProviders} from '@/api/data-collect/channel';
import { onlyMessage } from '@/utils/comm';
import { StatusColorEnum, updateStatus } from './data';
import { useMenuStore } from 'store/menu';
import Save from './Save/index.vue';
import { protocolList } from '@/utils/consts';
import _ from 'lodash';
const menuStory = useMenuStore();
@ -174,10 +175,20 @@ const columns = [
ellipsis: true,
search: {
type: 'select',
options: [
{ label: 'OPC_UA', value: 'OPC_UA' },
{ label: 'MODBUS_TCP', value: 'MODBUS_TCP' },
],
options: async() =>{
const resp: any = await getProviders();
if (resp.status === 200) {
const arr = resp.result
.filter(
(item: any) => ['GATEWAY', 'Modbus/TCP', 'opc-ua'].includes(item.name),
)
.map((it: any) => it.name);
const providers: any = protocolList.filter((item: any) =>
arr.includes(item.alias),
);
return providers
}
}
},
},
{
@ -204,8 +215,7 @@ const columns = [
type: 'select',
options: [
{ label: '运行中', value: 'running' },
{ label: '部分错误', value: 'partialError' },
{ label: '错误', value: 'failed' },
{ label: '已停止', value: 'stopped' },
],
},
},

View File

@ -1,6 +1,6 @@
export interface ConfigurationType {
port: string | undefined;
host: string | undefined;;
host: string | undefined;
username: string;
password: string;
endpoint: string,
@ -8,7 +8,8 @@ export interface ConfigurationType {
securityMode: string | undefined,
certId: string | undefined,
authType: string | undefined,
deviceId: string | undefined,
deviceName: string | undefined,
}
export interface FormDataType {

View File

@ -271,7 +271,7 @@ import {
import { ModBusRules, checkProviderData } from '../../data.ts';
import type { FormInstance } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/lib/form';
import { cloneDeep } from 'lodash-es';
import {cloneDeep, omit} from 'lodash-es';
const props = defineProps({
data: {
@ -347,6 +347,14 @@ const handleOk = async () => {
address: data?.pointKey,
};
if (props.data.provider === 'COLLECTOR_GATEWAY') {
const configuration = cloneDeep(params.configuration)
params.configuration = {
configuration: configuration,
interval: params.interval
}
}
loading.value = true;
const response = !id
? await savePointBatch(params).catch(() => {})
@ -439,11 +447,20 @@ watch(
watch(
() => props.data,
(value) => {
if (value.id && value.provider === 'MODBUS_TCP') {
if (value.id && ['MODBUS_TCP', 'COLLECTOR_GATEWAY'].includes(value.provider)) {
const _value: any = cloneDeep(value);
const { writeByteCount, byteCount } =
_value.configuration.parameter;
props.data.provider === 'COLLECTOR_GATEWAY' ? _value.configuration.configuration.parameter: _value.configuration.parameter;
if (props.data.provider === 'COLLECTOR_GATEWAY') {
formData.value = {
...omit(_value, ['configuration']),
..._value.configuration,
}
} else {
formData.value = _value;
}
if (!!_value.accessModes[0]?.value) {
formData.value.accessModes = value.accessModes.map(
(i: any) => i.value,

View File

@ -15,6 +15,8 @@
checkable
@check="onCheck"
:height="600"
:showLine="{ showLeafIcon: false }"
:show-icon="true"
>
<template #title="{ name, key }">
<span

View File

@ -20,7 +20,7 @@
<template #headerTitle>
<j-space>
<PermissionButton
v-if="data?.provider == 'MODBUS_TCP'"
v-if="['MODBUS_TCP', 'COLLECTOR_GATEWAY'].includes(data?.provider)"
type="primary"
@click="handlAdd"
hasPermission="DataCollect/Collector:add"
@ -329,6 +329,8 @@ import {
batchDeletePoint,
removePoint,
readPoint,
getProviders,
getStates
} from '@/api/data-collect/collector';
import { onlyMessage } from '@/utils/comm';
import PointCardBox from './components/PointCardBox/index.vue';
@ -342,6 +344,7 @@ import { cloneDeep, isNumber, throttle } from 'lodash-es';
import { getWebSocket } from '@/utils/websocket';
import { map } from 'rxjs/operators';
import dayjs from 'dayjs';
import { responsiveArray } from 'ant-design-vue/lib/_util/responsiveObserve';
const props = defineProps({
data: {
@ -409,16 +412,14 @@ const columns = [
key: 'provider',
search: {
type: 'select',
options: [
{
label: 'OPC_UA',
value: 'OPC_UA',
},
{
label: 'MODBUS_TCP',
value: 'MODBUS_TCP',
},
],
options: async () =>{
const resp:any = await getProviders();
if(resp.success){
return resp.result.map((item: any) => ({ label: item.name, value: item.id }))
}else{
return []
}
}
},
},
{
@ -436,16 +437,16 @@ const columns = [
key: 'runningState',
search: {
type: 'select',
options: [
{
label: '运行中',
value: 'running',
},
{
label: '已停止',
value: 'stopped',
},
],
options: async() =>{
const resq:any = await getStates();
if(resq.status === 200){
return resq.result.map((item:any)=>(
{label:item.text,value:item.value}
))
}else{
return []
}
}
},
},
{
@ -542,7 +543,7 @@ const getQuantity = (item: Partial<Record<string, any>>) => {
};
const getAddress = (item: Partial<Record<string, any>>) => {
const { address } = item.configuration?.parameter || '';
return !!address ? address + '(地址)' : '';
return (!!address || address === 0) ? address + '(地址)' : '';
};
const getScaleFactor = (item: Partial<Record<string, any>>) => {
const { scaleFactor } = item.configuration?.codec?.configuration || '';

View File

@ -27,6 +27,7 @@
show-search
:filter-option="filterOption"
:disabled="!!id"
@select="channelSelect"
/>
</j-form-item>
<j-form-item
@ -40,10 +41,27 @@
/>
</j-form-item>
<j-form-item
label="从机地址"
:name="['configuration', 'unitId']"
v-if="provider === 'COLLECTOR_GATEWAY'"
label="通讯协议"
:name="['collectorProvider']"
:rules="[{ required: true, message: '请选择通讯协议' }]"
>
<j-select
style="width: 100%"
v-model:value="formData.collectorProvider"
:options="providerListItems"
placeholder="请选择通讯协议"
allowClear
show-search
:filter-option="filterOption"
:disabled="!!id"
/>
</j-form-item>
<j-form-item
v-if="visibleUnitId"
:name="['configuration', 'unitId']"
:rules="LeftTreeRules.unitId"
label="从机地址"
>
<j-input-number
style="width: 100%"
@ -54,6 +72,7 @@
/>
</j-form-item>
<j-form-item
v-if="provider !== 'COLLECTOR_GATEWAY'"
:name="['configuration', 'inheritBreakerSpec', 'type']"
:rules="LeftTreeRules.type"
label="点位熔断处理"
@ -69,14 +88,14 @@
@change="changeCardSelectType"
/>
</j-form-item>
<p style="color: #616161">
<p style="color: #616161" v-if="provider !== 'COLLECTOR_GATEWAY'">
{{ getTypeTooltip(formData.configuration.inheritBreakerSpec.type) }}
</p>
<j-form-item
label="双字高低位切换"
:name="['configuration', 'endian']"
v-if="visibleEndian"
:name="['configuration', 'endian']"
:rules="LeftTreeRules.endian"
label="双字高低位切换"
>
<j-card-select
:showImage="false"
@ -90,10 +109,10 @@
/>
</j-form-item>
<j-form-item
label="单字高低位切换"
:name="['configuration', 'endianIn']"
v-if="visibleEndian"
:name="['configuration', 'endianIn']"
:rules="LeftTreeRules.endianIn"
label="单字高低位切换"
>
<j-card-select
:showImage="false"
@ -106,7 +125,7 @@
:column="2"
/>
</j-form-item>
<div style="color: #616161" v-if="visibleEndian">
<div v-if="visibleEndian" style="color: #616161">
<p>当前内存布局: {{ endianData }}</p>
<p>
只有4字节数据类型(int32ieee754 float)
@ -114,19 +133,18 @@
</p>
</div>
<j-form-item
label="请求超时时间配置"
:name="['configuration', 'requsetTimeout']"
:rules="LeftTreeRules.requsetTimeout"
>
<j-input-number
style="width: 100%"
placeholder="请输入请求超时时间配置"
v-model:value="formData.configuration.requsetTimeout"
addon-after="ms"
:max="2147483648"
:min="1"
:max="60000"
:min="2000"
/>
</j-form-item>
<j-form-item label="说明" name="description">
<j-textarea
placeholder="请输入说明"
@ -154,11 +172,12 @@
</template>
</j-modal>
</template>
<script lang="ts" setup>
import { save, update } from '@/api/data-collect/collector';
<script lang="ts" name="CollectorTreeSave" setup>
import { save, update, getProviders } from '@/api/data-collect/collector';
import { LeftTreeRules } from '../../data';
import type { FormInstance } from 'ant-design-vue';
import {cloneDeep} from "lodash-es";
import {cloneDeep, omit} from "lodash-es";
import {protocolList} from "@/utils/consts";
const loading = ref(false);
const visibleEndian = ref(false);
@ -179,17 +198,40 @@ const emit = defineEmits(['change']);
const id = props.data.id;
const formRef = ref<FormInstance>();
const provider = ref()
const providerListItems = ref()
const geyProviderList = async () => {
const resp: any = await getProviders();
if (resp.success) {
providerListItems.value = resp.result.map((item: any) => ({ label: item.name, value: item.id }))
} else {
providerListItems.value = []
}
}
const _channelListAll = computed(() => {
return props.channelListAll || [];
})
const channelList = computed(() => {
return _channelListAll.value.map((item: any) => ({
const list:any = [];
_channelListAll.value.forEach((item: any) => {
if(item?.state?.value !== 'disabled'){
list.push({
provider: item.provider,
value: item.id,
label: item.name,
}));
})
}
});
return list
})
const channelSelect = (key: string, detail: any) => {
console.log(detail)
provider.value = detail.provider
}
const endianData = computed(() => {
const { endian, endianIn } = formData.value.configuration;
@ -209,6 +251,7 @@ const endianData = computed(() => {
const formData = ref({
channelId: undefined,
name: '',
collectorProvider: undefined,
configuration: {
unitId: '',
type: 'LowerFrequency',
@ -227,25 +270,53 @@ const formData = ref({
});
const handleOk = async () => {
const data = await formRef.value?.validate();
const _data: any = await formRef.value?.validate();
const { provider, name } = _channelListAll.value.find(
if (_data) {
const { name } = _channelListAll.value.find(
(item: any) => item.id === formData.value.channelId,
);
let _copyData = _data
if (['COLLECTOR_GATEWAY'].includes(provider.value)) {
const copyData = cloneDeep(_data)
_copyData = omit(copyData, ['configuration', 'collectorProvider'])
_copyData.configuration = {
configuration: {
..._data.configuration,
inheritBreakerSpec: {
type: 'Ignore'
}
},
collectorProvider: _data.collectorProvider
}
}
const params = {
...data,
provider,
..._copyData,
provider: provider.value,
channelName: name,
circuitBreaker: {
type: 'Ignore'
}
};
loading.value = true;
try {
const response = !id
? await save(params).catch(() => { success: false })
: await update(id, { ...props.data, ...params }).catch(() => { success: false });
? await save(params)
: await update(id, { ...props.data, ...params })
loading.value = false;
if (response.success) {
emit('change', true);
}
} catch (e) {
loading.value = false;
}
}
};
const getTypeTooltip = (value: string) => {
@ -253,7 +324,8 @@ const getTypeTooltip = (value: string) => {
case 'LowerFrequency': return '连续20次采集异常后降低采集频率至设定频率的1/10故障处理后采集频率将恢复至设定频率。';
// case 'Break': return '10'
case 'Break': return '连续20次采集异常后降低采集频率至设定频率的1/1010分钟内未排除故障将停止采集。'
default: return '忽略异常,保持设定采集频率。';
case 'Ignore': return '忽略异常,保持设定采集频率。';
default: return '';
}
}
@ -280,7 +352,7 @@ watch(
(value) => {
const dt = _channelListAll.value.find((item) => item.id === value);
visibleUnitId.value = visibleEndian.value =
dt?.provider && dt?.provider === 'MODBUS_TCP';
dt?.provider && ['MODBUS_TCP', 'COLLECTOR_GATEWAY'].includes(dt?.provider);
},
{ deep: true },
);
@ -290,20 +362,37 @@ watch(
(value) => {
if (value.id) {
let copyValue = cloneDeep(value)
if (!copyValue?.configuration?.inheritBreakerSpec) {
if (!copyValue?.configuration?.inheritBreakerSpec && copyValue.provider !== 'COLLECTOR_GATEWAY') {
copyValue.configuration = {
...copyValue.configuration,
inheritBreakerSpec: {
type: value.circuitBreaker.type
type: value.circuitBreaker?.type
}
}
copyValue.circuitBreaker.type = 'Ignore'
}
if (copyValue.provider === 'COLLECTOR_GATEWAY') {
formData.value = {
...omit(copyValue, ['configuration']),
...copyValue.configuration,
}
} else {
formData.value = copyValue
}
provider.value = copyValue?.provider
};
},
{ immediate: true, deep: true },
);
watchEffect(() => {
if (provider.value === 'COLLECTOR_GATEWAY') {
geyProviderList()
}
})
</script>
<style lang="less" scoped>

View File

@ -31,30 +31,45 @@
:height="660"
@select='treeSelect'
defaultExpandAll
:showLine="{ showLeafIcon: false }"
:show-icon="true"
>
<template #title="{ name, data }">
<Ellipsis class="tree-left-title">
{{ name }}
</Ellipsis>
<!-- <j-tag-->
<!-- class="tree-left-tag"-->
<!-- v-if="data.id !== '*'"-->
<!-- :color="-->
<!-- data?.uniformState?.value === 'normal' || data?.state?.value === 'disabled' ?-->
<!-- colorMap.get(data?.runningState?.value) :-->
<!-- colorMap.get(data?.uniformState?.value)-->
<!-- "-->
<!-- >{{-->
<!-- data?.uniformState?.value === 'normal' || data?.state?.value === 'disabled' ?-->
<!-- data?.runningState?.text :-->
<!-- data?.uniformState?.text-->
<!-- }}</j-tag-->
<!-- >-->
<j-tag
class="tree-left-tag"
v-if="data.id !== '*'"
:color="
data?.uniformState?.value === 'normal' || data?.state?.value === 'disabled' ?
colorMap.get(data?.runningState?.value) :
colorMap.get(data?.uniformState?.value)
"
>{{
data?.uniformState?.value === 'normal' || data?.state?.value === 'disabled' ?
data?.runningState?.text :
data?.uniformState?.text
}}</j-tag
:color="colorMap.get(data?.uniformState?.value)"
>{{ data?.uniformState?.text }}</j-tag
>
<j-tag
class="tree-left-tag2"
v-if="data.id !== '*'"
:color="colorMap.get(data?.state?.value)"
>{{ data?.state?.text }}</j-tag
:color="
data?.state?.value === 'disabled' ? colorMap.get(data?.runningState?.value) :
colorMap.get(data?.state?.value)
"
>
{{
data?.state?.value === 'disabled' ? data?.state?.text : data?.runningState?.text
}}
</j-tag
>
<span
v-if="data.id !== '*'"
@ -79,6 +94,7 @@
? '启用'
: '禁用',
}"
:disabled="data?.runningState?.value === 'stopped' && data?.state?.value!== 'disabled'"
hasPermission="DataCollect/Collector:action"
:popConfirm="{
title:

View File

@ -57,7 +57,7 @@ export const ModBusRules = {
},
{
pattern: regOnlyNumber,
message: '请输入0-999999999之间的正整数',
message: '请输入0-999999之间的正整数',
},
],
quantity: [
@ -170,6 +170,9 @@ export const LeftTreeRules = {
endianIn: [
{ required: true, message: '请选择单字高低位切换', trigger: 'blur' },
],
requsetTimeout:[
{ pattern: /^\d+$/, message:'请输入2000-60000的正整数',trigger: 'change'}
]
};
export const FormTableColumns = [

View File

@ -83,7 +83,8 @@ const handleOptions = (x = [], y = []) => {
const chart: any = chartRef.value;
if (chart) {
const myChart = echarts.init(chart);
const maxY: number = y.sort((a,b)=>{
const _y = [...y];
const maxY: number = _y.sort((a,b)=>{
return b-a
})?.[0]
const options = {

View File

@ -12,54 +12,94 @@
</div>
<h1>1. 概述</h1>
<div>
DuerOS支持家居场景下的云端控制该页面主要将平台的产品与DuerOS支持语音控制的产品进行映射以到达小度平台控制本平台设备的目的
DuerOS支持在家居场景下进行云端控制该页面主要将平台的产品与DuerOS支持语音控制的产品进行映射达到小度平台控制本平台设备的目的
</div>
<h1>2. 操作步骤</h1>
<div>
<h2>1在百度小度技能平台创建技能并授权完成物联网平台与DuerOS的关联</h2>
<div class="image">
<j-image width="100%" :src="getImage('/cloud/dueros-doc.jpg')" />
</div>
<h1>授权地址</h1>
<div>物联网平台的登录地址注意需要为https</div>
<div>请复制并填写: https://{location.host}/#/user/login</div>
<h1>Client_Id</h1>
<div>请填写系统管理-应用管理中的clientId</div>
<div class="image">
<j-image width="100%" :src="getImage('/cloud/dueros-doc1.png')" />
</div>
<h1>回调地址</h1>
<div>请复制DuerOS平台中的值填写到系统管理-应用管理中-redirectUrl中</div>
<div class="image">
<j-image width="100%" :src="getImage('/cloud/dueros-doc2.png')" />
</div>
<h1>Token地址</h1>
<div>请复制并填写HTTPS://{location.host}/api/v1/token</div>
<h1>ClientSecret</h1>
<div>请复制系统管理-应用管理中的secureKey填写到DuerOS平台</div>
<div class="image">
<j-image width="100%" :src="getImage('/cloud/dueros-doc3.png')" />
</div>
<div></div>
<h1>WebService</h1>
<div>请复制并填写/dueros/product/_query</div>
<h2>2登录物联网平台进行平台内产品与DuerOS产品的数据映射</h2>
<h2>
3智能家居用户通过物联网平台中的用户登录小度APP获取平台内当前用户的所属设备获取后即可进行语音控制
</h2>
</div>
<h1>3. 配置说明</h1>
<div>1在百度小度技能平台创建技能并授权</div>
<div>
<h2>
1设备类型为DuerOS平台拟定的标准规范设备类型将决定动作映射动作的下拉选项以及属性映射Dueros属性的下拉选项
</h2>
2在物联网平台 系统管理--应用管理中配置应用完成与DuerOS的关联
</div>
<div class="image">
<j-image
width="100%"
:src="getImage('/cloud/dueros-doc.jpg')"
/>
<div class="desc">新建DuerOS</div>
</div>
<j-descriptions
bordered
size="small"
:column="1"
:labelStyle="{ width: '100px' }"
>
<j-descriptions-item label="参数">说明</j-descriptions-item>
<j-descriptions-item label="授权地址"
>物联网平台的登录地址http://host:port/JetLinks.cn</j-descriptions-item
>
<j-descriptions-item label="Client_Id">
请复制并填写物联网平台的appId
</j-descriptions-item>
<j-descriptions-item label="Scope">
以空格分割的权限列表若不传递此参数代表请求用户的默认权限
</j-descriptions-item>
<j-descriptions-item label="Token地址">
请复制并填写HTTPS://host:port/api/v1/token
</j-descriptions-item>
<j-descriptions-item label="ClientSecret">
请复制并填写物联网平台的secureKey
</j-descriptions-item>
<j-descriptions-item label="WebService">
请复制并填写/dueros/product/_query
</j-descriptions-item>
</j-descriptions>
<div class="image">
<j-image
width="100%"
:src="getImage('/cloud/dueros-doc1.png')"
/>
<div class="desc">新建应用</div>
</div>
<j-descriptions
bordered
size="small"
:column="1"
:labelStyle="{ width: '100px' }"
>
<j-descriptions-item label="参数">说明</j-descriptions-item>
<j-descriptions-item label="appId">
第三方应用唯一标识物联网平台的自动生成
</j-descriptions-item>
<j-descriptions-item label="secureKey">
secureKey 第三方应用唯一标识匹配的秘钥物联网平台的自动生成
</j-descriptions-item>
<j-descriptions-item label="角色">
为应用用户分配角色根据绑定的角色进行系统菜单赋权
</j-descriptions-item>
<j-descriptions-item label="组织">
为应用用户分配所属组织根据绑定的组织进行数据隔离
</j-descriptions-item>
<j-descriptions-item label="redirectUrl">
请复制并填写小度平台的回调地址
</j-descriptions-item>
<j-descriptions-item label="IP白名单">
允许指定IP地址访问
</j-descriptions-item>
</j-descriptions>
<div>3登录物联网平台进行平台内产品与DuerOS产品的数据映射</div>
<div>
4智能家居用户通过物联网平台中的用户登录小度APP获取平台内当前用户的所属设备获取后即可进行语音控制
</div>
</div>
<h1>配置说明</h1>
<div>
设备类型为DuerOS平台拟定的标准规范设备类型将决定动作映射动作的下拉选项以及属性映射Dueros属性的下拉选项
</div>
</div>
</template>
<script lang="ts" setup>
import { getImage } from '@/utils/comm';
</script>
<style lang="less" scoped>
@ -97,6 +137,13 @@ import { getImage } from '@/utils/comm';
.image {
margin: 16px 0;
.desc {
width: 100%;
text-align: center;
color: rgb(138, 143, 141);
margin-top: 10px;
}
}
}
</style>

View File

@ -1,4 +1,5 @@
<template>
<j-scrollbar :height="`calc(100% - 51px)`">
<div class="box">
<div class="content">
<template v-if="bindList.length">
@ -48,6 +49,7 @@
<j-empty v-else style="margin: 200px 0" />
</div>
</div>
</j-scrollbar>
</template>
<script setup lang="ts">

View File

@ -1,28 +1,6 @@
<template>
<div class="choose-view">
<div class="view-content">
<div
:span="8"
class="select-item"
@click="currentView = 'device'"
>
<img :src="getImage(`/home/home-view/device${currentView === 'device' ? '-active' : ''}.png`)" alt="" />
</div>
<div
:span="8"
class="select-item"
@click="currentView = 'ops'"
>
<img :src="getImage(`/home/home-view/ops${currentView === 'ops' ? '-active' : ''}.png`)" alt="" />
</div>
<div
:span="8"
class="select-item"
@click="currentView = 'comprehensive'"
>
<img :src="getImage(`/home/home-view/comprehensive${currentView === 'comprehensive' ? '-active' : ''}.png`)" alt="" />
</div>
</div>
<HomeView v-model:value="currentView" />
<div class="btn">
<j-button type="primary" @click="confirm">保存修改</j-button>
</div>
@ -31,7 +9,8 @@
<script lang="ts" setup>
import { getMe_api, getView_api, setView_api } from '@/api/home';
import { getImage, onlyMessage } from '@/utils/comm';
import { onlyMessage } from '@/utils/comm';
import HomeView from '@/components/HomeView/index.vue';
const currentView = ref<string>('');
const isApiUser = ref<boolean>();
@ -51,10 +30,13 @@ function getViews() {
})
.then((resp: any) => {
if (resp?.status === 200) {
if (resp.result) currentView.value = resp.result?.content;
else if (resp.result?.username === 'admin') {
if (resp.result) {
currentView.value = resp.result?.content;
} else if (resp.result?.username === 'admin') {
currentView.value = 'comprehensive';
} else currentView.value = 'init';
} else {
currentView.value = 'device';
}
}
});
}
@ -76,19 +58,6 @@ onMounted(() => {
width: 100%;
padding: 4% 9% 0 9%;
box-sizing: border-box;
.view-content {
display: flex;
justify-content: space-between;
.select-item {
cursor: pointer;
width: 30%;
img {
width: 100%;
background-size: cover;
}
}
}
.btn {
display: flex;

View File

@ -14,6 +14,7 @@
:params="queryParams"
:bodyStyle="{ padding: 0 }"
:defaultParams="defaultParams"
:scroll="{y:420}"
>
<!-- <template #rightExtraRender>
<j-popconfirm title="确认全部已读?" @confirm="onAllRead">

View File

@ -1,4 +1,5 @@
<template>
<j-scrollbar :height="`calc(100% - 51px)`">
<j-spin :spinning="loading">
<div style="padding: 0 10px">
<div class="alert">
@ -48,6 +49,7 @@
</div>
</div>
</j-spin>
</j-scrollbar>
</template>
<script lang="ts" setup>

View File

@ -53,9 +53,7 @@
:tab="item.title"
/>
</j-tabs>
<j-scrollbar :height="`calc(100% - 51px)`">
<component :is="tabs[user.tabKey]" />
</j-scrollbar>
</div>
</div>
</div>

View File

@ -21,7 +21,6 @@
<j-form-item label="名称" name="name">
<j-input
v-model:value="formModel.name"
:maxlength="64"
placeholder="请输入名称"
/>
</j-form-item>
@ -31,6 +30,7 @@
id="inputNumber"
v-model:value="formModel.sortIndex"
:min="1"
:max="9999"
placeholder="请输入排序"
/>
</j-form-item>
@ -102,7 +102,11 @@ const rules = ref({
message: '最多可输入64个字符',
},
],
sortIndex: [{ required: true, message: '请输入排序', trigger: 'blur' }],
sortIndex: [{ required: true, message: '请输入排序', trigger: 'blur' },{
pattern:/^\d+$/,
message:'请输入正整数',
trigger:'change'
}],
});
const visible = ref(false);
const { resetFields, validate, validateInfos } = useForm(
@ -174,7 +178,7 @@ const show = async (row: any) => {
childArr.value = row.children.sort(compare('sortIndex'));
formModel.value = {
name: '',
sortIndex:
sortIndex:childArr.value[childArr.value.length - 1].sortIndex === 9999 ? childArr.value[childArr.value.length - 1].sortIndex :
childArr.value[childArr.value.length - 1].sortIndex + 1,
description: '',
};
@ -186,7 +190,7 @@ const show = async (row: any) => {
if (arr.value.length > 0) {
formModel.value = {
name: '',
sortIndex: arr.value[arr.value.length - 1].sortIndex + 1,
sortIndex: arr.value[arr.value.length - 1].sortIndex === 9999 ? arr.value[arr.value.length - 1].sortIndex : arr.value[arr.value.length - 1].sortIndex + 1,
description: '',
};
}

View File

@ -107,11 +107,15 @@ const query = reactive({
key: 'sortIndex',
search: {
type: 'number',
componentProps:{
precision:0,
min:1
}
},
scopedSlots: true,
},
{
title: '描述',
title: '说明',
key: 'description',
dataIndex: 'description',
search: {

View File

@ -150,7 +150,7 @@ const menuStore = useMenuStore();
* 获取产品数量
*/
const getProductData = () => {
if (menuStore.hasMenu('device/Product')) {
// if (menuStore.hasMenu('device/Product')) {
productCount().then((res) => {
if (res.status == 200) {
productTotal.value = res.result;
@ -180,14 +180,14 @@ const getProductData = () => {
productFooter.value[1].value = res.result;
}
});
}
// }
};
getProductData();
/**
* 获取设备数量
*/
const getDeviceData = () => {
if (menuStore.hasMenu('device/Instance')) {
// if (menuStore.hasMenu('device/Instance')) {
deviceCount().then((res) => {
if (res.status == 200) {
deviceTotal.value = res.result;
@ -206,7 +206,7 @@ const getDeviceData = () => {
}
},
);
}
// }
};
getDeviceData();
/**
@ -214,7 +214,7 @@ getDeviceData();
*/
const getOnline = () => {
const startTime = dayjs().subtract(0, 'days').startOf('day').format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs().subtract(0, 'days').endOf('day').format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
dashboard([
{
@ -481,7 +481,7 @@ const setDevMesChartOption = (
//
const getDevice = () => {
const startTime = dayjs().subtract(0, 'days').startOf('day').format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs().subtract(0, 'days').endOf('day').format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
dashboard([
{

View File

@ -21,6 +21,8 @@
:load-data="onLoadData"
@check="onCheck"
v-model:expandedKeys="expandedKeys"
:showLine="{ showLeafIcon: false }"
:show-icon="true"
/>
</j-card>
<div style="width: 100px">

View File

@ -101,7 +101,9 @@
<j-col
:span="24"
v-if="
modelRef.type === 'INVOKE_FUNCTION' && modelRef.function && modelRef.inputs.length
modelRef.type === 'INVOKE_FUNCTION' &&
modelRef.function &&
modelRef.inputs.length
"
>
<!-- <j-form-item
@ -169,7 +171,7 @@ const funcChange = (val: string) => {
name: item.name,
value: undefined,
valueType: item?.valueType?.type,
required: item?.expands?.required
required: item?.expands?.required,
};
});
modelRef.inputs = list;
@ -177,22 +179,25 @@ const funcChange = (val: string) => {
};
const saveBtn = async () => {
const _inputs = await inputsRef.value.onSave();
console.log(_inputs)
if(!_inputs){
return
}
formRef.value.validate().then(async () => {
const _data = await formRef.value?.validate();
if (!_data) return;
const values = toRaw(modelRef);
let _inputs: any[] = [];
if (values.type === 'READ_PROPERTY') {
await readProperties(instanceStore.current?.id || '', [
values.properties,
]);
} else if (values.type === 'WRITE_PROPERTY') {
await settingProperties(instanceStore.current?.id || '', {
[values.properties || '']: values.propertyValue,
});
} else {
if (modelRef.inputs.length) {
_inputs = modelRef.inputs.filter((i: any) => !i.value && i?.required);
if (_inputs.length) {
const _inputs = await inputsRef.value?.onSave();
if (!_inputs) {
return;
}
}
if (values.type === 'INVOKE_FUNCTION') {
const list = (modelRef?.inputs || [])?.filter((it: any) => !!it.value);
const obj = {};
list.map((it: any) => {
@ -205,18 +210,7 @@ const saveBtn = async () => {
...obj,
},
);
} else {
if (values.type === 'READ_PROPERTY') {
await readProperties(instanceStore.current?.id || '', [
values.properties,
]);
} else {
await settingProperties(instanceStore.current?.id || '', {
[values.properties || '']: values.propertyValue,
});
}
}
});
};
defineExpose({ saveBtn });

View File

@ -21,6 +21,8 @@
:load-data="onLoadData"
@check="onCheck"
v-model:expandedKeys="expandedKeys"
:showLine="{ showLeafIcon: false }"
:show-icon="true"
/>
</j-card>
<div style="width: 100px">

View File

@ -29,7 +29,7 @@ const menuStory = useMenuStore();
const instanceStore = useInstanceStore();
// const emits = defineEmits(['onJump']);
const metadata = computed(() => JSON.parse(instanceStore.detail.metadata));
const metadata = computed(() => JSON.parse(instanceStore.detail?.metadata || '{}'));
const activeKey = ref('Simple');
const tabs = {

View File

@ -0,0 +1,7 @@
<template>
<EditTable provider="collector-gateway" />
</template>
<script lang="ts" setup>
import EditTable from '../components/EditTable/index.vue'
</script>

View File

@ -19,6 +19,8 @@
:tree-data="dataSource"
:checkedKeys="checkedKeys"
@check="onCheck"
:showLine="{ showLeafIcon: false }"
:show-icon="true"
/>
</j-card>
<div style="width: 100px">
@ -104,12 +106,12 @@ const handleData = (data: any[], type: string) => {
const handleSearch = async () => {
loading.value = true;
const resp = await treeMapping({
terms: [
{
column: 'provider',
value: _props.type,
},
],
// terms: [
// {
// column: 'provider',
// value: _props.type,
// },
// ],
});
loading.value = false;
if (resp.status === 200) {

View File

@ -1,5 +1,5 @@
<template>
<j-spin :spinning="loading" v-if="metadata.properties.length">
<j-spin v-if="metadata.properties?.length" :spinning="loading">
<j-card :bordered="false" borderStyle="padding: 0">
<template #extra>
<j-space>
@ -28,13 +28,8 @@
placeholder="请选择"
allowClear
:filter-option="filterOption"
>
<j-select-option
v-for="item in channelList"
:key="item.value"
:value="item.value"
:label="item.label"
>{{ item.label }}</j-select-option
:options="channelList"
@select="(_, option) => { record.provider = option.provider }"
>
</j-select>
</j-form-item>
@ -188,16 +183,16 @@ const visible = ref<boolean>(false);
const getChannel = async () => {
const resp: any = await queryChannelNoPaging({
paging: false,
terms: [
{
terms: [
{
column: 'provider',
value: props.provider,
},
],
},
],
// terms: [
// {
// terms: [
// {
// column: 'provider',
// value: props.provider,
// },
// ],
// },
// ],
});
if (resp.status === 200) {
channelList.value = resp.result?.map((item: any) => ({
@ -211,12 +206,12 @@ const getChannel = async () => {
const handleSearch = async () => {
loading.value = true;
getChannel();
const _metadata = metadata.properties.map((item: any) => ({
const _metadata = metadata.properties?.map?.((item: any) => ({
metadataId: item.id,
metadataName: `${item.name}(${item.id})`,
metadataType: 'property',
name: item.name,
}));
})) || [];
if (_metadata && _metadata.length) {
const resp: any = await queryMapping(
'device',
@ -269,6 +264,7 @@ const onSave = () => {
(i: any) => i.channelId,
);
if (arr && arr.length !== 0) {
console.log(arr)
const resp = await saveMapping(
instanceStore.current.id,
props.provider,

View File

@ -122,6 +122,7 @@ import Modbus from './Modbus/index.vue';
import OPCUA from './OPCUA/index.vue';
import EdgeMap from './EdgeMap/index.vue';
import Parsing from './Parsing/index.vue';
import GateWay from './GateWay/index.vue'
import Log from './Log/index.vue';
import { _deploy, _disconnect } from '@/api/device/instance';
import { getImage, onlyMessage } from '@/utils/comm';
@ -181,7 +182,8 @@ const tabs = {
EdgeMap,
Parsing,
Log,
MetadataMap
MetadataMap,
GateWay
};
const getStatus = (id: string) => {
@ -225,7 +227,7 @@ const getDetail = () => {
) {
list.value.push({
key: 'Modbus',
tab: 'Modbus TCP',
tab: '数采映射',
});
}
if (
@ -234,7 +236,16 @@ const getDetail = () => {
) {
list.value.push({
key: 'OPCUA',
tab: 'OPC UA',
tab: '数采映射',
});
}
if (
instanceStore.current?.protocol === 'collector-gateway' &&
!keys.includes('GateWay')
) {
list.value.push({
key: 'GateWay',
tab: '数采映射',
});
}
if (

View File

@ -26,17 +26,21 @@
<j-descriptions-item label="设备类型">{{
productStore.current.deviceType?.text
}}</j-descriptions-item>
<j-descriptions-item label="接入方式">
<PermissionButton
type="link"
style="width:100%;padding:0"
@click="changeTables"
hasPermission="device/Product:update"
>{{
>
<div style="white-space: normal">
<Ellipsis>{{
productStore.current.accessName
? productStore.current.accessName
: '配置接入方式'
}}</PermissionButton
}}</Ellipsis>
</div>
</PermissionButton
>
</j-descriptions-item>
<j-descriptions-item label="创建时间">{{

View File

@ -8,26 +8,28 @@
<template #title>
<div>
<div style="display: flex; align-items: center">
<a-tooltip>
<j-tooltip>
<template #title>{{
productStore.current.name
}}</template>
<div class="productDetailHead">
{{ productStore.current.name }}
</div>
</a-tooltip>
<div style="margin: -5px 0 0 20px">
</j-tooltip>
<div style="margin: -5px 0 0 20px" v-if="permissionStore.hasPermission('device/Product:action')">
<j-popconfirm
title="确认禁用"
@confirm="handleUndeploy"
v-if="productStore.current.state === 1"
okText="确定"
cancelText="取消"
:disabled="!permissionStore.hasPermission('device/Product:action')"
>
<j-switch
:checked="productStore.current.state === 1"
checked-children="正常"
un-checked-children="禁用"
:disabled="!permissionStore.hasPermission('device/Product:action')"
/>
</j-popconfirm>
<j-popconfirm
@ -36,6 +38,7 @@
v-if="productStore.current.state === 0"
okText="确定"
cancelText="取消"
:disabled="!permissionStore.hasPermission('device/Product:action')"
>
<j-switch
:unCheckedValue="
@ -43,9 +46,31 @@
"
checked-children="正常"
un-checked-children="禁用"
:disabled="!permissionStore.hasPermission('device/Product:action')"
/>
</j-popconfirm>
</div>
<div style="margin: -5px 0 0 20px" v-else>
<j-tooltip>
<template #title>暂无权限请联系管理员</template>
<j-switch
v-if="productStore.current.state === 1"
:checked="productStore.current.state === 1"
checked-children="正常"
un-checked-children="禁用"
:disabled="!permissionStore.hasPermission('device/Product:action')"
/>
<j-switch
v-if="productStore.current.state === 0"
:unCheckedValue="
productStore.current.state === 0
"
checked-children="正常"
un-checked-children="禁用"
:disabled="!permissionStore.hasPermission('device/Product:action')"
/>
</j-tooltip>
</div>
</div>
</div>
</template>
@ -87,6 +112,7 @@
: undefined
"
hasPermission="device/Product:update"
placement="topRight"
>应用配置</PermissionButton
>
</template>
@ -124,7 +150,9 @@ import { getImage, handleParamsToString, onlyMessage } from '@/utils/comm';
import { useMenuStore } from '@/store/menu';
import { useRouterParams } from '@/utils/hooks/useParams';
import {EventEmitter} from "@/utils/utils";
import { usePermissionStore } from '@/store/permission';
const permissionStore = usePermissionStore()
const menuStory = useMenuStore();
const route = useRoute();
const checked = ref<boolean>(true);
@ -169,7 +197,7 @@ const tabs = {
watch(
() => route.params.id,
(newId) => {
if (newId) {
if (newId && route.name === 'device/Product/Detail') {
productStore.reSet();
productStore.tabActiveKey = 'Info';
productStore.refresh(newId as string);

View File

@ -101,6 +101,10 @@ defineExpose({
}
.product-id {
margin: 10px 15px 10px 0px;
max-width: 520px;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
}
.product-btn {
margin: 10px 0px 10px 0;

View File

@ -306,6 +306,7 @@ const getActions = (
icon: 'icon-xiazai',
onClick: () => {
console.log(data);
const extra = omit(data, [
'transportProtocol',
'protocolName',
@ -314,7 +315,7 @@ const getActions = (
'accessProvider',
'messageProtocol',
]);
downloadObject(extra, '产品');
downloadObject(extra, data.name+'产品');
},
},
{
@ -388,28 +389,37 @@ const beforeUpload = (file: any) => {
reader.readAsText(file);
reader.onload = async (result) => {
const text = result.target?.result;
console.log('text: ', text);
// console.log('text: ', text);
// console.log(file);
if (!file.type.includes('json')) {
onlyMessage('请上传json格式文件', 'error');
return false;
}
if(!text){
onlyMessage('文件内容不能为空','error')
return false;
}
try {
const data = JSON.parse(text || '{}');
const data = JSON.parse(text);
//
data.state = 0;
if (Array.isArray(data)) {
onlyMessage('请上传json格式文件', 'error');
onlyMessage('请上传正确格式文件', 'error');
return false;
}
delete data.state;
if(!data?.name){
data.name = "产品" + Date.now();
}
const res = await updateDevice(data);
if (res.status === 200) {
onlyMessage('操作成功');
tableRef.value?.reload();
}
return true;
} catch {
// onlyMessage('json', 'error');
} catch(e) {
onlyMessage('请上传正确格式文件', 'error');
}
return true;
};

View File

@ -24,8 +24,10 @@
title: hasOperate('add', type)
? '当前的存储方式不支持新增'
: '新增',
getPopupContainer: getPopupContainer,
}"
@click="handleAddClick()"
placement="topRight"
>
新增
</PermissionButton>
@ -42,8 +44,10 @@
? '当前的存储方式不支持新增'
: !editStatus ? '暂无改动数据': '保存',
placement: hasOperate('add', type) ? 'topRight' : 'top',
getPopupContainer: getPopupContainer,
}"
@click="handleSaveClick()"
placement="topRight"
>
保存
</PermissionButton>
@ -92,11 +96,12 @@
v-else
v-model:value="data.record.expands"
:id="data.record.id"
:type="data.record.valueType.type"
:disabled="target === 'device' && productNoEdit.id?.includes?.(data.record._sortIndex)"
:record="data.record"
:tooltip="target === 'device' && productNoEdit.id?.includes?.(data.record._sortIndex) ? {
title: '继承自产品物模型的数据不支持删除',
} : undefined"
:type="data.record.valueType.type"
/>
</template>
<template #action="{data}">
@ -110,6 +115,7 @@
@click="copyItem(data.record, data.index)"
:tooltip="{
title: operateLimits('add', type) ? '当前的存储方式不支持复制' : '复制',
getPopupContainer: getPopupContainer,
}"
>
<AIcon type="CopyOutlined" />
@ -123,6 +129,7 @@
@click="handleAddClick(null, data.index)"
:tooltip="{
title: operateLimits('add', type) ? '当前的存储方式不支持新增' : '新增',
getPopupContainer: getPopupContainer,
}"
>
<AIcon type="PlusSquareOutlined" />
@ -135,6 +142,7 @@
@click="showDetail(data.record)"
:tooltip="{
title: '详情',
getPopupContainer: getPopupContainer,
}"
>
<AIcon type="FileSearchOutlined" />
@ -151,9 +159,11 @@
onConfirm: async () => {
await removeItem(data.index);
},
getPopupContainer: getPopupContainer
}"
:tooltip="{
placement: 'topRight',
getPopupContainer: getPopupContainer,
title: target === 'device' && productNoEdit.id?.includes?.(data.record._sortIndex) ? '继承自产品物模型的数据不支持删除' :'删除',
}"
:disabled="target === 'device' && productNoEdit.id?.includes?.(data.record._sortIndex)"
@ -166,21 +176,25 @@
<PropertiesModal
v-if="type === 'properties' && detailData.visible"
:data="detailData.data"
:getPopupContainer="getPopupContainer"
@cancel="cancelDetailModal"
/>
<FunctionModal
v-else-if="type === 'functions' && detailData.visible"
:data="detailData.data"
:getPopupContainer="getPopupContainer"
@cancel="cancelDetailModal"
/>
<EventModal
v-else-if="type === 'events' && detailData.visible"
:data="detailData.data"
:getPopupContainer="getPopupContainer"
@cancel="cancelDetailModal"
/>
<TagsModal
v-else-if="type === 'tags' && detailData.visible"
:data="detailData.data"
:getPopupContainer="getPopupContainer"
@cancel="cancelDetailModal"
/>
</template>
@ -209,10 +223,11 @@ import {omit} from "lodash-es";
import { PropertiesModal, FunctionModal, EventModal, TagsModal } from './DetailModal'
import { Modal } from 'jetlinks-ui-components'
import {EventEmitter} from "@/utils/utils";
import {watch} from "vue";
import {computed, watch} from "vue";
import {cloneDeep} from "lodash";
import {useSystem} from "store/system";
import {storeToRefs} from "pinia";
import { FULL_CODE } from 'jetlinks-ui-components/es/DataTable'
const props = defineProps({
target: {
@ -255,13 +270,23 @@ const detailData = reactive({
visible:false
})
const showSave = ref(metadata.value.length !== 0)
const showLastDelete = ref(false)
const dataSourceCache = ref<any[]>(metadata.value)
const fullRef = inject(FULL_CODE);
const getPopupContainer = (node: any) => {
const fullDom = tableRef.value?.fullRef?.()
return fullDom || node
}
const showLastDelete = computed(() => {
return dataSourceCache.value.length === 1
})
provide('_dataSource', dataSourceCache)
const showDetail = (data: any) => {
detailData.data = data
detailData.visible = true
@ -344,15 +369,15 @@ const handleAddClick = async (_data?: any, index?: number) => {
const newObject = _data || getDataByType()
const _addData = await tableRef.value.addItem(newObject, index)
if (_addData.length === 1) {
showLastDelete.value = true
}
// if (_addData.length === 1) {
// showLastDelete.value = true
// }
showSave.value = true
};
const copyItem = (record: any, index: number) => {
const copyData = cloneDeep(omit(record, ['_uuid', '_sortIndex']))
copyData.id = `copy_${copyData.id}`.slice(0,64)
copyData.id = `copy_${copyData.id}`
handleAddClick(copyData, index)
}
@ -361,9 +386,9 @@ const removeItem = (index: number) => {
// data.splice(index, 1);
// dataSource.value = data
const _data = tableRef.value.removeItem(index)
if (_data.length === 1) {
showLastDelete.value = true
}
// if (_data.length === 1) {
// showLastDelete.value = true
// }
if (_data.length === 0) {
showSave.value = false
@ -372,6 +397,7 @@ const removeItem = (index: number) => {
}
const editStatusChange = (status: boolean) => {
console.log('editStatusChange',status)
editStatus.value = status
}
@ -431,6 +457,7 @@ const handleSaveClick = async (next?: Function) => {
if(result.success) {
dataSource.value = resp
tableRef.value.cleanEditStatus()
editStatus.value = false
onlyMessage('操作成功!')
next?.()
}
@ -476,7 +503,11 @@ watch(() => metadata.value, () => {
dataSource.value = metadata.value
}, { immediate: true })
onBeforeRouteUpdate((to, from, next) => {
onBeforeRouteUpdate((to, from, next) => { //
parentTabsChange(next as Function)
})
onBeforeRouteLeave((to, from, next) => { //
parentTabsChange(next as Function)
})

View File

@ -4,6 +4,7 @@
:maskClosable="false"
title="事件详情"
width="650px"
:getContainer="getPopupContainer"
@cancel="cancel"
@ok="ok"
>
@ -36,6 +37,10 @@ const props = defineProps({
data: {
type: Object,
default: () => ({})
},
getPopupContainer: {
type: Function,
default: undefined
}
})

View File

@ -4,6 +4,7 @@
title="功能详情"
width="650px"
:maskClosable="false"
:getContainer="getPopupContainer"
@cancel="cancel"
@ok="ok"
>
@ -40,6 +41,10 @@ const props = defineProps({
data: {
type: Object,
default: () => ({})
},
getPopupContainer: {
type: Function,
default: undefined
}
})

View File

@ -3,6 +3,7 @@
visible
:maskClosable="false"
title="属性详情"
:getContainer="getPopupContainer"
@cancel="cancel"
@ok="ok"
>
@ -67,6 +68,10 @@ const props = defineProps({
data: {
type: Object,
default: () => ({})
},
getPopupContainer: {
type: Function,
default: undefined
}
})

View File

@ -3,6 +3,7 @@
visible
:maskClosable="false"
title="标签详情"
:getContainer="getPopupContainer"
@cancel="cancel"
@ok="ok"
>
@ -47,6 +48,10 @@ const props = defineProps({
data: {
type: Object,
default: () => ({})
},
getPopupContainer: {
type: Function,
default: undefined
}
})

View File

@ -4,7 +4,7 @@
{ max: 64, message: '最多可输入64个字符' },
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: 'ID只能由数字、字母、下划线、中划线组成',
message: '标识只能由数字、字母、下划线、中划线组成',
},
]">
<j-input v-model:value="value.id" size="small" @change="asyncOtherConfig" :disabled="metadataStore.model.action === 'edit'" placeholder="请输入标识"></j-input>

View File

@ -38,6 +38,9 @@ const type = {
};
export const validatorConfig = (value: any, isObject: boolean = false) => {
console.log(value)
if (!value) {
return Promise.resolve()
}
@ -53,7 +56,7 @@ export const validatorConfig = (value: any, isObject: boolean = false) => {
return Promise.reject('请添加参数')
}
if (value.type === 'file' && !value.fileType) {
if (value.type === 'file' && (!value.fileType || !Object.keys(value.fileType).length)) {
return Promise.reject('请选择文件类型')
}
@ -168,7 +171,7 @@ export const useColumns = (type?: MetadataType, target?: 'device' | 'product', n
{ max: 64, message: '最多可输入64个字符' },
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: 'ID只能由数字、字母、下划线、中划线组成',
message: '标识只能由数字、字母、下划线、中划线组成',
},
]
},
@ -419,32 +422,28 @@ export const useColumns = (type?: MetadataType, target?: 'device' | 'product', n
}
},
doubleClick(record){
if (target !== 'device') {
return true
} else {
if (record.expands.source === 'rule') {
return true
}
return !isExtendsProdcut(record._sortIndex, productNoEdit?.value, 'expands')
}
},
form: {
required: true,
rules: target !== 'device' ? [
rules: [
{
callback: async (rule: any, value: any, dataSource: any[]) => {
const field = rule.field.split('.')
const fieldIndex = Number(field[1])
const values = dataSource.find((item, index) => index === fieldIndex)
const virtualRule = values.elements?.virtualRule
const virtualRule = values.expands?.virtualRule
const source = value.source
const ids = (noEdit?.value?.id || []) as any[]
console.log(source, value)
if (source) {
if (source !== 'rule' && !value.type?.length) {
if (source === 'device' && !value.type?.length) {
return Promise.reject('请选择读写类型');
} else if(!ids.includes(values.id) && virtualRule){
} else if( source === 'rule' && !virtualRule){
return Promise.reject('请配置规则');
}
@ -454,7 +453,7 @@ export const useColumns = (type?: MetadataType, target?: 'device' | 'product', n
return Promise.reject('请选择属性来源');
}
},
]: []
]
},
control(newValue, oldValue) {
if (newValue && !oldValue) {
@ -542,12 +541,12 @@ export const useColumns = (type?: MetadataType, target?: 'device' | 'product', n
},
form: {
required: true,
rules: [{
rules: [
{
callback(rule:any,value: any, dataSource: any[]) {
const field = rule.field.split('.')
const fieldIndex = Number(field[1])
const values = dataSource.find((item, index) => index === fieldIndex)
if (!values?.expands?.type?.length) {
return Promise.reject('请选择读写类型')
}

View File

@ -150,6 +150,7 @@ const columns = [
dataIndex: 'id',
type: 'text',
width: 100,
placement: 'Left',
form: {
required: true,
rules: [
@ -170,7 +171,7 @@ const columns = [
{ max: 64, message: '最多可输入64个字符' },
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: 'ID只能由数字、字母、下划线、中划线组成',
message: '标识只能由数字、字母、下划线、中划线组成',
},
]
}

View File

@ -1,13 +1,15 @@
<template>
<j-select-boolean
<j-select
v-model:value="myValue"
tureTitle="必填"
falseTitle='不必填'
style="width: 100%;"
:options="[
{ label: '不必填', value: 'false'},
{ label: '必填', value: 'true'},
]"
@change="change"
>
</j-select-boolean>
</j-select>
</template>
<script setup name="ConstraintSelect">
@ -32,14 +34,14 @@ const myValue = ref()
const change = (e) => {
const newData = { ...props.value }
set(newData, props.name, myValue.value)
set(newData, props.name, myValue.value === 'true')
console.log(newData, e);
emit('update:value', newData)
}
watch(() => JSON.stringify(props.data), () => {
console.log(props.value, props.name);
myValue.value = get(props.value, props.name) || false
myValue.value = get(props.value, props.name) === true ? 'true' : 'false'
}, { immediate: true })
</script>

View File

@ -86,6 +86,7 @@ const columns = [{
dataIndex: 'id',
type: 'text',
width: 100,
placement: 'Left',
form: {
required: true,
rules: [{
@ -105,7 +106,7 @@ const columns = [{
{ max: 64, message: '最多可输入64个字符' },
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: 'ID只能由数字、字母、下划线、中划线组成',
message: '标识只能由数字、字母、下划线、中划线组成',
},
]
}
@ -191,6 +192,7 @@ watch(
() => {
type.value = props.value?.valueType?.type;
_valueType.value = props.value?.valueType
console.log(props.value)
// elements.value = props.value?.valueType.elements;
// if (['float', 'double', 'int', 'long'].includes(type.value)) {
// const res = getUnit().then((res) => {

View File

@ -27,6 +27,7 @@ const columns = [
title: '参数标识',
dataIndex: 'id',
type: 'text',
placement: 'Left',
form: {
required: true,
rules: [{
@ -46,7 +47,7 @@ const columns = [
{ max: 64, message: '最多可输入64个字符' },
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: 'ID只能由数字、字母、下划线、中划线组成',
message: '标识只能由数字、字母、下划线、中划线组成',
},
]
}

View File

@ -4,7 +4,7 @@
<span>{{ TypeStringMap[data.record.valueType?.type] }}</span>
</template>
<template #required="{ data }">
<span>{{ data.record.expands?.required ? "是": '否' }}</span>
<span>{{ data.record.expands?.required ? "必填": '不必填' }}</span>
</template>
<template #config="{ data }">
<ConfigModal v-model:value="data.record.valueType" :showOther="false" />
@ -64,6 +64,7 @@ const columns = ref([
title: '参数标识',
dataIndex: 'id',
type: 'text',
placement: 'Left',
form: {
required: true,
rules: [
@ -84,7 +85,7 @@ const columns = ref([
{ max: 64, message: '最多可输入64个字符' },
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: 'ID只能由数字、字母、下划线、中划线组成',
message: '标识只能由数字、字母、下划线、中划线组成',
},
]
}

View File

@ -1,7 +1,7 @@
<template>
<div class="metadata-type">
<div class="metadata-type-select">
<DataTableTypeSelect v-model:value="type" @change="typeChange" />
<DataTableTypeSelect v-model:value="type" :allowClear="true" @change="typeChange" />
</div>
<DataTableArray
v-if="type === 'array'"
@ -101,6 +101,7 @@ const columns = [
title: '参数标识',
dataIndex: 'id',
type: 'text',
placement: 'Left',
form: {
required: true,
rules: [{
@ -120,7 +121,7 @@ const columns = [
{ max: 64, message: '最多可输入64个字符' },
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: 'ID只能由数字、字母、下划线、中划线组成',
message: '标识只能由数字、字母、下划线、中划线组成',
},
]
}

View File

@ -11,7 +11,9 @@
{{ data.record.range === true ? '范围值' : '固定值'}}
</template>
<template #value="{data}">
{{ data.record.range === true ? data.record.value?.join('-') : data.record.value }}
<j-ellipsis>
{{ data.record.range === true ? data.record.value?.join('-') : showText(data.record.value) }}
</j-ellipsis>
</template>
<template #action="{data}">
<j-button
@ -43,6 +45,10 @@ const props = defineProps({
type: {
type: String,
default: undefined
},
options: {
type: Array,
default: () => []
}
})
@ -53,12 +59,31 @@ const tableRef = ref()
provide('metricsType', props.type)
const showText = (value: any) => {
switch (props.type) {
case 'date':
return value;
case 'boolean':
const item = props.options.find(item => item.value === value)
if (item) {
return item.label
}else if (value) {
return value === 'true' ? '是' : '否'
} else {
return ''
}
default:
return value
}
}
const columns: any = [
{
title: '指标标识',
dataIndex: 'id',
width: 120,
type: 'text',
placement: 'Left',
form: {
required: true,
rules: [{
@ -78,7 +103,7 @@ const columns: any = [
{ max: 64, message: '最多可输入64个字符' },
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: 'ID只能由数字、字母、下划线、中划线组成',
message: '标识只能由数字、字母、下划线、中划线组成',
},
]
},
@ -109,14 +134,17 @@ const columns: any = [
width: 100,
type: 'components',
components: {
name: MetricValueItem
name: MetricValueItem,
props: {
options: props.options
}
},
form: {
required: true,
rules: [
{
callback(rule:any,value: any) {
if (!value) {
if (!value || (Array.isArray(value) && value.some(item => !item))) {
return Promise.reject('请配置指标')
}
return Promise.resolve()

View File

@ -1,7 +1,9 @@
<template>
<div class="metrics-item-value">
<div class="metrics-item-text">
<j-ellipsis>
{{ showText }}
</j-ellipsis>
</div>
<j-popconfirm-modal
:show-cancel="false"
@ -11,8 +13,8 @@
>
<template #content>
<j-form ref="formRef" :model="formData">
<j-form-item v-if="value.range === false" :rules="[{ required: true, message: '请输入指标值'}]" name="value">
<Item v-model:value="formData.value" />
<j-form-item v-if="value.range === false" :rules="[{ validator: typeValidator}]" name="value">
<Item v-model:value="formData.value" :options="options" />
</j-form-item>
<div v-else class="data-table-boolean-item">
<div class="data-table-boolean-item--value">
@ -41,6 +43,7 @@ import Item from './item.vue'
import {Form} from "jetlinks-ui-components";
import {cloneDeep} from "lodash";
import { FULL_CODE } from 'jetlinks-ui-components/es/DataTable'
import dayjs from "dayjs";
type ValueType = number | Array<number | undefined> | undefined;
@ -53,12 +56,16 @@ const props = defineProps({
type: Object as PropType<any>,
default: undefined,
},
options: {
type: Array,
default: () => []
}
});
const emit = defineEmits<Emit>();
const formItemContext = Form.useInjectFormItemContext();
const fullRef = inject(FULL_CODE);
const type = inject<string>('metricsType')
const formData = reactive<{
value: ValueType;
@ -72,26 +79,81 @@ const formData = reactive<{
const formRef = ref()
const showText = computed(() => {
if (props.value.range === false) {
return props.value?.value || ''
switch (type) {
case 'date':
return props.value?.value;
case 'boolean':
const _value = props.value?.value
const item = props.options.find(item => item.value === props.value?.value)
if (item) {
return item.label
}else if (_value) {
return _value === 'true' ? '是' : '否'
} else {
return ''
}
default:
return props.value?.value
}
} else {
return props.value?.value?.[0] ? props.value.value.join('-') : ''
}
})
const validatorTip = () =>{
let tip = '请输入'
if (['boolean', 'date'].includes(type!)) {
tip = '请选择'
}
return `${tip}指标值`
}
const validator = (_: any, value: any) => {
if (props.value.range && formData.rangeValue![0] >= formData.rangeValue![1]) {
return Promise.reject('需大于左侧数值')
}
return Promise.resolve()
}
const typeValidator = (_: any, value: any) => {
if (value === undefined) {
return Promise.reject(validatorTip())
}
if (type === 'string' && value?.length > 64) {
return Promise.reject('最多可输入64个字符')
}
return Promise.resolve()
}
const handleValueByType = (value: any, isRange: boolean = false) => {
if (isRange) {
return (value as number[]).map(item => {
const itemStr = String(item)
const index = String(item).indexOf('.')
return index === -1 ? item : Number(itemStr.substring(0, index))
})
} else {
const itemStr = String(value)
const index = String(value).indexOf('.')
return index === -1 ? value : Number(itemStr.substring(0, index))
}
}
const confirm = () => {
return new Promise((resolve, reject) => {
formRef.value.validate().then(() => {
const value = props.value.range === true ? formData.rangeValue : formData.value
console.log('confirm',value, props.value)
let value = props.value.range === true ? formData.rangeValue : formData.value
if (['int', 'long'].includes(type)) {
value = handleValueByType(value, props.value.range)
console.log('confirm',value, type)
}
emit('update:value', {
...props.value,
value: value
@ -121,6 +183,7 @@ watch(() => props.value.range,(value, oldValue) => {
display: flex;
gap: 12px;
align-items: center;
width: 100%;
.metrics-item-text {
flex: 1;

View File

@ -2,7 +2,6 @@
<j-input
v-if="type === 'string'"
v-model:value="myValue"
:maxLength="64"
placeholder="请输入"
@change="change"
/>
@ -11,7 +10,7 @@
v-model:value="myValue"
:precision="0"
:max="2147483647"
:min="-2147483647"
:min="-2147483648"
style="width: 100%"
placeholder="请输入"
@change="change"
@ -19,8 +18,8 @@
<j-input-number
v-else-if="type === 'long'"
v-model:value="myValue"
:max="9223372036854775807"
:min="-9223372036854775808"
:max="999999999999999"
:min="-999999999999999"
:precision="0"
placeholder="请输入"
style="width: 100%"
@ -29,8 +28,8 @@
<j-input-number
v-else-if="['float', 'double'].includes(type)"
v-model:value="myValue"
:max="9999999999999999"
:min="-9999999999999999"
:max="999999999999999"
:min="-999999999999999"
placeholder="请输入"
style="width: 100%"
@change="change"
@ -38,18 +37,21 @@
<j-select
v-else-if="type === 'boolean'"
placeholder="请选择"
:options="[
{ label: '否', value: 'false'},
{ label: '是', value: 'true'},
]"
v-model:value="myValue"
style="width: 100%"
:options="options"
:get-popup-container="(node) => fullRef || node"
@change="change"
/>
<j-date-picker
v-else-if="type === 'date' "
v-model:value="myValue"
show-time
format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
placeholder="请选择"
:get-popup-container="(node) => fullRef || node"
valueFormat="YYYY-MM-DD HH:mm:ss"
@change="change"
/>
</template>
@ -61,6 +63,10 @@ const props = defineProps({
value: {
type: [String, Number, Array],
default: undefined
},
options: {
type: Array,
default: () => []
}
})
@ -72,8 +78,8 @@ const myValue = ref(props.value)
const fullRef = inject(FULL_CODE);
const change = () => {
// formItemContext.onFieldChange()
emit('update:value', myValue.value)
formItemContext.onFieldChange()
}
watch(() => props.value, () => {

View File

@ -8,7 +8,7 @@
@cancel="cancel"
@visibleChange="visibleChange"
>
<template #content>
<template v-if="visible" #content>
<j-scrollbar height="350" v-if="showMetrics || config.length > 0">
<j-collapse v-model:activeKey="activeKey">
<j-collapse-panel v-for="(item, index) in config" :key="'store_'+index" :header="item.name">
@ -40,7 +40,7 @@
<AIcon type="ExclamationCircleOutlined" style="padding-left: 12px;padding-top: 4px;" />
</j-tooltip>
</template>
<Metrics ref="metricsRef" :value="myValue.metrics" :type="props.type"/>
<Metrics ref="metricsRef" :options="booleanOptions" :type="props.type" :value="myValue.metrics"/>
</j-collapse-panel>
</j-collapse>
@ -84,6 +84,10 @@ const props = defineProps({
type: String,
default: undefined
},
record: {
type: Object,
default: () => ({})
},
})
const fullRef = inject(FULL_CODE);
@ -109,6 +113,11 @@ const showMetrics = computed(() => {
return ['int', 'long', 'float', 'double', 'string', 'boolean', 'date'].includes(props.type as any)
})
const booleanOptions = ref([
{ label: '否', value: 'false'},
{ label: '是', value: 'true'},
])
const columns = ref([
{
title: '参数名称',
@ -129,9 +138,16 @@ const columns = ref([
const getConfig = async () => {
const id = type === 'product' ? productStore.current?.id : deviceStore.current.id
console.log(props.id, id, props.type)
console.log(props.id, id, props)
if(!props.id || !id || !props.type) return
if (props.type === 'boolean') {
const booleanValue = props.record.valueType
booleanOptions.value[0] = { label: booleanValue.falseText || '否', value: booleanValue.falseValue || 'false'}
booleanOptions.value[1] = { label: booleanValue.trueText || '是', value: booleanValue.trueValue || 'true'}
}
const params: any = {
deviceId: id,
metadata: {
@ -189,6 +205,7 @@ const confirm = () => {
}
const visibleChange = (e: boolean) => {
visible.value = e
if (e) {
configValue.value = omit(props.value, ['source', 'type', 'metrics', 'required'])
getConfig()
@ -200,6 +217,7 @@ const cancel = () => {
}
watch(() => props.value, () => {
console.log(props.value)
myValue.value = cloneDeep(props.value)
}, {immediate: true, deep: true})

View File

@ -18,11 +18,13 @@
:get-popup-container="(node) => fullRef || node"
placement="topLeft"
@confirm="confirm"
@visibleChange="visibleChange"
>
<template #content>
<j-scrollbar v-if="myValue">
<div style="padding: 0 10px">
<VirtualRule
v-if="visible"
:value="value"
:source="myValue"
:dataSource="dataSource"
@ -103,11 +105,16 @@ const formItemContext = Form.useInjectFormItemContext();
const myValue = ref<SourceType>('');
const type = ref<string>('');
const virtualRuleRef = ref<any>(null);
const visible = ref(false)
const visibleChange = (e: boolean) => {
visible.value = e
}
const disabled = computed(() => {
if (props.target === 'device') {
return true;
}
// if (props.target === 'device') {
// return true;
// }
return props.noEdit?.length
? props.noEdit.includes(props.value._sortIndex)
: false;
@ -149,6 +156,15 @@ const confirm = async () => {
});
};
const cancel = () => {
if (props.value.id && !props.value?.expands?.source) {
myValue.value = 'device';
} else {
myValue.value = props.value?.expands?.source || '';
}
type.value = props.value?.expands?.type || [];
}
watch(
() => props.value,
() => {

View File

@ -2,7 +2,7 @@
<j-button @click="visible = true" style="width: 100%" type="dashed">
编辑规则
</j-button>
<FRuleEditor :aggList="aggList" @close="onClose" v-if="visible" :value="value" @save="onChange" :id="id" :virtualRule="virtualRule" />
<FRuleEditor v-if="visible" :id="id" :aggList="aggList" :propertiesOptions="propertiesOptions" :value="value" :virtualRule="virtualRule" @close="onClose" @save="onChange" />
</template>
<script setup lang="ts" name="Rule">
@ -21,7 +21,8 @@ const props = defineProps({
value: String,
id: String,
virtualRule: Object,
aggList: Array
aggList: Array,
propertiesOptions: Array
});
const visible = ref<boolean>(false);

View File

@ -6,7 +6,10 @@
:options="typeOptions"
/>
<template v-if="source === 'rule'">
<j-form-item :name="['virtualRule', 'triggerProperties']" required>
<j-form-item :name="['virtualRule', 'triggerProperties']" :rules="[{
required: true,
message: '请选择触发属性'
}]">
<template #label>
触发属性
<j-tooltip>
@ -23,7 +26,7 @@
<j-select
v-model:value="formData.virtualRule.triggerProperties"
mode="multiple"
placeholder="请选择属性"
placeholder="请选择触发属性"
show-search
max-tag-count="responsive"
>
@ -50,21 +53,25 @@
</j-select>
</j-form-item>
<j-form-item
:name="['virtualRule', 'rule', 'script']"
:name="['virtualRule', 'script']"
label="计算规则"
required
>
<Rule
v-model:value="formData.virtualRule.script"
:virtualRule="_virtualRule.virtualRule"
:propertiesOptions="options"
:id="value.id"
:aggList="aggList"
/>
</j-form-item>
<j-form-item
label="窗口"
:name="['virtualRule', 'rule', 'windowType']"
required
:name="['virtualRule', 'windowType']"
:rules="[{
required: true,
message: '请选择窗口类型'
}]"
>
<j-select
v-model:value="formData.virtualRule.windowType"
@ -84,7 +91,10 @@
<j-form-item
label="聚合函数"
:name="['virtualRule', 'aggType']"
required
:rules="[{
required: true,
message: '请选择聚合函数'
}]"
>
<j-select
v-model:value="formData.virtualRule.aggType"
@ -107,7 +117,7 @@
},
{
pattern: /^\d+$/,
message: '请输入0-999999之间的正整数',
message: '请输入1-999999之间的正整数',
},
]"
>
@ -134,7 +144,7 @@
},
{
pattern: /^\d+$/,
message: '请输入0-999999之间的正整数',
message: '请输入1-999999之间的正整数',
},
]"
>
@ -249,11 +259,10 @@ const options = computed(() => {
});
const setInitVirtualRule = () => {
console.log(props.value?.expands?.virtualRule);
formData.virtualRule = {
...initData,
...(props.value?.expands?.virtualRule || {}),
triggerProperties: props.value?.expands?.virtualRule?.triggerProperties || ['*'],
triggerProperties: props.value?.expands?.virtualRule?.triggerProperties?.length ? props.value?.expands?.virtualRule?.triggerProperties : ['*']
}
}
@ -273,8 +282,9 @@ const handleSearch = async () => {
);
}
if (resp && resp.status === 200 && resp.result) {
const _triggerProperties = props.value?.expands?.virtualRule?.triggerProperties?.length ? props.value?.expands?.virtualRule?.triggerProperties : resp.result.triggerProperties
formData.virtualRule = {
triggerProperties: resp.result.triggerProperties,
triggerProperties: _triggerProperties?.length ? _triggerProperties : ['*'],
...resp.result.rule,
}
} else {
@ -317,6 +327,7 @@ watch(
formData.virtualRule = initData;
handleSearch();
setInitVirtualRule()
} else {
formData.virtualRule = undefined;
}
@ -332,8 +343,8 @@ const _virtualRule = computed(() => {
return {
type: formData?.type,
virtualRule: {
type: flag ? 'window' : 'script',
...formData?.virtualRule,
type: flag ? 'window' : 'script',
isVirtualRule: flag,
triggerProperties: formData?.virtualRule?.triggerProperties.includes('*')
? []

View File

@ -97,6 +97,7 @@ const handleConvertMetadata = (key: Key) => {
convertMetadata('to', 'alink', JSON.parse(metadata.value)).then(res => {
if (res.status === 200) {
value.value = JSON.stringify(res.result)
monacoValue.value = JSON.stringify(res.result)
}
});
}

View File

@ -1,9 +1,19 @@
<template>
<j-modal :mask-closable="false" title="导入物模型" destroy-on-close v-model:visible="_visible" @cancel="close"
@ok="handleImport" :confirm-loading="loading">
<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">
<AIcon type="ExclamationCircleOutlined" style="margin-right: 5px" />
<AIcon
type="ExclamationCircleOutlined"
style="margin-right: 5px"
/>
<template v-if="type === 'product'">
导入的物模型会覆盖原来的属性功能事件标签请谨慎操作
</template>
@ -12,83 +22,197 @@
</template>
</p>
</div>
<j-form layout="vertical" v-model="formModel">
<j-form-item v-if="type === 'product'" label="导入方式" v-bind="validateInfos.type">
<j-form layout="vertical" ref="formRef" :model="formModel">
<j-form-item
v-if="type === 'product'"
label="导入方式"
name="type"
:rules="[
{
required: true,
message: '请选择导入方式',
},
]"
>
<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" showSearch></j-select>
<j-form-item
label="选择产品"
:rules="[
{
required: true,
message: '请选择产品',
},
]"
name="copy"
v-if="formModel.type === 'copy'"
>
<j-select
:options="productList"
v-model:value="formModel.copy"
option-filter-prop="label"
placeholder="请选择产品"
showSearch
></j-select>
</j-form-item>
<j-form-item label="物模型类型" v-bind="validateInfos.metadata" v-if="type === 'device' || formModel.type === 'import'">
<j-form-item
label="物模型类型"
:rules="[
{
required: true,
message: '请选择物模型类型',
},
]"
name="metadata"
v-if="type === 'device' || formModel.type === 'import'"
>
<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-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'">
<j-select v-model:value="formModel.metadataType">
<j-form-item
label="导入类型"
:rules="[
{
required: true,
message: '请选择导入类型',
},
]"
name="metadataType"
v-if="type === 'device' || formModel.type === 'import'"
>
<j-select v-model:value="formModel.metadataType" @change="formModel.import = undefined">
<j-select-option value="file">文件上传</j-select-option>
<j-select-option value="script">脚本</j-select-option>
</j-select>
</j-form-item>
<j-form-item v-if="formModel.type === 'import' && formModel.metadataType === 'file'" label="文件上传" v-bind="validateInfos.upload">
<j-input v-model:value="formModel.upload">
<j-form-item
v-if="
formModel.type === 'import' &&
formModel.metadataType === 'file'
"
label="文件上传"
name="import"
:rules="[
{
required: true,
message: '请上传文件',
},
]"
>
<!-- <j-input v-model:value="formModel.upload">
<template #addonAfter>
<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" />
<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"
/>
</j-upload>
</template>
</j-input>
</j-input> -->
<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() }"
>
<j-button>
<template #icon><AIcon type="UploadOutlined" /></template>
上传文件
</j-button>
</j-upload>
<div style="margin-left: 10px; color: rgba(0, 0, 0, .6);">支持扩展名.json</div>
</j-form-item>
<j-form-item v-bind="validateInfos.import" v-if="(type === 'device' || formModel.type === 'import') && formModel.metadataType === 'script'">
<j-form-item
:rules="[
{
required: true,
message: '请输入物模型',
},
]"
name="import"
v-if="
(type === 'device' || formModel.type === 'import') &&
formModel.metadataType === 'script'
"
>
<template #label>
<j-space>
物模型
<j-tooltip title="在线编辑器中编写物模型脚本">
<AIcon type="QuestionCircleOutlined" style="color: rgb(136, 136, 136);"/>
<AIcon
type="QuestionCircleOutlined"
style="color: rgb(136, 136, 136)"
/>
</j-tooltip>
</j-space>
</template>
<JMonacoEditor v-model="formModel.import" theme="vs" style="height: 300px" lang="json"></JMonacoEditor>
<JMonacoEditor
v-model:modelValue="formModel.import"
theme="vs"
style="height: 300px"
lang="json"
></JMonacoEditor>
</j-form-item>
</j-form>
</j-modal>
</template>
<script setup lang="ts" name="Import">
import { useForm } from 'ant-design-vue/es/form';
import { saveMetadata } from '@/api/device/instance'
import { queryNoPagingPost, convertMetadata, modify } from '@/api/device/product'
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 } from '@/views/device/Product/typings'
import { useInstanceStore } from '@/store/instance'
import type {
UploadProps,
UploadFile,
UploadChangeParam,
} from 'ant-design-vue/es';
import type { DeviceMetadata } from '@/views/device/Product/typings';
import { useInstanceStore } from '@/store/instance';
import { useProductStore } from '@/store/product';
import { FILE_UPLOAD } from '@/api/comm';
import { getToken, onlyMessage } from '@/utils/comm';
import { useMetadataStore } from '@/store/metadata';
import {omit} from "lodash-es";
import { Modal } from 'jetlinks-ui-components'
import { omit } from 'lodash-es';
import { Modal } from 'jetlinks-ui-components';
const route = useRoute()
const instanceStore = useInstanceStore()
const productStore = useProductStore()
const route = useRoute();
const instanceStore = useInstanceStore();
const productStore = useProductStore();
interface Props {
visible: boolean,
type: 'device' | 'product',
visible: boolean;
type: 'device' | 'product';
}
interface Emits {
(e: 'update:visible', data: boolean): void;
(e: 'submit', data: any): void;
}
const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const loading = ref(false)
const props = defineProps<Props>();
const emits = defineEmits<Emits>();
const loading = ref(false);
const _visible = computed({
get: () => {
@ -97,206 +221,224 @@ const _visible = computed({
set: (val: any) => {
emits('update:visible', val);
},
})
});
const close = () => {
emits('update:visible', false);
}
};
/** form表单 */
const formModel = reactive<Record<string, any>>({
type: 'import',
metadata: 'jetlinks',
metadataType: 'script',
})
const rules = reactive({
type: [
{
required: true,
message: '请选择导入方式',
},
],
copy: [
{
required: true,
message: '请选择产品',
},
],
metadata: [
{
required: true,
message: '请选择物模型类型',
},
],
metadataType: [
{
required: true,
message: '请选择导入类型',
},
],
upload: [
{
required: true,
message: '请上传文件',
},
],
import: [
{
required: true,
message: '请输入物模型',
},
],
})
const { validate, validateInfos } = useForm(formModel, rules);
const fileList = ref<UploadFile[]>([])
const hasVirtualRule = ref(false)
});
// const { validate, validateInfos } = useForm(formModel, rules);
const fileList = ref<UploadFile[]>([]);
const hasVirtualRule = ref(false);
const formRef = ref();
const productList = ref<DefaultOptionType[]>([])
const productList = ref<DefaultOptionType[]>([]);
const loadData = async () => {
const { id } = route.params || {}
const product = await queryNoPagingPost({
const { id } = route.params || {};
const product = (await queryNoPagingPost({
paging: false,
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [{ column: 'id$not', value: id }],
}) as any
productList.value = product.result.filter((i: any) => i?.metadata).map((item: any) => ({
})) as any;
productList.value = product.result
.filter((i: any) => i?.metadata)
.map((item: any) => ({
label: item.name,
value: item.metadata,
key: item.id
})) as DefaultOptionType[]
}
loadData()
key: item.id,
})) as DefaultOptionType[];
};
loadData();
const beforeUpload: UploadProps['beforeUpload'] = file => {
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
if(file.type === 'application/json') {
const reader = new FileReader();
reader.readAsText(file);
reader.onload = (json) => {
if(json.target?.result){
onlyMessage('操作成功!')
formModel.import = json.target?.result;
};
} else {
onlyMessage('文件内容不能为空', 'error')
}
};
} else {
onlyMessage('请上传json格式的文件', 'error')
}
};
const fileChange = (info: UploadChangeParam) => {
if (info.file.status === 'done') {
const { response } = info.file
const { response } = info.file;
if (response.status === 200) {
formModel.upload = response.result
formModel.upload = response.result;
}
}
};
const uniqArray = (arr: any[]) => {
const _map = new Map();
for(let item of arr) {
_map.set(item.id, item)
}
return [..._map.values()]
}
const operateLimits = (mdata: DeviceMetadata) => {
hasVirtualRule.value = false
hasVirtualRule.value = false;
const obj: DeviceMetadata = { ...mdata };
const old = JSON.parse(instanceStore.detail?.metadata || '{}');
const fid = instanceStore.detail?.features?.map(item => item.id);
const old = JSON.parse((props.type === 'device' ? instanceStore.detail?.metadata : productStore.detail?.metadata) || '{}');
const fid = instanceStore.detail?.features?.map((item) => item.id);
const _data: DeviceMetadata = {
properties: [],
events: [],
functions: [],
tags: []
}
_data.properties = uniqArray([...(old?.properties || []), ...uniqArray(obj?.properties || [])])
_data.events = uniqArray([...(old?.events || []), ...uniqArray(obj?.events || [])])
_data.functions = uniqArray([...(old?.functions || []), ...uniqArray(obj?.functions || [])])
_data.tags = uniqArray([...(old?.tags || []), ...uniqArray(obj?.tags || [])])
if (fid?.includes('eventNotModifiable')) {
obj.events = old?.events || [];
_data.events = old?.events || [];
}
if (fid?.includes('propertyNotModifiable')) {
obj.properties = old?.properties || [];
_data.properties = old?.properties || [];
}
(obj?.events || []).map((item, index) => {
return { ...item, sortsIndex: index };
});
(obj?.properties || []).map((item, index) => {
(_data?.properties || []).map((item) => {
if (item.expands?.source === 'rule') {
hasVirtualRule.value = true
item.expands = omit(item.expands, ['virtualRule'])
hasVirtualRule.value = true;
item.expands = omit(item.expands, ['virtualRule']);
}
return { ...item, sortsIndex: index };
return item
});
(obj?.functions || []).map((item, index) => {
return { ...item, sortsIndex: index };
});
(obj?.tags || []).map((item, index) => {
return { ...item, sortsIndex: index };
});
return obj;
return _data;
};
const metadataStore = useMetadataStore()
const metadataStore = useMetadataStore();
const handleImport = async () => {
validate().then(async (data) => {
loading.value = true
const { id } = route.params || {}
formRef.value.validate().then(async (data: any) => {
const { id } = route.params || {};
if (data.metadata === 'alink') {
const res = await convertMetadata('from', 'alink', JSON.parse(data.import)).catch(err => err)
try {
const _import = JSON.parse(data.import);
loading.value = true;
const res = await convertMetadata(
'from',
'alink',
_import,
).catch((err) => err);
if (res.status === 200) {
const metadata = operateLimits(res.result)
const metadata = operateLimits(res.result);
let result;
if (props?.type === 'device') {
result = await saveMetadata(id as string, metadata).catch(err => err)
result = await saveMetadata(
id as string,
metadata,
).catch((err) => err);
} else {
result = await modify(id as string, { id, metadata: JSON.stringify(metadata) }).catch(err => err)
result = await modify(id as string, {
id,
metadata: JSON.stringify(metadata),
}).catch((err) => err);
}
if (result.success) {
onlyMessage('导入成功')
onlyMessage('导入成功');
}
loading.value = false
loading.value = false;
} else {
loading.value = false
// onlyMessage('!', 'error')
return
}
if (props?.type === 'device') {
await instanceStore.refresh(id as string)
} else {
await productStore.getDetail(id as string)
}
metadataStore.set('importMetadata', true)
// Store.set(SystemConst.GET_METADATA, true)
// Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
close()
} else {
try {
const _object = JSON.parse(data[props?.type === 'device' ? 'import' : data?.type] || '{}')
if (
!(!!_object?.properties || !!_object?.events || !!_object?.functions || !!_object?.tags)
) {
onlyMessage('物模型数据不正确', 'error')
loading.value = false;
return;
}
const { id } = route.params || {}
const copyOperateLimits = operateLimits(_object as DeviceMetadata)
if (props?.type === 'device') {
await instanceStore.refresh(id as string);
} else {
await productStore.getDetail(id as string);
}
metadataStore.set('importMetadata', true);
close();
} catch (e) {
onlyMessage(
e === 'error'
? '物模型数据不正确'
: '上传json格式的物模型文件',
'error',
);
}
} else {
try {
const _object = JSON.parse(
data[data?.type === 'copy' ? 'copy' : 'import'] ||
'{}',
);
if (
!(
!!_object?.properties ||
!!_object?.events ||
!!_object?.functions ||
!!_object?.tags
)
) {
onlyMessage('物模型数据不正确', 'error');
loading.value = false;
return;
}
const { id } = route.params || {};
const copyOperateLimits = operateLimits(
_object as DeviceMetadata,
);
const params = {
id,
metadata: JSON.stringify(copyOperateLimits),
};
const paramsDevice = copyOperateLimits
let resp = undefined
const paramsDevice = copyOperateLimits;
let resp = undefined;
loading.value = true;
if (props?.type === 'device') {
resp = await saveMetadata(id as string, paramsDevice)
resp = await saveMetadata(id as string, paramsDevice);
} else {
resp = await modify(id as string, params)
resp = await modify(id as string, params);
}
loading.value = false
loading.value = false;
if (resp.success) {
onlyMessage('导入成功')
onlyMessage('导入成功');
if (hasVirtualRule.value) {
setTimeout(() => {
Modal.info({
title: '导入数据存在虚拟属性,请及时添加虚拟属性计算规则。',
okText: '确认'
})
}, 300)
okText: '确认',
});
}, 300);
}
}
if (props?.type === 'device') {
await instanceStore.refresh(id as string)
await instanceStore.refresh(id as string);
} else {
await productStore.getDetail(id as string)
await productStore.getDetail(id as string);
}
metadataStore.set('importMetadata', true)
metadataStore.set('importMetadata', true);
close();
} catch (e) {
loading.value = false
onlyMessage(e === 'error' ? '物模型数据不正确' : '上传json格式的物模型文件', 'error')
loading.value = false;
onlyMessage(
e === 'error'
? '物模型数据不正确'
: '上传json格式的物模型文件',
'error',
);
}
}
})
}
});
};
// const showProduct = computed(() => formModel.type === 'copy')
</script>

Some files were not shown because too many files have changed in this diff Show More