fix: 运行状态详情查看

This commit is contained in:
100011797 2023-02-24 18:07:41 +08:00
parent 15be308dc8
commit 25ee85681c
19 changed files with 1466 additions and 1108 deletions

View File

@ -37,6 +37,7 @@
"unplugin-vue-components": "^0.22.12", "unplugin-vue-components": "^0.22.12",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-json-viewer": "^3.0.4",
"vue-router": "^4.1.6", "vue-router": "^4.1.6",
"vue3-markdown-it": "^1.0.10", "vue3-markdown-it": "^1.0.10",
"vue3-ts-jsoneditor": "^2.7.1" "vue3-ts-jsoneditor": "^2.7.1"

View File

@ -466,4 +466,20 @@ export const saveEdgeMap = (deviceId: string, data?: any) => server.post(`/edge/
* @param params * @param params
* @returns * @returns
*/ */
export const getPropertyData = (deviceId: string, params: Record<string, unknown>) => server.get(`/device-instance/${deviceId}/properties/_query`, params) export const getPropertyData = (deviceId: string, params: Record<string, unknown>) => server.get(`/device-instance/${deviceId}/properties/_query`, params)
/**
*
* @param deviceId
* @param data
* @returns
*/
export const getPropertiesInfo = (deviceId: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/agg/_query`, data)
/**
*
* @param deviceId
* @param data
* @returns
*/
export const getPropertiesList = (deviceId: string, property: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/property/${property}/_query`, data)

View File

@ -4,6 +4,8 @@ import { filterAsnycRouter, MenuItem } from '@/utils/menu'
import { isArray } from 'lodash-es' import { isArray } from 'lodash-es'
import { usePermissionStore } from './permission' import { usePermissionStore } from './permission'
import router from '@/router' import router from '@/router'
import { message } from 'ant-design-vue'
import { onlyMessage } from '@/utils/comm'
const defaultOwnParams = [ const defaultOwnParams = [
{ {
@ -77,6 +79,7 @@ export const useMenuStore = defineStore({
name, params, query name, params, query
}) })
} else { } else {
onlyMessage('暂无权限,请联系管理员', 'error')
console.warn(`没有找到对应的页面: ${name}`) console.warn(`没有找到对应的页面: ${name}`)
} }
}, },

View File

@ -1,4 +1,6 @@
import AIcon from "@/components/AIcon"; import AIcon from "@/components/AIcon";
import { useInstanceStore } from "@/store/instance";
import { useMenuStore } from "@/store/menu";
import { Button, Descriptions, Modal } from "ant-design-vue" import { Button, Descriptions, Modal } from "ant-design-vue"
import styles from './index.module.less' import styles from './index.module.less'
@ -14,6 +16,10 @@ const ManualInspection = defineComponent({
const { data } = props const { data } = props
const instanceStore = useInstanceStore();
const menuStory = useMenuStore();
const dataRender = () => { const dataRender = () => {
if (data.type === 'device' || data.type === 'product') { if (data.type === 'device' || data.type === 'product') {
return ( return (
@ -207,7 +213,13 @@ const ManualInspection = defineComponent({
emit('save', data) emit('save', data)
}} }}
onCancel={() => { onCancel={() => {
// TODO 跳转设备和产品 if (data.type === 'device') {
instanceStore.tabActiveKey = 'Info'
} else if (data.type === 'product') {
menuStory.jumpPage('device/Product/Detail', { id: data.productId, tab: 'access' });
} else {
menuStory.jumpPage('link/AccessConfig/Detail', { id: data.configuration?.id });
}
}}> }}>
<div style={{ display: 'flex' }}>{dataRender()}</div> <div style={{ display: 'flex' }}>{dataRender()}</div>
</Modal> </Modal>

View File

@ -11,6 +11,8 @@ import _ from "lodash"
import DiagnosticAdvice from './DiagnosticAdvice' import DiagnosticAdvice from './DiagnosticAdvice'
import ManualInspection from './ManualInspection' import ManualInspection from './ManualInspection'
import { deployDevice } from "@/api/initHome" import { deployDevice } from "@/api/initHome"
import PermissionButton from '@/components/PermissionButton/index.vue'
import { useMenuStore } from "@/store/menu"
type TypeProps = 'network' | 'child-device' | 'media' | 'cloud' | 'channel' type TypeProps = 'network' | 'child-device' | 'media' | 'cloud' | 'channel'
@ -41,6 +43,7 @@ const Status = defineComponent({
const diagnoseData = ref<Partial<Record<string, any>>>() const diagnoseData = ref<Partial<Record<string, any>>>()
const bindParentVisible = ref<boolean>(false) const bindParentVisible = ref<boolean>(false)
const menuStory = useMenuStore();
const configuration = reactive<{ const configuration = reactive<{
product: Record<string, any>, product: Record<string, any>,
@ -57,19 +60,8 @@ const Status = defineComponent({
artificialData.value = params artificialData.value = params
} }
// TODO
const jumpAccessConfig = () => { const jumpAccessConfig = () => {
// const purl = getMenuPathByCode(MENUS_CODE['device/Product/Detail']); menuStory.jumpPage('device/Product/Detail', { id: unref(device).productId, tab: 'access' });
// if (purl) {
// history.push(
// `${getMenuPathByParams(MENUS_CODE['device/Product/Detail'], device.productId)}`,
// {
// tab: 'access',
// },
// );
// } else {
// message.error('规则可能有加密处理,请联系管理员');
// }
}; };
const jumpDeviceConfig = () => { const jumpDeviceConfig = () => {
@ -123,34 +115,40 @@ const Status = defineComponent({
<Badge <Badge
status="default" status="default"
text={ text={
<span><Popconfirm <span>
title="确认启用" <PermissionButton
onConfirm={async () => { type="link"
const res = await startNetwork( hasPermission="link/Type:action"
unref(gateway)?.channelId, popConfirm={{
); title: '确认启用',
if (res.status === 200) { onConfirm: async () => {
message.success('操作成功!'); const res = await startNetwork(
list.value = modifyArrayList( unref(gateway)?.channelId,
list.value, );
{ if (res.status === 200) {
key: 'network', message.success('操作成功!');
name: '网络组件', list.value = modifyArrayList(
desc: '诊断网络组件配置是否正确,配置错误将导致设备连接失败', list.value,
status: 'success', {
text: '正常', key: 'network',
info: null, name: '网络组件',
}, desc: '诊断网络组件配置是否正确,配置错误将导致设备连接失败',
); status: 'success',
} text: '正常',
}} info: null,
> },
<Button type="link" style="padding: 0"></Button> );
</Popconfirm></span> }
}
}}
>
</PermissionButton>
</span>
} }
/> />
</div> </div>
</div> </div >
) : ( ) : (
<div> <div>
<div class={styles.infoItem}> <div class={styles.infoItem}>
@ -287,28 +285,31 @@ const Status = defineComponent({
<Badge <Badge
status="default" status="default"
text={<span> text={<span>
<Popconfirm <PermissionButton
title="确认启用" hasPermission="link/Type:action"
onConfirm={async () => { popConfirm={{
const resp = await startGateway(unref(device).accessId || ''); title: '确认启用',
if (resp.status === 200) { onConfirm: async () => {
message.success('操作成功!'); const resp = await startGateway(unref(device).accessId || '');
list.value = modifyArrayList( if (resp.status === 200) {
list.value, message.success('操作成功!');
{ list.value = modifyArrayList(
key: 'gateway', list.value,
name: '设备接入网关', {
desc: desc, key: 'gateway',
status: 'success', name: '设备接入网关',
text: '正常', desc: desc,
info: null, status: 'success',
}, text: '正常',
); info: null,
},
);
}
} }
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span>} </span>}
/> />
</div> </div>
@ -411,28 +412,32 @@ const Status = defineComponent({
status="default" status="default"
text={ text={
<span> <span>
<Popconfirm
title="确认启用" <PermissionButton
onConfirm={async () => { hasPermission="link/AccessConfig:action"
const resp = await startGateway(unref(device).accessId || ''); popConfirm={{
if (resp.status === 200) { title: '确认启用',
message.success('操作成功!'); onConfirm: async () => {
list.value = modifyArrayList( const resp = await startGateway(unref(device).accessId || '');
list.value, if (resp.status === 200) {
{ message.success('操作成功!');
key: 'gateway', list.value = modifyArrayList(
name: '设备接入网关', list.value,
desc: desc, {
status: 'success', key: 'gateway',
text: '正常', name: '设备接入网关',
info: null, desc: desc,
}, status: 'success',
); text: '正常',
info: null,
},
);
}
} }
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span> </span>
} }
/> />
@ -519,28 +524,32 @@ const Status = defineComponent({
status="default" status="default"
text={ text={
<span> <span>
<Popconfirm
title="确认启用" <PermissionButton
onConfirm={async () => { hasPermission="device/Product:action"
const resp = await _deploy(response?.result?.id || ''); popConfirm={{
if (resp.status === 200) { title: '确认启用',
message.success('操作成功!'); onConfirm: async () => {
list.value = modifyArrayList( const resp = await _deploy(response?.result?.id || '');
list.value, if (resp.status === 200) {
{ message.success('操作成功!');
key: 'parent-device', list.value = modifyArrayList(
name: '网关父设备', list.value,
desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败', {
status: 'success', key: 'parent-device',
text: '正常', name: '网关父设备',
info: null, desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败',
}, status: 'success',
); text: '正常',
info: null,
},
);
}
} }
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span> </span>
} }
/> />
@ -623,28 +632,32 @@ const Status = defineComponent({
status="default" status="default"
text={ text={
<span> <span>
<Popconfirm
title="确认启用" <PermissionButton
onConfirm={async () => { hasPermission="device/Product:action"
const resp = await _deployProduct(unref(device).productId || ''); popConfirm={{
if (resp.status === 200) { title: '确认启用',
message.success('操作成功!'); onConfirm: async () => {
list.value = modifyArrayList( const resp = await _deployProduct(unref(device).productId || '');
list.value, if (resp.status === 200) {
{ message.success('操作成功!');
key: 'product', list.value = modifyArrayList(
name: '产品状态', list.value,
desc: '诊断产品状态是否正常,禁用状态将导致设备连接失败', {
status: 'success', key: 'product',
text: '正常', name: '产品状态',
info: null, desc: '诊断产品状态是否正常,禁用状态将导致设备连接失败',
}, status: 'success',
); text: '正常',
info: null,
},
);
}
} }
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span> </span>
} }
@ -695,29 +708,34 @@ const Status = defineComponent({
status="default" status="default"
text={ text={
<span> <span>
<Popconfirm
title="确认启用" <PermissionButton
onConfirm={async () => { hasPermission="device/Instance:action"
const resp = await _deploy(unref(device)?.id || ''); popConfirm={{
if (resp.status === 200) { title: '确认启用',
instanceStore.current.state = { value: 'offline', text: '离线' } onConfirm: async () => {
message.success('操作成功!'); const resp = await _deploy(unref(device)?.id || '');
list.value = modifyArrayList( if (resp.status === 200) {
list.value, instanceStore.current.state = { value: 'offline', text: '离线' }
{ message.success('操作成功!');
key: 'device', list.value = modifyArrayList(
name: '设备状态', list.value,
desc: '诊断设备状态是否正常,禁用状态将导致设备连接失败', {
status: 'success', key: 'device',
text: '正常', name: '设备状态',
info: null, desc: '诊断设备状态是否正常,禁用状态将导致设备连接失败',
}, status: 'success',
); text: '正常',
info: null,
},
);
}
} }
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span> </span>
} }
/> />

View File

@ -5,7 +5,7 @@
:columns="columns" :columns="columns"
:request="_getEventList" :request="_getEventList"
model="TABLE" model="TABLE"
:bodyStyle="{padding: '0 24px'}" :bodyStyle="{ padding: '0 24px' }"
> >
<template #timestamp="slotProps"> <template #timestamp="slotProps">
{{ moment(slotProps.timestamp).format('YYYY-MM-DD HH:mm:ss') }} {{ moment(slotProps.timestamp).format('YYYY-MM-DD HH:mm:ss') }}
@ -19,18 +19,18 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import moment from 'moment' import moment from 'moment';
import { getEventList } from '@/api/device/instance' import { getEventList } from '@/api/device/instance';
import { useInstanceStore } from '@/store/instance' import { useInstanceStore } from '@/store/instance';
import { Modal } from 'ant-design-vue' import { Modal } from 'ant-design-vue';
const events = defineProps({ const events = defineProps({
data: { data: {
type: Object, type: Object,
default: () => {} default: () => {},
} },
}) });
const instanceStore = useInstanceStore() const instanceStore = useInstanceStore();
const columns = ref<Record<string, any>>([ const columns = ref<Record<string, any>>([
{ {
@ -38,43 +38,52 @@ const columns = ref<Record<string, any>>([
dataIndex: 'timestamp', dataIndex: 'timestamp',
key: 'timestamp', key: 'timestamp',
scopedSlots: true, scopedSlots: true,
search: {
type: 'date',
},
}, },
{ {
title: '操作', title: '操作',
dataIndex: 'action', dataIndex: 'action',
key: 'action', key: 'action',
scopedSlots: true, scopedSlots: true,
} },
]) ]);
const params = ref<Record<string, any>>({}) const params = ref<Record<string, any>>({});
const _getEventList = () => getEventList(instanceStore.current.id || '', events.data.id || '', params.value) const _getEventList = () =>
getEventList(
instanceStore.current.id || '',
events.data.id || '',
params.value,
);
watchEffect(() => { watchEffect(() => {
if(events.data?.valueType?.type === 'object'){ if (events.data?.valueType?.type === 'object') {
(events.data.valueType?.properties || []).map((i: any) => { (events.data.valueType?.properties || []).map((i: any) => {
columns.value.splice(0, 0, { columns.value.splice(0, 0, {
key: i.id, key: i.id,
title: i.name, title: i.name,
dataIndex: `${i.id}_format` dataIndex: `${i.id}_format`,
}) search: {
}) type: 'string',
},
});
});
} else { } else {
columns.value.splice(0, 0, { columns.value.splice(0, 0, {
title: '数据', title: '数据',
dataIndex: 'value', dataIndex: 'value',
}) });
} }
}) });
const detail = () => { const detail = () => {
Modal.info({ Modal.info({
title: () => '详情', title: () => '详情',
width: 850, width: 850,
content: () => h('div', {}, [ content: () => h('div', {}, [h('p', '暂未开发')]),
h('p', '暂未开发'), okText: '关闭',
]), });
okText: '关闭' };
});
}
</script> </script>

View File

@ -1,3 +1,54 @@
<!-- 坐标点拾取组件 -->
<template> <template>
<div>AMap</div> <div style="width: 100%; height: 400px">
</template> <div style="position: relative">
<div style="position: absolute; right: 0; top: 5px; z-index: 999">
<a-space>
<a-button type="primary" @click="start">开始动画</a-button>
<a-button type="primary" @click="stop">停止动画</a-button>
</a-space>
</div>
</div>
<el-amap :center="center" :zooms="[3, 20]" @init="initMap" ref="map"></el-amap>
</div>
</template>
<script setup lang="ts">
import { initAMapApiLoader } from '@vuemap/vue-amap';
import AMapUI from '@vuemap/vue-amap'
import '@vuemap/vue-amap/dist/style.css';
initAMapApiLoader({
key: 'a0415acfc35af15f10221bfa5a6850b4',
securityJsCode: 'cae6108ec3dd222f946d1a7237c78be0',
});
interface EmitProps {
(e: 'update:points', data: string): void;
}
const props = defineProps({
points: { type: Array, default: () => [] },
});
const emit = defineEmits<EmitProps>();
// ()
const mapPoint = ref('');
const map = ref(null);
const center = ref([106.55, 29.56]);
const marker = ref(null);
/**
* 地图初始化
* @param e
*/
const initMap = (e: any) => {
console.log(e)
// map = e;
// const pointStr = mapPoint.value as string;
};
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,43 @@
<template>
<div class="chart" ref="chart"></div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
const { proxy } = <any>getCurrentInstance();
const props = defineProps({
//
options:{
type:Object,
default:()=>{}
}
});
/**
* 绘制图表
*/
const createChart = () => {
nextTick(() => {
const myChart = echarts.init(proxy.$refs.chart);
myChart.setOption(props.options);
window.addEventListener('resize', function () {
myChart.resize();
});
});
};
watch(
() => props.options,
() => createChart(),
{ immediate: true, deep: true },
);
</script>
<style scoped lang="less">
.chart {
width: 100%;
height: 100%;
}
</style>

View File

@ -1,3 +1,218 @@
<template> <template>
<div>Charts</div> <a-spin :spinning="loading">
</template> <div>
<a-space>
<div>
统计周期
<a-select v-model:value="cycle" style="width: 120px">
<a-select-option value="*" v-if="_type"
>实际值</a-select-option
>
<a-select-option value="1m">按分钟统计</a-select-option>
<a-select-option value="1h">按小时统计</a-select-option>
<a-select-option value="1d">按天统计</a-select-option>
<a-select-option value="1w">按周统计</a-select-option>
<a-select-option value="1M">按月统计</a-select-option>
</a-select>
</div>
<div v-if="cycle !== '*' && _type">
统计规则
<a-select v-model:value="agg" style="width: 120px">
<a-select-option value="AVG">平均值</a-select-option>
<a-select-option value="MAX">最大值</a-select-option>
<a-select-option value="MIN">最小值</a-select-option>
<a-select-option value="COUNT">总数</a-select-option>
</a-select>
</div>
</a-space>
</div>
<div style="width: 100%; height: 500px">
<Chart :options="options" v-if="chartsList.length" />
<JEmpty v-else />
</div>
</a-spin>
</template>
<script lang="ts" setup>
import { getPropertiesInfo, getPropertiesList } from '@/api/device/instance';
import { useInstanceStore } from '@/store/instance';
import Chart from './Chart.vue';
import * as echarts from 'echarts';
const list = ['int', 'float', 'double', 'long'];
const prop = defineProps({
data: {
type: Object,
default: () => {},
},
time: {
type: Array,
default: () => [],
},
});
const cycle = ref<string>('*');
const agg = ref<string>('AVG');
const loading = ref<boolean>(false);
const chartsList = ref<any[]>([]);
const instanceStore = useInstanceStore();
const options = ref({});
const _type = computed(() => {
const flag = list.includes(prop.data?.valueType?.type || '')
cycle.value = flag ? '*' : '1m'
return flag
});
const queryChartsAggList = async () => {
loading.value = true;
const resp = await getPropertiesInfo(instanceStore.current.id, {
columns: [
{
property: prop.data.id,
alias: prop.data.id,
agg: agg.value,
},
],
query: {
interval: cycle.value,
format: 'yyyy-MM-dd HH:mm:ss',
from: prop.time[0],
to: prop.time[1],
},
});
loading.value = false;
if (resp.status === 200) {
const dataList: any[] = [
{
year: prop.time[1],
value: undefined,
type: prop.data?.name || '',
},
];
(resp.result as any[]).forEach((i: any) => {
dataList.push({
...i,
year: i.time,
value: Number(i[prop.data.id || '']),
type: prop.data?.name || '',
});
});
dataList.push({
year: prop.time[0],
value: undefined,
type: prop.data?.name || '',
});
chartsList.value = (dataList || []).reverse();
}
};
const queryChartsList = async () => {
loading.value = true;
const resp = await getPropertiesList(
instanceStore.current.id,
prop.data.id,
{
paging: false,
terms: [
{
column: 'timestamp$BTW',
value:
prop.time[0] && prop.time[1]
? [prop.time[0], prop.time[1]]
: [],
type: 'and',
},
],
sorts: [{ name: 'timestamp', order: 'asc' }],
},
);
loading.value = false;
if (resp.status === 200) {
const dataList: any[] = [
{
year: prop.time[0],
value: undefined,
type: prop.data?.name || '',
},
];
(resp.result as any)?.data?.forEach((i: any) => {
dataList.push({
...i,
year: i.timestamp,
value: i.value,
type: prop.data?.name || '',
});
});
dataList.push({
year: prop.time[1],
value: undefined,
type: prop.data?.name || '',
});
chartsList.value = dataList || [];
}
};
const getOptions = (arr: any[]) => {
options.value = {
xAxis: {
type: 'category',
data: arr.map((item) => {
return echarts.format.formatTime(
'yyyy-MM-dd\nhh:mm:ss',
item.year,
false,
);
}),
name: '时间',
},
yAxis: {
type: 'value',
name: arr[0]?.type,
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 10,
},
{
start: 0,
end: 10,
},
],
tooltip: {
trigger: 'axis',
position: function (pt: any) {
return [pt[0], '10%'];
},
},
series: [
{
data: arr.map((i: any) => i.value),
type: 'line',
areaStyle: {},
},
],
};
};
watch(
() => [cycle, agg],
([newCycle, newAgg]) => {
if (newCycle.value === '*' && _type.value) {
queryChartsList();
} else {
queryChartsAggList();
}
},
{ deep: true, immediate: true },
);
watchEffect(() => {
if (chartsList.value.length) {
getOptions(chartsList.value);
}
});
</script>

View File

@ -0,0 +1,74 @@
<template>
<a-spin :spinning="loading">
<div style="position: relative">
<div style="position: absolute; right: 0; top: 5px; z-index: 999">
<a-space>
<a-button type="primary">开始动画</a-button>
<a-button type="primary">停止动画</a-button>
</a-space>
</div>
</div>
<AMap :points="geoList" />
</a-spin>
</template>
<script lang="ts" setup>
import { getPropertyData } from '@/api/device/instance';
import { useInstanceStore } from '@/store/instance';
import encodeQuery from '@/utils/encodeQuery';
import AMap from './AMap.vue';
const instanceStore = useInstanceStore();
const prop = defineProps({
data: {
type: Object,
default: () => {},
},
time: {
type: Array,
default: () => [],
},
});
const geoList = ref<any[]>([]);
const loading = ref<boolean>(false);
const query = async () => {
loading.value = true;
const resp = await getPropertyData(
instanceStore.current.id,
encodeQuery({
paging: false,
terms: {
property: prop.data.id,
timestamp$BTW: prop.time[0] && prop.time[1] ? prop.time : [],
},
sorts: { timestamp: 'asc' },
}),
);
loading.value = false;
if (resp.status === 200) {
const list: any[] = [];
((resp.result as any)?.data || []).forEach((item: any) => {
list.push([item.value.lon, item.value.lat]);
});
geoList.value = list
}
};
watch(
() => [prop.data.id, prop.time],
([newVal]) => {
if (newVal) {
query();
}
},
{
deep: true, immediate: true
}
);
</script>
<style lang="less" scoped>
</style>

View File

@ -18,6 +18,13 @@
<template v-if="column.key === 'timestamp'"> <template v-if="column.key === 'timestamp'">
{{ moment(record.timestamp).format('YYYY-MM-DD HH:mm:ss') }} {{ moment(record.timestamp).format('YYYY-MM-DD HH:mm:ss') }}
</template> </template>
<template v-if="column.key === 'value'">
<ValueRender
type="table"
:data="_props.data"
:value="{ formatValue: record.value }"
/>
</template>
<template v-else-if="column.key === 'action'"> <template v-else-if="column.key === 'action'">
<a-space> <a-space>
<a-button <a-button
@ -30,7 +37,7 @@
@click="_download(record)" @click="_download(record)"
><AIcon type="DownloadOutlined" ><AIcon type="DownloadOutlined"
/></a-button> /></a-button>
<a-button type="link" <a-button type="link" @click="showDetail(record)"
><AIcon type="SearchOutlined" ><AIcon type="SearchOutlined"
/></a-button> /></a-button>
</a-space> </a-space>
@ -38,6 +45,28 @@
</template> </template>
</a-table> </a-table>
</div> </div>
<a-modal
title="详情"
:visible="visible"
@ok="visible = false"
@cancel="visible = false"
>
<div>自定义属性</div>
<JsonViewer
v-if="
data?.valueType?.type === 'object' ||
data?.valueType?.type === 'array'
"
:expand-depth="5"
:value="current.formatValue"
/>
<a-textarea
v-else-if="data?.valueType?.type === 'file'"
:value="current.formatValue"
:row="3"
/>
<a-input v-else disabled :value="current.formatValue" />
</a-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -46,6 +75,8 @@ import { useInstanceStore } from '@/store/instance';
import encodeQuery from '@/utils/encodeQuery'; import encodeQuery from '@/utils/encodeQuery';
import moment from 'moment'; import moment from 'moment';
import { getType } from '../index'; import { getType } from '../index';
import ValueRender from '../ValueRender.vue';
import JsonViewer from 'vue-json-viewer';
const _props = defineProps({ const _props = defineProps({
data: { data: {
@ -57,8 +88,11 @@ const _props = defineProps({
default: () => [], default: () => [],
}, },
}); });
const instanceStore = useInstanceStore(); const instanceStore = useInstanceStore();
const dataSource = ref({}); const dataSource = ref({});
const current = ref<any>({});
const visible = ref<boolean>(false);
const columns = computed(() => { const columns = computed(() => {
const arr: any[] = [ const arr: any[] = [
@ -92,6 +126,11 @@ const showLoad = computed(() => {
); );
}); });
const showDetail = (item: any) => {
visible.value = true;
current.value = item;
};
const queryPropertyData = async (params: any) => { const queryPropertyData = async (params: any) => {
const resp = await getPropertyData( const resp = await getPropertyData(
instanceStore.current.id, instanceStore.current.id,
@ -99,7 +138,7 @@ const queryPropertyData = async (params: any) => {
...params, ...params,
terms: { terms: {
property: _props.data.id, property: _props.data.id,
timestamp$BTW: _props.time?.length ? _props.time : [], timestamp$BTW: _props.time,
}, },
sorts: { timestamp: 'desc' }, sorts: { timestamp: 'desc' },
}), }),
@ -109,14 +148,20 @@ const queryPropertyData = async (params: any) => {
} }
}; };
watchEffect(() => { watch(
if (_props.data.id) { () => [_props.data.id, _props.time],
queryPropertyData({ ([newVal]) => {
pageSize: _props.data.valueType?.type === 'file' ? 5 : 10, if (newVal) {
pageIndex: 0, queryPropertyData({
}); pageSize: _props.data.valueType?.type === 'file' ? 5 : 10,
pageIndex: 0,
});
}
},
{
deep: true, immediate: true
} }
}); );
const onChange = (page: any) => { const onChange = (page: any) => {
queryPropertyData({ queryPropertyData({
@ -140,4 +185,10 @@ const _download = (record: any) => {
// //
document.body.removeChild(downNode); document.body.removeChild(downNode);
}; };
</script> </script>
<style lang="less" scoped>
:deep(.ant-pagination-item) {
display: none !important;
}
</style>

View File

@ -2,15 +2,15 @@
<a-modal title="详情" visible width="50vw" @ok="onCancel" @cancel="onCancel"> <a-modal title="详情" visible width="50vw" @ok="onCancel" @cancel="onCancel">
<div style="margin-bottom: 10px"><TimeComponent v-model="dateValue" /></div> <div style="margin-bottom: 10px"><TimeComponent v-model="dateValue" /></div>
<div> <div>
<a-tabs v-model:activeKey="activeKey"> <a-tabs v-model:activeKey="activeKey" style="max-height: 600px; overflow-y: auto">
<a-tab-pane key="table" tab="列表"> <a-tab-pane key="table" tab="列表">
<Table :data="props.data" :time="_getTimes" /> <Table :data="props.data" :time="_getTimes" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="charts" tab="图表"> <a-tab-pane key="charts" tab="图表">
<Charts /> <Charts :data="props.data" :time="_getTimes" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="geo" tab="轨迹"> <a-tab-pane key="geo" tab="轨迹" v-if="data?.valueType?.type === 'geoPoint'">
<AMap /> <PropertyAMap :data="props.data" :time="_getTimes" />
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</div> </div>
@ -21,7 +21,7 @@
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import TimeComponent from './TimeComponent.vue' import TimeComponent from './TimeComponent.vue'
import Charts from './Charts.vue' import Charts from './Charts.vue'
import AMap from './AMap.vue' import PropertyAMap from './PropertyAMap.vue'
import Table from './Table.vue' import Table from './Table.vue'
const props = defineProps({ const props = defineProps({

View File

@ -1,15 +1,15 @@
<template> <template>
<a-card :hoverable="true" class="card-box"> <a-card :hoverable="true" class="card-box">
<!-- <a-spin :spinning="loading"> --> <!-- <a-spin :spinning="loading"> -->
<div class="card-container"> <div class="card-container">
<div class="header"> <div class="header">
<div class="title">{{ _props.data.name }}</div> <div class="title">{{ _props.data.name }}</div>
<div class="extra"> <div class="extra">
<a-space :size="16"> <a-space :size="16">
<template v-for="i in actions" :key="i.key">
<a-tooltip <a-tooltip
v-for="i in actions"
:key="i.key"
v-bind="i.tooltip" v-bind="i.tooltip"
v-if="i.key !== 'edit'"
> >
<a-button <a-button
style="padding: 0; margin: 0" style="padding: 0; margin: 0"
@ -17,26 +17,48 @@
:disabled="i.disabled" :disabled="i.disabled"
@click="i.onClick && i.onClick(data)" @click="i.onClick && i.onClick(data)"
> >
<AIcon :type="i.icon" style="color: #323130; font-size: 12px" /> <AIcon
:type="i.icon"
style="color: #323130; font-size: 12px"
/>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
</a-space> <PermissionButton
</div> :disabled="i.disabled"
</div> v-else
<div class="value"> :popConfirm="i.popConfirm"
<ValueRender :data="data" :value="_props.data" type="card" /> :tooltip="i.tooltip"
</div> @click="i.onClick && i.onClick(slotProps)"
<div class="bottom"> type="link"
<div style="color: rgba(0, 0, 0, .65); font-size: 12px">更新时间</div> style="padding: 0px"
<div class="time-value">{{_props?.data?.timeString || '--'}}</div> :hasPermission="'device/Instance:update'"
>
<template #icon
><AIcon :type="i.icon" style="color: #323130; font-size: 12px"
/></template>
</PermissionButton>
</template>
</a-space>
</div> </div>
</div> </div>
<div class="value">
<ValueRender :data="data" :value="_props.data" type="card" />
</div>
<div class="bottom">
<div style="color: rgba(0, 0, 0, 0.65); font-size: 12px">
更新时间
</div>
<div class="time-value">
{{ _props?.data?.timeString || '--' }}
</div>
</div>
</div>
<!-- </a-spin> --> <!-- </a-spin> -->
</a-card> </a-card>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import ValueRender from './ValueRender.vue' import ValueRender from './ValueRender.vue';
const _props = defineProps({ const _props = defineProps({
data: { data: {
type: Object, type: Object,
@ -44,7 +66,7 @@ const _props = defineProps({
}, },
actions: { actions: {
type: Array, type: Array,
default: () => [] default: () => [],
}, },
}); });
// const loading = ref<boolean>(true); // const loading = ref<boolean>(true);
@ -101,6 +123,6 @@ const _props = defineProps({
color: #000; color: #000;
} }
} }
} }
} }
</style> </style>

View File

@ -13,23 +13,18 @@
<a-image :src="value?.formatValue" /> <a-image :src="value?.formatValue" />
</template> </template>
<template v-else-if="['.flv', '.m3u8', '.mp4'].includes(type)"> <template v-else-if="['.flv', '.m3u8', '.mp4'].includes(type)">
<!-- TODO 视频组件缺失 -->
</template> </template>
<template v-else> <template v-else>
<!-- <json-viewer <JsonViewer
:value="{ :expand-depth="5"
'id': '123' :value="value?.formatValue"
}" />
copyable
boxed
sort
></json-viewer> -->
</template> </template>
</a-modal> </a-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// import JsonViewer from 'vue3-json-viewer'; import JsonViewer from 'vue-json-viewer';
const _data = defineProps({ const _data = defineProps({
type: { type: {
@ -46,9 +41,6 @@ const handleCancel = () => {
_emit('close'); _emit('close');
}; };
// watchEffect(() => {
// console.log(_data.value?.formatValue)
// })
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="value"> <div class="value">
<div v-if="value?.formatValue !== 0 && !value?.formatValue" :class="valueClass">--</div> <div v-if="value?.formatValue !== 0 && !value?.formatValue" :class="valueClass">--</div>
<div v-else-if="data?.valueType?.type === 'file'"> <div v-else-if="_data.data?.valueType?.type === 'file'">
<template v-if="data?.valueType?.fileType === 'base64'"> <template v-if="data?.valueType?.fileType === 'base64'">
<div :class="valueClass" v-if="!!getType(value?.formatValue)"> <div :class="valueClass" v-if="!!getType(value?.formatValue)">
<img :src="imgMap.get(_type)" @error="onError" /> <img :src="imgMap.get(_type)" @error="onError" />
@ -36,10 +36,10 @@
</template> </template>
</template> </template>
</div> </div>
<div v-else-if="data?.valueType?.type === 'object'" @click="getDetail('obj')" :class="valueClass"> <div v-else-if="_data.data?.valueType?.type === 'object'" @click="getDetail('obj')" :class="valueClass">
<img :src="imgMap.get('obj')" /> <img :src="imgMap.get('obj')" />
</div> </div>
<div v-else-if="data?.valueType?.type === 'geoPoint' || data?.valueType?.type === 'array'" :class="valueClass"> <div v-else-if="_data.data?.valueType?.type === 'geoPoint' || _data.data?.valueType?.type === 'array'" :class="valueClass">
{{JSON.stringify(value?.formatValue)}} {{JSON.stringify(value?.formatValue)}}
</div> </div>
<div v-else :class="valueClass"> <div v-else :class="valueClass">
@ -53,7 +53,7 @@
import { getImage } from "@/utils/comm"; import { getImage } from "@/utils/comm";
import { message } from "ant-design-vue"; import { message } from "ant-design-vue";
import ValueDetail from './ValueDetail.vue' import ValueDetail from './ValueDetail.vue'
import {getType, imgMap} from './index' import {getType, imgMap, imgList, videoList, fileList} from './index'
const _data = defineProps({ const _data = defineProps({
data: { data: {
@ -115,7 +115,6 @@ const getDetail = (_type: string) => {
_types.value = flag _types.value = flag
visible.value = true visible.value = true
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -32,20 +32,30 @@
</template> </template>
<template #action="slotProps"> <template #action="slotProps">
<a-space :size="16"> <a-space :size="16">
<a-tooltip <template v-for="i in getActions(slotProps)" :key="i.key">
v-for="i in getActions(slotProps)" <a-tooltip v-bind="i.tooltip" v-if="i.key !== 'edit'">
:key="i.key" <a-button
v-bind="i.tooltip" style="padding: 0"
> type="link"
<a-button :disabled="i.disabled"
style="padding: 0" @click="i.onClick && i.onClick(slotProps)"
type="link" >
<AIcon :type="i.icon" />
</a-button>
</a-tooltip>
<PermissionButton
:disabled="i.disabled" :disabled="i.disabled"
v-else
:popConfirm="i.popConfirm"
:tooltip="i.tooltip"
@click="i.onClick && i.onClick(slotProps)" @click="i.onClick && i.onClick(slotProps)"
type="link"
style="padding: 0px"
:hasPermission="'device/Instance:update'"
> >
<AIcon :type="i.icon" /> <template #icon><AIcon :type="i.icon" /></template>
</a-button> </PermissionButton>
</a-tooltip> </template>
</a-space> </a-space>
</template> </template>
<template #paginationRender> <template #paginationRender>
@ -76,7 +86,11 @@
@close="indicatorVisible = false" @close="indicatorVisible = false"
:data="currentInfo" :data="currentInfo"
/> />
<Detail v-if="detailVisible" :data="currentInfo" @close="detailVisible = false" /> <Detail
v-if="detailVisible"
:data="currentInfo"
@close="detailVisible = false"
/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -240,11 +254,15 @@ const subscribeProperty = () => {
?.pipe(map((res: any) => res.payload)) ?.pipe(map((res: any) => res.payload))
.subscribe((payload) => { .subscribe((payload) => {
list.value = [...list.value, payload]; list.value = [...list.value, payload];
unref(list).sort((a: any, b: any) => a.timestamp - b.timestamp) unref(list)
.forEach((item: any) => { .sort((a: any, b: any) => a.timestamp - b.timestamp)
const { value } = item; .forEach((item: any) => {
propertyValue.value[value?.property] = { ...item, ...value }; const { value } = item;
}); propertyValue.value[value?.property] = {
...item,
...value,
};
});
// list.value = [...list.value, payload]; // list.value = [...list.value, payload];
// throttle(valueChange(list.value), 500); // throttle(valueChange(list.value), 500);
}); });
@ -337,8 +355,8 @@ const onSearch = () => {
}; };
onUnmounted(() => { onUnmounted(() => {
subRef.value && subRef.value?.unsubscribe() subRef.value && subRef.value?.unsubscribe();
}) });
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -2,7 +2,7 @@
<page-container <page-container
:tabList="list" :tabList="list"
@back="onBack" @back="onBack"
:tabActiveKey="instanceStore.active" :tabActiveKey="instanceStore.tabActiveKey"
@tabChange="onTabChange" @tabChange="onTabChange"
> >
<template #title> <template #title>

View File

@ -155,13 +155,12 @@
/> />
</template> </template>
<template #content> <template #content>
<h3 <Ellipsis style="width: calc(100% - 100px)">
class="card-item-content-title" <span style="font-size: 16px; font-weight: 600" @click.stop="handleView(slotProps.id)">
@click.stop="handleView(slotProps.id)" {{ slotProps.name }}
> </span>
{{ slotProps.name }} </Ellipsis>
</h3> <a-row style="margin-top: 20px">
<a-row>
<a-col :span="12"> <a-col :span="12">
<div class="card-item-content-text"> <div class="card-item-content-text">
设备类型 设备类型
@ -172,7 +171,9 @@
<div class="card-item-content-text"> <div class="card-item-content-text">
产品名称 产品名称
</div> </div>
<div>{{ slotProps.productName }}</div> <Ellipsis style="width: 100%">
{{ slotProps.productName }}
</Ellipsis>
</a-col> </a-col>
</a-row> </a-row>
</template> </template>

1555
yarn.lock

File diff suppressed because it is too large Load Diff