Merge branch 'dev'

# Conflicts:
#	src/views/notice/Template/Detail/index.vue
This commit is contained in:
xieyonghong 2023-04-25 18:38:49 +08:00
commit 3f80c9b693
77 changed files with 1943 additions and 408 deletions

View File

@ -24,7 +24,16 @@ yarn add jetlinks-ui-components@latest
yarn dev:force
```
## Node
* node >= 18.14.0
## 浏览器兼容
* Chrome >= 100
* Firefox >= 100
* Edge >= 100
不支持IE
### 备注
项目在开发模式下,首页加载慢属于正常现象;
* 项目在开发模式下,首页加载慢属于正常现象;
* 打开F12后页面卡顿是`vuetools`引起,[https://github.com/vuejs/devtools/issues/1987](https://github.com/vuejs/devtools/issues/1987)

View File

@ -1,3 +1,3 @@
#!/usr/bin/env bash
docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:1.0.0 .
docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:1.0.0
docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.1 .
docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.1

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.8",
"jetlinks-ui-components": "^1.0.9",
"js-cookie": "^3.0.1",
"less": "^4.1.3",
"less-loader": "^11.1.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -573,4 +573,17 @@ export const queryLogsType = () => server.get(`/dictionary/device-log-type/items
export const getDeviceNumber = (data?:any) => server.post<number>('/device-instance/_count', data)
/**
*
* @param productId
* @param data
*/
export const importDeviceByPlugin = (productId: string, data: any[]) => server.post(`/device/instance/plugin/${productId}/import`, data)
export const metadateMapById = (productId: string, data: ant[]) => server.patch(`/device/metadata/mapping/product/${productId}`, data)
export const getMetadateMapById = (productId: string) => server.get(`/device/metadata/mapping/product/${productId}`)
export const getInkingDevices = (data: string[]) => server.post('/plugin/mapping/device/_all', data)

View File

@ -146,8 +146,8 @@ const getBackgroundColor = (code: string | number) => {
const _color = color[code] || color.default;
return `linear-gradient(
188.4deg,
rgba(${_color}, 0.03) 22.94%,
rgba(${_color}, 0) 94.62%
rgba(${_color}, 0.03) 30%,
rgba(${_color}, 0) 80%
)`;
};

View File

@ -201,6 +201,7 @@ onUnmounted(() => {
if (ws.value) {
ws.value.unsubscribe?.();
}
clearAction()
})
const options = ref<{ label: string, value: string }[]>()

View File

