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

@ -467,3 +467,19 @@ export const saveEdgeMap = (deviceId: string, data?: any) => server.post(`/edge/
* @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,9 +115,13 @@ const Status = defineComponent({
<Badge <Badge
status="default" status="default"
text={ text={
<span><Popconfirm <span>
title="确认启用" <PermissionButton
onConfirm={async () => { type="link"
hasPermission="link/Type:action"
popConfirm={{
title: '确认启用',
onConfirm: async () => {
const res = await startNetwork( const res = await startNetwork(
unref(gateway)?.channelId, unref(gateway)?.channelId,
); );
@ -143,10 +139,12 @@ const Status = defineComponent({
}, },
); );
} }
}
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm></span> </PermissionButton>
</span>
} }
/> />
</div> </div>
@ -287,9 +285,11 @@ const Status = defineComponent({
<Badge <Badge
status="default" status="default"
text={<span> text={<span>
<Popconfirm <PermissionButton
title="确认启用" hasPermission="link/Type:action"
onConfirm={async () => { popConfirm={{
title: '确认启用',
onConfirm: async () => {
const resp = await startGateway(unref(device).accessId || ''); const resp = await startGateway(unref(device).accessId || '');
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
@ -305,10 +305,11 @@ const Status = defineComponent({
}, },
); );
} }
}
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span>} </span>}
/> />
</div> </div>
@ -411,9 +412,12 @@ const Status = defineComponent({
status="default" status="default"
text={ text={
<span> <span>
<Popconfirm
title="确认启用" <PermissionButton
onConfirm={async () => { hasPermission="link/AccessConfig:action"
popConfirm={{
title: '确认启用',
onConfirm: async () => {
const resp = await startGateway(unref(device).accessId || ''); const resp = await startGateway(unref(device).accessId || '');
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
@ -429,10 +433,11 @@ const Status = defineComponent({
}, },
); );
} }
}
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span> </span>
} }
/> />
@ -519,9 +524,12 @@ const Status = defineComponent({
status="default" status="default"
text={ text={
<span> <span>
<Popconfirm
title="确认启用" <PermissionButton
onConfirm={async () => { hasPermission="device/Product:action"
popConfirm={{
title: '确认启用',
onConfirm: async () => {
const resp = await _deploy(response?.result?.id || ''); const resp = await _deploy(response?.result?.id || '');
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
@ -537,10 +545,11 @@ const Status = defineComponent({
}, },
); );
} }
}
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span> </span>
} }
/> />
@ -623,9 +632,12 @@ const Status = defineComponent({
status="default" status="default"
text={ text={
<span> <span>
<Popconfirm
title="确认启用" <PermissionButton
onConfirm={async () => { hasPermission="device/Product:action"
popConfirm={{
title: '确认启用',
onConfirm: async () => {
const resp = await _deployProduct(unref(device).productId || ''); const resp = await _deployProduct(unref(device).productId || '');
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
@ -641,10 +653,11 @@ const Status = defineComponent({
}, },
); );
} }
}
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span> </span>
} }
@ -695,9 +708,12 @@ const Status = defineComponent({
status="default" status="default"
text={ text={
<span> <span>
<Popconfirm
title="确认启用" <PermissionButton
onConfirm={async () => { hasPermission="device/Instance:action"
popConfirm={{
title: '确认启用',
onConfirm: async () => {
const resp = await _deploy(unref(device)?.id || ''); const resp = await _deploy(unref(device)?.id || '');
if (resp.status === 200) { if (resp.status === 200) {
instanceStore.current.state = { value: 'offline', text: '离线' } instanceStore.current.state = { value: 'offline', text: '离线' }
@ -714,10 +730,12 @@ const Status = defineComponent({
}, },
); );
} }
}
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span> </span>
} }
/> />

View File

@ -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,17 +38,25 @@ 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') {
@ -56,25 +64,26 @@ watchEffect(() => {
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">
<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> </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">
<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> </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],
([newVal]) => {
if (newVal) {
queryPropertyData({ queryPropertyData({
pageSize: _props.data.valueType?.type === 'file' ? 5 : 10, pageSize: _props.data.valueType?.type === 'file' ? 5 : 10,
pageIndex: 0, pageIndex: 0,
}); });
} }
}); },
{
deep: true, immediate: true
}
);
const onChange = (page: any) => { const onChange = (page: any) => {
queryPropertyData({ queryPropertyData({
@ -141,3 +186,9 @@ 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

@ -6,10 +6,10 @@
<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,9 +17,27 @@
: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>
<PermissionButton
:disabled="i.disabled"
v-else
:popConfirm="i.popConfirm"
:tooltip="i.tooltip"
@click="i.onClick && i.onClick(slotProps)"
type="link"
style="padding: 0px"
:hasPermission="'device/Instance:update'"
>
<template #icon
><AIcon :type="i.icon" style="color: #323130; font-size: 12px"
/></template>
</PermissionButton>
</template>
</a-space> </a-space>
</div> </div>
</div> </div>
@ -27,8 +45,12 @@
<ValueRender :data="data" :value="_props.data" type="card" /> <ValueRender :data="data" :value="_props.data" type="card" />
</div> </div>
<div class="bottom"> <div class="bottom">
<div style="color: rgba(0, 0, 0, .65); font-size: 12px">更新时间</div> <div style="color: rgba(0, 0, 0, 0.65); font-size: 12px">
<div class="time-value">{{_props?.data?.timeString || '--'}}</div> 更新时间
</div>
<div class="time-value">
{{ _props?.data?.timeString || '--' }}
</div>
</div> </div>
</div> </div>
<!-- </a-spin> --> <!-- </a-spin> -->
@ -36,7 +58,7 @@
</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);

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,11 +32,8 @@
</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"
v-bind="i.tooltip"
>
<a-button <a-button
style="padding: 0" style="padding: 0"
type="link" type="link"
@ -46,6 +43,19 @@
<AIcon :type="i.icon" /> <AIcon :type="i.icon" />
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<PermissionButton
:disabled="i.disabled"
v-else
:popConfirm="i.popConfirm"
:tooltip="i.tooltip"
@click="i.onClick && i.onClick(slotProps)"
type="link"
style="padding: 0px"
:hasPermission="'device/Instance:update'"
>
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</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,10 +254,14 @@ 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)
.sort((a: any, b: any) => a.timestamp - b.timestamp)
.forEach((item: any) => { .forEach((item: any) => {
const { value } = item; const { value } = item;
propertyValue.value[value?.property] = { ...item, ...value }; 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 }} {{ slotProps.name }}
</h3> </span>
<a-row> </Ellipsis>
<a-row style="margin-top: 20px">
<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