fix: 运行状态操作

This commit is contained in:
100011797 2023-02-02 17:55:36 +08:00
parent 2c6e4173f8
commit fa00834c95
9 changed files with 500 additions and 90 deletions

View File

@ -175,3 +175,45 @@ export const delTags = (deviceId: string, id: string) => server.remove(`/device/
* @returns
*/
export const configurationReset = (deviceId: string) => server.put(`/device-instance/${deviceId}/configuration/_reset`)
/**
*
* @param deviceId id
* @param eventId id
* @param data
* @returns
*/
export const getEventList = (deviceId: string, eventId: string, data: Record<string, any>) => server.post(`/device-instance/${deviceId}/event/${eventId}?format=true`, data)
/**
*
* @param deviceId id
* @param data
* @returns
*/
export const setProperty = (deviceId: string, data: Record<string, any>) => server.put(`/device-instance/${deviceId}/property`, data)
/**
*
* @param deviceId id
* @param type id
* @returns
*/
export const getProperty = (deviceId: string, type: string) => server.get(`/device/standard/${deviceId}/property/${type}`)
/**
*
* @param deviceId id
* @param propertyId id
* @returns
*/
export const queryMetric = (deviceId: string, propertyId: string) => server.get(`/device-instance/${deviceId}/metric/property/${propertyId}`)
/**
*
* @param deviceId id
* @param propertyId id
* @param data
* @returns
*/
export const saveMetric = (deviceId: string, propertyId: string, data: Record<string, any>) => server.patch(`/device-instance/${deviceId}/metric/property/${propertyId}`, data)

View File

@ -74,7 +74,8 @@ const JTable = defineComponent<JTableProps>({
slots: [
'headerTitle', // 顶部左边插槽
'card', // 卡片内容
'rightExtraRender'
'rightExtraRender',
'paginationRender' // 分页
],
emits: [
'modelChange', // 切换卡片和表格
@ -354,6 +355,9 @@ const JTable = defineComponent<JTableProps>({
{
(!!_dataSource.value.length) && !props.noPagination && props.type === 'PAGE' &&
<div class={styles['jtable-pagination']}>
{
slots?.paginationRender ?
slots.paginationRender() :
<Pagination
size="small"
total={total.value}
@ -375,6 +379,7 @@ const JTable = defineComponent<JTableProps>({
})
}}
/>
}
</div>
}
</div>

View File

@ -3,7 +3,7 @@
<JTable
ref="eventsRef"
:columns="columns"
:dataSource="dataSource"
:request="_getEventList"
model="TABLE"
:bodyStyle="{padding: '0 24px'}"
>
@ -16,16 +16,24 @@
</a-button>
</template>
</JTable>
<a-button type="link" @click="detail(slotProps)">
<AIcon type="SearchOutlined" />
</a-button>
</template>
<script lang="ts" setup>
import moment from 'moment'
import { getEventList } from '@/api/device/instance'
import { useInstanceStore } from '@/store/instance'
import { Modal } from 'ant-design-vue'
const events = defineProps({
data: {
type: Object,
default: () => {}
}
})
const instanceStore = useInstanceStore()
const columns = ref<Record<string, any>>([
{
@ -41,8 +49,9 @@ const columns = ref<Record<string, any>>([
scopedSlots: true,
}
])
const params = ref<Record<string, any>>({})
const dataSource = ref<Record<string, any>[]>([])
const _getEventList = () => getEventList(instanceStore.current.id || '', events.data.id || '', params.value)
watchEffect(() => {
if(events.data?.valueType?.type === 'object'){
@ -50,8 +59,7 @@ watchEffect(() => {
columns.value.splice(0, 0, {
key: i.id,
title: i.name,
dataIndex: `${i.id}_format`,
// renderText: (text) => (typeof text === 'object' ? JSON.stringify(text) : text),
dataIndex: `${i.id}_format`
})
})
} else {
@ -63,6 +71,13 @@ watchEffect(() => {
})
const detail = () => {
Modal.info({
title: () => '详情',
width: 850,
content: () => h('div', {}, [
h('p', '暂未开发'),
]),
okText: '关闭'
});
}
</script>

View File

@ -0,0 +1,156 @@
<template>
<a-modal
:maskClosable="false"
:visible="true"
title="编辑指标"
@ok="handleSave"
@cancel="handleCancel"
:confirmLoading="loading"
>
<a-alert message="场景联动页面可引用指标配置触发条件" type="warning" showIcon />
<a-form layout="vertical" ref="formRef" :model="modelRef" style="margin-top: 20px">
<template v-for="(item, index) in modelRef.metrics" :key="index">
<a-row type="flex" justify="space-between" align="bottom">
<a-col :span="11">
<a-form-item
:rules="{
required: true,
message: `${['date', 'boolean'].includes(data?.valueType?.type)? '选择': '输入'}指标值`,
}"
:name="['metrics', index, 'value', 0]"
:label="item?.name || '指标值'"
>
<ValueItem
v-model:modelValue="item.value[0]"
:itemType="data.valueType?.type"
:options="
data.valueType?.type === 'boolean'
? [
{
label: data.valueType?.trueText,
value: String(data.valueType?.trueValue),
},
{
label: data.valueType?.falseText,
value: String(data.valueType?.falseValue),
},
]
: undefined
"
/>
</a-form-item>
</a-col>
<template v-if="item.range">
<a-col><div class="center-icon">~</div></a-col>
<a-col :span="11">
<a-form-item
:name="['metrics', index, 'value', 1]"
:rules="{
required: true,
message: `${['date', 'boolean'].includes(data?.valueType?.type)? '选择': '输入'}指标值`,
}"
>
<ValueItem
v-model:modelValue="item.value[1]"
:itemType="data.valueType?.type"
/>
</a-form-item>
</a-col>
</template>
</a-row>
</template>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { queryMetric, saveMetric } from '@/api/device/instance'
const emit = defineEmits(['close']);
import { useInstanceStore } from "@/store/instance"
import { message } from 'ant-design-vue';
const props = defineProps({
data: {
type: Object,
default: () => {}
}
})
const loading = ref<boolean>(false)
const instanceStore = useInstanceStore()
const formRef = ref();
const modelRef = reactive({
metrics: []
});
const handleCancel = () => {
emit('close')
}
watch(() => props.data.id, (newVal) => {
if(newVal && instanceStore.current.id){
queryMetric(instanceStore.current.id, props.data.id).then(resp => {
if (resp.status === 200) {
if (Array.isArray(resp?.result) && resp?.result.length) {
const list = resp?.result.map((item: any) => {
const val = Array.isArray(item?.value) ? [item?.value] : item?.value?.split(',')
return {
...item,
value: val
};
});
modelRef.metrics = list as any
} else {
const type = props.data.valueType?.type;
if (type === 'boolean') {
const list = props.data.expands?.metrics.map((item: any) => {
const value = (item?.value || {}).map((i: any) => String(i)) || {};
return {
...item,
value,
};
});
modelRef.metrics = list || []
} else {
modelRef.metrics = props.data.expands?.metrics || []
}
}
}
})
}
}, {immediate: true, deep: true})
const handleSave = () => {
formRef.value
.validate()
.then(async () => {
loading.value = true;
const list = (toRaw(modelRef)?.metrics || []).map((item: any) => {
return {
...item,
value: item.value.join(','),
};
});
const resp = await saveMetric(instanceStore.current.id || '', props.data.id || '', list).finally(() => {
loading.value = false
})
if (resp.status === 200) {
message.success('操作成功!');
emit('close')
formRef.value.resetFields();
}
})
.catch((err: any) => {
console.log('error', err);
});
}
</script>
<style lang="less" scoped>
.center-icon {
height: 86px;
display: flex;
align-items: center;
}
</style>

View File

@ -5,33 +5,20 @@
<div class="header">
<div class="title">{{ _props.data.name }}</div>
<div class="extra">
<a-space>
<a-tooltip title="设置属性至设备" v-if="data.expands?.type?.includes('write')">
<AIcon
type="EditOutlined"
style="font-size: 12px"
/>
</a-tooltip>
<a-tooltip title="指标" v-if="(data.expands?.metrics || []).length > 0 &&
['int', 'long', 'float', 'double', 'string', 'boolean', 'date'].includes(
data.valueType?.type || '',
)">
<AIcon
type="ClockCircleOutlined"
style="font-size: 12px"
/>
</a-tooltip>
<a-tooltip title="获取最新属性值" v-if="data.expands?.type?.includes('read')">
<AIcon
type="SyncOutlined"
style="font-size: 12px"
/>
</a-tooltip>
<a-tooltip title="详情">
<AIcon
type="BarsOutlined"
style="font-size: 12px"
/>
<a-space :size="16">
<a-tooltip
v-for="i in actions"
:key="i.key"
v-bind="i.tooltip"
>
<a-button
style="padding: 0; margin: 0"
type="link"
:disabled="i.disabled"
@click="i.onClick && i.onClick(data)"
>
<AIcon :type="i.icon" style="color: #323130; font-size: 12px" />
</a-button>
</a-tooltip>
</a-space>
</div>
@ -55,6 +42,10 @@ const _props = defineProps({
type: Object,
default: () => {},
},
actions: {
type: Array,
default: () => []
},
});
const loading = ref<boolean>(true);

View File

@ -0,0 +1,85 @@
<template>
<a-modal
:maskClosable="false"
:visible="true"
title="编辑"
@ok="handleSave"
@cancel="handleCancel"
:confirmLoading="loading"
>
<a-alert message="当数据来源为设备时,填写的值将下发到设备" type="warning" showIcon />
<a-form :rules="rules" layout="vertical" ref="formRef" :model="modelRef" style="margin-top: 20px">
<a-form-item name="propertyValue" :label="data?.name || '自定义属性'">
<ValueItem
v-model:modelValue="modelRef.propertyValue"
:itemType="data?.valueType?.type || data?.dataType"
:options="
(data?.valueType?.type || data?.dataType) === 'enum'
? (data?.valueType?.elements || []).map((item) => {
return {
label: item?.text,
value: item?.value
};
})
: undefined
"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { setProperty } from '@/api/device/instance'
const emit = defineEmits(['close']);
import { useInstanceStore } from "@/store/instance"
import { message } from 'ant-design-vue';
const props = defineProps({
data: {
type: Object,
default: () => {}
}
})
const loading = ref<boolean>(false)
const instanceStore = useInstanceStore()
const formRef = ref();
const modelRef = reactive({
propertyValue: undefined
});
const handleCancel = () => {
emit('close')
}
const rules = {
propertyValue: [
{
required: true,
message: '该字段是必填字段',
}
],
}
const handleSave = () => {
formRef.value
.validate()
.then(async () => {
loading.value = true;
const resp = await setProperty(instanceStore.current?.id || '', {[props.data?.id]: toRaw(modelRef)?.propertyValue}).finally(() => {
loading.value = false
})
if (resp.status === 200) {
message.success('操作成功!');
emit('close')
formRef.value.resetFields();
}
})
.catch((err: any) => {
console.log('error', err);
});
}
</script>

View File

@ -1,14 +1,74 @@
<template>
<div>
{{data.value || '--'}}
<div class="value">
{{value}}
</div>
</template>
<script lang="ts" setup>
import { getImage } from "@/utils/comm";
const _data = defineProps({
data: {
type: Object,
default: () => {},
},
value: {
type: [Object, String, Number],
default: '--'
},
type: {
type: String,
default: 'card'
}
});
const imgMap = new Map<any, any>();
imgMap.set('txt', getImage('/running/txt.png'));
imgMap.set('doc', getImage('/running/doc.png'));
imgMap.set('xls', getImage('/running/xls.png'));
imgMap.set('ppt', getImage('/running/ppt.png'));
imgMap.set('docx', getImage('/running/docx.png'));
imgMap.set('xlsx', getImage('/running/xlsx.png'));
imgMap.set('pptx', getImage('/running/pptx.png'));
imgMap.set('pdf', getImage('/running/pdf.png'));
imgMap.set('img', getImage('/running/img.png'));
imgMap.set('error', getImage('/running/error.png'));
imgMap.set('video', getImage('/running/video.png'));
imgMap.set('other', getImage('/running/other.png'));
imgMap.set('obj', getImage('/running/obj.png'));
const imgList = ['.jpg', '.png', '.swf', '.tiff'];
const videoList = ['.m3u8', '.flv', '.mp4', '.rmvb', '.mvb'];
const fileList = ['.txt', '.doc', '.xls', '.pdf', '.ppt', '.docx', '.xlsx', '.pptx'];
</script>
<style lang="less" scoped>
.value {
display: flex;
align-items: center;
width: 100%;
.cardValue {
display: flex;
align-items: center;
width: 100%;
height: 60px;
overflow: hidden;
color: #323130;
font-weight: 700;
font-size: 24px;
white-space: nowrap;
text-overflow: ellipsis;
img {
width: 60px;
}
}
.otherValue {
img {
width: 40px;
}
}
}
</style>

View File

@ -1,19 +1,20 @@
<template>
<JTable
ref="metadataRef"
:columns="columns"
:dataSource="dataSource"
:bodyStyle="{padding: 0}"
:bodyStyle="{padding: '0 0 0 20px'}"
>
<template #headerTitle>
<a-input-search
placeholder="请输入名称"
style="width: 300px; margin-bottom: 10px"
@search="onSearch"
v-model:value="value"
:allowClear="true"
/>
</template>
<template #card="slotProps">
<PropertyCard :data="slotProps" />
<PropertyCard :data="slotProps" :actions="getActions(slotProps)" />
</template>
<template #value="slotProps">
<ValueRender :data="slotProps" />
@ -28,41 +29,45 @@
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
:disabled="i.disabled"
>
<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
:disabled="i.disabled"
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
<AIcon :type="i.icon" />
</a-button>
</a-tooltip>
</a-space>
</template>
<template #paginationRender>
<a-pagination
size="small"
:total="total"
:showQuickJumper="false"
:showSizeChanger="true"
:current="pageIndex + 1"
:pageSize="pageSize"
:pageSizeOptions="['8', '12', '24', '60', '100']"
:show-total="(num) => `第 ${pageIndex * pageSize + 1} - ${(pageIndex + 1) * pageSize > num ? num : (pageIndex + 1) * pageSize} 条/总共 ${num} 条`"
@change="pageChange"
/>
</template>
</JTable>
<Save v-if="editVisible" @close="editVisible = false" :data="currentInfo" />
<Indicators v-if="indicatorVisible" @close="indicatorVisible = false" :data="currentInfo" />
</template>
<script lang="ts" setup>
import _ from "lodash"
import { PropertyData } from "../../../typings"
import PropertyCard from './PropertyCard.vue'
import ValueRender from './ValueRender.vue'
import Save from './Save.vue'
import Indicators from './Indicators.vue'
import { getProperty } from '@/api/device/instance'
import { useInstanceStore } from "@/store/instance"
import { message } from "ant-design-vue"
const columns = [
{
@ -96,8 +101,17 @@ const _data = defineProps({
default: () => []
}
})
const value = ref<string>('')
const dataSource = ref<PropertyData[]>([])
const _dataSource = ref<PropertyData[]>([])
const pageIndex = ref<number>(0)
const pageSize = ref<number>(8)
const total = ref<number>(0)
const editVisible = ref<boolean>(false) //
const detailVisible = ref<boolean>(false) //
const currentInfo = ref<Record<string, any>>({})
const instanceStore = useInstanceStore()
const indicatorVisible = ref<boolean>(false) //
const getActions = (data: Partial<Record<string, any>>) => {
const arr = []
@ -109,7 +123,8 @@ const getActions = (data: Partial<Record<string, any>>) => {
},
icon: 'EditOutlined',
onClick: () => {
editVisible.value = true
currentInfo.value = data
},
})
}
@ -123,7 +138,8 @@ const getActions = (data: Partial<Record<string, any>>) => {
},
icon: 'ClockCircleOutlined',
onClick: () => {
indicatorVisible.value = true
currentInfo.value = data
},
})
}
@ -134,8 +150,13 @@ const getActions = (data: Partial<Record<string, any>>) => {
title: '获取最新属性值',
},
icon: 'SyncOutlined',
onClick: () => {
onClick: async () => {
if(instanceStore.current.id && data.id){
const resp = await getProperty(instanceStore.current.id, data.id)
if(resp.status === 200){
message.success('操作成功!')
}
}
},
})
}
@ -147,17 +168,53 @@ const getActions = (data: Partial<Record<string, any>>) => {
},
icon: 'BarsOutlined',
onClick: () => {
detailVisible.value = true
currentInfo.value = data
},
})
return arr
}
watchEffect(() => {
dataSource.value = _data.data as PropertyData[]
const query = (page: number, size: number, value: string) => {
pageIndex.value = page || 0
pageSize.value = size || 8
const _from = pageIndex.value * pageSize.value
const _to = (pageIndex.value + 1) * pageSize.value
const arr = _.cloneDeep(_dataSource.value)
if(value){
const li = arr.filter((i: any) => {
return i?.name.indexOf(value) !== -1;
})
dataSource.value = li.slice(_from, _to)
total.value = li.length
} else {
dataSource.value = arr.slice(_from, _to)
total.value = arr.length
}
}
const pageChange = (page: number, size: number) => {
if(size === pageSize.value) {
query(page - 1, size, value.value)
} else {
query(0, size, value.value)
}
}
watch(() => _data.data,
(newVal) => {
if(newVal.length) {
_dataSource.value = newVal as PropertyData[]
query(0, 8, value.value)
}
}, {
deep: true,
immediate: true
})
const onSearch = () => {};
const onSearch = () => {
query(0, 8, value.value)
};
</script>

View File

@ -111,7 +111,6 @@ const tabChange = (key: string) => {
display: flex;
.property-box-left {
width: 200px;
margin-right: 20px;
}
.property-box-right {
flex: 1;