@ -32,20 +32,14 @@ export const defaultBranches = [
{
terms: [
{
terms: [
{
column: undefined,
value: {
source: 'fixed',
value: undefined
},
termType: undefined,
key: 'params_1',
type: 'and',
},
],
column: undefined,
value: {
source: 'fixed',
value: undefined
},
termType: undefined,
key: 'params_1',
type: 'and',
key: 'terms_1_terms_1',
},
],
type: 'and',
@ -124,6 +118,7 @@ export const useSceneStore = defineStore('scene', () => {
branches.push(null);
}
}
console.log(branches)
data.value = {
...result,
trigger: result.trigger || {},

View File

@ -63,11 +63,12 @@ export const put = function <T>(url: string, data = {}) {
* @param {Object} [data]
* @returns {AxiosInstance}
*/
export const patch = function <T>(url: string, data = {}) {
export const patch = function <T>(url: string, data = {}, ext: any = {}) {
return request<any, AxiosResponseRewrite<T>>({
method: 'PATCH',
url,
data
data,
...ext
})
}
/**

View File

@ -72,6 +72,13 @@
}"
:params="params"
>
<template #completeTime="slotProps">
<span>{{
moment(slotProps.completeTime).format(
'YYYY-MM-DD HH:mm:ss',
)
}}</span>
</template>
<template #createTime="slotProps">
<span>{{
moment(slotProps.createTime).format(

View File

@ -0,0 +1,3 @@
import Inkling from './index.vue'
export default Inkling

View File

@ -0,0 +1,72 @@
<template>
<j-modal
:width="800"
:mask-closable="false"
:visible="true"
title="设备ID映射"
:confirmLoading="loading"
@ok="handleOk"
@cancel="handleCancel"
>
<InklingDevice
v-model:value='checkKey'
:accessId='accessId'
/>
</j-modal>
</template>
<script setup lang='ts' name='InklingModal'>
import InklingDevice from '@/views/device/components/InklingDevice'
import { onlyMessage } from '@/utils/comm'
import { savePluginData } from '@/api/link/plugin'
type Emit = {
(e: 'cancel'): void
(e: 'submit', data: string): void
}
const props = defineProps({
accessId: {
type: String,
default: undefined
},
channelId: {
type: String,
default: undefined
},
id: {
type: String,
default: undefined
}
})
const emit = defineEmits<Emit>()
const checkKey = ref(props.id)
const loading = ref(false)
const route = useRoute()
const handleOk = async () => {
if (checkKey.value) {
const res = await savePluginData(
'device',
props.channelId!,
route.params.id as string,
checkKey.value
).catch(() => ({ success: false }))
if (res.success) {
emit('submit', checkKey.value)
}
} else {
onlyMessage('请选择设备', 'error')
}
}
const handleCancel = () => {
emit('cancel')
}
</script>
<style scoped>
</style>

View File

@ -14,12 +14,36 @@
<j-descriptions-item label="设备ID">{{
instanceStore.current?.id
}}</j-descriptions-item>
<j-descriptions-item v-if='instanceStore.current?.accessProvider === "plugin_gateway"'>
<template #label>
<div>
第三方系统设备ID
<j-tooltip>
<template #title>
<p>通过调用SDK或HTTP请求的方式接入第三方系统设备数据时第三方系统与平台当前设备对应的设备ID</p>
如双方ID值一致则无需填写
</template>
<a-icon type='QuestionCircleOutlined' />
</j-tooltip>
</div>
</template>
<j-button v-if='!inklingDeviceId' type="link" @click='giveAnInkling'>映射</j-button>
<div v-else style='display: flex;justify-content: space-between;align-items: center;'>
<div style='flex: 1 1 auto;'>
<j-ellipsis>{{ inklingDeviceId }}</j-ellipsis>
</div>
<j-button type='link'>
<a-icon
type='EditOutlined'
@click='inkingVisible = true'
/>
</j-button>
</div>
</j-descriptions-item>
<j-descriptions-item label="产品名称">{{
instanceStore.current?.productName
}}</j-descriptions-item>
<!-- <j-descriptions-item label="产品分类">{{-->
<!-- instanceStore.current?.classifiedName-->
<!-- }}</j-descriptions-item>-->
<j-descriptions-item label="设备类型">{{
instanceStore.current?.deviceType?.text
}}</j-descriptions-item>
@ -83,6 +107,14 @@
@close="visible = false"
@save="saveBtn"
/>
<InkingModal
v-if='inkingVisible'
:id='inklingDeviceId'
:channelId='channelId'
:accessId='instanceStore.current.accessId'
@cancel="inkingVisible = false"
@submit='saveInkling'
/>
</template>
<script lang="ts" setup>
@ -91,10 +123,16 @@ import Save from '../../Save/index.vue';
import Config from './components/Config/index.vue';
import Tags from './components/Tags/index.vue';
import Relation from './components/Relation/index.vue';
import InkingModal from './components/InklingModal'
import moment from 'moment';
import { detail as queryPluginAccessDetail } from '@/api/link/accessConfig'
import { getPluginData } from '@/api/link/plugin'
const visible = ref<boolean>(false);
const inkingVisible = ref<boolean>(false);
const instanceStore = useInstanceStore();
const inklingDeviceId = ref()
const channelId = ref()
const saveBtn = () => {
if (instanceStore.current?.id) {
@ -102,4 +140,37 @@ const saveBtn = () => {
}
visible.value = false;
};
const saveInkling = (id: string) => {
if (instanceStore.current?.id) {
instanceStore.refresh(instanceStore.current?.id);
}
channelId.value = id
giveAnInkling()
}
const giveAnInkling = () => {
inkingVisible.value = true
}
const queryInkling = () => {
if (instanceStore.current?.accessProvider === 'plugin_gateway') {
queryPluginAccessDetail(instanceStore.current?.accessId).then(async res => {
if (res.success) {
channelId.value = res.result.channelId
const pluginRes = await getPluginData('device', res.result.channelId, instanceStore.current?.id)
if (pluginRes.success) {
inklingDeviceId.value = pluginRes.result?.externalId
}
}
})
}
}
watch(() => instanceStore.current?.id, () => {
if (instanceStore.current?.id) {
queryInkling()
}
}, { immediate: true })
</script>

View File

@ -0,0 +1,178 @@
<template>
<div class='file'>
<j-form layout='vertical'>
<j-form-item label='文件格式' >
<div class='file-type-label'>
<a-radio-group class='file-type-radio' v-model:value="modelRef.file.fileType" >
<a-radio-button value="xlsx">xlsx</a-radio-button>
<a-radio-button value="csv">csv</a-radio-button>
</a-radio-group>
<a-checkbox v-model:checked="modelRef.file.autoDeploy">自动启用</a-checkbox>
</div>
</j-form-item>
<j-form-item label="文件上传">
<j-upload
v-model:fileList="modelRef.upload"
name="file"
:action="FILE_UPLOAD"
:headers="{
'X-Access-Token': LocalStore.get(TOKEN_KEY),
}"
:maxCount="1"
:showUploadList="false"
@change="uploadChange"
:accept="
modelRef?.file?.fileType ? `.${modelRef?.file?.fileType}` : '.xlsx'
"
:before-upload="beforeUpload"
:disabled='disabled'
>
<j-button style='width: 760px;'>
<template #icon><AIcon type="UploadOutlined" /></template>
上传文件
</j-button>
</j-upload>
</j-form-item>
<j-form-item label='下载模板'>
<div class='file-download'>
<j-button @click="downFile('xlsx')">.xlsx</j-button>
<j-button @click="downFile('csv')">.csv</j-button>
</div>
</j-form-item>
</j-form>
<div v-if="importLoading">
<j-badge v-if="flag" status="processing" text="进行中" />
<j-badge v-else status="success" text="已完成" />
<span>总数量{{ count }}</span>
<p style="color: red">{{ errMessage }}</p>
</div>
</div>
</template>
<script setup lang='ts' name='DeviceImportFile'>
import { FILE_UPLOAD } from '@/api/comm';
import { TOKEN_KEY } from '@/utils/variable';
import { LocalStore, onlyMessage } from '@/utils/comm';
import { downloadFileByUrl } from '@/utils/utils';
import {
deviceImport,
templateDownload,
} from '@/api/device/instance';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { message } from 'jetlinks-ui-components'
const props = defineProps({
product: {
type: String,
default: undefined
}
})
const modelRef = reactive({
product: props.product,
upload: [],
file: {
fileType: 'xlsx',
autoDeploy: false,
},
});
const importLoading = ref<boolean>(false);
const flag = ref<boolean>(false);
const count = ref<number>(0);
const errMessage = ref<string>('');
const disabled = ref(false)
const downFile = async (type: string) => {
const res: any = await templateDownload(props.product!, type);
if (res) {
const blob = new Blob([res], { type: type });
const url = URL.createObjectURL(blob);
downloadFileByUrl(url, `设备导入模板`, type);
}
};
const beforeUpload = (_file: any) => {
const fileType = modelRef.file?.fileType === 'csv' ? 'csv' : 'xlsx';
const isCsv = _file.type === 'text/csv';
const isXlsx = _file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
if (!isCsv && fileType !== 'xlsx') {
onlyMessage('请上传.csv格式文件', 'warning');
}
if (!isXlsx && fileType !== 'csv') {
onlyMessage('请上传.xlsx格式文件', 'warning');
}
return (isCsv && fileType !== 'xlsx') || (isXlsx && fileType !== 'csv');
};
const submitData = async (fileUrl: string) => {
if (!!fileUrl) {
count.value = 0;
errMessage.value = '';
const autoDeploy = !!modelRef?.file?.autoDeploy || false;
importLoading.value = true;
let dt = 0;
const source = new EventSourcePolyfill(
deviceImport(props.product!, fileUrl, autoDeploy),
);
source.onmessage = (e: any) => {
const res = JSON.parse(e.data);
if (res.success) {
const temp = res.result.total;
dt += temp;
count.value = dt;
} else {
errMessage.value = res.message || '失败';
}
disabled.value = false
};
source.onerror = (e: { status: number }) => {
if (e.status === 403) errMessage.value = '暂无权限,请联系管理员';
flag.value = false;
disabled.value = false
source.close();
};
source.onopen = () => {};
} else {
message.error('请先上传文件');
}
};
const uploadChange = async (info: Record<string, any>) => {
disabled.value = true
console.log(info.file)
if (info.file.status === 'done') {
const resp: any = info.file.response || { result: '' };
await submitData(resp?.result || '');
}
};
</script>
<style scoped lang='less'>
.file {
.file-type-label {
display: flex;
gap: 16px;
align-items: center;
.file-type-radio {
display: flex;
flex-grow: 1;
:deep(.ant-radio-button-wrapper) {
width: 50%;
}
}
}
.file-download {
display: flex;
gap: 16px;
>button {
flex: 1;
min-width: 0;
}
}
}
</style>

View File

@ -1,17 +1,39 @@
<template>
<j-modal
:maskClosable="false"
:visible="true"
:visible="visible"
width="800px"
title="导入"
title="批量导入"
@cancel='cancel'
>
<div>
<!-- 选择产品 -->
<div v-if='steps === 0'>
<Product
v-model:rowKey='importData.productId'
@change='productChange'
/>
</div>
<div v-else-if='steps === 1'>
<j-form :layout="'vertical'">
<j-form-item required label='选择导入方式'>
<j-card-select
:value="[importData.type]"
:column='typeOptions.length'
:options="typeOptions"
@change='typeChange'
>
<template #image='{image}'>
<img :src='image' />
</template>
</j-card-select>
</j-form-item>
</j-form>
</div>
<div v-else>
<File v-if='importData.type ==="file"' :product='importData.productId' />
<Plugin v-else :accessId='productDetail.accessId' @change='pluginChange'/>
</div>
</div>
<template #footer>
<j-button v-if='steps === 0' @click='cancel' >取消</j-button>
@ -20,26 +42,79 @@
<j-button v-if='steps === 2' @click='save' type='primary'>确认</j-button>
</template>
</j-modal>
<j-modal
:maskClosable="false"
:visible="importVisible"
width="400px"
title="导入完成"
@cancel='importCancel'
@ok='importCancel'
>
<a-icon type='CheckOutlined' style='color: #2F54EB;' /> 已完成 新增设备 <span style='color: #2F54EB;'>{{count}}</span>
</j-modal>
</template>
<script lang='ts' setup name='DeviceImport'>
import Product from './product.vue'
import { onlyMessage } from '@/utils/comm'
import { queryList } from '@/api/device/product';
import { getImage, onlyMessage } from '@/utils/comm'
import File from './file.vue'
import Plugin from './plugin.vue'
import { importDeviceByPlugin } from '@/api/device/instance'
const emit = defineEmits(['cancel', 'save']);
const steps = ref(0) //
const importData = reactive({
const importData = reactive<{productId?: string, type?: string}>({
productId: undefined,
type: undefined,
})
const productDetail = ref()
const deviceList = ref<any[]>([])
const visible = ref(true)
const importVisible = ref(false)
const count = ref(0)
const typeOptions = computed(() => {
const array = [
{
value: 'file',
label: '文件导入',
subLabel: '支持上传XLSX、CSV格式文件',
iconUrl: getImage('/device/import1.png'),
},
]
if (productDetail.value?.accessProvider === 'plugin_gateway') {
array.push({
value: 'plugin',
label: '插件导入',
subLabel: '读取插件中的设备信息同步至平台',
iconUrl: getImage('/device/import2.png'),
})
}
return array
})
const typeChange = (types: string[]) => {
importData.type = types[0]
}
const productChange = (detail: any) => {
productDetail.value = detail
}
const next = () => {
if (steps.value === 0 && !importData.productId) {
return onlyMessage('请选择产品', 'error')
if (steps.value === 0) {
if (!importData.productId) {
return onlyMessage('请选择产品', 'error')
}
if (productDetail.value?.accessProvider !== 'plugin_gateway') {
importData.type = 'file'
importData.productId = productDetail.value?.id
steps.value = 2
return
}
}
if (steps.value === 1 && !importData.type) {
return onlyMessage('请选择导入方式', 'error')
@ -48,7 +123,7 @@ const next = () => {
}
const prev = () => {
if (steps.value === 2 && importData.type) {
if (productDetail.value?.accessProvider !== 'plugin_gateway') {
steps.value = 0
} else {
steps.value -= 1
@ -59,7 +134,33 @@ const cancel = () => {
emit('cancel')
}
const pluginChange = (options: any[]) => {
deviceList.value = options
}
const save = () => {
if (importData.type === 'file') {
cancel()
emit('save')
} else {
if (deviceList.value.length) {
importDeviceByPlugin(importData.productId!, deviceList.value).then(res => {
if (res.success) {
onlyMessage('操作成功')
// cancel()
visible.value = false
importVisible.value = true
count.value = res.result?.[0]?.result?.updated
}
})
} else {
onlyMessage('请选择设备', 'error')
}
}
}
const importCancel = () => {
importVisible.value = false
emit('save')
}
</script>

View File

@ -0,0 +1,40 @@
<template>
<div style=''>
<InklingDevice
:accessId='accessId'
:multiple='true'
@change='change'
/>
</div>
</template>
<script setup lang='ts' name='DeviceImportPlugin'>
import InklingDevice from '@/views/device/components/InklingDevice'
type Emit = {
(e: 'change', data: any[]): void
}
const props = defineProps({
accessId: {
type: String,
default: undefined
}
})
const emit = defineEmits<Emit>()
const change = (options: any[]) => {
emit('change', options)
}
</script>
<style scoped lang='less'>
:deep(.device-import-product) {
margin-bottom: 0;
padding-right: 0px;
padding-left: 0px;
padding-top: 6px;
}
</style>

View File

@ -3,56 +3,72 @@
:columns="columns"
type='simple'
@search="handleSearch"
class="scene-search"
class="device-import-product"
target="device-import-product"
/>
<j-divider style='margin: 0' />
<j-pro-table
model='CARD'
:columns='columns'
:params='params'
:request='productQuery'
:gridColumn='2'
:gridColumns='[2,2,2]'
:bodyStyle='{
paddingRight: 0,
paddingLeft: 0
}'
>
<template #card="slotProps">
<CardBox
:value='slotProps'
:active="rowKey === slotProps.id"
:status="slotProps.state"
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
:statusNames="{ 1: 'processing', 0: 'error', }"
@click="handleClick"
>
<template #img>
<slot name="img">
<img width='80' height='80' :src="slotProps.photoUrl || getImage('/device-product.png')" />
</slot>
</template>
<template #content>
<div style='width: calc(100% - 100px)'>
<Ellipsis>
<span style="font-size: 16px;font-weight: 600" >
{{ slotProps.name }}
</span>
</Ellipsis>
</div>
<j-row>
<j-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>直连设备</div>
</j-col>
</j-row>
</template>
</CardBox>
</template>
</j-pro-table>
<j-scrollbar :height='400'>
<j-pro-table
model='CARD'
:columns='columns'
:params='params'
:request='queryProductList'
:gridColumn='2'
:defaultParams="{
terms: [
{
column: 'state',
value: '1',
type: 'and'
},
{
column: 'accessProvider',
value: props?.type
}
],
sorts: [{ name: 'createTime', order: 'desc' }]
}"
:gridColumns='[2,2,2]'
:bodyStyle='{
paddingRight: 0,
paddingLeft: 0
}'
>
<template #card="slotProps">
<CardBox
:value='slotProps'
:active="rowKey === slotProps.id"
:status="slotProps.state"
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
:statusNames="{ 1: 'processing', 0: 'error', }"
@click="handleClick"
>
<template #img>
<slot name="img">
<img width='80' height='80' :src="slotProps.photoUrl || getImage('/device-product.png')" />
</slot>
</template>
<template #content>
<div style='width: calc(100% - 100px)'>
<Ellipsis>
<span style="font-size: 16px;font-weight: 600" >
{{ slotProps.name }}
</span>
</Ellipsis>
</div>
<j-row>
<j-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>直连设备</div>
</j-col>
</j-row>
</template>
</CardBox>
</template>
</j-pro-table>
</j-scrollbar>
</template>
<script setup lang='ts' name='Product'>
@ -112,32 +128,18 @@ const columns = [
})
}
},
{
title: '接入方式',
dataIndex: 'accessName',
width: 150,
ellipsis: true,
search: {
type: 'select',
options: () => queryGatewayList().then((resp: any) =>
resp.result.map((item: any) => ({
label: item.name, value: item.id
}))
)
}
},
{
title: '设备类型',
dataIndex: 'deviceType',
width: 150,
search: {
type: 'select',
options: [
{ label: '直连设备', value: 'device' },
{ label: '网关子设备', value: 'childrenDevice' },
{ label: '网关设备', value: 'gateway' },
]
}
// search: {
// type: 'select',
// options: [
// { label: '', value: 'device' },
// { label: '', value: 'childrenDevice' },
// { label: '', value: 'gateway' },
// ]
// }
},
{
title: '状态',
@ -156,88 +158,16 @@ const columns = [
dataIndex: 'describe',
ellipsis: true,
width: 300,
},
{
dataIndex: 'classifiedId',
title: '分类',
hideInTable: true,
search: {
type: 'treeSelect',
options: () => {
return new Promise((res => {
queryTree({ paging: false }).then(resp => {
res(resp.result)
})
}))
},
componentProps: {
fieldNames: {
label: 'name',
value: 'id',
}
}
type: 'string',
}
},
{
dataIndex: 'id$dim-assets',
title: '所属组织',
hideInTable: true,
search: {
type: 'treeSelect',
options: () => new Promise((resolve) => {
getTreeData_api({ paging: false }).then((resp: any) => {
const formatValue = (list: any[]) => {
return list.map((item: any) => {
if (item.children) {
item.children = formatValue(item.children);
}
return {
...item,
value: JSON.stringify({
assetType: 'product',
targets: [
{
type: 'org',
id: item.id,
},
],
}),
}
})
}
resolve(formatValue(resp.result) || [])
})
})
}
}
]
const handleSearch = (p: any) => {
params.value = p
}
const productQuery = async (p: any) => {
const sorts: any = [];
if (props.rowKey) {
sorts.push({
name: 'id',
value: props.rowKey,
});
}
sorts.push({ name: 'createTime', order: 'desc' });
p.sorts = sorts
const resp = await queryProductList(p)
if (resp.success && props.rowKey && firstFind.value) {
const productItem = (resp.result as { data: any[]}).data.find((item: any) => item.id === props.rowKey)
emit('update:detail', productItem)
firstFind.value = false
}
return {
...resp
}
}
const handleClick = (detail: any) => {
emit('update:rowKey', detail.id)
emit('change', detail)
@ -246,7 +176,7 @@ const handleClick = (detail: any) => {
</script>
<style scoped lang='less'>
.search {
:deep(.device-import-product) {
margin-bottom: 0;
padding-right: 0px;
padding-left: 0px;

View File

@ -271,7 +271,7 @@
</page-container>
<Import
v-if="importVisible"
@close="importVisible = false"
@cancel="importVisible = false"
@save="onRefresh"
/>
<Export
@ -308,7 +308,7 @@ import {
} from '@/api/device/instance';
import { getImage, LocalStore } from '@/utils/comm';
import { message } from 'jetlinks-ui-components';
import Import from './Import/index.vue';
import Import from './Import/modal.vue';
import Export from './Export/index.vue';
import Process from './Process/index.vue';
import Save from './Save/index.vue';

View File

@ -129,6 +129,7 @@ import { getImage } from '@/utils/comm';
import { queryList, getAccessConfig } from '@/api/device/product'
import { message } from 'jetlinks-ui-components'
import { useMenuStore } from '@/store/menu';
import { getProductByPluginId } from '@/api/link/plugin'
type Emit = {
(e: 'submit', data: any): void
@ -255,16 +256,25 @@ const findProvidersByProvider = (provider: string) => {
*/
const submitData = async () => {
if (selectedRowKeys.value.length) {
loading.value= true
const resp = await getAccessConfig(props.productId!, checkData.value.id).catch(() => ({ success: false, result: {}}))
//
loading.value = false
if (resp.success) {
// const providers = findProvidersByProvider((resp.result as any)[0]?.provider)
if (checkData.value.channel === 'plugin') {
const resp = await getProductByPluginId(checkData.value.channelId).catch(() => ({ success: false, result: []}))
emit('submit', {
access: {...checkData.value},
metadata: resp.result
productTypes: resp.result
})
} else {
loading.value= true
const resp = await getAccessConfig(props.productId!, checkData.value.id).catch(() => ({ success: false, result: {}}))
//
loading.value = false
if (resp.success) {
// const providers = findProvidersByProvider((resp.result as any)[0]?.provider)
emit('submit', {
access: {...checkData.value},
metadata: resp.result
})
}
}
} else {
message.error('请选择接入方式');

View File

@ -73,6 +73,19 @@
</div>
<div v-else>{{ '暂无连接信息' }}</div>
</div>
<!-- 产品类型 -->
<j-form ref="pluginFormRef" :model="productData" layout="vertical" v-if='productTypes.length'>
<j-form-item name='id' label='产品类型' :rules='[{ required: true, message: "请选择产品类型"}]'>
<j-select
v-model:value='productData.id'
:options='productTypes'
@change='productTypeChange'
placeholder='请选择产品类型'
/>
</j-form-item>
</j-form>
<!-- 其它接入配置 -->
<Title
v-if="metadata?.name"
:data="metadata?.name"
@ -160,6 +173,7 @@
type="primary"
@click="submitDevice"
hasPermission="device/Instance:update"
:loading='submitLoading'
>保存</PermissionButton
>
</j-col>
@ -246,6 +260,15 @@
@cancel=' visible = false'
@submit='checkAccess'
/>
<!-- 物模型处理方式 -->
<MetaDataModal
v-if='metadataVisible'
:metadata='productData.metadata'
:access='access'
:data='metadataModalCacheData'
@cancel=' () => { metadataVisible = false, metadataModalCacheData = {}}'
@submit='MetaDataModalSubmit'
/>
</template>
<script lang="ts" setup name='AccessConfig'>
@ -257,20 +280,20 @@ import { usePermissionStore } from '@/store/permission';
import { steps, steps1 } from './util';
import './index.less';
import {
getProviders,
_deploy,
_undeploy,
queryList,
getConfigView,
getConfigMetadata,
productGuide,
productGuideSave,
getStoragList,
saveDevice,
updateDevice,
detail,
modify,
} from '@/api/device/product';
getProviders,
_deploy,
_undeploy,
queryList,
getConfigView,
getConfigMetadata,
productGuide,
productGuideSave,
getStoragList,
saveDevice,
updateDevice,
detail,
modify, getAccessConfig
} from '@/api/device/product'
import Driver from 'driver.js';
import 'driver.js/dist/driver.min.css';
@ -280,6 +303,9 @@ import { useMenuStore } from '@/store/menu';
import _ from 'lodash';
import { accessConfigTypeFilter } from '@/utils/setting';
import AccessModal from './accessModal.vue'
import MetaDataModal from './metadataModal.vue'
import { getPluginData, getProductByPluginId, savePluginData } from '@/api/link/plugin'
import { detail as queryPluginAccessDetail } from '@/api/link/accessConfig'
const productStore = useProductStore();
const tableRef = ref();
@ -319,6 +345,17 @@ const form = reactive<Record<string, any>>({
const formData = reactive<Record<string, any>>({
data: productStore.current?.configuration || {},
});
//
const productTypes = ref([])
const productData = reactive({
id: undefined,
metadata: {} //
})
const pluginFormRef = ref()
const metadataVisible = ref(false)
const metadataModalCacheData = ref()
const submitLoading = ref(false)
/**
* 显示弹窗
*/
@ -571,11 +608,27 @@ const checkAccess = async (data: any) => {
visible.value = false
accessId.value = data.access.id
access.value = data.access
metadata.value = data.metadata[0]
config.value = data.access?.transportDetail || {}
handleColumns()
markdownToHtml.value = config.value?.document ? marked(config.value.document) : '';
getGuide(!!data.metadata.length); //
productTypes.value = []
productData.id = undefined
productData.metadata = {}
if (data.access.channel === 'plugin') { //
markdownToHtml.value = ''
productTypes.value = data.productTypes.map(item => ({ ...item, label: item.name, value: item.id}))
} else {
metadata.value = data.metadata[0]
handleColumns()
markdownToHtml.value = config.value?.document ? marked(config.value.document) : '';
getGuide(!!data.metadata.length); //
if (data.access?.transportDetail?.metadata) {
productData.metadata = JSON.parse(data.access?.transportDetail?.metadata)
}
}
}
const productTypeChange = (id: string, items: any) => {
productData.metadata = items?.metadata || {}
}
/**
@ -623,20 +676,32 @@ const getData = async (accessId?: string) => {
// if (metadataResp.success) {
// metadata.value = (metadataResp.result?.[0] as ConfigMetadata[]) || [];
// }
queryAccessDetail(_accessId);
queryAccessDetail(_accessId);
if (productStore.current?.accessProvider === 'plugin_gateway') {
queryPluginAccessDetail(_accessId).then(async res => { //
if (res.success) {
const pluginRes = await getPluginData('product', res.result.channelId, productStore.current?.id)
const resp = await getProductByPluginId(res.result.channelId).catch(() => ({ success: false, result: []}))
if (resp.success) {
productTypes.value = resp.result.map(item => {
if (pluginRes?.result?.externalId === item.id) {
productData.id = pluginRes?.result?.externalId
productData.metadata = JSON.stringify(item.metadata || {})
}
return { ...item, label: item.name, value: item.id }
})
}
}
})
} else {
getConfigDetail(
productStore.current?.messageProtocol || '',
productStore.current?.transportProtocol || '',
);
}
}
// else {
// if (productStore.current?.id) {
// getConfigMetadata(productStore.current?.id).then((resp: any) => {
// metadata.value = resp?.result[0] as ConfigMetadata[];
// });
// }
// }
getStoragList().then((resp: any) => {
if (resp.status === 200) {
storageList.value = resp.result;
@ -648,47 +713,80 @@ const getData = async (accessId?: string) => {
* 保存设备接入
*/
const submitDevice = async () => {
const res = await formRef.value.validate();
const values = { storePolicy: form.storePolicy, ...formData.data };
const result: any = {};
flatObj(values, result);
const { storePolicy, ...extra } = result;
const id = productStore.current?.id;
//TODO
// ()
const accessObj = {
...productStore.current,
transportProtocol: access.value?.transport,
protocolName: access.value?.protocolDetail?.name,
accessId: access.value?.id,
accessName: access.value?.name,
accessProvider: access.value?.provider,
messageProtocol: access.value?.protocol,
if (pluginFormRef.value) { //
const pluginRef = await pluginFormRef.value.validate();
if (!pluginRef) return
}
const updateDeviceResp = await updateDevice(accessObj)
if (!updateDeviceResp.success) return
//
const resp = await modify(id || '', {
id: id,
configuration: { ...extra },
storePolicy: storePolicy,
});
if (resp.status === 200) {
message.success('操作成功!');
productStore.current!.storePolicy = storePolicy;
if ((window as any).onTabSaveSuccess) {
if (resp.result) {
(window as any).onTabSaveSuccess(resp);
setTimeout(() => window.close(), 300);
}
} else {
getDetailInfo();
}
const res = await formRef.value.validate();
if (!res) return
const values = { storePolicy: form.storePolicy, ...formData.data };
const id = productStore.current?.id;
//
const _metadata = JSON.parse(productStore.current?.metadata || '{}')
if (_metadata.properties?.length || _metadata.events?.length || _metadata.functions?.length || _metadata.tags?.length) {
metadataModalCacheData.value = {
id,
values,
productTypeId: productData.id
}
metadataVisible.value = true
} else {
updateAccessData(id, values)
}
};
const updateAccessData = async (id: string, values: any) => {
const result: any = {};
flatObj(values, result);
const { storePolicy, ...extra } = result;
// ()
const accessObj = {
...productStore.current,
metadata: JSON.stringify(productData.metadata || "{}"),
transportProtocol: access.value?.transport,
protocolName: access.value?.protocolDetail?.name,
accessId: access.value?.id,
accessName: access.value?.name,
accessProvider: access.value?.provider,
messageProtocol: access.value?.protocol,
}
submitLoading.value = true
const updateDeviceResp = await updateDevice(accessObj).catch(() => { success: false})
if (!updateDeviceResp.success) {
submitLoading.value = false
}
if (access.value?.provider === "plugin_gateway") {
await savePluginData(
'product',
access.value?.channelId,
productStore.current.id,
productData.id
).catch(() => ({}))
}
//
const resp = await modify(id || '', {
id: id,
configuration: { ...extra },
storePolicy: storePolicy,
});
submitLoading.value = false
if (resp.status === 200) {
message.success('操作成功!');
productStore.current!.storePolicy = storePolicy;
if ((window as any).onTabSaveSuccess) {
if (resp.result) {
(window as any).onTabSaveSuccess(resp);
setTimeout(() => window.close(), 300);
}
} else {
getDetailInfo();
}
}
}
const flatObj = (obj: any, result: any) => {
Object.keys(obj).forEach((key: string) => {
if (typeof obj[key] === 'string') {
@ -699,8 +797,15 @@ const flatObj = (obj: any, result: any) => {
});
};
const getDetailInfo = () => {};
const getDetailInfo = async () => {
await productStore.getDetail(productStore.detail.id)
MetaDataModalSubmit()
};
const MetaDataModalSubmit = () => {
//
productStore.tabActiveKey = 'Metadata'
}
getProvidersList()
/**

View File

@ -0,0 +1,259 @@
<template>
<j-modal
title="选择处理方式"
visible
width="900px"
okText="确定"
cancelText="取消"
:confirmLoading='loading'
@ok="submitData"
@cancel="cancel"
>
<div class='tip'>
<a-icon type='ExclamationCircleOutlined'/>
平台
<span style='font-weight: bold;padding:0 4px;'>物模型</span>
中已有数据请选择处理方式
<j-tooltip title='默认采用覆盖的方式处理功能、事件、标签下的数据'>
<a-icon type='QuestionCircleOutlined' />
</j-tooltip>
</div>
<j-form :layout="'vertical'" ref='formRef' :model='handleData'>
<j-form-item label='处理方式' :rules='[{ required: true, message: "请选择处理方式"}]' >
<j-card-select
v-model:value="handleData.type"
:column='4'
:options="options"
>
<template #image='{image}'>
<img :src='image' />
</template>
</j-card-select>
</j-form-item>
</j-form>
</j-modal>
</template>
<script lang='ts' setup name='MetadataModal'>
import { useProductStore } from '@/store/product';
import { getImage } from '@/utils/comm'
import { storeToRefs } from 'pinia'
import { modify, updateDevice } from '@/api/device/product'
import { message } from 'jetlinks-ui-components'
import { savePluginData } from '@/api/link/plugin'
type Emit = {
(e: 'submit'): void
(e: 'cancel'): void
}
const emit = defineEmits<Emit>()
const props = defineProps({
metadata: {
type: Object,
default: () => ({})
},
access: {
type: Object,
default: () => ({})
},
data: {
type: Object,
default: () => ({})
}
})
const productStore = useProductStore();
const { current: productDetail } = storeToRefs(productStore)
const formRef = ref()
const handleData = reactive({
type: undefined
})
const loading = ref(false)
const options = [
{
value: 'intersection',
label: '取交集',
subLabel: '仅保留标识一致的属性',
iconUrl: getImage('/device/intersection.png'),
},
{
value: 'union',
label: '取并集',
subLabel: '保留平台、插件中的所有属性',
iconUrl: getImage('/device/union.png'),
},
{
value: 'ignore',
label: '忽略',
subLabel: '仅保留平台中的属性',
iconUrl: getImage('/device/ignore.png'),
},
{
value: 'cover',
label: '覆盖',
subLabel: '仅保留插件中的属性',
iconUrl: getImage('/device/cover.png'),
}
]
const flatObj = (obj: any, result: any) => {
Object.keys(obj).forEach((key: string) => {
if (typeof obj[key] === 'string') {
result[key] = obj[key];
} else {
flatObj(obj[key], result);
}
});
};
const updateAccessData = async (id: string, values: any, metadata: string) => {
const result: any = {};
flatObj(values, result);
const { storePolicy, ...extra } = result;
// ()
const accessObj = {
...productDetail.value,
metadata: JSON.stringify(metadata),
transportProtocol: props.access?.transport,
protocolName: props.access?.protocolDetail?.name,
accessId: props.access?.id,
accessName: props.access?.name,
accessProvider: props.access?.provider,
messageProtocol: props.access?.protocol,
}
loading.value = true
const updateDeviceResp = await updateDevice(accessObj)
if (!updateDeviceResp.success) {
loading.value = false
return
}
if (props.access?.provider === 'plugin_gateway') {
await savePluginData(
'product',
props.access.channelId,
props.data.id,
props.data.productTypeId
).catch(() => ({}))
}
//
const resp = await modify(id || '', {
id: id,
configuration: { ...extra },
storePolicy: storePolicy,
});
loading.value = false
if (resp.status === 200) {
message.success('操作成功!');
productStore.current!.storePolicy = storePolicy;
if ((window as any).onTabSaveSuccess) {
if (resp.result) {
(window as any).onTabSaveSuccess(resp);
setTimeout(() => window.close(), 300);
}
} else {
await productStore.getDetail(productDetail.value.id)
emit('submit')
}
}
}
const submitData = () => {
formRef.value.validate().then((res) => {
if (res) {
let metadata = JSON.parse(productDetail.value?.metadata || '{}') //
switch (handleData.type![0]) {
case 'intersection': //
metadata.properties = IntersectionFn(metadata.properties, props.metadata.properties)
metadata.events = IntersectionFn(metadata.events, props.metadata.events)
metadata.functions = IntersectionFn(metadata.functions, props.metadata.functions)
metadata.tags = IntersectionFn(metadata.tags, props.metadata.tags)
break;
case 'union': //
metadata.properties = UnionFn(metadata.properties, props.metadata.properties)
metadata.functions = UnionFn(metadata.functions, props.metadata.functions)
metadata.events = UnionFn(metadata.events, props.metadata.events)
metadata.tags = UnionFn(metadata.tags, props.metadata.tags)
break;
case 'cover': //
metadata = props.metadata
break;
default:
break
}
updateAccessData(
props.data.id,
props.data.values,
metadata
)
}
}).catch(() => {
})
}
const cancel = () => {
emit('cancel')
}
/**
* 交集处理函数 只保留来自插件中的属性
* @param DataA 产品物模型
* @param DataB 插件物模型
* @constructor
*/
const IntersectionFn = (DataA: any[] = [], DataB: any[] = []): any[] => {
const newData: any[] = []
if (!DataA.length) return []
DataB.forEach((item) => {
console.log(item, item.id)
if (DataA.some((aItem) => aItem.id === item.id)) {
newData.push(item)
}
})
return newData
}
/**
* 并集函数处理保留平台插件中的所有属性ID重复时只保留来自插件中的1条属性
* @param DataA 产品物模型
* @param DataB 插件物模型
* @constructor
*/
const UnionFn = (DataA: any[] = [], DataB: any[] = []): any[] => {
const dataMap = new Map()
DataB.forEach((item) => {
dataMap.set(item.id, item)
})
DataA.forEach((item) => {
if (!dataMap.has(item.id)) {
dataMap.set(item.id, item)
}
})
console.log(DataA, DataB, [...dataMap.values()])
return [...dataMap.values()]
}
</script>
<style scoped lang='less'>
.tip {
background: #F6F6F6;
color: #999;
padding: 10px 26px;
margin-bottom: 24px;
}
:deep(.j-card-item) {
padding: 16px !important;
}
</style>

View File

@ -0,0 +1,3 @@
import Index from './index.vue'
export default Index

View File

@ -0,0 +1,233 @@
<template>
<div class='metadata-map'>
<div class='left'>
<j-input-search
style='width: 350px;margin-bottom:24px;'
placeholder='搜索平台属性名称'
allowClear
@search='search'
/>
<j-table
:columns="columns"
:data-source="dataSource"
:pagination='false'
:rowSelection='{
selectedRowKeys: selectedKeys,
hideSelectAll: true,
columnWidth: 0
}'
rowKey='id'
>
<template #bodyCell="{ column, text, record, index }">
<template v-if='column.dataIndex === "name"'>
<span class='metadata-title'>{{ text }} ({{ record.id }})</span>
</template>
<template v-if='column.dataIndex === "plugin"'>
<j-select
v-model:value='record.plugin'
style='width: 100%'
@change='(id) => pluginChange(record, id)'
>
<j-select-option
v-for='(item, index) in pluginOptions'
:key='index + "_" + item.id'
:value='item.value'
:disabled='selectedPluginKeys.includes(item.id)'
>{{ item.label }} ({{ item.id }})</j-select-option>
</j-select>
</template>
</template>
</j-table>
</div>
<div class='right'>
<div class='title'>
功能说明
</div>
<p>
该功能用于将插件中的
<b>物模型属性标识</b>
<b>平台物模型属性标识</b>进行映射,当两方属性标识不一致时可在当前页面直接修改映射管理系统将以映射后的物模型属性进行数据处理
</p>
<p>
未完成映射的属性标识目标属性列数据为空代表该属性值来源以在平台配置的来源为准
</p>
<p>
数据条背景亮起代表<b>标识一致</b><b>已完成映射</b>的属性
</p>
<div class='title'>
功能图示
</div>
<div>
<img :src='getImage("/device/matadataMap.png")' />
</div>
</div>
</div>
</template>
<script setup lang='ts' name='MetadataMap'>
import { storeToRefs } from 'pinia'
import { useProductStore } from '@/store/product';
import { detail as queryPluginAccessDetail } from '@/api/link/accessConfig'
import { getPluginData, getProductByPluginId } from '@/api/link/plugin'
import { getImage, onlyMessage } from '@/utils/comm'
import { getMetadateMapById, metadateMapById } from '@/api/device/instance'
const productStore = useProductStore();
const { current: productDetail } = storeToRefs(productStore)
const dataSourceCache = ref([])
const dataSource = ref([])
const pluginOptions = ref<any[]>([])
const tableFilter = (value: string, record: any) => {
console.log(value, record)
return true
}
const columns = [
{
title: '序号',
dataIndex: 'index',
width: 120
},
{
title: '平台属性',
dataIndex: 'name',
},
{
title: '目标属性',
dataIndex: 'plugin',
sorter: tableFilter
}
]
const selectedKeys = computed(() => {
return dataSource.value.filter(item => !!item?.plugin).map(item => item.id)
})
const selectedPluginKeys = computed(() => {
return dataSource.value.filter(item => !!item?.plugin).map(item => item.plugin)
})
const getMetadataMapData = () => {
return new Promise(resolve => {
getMetadateMapById(productDetail.value?.id).then(res => {
if (res.success) {
resolve(res.result?.filter(item => item.customMapping)?.map(item => {
return {
id: item.metadataId,
pluginId: item.originalId
}
}) || [])
}
})
})
}
const search = (value: string) => {
console.log(value)
if (value) {
dataSource.value = dataSourceCache.value.filter((item: any) => {
return !!item.name?.includes(value)
})
} else {
dataSource.value = dataSourceCache.value
}
}
const getDefaultMetadata = async () => {
const metadata = JSON.parse(productDetail.value?.metadata || '{}')
const properties = metadata.properties
const pluginMedata = await getPluginMetadata()
const pluginProperties = pluginMedata?.properties || []
const metadataMap = await getMetadataMapData()
pluginOptions.value = pluginProperties.map(item => ({...item, label: item.name, value: item.id}))
const concatProperties = [ ...pluginProperties.map(item => ({ id: item.id, pluginId: item.id})), ...metadataMap]
dataSource.value = properties?.map((item: any, index: number) => {
const _m = concatProperties.find(p => p.id === item.id)
return {
index: index + 1,
id: item.id, // id
name: item.name,
type: item.valueType?.type,
plugin: _m?.pluginId, // id
}
})
dataSourceCache.value = dataSource.value
}
const getPluginMetadata = (): Promise<{ properties: any[]}> => {
return new Promise(resolve => {
queryPluginAccessDetail(productDetail.value?.accessId!).then(async res => {
if (res.success) {
const _channelId = (res.result as any)!.channelId
const pluginRes = await getPluginData('product', _channelId, productDetail.value?.id).catch(() => ({ success: false, result: {}}))
const resp = await getProductByPluginId(_channelId).catch(() => ({ success: false, result: []}))
if (resp.success) {
const _item = (resp.result as any[])?.find((item: any) => item.id === (pluginRes?.result as any)?.externalId)
resolve(_item ? _item.metadata : { properties: [] })
}
}
resolve({ properties: [] })
})
})
}
const pluginChange = async (value: any, id: string) => {
const res = await metadateMapById(productDetail.value?.id, [{
metadataType: 'property',
metadataId: value.id,
originalId: id
}])
if (res.success) {
onlyMessage('操作成功')
}
}
getDefaultMetadata()
</script>
<style scoped lang='less'>
.metadata-map {
position: relative;
min-height: 100%;
.left {
margin-right: 424px;
}
.right {
position: absolute;
border: 1px solid rgba(0, 0, 0, 0.08);
height: 100%;
width: 400px;
top: 0;
right: 0;
padding: 16px;
.title {
margin-bottom: 16px;
color: rgba(#000, .85);
font-weight: bold;
p {
initial-letter: 28px;
color: #666666;
}
}
}
.metadata-title {
color: #666666;
}
:deep(.ant-table-selection-column) {
padding: 0;
label {
display: none;
}
}
}
</style>

View File

@ -112,6 +112,7 @@ import Info from './BasicInfo/indev.vue';
import Device from './DeviceAccess/index.vue';
import Metadata from '../../../device/components/Metadata/index.vue';
import DataAnalysis from './DataAnalysis/index.vue';
import MetadataMap from './MetadataMap'
// import Metadata from '../../../components/Metadata/index.vue';
import {
_deploy,
@ -163,6 +164,7 @@ const tabs = {
Metadata,
Device,
DataAnalysis,
MetadataMap
};
watch(
@ -280,6 +282,9 @@ const getProtocol = async () => {
];
}
}
if (productStore.current?.accessProvider === 'plugin_gateway') {
list.value.push({ key: 'MetadataMap', tab: '物模型映射'})
}
}
};
/**

View File

@ -0,0 +1,2 @@
import InklingDevice from './index.vue'
export default InklingDevice

View File

@ -0,0 +1,305 @@
<template>
<div class='inkling-device'>
<j-spin :spinning='spinning'>
<div class='search-box'>
<div class='search-warp'>
<j-advanced-search
v-if='!spinning'
:columns='columns'
type='simple'
@search='handleSearch'
class='device-inkling'
target='device-inkling'
/>
</div>
<div class='multiple' v-if='multiple'>
<j-checkbox @change='checkChange'>全选</j-checkbox>
</div>
</div>
<div class='device-list-warp'>
<j-scrollbar v-if='deviceList.length'>
<j-spin :spinning='deviceSpinning'>
<div class='device-list-items'>
<div
v-for='item in deviceList'
:class='{
"device-list-item": true,
"active": checkKeys.includes(item.id),
"disabled": disabledKeys.includes(item.id)
}'
@click='() => deviceClick(item.id, item)'
>
<template v-if='disabledKeys.includes(item.id)'>
<j-tooltip
title='该设备已绑定平台设备'
>
<span class='item-title'>{{ item.id }}</span>
</j-tooltip>
</template>
<span v-else class='item-title'>
{{ item.id }}
</span>
<a-icon
v-if='checkKeys.includes(item.id)'
type='CheckOutlined'
/>
</div>
</div>
</j-spin>
</j-scrollbar>
<j-empty
v-else
description='暂无数据'
style='padding-top: 24px'
/>
<div class='device-list-pagination'>
<j-pagination
v-if='showPage'
:total='pageData.total'
:current='pageData.pageIndex + 1'
:pageSize='pageData.pageSize'
:show-total='() => {
const minSize = pageData.pageIndex * pageData.pageSize + 1;
const MaxSize = (pageData.pageIndex + 1) * pageData.pageSize;
return `${minSize} - ${MaxSize > pageData.total ? pageData.total : MaxSize } 条/总共 ${pageData.total}`;
}'
@change='pageChange'
/>
</div>
</div>
</j-spin>
</div>
</template>
<script setup lang='ts' name='InklingDevice'>
import { getCommandsByAccess, getCommandsDevicesByAccessId } from '@/api/link/accessConfig'
import { getInkingDevices } from '@/api/device/instance'
import { isArray } from 'lodash-es'
type Emit = {
(e: 'update:value', data: string | string[]): void
(e: 'change', data: any | any[]): void
}
const props = defineProps({
value: {
type: [String, Array],
default: undefined
},
accessId: {
type: String,
default: undefined
},
multiple: {
type: Boolean,
default: false
}
})
const emit = defineEmits<Emit>()
const spinning = ref(true)
const deviceSpinning = ref(false)
const deviceList = ref([])
const disabledKeys = ref<string[]>([])
const checkKeys = ref<string[]>([])
const checkCache = ref<Map<string, any>>(new Map())
const showPage = ref(false)
const pageData = reactive({
pageSize: 10,
pageIndex: 0,
total: 0
})
const params = ref({
terms: []
})
const columns = ref([])
const queryInkingDevices = (data: string[]) => {
return new Promise(async (resolve) => {
if (!data.length) {
resolve(true)
return
}
const res = await getInkingDevices(data)
if (res) {
disabledKeys.value = res.result?.map(item => item.externalId)
}
resolve(true)
})
}
const getDeviceList = async () => {
const resp = await getCommandsDevicesByAccessId(props.accessId!, {
pageIndex: pageData.pageIndex,
pageSize: pageData.pageSize,
terms: params.value.terms
}).catch(() => ({ success: false }))
if (resp.success) {
await queryInkingDevices(resp.result?.data.map(item => item.id) || [])
deviceList.value = resp.result?.data || []
pageData.total = resp.result?.total || 0
}
}
const checkChange = (e: any) => { //
if (e.target.checked) {
const keys = deviceList.value.filter(item => {
//
const type = !checkKeys.value.includes(item.id) && !disabledKeys.value.includes(item.id)
if (type && checkCache.value.has(item.id)) {
checkCache.value.set(item.id, item)
}
return type
}).map(item => item.id)
checkKeys.value = [...checkKeys.value, ...keys]
emit('update:value', checkKeys.value)
emit('change', [...checkCache.value.values()])
} else {
checkCache.value.clear()
checkKeys.value = []
emit('update:value', [])
emit('change', [])
}
}
const handleSearch = (p: any) => { //
pageData.pageIndex = 0
params.value = p
getDeviceList()
}
const pageChange = (page: number, pageSize: number) => { //
pageData.pageSize = pageSize
pageData.pageIndex = page - 1
getDeviceList()
}
const init = async () => {
if (props.accessId) {
const resp = await getCommandsByAccess(props.accessId)
if (resp.success) {
const item = resp.result?.[0]
if (item) {
showPage.value = item.id === 'QueryDevicePage' //
columns.value = item.expands?.terms?.map(t => ({
title: t.name,
dataIndex: t.id,
search: {
type: t.valueType.type
}
}))
}
}
spinning.value = false
await getDeviceList()
}
}
const deviceClick = (id: string, option: any) => {
if (option.disabled || disabledKeys.value.includes(id)) return
const _check = new Set(checkKeys.value)
if (props.multiple) { //
if (_check.has(id)) {
_check.delete(id)
checkCache.value.delete(id)
} else {
checkCache.value.set(id, option)
_check.add(id)
}
checkKeys.value = [..._check.values()]
emit('update:value', checkKeys.value)
emit('change', [...checkCache.value.values()])
} else {
checkKeys.value = [id]
emit('update:value', id)
emit('change', option)
}
}
watch(() => props.value, (newValue) => {
if (!newValue) {
checkKeys.value = []
return
}
if (isArray(newValue)) {
checkKeys.value = newValue
} else {
checkKeys.value = [newValue as string]
}
}, { immediate: true, deep: true })
onMounted(() => {
init()
})
</script>
<style scoped lang='less'>
.inkling-device {
min-height: 200px;
}
.search-box {
padding-bottom: 24px;
border-bottom: 1px solid #f0f0f0;
display: flex;
margin-bottom: 12px;
gap: 24px;
align-items: center;
:deep(.device-inkling) {
padding: 0;
margin: 0;
padding-bottom: 0;
}
.search-warp {
flex: 1 1 auto;
}
.multiple {
width: 60px;
}
}
.device-list-warp {
.device-list-items {
.device-list-item {
padding: 10px 16px;
color: #4F4F4F;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
> .item-title {
flex: 1 1 auto;
}
&:hover {
background-color: rgba(47, 84, 235, 0.06);
}
&.active {
background-color: rgba(153, 153, 153, 0.06);
color: @primary-color;
}
&.disabled {
cursor: not-allowed;
background-color: rgba(153, 153, 153, 0.06);
}
}
}
.device-list-pagination {
margin-top: 24px;
text-align: right;
}
}
</style>

View File

@ -4,7 +4,7 @@
<j-input-search v-model:value="searchValue" placeholder="请输入名称" @search="handleSearch" allowClear></j-input-search>
</div>
<div>
<PermissionButton type="primary" :uhas-permission="`${permission}:update`" key="add" @click="handleAddClick"
<PermissionButton type="primary" :hasPermission="`${permission}:update`" key="add" @click="handleAddClick"
:disabled="operateLimits('add', type)" :tooltip="{
title: operateLimits('add', type) ? '当前的存储方式不支持新增' : '新增',
}">

View File

@ -4,7 +4,7 @@
<j-tooltip :title="instanceStore.detail?.independentMetadata && type === 'device'
? '该设备已脱离产品物模型,修改产品物模型对该设备无影响'
: '设备会默认继承产品的物模型,修改设备物模型后将脱离产品物模型'">
<div class="ellipsis">
<div class="ellipsis" style='color: #999;'>
<AIcon type="InfoCircleOutlined" style="margin-right: 3px" />
{{
instanceStore.detail?.independentMetadata && type === 'device'

View File

@ -12,7 +12,7 @@
<div class="box-item">
<div class="label">设备数量</div>
<div class="value">{{ deviceNum }}</div>
<img src="/images/home/Group3793.png" alt="" />
<img src="/images/home/top-1.png" alt="" />
</div>
</div>
</div>

View File

@ -469,7 +469,7 @@ export default {
id: '1-4-9',
parentId: '1-4',
path: 'T4zX-A0TC-BFum',
sortIndex: 9999,
sortIndex: 9998,
level: 1,
name: '远程升级',
code: 'device/Firmware',
@ -493,6 +493,34 @@ export default {
accessDescription: '此菜单不支持数据权限控制',
granted: true,
},
{
id: '1-4-10',
parentId: '1-4',
path: 'T4zX-A0TC-BFum',
sortIndex: 9999,
level: 1,
name: '插件管理',
code: 'link/plugin',
icon: 'BoxPlotOutlined',
url: '/iot/link/plugin',
buttons: [
{ id: 'view', name: '查看', enabled: true, granted: true },
{ id: 'update', name: '编辑', enabled: true, granted: true },
{ id: 'delete', name: '删除', enabled: true, granted: true },
{
id: 'add',
name: '新增',
enabled: true,
granted: true,
},
],
accessSupport: { text: '不支持', value: 'unsupported' },
assetAccesses: [],
options: {},
createTime: 1659344075524,
accessDescription: '此菜单不支持数据权限控制',
granted: true,
},
],
[ROLEKEYS.complex]: [
{

View File

@ -1436,6 +1436,63 @@ export default [
supportDataAccess: false,
indirectMenus: ['8ddbb67de5f65514105d47b448bfd70e']
},
{
code: 'link/plugin',
name: '插件管理',
owner: 'iot',
//parentId: '1-4',
id: 'a20354876e9519e48f5ed6710ba6efb3',
sortIndex: 10,
url: '/iot/link/plugin',
icon: 'BoxPlotOutlined',
showPage: ['plugin-driver'],
permissions: [],
buttons: [
{
id: 'view',
name: '查看',
permissions: [
{
permission: 'plugin-driver',
actions: ['save'],
},
],
},
{
id: 'update',
name: '编辑',
permissions: [
{
permission: 'plugin-driver',
actions: ['save'],
},
],
},
{
id: 'delete',
name: '删除',
permissions: [
{
permission: 'plugin-driver',
actions: ['delete'],
},
],
},
{
id: 'add',
name: '新增',
permissions: [
{
permission: 'plugin-driver',
actions: ['save'],
},
],
},
],
accessSupport: { text: "不支持", value: "unsupported" },
supportDataAccess: false,
},
],
},
{

View File

@ -135,11 +135,11 @@ const submitData = async () => {
const judgeInitSet = async () => {
const resp: any = await getInit();
if (resp.status === 200 && resp.result.length) {
window.location.href = '/';
// window.location.href = '/';
}
};
onBeforeMount(() => {
// judgeInitSet();
judgeInitSet();
});
</script>
<style scoped lang="less">

View File

@ -287,14 +287,8 @@
</j-row>
</div>
<div :class="current !== 2 ? 'steps-action' : 'steps-action-save'">
<j-button
v-if="[0, 1].includes(current)"
type="primary"
style="margin-right: 8px"
@click="next"
>
下一步
</j-button>
<j-button v-if="current > 0" @click="prev" style="margin-right: 8px"> 上一步 </j-button>
<PermissionButton
v-if="current === 2 && view === 'false'"
type="primary"
@ -306,7 +300,14 @@
>
保存
</PermissionButton>
<j-button v-if="current > 0" @click="prev"> 上一步 </j-button>
<j-button
v-if="[0, 1].includes(current)"
type="primary"
@click="next"
>
下一步
</j-button>
</div>
</div>
</template>

View File

@ -372,14 +372,7 @@
</j-row>
</div>
<div :class="current !== 2 ? 'steps-action' : 'steps-action-save'">
<j-button
v-if="[0, 1].includes(current)"
type="primary"
style="margin-right: 8px"
@click="next"
>
下一步
</j-button>
<j-button v-if="current > 0" @click="prev" style="margin-right: 8px"> 上一步 </j-button>
<PermissionButton
style="margin-right: 8px"
v-if="current === 2 && view === 'false'"
@ -391,7 +384,15 @@
>
保存
</PermissionButton>
<j-button v-if="current > 0" @click="prev"> 上一步 </j-button>
<j-button
v-if="[0, 1].includes(current)"
type="primary"
@click="next"
>
下一步
</j-button>
</div>
</div>
</template>

View File

@ -178,13 +178,7 @@
v-if="channel !== 'edge-child-device'"
:class="current !== 1 ? 'steps-action' : 'steps-action-save'"
>
<j-button
v-if="[0].includes(current)"
style="margin-right: 8px"
@click="next"
>
下一步
</j-button>
<j-button v-if="current > 0" @click="prev" style="margin-right: 8px"> 上一步 </j-button>
<PermissionButton
v-if="current === 1 && view === 'false'"
type="primary"
@ -196,7 +190,13 @@
>
保存
</PermissionButton>
<j-button v-if="current > 0" @click="prev"> 上一步 </j-button>
<j-button
v-if="[0].includes(current)"
@click="next"
>
下一步
</j-button>
</div>
</div>
</template>

View File

@ -279,12 +279,11 @@
</div>
<div class="steps-action">
<j-button
v-if="[0, 1].includes(current)"
type="primary"
style="margin-right: 8px"
@click="next"
v-if="type === 'child-device' ? current > 1 : current > 0"
style="margin-right: 8px"
@click="prev"
>
一步
一步
</j-button>
<PermissionButton
v-if="current === 2 && view === 'false'"
@ -299,10 +298,11 @@
保存
</PermissionButton>
<j-button
v-if="type === 'child-device' ? current > 1 : current > 0"
@click="prev"
v-if="[0, 1].includes(current)"
type="primary"
@click="next"
>
一步
一步
</j-button>
</div>
</div>

View File

@ -90,7 +90,6 @@
{
max: 64,
message: '最多可输入64个字符',
trigger: 'blur',
},
]"
name='name'
@ -135,13 +134,13 @@
</div>
<div class="steps-action">
<j-button
v-if="current === 0"
type="primary"
v-if="current > 0"
@click="prev"
style="margin-right: 8px"
@click="next"
>
一步
一步
</j-button>
<PermissionButton
v-if="current === 1 && view === 'false'"
type="primary"
@ -155,10 +154,12 @@
保存
</PermissionButton>
<j-button
v-if="current > 0"
@click="prev"
v-if="current === 0"
type="primary"
@click="next"
>
上一步
一步
</j-button>
</div>
</div>
@ -325,10 +326,10 @@ const saveData = () => {
loading.value = true
const resp =
paramsId === ':id'
? await save(params)
: await update({ ...params, id: paramsId });
? await save(params).catch(() => { success: false})
: await update({ ...params, id: paramsId }).catch(() => { success: false});
loading.value = false
if (resp.status === 200) {
if (resp.success) {
onlyMessage('操作成功', 'success');
history.back();
if ((window as any).onTabSaveSuccess) {

View File

@ -41,7 +41,7 @@
name='version'
:rules='[{ required: true, message: "请上传文件" }]'
>
<UploadFile v-model:modelValue='modelRef.version' @change='uploadChange' />
<UploadFile v-model:modelValue='modelRef.version' @change='uploadChange' :fileName='data.filename' />
</j-form-item>
<div v-if='modelRef.version' class='file-detail'>
<div>
@ -74,7 +74,7 @@
import { ID_Rule, Max_Length_64, Max_Length_200, RequiredStringFn } from '@/components/Form/rules'
import UploadFile from './UploadFile.vue'
import { FileUploadResult } from '@/views/link/plugin/typings'
import { add, vailIdFn } from '@/api/link/plugin'
import { add, update, vailIdFn } from '@/api/link/plugin'
import { message } from 'jetlinks-ui-components'
import { TypeMap } from './util'
@ -92,7 +92,7 @@ const fileType = ref(props.data.type)
const loading = ref(false)
const vailId = async (_: any, value: string) => {
if (!!props.data.id && value) { //
if (!props.data.id && value) { //
const resp = await vailIdFn(value)
if (resp.success && resp.result) {
return Promise.reject('ID重复');
@ -135,7 +135,7 @@ const handleSave = async () => {
const data = await formRef.value.validate()
if (data) {
loading.value = true
const resp = await add(modelRef).catch(() => { success: false })
const resp = props.data.id ? await update(modelRef).catch(() => { success: false }) : await add(modelRef).catch(() => { success: false })
loading.value = false
if (resp.success) {
message.success('操作成功!');

View File

@ -11,6 +11,8 @@
:before-upload="beforeUpload"
:disabled='loading'
:maxCount='1'
:fileList='list'
@remove='remove'
>
<div>
<j-button>上传文件</j-button>
@ -39,6 +41,10 @@ const props = defineProps({
disabled: {
type: Boolean,
default: false
},
fileName: {
type: String,
default: undefined
}
});
@ -47,9 +53,17 @@ const paths: string = useSystem().$state.configInfo.paths?.[
] as string;
const value = ref(props.modelValue);
const list = ref<any>(props.fileName ? [{ name: props.fileName}] : [])
const loading = ref(false);
const remove = () => {
list.value = []
emit('update:modelValue', '');
emit('change', {});
}
const beforeUpload: UploadProps['beforeUpload'] = (file, fl) => {
list.value = fl
const arr = file.name.split('.');
const isFile = ['jar', 'zip'].includes(arr[arr.length - 1]); // file.type === 'application/zip' || file.type === 'application/javj-archive'
if (!isFile) {
@ -62,7 +76,6 @@ const handleChange = async (info: UploadChangeParam) => {
loading.value = true;
if (info.file.status === 'done') {
loading.value = false;
console.log(info.file)
const result = info.file.response?.result;
const f = result.accessUrl;
onlyMessage('上传成功!', 'success');

View File

@ -76,8 +76,9 @@ const props = defineProps({
align-items: flex-end;
justify-content: flex-end;
img {
width: 100%;
width: 100px;
height: 100%;
transform: translateY(2px);
}
}
}

View File

@ -15,7 +15,7 @@
<div class="box-item">
<div class="label">通道数量</div>
<div class="value">{{ channelCount }}</div>
<img :src="getImage('/home/top-2.png')" alt="" />
<img :src="getImage('/home/product.png')" alt="" />
</div>
</div>
</div>

View File

@ -1219,8 +1219,13 @@ const handleSubmit = () => {
delete formData.value.template.ttsmessage;
}
if (formData.value.provider === 'dingTalkRobotWebHook' && formData.value.template?.messageType === 'text') {
formData.value.template.text!.content = formData.value.template.message as string
if (formData.value.provider === 'dingTalkRobotWebHook') {
if (formData.value.template?.messageType === 'text') {
formData.value.template.text!.content = formData.value.template.message as string
}
if (formData.value.template.messageType === 'markdown') {
formData.value.template.markdown!.text = formData.value.template.message
}
}
formRef.value?.validate()

View File

@ -65,6 +65,7 @@ const columns = [
dataIndex: 'handleTime',
key: 'handleTime',
scopedSlots: true,
width: 180,
search: {
type: 'date',
},
@ -74,6 +75,7 @@ const columns = [
title: '处理类型',
key: 'handleType',
scopedSlots: true,
width: 120,
search: {
type: 'select',
options: [
@ -96,11 +98,13 @@ const columns = [
search: {
type: 'date',
},
width:180,
},
{
title: '告警处理',
dataIndex: 'description',
key: 'description',
ellipsis: true,
search: {
type: 'string',
},

View File

@ -120,7 +120,7 @@ const jumpDetail = (item:any) =>{
}
}
.new-alarm-item-level {
width: 52px;
width: 70px;
padding: 2px 8px;
color: #fff;
text-align: center;

View File

@ -78,8 +78,8 @@ const props = defineProps({
align-items: flex-end;
justify-content: flex-end;
img {
width: 100%;
height: 100%;
width: 100px;
height: 100px;
}
}
.content-right-echart{

View File

@ -52,7 +52,7 @@
</template>
<script lang="ts" setup>
import { cloneDeep } from 'lodash-es';
import { cloneDeep, isObject } from 'lodash-es'
import ParamsDropdown from '../../../components/ParamsDropdown';
import { handleParamsData } from './index';
const props = defineProps({
@ -172,6 +172,7 @@ const onChange = () => {
};
const onValueChange = (val: any, label: string) => {
const optionColumn = isObject(val) && (val as any).metadata ? [(val as any).column] : []
const obj = {
[`${propertyModelRef.properties}`]: {
value: propertyModelRef?.propertiesValue,
@ -179,7 +180,7 @@ const onValueChange = (val: any, label: string) => {
},
};
emit('update:value', obj);
emit('change', label || val)
emit('change', label || val, optionColumn)
};
watch(

View File

@ -298,7 +298,7 @@ watch(
{ immediate: true },
);
const onWriteChange = (val: string) => {
const onWriteChange = (val: string, optionColumn: string[]) => {
modelRef.propertiesValue = val;
emit('change', {
propertiesName:
@ -306,7 +306,7 @@ const onWriteChange = (val: string) => {
? _function.value?.name
: _property.value?.name,
propertiesValue: modelRef.propertiesValue,
});
}, optionColumn);
};
const onFormSave = () => {

View File

@ -102,6 +102,10 @@ const props = defineProps({
parallel: {
type: Boolean,
},
options: {
type: Object,
default: () => ({})
}
});
const current = ref<number>(0);
@ -125,6 +129,7 @@ const DeviceModel = reactive<DeviceModelType>({
const DeviceOptions = ref<DeviceOptionType>({});
const emit = defineEmits<Emit>();
const optionColumnCache = ref<string[]>(props.options?.otherColumn || [])
const onCancel = () => {
emit('cancel');
@ -150,6 +155,7 @@ const onSave = (_data: any) => {
selector: DeviceModel.selector, //
triggerName: data.value.options?.trigger?.name || '触发设备',
...DeviceOptions.value,
otherColumns: []
};
const _type = _data.message.messageType;
if (_type === 'INVOKE_FUNCTION') {
@ -164,8 +170,9 @@ const onSave = (_data: any) => {
(typeof _options?.propertiesValue === 'object'
? JSON.stringify(_options?.propertiesValue)
: _options?.propertiesValue)
_options.otherColumns = optionColumnCache.value
}
console.log(item)
emit('save', item, JSON.parse(JSON.stringify(_options)));
};
@ -195,7 +202,8 @@ const onDeviceSave = (_data: any, obj?: any) => {
DeviceOptions.value = { ...unref(DeviceOptions), ...obj };
};
const onActionsChange = (options?: any) => {
const onActionsChange = (options?: any, optionColumn: string[]) => {
optionColumnCache.value = optionColumn
const obj = {
...DeviceOptions.value,
...options,

View File

@ -368,6 +368,7 @@
:branchGroup="thenName"
:branchesName="branchesName"
:data="data"
:options='_data.branches[branchesName].then[thenName].actions[name].options'
@cancel="onClose"
@save="onSave"
/>
@ -377,6 +378,7 @@
v-bind="props"
v-if="!!actionType"
:actionType="actionType"
:options='_data.branches[branchesName].then[thenName].actions[name].options'
@save="onPropsOk"
@cancel="onPropsCancel"
/>
@ -523,12 +525,17 @@ const onType = (_type: string) => {
const onSave = (data: ActionsType, options: any) => {
const { key, terms } = _data.value.branches![props.branchesName].then?.[props.thenName].actions?.[props.name]
console.log({...props.options, ...options})
const columns = new Set([...(props.options?.termsColumns || []), ...(options.otherColumns.filter((item?: string) => item))])
const actionItem: ActionsType = {
...data,
options: {...props.options, ...options},
options: {...props.options, ...options, columns: [...columns.values()]},
key,
terms
}
console.log(actionItem)
_data.value.branches![props.branchesName].then[props.thenName].actions.splice(props.name, 1, actionItem)
visible.value = false;

View File

@ -82,7 +82,10 @@ const onSave = (data: any, options?: any) => {
const item: ActionsType = {
...extra,
key: data.key,
options,
options: {
...options,
columns: options.otherColumns.filter((item?: string) => item)
},
};
emit('add', item)
visible.value = false

View File

@ -1,7 +1,7 @@
<template>
<div>
<template v-if="actionType === 'device'">
<Device v-bind="props" :value="data?.device" @cancel="onCancel" @save="onPropsOk" />
<Device v-bind="props" :value="data?.device" :options='options' @cancel="onCancel" @save="onPropsOk" />
</template>
<template v-else-if="actionType === 'notify'">
<Notify :options="data?.options" :value="data?.notify" @cancel="onCancel" @save="onPropsOk" />
@ -42,6 +42,10 @@ const props = defineProps({
type: String,
default: '',
},
options: {
type: Object,
default: () => ({})
}
});
const emit = defineEmits(['cancel', 'save']);

View File

@ -24,6 +24,7 @@
v-bind="props"
v-if="!!actionType"
:actionType="actionType"
:options='actionOptions'
@save="onPropsOk"
@cancel="onPropsCancel"
/>
@ -64,6 +65,10 @@ const props = defineProps({
parallel: {
type: Boolean,
},
actionOptions: {
type: Object,
default: () => ({})
}
});
const emit = defineEmits(['cancel', 'save']);

View File

@ -8,13 +8,13 @@
<j-form-item
:name="`${item?.id}`"
:label="item?.name"
v-for="item in variableDefinitions"
v-for="(item, index) in variableDefinitions"
:key="item.id"
:required="getType(item) !== 'file' ? true : false"
:rules="[
{
validator: (_rule, value) => checkValue(_rule, value, item),
trigger: ['change', 'blur'],
trigger: ['blur', 'change'],
},
]"
>
@ -22,19 +22,19 @@
:notify="notify"
v-if="getType(item) === 'user'"
v-model:value="modelRef[item.id]"
@change="(val) => onChange(val, 'user')"
@change="(val) => onChange(val, 'user', index)"
/>
<Org
:notify="notify"
v-else-if="getType(item) === 'org'"
v-model:value="modelRef[item.id]"
@change="(val) => onChange(val, 'org')"
@change="(val) => onChange(val, 'org', index)"
/>
<Tag
:notify="notify"
v-else-if="getType(item) === 'tag'"
v-model:value="modelRef[item.id]"
@change="(val) => onChange(val, 'tag')"
@change="(val) => onChange(val, 'tag', index)"
/>
<InputFile
v-else-if="getType(item) === 'file'"
@ -48,7 +48,7 @@
v-else
:item="item"
v-model:value="modelRef[item.id]"
@change="(val) => onChange(val, 'build-in')"
@change="(val, _options) => onChange(val, 'build-in', index, _options)"
/>
</j-form-item>
</j-form>
@ -70,16 +70,20 @@ const props = defineProps({
},
value: {
type: Object,
default: () => {},
default: () => ({}),
},
notify: {
type: Object,
default: () => {},
default: () => ({}),
},
template: {
type: Object,
default: () => {},
default: () => ({}),
},
options: {
type: Object,
default: () => ({})
}
});
const emit = defineEmits(['update:value', 'change']);
@ -87,15 +91,14 @@ const emit = defineEmits(['update:value', 'change']);
const formRef = ref();
const modelRef = reactive({});
const otherColumns = ref<(string | undefined)[]>(props.options?.otherColumns || [])
watchEffect(() => {
Object.assign(modelRef, props?.value);
});
watchEffect(() => {
if(props?.template?.template?.sendTo && props?.template?.template?.sendTo?.length){
emit('change', { sendTo: props?.template?.template?.sendTo.join(' ') });
}
emit('change', { sendTo: props?.template?.template?.sendTo?.join(' ') });
});
const getType = (item: any) => {
@ -184,13 +187,21 @@ const checkValue = (_rule: any, value: any, item: any) => {
return Promise.resolve();
};
const onChange = (val: any, type: any) => {
const onChange = (val: any, type: any, index: number, options?: string) => {
if (type === 'build-in') {
otherColumns.value[index] = options
} else {
otherColumns.value[index] = undefined
}
if (type === 'org') {
emit('change', { orgName: val.join(',') });
emit('change', { orgName: val.join(','), otherColumns: [] });
} else if (type === 'tag') {
emit('change', { tagName: val });
emit('change', { tagName: val, otherColumns: [] });
} else if (type === 'user') {
emit('change', { sendTo: val });
emit('change', { sendTo: val, otherColumns: [] });
} else {
emit('change', { otherColumns: otherColumns.value });
}
};

View File

@ -60,6 +60,7 @@
:value="formModel.variables"
:notify="formModel"
:template="template"
:options='options'
@change="(val) => onValChange(val, 'variables')"
ref="variableRef"
/>
@ -82,7 +83,7 @@
</j-modal>
</template>
<script lang="ts" setup>
<script lang="ts" setup name='NotifyIndex'>
import NotifyWay from './NotifyWay.vue';
import NotifyConfig from './NotifyConfig.vue';
import NotifyTemplate from './NotifyTemplate.vue';
@ -99,7 +100,7 @@ const props = defineProps({
},
options: {
type: Object,
default: () => {},
default: () => ({}),
},
name: {
type: Number,

View File

@ -16,7 +16,7 @@
placeholder="请选择参数"
style="width: calc(100% - 120px)"
:fieldNames="{ label: 'name', value: 'id' }"
@change="(val, label) => itemOnChange(undefined, val, label)"
@change="(val, label, extra) => itemOnChange(undefined, val, label, extra)"
>
<template #title="{ fullName, description }">
<j-space>
@ -57,7 +57,7 @@
</j-input-group>
</template>
<script lang="ts" setup>
<script lang="ts" setup name='NotifyBuildIn'>
import { queryBuiltInParams } from '@/api/rule-engine/scene';
import { useSceneStore } from '@/store/scene';
import { storeToRefs } from 'pinia';
@ -103,15 +103,22 @@ const sourceChange = (val: any) => {
});
};
const itemOnChange = (val: any, _upperKey?: string, label?: any) => {
const itemOnChange = (val: any, _upperKey?: string, label?: any, extra?: any) => {
const item = extra?.triggerNode?.props
let othersColumns = ''
if (item && item.metadata) {
othersColumns = item.column
}
emit('update:value', {
...props.value,
value: val,
upperKey: _upperKey,
});
emit('change', {
sendTo: label?.[0] || val,
});
}, othersColumns);
};
const treeDataFilter = (arr: any[], type: string) => {

View File

@ -65,8 +65,8 @@
v-else
style="width: calc(100% - 120px)"
placeholder="请选择收信人"
@select="
(key, node) => onChange(source, key, node?.isRelation, node?.name)
@change="
(key, label) => onChange(source, key, undefined, label)
"
:tree-data="treeData"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
@ -209,6 +209,7 @@ const treeData = ref<any[]>([
]);
const mySource = ref<string>('relation');
const labelMap = new Map();
const treeDataMap = new Map()
const getRelationUsers = async (notifyType: string, notifierId: string) => {
let resp = undefined;
@ -250,6 +251,7 @@ const getUser = async (_source: string, triggerType: string) => {
}
if (platformResp.status === 200) {
newTree[0].children = platformResp.result.map((item: any) => {
treeDataMap.set(item.id, item)
return {
...item,
value: item.id,
@ -265,6 +267,7 @@ const getUser = async (_source: string, triggerType: string) => {
key: 'p2',
selectable: false,
children: relationResp.result.map((item: any) => {
treeDataMap.set(item.id, item)
return {
...item,
value: item.id,
@ -324,6 +327,7 @@ const onChange = (
_name?: string,
) => {
let _values: any = undefined;
const _names: string[] = Array.isArray(_name) ? _name : [_name || ''];
if (Array.isArray(_value)) {
if (props?.notify?.notifyType === 'email') {
@ -339,7 +343,9 @@ const onChange = (
}
}
} else {
_values = getObj(_source, _value, isRelation);
const item = treeDataMap.get(_value)
const _isRelation = item.isRelation
_values = getObj(_source, _value, _isRelation);
}
emit('update:value', _values);
emit('change', _names.filter((item) => !!item).join(','));

View File

@ -10,6 +10,7 @@
:getPopupContainer='getPopupContainer'
popupClassName='manual-time-picker-popup'
@change='change'
@ok='change'
/>
<j-date-picker
v-else
@ -21,6 +22,7 @@
:getPopupContainer='getPopupContainer'
popupClassName='manual-time-picker-popup'
@change='change'
@ok='change'
/>
</div>
</template>
@ -58,6 +60,7 @@ const getPopupContainer = (trigger: HTMLElement) => {
}
const change = (e: string) => {
console.log('Time',e)
myValue.value = e
emit('update:value', e)
emit('change', e)

View File

@ -163,7 +163,6 @@ watchEffect(() => {
const option = getOption(_options, props.value as string, props.valueName) // label
myValue.value = props.value
mySource.value = props.source
console.log(_options, props.value, props.valueName, option)
if (option) {
label.value = option[props.labelName] || option.name
treeOpenKeys.value = openKeysByTree(_options, props.value, props.valueName)
@ -176,6 +175,8 @@ watchEffect(() => {
}
})
</script>
<style scoped lang='less'>

View File

@ -141,19 +141,13 @@ const addWhen = () => {
type: 'and',
terms: [
{
terms: [
{
column: undefined,
value: {
source: 'fixed',
value: undefined
},
termType: undefined,
key: `params_${randomString()}`,
type: 'and',
}
],
key: `terms_2_${randomString()}`,
column: undefined,
value: {
source: 'fixed',
value: undefined
},
termType: undefined,
key: `params_${randomString()}`,
type: 'and',
}
],

View File

@ -276,16 +276,24 @@ const columnSelect = (option: any) => {
} else if (termTypeChange) {
const oldValue = isArray(paramsValue.value!.value) ? paramsValue.value!.value[0] : paramsValue.value!.value
const value = arrayParamsKey.includes(paramsValue.termType as string) ? [ oldValue, undefined ] : oldValue
paramsValue.value = {
source: paramsValue.value?.source || tabsOptions.value[0].key,
const _source = paramsValue.value?.source || tabsOptions.value[0].key
const newValue: any = {
source: _source,
value: value
}
if (_source === 'metric') {
newValue.metric = paramsValue.value?.metric
}
paramsValue.value = newValue
}
handOptionByColumn(option)
emit('update:value', { ...paramsValue })
formItemContext.onFieldChange()
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[props.name][0] = option.name
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[props.name][1] = paramsValue.termType
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[props.termsName][0] = option.name
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[props.termsName][1] = paramsValue.termType
}
const termsTypeSelect = (e: { key: string, name: string }) => {
@ -304,35 +312,41 @@ const termsTypeSelect = (e: { key: string, name: string }) => {
}
}
paramsValue.value = {
source: paramsValue.value?.source || tabsOptions.value[0].key,
const _source = paramsValue.value?.source || tabsOptions.value[0].key
const newValue: any = {
source: _source,
value: value
}
if (_source === 'metric') {
newValue.metric = paramsValue.value?.metric
}
paramsValue.value = newValue
emit('update:value', { ...paramsValue })
formItemContext.onFieldChange()
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[props.name][1] = e.name
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[props.termsName][1] = e.name
}
const valueSelect = (v: any, label: string, labelObj: Record<number, any>, option: any) => {
if (paramsValue.value?.source === 'metric') {
paramsValue.metric = option?.id
paramsValue.value.metric = option?.id
}
const newValues = { ...paramsValue }
if (paramsValue.value?.source !== 'metric') {
delete newValues.metric
delete newValues.value.metric
}
emit('update:value', { ...newValues })
formItemContext.onFieldChange()
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[props.name][2] = labelObj
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[props.termsName][2] = labelObj
}
const typeSelect = (e: any) => {
emit('update:value', { ...paramsValue })
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[props.name][3] = e.label
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[props.termsName][3] = e.label
}
const termAdd = () => {
@ -346,17 +360,17 @@ const termAdd = () => {
type: 'and',
key: `params_${new Date().getTime()}`
}
formModel.value.branches?.[props.branchName]?.when?.[props.whenName]?.terms?.[props.termsName]?.terms?.push(terms)
formModel.value.branches?.[props.branchName]?.when?.[props.whenName]?.terms?.push(terms)
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[props.termsName].push(['', '', '', '并且'])
}
const onDelete = () => {
formModel.value.branches?.[props.branchName]?.when?.[props.whenName]?.terms?.[props.termsName]?.terms?.splice(props.name, 1)
formModel.value.options!.when[props.branchName].terms[props.whenName].terms.splice(props.name, 1)
formModel.value.branches?.[props.branchName]?.when?.[props.whenName]?.terms?.splice(props.termsName, 1)
formModel.value.options!.when[props.branchName].terms[props.whenName].terms.splice(props.termsName, 1)
}
nextTick(() => {
Object.assign(paramsValue, pick(props.value, ['column', 'options', 'termType', 'terms', 'type', 'value']))
Object.assign(paramsValue, pick(props.value, ['column', 'options', 'termType', 'terms', 'type', 'value', 'metric', 'key']))
})
</script>

View File

@ -3,7 +3,7 @@
<TitleComponent data='触发条件' style='font-size: 14px;' >
<template #extra>
<j-switch
:checked='open'
v-model:checked='open'
@change='change'
checkedChildren='开'
unCheckedChildren='关'
@ -60,17 +60,35 @@ import Action from '../../action/index.vue'
const sceneStore = useSceneStore()
const { data } = storeToRefs(sceneStore)
const open = ref(false)
const open = ref<boolean>(false)
const columnOptions = ref<any>([])
provide(ContextKey, columnOptions)
const change = (e: boolean) => {
open.value = e
if (!e) {
data.value.branches!.length = 1
data.value.branches![0].when = []
} else {
data.value.branches!.push(null as any)
data.value.branches![0].when = [
{
terms: [
{
column: undefined,
value: {
source: 'fixed',
value: undefined
},
termType: undefined,
key: 'params_1',
type: 'and',
},
],
type: 'and',
key: 'terms_1',
},
]
}
}
@ -119,10 +137,15 @@ watchEffect(() => {
})
watchEffect(() => {
open.value = !(
data.value.branches &&
data.value.branches?.length === 1
)
if (data.value.branches?.filter(item => item).length) {
open.value = !!data.value.branches[0].when.length
} else {
open.value = true
}
})
onMounted(() => {
console.log('terms-onMounted')
})
</script>

View File

@ -13,19 +13,15 @@
<AIcon type='CloseOutlined' />
</div>
</j-popconfirm>
<j-form-item
v-for='(item, index) in termsData'
:key='item.key'
:name='["branches", branchName, "when", whenName, "terms", props.name, "terms", index]'
:name='["branches", branchName, "when", whenName, "terms", props.name]'
:rules='rules'
>
<ParamsItem
v-model:value='formModel.branches[branchName].when[whenName].terms[props.name].terms[index]'
:isFirst='index === 0'
:isLast='index === termsData.length - 1'
:showDeleteBtn='termsData.length !== 1'
:name='index'
v-model:value='formModel.branches[branchName].when[whenName].terms[props.name]'
:isFirst='isFirst'
:isLast='isLast'
:showDeleteBtn='showDeleteBtn'
:termsName='name'
:whenName='whenName'
:branchName='branchName'

View File

@ -18,7 +18,7 @@
:branchName='branchName'
:whenName='props.name'
:name='index'
:showDeleteBtn='showDeleteBtn'
:showDeleteBtn='termsData.length > 1'
:isFirst='index === 0'
:isLast='index === termsData.length -1'
:data='item'
@ -93,19 +93,13 @@ const addWhen = () => {
type: 'and',
terms: [
{
terms: [
{
column: undefined,
value: {
source: 'manual',
value: undefined
},
termType: undefined,
key: `params_${randomString()}`,
type: 'and',
}
],
key: `terms_2_${randomString()}`,
column: undefined,
value: {
source: 'manual',
value: undefined
},
termType: undefined,
key: `params_${randomString()}`,
type: 'and',
}
],

View File

@ -389,6 +389,8 @@ const handleView = (id: string, triggerType: string) => {
.subTitle {
position: relative;
margin-top: 18px;
display: inline-block;
height: 50px;
.subTitle-title {
position: absolute;
@ -400,6 +402,7 @@ const handleView = (id: string, triggerType: string) => {
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
text-indent: 38px;
}
}
</style>

View File

@ -3836,10 +3836,10 @@ jetlinks-ui-components@1.0.5:
lodash-es "^4.17.21"
monaco-editor "^0.35.0"
jetlinks-ui-components@^1.0.8:
version "1.0.8"
resolved "https://registry.jetlinks.cn/jetlinks-ui-components/-/jetlinks-ui-components-1.0.8.tgz#bcbdbbdc6c0011207f15cbb51fadcc8389803f53"
integrity sha512-FdXSS4Wdnq5cCUKP5f6Z/3FHu3XHFkRIzSAvkUQdneHbYO6iHkEjMJyHChttlP9cp4s6ydRpeqY2jjtoftYhtA==
jetlinks-ui-components@^1.0.9:
version "1.0.9"
resolved "https://registry.jetlinks.cn/jetlinks-ui-components/-/jetlinks-ui-components-1.0.9.tgz#c71e593e65b9e8dd16c746cebf16d45339a0d340"
integrity sha512-NRKA20IYMvaGabJTnt180ahjL6ERJz8rDohAMtaP4bWQeSAq89hBB5s6XMRJK4VexliEEo4+V3E/edK2iNsGWg==
dependencies:
"@vueuse/core" "^9.12.0"
"@vueuse/router" "^9.13.0"