Merge branch 'dev' into dev-hub

This commit is contained in:
jackhoo_98 2023-03-20 09:27:01 +08:00
commit 865c9e989c
66 changed files with 1303 additions and 1201 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.4",
"jetlinks-ui-components": "^1.0.5",
"js-cookie": "^3.0.1",
"less": "^4.1.3",
"less-loader": "^11.1.0",

View File

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -131,7 +131,7 @@ export const saveProductMetadata = (data: Record<string, unknown>) => server.pat
* @param data
* @returns
*/
export const getDeviceNumber = (params:any) => server.get('/device-instance/_count', params)
export const getDeviceNumber = (params:any) => server.get<number>('/device-instance/_count', params)
/**
*

View File

@ -1,12 +1,12 @@
const color = {
'processing': '64, 169, 255',
'error': '247, 79, 70',
'success': '74, 234, 220',
'warning': '250, 178, 71',
'default': '63, 73, 96'
'processing': '9, 46, 231',
'error': '229, 0, 18',
'success': '36, 178, 118',
'warning': '255, 144, 0',
'default': '102, 102, 102'
}
export const getHexColor = (code: string, pe: number = 0.3) => {
export const getHexColor = (code: string, pe: number = 0.1) => {
const _color = color[code] || color.default
if (code === 'default') {
pe = 0.1

View File

@ -6,6 +6,18 @@
@click="handleClick"
>
<div class="card-content">
<div
class="card-content-bg1"
:style="{
background: getBackgroundColor(statusNames[status]),
}"
></div>
<div
class="card-content-bg2"
:style="{
background: getBackgroundColor(statusNames[status]),
}"
></div>
<div style="display: flex">
<!-- 图片 -->
<div class="card-item-avatar">
@ -17,21 +29,19 @@
<slot name="content"></slot>
</div>
</div>
<!-- 勾选 -->
<div v-if="active" class="checked-icon">
<div>
<AIcon type="CheckOutlined" />
</div>
</div>
<!-- 状态 -->
<div
v-if="showStatus"
class="card-state"
:style='{
backgroundColor: getHexColor(statusNames[status])
}'
:style="{
backgroundColor: getHexColor(statusNames[status]),
}"
>
<div class="card-state-content">
<BadgeStatus
@ -72,7 +82,7 @@
<script setup lang="ts" name='CardBox'>
import BadgeStatus from '@/components/BadgeStatus/index.vue';
import { getHexColor } from '../BadgeStatus/color'
import color, { getHexColor } from '../BadgeStatus/color';
import type { ActionsType } from '@/components/Table';
import { PropType } from 'vue';
@ -123,6 +133,15 @@ const props = defineProps({
},
});
const getBackgroundColor = (code: string) => {
const _color = color[code] || color.default;
return `linear-gradient(
188.4deg,
rgba(${_color}, 0.03) 22.94%,
rgba(${_color}, 0) 94.62%
)`;
};
const handleClick = () => {
emit('click', props.value);
};
@ -257,6 +276,33 @@ const handleClick = () => {
}
}
.card-content-bg1 {
position: absolute;
right: -5%;
height: 100%;
width: 44.65%;
top: 0;
background: linear-gradient(
188.4deg,
rgba(229, 0, 18, 0.03) 22.94%,
rgba(229, 0, 18, 0) 94.62%
);
transform: skewX(-15deg);
}
.card-content-bg2 {
position: absolute;
right: -5%;
height: 100%;
width: calc(44.65% + 34px);
top: 0;
background: linear-gradient(
188.4deg,
rgba(229, 0, 18, 0.03) 22.94%,
rgba(229, 0, 18, 0) 94.62%
);
transform: skewX(-15deg);
}
.card-mask {
position: absolute;
top: 0;
@ -268,7 +314,7 @@ const handleClick = () => {
width: 100%;
height: 100%;
color: #fff;
background-color: rgba(#000, .5);
background-color: rgba(#000, 0.5);
visibility: hidden;
cursor: pointer;
transition: all 0.3s;

View File

@ -5,7 +5,7 @@
<AIcon type="MenuOutlined" class="item-drag item-icon" />
</div>
<div class="item-middle item-editable">
<j-popover :visible="editIndex === index" placement="top">
<j-popover :visible="editIndex === index" placement="left">
<template #title>
<div class="edit-title" style="display: flex; justify-content: space-between; align-items: center;">
<div style="width: 150px;">枚举项配置</div>

View File

@ -83,7 +83,7 @@ const props = defineProps({
type: Object as PropType<PopconfirmProps>,
},
hasPermission: {
type: String || Array,
type: String || Array || Boolean,
},
style: {
type: Object as PropType<CSSProperties>
@ -96,7 +96,7 @@ const props = defineProps({
const permissionStore = usePermissionStore()
const isPermission = computed(() => {
if (!props.hasPermission) {
if (!props.hasPermission || props.hasPermission === true) {
return true
}
return permissionStore.hasPermission(props.hasPermission)

View File

@ -50,36 +50,16 @@ export const AccountMenu = {
export default [
{ path: '/*', redirect: '/'},
// start: 测试用, 可删除
{
path: '/login',
component: () => import('@/views/user/Login/index.vue')
},
{
path: '/demo',
component: () => import('@/views/demo/index.vue')
},
{
path: '/account/center/bind',
component: () => import('@/views/account/Center/bind/index.vue')
},
{
path: '/table',
component: () => import('@/views/demo/table/index.vue')
},
{
path: '/form',
component: () => import('@/views/demo/Form.vue')
},
// {
// path: '/system/Api',
// component: () => import('@/views/system/Platforms/index.vue')
// },
// end: 测试用, 可删除
// 初始化
{
path: '/init-home',
path: '/init-home', // 初始化
component: () => import('@/views/init-home/index.vue')
},

View File

@ -15,19 +15,26 @@ export const useProductStore = defineStore({
this.current = current
this.detail = current
},
async refresh(id: string) {
async getDetail(id: string) {
const resp = await detail(id)
const res = await getDeviceNumber(encodeQuery({ terms: { productId: id } }))
if(resp.status === 200){
this.current = resp.result
this.detail = resp.result
if(res.status === 200){
this.current.count = res.result
}
}
},
async refresh(id: string) {
this.getDetail(id)
const res = await getDeviceNumber(encodeQuery({ terms: { productId: id } }))
if(res.status === 200){
this.current.count = res.result
}
},
setTabActiveKey(key: string) {
this.tabActiveKey = key
},
reSet(){
this.current = {} as ProductItem
this.detail = {} as ProductItem
}
}
})

View File

@ -1,6 +1,6 @@
<template>
<a-result status="404" title="404" sub-title="Sorry, the page you visited does not exist.">
</a-result>
<j-result status="404" title="404">
</j-result>
</template>
<script>

View File

@ -27,11 +27,12 @@
<template #card="slotProps">
<CardBox
:value="slotProps"
@click="handleView(slotProps.id)"
:actions="getActions(slotProps, 'card')"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
enabled: 'success',
enabled: 'processing',
disabled: 'error',
}"
>
@ -39,19 +40,20 @@
<img :src="getImage('/northbound/aliyun.png')" />
</template>
<template #content>
<h3
class="card-item-content-title"
@click.stop="handleView(slotProps.id)"
>
{{ slotProps.name }}
</h3>
<j-row>
<Ellipsis style="width: calc(100% - 100px)">
<span style="font-size: 16px; font-weight: 600">
{{ slotProps.name }}
</span>
</Ellipsis>
<j-row style="margin-top: 15px">
<j-col :span="12">
<div class="card-item-content-text">
网桥产品
</div>
<Ellipsis>
<div>{{ slotProps?.bridgeProductName }}</div>
<div>
{{ slotProps?.bridgeProductName }}
</div>
</Ellipsis>
</j-col>
<j-col :span="12">
@ -85,9 +87,13 @@
</CardBox>
</template>
<template #state="slotProps">
<j-badge
<BadgeStatus
:status="slotProps.state?.value"
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
:statusNames="{
enabled: 'processing',
disabled: 'error',
}"
/>
</template>
<template #action="slotProps">
@ -100,7 +106,7 @@
:disabled="i.disabled"
:popConfirm="i.popConfirm"
:tooltip="i.tooltip"
style="padding: 0px"
style="padding: 0 5px"
@click="i.onClick"
type="link"
:hasPermission="'Northbound/AliCloud:' + i.key"
@ -116,20 +122,17 @@
<script setup lang="ts">
import { query, _undeploy, _deploy, _delete } from '@/api/northbound/alicloud';
import type { ActionsType } from '@/views/device/Instance/typings'
import type { ActionsType } from '@/views/device/Instance/typings';
import { getImage } from '@/utils/comm';
import { message } from 'jetlinks-ui-components';
import { useMenuStore } from 'store/menu';
import BadgeStatus from '@/components/BadgeStatus/index.vue';
const instanceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const menuStory = useMenuStore();
const statusMap = new Map();
statusMap.set('enabled', 'success');
statusMap.set('disabled', 'error');
const columns = [
{
title: '名称',
@ -151,6 +154,7 @@ const columns = [
title: '说明',
dataIndex: 'description',
key: 'description',
ellipsis: true,
search: {
type: 'string',
},
@ -172,7 +176,7 @@ const columns = [
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
width: 200,
scopedSlots: true,
},
];

View File

@ -1,47 +1,71 @@
<template>
<j-table
rowKey="id"
:columns="columns"
:data-source="dataSource"
bordered
:pagination="false"
>
<template #bodyCell="{ column, text, record }">
<div style="width: 280px">
<template v-if="['valueType', 'name'].includes(column.dataIndex)">
<span>{{ text }}</span>
<div class="inputs">
<j-form ref="formRef" :model="modelRef">
<j-table
rowKey="id"
:columns="columns"
:data-source="modelRef.dataSource"
bordered
:pagination="false"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'value'">
<j-form-item
:name="['dataSource', index, 'value']"
:rules="[
{
required: true,
message:
record.type === 'enum' ||
record.type === 'boolean'
? '请选择'
: '请输入',
},
]"
>
<ValueItem
v-model:modelValue="record.value"
:itemType="record.type"
:options="
record.type === 'enum'
? (
record?.dataType?.elements || []
).map((item) => {
return {
label: item.text,
value: item.value,
};
})
: record.type === 'boolean'
? [
{ label: '是', value: true },
{ label: '否', value: false },
]
: undefined
"
@change="onChange"
/>
</j-form-item>
</template>
<template v-else>
<j-form-item
:name="['dataSource', index, column.dataIndex]"
>
<j-input
readonly
:bordered="false"
v-model:value="record[column.dataIndex]"
/>
</j-form-item>
</template>
</template>
<template v-else>
<ValueItem
v-model:modelValue="record.value"
:itemType="record.type"
:options="
record.type === 'enum'
? (record?.dataType?.elements || []).map(
(item) => {
return {
label: item.text,
value: item.value,
};
},
)
: record.type === 'boolean'
? [
{ label: '是', value: true },
{ label: '否', value: false },
]
: undefined
"
/>
</template>
</div>
</template>
</j-table>
</j-table>
</j-form>
</div>
</template>
<script lang="ts" setup>
import { PropType } from "vue";
import { PropType } from 'vue';
type Emits = {
(e: 'update:modelValue', data: Record<string, any>[]): void;
@ -52,9 +76,14 @@ const _props = defineProps({
modelValue: {
type: Array as PropType<Record<string, any>[]>,
default: '',
}
},
});
const columns = [
// {
// title: 'ID',
// dataIndex: 'id',
// with: '33%',
// },
{
title: '参数名称',
dataIndex: 'name',
@ -72,22 +101,35 @@ const columns = [
},
];
// const dataSource = ref<Record<any, any>[]>(_props.modelValue || []);
const modelRef = reactive<{
dataSource: any[];
}>({
dataSource: [],
});
const formRef = ref();
const dataSource = computed({
get: () => {
return _props.modelValue || {
messageType: undefined,
message: {
properties: undefined,
functionId: undefined,
inputs: []
}
}
},
set: (val: any) => {
_emit('update:modelValue', val);
}
})
watchEffect(() => {
modelRef.dataSource = _props.modelValue || [];
});
</script>
const onChange = () => {
_emit('update:modelValue', modelRef.dataSource);
};
const onSave = () =>
new Promise((resolve, reject) => {
formRef.value
.validate()
.then(() => {
resolve([...modelRef.dataSource]);
})
.catch(() => {
reject(false);
});
});
defineExpose({ onSave });
</script>
<style lang="less" scoped>
</style>

View File

@ -1,76 +1,156 @@
<template>
<j-form
:layout="'vertical'"
ref="formRef"
:model="modelRef"
>
<j-form :layout="'vertical'" ref="formRef" :model="modelRef">
<j-row :gutter="24">
<j-col :span="24" v-if="actionType === 'command'">
<j-form-item name="messageType" label="指令类型" :rules="{
required: true,
message: '请选择指令类型',
}" class="other">
<j-select placeholder="请选择指令类型" v-model:value="modelRef.messageType" show-search>
<j-select-option value="READ_PROPERTY">读取属性</j-select-option>
<j-select-option value="WRITE_PROPERTY">修改属性</j-select-option>
<j-select-option value="INVOKE_FUNCTION">调用功能</j-select-option>
<j-form-item
name="messageType"
label="指令类型"
:rules="{
required: true,
message: '请选择指令类型',
}"
>
<j-select
placeholder="请选择指令类型"
v-model:value="modelRef.messageType"
show-search
>
<j-select-option value="READ_PROPERTY"
>读取属性</j-select-option
>
<j-select-option value="WRITE_PROPERTY"
>修改属性</j-select-option
>
<j-select-option value="INVOKE_FUNCTION"
>调用功能</j-select-option
>
</j-select>
</j-form-item>
</j-col>
<j-col :span="(modelRef.messageType === 'READ_PROPERTY' || actionType === 'latestData') ? 24 : 12" v-if="(actionType === 'command' && ['READ_PROPERTY','WRITE_PROPERTY'].includes(modelRef.messageType)) || actionType === 'latestData'">
<j-form-item :name="['message', 'properties']" label="属性" :rules="{
required: true,
message: '请选择属性',
}">
<j-select placeholder="请选择属性" v-model:value="modelRef.message.properties" show-search @change="onPropertyChange">
<j-select-option v-for="i in (metadata?.properties) || []" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</j-select-option>
<j-col
:span="
modelRef.messageType === 'READ_PROPERTY' ||
actionType === 'latestData'
? 24
: 12
"
v-if="
(actionType === 'command' &&
['READ_PROPERTY', 'WRITE_PROPERTY'].includes(
modelRef.messageType,
)) ||
actionType === 'latestData'
"
>
<j-form-item
:name="['message', 'properties']"
label="属性"
:rules="{
required: true,
message: '请选择属性',
}"
>
<j-select
placeholder="请选择属性"
v-model:value="modelRef.message.properties"
show-search
@change="onPropertyChange"
>
<j-select-option
v-for="i in metadata?.properties || []"
:key="i.id"
:value="i.id"
:label="i.name"
>{{ i.name }}</j-select-option
>
</j-select>
</j-form-item>
</j-col>
<j-col :span="12" v-if="modelRef.messageType === 'WRITE_PROPERTY' && actionType === 'command'">
<j-form-item :name="['message', 'value']" label="值" :rules="{
required: true,
message: '请输入值',
}">
<j-col
:span="12"
v-if="
modelRef.messageType === 'WRITE_PROPERTY' &&
actionType === 'command'
"
>
<j-form-item
:name="['message', 'value']"
label="值"
:rules="{
required: true,
message: '请输入值',
}"
>
<ValueItem
v-model:modelValue="modelRef.message.value"
:itemType="property.type || property.valueType?.type || 'int'"
:itemType="
property.type || property.valueType?.type || 'int'
"
:options="
property.valueType?.type === 'enum'
? (property?.dataType?.elements || []).map(
(item) => {
return {
label: item?.text,
value: item?.value,
};
},
)
(item) => {
return {
label: item?.text,
value: item?.value,
};
},
)
: property.valueType?.type === 'boolean'
? [
{ label: '是', value: true },
{ label: '否', value: false },
]
{ label: '是', value: true },
{ label: '否', value: false },
]
: undefined
"
/>
</j-form-item>
</j-col>
<j-col :span="24" v-if="modelRef.messageType === 'INVOKE_FUNCTION'">
<j-form-item :name="['message', 'functionId']" label="功能" :rules="{
required: true,
message: '请选择功能',
}">
<j-select placeholder="请选择功能" v-model:value="modelRef.message.functionId" show-search @change="funcChange">
<j-select-option v-for="i in (metadata?.functions) || []" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</j-select-option>
<j-form-item
:name="['message', 'functionId']"
label="功能"
:rules="{
required: true,
message: '请选择功能',
}"
>
<j-select
placeholder="请选择功能"
v-model:value="modelRef.message.functionId"
show-search
@change="funcChange"
>
<j-select-option
v-for="i in metadata?.functions || []"
:key="i.id"
:value="i.id"
:label="i.name"
>{{ i.name }}</j-select-option
>
</j-select>
</j-form-item>
</j-col>
<j-col :span="24" v-if="modelRef.messageType === 'INVOKE_FUNCTION' && modelRef.message.functionId">
<j-form-item :name="['message', 'inputs']" label="参数列表" :rules="{
required: true,
message: '请输入参数列表',
}">
<EditTable v-model="modelRef.message.inputs"/>
<j-col
:span="24"
v-if="
modelRef.messageType === 'INVOKE_FUNCTION' &&
modelRef.message.functionId
"
class="inputs"
>
<j-form-item
:name="['message', 'inputs']"
label="参数列表"
:rules="{
required: true,
message: '请输入参数列表',
}"
>
<EditTable
ref="editRef"
v-model="modelRef.message.inputs"
/>
</j-form-item>
</j-col>
</j-row>
@ -78,101 +158,106 @@
</template>
<script lang="ts" setup>
import EditTable from './EditTable.vue'
import EditTable from './EditTable.vue';
const formRef = ref();
const props = defineProps({
actionType: {
type: String,
default: ''
default: '',
},
modelValue: {
type: Object,
default: () => {}
default: () => {},
},
metadata: {
type: Object,
default: () => {
return {
properties: [],
functions: []
}
}
}
})
functions: [],
};
},
},
});
type Emits = {
(e: 'update:modelValue', data: any): void;
};
const editRef = ref();
const emit = defineEmits<Emits>();
const modelRef = reactive({
messageType: 'READ_PROPERTY',
message: {
properties: undefined,
functionId: undefined,
inputs: [],
},
});
const modelRef = computed({
get: () => {
onPropertyChange(props.modelValue?.message?.properties)
return props.modelValue || {
messageType: undefined,
message: {
properties: undefined,
functionId: undefined,
inputs: []
watch(
() => props.modelValue,
(newVal) => {
if (newVal) {
Object.assign(modelRef, newVal);
if(newVal?.message?.properties){
onPropertyChange(newVal?.message?.properties);
}
}
},
set: (val: any) => {
emit('update:modelValue', val);
}
})
{
immediate: true,
},
);
const property = ref<any>({})
const property = ref<any>({});
const funcChange = (val: string) => {
if(val){
const arr = props.metadata?.functions.find((item: any) => item.id === val)?.inputs || []
if (val) {
const arr =
props.metadata?.functions.find((item: any) => item.id === val)
?.inputs || [];
const list = arr.map((item: any) => {
return {
id: item.id,
name: item.name,
value: undefined,
valueType: item?.valueType?.type,
}
})
modelRef.value.message.inputs = list
};
});
modelRef.message.inputs = list;
}
}
};
const onPropertyChange = (val: string) => {
if(val){
const _item = props.metadata?.properties.find((item: any) => item.id === val)
property.value = _item?.[0] || {}
if (val) {
const _item = props.metadata?.properties.find(
(item: any) => item.id === val,
);
property.value = _item?.[0] || {};
}
}
};
const saveBtn = () => new Promise((resolve) => {
formRef.value.validate()
.then(() => {
const _arr = toRaw(modelRef).value?.message?.inputs || []
if(_arr.length && !_arr.every((_a: any) => _a.value)){
resolve(false)
} else {
resolve(toRaw(modelRef))
}
})
.catch((err: any) => {
resolve(err)
const saveBtn = () =>
new Promise((resolve) => {
formRef.value
.validate()
.then(async (_data: any) => {
await editRef.value.onSave().catch(() => {
resolve(false)
})
resolve(_data)
})
.catch((err: any) => {
resolve(err);
});
});
})
defineExpose({ saveBtn })
defineExpose({ saveBtn });
</script>
<style lang="less" scoped>
:deep(.ant-form-item){
margin-bottom: 0;
}
.other {
margin-bottom: 24px;
.inputs {
.ant-form-item:last-child {
margin-bottom: 0;
}
}
</style>

View File

@ -171,7 +171,8 @@
>
<j-select-option
v-for="i in getTypesActions(
item.action || ''
item.action ||
'',
)"
:key="i.id"
:value="i.id"
@ -321,7 +322,8 @@
>
<j-select-option
v-for="i in getDuerOSProperties(
item.source || '',
item.source ||
'',
)"
:key="i.id"
:value="i.id"
@ -410,7 +412,10 @@
type="primary"
:loading="loading"
@click="saveBtn"
:hasPermission="['Northbound/DuerOS:add', 'Northbound/DuerOS:update']"
:hasPermission="[
'Northbound/DuerOS:add',
'Northbound/DuerOS:update',
]"
>
保存
</PermissionButton>
@ -626,12 +631,11 @@ const saveBtn = async () => {
loading.value = true;
const resp = await savePatch(data).finally(() => {
loading.value = false;
})
});
if (resp.status === 200) {
message.success('操作成功!');
formRef.value.resetFields();
menuStory.jumpPage('Northbound/DuerOS');
}
}
})

View File

@ -27,11 +27,12 @@
<template #card="slotProps">
<CardBox
:value="slotProps"
@click="handleView(slotProps.id)"
:actions="getActions(slotProps, 'card')"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
enabled: 'success',
enabled: 'processing',
disabled: 'error',
}"
>
@ -39,13 +40,12 @@
<img :src="getImage('/cloud/dueros.png')" />
</template>
<template #content>
<h3
class="card-item-content-title"
@click.stop="handleView(slotProps.id)"
>
{{ slotProps.name }}
</h3>
<j-row>
<Ellipsis style="width: calc(100% - 100px)">
<span style="font-size: 16px; font-weight: 600">
{{ slotProps.name }}
</span>
</Ellipsis>
<j-row style="margin-top: 15px">
<j-col :span="12">
<div class="card-item-content-text">产品</div>
<Ellipsis>
@ -85,9 +85,13 @@
</CardBox>
</template>
<template #state="slotProps">
<j-badge
<BadgeStatus
:status="slotProps.state?.value"
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
:statusNames="{
enabled: 'processing',
disabled: 'error',
}"
/>
</template>
<template #applianceType="slotProps">
@ -105,7 +109,7 @@
:tooltip="{
...i.tooltip,
}"
style="padding: 0px"
style="padding: 0 5px"
@click="i.onClick"
type="link"
:hasPermission="'Northbound/DuerOS:' + i.key"
@ -132,15 +136,12 @@ import type { ActionsType } from '@/views/device/Instance/typings';
import { getImage } from '@/utils/comm';
import { message } from 'jetlinks-ui-components';
import { useMenuStore } from 'store/menu';
import BadgeStatus from '@/components/BadgeStatus/index.vue';
const instanceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const menuStory = useMenuStore();
const statusMap = new Map();
statusMap.set('enabled', 'success');
statusMap.set('disabled', 'error');
const columns = [
{
title: '名称',
@ -193,6 +194,7 @@ const columns = [
title: '说明',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
{
title: '状态',
@ -211,7 +213,7 @@ const columns = [
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
width: 200,
scopedSlots: true,
},
];

View File

@ -1,61 +0,0 @@
<template>
<Form
ref='form'
:options='options'
:initValue='initValue'
/>
<a-button @click='submit'>提交</a-button>
<a-button @click='reset'>重置</a-button>
<a-button @click='setValue'>修改name</a-button>
</template>
<script setup name='FormDemo'>
import { componentType } from 'components/Form'
const form = ref()
const initValue = reactive({})
const submit = () => {
form.value.formValidate().then(res => {
console.log(res)
})
}
const reset = () => {
form.value.reset()
}
const setValue =() => {
initValue.name = '111111'
}
const options = reactive({
name: {
component: componentType.input,
componentProps: {
style: {
width: '200px'
}
},
title: '测试',
required: true
},
sex: {
component: componentType.select,
title: '性别',
options: [
{ label: '111', value: 1 },
{ label: '222', value: 2 },
],
required: true,
rules: [
{ required: true, message: '请选择性别'}
],
tooltip: '性别',
default: 1
}
})
</script>
<style scoped>
</style>

View File

@ -1,83 +0,0 @@
<!-- test demo -->
<template>
<div class="page-container">
父级: {{ testValue }}
<ValueItem v-model="testValue" />
<!-- 卡片 -->
<br />卡片组件
<a-row :gutter="20">
<a-col :span="6">
<CardBox
status="disable"
:statusNames="{ disable: StatusColorEnum.error }"
statusText="正常"
:actions="actions"
v-model="data"
>
<template #img>
<img :src="getImage('/device-product.png')" />
</template>
<template #content>
<div class="card-item-heard-name">设备名称</div>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>直连设备</div>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">
产品名称
</div>
<div>测试固定地址</div>
</a-col>
</a-row>
</template>
</CardBox>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import CardBox from '@/components/CardBox/index.vue';
import { StatusColorEnum } from '@/utils/consts';
import { getImage } from '@/utils/comm';
const testValue = ref('');
//
const actions = ref([
{
key: 'check',
label: '查看',
},
{
key: 'edit',
label: '编辑',
disabled: true,
message: '暂无权限,请联系管理员',
},
{
key: 'delete',
label: '删除',
},
]);
const data = ref({
id: 123
})
</script>
<style lang="less" scoped>
.card-item-heard-name {
font-weight: 700;
font-size: 16px;
margin-bottom: 12px;
}
.card-item-content-text {
color: rgba(0, 0, 0, 0.75);
font-size: 12px;
}
</style>

View File

@ -1,187 +0,0 @@
<template>
<div class="box">
<JTable
:columns="columns"
:request="request"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange
}"
@cancelSelect="cancelSelect"
>
<template #headerTitle>
<a-button type="primary" @click="add">新增</a-button>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
@click="handleClick"
:actions="getActions(slotProps)"
v-bind="slotProps"
:active="_selectedRowKeys.includes(slotProps.id)"
:status="slotProps.state ? 'success' : 'error'"
>
<template #img>
<slot name="img">
<img :src="getImage('/device-product.png')" />
</slot>
</template>
<template #content>
<h3>{{slotProps.name}}</h3>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>直连设备</div>
</a-col>
</a-row>
</template>
<template #actions="item">
<a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
<a-button :disabled="item.disabled">
<DeleteOutlined v-if="item.key === 'delete'" />
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</template>
</a-button>
</a-popconfirm>
<template v-else>
<a-button :disabled="item.disabled">
<DeleteOutlined v-if="item.key === 'delete'" />
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</template>
</a-button>
</template>
</template>
</CardBox>
</template>
<template #id="slotProps">
<a>{{slotProps.id}}</a>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip v-for="i in getActions(slotProps)" :key="i.key" v-bind="i.tooltip">
<a-popconfirm v-if="i.popConfirm" v-bind="i.popConfirm">
<a-button :disabled="i.disabled" style="padding: 0" type="link"><AIcon :type="i.icon" /></a-button>
</a-popconfirm>
<a-button style="padding: 0" type="link" v-else @click="i.onClick && i.onClick(slotProps)">
<a-button :disabled="i.disabled" style="padding: 0" type="link"><AIcon :type="i.icon" /></a-button>
</a-button>
</a-tooltip>
</a-space>
</template>
</JTable>
</div>
</template>
<script setup lang="ts">
import server from "@/utils/request";
import type { ActionsType } from '@/components/Table/index.vue'
import { getImage } from '@/utils/comm';
import { DeleteOutlined } from '@ant-design/icons-vue'
import { message } from "ant-design-vue";
const request = (data: any) => server.post(`/device-product/_query`, data)
// const request = (data: any) => server.post(`/device/category/_tree`, {paging: false})
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: 'ID',
dataIndex: 'id',
key: 'id',
scopedSlots: true
},
{
title: '分类',
dataIndex: 'classifiedName',
key: 'classifiedName',
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true
}
]
const _selectedRowKeys = ref<string[]>([])
const onSelectChange = (keys: string[]) => {
_selectedRowKeys.value = [...keys]
}
const cancelSelect = () => {
_selectedRowKeys.value = []
}
const handleClick = (dt: any) => {
// _selectedRowKeys.value = [dt.id] //
// _selectedRowKeys.value = [..._selectedRowKeys.value, dt.id] //
if(_selectedRowKeys.value.includes(dt.id)) {
const _index = _selectedRowKeys.value.findIndex(i => i === dt.id)
_selectedRowKeys.value.splice(_index, 1)
} else {
_selectedRowKeys.value = [..._selectedRowKeys.value, dt.id]
}
}
const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
if(!data){
return []
}
return [
{
key: 'edit',
text: "编辑",
tooltip: {
title: '编辑'
},
icon: 'icon-rizhifuwu'
},
{
key: 'import',
text: "导入",
tooltip: {
title: '导入'
},
disabled: true,
icon: 'icon-xiazai'
},
{
key: 'delete',
text: "删除",
tooltip: {
title: !!data?.state ? '正常的产品不能删除' : '删除'
},
popConfirm: {
title: '确认删除?'
},
icon: 'icon-huishouzhan'
}
]
}
const add = () => {
message.warn('123')
}
</script>
<style lang="less" scoped>
.box {
padding: 20px;
background: #f0f2f5;
}
</style>

View File

@ -1,9 +0,0 @@
import { Button } from 'ant-design-vue'
export default defineComponent({
setup(){
return () => <div>
<Button type='primary'></Button>
</div>
}
})

View File

@ -76,6 +76,7 @@ import type { ActionsType } from '@/components/Table/index.vue';
import ModifyModal from './components/modifyModal/index.vue';
import type { TableColumnType, TableProps } from 'ant-design-vue';
import { message } from 'ant-design-vue';
const expandedRowKeys = ref<any>([]);
const tableRef = ref<Record<string, any>>({});
const modifyRef = ref();
const dataSource = ref<any>([]);
@ -126,7 +127,7 @@ let params = ref();
* 搜索
*/
const handleSearch = (e: any) => {
params.value = e
params.value = e;
};
/**
* 操作栏按钮

View File

@ -8,20 +8,25 @@
<template #title>
<div>
<div style="display: flex; align-items: center">
<AIcon type="ArrowLeftOutlined" @click="onBack" />
<div style="margin-left: 20px">
<j-button @click="onBack" size="small">返回</j-button>
<div style="margin-left: 20px; font-size: 24px">
{{ instanceStore.current.name }}
</div>
<j-divider type="vertical" />
<j-space>
<j-badge
:text="instanceStore.current.state?.text"
:status="
statusMap.get(
instanceStore.current.state?.value,
)
"
/>
<span
style="font-size: 14px; color: rgba(0, 0, 0, 0.85)"
>
状态
<j-badge
:status="
statusMap.get(
instanceStore.current.state?.value,
)
"
/>
{{ instanceStore.current.state?.text }}
</span>
<PermissionButton
v-if="
instanceStore.current.state?.value ===
@ -73,7 +78,7 @@
</j-tooltip>
</j-space>
</div>
<div style="padding-top: 10px">
<div style="padding-top: 24px">
<j-descriptions size="small" :column="4">
<j-descriptions-item label="ID">{{
instanceStore.current.id
@ -199,8 +204,8 @@ watch(
);
onMounted(() => {
instanceStore.tabActiveKey = history.state?.params?.tab || 'Info'
})
instanceStore.tabActiveKey = history.state?.params?.tab || 'Info';
});
const onBack = () => {
menuStory.jumpPage('device/Instance');
@ -284,7 +289,10 @@ watchEffect(() => {
tab: 'OPC UA',
});
}
if (instanceStore.current.deviceType?.value === 'gateway' && !keys.includes('ChildDevice')) {
if (
instanceStore.current.deviceType?.value === 'gateway' &&
!keys.includes('ChildDevice')
) {
//
list.value.push({
key: 'ChildDevice',

View File

@ -143,7 +143,7 @@
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
online: 'success',
online: 'processing',
offline: 'error',
notActive: 'warning',
}"
@ -202,9 +202,14 @@
</CardBox>
</template>
<template #state="slotProps">
<j-badge
<BadgeStatus
:status="slotProps.state?.value"
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
:statusNames="{
online: 'processing',
offline: 'error',
notActive: 'warning',
}"
/>
</template>
<template #createTime="slotProps">
@ -226,7 +231,7 @@
}"
@click="i.onClick"
type="link"
style="padding: 0px"
style="padding: 0 5px"
:hasPermission="'device/Instance:' + i.key"
>
<template #icon><AIcon :type="i.icon" /></template>
@ -289,6 +294,7 @@ import { queryTree } from '@/api/device/category';
import { useMenuStore } from '@/store/menu';
import type { ActionsType } from './typings';
import dayjs from 'dayjs';
import BadgeStatus from '@/components/BadgeStatus/index.vue';
const instanceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
@ -303,11 +309,6 @@ const type = ref<string>('');
const menuStory = useMenuStore();
const statusMap = new Map();
statusMap.set('online', 'success');
statusMap.set('offline', 'error');
statusMap.set('notActive', 'warning');
const columns = [
{
title: 'ID',
@ -479,6 +480,7 @@ const columns = [
title: '说明',
dataIndex: 'describe',
key: 'describe',
ellipsis: true,
search: {
type: 'string',
},
@ -487,7 +489,7 @@ const columns = [
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
width: 200,
scopedSlots: true,
},
];
@ -525,10 +527,10 @@ const paramsFormat = (
};
onMounted(() => {
if(history.state?.params?.type === 'add'){
handleAdd()
if (history.state?.params?.type === 'add') {
handleAdd();
}
})
});
const handleParams = (config: Record<string, any>) => {
const _terms: Record<string, any> = {};

View File

@ -374,6 +374,7 @@ import { Empty, FormItem, message } from 'ant-design-vue';
import { getImage } from '@/utils/comm';
import Title from '../Title/index.vue';
import { usePermissionStore } from '@/store/permission';
import { steps, steps1 } from './util'
import './index.less';
import {
getProviders,
@ -412,7 +413,7 @@ const visible = ref<boolean>(false);
const listData = ref<string[]>([]);
const access = ref({});
const config = ref<any>({});
const metadata = ref<ConfigMetadata[]>([]);
const metadata = ref<ConfigMetadata>({ properties: [] });
const dataSource = ref<string[]>([]);
const storageList = ref<any[]>([]);
const markdownToHtml = shallowRef('');
@ -589,74 +590,8 @@ const search = (e: any) => {
};
};
//
const steps = [
{
element: '#rc-tabs-0-tab-Metadata',
popover: {
id: 'driver',
title: `<div id='title'>配置物模型</div><div id='guide'>1/3</div>`,
description: `配置产品物模型,实现设备在云端的功能描述。`,
position: 'bottom',
},
},
{
element: '.ant-switch',
popover: {
className: 'driver',
title: `<div id='title'>启用产品</div><div id='guide'>2/3</div>`,
description: '启用产品后,可在产品下新增设备。',
position: 'bottom',
},
},
{
element: '.ant-descriptions-item-label',
popover: {
className: 'driver',
title: `<div id='title'>添加设备</div><div id='guide'>3/3</div>`,
description: '添加设备,并连接到平台。',
position: 'bottom',
},
},
];
const steps1 = [
{
element: '.config',
popover: {
className: 'driver',
title: `<div id='title'>填写配置</div><div id='guide'>1/4</div>`,
description: `填写设备接入所需的配置参数。`,
position: 'right',
},
},
{
element: '#rc-tabs-0-tab-Metadata',
popover: {
className: 'driver',
title: `<div id='title'>配置物模型</div><div id='guide'>2/4</div>`,
description: `配置产品物模型,实现设备在云端的功能描述。`,
position: 'bottom',
},
},
{
element: '.ant-switch',
popover: {
className: 'driver',
title: `<div id='title'>启用产品</div><div id='guide'>3/4</div>`,
description: '启用产品后,可在产品下新增设备。',
position: 'bottom',
},
},
{
element: '.ant-descriptions-item-label',
popover: {
className: 'driver',
title: `<div id='title'>添加设备</div><div id='guide'>4/4</div>`,
description: '添加设备,并连接到平台。',
position: 'bottom',
},
},
];
const stepsRef = reactive({current:0})
/**
* 保存引导页数据
*/
@ -673,19 +608,19 @@ const driver = new Driver({
nextBtnText: '下一步',
prevBtnText: '上一步',
onNext: () => {
// ref.current = ref.current + 1;
stepsRef.current = stepsRef.current + 1;
},
onPrevious: () => {
// ref.current = ref.current - 1;
stepsRef.current = stepsRef.current - 1;
},
onReset: () => {
// if (ref.current !== 3) {
// guide({
// name: 'guide',
// content: 'skip',
// });
// }
// ref.current = 0;
if (stepsRef.current !== 3) {
guide({
name: 'guide',
content: 'skip',
});
}
stepsRef.current = 0;
},
});
@ -695,16 +630,20 @@ const driver1 = new Driver({
closeBtnText: '不再提示',
nextBtnText: '下一步',
prevBtnText: '上一步',
onNext: () => {},
onPrevious: () => {},
onNext: () => {
stepsRef.current = stepsRef.current + 1;
},
onPrevious: () => {
stepsRef.current = stepsRef.current - 1;
},
onReset: () => {
// if (ref.current !== 4) {
// // guide({
// // name: 'guide',
// // content: 'skip',
// // });
// }
// ref.current = 0;
if (stepsRef.current !== 4) {
guide({
name: 'guide',
content: 'skip',
});
}
stepsRef.current = 0;
},
});
@ -865,11 +804,11 @@ const queryAccessDetail = async (id: string) => {
/**
* 查询协议信息
*/
const getConfigDetail = async (
const getConfigDetail = (
messageProtocol: string,
transportProtocol: string,
) => {
const res = await getConfigView(messageProtocol, transportProtocol).then(
getConfigView(messageProtocol, transportProtocol).then(
(resp) => {
if (resp.status === 200) {
config.value = resp.result;
@ -960,10 +899,12 @@ const submitData = async () => {
obj.metadata = JSON.stringify(mdata);
}
}
//
const resp: any = obj.id
? await updateDevice(obj)
: await saveDevice(obj);
if (resp.status === 200) {
detail(productStore.current?.id || '').then((res) => {
if (res.status === 200) {
productStore.current = { ...res.result };
@ -972,7 +913,9 @@ const submitData = async () => {
}
visible.value = false;
queryParams.value = {};
});
getData(obj.accessId);
}
} else {
message.error('请选择接入方式');
@ -988,48 +931,59 @@ const modifyArray = (oldData: any[], newData: any[]) => {
return { ...item, sortsIndex: index };
});
};
/**
*
*/
const getGuide = async (isDriver1: boolean = false) => {
const res: any = await productGuide();
if (res.result && res.result?.content === 'skip') {
return;
} else {
if (isDriver1) {
driver1.defineSteps(steps1);
driver1.start();
} else {
driver.defineSteps(steps);
driver.start();
}
}
}
/**
* 查询保存数据信息
*/
const getData = async () => {
if (productStore.current?.accessId) {
if (productStore.current?.id) {
getConfigMetadata(productStore.current?.id).then(
async (resp: any) => {
metadata.value =
(resp?.result[0] as ConfigMetadata[]) || [];
const res: any = await productGuide();
if (res.result && res.result?.content === 'skip') {
return;
} else {
if (resp.result && resp.result.length > 0) {
driver1.defineSteps(steps1);
driver1.start();
} else {
driver.defineSteps(steps);
driver.start();
}
}
},
);
}
queryAccessDetail(productStore.current?.accessId);
getConfigDetail(
productStore.current?.messageProtocol || '',
productStore.current?.transportProtocol || '',
);
getProviders().then((resp) => {
if (resp.status === 200) {
dataSource.value = resp.result;
}
});
} else {
if (productStore.current?.id) {
getConfigMetadata(productStore.current?.id).then((resp: any) => {
metadata.value = resp?.result[0] as ConfigMetadata[];
});
const getData = async (accessId?: string) => {
const _accessId = accessId || productStore.current?.accessId
if (productStore.current?.id) {
getConfigMetadata(productStore.current?.id).then((resp: any) => {
metadata.value = resp?.result[0] as ConfigMetadata || { properties: [] };
if (accessId) { //
getGuide(!resp?.result.length) //
}
});
}
if (_accessId) { //
// const metadataResp = await getConfigMetadata(productStore.current!.id)
// if (metadataResp.success) {
// metadata.value = (metadataResp.result?.[0] as ConfigMetadata[]) || [];
// }
queryAccessDetail(_accessId);
getConfigDetail(
productStore.current?.messageProtocol || '',
productStore.current?.transportProtocol || '',
);
getProviders().then((resp) => {
if (resp.status === 200) {
dataSource.value = resp.result;
}
});
}
// 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;
@ -1086,10 +1040,14 @@ const add = () => {
* 初始化
*/
watchEffect(() => {
if (productStore.current?.accessId) {
getData();
}
});
if (productStore.current?.storePolicy) {
form.storePolicy = productStore.current!.storePolicy
}
})
nextTick(() => {
getData();
})
</script>
<style lang="less" scoped>
:deep(

View File

@ -0,0 +1,69 @@
//引导页数据
export const steps = [
{
element: '#rc-tabs-0-tab-Metadata',
popover: {
id: 'driver',
title: `<div id='title'>配置物模型</div><div id='guide'>1/3</div>`,
description: `配置产品物模型,实现设备在云端的功能描述。`,
position: 'bottom',
},
},
{
element: '.ant-switch',
popover: {
className: 'driver',
title: `<div id='title'>启用产品</div><div id='guide'>2/3</div>`,
description: '启用产品后,可在产品下新增设备。',
position: 'bottom',
},
},
{
element: '.ant-descriptions-item-label',
popover: {
className: 'driver',
title: `<div id='title'>添加设备</div><div id='guide'>3/3</div>`,
description: '添加设备,并连接到平台。',
position: 'bottom',
},
},
];
export const steps1 = [
{
element: '.config',
popover: {
className: 'driver',
title: `<div id='title'>填写配置</div><div id='guide'>1/4</div>`,
description: `填写设备接入所需的配置参数。`,
position: 'right',
},
},
{
element: '#rc-tabs-0-tab-Metadata',
popover: {
className: 'driver',
title: `<div id='title'>配置物模型</div><div id='guide'>2/4</div>`,
description: `配置产品物模型,实现设备在云端的功能描述。`,
position: 'bottom',
},
},
{
element: '.ant-switch',
popover: {
className: 'driver',
title: `<div id='title'>启用产品</div><div id='guide'>3/4</div>`,
description: '启用产品后,可在产品下新增设备。',
position: 'bottom',
},
},
{
element: '.ant-descriptions-item-label',
popover: {
className: 'driver',
title: `<div id='title'>添加设备</div><div id='guide'>4/4</div>`,
description: '添加设备,并连接到平台。',
position: 'bottom',
},
},
];

View File

@ -87,6 +87,7 @@
<component
:is="tabs[productStore.tabActiveKey]"
:class="productStore.tabActiveKey === 'Metadata' ? 'metedata' : ''"
v-bind="{ type: 'product' }"
/>
</page-container>
</template>
@ -151,6 +152,7 @@ watch(
() => route.params.id,
(newId) => {
if (newId) {
productStore.reSet();
productStore.tabActiveKey = 'Info';
productStore.refresh(newId as string);
}

View File

@ -132,7 +132,7 @@
<PermissionButton
:disabled="i.disabled"
:popConfirm="i.popConfirm"
:hasPermission="'device/Product:' + i.key"
:hasPermission="i.key === 'view' ? true : 'device/Product:' + i.key"
:tooltip="{
...i.tooltip,
}"

View File

@ -47,9 +47,9 @@ export type ConfigProperty = {
};
export type ConfigMetadata = {
name: string;
description: string;
scopes: any[];
name?: string;
description?: string;
scopes?: any[];
properties: ConfigProperty[];
};

View File

@ -31,7 +31,7 @@
</j-radio-group>
</j-form-item>
<j-form-item label="输入参数" name="inputs" :rules="[
{ required: true, validator: (_rule: Rule, val: Record<any, any>[]) => validateJson(_rule, val, '输入参数') },
{ validator: (_rule: Rule, val: Record<any, any>[]) => validateJson(_rule, val, '输入参数', false) },
]">
<JsonParam v-model:value="value.inputs" :name="['inputs']"></JsonParam>
</j-form-item>

View File

@ -2,7 +2,7 @@
<j-form-item :label="title" :name="name.concat(['type'])" :rules="[
required ? { required: true, message: `请选择${title}` } : {},
]">
<j-select v-model:value="_value.type"
<j-select v-model:value="_value.type" :disabled="onlyObject"
:options="onlyObject ? eventDataTypeList : _dataTypeList" size="small"
@change="changeType"></j-select>
</j-form-item>
@ -124,7 +124,7 @@ onMounted(() => {
}
})
const unit = {
const unit = reactive({
unitOptions: [] as DefaultOptionType[],
getUnit: () => {
getUnit().then(resp => {
@ -137,7 +137,7 @@ const unit = {
unit.unitOptions = _data;
})
},
}
})
unit.getUnit()
const _dataTypeList = computed(() => props.isSub ? DataTypeList.filter(item => item.value !== 'array' && item.value !== 'object') : DataTypeList)

View File

@ -2,7 +2,7 @@
<j-drawer :mask-closable="false" width="25vw" visible :title="`${title}-${typeMapping[metadataStore.model.type]}`"
@close="close" destroy-on-close :z-index="1000" placement="right">
<template #extra>
<j-button :loading="save.loading" type="primary" @click="save.saveMetadata">保存</j-button>
<j-button :loading="save.loading" type="primary" @click="() => save.saveMetadata()">保存</j-button>
</template>
<j-form ref="formRef" :model="form.model" layout="vertical">
<BaseForm :model-type="metadataStore.model.type" :type="type" v-model:value="form.model"></BaseForm>
@ -98,7 +98,7 @@ const save = reactive({
}
const _data = updateMetadata(type, [formValue], _detail, updateStore)
const result = await asyncUpdateMetadata(props.type, _data)
if (result.status === 200) {
if (result.success) {
if ((window as any).onTabSaveSuccess) {
if (result) {
(window as any).onTabSaveSuccess(result);
@ -113,7 +113,16 @@ const save = reactive({
}
// Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
if (deploy) {
_deploy(id as string)
const res = await _deploy(id as string)
if (res.success) {
save.resetMetadata();
message.success({
key: 'metadata',
content: '操作成功!',
});
} else {
message.error('操作失败!');
}
// Store.set('product-deploy', deploy);
} else {
save.resetMetadata();

View File

@ -17,8 +17,8 @@ export const validateArray = async (_rule: Rule, val: Record<any, any>) => {
return Promise.resolve();
}
export const validateJson = async (_rule: Rule, val: Record<any, any>[], title = '配置参数') => {
if (!val || val.length === 0) {
export const validateJson = async (_rule: Rule, val: Record<any, any>[], title = '配置参数', required = true) => {
if (required && (!val || val.length === 0)) {
return Promise.reject(new Error(`请输入${title}`));
}
for (let item of val) {

View File

@ -39,7 +39,7 @@
<template v-if="column.dataIndex === 'action'">
<j-space>
<PermissionButton :has-permission="`${permission}:update`" type="link" key="edit" style="padding: 0"
:udisabled="operateLimits('updata', type)" @click="handleEditClick(record)" :tooltip="{
:disabled="operateLimits('updata', type)" @click="handleEditClick(record)" :tooltip="{
title: operateLimits('updata', type) ? '当前的存储方式不支持编辑' : '编辑',
}">
<AIcon type="EditOutlined" />
@ -68,7 +68,6 @@ import { useMetadataStore } from '@/store/metadata'
import PermissionButton from '@/components/PermissionButton/index.vue'
import { TablePaginationConfig, message } from 'ant-design-vue/es'
import { asyncUpdateMetadata, removeMetadata } from '../metadata'
import { detail } from '@/api/device/instance'
import Edit from './Edit/index.vue'
interface Props {
type: MetadataType;
@ -153,6 +152,9 @@ const handleAddClick = () => {
metadataStore.set('item', undefined)
metadataStore.set('type', type)
metadataStore.set('action', 'add')
if (props.target === 'device' && !instanceStore.detail?.independentMetadata) {
message.warning('修改物模型后会脱离产品物模型')
}
}
const limitsMap = new Map<string, any>();
@ -172,7 +174,7 @@ const handleEditClick = (record: MetadataItem) => {
metadataStore.model.item = record;
metadataStore.model.type = type;
metadataStore.model.action = 'edit';
if (!instanceStore.detail?.independentMetadata && props.target === 'device') {
if (props.target === 'device' && !instanceStore.detail?.independentMetadata) {
message.warning('修改物模型后会脱离产品物模型');
}
}

View File

@ -2,7 +2,7 @@
<j-card>
<div class='device-detail-metadata' style="position: relative;">
<div class="tips">
<j-tooltip :title="instanceStore.detail?.independentMetadata && type === 'device'
<j-tooltip v-if="type === 'device'" :title="instanceStore.detail?.independentMetadata && type === 'device'
? '该设备已脱离产品物模型,修改产品物模型对该设备无影响'
: '设备会默认继承产品的物模型,修改设备物模型后将脱离产品物模型'">
<div class="ellipsis">
@ -41,7 +41,7 @@
<BaseMetadata :target="type" type="tags" :permission="permission" />
</j-tab-pane>
</j-tabs>
<Import v-model:visible="visible" :type="type" @close="visible = false" />
<Import v-if="visible" v-model:visible="visible" :type="type" @close="visible = false" />
<Cat v-model:visible="cat" @close="cat = false" :type="type" />
</div>
</j-card>

View File

@ -51,7 +51,7 @@
<h3 class="card-item-content-title">
{{ slotProps.name }}
</h3>
<p>通道数量{{ slotProps.count }}</p>
<p>通道数量{{ slotProps.count || 0 }}</p>
<Ellipsis>
<j-badge
:text="`sip:${slotProps.sipConfigs[0]?.sipId}@${slotProps.sipConfigs[0]?.hostAndPort}`"
@ -91,6 +91,9 @@
<template #publicHost="slotProps">
{{ slotProps.sipConfigs[0]?.publicHost }}
</template>
<template #count="slotProps">
{{ slotProps.count || 0 }}
</template>
<template #status="slotProps">
<j-badge
:text="slotProps.status?.text"
@ -180,6 +183,7 @@ const columns = [
title: '通道数量',
dataIndex: 'count',
key: 'count',
scopedSlots: true,
width: 100,
},
{
@ -237,13 +241,17 @@ const handleSearch = (e: any) => {
* 处理表格数据
* @param params
*/
const lastValueFrom = async (params: any) => {
const res = await CascadeApi.list(params);
res.result.data.forEach(async (item: any) => {
const resp = await CascadeApi.queryBindChannel(item.id, {});
item.count = resp.result.total;
const lastValueFrom = (params: any) => {
return new Promise(async (resolve) => {
const res = await CascadeApi.list(params);
res.result.data.forEach(async (item: any) => {
const resp = await CascadeApi.queryBindChannel(item.id, {});
item.count = resp.result.total;
});
setTimeout(() => {
resolve(res);
}, 1000);
});
return res;
};
/**

View File

@ -22,10 +22,18 @@
: '停止录像'
}}
</div>
<div class="tool-item">刷新</div>
<div class="tool-item" @click.stop="handleRefresh">
刷新
</div>
<div class="tool-item" @click.stop="handleReset">重置</div>
</div>
<LivePlayer :live="true" :url="url" :protocol="mediaType" autoplay />
<LivePlayer
ref="player"
:live="true"
:url="url"
:protocol="mediaType"
autoplay
/>
</div>
<MediaTool
@onMouseDown="handleMouseDown"
@ -70,6 +78,8 @@ const _vis = computed({
set: (val) => emit('update:visible', val),
});
//
const player = ref();
//
const url = ref('');
//
@ -126,6 +136,13 @@ const handleRecord = async () => {
}
};
/**
* 刷新
*/
const handleRefresh = () => {
player.value.play();
};
/**
* 重置
*/

View File

@ -24,9 +24,8 @@
</j-form-item>
<j-row :gutter="24">
<j-col :span="8">
<JUpload
<JProUpload
v-model:modelValue="formData.photoUrl"
:bgImage="formData.photoUrl"
/>
</j-col>
<j-col :span="16">
@ -92,11 +91,14 @@
v-model:value="formData.productId"
placeholder="请选择所属产品"
:disabled="!!route.query.id"
showSearch
@change="handleProductChange"
>
<j-select-option
v-for="(item, index) in productList"
:key="index"
:value="item.id"
:label="item.name"
>
{{ item.name }}
</j-select-option>
@ -345,6 +347,12 @@ const getProductList = async () => {
};
getProductList();
const handleProductChange = () => {
formData.value.others.access_pwd =
productList.value.find((f: any) => f.id === formData.value.productId)
?.configuration.access_pwd || '';
};
/**
* 新增产品
*/
@ -408,7 +416,7 @@ const handleSubmit = () => {
: await DeviceApi.update(params);
if (res?.success) {
message.success('保存成功');
router.back();
history.back();
}
})
.catch((err: any) => {

View File

@ -104,6 +104,9 @@
<template #provider="slotProps">
{{ providerType[slotProps.provider] }}
</template>
<template #productId="slotProps">
{{ getProductName(slotProps.productId) }}
</template>
<template #state="slotProps">
<j-badge
:text="slotProps.state?.text"
@ -131,7 +134,10 @@
style="padding: 0px"
:hasPermission="
'media/Device:' +
(i.key !== 'updateChannel' ? i.key : 'update')
(i.key !== 'updateChannel' &&
i.key !== 'viewDevice'
? i.key
: 'update')
"
>
<template #icon><AIcon :type="i.icon" /></template>
@ -211,7 +217,7 @@ const columns = [
title: '产品名称',
dataIndex: 'productId',
key: 'productId',
// scopedSlots: true,
scopedSlots: true,
search: {
type: 'select',
options: () =>
@ -325,6 +331,17 @@ const getActions = (
);
},
},
{
key: 'viewDevice',
text: '查看',
tooltip: {
title: '查看',
},
icon: 'EyeOutlined',
onClick: () => {
menuStory.jumpPage('device/Instance/Detail', { id: data.id });
},
},
{
key: 'updateChannel',
text: '更新通道',
@ -374,8 +391,42 @@ const getActions = (
icon: 'DeleteOutlined',
},
];
return data.provider === 'fixed-media'
? actions.filter((f: any) => f.key !== 'updateChannel')
: actions;
let acts: any = [];
if (type === 'card') {
//
const tempActs = actions.filter((f: any) => f.key !== 'viewDevice');
acts =
data.provider === 'fixed-media'
? tempActs.filter((f: any) => f.key !== 'updateChannel')
: tempActs;
} else {
acts =
data.provider === 'fixed-media'
? actions.filter((f: any) => f.key !== 'updateChannel')
: actions;
}
return acts;
};
const productList = ref<any[]>([]);
const getProductList = () => {
DeviceApi.getProductList(
encodeQuery({
terms: {
messageProtocol$in: ['gb28181-2016', 'fixed-media'],
},
}),
).then((resp: any) => {
productList.value = resp.result.map((pItem: any) => ({
label: pItem.name,
value: pItem.id,
}));
});
};
getProductList();
const getProductName = (pid: string) => {
return productList.value.find((f: any) => f.value === pid)?.label;
};
</script>

View File

@ -23,6 +23,11 @@ export type DeviceItem = {
transport: string;
} & BaseItem;
type configuration = {
access_pwd: string;
stream_mode: string;
}
export type ProductType = {
accessId: string;
accessName: string;
@ -42,6 +47,7 @@ export type ProductType = {
protocolName: string;
state: number;
transportProtocol: string;
configuration: configuration;
}

View File

@ -50,9 +50,9 @@
<template #card="slotProps">
<CardBox
:showStatus="false"
:statusNames="{}"
:value="slotProps"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
>
<template #img>
<slot name="img">

View File

@ -56,7 +56,7 @@
:showStatus="false"
:value="slotProps"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:statusNames="{}"
>
<template #img>
<slot name="img">

View File

@ -1,23 +1,23 @@
<template>
<a-modal
<j-modal
title='触发规则'
visible
:width='820'
@click='save'
@cancel='cancel'
>
<a-steps :current='addModel.stepNumber' @change='stepChange'>
<a-step>
<j-steps :current='addModel.stepNumber' @change='stepChange'>
<j-step>
<template #title>选择产品</template>
</a-step>
<a-step>
</j-step>
<j-step>
<template #title>选择设备</template>
</a-step>
<a-step>
</j-step>
<j-step>
<template #title>触发类型</template>
</a-step>
</a-steps>
<a-divider style='margin-bottom: 0px' />
</j-step>
</j-steps>
<j-divider style='margin-bottom: 0px' />
<div class='steps-content'>
<Product
v-if='addModel.stepNumber === 0'
@ -42,13 +42,13 @@
</div>
<template #footer>
<div class='steps-action'>
<a-button v-if='addModel.stepNumber === 0' @click='cancel'>取消</a-button>
<a-button v-else @click='prev'>上一步</a-button>
<a-button type='primary' v-if='addModel.stepNumber < 2' @click='saveClick'>下一步</a-button>
<a-button type='primary' v-else @click='saveClick'>确定</a-button>
<j-button v-if='addModel.stepNumber === 0' @click='cancel'>取消</j-button>
<j-button v-else @click='prev'>上一步</j-button>
<j-button type='primary' v-if='addModel.stepNumber < 2' @click='saveClick'>下一步</j-button>
<j-button type='primary' v-else @click='saveClick'>确定</j-button>
</div>
</template>
</a-modal>
</j-modal>
</template>
<script setup lang='ts' name='AddModel'>
@ -59,7 +59,7 @@ import { detail as deviceDetail } from '@/api/device/instance'
import Product from './Product.vue'
import DeviceSelect from './DeviceSelect.vue'
import Type from './Type.vue'
import { continuousValue, timeUnitEnum } from '@/views/rule-engine/Scene/Save/components/Timer/util'
import {continuousValue, handleTimerOptions, timeUnitEnum} from '@/views/rule-engine/Scene/Save/components/Timer/util'
type Emit = {
(e: 'cancel'): void
@ -161,46 +161,50 @@ const handleOptions = (data: TriggerDeviceOptions) => {
if (data.timer) {
const _timer = data.timer;
if (_timer.trigger === 'cron') {
_options.time = _timer.cron;
} else {
// console.log('continuousValue', continuousValue(_timer.when! || [], _timer!.trigger))
let whenStr = '每天';
if (_timer.when!.length) {
whenStr = _timer!.trigger === 'week' ? '每周' : '每月';
const whenStrArr = continuousValue(_timer.when! || [], _timer!.trigger);
const whenStrArr3 = whenStrArr.splice(0, 3);
whenStr += whenStrArr3.join('、');
whenStr += `${_timer.when!.length}`;
}
_options.when = whenStr;
if (_timer.once) {
_options.time = _timer.once.time + ' 执行1次';
} else if (_timer.period) {
_options.time = _timer.period.from + '-' + _timer.period.to;
_options.extraTime = `${_timer.period.every}${timeUnitEnum[_timer.period.unit]}执行1次`;
}
}
if (data.operator === 'online') {
_options.type = '上线';
_options.action = '';
_options.typeIcon = 'icon-a-Group4713';
}
if (data.operator === 'offline') {
_options.type = '离线';
_options.action = '';
_options.typeIcon = 'icon-a-Group4892';
}
if (data.operator === 'reportProperty') {
_options.type = '属性上报';
_options.action = '';
_options.typeIcon = 'icon-file-upload-outline';
}
return _options;
const { time, extraTime, when } = handleTimerOptions(_timer)
_options.when = when;
_options.time = time;
_options.extraTime = extraTime;
// if (_timer.trigger === 'cron') {
// _options.time = _timer.cron;
// } else {
// // console.log('continuousValue', continuousValue(_timer.when! || [], _timer!.trigger))
// let whenStr = '';
// if (_timer.when!.length) {
// whenStr = _timer!.trigger === 'week' ? '' : '';
// const whenStrArr = continuousValue(_timer.when! || [], _timer!.trigger);
// const whenStrArr3 = whenStrArr.splice(0, 3);
// whenStr += whenStrArr3.join('');
// whenStr += `${_timer.when!.length}`;
// }
// _options.when = whenStr;
// if (_timer.once) {
// _options.time = _timer.once.time + ' 1';
// } else if (_timer.period) {
// _options.time = _timer.period.from + '-' + _timer.period.to;
// _options.extraTime = `${_timer.period.every}${timeUnitEnum[_timer.period.unit]}1`;
// }
// }
}
if (data.operator === 'online') {
_options.type = '上线';
_options.action = '';
_options.typeIcon = 'icon-a-Group4713';
}
if (data.operator === 'offline') {
_options.type = '离线';
_options.action = '';
_options.typeIcon = 'icon-a-Group4892';
}
if (data.operator === 'reportProperty') {
_options.type = '属性上报';
_options.action = '';
_options.typeIcon = 'icon-file-upload-outline';
}
return _options;
}
const prev = () => {

View File

@ -6,7 +6,7 @@
class='search'
target="scene-triggrt-device-device"
/>
<a-divider style='margin: 0' />
<j-divider style='margin: 0' />
<j-pro-table
ref='actionRef'
model='CARD'
@ -43,20 +43,20 @@
{{ slotProps.name }}
</span>
</Ellipsis>
<a-row>
<a-col :span="12">
<j-row>
<j-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>{{ slotProps.deviceType?.text }}</div>
</a-col>
<a-col :span="12">
</j-col>
<j-col :span="12">
<div class="card-item-content-text">
产品名称
</div>
<div>{{ slotProps.productName }}</div>
</a-col>
</a-row>
</j-col>
</j-row>
</template>
</CardBox>
</template>
@ -172,7 +172,6 @@ const handleClick = (detail: any) => {
value: detail.id
})
}
console.log('cloneRowKeys', cloneRowKeys)
emit('update', cloneRowKeys)
}

View File

@ -1,12 +1,12 @@
<template>
<a-form ref='invokeForm' :model='formModel' layout='vertical' :colon='false'>
<a-row :gutter='24'>
<a-col :span='10'>
<a-form-item
<j-form ref='invokeForm' :model='formModel' layout='vertical' :colon='false'>
<j-row :gutter='24'>
<j-col :span='10'>
<j-form-item
name='functionId'
:rules="[{ required: true, message: '请选择功能' }]"
>
<a-select
<j-select
showSearch
allowClear
v-model:value='formModel.functionId'
@ -16,20 +16,20 @@
:filterOption='filterSelectNode'
@select='onSelect'
/>
</a-form-item>
</a-col>
<a-col :span='14'>
<a-form-item>定时调用所选功能</a-form-item>
</a-col>
<a-col :span='24'>
</j-form-item>
</j-col>
<j-col :span='14'>
<j-form-item>定时调用所选功能</j-form-item>
</j-col>
<j-col :span='24'>
<FunctionCall
:value='_value'
:data='functionData'
@change='callDataChange'
/>
</a-col>
</a-row>
</a-form>
</j-col>
</j-row>
</j-form>
</template>
<script setup lang='ts' name='InvokeFunction'>

View File

@ -6,7 +6,7 @@
class='search'
target="scene-triggrt-device-category"
/>
<a-divider style='margin: 0' />
<j-divider style='margin: 0' />
<j-pro-table
ref="instanceRef"
model='TABLE'

View File

@ -6,7 +6,7 @@
class='search'
target="scene-triggrt-device-device"
/>
<a-divider style='margin: 0' />
<j-divider style='margin: 0' />
<j-pro-table
ref='actionRef'
model='CARD'
@ -42,14 +42,14 @@
</span>
</Ellipsis>
</div>
<a-row>
<a-col :span="12">
<j-row>
<j-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>直连设备</div>
</a-col>
</a-row>
</j-col>
</j-row>
</template>
</CardBox>
</template>

View File

@ -1,11 +1,11 @@
<template>
<a-row :gutter='[24]'>
<a-col :span='10'>
<a-form-item
<j-row :gutter='[24]'>
<j-col :span='10'>
<j-form-item
name='readProperties'
:rules="[{ required: true, message: '请选择属性' }]"
>
<a-select
<j-select
show-search
mode='multiple'
max-tag-count='responsive'
@ -16,12 +16,12 @@
:filter-option='filterSelectNode'
@change='change'
/>
</a-form-item>
</a-col>
<a-col :span='14'>
<a-form-item>定时读取所选属性值</a-form-item>
</a-col>
</a-row>
</j-form-item>
</j-col>
<j-col :span='14'>
<j-form-item>定时读取所选属性值</j-form-item>
</j-col>
</j-row>
</template>
<script setup lang='ts' name='ReadProperties'>

View File

@ -1,7 +1,7 @@
<template>
<div class='type'>
<a-form ref='typeForm' :model='formModel' layout='vertical' :colon='false'>
<a-form-item
<j-form ref='typeForm' :model='formModel' layout='vertical' :colon='false'>
<j-form-item
required
label='触发类型'
>
@ -10,7 +10,7 @@
:options='topOptions'
v-model:value='formModel.operator'
/>
</a-form-item>
</j-form-item>
<template v-if='showTimer'>
<Timer ref='timerRef' v-model:value='formModel.timer' />
</template>
@ -27,12 +27,12 @@
v-model:action='optionCache.action'
:properties='writeProperties'
/>
<a-form-item
<j-form-item
v-if='showReportEvent'
name='eventId'
:rules="[{ required: true, message: '请选择事件' }]"
>
<a-select
<j-select
v-model:value='formModel.eventId'
:filter-option='filterSelectNode'
:options='eventOptions'
@ -40,7 +40,7 @@
style='width: 100%'
@select='eventSelect'
/>
</a-form-item>
</j-form-item>
<template v-if='showInvokeFunction'>
<InvokeFunction
ref='invokeRef'
@ -49,7 +49,7 @@
:functions='functionOptions'
/>
</template>
</a-form>
</j-form>
</div>
</template>

View File

@ -1,12 +1,12 @@
<template>
<a-form ref='writeForm' :model='formModel' layout='vertical' :colon='false'>
<a-row :futter='[24, 24]'>
<a-col :span='10'>
<a-form-item
<j-form ref='writeForm' :model='formModel' layout='vertical' :colon='false'>
<j-row :futter='[24, 24]'>
<j-col :span='10'>
<j-form-item
name='reportKey'
:rules="[{ required: true, message: '请输入修改值' }]"
>
<a-select
<j-select
showSearch
style='width: 100%'
placeholder='请选择属性'
@ -15,22 +15,22 @@
:filter-option='filterSelectNode'
@change='change'
/>
</a-form-item>
</a-col>
<a-col :span='14'>
</j-form-item>
</j-col>
<j-col :span='14'>
<span style='line-height: 32px;padding-left: 24px'>
定时调用所选属性
</span>
</a-col>
<a-col :span='24' v-if='showTable'>
</j-col>
<j-col :span='24' v-if='showTable'>
<FunctionCall
:value='_value'
:data='callDataOptions'
@change='callDataChange'
/>
</a-col>
</a-row>
</a-form>
</j-col>
</j-row>
</j-form>
</template>
<script setup lang='ts' name='WriteProperties'>

View File

@ -1,6 +1,6 @@
<template>
<div class='device'>
<a-form-item
<j-form-item
:rules='rules'
name='device'
>
@ -13,7 +13,7 @@
>
<Title :options='data.options.trigger' />
</AddButton>
</a-form-item>
</j-form-item>
<Terms />
<AddModel v-if='visible' @cancel='visible = false' @save='save' :value='data.trigger.device' :options='data.options.trigger' />
</div>
@ -25,7 +25,6 @@ import { useSceneStore } from '@/store/scene'
import AddModel from './AddModal.vue'
import AddButton from '../components/AddButton.vue'
import Title from '../components/Title.vue'
import Action from '../action/index.vue'
import Terms from '../components/Terms'
import type { TriggerDevice } from '@/views/rule-engine/Scene/typings'

View File

@ -0,0 +1,65 @@
<template>
<j-modal
title='触发规则'
visible
:width='820'
@click='save'
@cancel='cancel'
>
<Timer
ref='timerRef' v-model:value='addModel.timer'
/>
</j-modal>
</template>
<script setup lang="ts" name="timerAddModel">
import Timer from '../components/Timer'
import type { OperationTimer } from '@/views/rule-engine/Scene/typings'
import {nextTick, PropType} from "vue";
import {TriggerDevice} from "@/views/rule-engine/Scene/typings";
import {handleTimerOptions} from "@/views/rule-engine/Scene/Save/components/Timer/util";
type Emit = {
(e: 'cancel'): void
(e: 'save', data: TriggerDevice, options: Record<string, any>): void
}
const props = defineProps({
timer: {
type: Object as PropType<OperationTimer>,
default: () => ({})
}
})
const emit = defineEmits<Emit>()
const timerRef = ref()
interface AddModelType {
timer: OperationTimer
}
const addModel = reactive<AddModelType>({
timer: props.timer
})
const save = async () => {
const timerData = await timerRef.value?.validateFields()
if (timerData) {
const options = handleTimerOptions(timerData)
emit("save", timerData, options)
}
}
const cancel = () => {
emit("cancel")
}
nextTick(() => {
Object.assign(addModel, props.timer)
})
</script>
<style scoped>
</style>

View File

@ -1,28 +1,67 @@
<template>
<div>
<div class='timer'>
<j-form-item
:rules="rules"
name="timer"
>
<template #label>
<TitleComponent data='触发规则' style='font-size: 14px;' />
</template>
<AddButton
style='width: 100%'
@click='visible = true'
>
<Title :options='data.options.trigger' />
</AddButton>
</j-form-item>
<j-form-item
:rules="actionRules"
:name="['branches', 0, 'then']"
>
<Action
:thenOptions="data.branches ? data?.branches[0].then : []"
:name="0"
@add="onActionAdd"
@update="onActionUpdate"
/>
</j-form-item>
<AddModel
v-if="visible"
@cancel='visible = false'
@save="save"
:value="data.trigger.device"
:options="data.options.trigger"
/>
</div>
</template>
<script lang="ts" setup>
import { useSceneStore } from '@/store/scene';
import Action from '../action/index.vue';
import { storeToRefs } from 'pinia';
import Action from '../action/index.vue';
import AddModel from './AddModal.vue'
import AddButton from '../components/AddButton.vue'
import type { OperationTimer } from '@/views/rule-engine/Scene/typings'
const sceneStore = useSceneStore();
const { data } = storeToRefs(sceneStore);
const visible = ref(false)
const onActionAdd = (_data: any) => {
if (data.value?.branches && _data) {
const newThen = [...data.value.branches[0].then, _data];
data.value.branches![0].then = newThen
const rules = [{
validator(_: any, v: any) {
if (!v) {
return Promise.reject(new Error('请配置定时触发规则'));
}
};
return Promise.resolve();
},
}]
const actionRules = [{
validator(_, v) {
if (!v || (v && !v.length)) {
return Promise.reject('至少配置一个执行动作');
}
return Promise.resolve();
},
}]
const onActionUpdate = (_data: any, type: boolean) => {
const indexOf = data.value.branches![0].then.findIndex(
@ -36,6 +75,11 @@ const onActionUpdate = (_data: any, type: boolean) => {
}
}
};
const save = (data: OperationTimer, options: Record<string, any>) => {
data.value.trigger!.timer = data
data.value.options!.trigger = options
}
</script>
<style scoped>

View File

@ -12,7 +12,7 @@
</template>
<template v-if='column.dataIndex === "type"'>
{{ record.type }}
<a-tooltip
<j-tooltip
v-if="record.type === 'object'"
>
<template slot="title">
@ -26,7 +26,7 @@
cursor: 'help',
}"
/>
</a-tooltip>
</j-tooltip>
</template>
<template v-if='column.dataIndex === "value"'>
<ValueItem

View File

@ -1,12 +1,12 @@
<template>
<a-form
<j-form
ref='timerForm'
:model='formModel'
layout='vertical'
:colon='false'
>
<a-form-item name='trigger'>
<a-radio-group
<j-form-item name='trigger'>
<j-radio-group
v-model:value='formModel.trigger'
:options='[
{ label: "按周", value: "week" },
@ -16,8 +16,8 @@
option-type='button'
button-style='solid'
/>
</a-form-item>
<a-form-item v-if='showCron' name='cron' :rules="[
</j-form-item>
<j-form-item v-if='showCron' name='cron' :rules="[
{ max: 64, message: '最多可输入64个字符' },
{
validator: async (_, v) => {
@ -32,14 +32,14 @@
}
}
]">
<a-input placeholder='corn表达式' v-model:value='formModel.cron' />
</a-form-item>
<j-input placeholder='corn表达式' v-model:value='formModel.cron' />
</j-form-item>
<template v-else>
<a-form-item name='when'>
<j-form-item name='when'>
<WhenOption v-model:value='formModel.when' :type='formModel.trigger' />
</a-form-item>
<a-form-item name='mod'>
<a-radio-group
</j-form-item>
<j-form-item name='mod'>
<j-radio-group
v-model:value='formModel.mod'
:options='[
{ label: "周期执行", value: "period" },
@ -48,18 +48,18 @@
option-type='button'
button-style='solid'
/>
</a-form-item>
</j-form-item>
</template>
<a-space v-if='showOnce' style='display: flex;gap: 24px'>
<a-form-item :name="['once', 'time']">
<a-time-picker valueFormat='HH:mm:ss' v-model:value='formModel.once.time' style='width: 100%'
<j-space v-if='showOnce' style='display: flex;gap: 24px'>
<j-form-item :name="['once', 'time']">
<j-time-picker valueFormat='HH:mm:ss' v-model:value='formModel.once.time' style='width: 100%'
format='HH:mm:ss' />
</a-form-item>
<a-form-item> 执行一次</a-form-item>
</a-space>
<a-space v-if='showPeriod' style='display: flex;gap: 24px'>
<a-form-item>
<a-time-range-picker
</j-form-item>
<j-form-item> 执行一次</j-form-item>
</j-space>
<j-space v-if='showPeriod' style='display: flex;gap: 24px'>
<j-form-item>
<j-time-range-picker
valueFormat='HH:mm:ss'
:value='[
formModel.period.from,
@ -70,13 +70,13 @@
formModel.period.to = v[1]
}'
/>
</a-form-item>
<a-form-item></a-form-item>
<a-form-item
</j-form-item>
<j-form-item></j-form-item>
<j-form-item
:name='["period", "every"]'
:rules='[{ required: true, message: "请输入时间" }]'
>
<a-input-number
<j-input-number
placeholder='请输入时间'
style='max-width: 170px'
:precision='0'
@ -85,7 +85,7 @@
v-model:value='formModel.period.every'
>
<template #addonAfter>
<a-select
<j-select
v-model:value='formModel.period.unit'
:options='[
{ label: "秒", value: "seconds" },
@ -94,11 +94,11 @@
]'
/>
</template>
</a-input-number>
</a-form-item>
<a-form-item>执行一次</a-form-item>
</a-space>
</a-form>
</j-input-number>
</j-form-item>
<j-form-item>执行一次</j-form-item>
</j-space>
</j-form>
</template>
<script setup lang='ts' name='Timer'>

View File

@ -1,4 +1,5 @@
import { isArray } from 'lodash-es'
import type { OperationTimer } from "@/views/rule-engine/Scene/typings";
export const numberToString = {
1: '星期一',
2: '星期二',
@ -49,4 +50,42 @@ export const continuousValue: continuousValueFn = (data, type) => {
});
}
return newArray;
};
};
type TimerOption = {
when?: string
time?: string
extraTime?: string
}
export const handleTimerOptions = (timer: OperationTimer):TimerOption => {
let when = '每天'
let time = undefined
let extraTime = undefined
if (timer.trigger === 'cron') {
time = timer.cron
return { time }
}
if (timer.when?.length) {
when = timer!.trigger === 'week' ? '每周' : '每月';
const whenStrArr = continuousValue(timer.when! || [], timer!.trigger);
const whenStrArr3 = whenStrArr.splice(0, 3);
when += whenStrArr3.join('、');
when += `${timer.when!.length}`;
}
if (timer.once) {
time = timer.once.time + ' 执行1次';
} else if (timer.period) {
time = timer.period.from + '-' + timer.period.to;
extraTime = `${timer.period.every}${timeUnitEnum[timer.period.unit]}执行1次`;
}
return {
when,
time,
extraTime
}
}

View File

@ -12,9 +12,9 @@
>
<div class='way-item-title'>
<span class='label'>{{ item.label }}</span>
<a-popover v-if='item.tip' :content='item.tip'>
<j-popover v-if='item.tip' :content='item.tip'>
<AIcon type='QuestionCircleOutlined' class='icon' />
</a-popover>
</j-popover>
</div>
<div class='way-item-image'>
<img

View File

@ -10,11 +10,20 @@
{{ keyByLabel[data.triggerType] }}
</div>
</div>
<a-form ref='sceneForm' :model='data' :colon='false' layout='vertical'>
<j-form ref='sceneForm' :model='data' :colon='false' layout='vertical'>
<Device v-if='data.triggerType === "device"' />
<Manual v-else-if='data.triggerType === "manual"' />
<Timer v-else-if='data.triggerType === "timer"' />
</a-form>
<j-form-item>
<j-textarea
v-model:value="data.description"
placeholder=''
:rows="4"
show-count
:maxLength="200"
/>
</j-form-item>
</j-form>
<PermissionButton
type='primary'
hasPermission='rule-engine/Scene:update'

View File

@ -1,5 +1,5 @@
<template>
<a-modal
<j-modal
visible
:title='title'
:width='750'
@ -8,13 +8,13 @@
@cancel='emit("close")'
@ok='handleOk'
>
<a-form
<j-form
layout='vertical'
name='scene-save'
ref="formRef"
:model='formModel'
>
<a-form-item
<j-form-item
name='name'
label='名称'
:rules="[
@ -22,17 +22,17 @@
{ max: 64, message: '最多输入64个字符' }
]"
>
<a-input v-model:value='formModel.name' placeholder='请输入名称' />
</a-form-item>
<a-form-item
<j-input v-model:value='formModel.name' placeholder='请输入名称' />
</j-form-item>
<j-form-item
:name='["trigger", "type"]'
label='触发方式'
:rules="[{ required: true, message: '请选择触发方式' }]"
>
<TriggerWay v-model:modelValue='formModel.trigger.type' :disabled='disabled' />
</a-form-item>
</a-form>
</a-modal>
</j-form-item>
</j-form>
</j-modal>
</template>
<script setup lang='ts'>

View File

@ -46,4 +46,4 @@ export const useParams = (params: Params) => {
return {
columnOptions
}
}
}

View File

@ -290,7 +290,7 @@ import { message } from 'ant-design-vue';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { LocalStore } from '@/utils/comm';
import { save_api, getDetails_api } from '@/api/system/basis';
import { save_api } from '@/api/system/basis';
import { usePermissionStore } from '@/store/permission';
import { useSystem } from '@/store/system';

View File

@ -21,56 +21,59 @@
</PermissionButton>
</div>
<jTree
:tree-data="treeData"
v-model:selected-keys="selectedKeys"
:fieldNames="{ key: 'id' }"
v-loading="loading"
>
<template #title="{ name, data }">
<span>{{ name }}</span>
<span class="func-btns" @click="(e) => e.stopPropagation()">
<PermissionButton
:hasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '编辑',
}"
@click="openDialog(data)"
>
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton
:hasPermission="`${permission}:add`"
type="link"
:tooltip="{
title: '新增子组织',
}"
@click="
openDialog({
...data,
id: '',
parentId: data.id,
})
"
>
<AIcon type="PlusCircleOutlined" />
</PermissionButton>
<PermissionButton
type="link"
:hasPermission="`${permission}:delete`"
:tooltip="{ title: '删除' }"
:popConfirm="{
title: `确定要删除吗`,
onConfirm: () => delDepartment(data.id),
}"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</span>
</template>
</jTree>
<div class="tree">
<jTree
v-if="treeData.length > 0"
:tree-data="treeData"
v-model:selected-keys="selectedKeys"
:fieldNames="{ key: 'id' }"
>
<template #title="{ name, data }">
<span>{{ name }}</span>
<span class="func-btns" @click="(e) => e.stopPropagation()">
<PermissionButton
:hasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '编辑',
}"
@click="openDialog(data)"
>
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton
:hasPermission="`${permission}:add`"
type="link"
:tooltip="{
title: '新增子组织',
}"
@click="
openDialog({
...data,
id: '',
parentId: data.id,
})
"
>
<AIcon type="PlusCircleOutlined" />
</PermissionButton>
<PermissionButton
type="link"
:hasPermission="`${permission}:delete`"
:tooltip="{ title: '删除' }"
:popConfirm="{
title: `确定要删除吗`,
onConfirm: () => delDepartment(data.id),
}"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</span>
</template>
</jTree>
<div class="loading" v-else-if="loading"><j-spin /></div>
<j-empty v-else description="暂无数据" />
</div>
<!-- 编辑弹窗 -->
<EditDepartmentDialog
v-if="dialog.visible"
@ -193,9 +196,11 @@ const openDialog = (row: any = {}) => {
if (row.parentId) {
childrens = row.children;
} else childrens = treeData.value;
const indexs = childrens.length > 0 ? childrens?.map((item) => item.sortIndex) :[0]
sortIndex =
Math.max(...indexs) + 1;
const indexs =
childrens.length > 0
? childrens?.map((item) => item.sortIndex)
: [0];
sortIndex = Math.max(...indexs) + 1;
}
dialog.selectItem = { ...row, sortIndex };
@ -248,5 +253,11 @@ function init() {
}
}
}
.loading {
display: flex;
justify-content: center;
margin-top: 20px;
}
}
</style>

View File

@ -91,6 +91,16 @@ import 'vue3-json-viewer/dist/index.css';
import type { apiDetailsType } from '../typing';
import InputCard from './InputCard.vue';
import { PropType } from 'vue';
import { findData, getCodeText } from '../utils';
type cardType = {
columns: object[];
tableData: any[];
codeText?: any;
activeKey?: string;
getData: Function;
};
const props = defineProps({
selectApi: {
@ -104,14 +114,8 @@ const props = defineProps({
});
const { selectApi } = toRefs(props);
type tableCardType = {
columns: object[];
tableData: any[];
codeText?: any;
activeKey?: any;
getData?: any;
};
const requestCard = reactive<tableCardType>({
const requestCard = reactive<cardType>({
columns: [
{
title: '参数名',
@ -149,16 +153,16 @@ const requestCard = reactive<tableCardType>({
const schema =
props.selectApi.requestBody.content['application/json'].schema;
const _ref = schema.$ref || schema?.items?.$ref;
if(!_ref) return; // schemaJava
if (!_ref) return; // schemaJava
const schemaName = _ref?.split('/').pop();
const type = schema.type || '';
const tableData = findData(schemaName);
if (type === 'array') {
requestCard.codeText = [getCodeText(tableData, 3)];
} else requestCard.codeText = getCodeText(tableData, 3);
// console.clear();
// console.log(schemaName, tableData);
const tableData = findData(props.schemas, schemaName);
requestCard.codeText =
type === 'array'
? [getCodeText(props.schemas, tableData, 3)]
: getCodeText(props.schemas, tableData, 3);
requestCard.tableData = [
{
name: schemaName[0].toLowerCase() + schemaName.substring(1),
@ -176,7 +180,7 @@ const requestCard = reactive<tableCardType>({
];
},
});
const responseStatusCard = reactive<tableCardType>({
const responseStatusCard = reactive<cardType>({
activeKey: '',
columns: [
{
@ -220,7 +224,7 @@ const tabs = computed(() =>
.map((item: any) => item.code + '')
.filter((code: string) => code !== '400'),
);
const respParamsCard = reactive<tableCardType>({
const respParamsCard = reactive<cardType>({
columns: [
{
title: '参数名称',
@ -242,9 +246,8 @@ const respParamsCard = reactive<tableCardType>({
(item: any) => item.code === code,
)?.schema;
const tableData = findData(schemaName);
respParamsCard.codeText = getCodeText(tableData, 3);
const tableData = findData(props.schemas, schemaName);
respParamsCard.codeText = getCodeText(props.schemas, tableData, 3);
respParamsCard.tableData = tableData;
},
});
@ -258,95 +261,18 @@ const getContent = (data: any) => {
onMounted(() => {
requestCard.getData();
responseStatusCard.getData();
});
watch(
() => props.selectApi,
() => {
requestCard.getData();
responseStatusCard.getData();
},
);
watch(
() => props.selectApi,
() => {
requestCard.getData();
responseStatusCard.getData();
},
);
watch([() => responseStatusCard.activeKey, () => props.selectApi], (n) => {
n[0] && respParamsCard.getData(n[0]);
});
function findData(schemaName: string) {
const schemas = toRaw(props.schemas);
const basicType = ['string', 'integer', 'boolean'];
if (!schemaName || !schemas[schemaName]) {
return [];
}
const result: schemaObjType[] = [];
const schema = schemas[schemaName];
Object.entries(schema.properties).forEach((item: [string, any]) => {
const paramsType =
item[1].type ||
(item[1].$ref && item[1].$ref.split('/').pop()) ||
(item[1].items && item[1].items.$ref.split('/').pop()) ||
'';
const schemaObj: schemaObjType = {
paramsName: item[0],
paramsType,
desc: item[1].description || '',
};
if (!basicType.includes(paramsType))
schemaObj.children = findData(paramsType);
result.push(schemaObj);
watch([() => responseStatusCard.activeKey, () => props.selectApi], (n) => {
n[0] && respParamsCard.getData(n[0]);
});
return result;
}
function getCodeText(arr: schemaObjType[], level: number): object {
const result = {};
const schemas = toRaw(props.schemas);
arr.forEach((item) => {
switch (item.paramsType) {
case 'string':
result[item.paramsName] = '';
break;
case 'integer':
result[item.paramsName] = 0;
break;
case 'boolean':
result[item.paramsName] = true;
break;
case 'array':
result[item.paramsName] = [];
break;
case 'object':
result[item.paramsName] = '';
break;
default: {
const properties = schemas[item.paramsType]
.properties as object;
const newArr = Object.entries(properties).map(
(item: [string, any]) => ({
paramsName: item[0],
paramsType: level
? (item[1].$ref && item[1].$ref.split('/').pop()) ||
(item[1].items &&
item[1].items.$ref.split('/').pop()) ||
item[1].type ||
''
: item[1].type,
}),
);
result[item.paramsName] = getCodeText(newArr, level - 1);
}
}
});
return result;
}
type schemaObjType = {
paramsName: string;
paramsType: string;
desc?: string;
children?: schemaObjType[];
};
});
</script>
<style lang="less" scoped>

View File

@ -121,6 +121,7 @@ import InputCard from './InputCard.vue';
import { cloneDeep, toLower } from 'lodash';
import { FormInstance } from 'ant-design-vue';
import server from '@/utils/request';
import { findData, getCodeText } from '../utils';
const props = defineProps<{
selectApi: apiDetailsType;
@ -238,92 +239,17 @@ function getDefaultParams() {
if (!refStr.value) return ''; // schemaJava
const schemaName = refStr.value?.split('/').pop() as string;
const type = schema.type || '';
const tableData = findData(schemaName);
if (type === 'array') {
return [getCodeText(tableData, 3)];
} else return getCodeText(tableData, 3);
}
const tableData = findData(props.schemas, schemaName);
function findData(schemaName: string) {
const schemas = toRaw(props.schemas);
const basicType = ['string', 'integer', 'boolean'];
if (!schemaName || !schemas[schemaName]) {
return [];
}
const result: schemaObjType[] = [];
const schema = schemas[schemaName];
Object.entries(schema.properties).forEach((item: [string, any]) => {
const paramsType =
item[1].type ||
(item[1].$ref && item[1].$ref.split('/').pop()) ||
(item[1].items && item[1].items.$ref.split('/').pop()) ||
'';
const schemaObj: schemaObjType = {
paramsName: item[0],
paramsType,
desc: item[1].description || '',
};
if (!basicType.includes(paramsType))
schemaObj.children = findData(paramsType);
result.push(schemaObj);
});
return result;
}
function getCodeText(arr: schemaObjType[], level: number): object {
const result = {};
const schemas = toRaw(props.schemas);
arr.forEach((item) => {
switch (item.paramsType) {
case 'string':
result[item.paramsName] = '';
break;
case 'integer':
result[item.paramsName] = 0;
break;
case 'boolean':
result[item.paramsName] = true;
break;
case 'array':
result[item.paramsName] = [];
break;
case 'object':
result[item.paramsName] = '';
break;
default: {
const properties = schemas[item.paramsType]
.properties as object;
const newArr = Object.entries(properties).map(
(item: [string, any]) => ({
paramsName: item[0],
paramsType: level
? (item[1].$ref && item[1].$ref.split('/').pop()) ||
(item[1].items &&
item[1].items.$ref.split('/').pop()) ||
item[1].type ||
''
: item[1].type,
}),
);
result[item.paramsName] = getCodeText(newArr, level - 1);
}
}
});
return result;
return type === 'array'
? [getCodeText(props.schemas, tableData, 3)]
: getCodeText(props.schemas, tableData, 3);
}
type requestObj = {
name: string;
value: string;
};
type schemaObjType = {
paramsName: string;
paramsType: string;
desc?: string;
children?: schemaObjType[];
};
</script>
<style lang="less" scoped>

View File

@ -29,4 +29,11 @@ export type apiDetailsType = {
* appManger -
* home-
*/
export type modeType = 'api'| 'appManger' | 'home'
export type modeType = 'api'| 'appManger' | 'home'
export type schemaObjType = {
paramsName: string;
paramsType: string;
desc?: string;
children?: schemaObjType[];
};

View File

@ -0,0 +1,90 @@
import { schemaObjType } from "./typing";
/**
*
* @param schemas map
* @param schemaName
*/
export function findData(schemas: object, schemaName: string) {
const basicType = ['string', 'integer', 'boolean'];
if (!schemaName || !schemas[schemaName]) {
return [];
}
const result: schemaObjType[] = [];
const schema = schemas[schemaName];
Object.entries(schema.properties).forEach((item: [string, any]) => {
const paramsType =
item[1].type ||
(item[1].$ref && item[1].$ref.split('/').pop()) ||
(item[1].items && item[1].items.$ref.split('/').pop()) ||
'';
const schemaObj: schemaObjType = {
paramsName: item[0],
paramsType,
desc: item[1].description || '',
};
if (!basicType.includes(paramsType))
schemaObj.children = findData(schemas, paramsType);
result.push(schemaObj);
});
return result;
}
/**
* JSON代码
* @param schemas
* @param arr
* @param level
*/
export function getCodeText(
schemas: object,
arr: schemaObjType[],
level: number,
): object {
const result = {};
arr.forEach((item) => {
switch (item.paramsType) {
case 'string':
result[item.paramsName] = '';
break;
case 'integer':
result[item.paramsName] = 0;
break;
case 'boolean':
result[item.paramsName] = true;
break;
case 'array':
result[item.paramsName] = [];
break;
case 'object':
result[item.paramsName] = '';
break;
default: {
const properties = schemas[item.paramsType]
.properties as object;
const newArr = Object.entries(properties).map(
(item: [string, any]) => ({
paramsName: item[0],
paramsType: level
? (item[1].$ref && item[1].$ref.split('/').pop()) ||
(item[1].items &&
item[1].items.$ref.split('/').pop()) ||
item[1].type ||
''
: item[1].type,
}),
);
result[item.paramsName] = getCodeText(
schemas,
newArr,
level - 1,
);
}
}
});
return result;
}

View File

@ -92,7 +92,7 @@
}"
:popConfirm="{
title: `确认删除`,
onConfirm: () => table.clickDel(slotProps),
onConfirm: () => table.clickDel(slotProps.id),
}"
:disabled="slotProps.status"
>
@ -223,10 +223,10 @@ const table = {
dialog.type = type;
dialog.visible = true;
},
changeStatus: (row: any) => {
changeStatus: ({ id, status }: any) => {
const params = {
id: row.id,
status: row.status === 0 ? 1 : 0,
status: status === 0 ? 1 : 0,
id,
};
changeUserStatus_api(params).then(() => {
message.success('操作成功');
@ -234,8 +234,8 @@ const table = {
});
},
//
clickDel: (row: any) => {
deleteUser_api(row.id).then(() => {
clickDel: (id: string) => {
deleteUser_api(id).then(() => {
message.success('操作成功');
table.refresh();
});

View File

@ -3709,6 +3709,18 @@ jetlinks-ui-components@^1.0.4:
lodash-es "^4.17.21"
monaco-editor "^0.35.0"
jetlinks-ui-components@^1.0.5:
version "1.0.5"
resolved "http://47.108.170.157:9013/jetlinks-ui-components/-/jetlinks-ui-components-1.0.5.tgz#0c4999d28c96c11ce266c5c9706cc895010450dc"
integrity sha512-buCf4mWJ8cUyn+12nRRLIr25MwG60nxqWH4pZidKy/npNKt5WQXLV8PmHmf04z0xpJUnW5yY3C7QBkYoAkSgVw==
dependencies:
"@vueuse/core" "^9.12.0"
ant-design-vue "^3.2.15"
colorpicker-v3 "^2.10.2"
jetlinks-ui-components "^1.0.4"
lodash-es "^4.17.21"
monaco-editor "^0.35.0"
js-cookie@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz"