Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev
This commit is contained in:
commit
277b77f59e
|
@ -482,4 +482,19 @@ export const getPropertiesInfo = (deviceId: string, data: Record<string, unknown
|
|||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const getPropertiesList = (deviceId: string, property: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/property/${property}/_query`, data)
|
||||
export const getPropertiesList = (deviceId: string, property: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/property/${property}/_query`, data)
|
||||
|
||||
/**
|
||||
* 查询设备日志
|
||||
* @param deviceId
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const queryLog = (deviceId: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/logs`, data)
|
||||
|
||||
/**
|
||||
* 查询设备日志类型
|
||||
* @returns
|
||||
*/
|
||||
export const queryLogsType = () => server.get(`/dictionary/device-log-type/items`)
|
||||
|
||||
|
|
|
@ -184,4 +184,13 @@ export const getOperator = () => server.get<OperatorItem>('/property-calculate-r
|
|||
/**
|
||||
* 获取聚合函数列表
|
||||
*/
|
||||
export const getStreamingAggType = () => server.get<Record<string, string>[]>('/dictionary/streaming-agg-type/items')
|
||||
export const getStreamingAggType = () => server.get<Record<string, string>[]>('/dictionary/streaming-agg-type/items')
|
||||
|
||||
export const getMetadataConfig = (params: {
|
||||
deviceId: string;
|
||||
metadata: {
|
||||
type: MetadataType | 'property';
|
||||
id: string;
|
||||
dataType: string;
|
||||
};
|
||||
}) => server.get<Record<any, any>[]>(`/device/product/${params.deviceId}/config-metadata/${params.metadata.type}/${params.metadata.id}/${params.metadata.dataType}`)
|
|
@ -0,0 +1,16 @@
|
|||
import server from '@/utils/request'
|
||||
|
||||
export default {
|
||||
// 列表
|
||||
list: (data: any, id: string) => server.post(`/media/gb28181-cascade/_query`, data),
|
||||
// 列表字段通道数量, 来自下面接口的total
|
||||
queryCount: (id: string) => server.post(`/media/gb28181-cascade/${id}/bindings/_query`),
|
||||
// 详情
|
||||
detail: (id: string): any => server.get(`/media/gb28181-cascade/${id}`),
|
||||
// 新增
|
||||
save: (data: any) => server.post(`/media/gb28181-cascade`, data),
|
||||
// 修改
|
||||
update: (id: string, data: any) => server.put(`/media/gb28181-cascade/${id}`, data),
|
||||
// 删除
|
||||
del: (id: string) => server.remove(`media/gb28181-cascade/${id}`),
|
||||
}
|
|
@ -6,10 +6,11 @@ export default {
|
|||
// 详情
|
||||
detail: (id: string): any => server.get(`/media/channel/${id}`),
|
||||
// 验证通道ID是否存在
|
||||
validateField: (params: string): any => server.get(`/media/channel/channelId/_validate`, params),
|
||||
validateField: (params: any): any => server.get(`/media/channel/channelId/_validate`, params),
|
||||
// 新增
|
||||
save: (data: any) => server.post(`/media/channel`, data),
|
||||
// 修改
|
||||
update: (data: any) => server.put(`/media/channel`, data),
|
||||
update: (id: string, data: any) => server.put(`/media/channel/${id}`, data),
|
||||
// 删除
|
||||
del: (id: string) => server.remove(`media/channel/${id}`),
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import server from '@/utils/request';
|
||||
|
||||
/**
|
||||
* 获取产品列表
|
||||
*/
|
||||
export const getProductList = (parmas?:any) => server.get('/device/product/_query/no-paging?paging=false',parmas);
|
||||
|
||||
/**
|
||||
* 获取设备列表
|
||||
*/
|
||||
export const getDeviceList = (parmas?:any) => server.get('/device-instance/_query/no-paging?paging=false',parmas);
|
||||
|
||||
/**
|
||||
* 获取组织列表
|
||||
*/
|
||||
export const getOrgList = (parmas?:any) => server.get('/organization/_query/no-paging?paging=false',parmas);
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
*/
|
||||
export const query = (data:any) => server.post('/alarm/record/_query/',data);
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { registerMixin } from '@vuemap/vue-amap';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import type { PathSimplifier, PathDataItemType, PathNavigator } from './types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PathSimplifier',
|
||||
mixins: [registerMixin],
|
||||
props: {
|
||||
pathData: Array as PropType<PathDataItemType[]>,
|
||||
},
|
||||
data(): {
|
||||
pathSimplifierRef: PathSimplifier | null,
|
||||
PathNavigatorRef: PathNavigator | null,
|
||||
distance: number
|
||||
}{
|
||||
return {
|
||||
pathSimplifierRef: null,
|
||||
PathNavigatorRef: null,
|
||||
distance: 0,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
pathSimplifier(PathObj: PathSimplifier) {
|
||||
this.pathSimplifierRef = new PathObj({
|
||||
zIndex: 100,
|
||||
getPath: (_pathData: any) => {
|
||||
return _pathData.path;
|
||||
},
|
||||
getHoverTitle: (_pathData: any) => {
|
||||
return _pathData.name;
|
||||
},
|
||||
map: this.parentInstance?.$amapComponent
|
||||
});
|
||||
|
||||
this.PathNavigatorRef?.destroy();
|
||||
|
||||
if (this.pathData) {
|
||||
this.pathSimplifierRef?.setData(
|
||||
this.pathData.map((item) => ({
|
||||
name: item.name || '路线',
|
||||
path: item.path,
|
||||
})),
|
||||
);
|
||||
|
||||
const pathData = this.pathSimplifierRef?.getPathData(0);
|
||||
|
||||
if (pathData?.path && pathData?.path.length) {
|
||||
this.PathNavigatorRef =
|
||||
this.pathSimplifierRef?.createPathNavigator(0, {
|
||||
speed: this.distance
|
||||
? (this.distance / 5) * 3.6
|
||||
: 10,
|
||||
}) as any;
|
||||
}
|
||||
}
|
||||
},
|
||||
loadUI() {
|
||||
if ((window as any).AMapUI) {
|
||||
(window as any).AMapUI.load(
|
||||
['ui/misc/PathSimplifier', 'lib/$'],
|
||||
(path: PathSimplifier) => {
|
||||
if (!path.supportCanvas) {
|
||||
console.warn('当前环境不支持 Canvas!');
|
||||
return;
|
||||
}
|
||||
this.pathSimplifier(path);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
start() {
|
||||
this.PathNavigatorRef?.start();
|
||||
},
|
||||
stop() {
|
||||
this.PathNavigatorRef?.moveToPoint(0, 0);
|
||||
this.PathNavigatorRef?.stop();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
pathData: {
|
||||
handler(newVal) {
|
||||
if (
|
||||
this.parentInstance.$amapComponent &&
|
||||
newVal?.[0]?.path &&
|
||||
newVal?.[0]?.path.length >= 2
|
||||
) {
|
||||
this.loadUI()
|
||||
// 计算速度
|
||||
const pointArr = newVal?.[0]?.path.map(
|
||||
(point: number[]) => new (AMap as any).LngLat(point[0], point[1]),
|
||||
);
|
||||
const distanceOfLine = (AMap as any).GeometryUtil.distanceOfLine(pointArr);
|
||||
this.distance = Math.round(distanceOfLine);
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
expose: ['start', 'stop']
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<div
|
||||
:style="props.style || { width: '100%', height: '100%' }"
|
||||
:class="props.class"
|
||||
>
|
||||
<el-amap v-if="amapKey" :zooms="[3, 20]" @init="initMap" ref="mapRef">
|
||||
<template v-if="isOpenUi">
|
||||
<template v-if="uiLoading">
|
||||
<slot></slot>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else><slot></slot></template>
|
||||
</el-amap>
|
||||
<JEmpty v-else description="请配置高德地图key" style="padding: 20%" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { CSSProperties, PropType } from 'vue';
|
||||
import AMap, { initAMapApiLoader } from '@vuemap/vue-amap';
|
||||
import '@vuemap/vue-amap/dist/style.css';
|
||||
import { getAMapUiPromise } from './utils';
|
||||
|
||||
interface AMapProps {
|
||||
style?: CSSProperties;
|
||||
class?: string;
|
||||
AMapUI?: string | boolean;
|
||||
}
|
||||
const amapKey = localStorage.getItem('amap_key') || 'a0415acfc35af15f10221bfa5a6850b4';
|
||||
|
||||
initAMapApiLoader({
|
||||
key: amapKey || '',
|
||||
securityJsCode: 'cae6108ec3dd222f946d1a7237c78be0',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
style: Object as PropType<AMapProps['style']>,
|
||||
class: String as PropType<AMapProps['class']>,
|
||||
AMapUI: [String, Boolean],
|
||||
center: Array,
|
||||
});
|
||||
|
||||
const mapRef = ref();
|
||||
|
||||
const uiLoading = ref<boolean>(false);
|
||||
|
||||
const map = ref<any>(null);
|
||||
|
||||
const isOpenUi = computed(() => {
|
||||
return 'AMapUI' in props || props.AMapUI;
|
||||
});
|
||||
|
||||
const getAMapUI = () => {
|
||||
const version = typeof props.AMapUI === 'string' ? props.AMapUI : '1.1';
|
||||
getAMapUiPromise(version).then(() => {
|
||||
uiLoading.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const marker = ref<any[]>([]);
|
||||
|
||||
const initMap = (e: any) => {
|
||||
map.value = e;
|
||||
if (isOpenUi.value) {
|
||||
getAMapUI();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
|
@ -0,0 +1,130 @@
|
|||
export type PathDataType = number[][];
|
||||
|
||||
export type PathSimplifierOptions = {
|
||||
map?: any;
|
||||
zIndex?: number;
|
||||
data?: number[][];
|
||||
getPath?: (pathData: {}, pathIndex: number) => PathDataType;
|
||||
getZIndex?: (pathData: any, pathIndex: number) => number;
|
||||
getHoverTitle?: (pathData: any, pathIndex: number, pointIndex: number) => string;
|
||||
autoSetFitView?: boolean;
|
||||
clickToSelectPath?: boolean;
|
||||
onTopWhenSelected?: boolean;
|
||||
renderConstructor?: Function;
|
||||
renderOptions?: {};
|
||||
};
|
||||
|
||||
export type PathDataItemType = {
|
||||
name?: string;
|
||||
path: PathDataType;
|
||||
};
|
||||
|
||||
export interface PathSimplifier {
|
||||
new (options: PathSimplifierOptions);
|
||||
|
||||
readonly supportCanvas: boolean;
|
||||
|
||||
getZIndexOfPath: (pathIndex: number) => number;
|
||||
|
||||
setZIndexOfPath: (pathIndex: number, zIndex: number) => void;
|
||||
|
||||
/**
|
||||
* 是否置顶显示pathIndex对应的轨迹
|
||||
* @param pathIndex
|
||||
* @param isTop isTop为真,设置 zIndex 为 现存最大zIndex+1; isTop为假,设置 zIndex 为 构造参数中 getZIndex 的返回值
|
||||
*/
|
||||
toggleTopOfPath: (pathIndex: number, isTop: boolean) => void;
|
||||
|
||||
getPathData: (pathIndex: number) => any;
|
||||
|
||||
createPathNavigator: (pathIndex: number, options: {}) => PathNavigator;
|
||||
|
||||
getPathNavigators: () => any[];
|
||||
|
||||
clearPathNavigators: () => void;
|
||||
|
||||
getSelectedPathData: () => any;
|
||||
|
||||
getSelectedPathIndex: () => number;
|
||||
|
||||
isSelectedPathIndex: (pathIndex: number) => boolean;
|
||||
|
||||
setSelectedPathIndex: (pathIndex: number) => void;
|
||||
|
||||
render: () => void;
|
||||
|
||||
renderLater: (delay: number[]) => void;
|
||||
|
||||
setData: (data: any[]) => void;
|
||||
|
||||
setFitView: (pathIndex: number) => void;
|
||||
|
||||
on: (eventName: string, handler: Function) => void;
|
||||
|
||||
off: (eventName: string, handler: Function) => void;
|
||||
|
||||
hide: () => void;
|
||||
|
||||
show: () => void;
|
||||
|
||||
isHidden: () => boolean;
|
||||
|
||||
getRender: () => boolean;
|
||||
|
||||
getRenderOptions: () => any;
|
||||
}
|
||||
|
||||
export interface PathNavigatorOptions {
|
||||
loop?: boolean;
|
||||
speed?: number;
|
||||
pathNavigatorStyle?: {};
|
||||
animInterval?: number;
|
||||
dirToPosInMillsecs?: number;
|
||||
range?: [number, number];
|
||||
}
|
||||
|
||||
export interface PathNavigator {
|
||||
new (options: PathNavigatorOptions);
|
||||
|
||||
start: (pointIndex?: number) => void;
|
||||
|
||||
pause: () => void;
|
||||
|
||||
resume: () => void;
|
||||
|
||||
stop: () => void;
|
||||
|
||||
destroy: () => void;
|
||||
|
||||
getCursor: () => any;
|
||||
|
||||
getNaviStatus: () => string;
|
||||
|
||||
getPathIndex: () => number;
|
||||
|
||||
getPosition: () => [number, number];
|
||||
|
||||
getSpeed: () => number;
|
||||
|
||||
getMovedDistance: () => number;
|
||||
|
||||
getPathStartIdx: () => number;
|
||||
|
||||
getPathEndIdx: () => number;
|
||||
|
||||
moveByDistance: (distance: number) => void;
|
||||
|
||||
moveToPoint: (idx: number, tail: number) => void;
|
||||
|
||||
isCursorAtPathEnd: () => boolean;
|
||||
|
||||
isCursorAtPathStart: () => boolean;
|
||||
|
||||
setSpeed: (speed: number) => void;
|
||||
|
||||
setRange: (startIndex: number, endIndex: number) => void;
|
||||
|
||||
on: (eventName: string, handler: Function) => void;
|
||||
|
||||
off: (eventName: string, handler: Function) => void;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
const protocol = window.location.protocol;
|
||||
|
||||
const buildScriptTag = (src: string): HTMLScriptElement => {
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.src = src;
|
||||
return script;
|
||||
};
|
||||
|
||||
export const getAMapUiPromise = (version: string = '1.0'): Promise<any> => {
|
||||
if ((window as any).AMapUI) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const script = buildScriptTag(`${protocol}//webapi.amap.com/ui/${version}/main-async.js`);
|
||||
const pro = new Promise((resolve) => {
|
||||
script.onload = () => {
|
||||
(window as any).initAMapUI();
|
||||
resolve(true);
|
||||
};
|
||||
});
|
||||
|
||||
document.body.append(script);
|
||||
return pro;
|
||||
};
|
|
@ -0,0 +1,115 @@
|
|||
<template>
|
||||
<div class="indicator-box">
|
||||
<template v-if="['int', 'long', 'double', 'float'].includes(type)">
|
||||
<template v-if="value.range">
|
||||
<a-input-number v-model:value="value.value[0]" :max="value.value[1]" size="small"
|
||||
style="width: 100%;"></a-input-number>
|
||||
~
|
||||
<a-input-number v-model:value="value.value[1]" :min="value.value[0]" size="small"
|
||||
style="width: 100%;"></a-input-number>
|
||||
</template>
|
||||
<a-input-number v-else v-model:value="value.value" size="small" style="width: 100%;"></a-input-number>
|
||||
</template>
|
||||
<template v-else-if="type === 'date'">
|
||||
<a-range-picker v-if="value.range" show-time v-model:value="value.value" size="small" />
|
||||
<a-date-picker v-else show-time v-model:value="value.value" size="small" />
|
||||
</template>
|
||||
<template v-else-if="type === 'boolean'">
|
||||
<a-select v-model:value="value.value[0]" :options="list" size="small" placeholder="请选择"></a-select>
|
||||
</template>
|
||||
<template v-else-if="type === 'string'">
|
||||
<a-input v-model:value="value.value" size="small" placeholder="请输入"></a-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="value.range">
|
||||
<a-input v-model:value="value.value[0]" :max="value.value[1]" size="small" placeholder="请输入"></a-input>
|
||||
~
|
||||
<a-input v-model:value="value.value[1]" :min="value.value[0]" size="small" placeholder="请输入"></a-input>
|
||||
</template>
|
||||
<a-input-number v-else v-model:value="value.value" size="small" placeholder="请输入"></a-input-number>
|
||||
</template>
|
||||
<div v-if="type !== 'boolean' && type !== 'string'">
|
||||
<a-checkbox style="min-width: 60px; margin-left: 5px;" v-model:checked="value.range" @change="changeChecked">
|
||||
范围
|
||||
</a-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="JIndicators">
|
||||
import { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';
|
||||
import { Form } from 'ant-design-vue'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
range: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: [String, Number, Array] as any
|
||||
},
|
||||
enum: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
Form.useInjectFormItemContext()
|
||||
|
||||
const changeChecked = (e: CheckboxChangeEvent) => {
|
||||
if (e.target.checked) {
|
||||
props.value.value = []
|
||||
} else {
|
||||
delete props.value.value
|
||||
}
|
||||
}
|
||||
|
||||
const list = ref<{ label: any; value: any; }[]>([])
|
||||
watch(() => props.enum,
|
||||
() => {
|
||||
const arr = [];
|
||||
if (!!props.enum?.falseText && props.enum?.falseValue !== undefined) {
|
||||
arr.push({ label: props.enum?.falseText, value: props.enum?.falseValue });
|
||||
}
|
||||
if (!!props.enum?.trueText && props.enum?.trueValue !== undefined) {
|
||||
arr.push({ label: props.enum?.trueText, value: props.enum?.trueValue });
|
||||
}
|
||||
list.value = arr
|
||||
},
|
||||
{ immediate: true, deep: true })
|
||||
|
||||
watch(() => props.type,
|
||||
(value) => {
|
||||
if (value === 'boolean') {
|
||||
if (!props.value.value) props.value.value = []
|
||||
}
|
||||
},
|
||||
{ immediate: true })
|
||||
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.indicator-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label) {
|
||||
line-height: 1;
|
||||
|
||||
>label {
|
||||
font-size: 12px;
|
||||
|
||||
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(input) {
|
||||
height: 22px;
|
||||
}
|
||||
</style>
|
|
@ -77,5 +77,8 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
}
|
||||
:deep(input) {
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<a-popover placement="left" trigger="click">
|
||||
<template #title>
|
||||
<div class="edit-title" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="width: 150px;">{{ config.name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div style="max-width: 400px;" class="ant-form-vertical">
|
||||
<a-form-item v-for="item in config.properties" :name="name.concat([item.property])" :label="item.name">
|
||||
<a-select v-model:value="value[item.property]" :options="item.type?.elements?.map((e: { 'text': string, 'value': string }) => ({
|
||||
label: e.text,
|
||||
value: e.value,
|
||||
}))" size="small"></a-select>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</template>
|
||||
<a-button type="dashed" block>
|
||||
存储配置<edit-outlined class="item-icon" />
|
||||
</a-button>
|
||||
</a-popover>
|
||||
</template>
|
||||
<script setup lang="ts" name="ConfigParam">
|
||||
import { PropType } from 'vue';
|
||||
import { EditOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
type ValueType = Record<any, any>;
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
name: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => ([]),
|
||||
required: true
|
||||
},
|
||||
config: {
|
||||
type: Array as PropType<ValueType>,
|
||||
default: () => ({ properties: [] })
|
||||
}
|
||||
})
|
||||
|
||||
// interface Emits {
|
||||
// (e: 'update:value', data: string | undefined): void;
|
||||
// }
|
||||
// const emit = defineEmits<Emits>()
|
||||
|
||||
// const _value = computed({
|
||||
// get: () => props.value,
|
||||
// set: (val: string | undefined) => {
|
||||
// emit('update:value', val)
|
||||
// }
|
||||
// })
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.item-icon {
|
||||
color: rgb(136, 136, 136);
|
||||
font-size: 12px;
|
||||
}
|
||||
:deep(.ant-form-item-label) {
|
||||
>label {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
:deep(.ant-select) {
|
||||
font-size: 12px;
|
||||
}
|
||||
:deep(input) {
|
||||
height: 22px;
|
||||
}
|
||||
</style>
|
|
@ -157,4 +157,8 @@ const handleAdd = () => {
|
|||
:deep(.ant-select) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(input) {
|
||||
height: 22px;
|
||||
}
|
||||
</style>
|
|
@ -19,8 +19,8 @@
|
|||
{ required: true, message: '请输入标识' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_]+$/,
|
||||
message: '请输入英文或者数字或者-或者_',
|
||||
pattern: /^[a-zA-Z0-9_\-]+$/,
|
||||
message: 'ID只能由数字、字母、下划线、中划线组成',
|
||||
},
|
||||
]">
|
||||
<a-input v-model:value="_value[index].id" size="small"></a-input>
|
||||
|
@ -169,4 +169,8 @@ const handleAdd = () => {
|
|||
:deep(.ant-select) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(input) {
|
||||
height: 22px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,202 @@
|
|||
<template>
|
||||
<div class="json-param">
|
||||
<div class="list-item" v-for="(item, index) in _value" :key="`object_${index}`">
|
||||
<div class="item-left">
|
||||
<menu-outlined class="item-drag item-icon" />
|
||||
{{ `#${index + 1}.` }}
|
||||
</div>
|
||||
<div class="item-middle item-editable">
|
||||
<a-popover :visible="editIndex === index" placement="top" @visible-change="change" trigger="click">
|
||||
<template #title>
|
||||
<div class="edit-title" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="width: 150px;">配置参数</div>
|
||||
<close-outlined @click="handleClose" />
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div>
|
||||
<a-form :model="_value[index]" layout="vertical">
|
||||
<a-form-item label="标识" name="id" :rules="[
|
||||
{ required: true, message: '请输入标识' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_\-]+$/,
|
||||
message: 'ID只能由数字、字母、下划线、中划线组成',
|
||||
},
|
||||
]">
|
||||
<a-input v-model:value="_value[index].id" size="small"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="名称" name="name" :rules="[
|
||||
{ required: true, message: '请输入名称' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
]">
|
||||
<a-input v-model:value="_value[index].name" size="small"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="指标值" name="value" :rules="[
|
||||
{ required: true, message: '请输入指标值' },
|
||||
{ validator: () => validateIndicator(_value[index]), message: '请输入指标值' }
|
||||
]">
|
||||
<JIndicators v-model:value="_value[index]" :type="type" size="small" :enum="enum"/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
<div class="item-edit" @click="handleEdit(index)">
|
||||
{{ item.name || '配置参数' }}
|
||||
<edit-outlined class="item-icon" />
|
||||
</div>
|
||||
</a-popover>
|
||||
</div>
|
||||
<div class="item-right">
|
||||
<delete-outlined @click="handleDelete(index)" />
|
||||
</div>
|
||||
</div>
|
||||
<a-button type="dashed" block @click="handleAdd">
|
||||
<template #icon><plus-outlined class="item-icon" /></template>
|
||||
添加指标
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="MetricsParam">
|
||||
import { PropType } from 'vue'
|
||||
import { MenuOutlined, EditOutlined, DeleteOutlined, PlusOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
||||
import JIndicators from '@/components/JIndicators/index.vue';
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', data: Record<any, any>[]): void;
|
||||
}
|
||||
const emit = defineEmits<Emits>()
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object as PropType<Record<any, any>[]>,
|
||||
default: () => ([])
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
enum: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const _value = ref<Record<any, any>[]>([])
|
||||
watchEffect(() => {
|
||||
_value.value = props.value
|
||||
})
|
||||
|
||||
watch(_value,
|
||||
() => {
|
||||
emit('update:value', _value.value)
|
||||
},
|
||||
{ deep: true })
|
||||
|
||||
const editIndex = ref<number>(-1)
|
||||
const handleEdit = (index: number) => {
|
||||
editIndex.value = index
|
||||
}
|
||||
const handleDelete = (index: number) => {
|
||||
editIndex.value = -1
|
||||
_value.value.splice(index, 1)
|
||||
}
|
||||
const handleClose = () => {
|
||||
editIndex.value = -1
|
||||
}
|
||||
const handleAdd = () => {
|
||||
_value.value.push({})
|
||||
emit('update:value', _value.value)
|
||||
}
|
||||
|
||||
const validateIndicator = (value: any) => {
|
||||
if (value?.range) {
|
||||
if (!value?.value || !value?.value[0] || !value?.value[1]) {
|
||||
return Promise.reject(new Error('请输入指标值'));
|
||||
}
|
||||
} else {
|
||||
if (value?.value === '' || value?.value === undefined) {
|
||||
return Promise.reject(new Error('请输入指标值'));
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const change = (visible: boolean) => {
|
||||
if (!visible) {
|
||||
editIndex.value = -1
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.json-param {
|
||||
.list-item {
|
||||
border: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
padding: 3px 6px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #fff;
|
||||
line-height: 26px;
|
||||
font-size: 14px;
|
||||
|
||||
// .item-left {
|
||||
// .item-drag {
|
||||
// cursor: move;
|
||||
// }
|
||||
// }
|
||||
.item-edit {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
color: rgb(136, 136, 136);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label) {
|
||||
line-height: 1;
|
||||
|
||||
>label {
|
||||
font-size: 12px;
|
||||
|
||||
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-explain) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-with-help) {
|
||||
.ant-form-item-explain {
|
||||
min-height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&.ant-form-item-with-help {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-input),
|
||||
:deep(.ant-select) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(input) {
|
||||
height: 22px;
|
||||
}
|
||||
</style>
|
|
@ -12,6 +12,8 @@ import JUpload from './JUpload/index.vue'
|
|||
import { BasicLayoutPage, BlankLayoutPage, PageContainer } from './Layout'
|
||||
import Ellipsis from './Ellipsis/index.vue'
|
||||
import JEmpty from './Empty/index.vue'
|
||||
import AMapComponent from './AMapComponent/index.vue'
|
||||
import PathSimplifier from './AMapComponent/PathSimplifier.vue'
|
||||
|
||||
export default {
|
||||
install(app: App) {
|
||||
|
@ -30,5 +32,7 @@ export default {
|
|||
.component('PageContainer', PageContainer)
|
||||
.component('Ellipsis', Ellipsis)
|
||||
.component('JEmpty', JEmpty)
|
||||
.component('AMapComponent', AMapComponent)
|
||||
.component('PathSimplifier', PathSimplifier)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export const useAlarmStore = defineStore('alarm',()=>{
|
||||
const data = reactive({
|
||||
tab: 'all',
|
||||
current: {},
|
||||
solveVisible: false,
|
||||
logVisible: false,
|
||||
defaultLevel: [],
|
||||
columns: [
|
||||
{
|
||||
dataIndex: 'alarmConfigName',
|
||||
title: '告警名称',
|
||||
// hideInSearch: true,
|
||||
},
|
||||
{
|
||||
dataIndex: 'alarmTime',
|
||||
title: '告警时间',
|
||||
valueType: 'dateTime',
|
||||
},
|
||||
{
|
||||
dataIndex: 'description',
|
||||
title: '说明',
|
||||
// hideInSearch: true,
|
||||
},
|
||||
{
|
||||
dataIndex: 'action',
|
||||
title: '操作',
|
||||
hideInSearch: true,
|
||||
valueType: 'option',
|
||||
},
|
||||
],
|
||||
})
|
||||
return {
|
||||
data
|
||||
}
|
||||
})
|
|
@ -44,6 +44,5 @@ export const SystemConst = {
|
|||
REFRESH_METADATA_TABLE: 'refresh_metadata_table',
|
||||
GET_METADATA: 'get_metadata',
|
||||
REFRESH_DEVICE: 'refresh_device',
|
||||
AMAP_KEY: 'amap_key',
|
||||
VERSION_CODE: 'version_code',
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
instanceStore.current.firmwareInfo?.version
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="连接协议">{{
|
||||
instanceStore.current.protocolName
|
||||
instanceStore.current?.protocolName
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="消息协议">{{
|
||||
instanceStore.current.transport
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
<template>
|
||||
<a-card>
|
||||
<Search
|
||||
:columns="columns"
|
||||
target="device-instance-log"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<JTable
|
||||
ref="instanceRefLog"
|
||||
:columns="columns"
|
||||
:request="(e: Record<string, any>) => queryLog(instanceStore.current.id, e)"
|
||||
model="TABLE"
|
||||
:defaultParams="{ sorts: [{ name: 'timestamp', order: 'desc' }] }"
|
||||
:params="params"
|
||||
>
|
||||
<template #type="slotProps">
|
||||
{{ slotProps?.type?.text }}
|
||||
</template>
|
||||
<template #timestamp="slotProps">
|
||||
{{
|
||||
slotProps.timestamp
|
||||
? moment(slotProps.timestamp).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)
|
||||
: ''
|
||||
}}
|
||||
</template>
|
||||
<template #action="slotProps">
|
||||
<a-space>
|
||||
<template
|
||||
v-for="i in getActions(slotProps, 'table')"
|
||||
:key="i.key"
|
||||
>
|
||||
<a-button
|
||||
@click="i.onClick"
|
||||
type="link"
|
||||
style="padding: 0px"
|
||||
>
|
||||
<template #icon><AIcon :type="i.icon" /></template>
|
||||
</a-button>
|
||||
</template>
|
||||
</a-space>
|
||||
</template>
|
||||
</JTable>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ActionsType } from '@/components/Table';
|
||||
import { queryLog, queryLogsType } from '@/api/device/instance';
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
import moment from 'moment';
|
||||
import { Modal, Textarea } from 'ant-design-vue';
|
||||
|
||||
const params = ref<Record<string, any>>({});
|
||||
const instanceStore = useInstanceStore();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
scopedSlots: true,
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: () =>
|
||||
new Promise((resolve) => {
|
||||
queryLogsType().then((resp: any) => {
|
||||
resolve(
|
||||
resp.result.map((item: any) => ({
|
||||
label: item.text,
|
||||
value: item.value,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
scopedSlots: true,
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '内容',
|
||||
ellipsis: true,
|
||||
dataIndex: 'content',
|
||||
key: 'content',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 250,
|
||||
scopedSlots: true,
|
||||
},
|
||||
];
|
||||
|
||||
const getActions = (
|
||||
data: Partial<Record<string, any>>,
|
||||
type: 'card' | 'table',
|
||||
): ActionsType[] => {
|
||||
if (!data) return [];
|
||||
return [
|
||||
{
|
||||
key: 'view',
|
||||
text: '查看',
|
||||
tooltip: {
|
||||
title: '查看',
|
||||
},
|
||||
icon: 'SearchOutlined',
|
||||
onClick: () => {
|
||||
let content = '';
|
||||
try {
|
||||
content = JSON.stringify(
|
||||
JSON.parse(data.content),
|
||||
null,
|
||||
2,
|
||||
);
|
||||
} catch (error) {
|
||||
content = data.content;
|
||||
}
|
||||
Modal.info({
|
||||
title: '详细信息',
|
||||
width: 700,
|
||||
content: h(Textarea, {
|
||||
bordered: false,
|
||||
rows: 15,
|
||||
value: content
|
||||
}),
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const handleSearch = (_params: any) => {
|
||||
params.value = _params;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
|
@ -1,54 +0,0 @@
|
|||
<!-- 坐标点拾取组件 -->
|
||||
<template>
|
||||
<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>
|
||||
|
||||
<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>
|
|
@ -3,12 +3,14 @@
|
|||
<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-button type="primary" @click="onStart">开始动画</a-button>
|
||||
<a-button type="primary" @click="onStop">停止动画</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
<AMap :points="geoList" />
|
||||
<AMapComponent style="height: 500px">
|
||||
<PathSimplifier :pathData="geoList" ref="amapPath"></PathSimplifier>
|
||||
</AMapComponent>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
|
@ -16,7 +18,6 @@
|
|||
import { getPropertyData } from '@/api/device/instance';
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
import encodeQuery from '@/utils/encodeQuery';
|
||||
import AMap from './AMap.vue';
|
||||
|
||||
const instanceStore = useInstanceStore();
|
||||
|
||||
|
@ -33,6 +34,15 @@ const prop = defineProps({
|
|||
|
||||
const geoList = ref<any[]>([]);
|
||||
const loading = ref<boolean>(false);
|
||||
const amapPath = ref()
|
||||
|
||||
const onStart = () => {
|
||||
amapPath.value.start()
|
||||
}
|
||||
|
||||
const onStop = () => {
|
||||
amapPath.value.stop()
|
||||
}
|
||||
|
||||
const query = async () => {
|
||||
loading.value = true;
|
||||
|
@ -53,7 +63,10 @@ const query = async () => {
|
|||
((resp.result as any)?.data || []).forEach((item: any) => {
|
||||
list.push([item.value.lon, item.value.lat]);
|
||||
});
|
||||
geoList.value = list
|
||||
geoList.value = [{
|
||||
name: prop?.data?.name,
|
||||
path: list
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<a-modal title="详情" visible width="50vw" @ok="onCancel" @cancel="onCancel">
|
||||
<div style="margin-bottom: 10px"><TimeComponent v-model="dateValue" /></div>
|
||||
<div>
|
||||
<a-tabs v-model:activeKey="activeKey" style="max-height: 600px; overflow-y: auto">
|
||||
<a-tabs :destroyInactiveTabPane="true" v-model:activeKey="activeKey" style="max-height: 600px; overflow-y: auto">
|
||||
<a-tab-pane key="table" tab="列表">
|
||||
<Table :data="props.data" :time="_getTimes" />
|
||||
</a-tab-pane>
|
||||
|
|
|
@ -116,6 +116,7 @@ import Function from './Function/index.vue';
|
|||
import Modbus from './Modbus/index.vue';
|
||||
import OPCUA from './OPCUA/index.vue';
|
||||
import EdgeMap from './EdgeMap/index.vue';
|
||||
import Log from './Log/index.vue'
|
||||
import { _deploy, _disconnect } from '@/api/device/instance';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
|
@ -147,6 +148,10 @@ const list = ref([
|
|||
key: 'Metadata',
|
||||
tab: '物模型',
|
||||
},
|
||||
{
|
||||
key: 'Log',
|
||||
tab: '日志管理',
|
||||
},
|
||||
{
|
||||
key: 'Function',
|
||||
tab: '设备功能',
|
||||
|
@ -167,6 +172,7 @@ const tabs = {
|
|||
Modbus,
|
||||
OPCUA,
|
||||
EdgeMap,
|
||||
Log
|
||||
};
|
||||
|
||||
const getStatus = (id: string) => {
|
||||
|
|
|
@ -694,7 +694,6 @@ const saveBtn = () => {
|
|||
};
|
||||
|
||||
const handleSearch = (_params: any) => {
|
||||
console.log(_params);
|
||||
params.value = _params;
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
<template #card="slotProps">
|
||||
<CardBox
|
||||
:value="slotProps"
|
||||
@click="handleClick"
|
||||
:actions="getActions(slotProps, 'card')"
|
||||
v-bind="slotProps"
|
||||
:active="_selectedRowKeys.includes(slotProps.id)"
|
||||
|
@ -272,14 +271,14 @@ const cancelSelect = () => {
|
|||
_selectedRowKeys.value = [];
|
||||
};
|
||||
|
||||
const handleClick = (dt: any) => {
|
||||
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 handleClick = (dt: any) => {
|
||||
// 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>>,
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<template></template>
|
||||
<script setup lang="ts" name="CommonForm"></script>
|
|
@ -15,12 +15,35 @@
|
|||
]">
|
||||
<a-input v-model:value="value.name" size="small"></a-input>
|
||||
</a-form-item>
|
||||
<value-type-form :name="['valueType']" v-model:value="value.valueType" key="property"
|
||||
@change-type="changeValueType"></value-type-form>
|
||||
{{ modelType }}
|
||||
<template v-if="modelType === 'properties'">
|
||||
<value-type-form :name="['valueType']" v-model:value="value.valueType" key="property"
|
||||
@change-type="changeValueType"></value-type-form>
|
||||
|
||||
<expands-form :name="['expands']" v-model:value="value.expands" :type="type" :id="value.id" :config="config"
|
||||
:valueType="value.valueType"></expands-form>
|
||||
<expands-form :name="['expands']" v-model:value="value.expands" :type="type" :id="value.id" :config="config"
|
||||
:valueType="value.valueType"></expands-form>
|
||||
</template>
|
||||
<template v-if="modelType === 'functions'">
|
||||
<a-form-item label="是否异步" name="async" :rules="[
|
||||
{ required: true, message: '请选择是否异步' },
|
||||
]">
|
||||
<a-radio-group v-model:value="value.async">
|
||||
<a-radio :value="true">是</a-radio>
|
||||
<a-radio :value="false">否</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="输入参数" name="inputs" :rules="[
|
||||
{ required: true, message: '请输入输入参数' },
|
||||
]">
|
||||
<JsonParam v-model:value="value.inputs" :name="['inputs']"></JsonParam>
|
||||
</a-form-item>
|
||||
<a-form-item label="输出参数" name="output">
|
||||
<JsonParam v-model:value="value.output" :name="['output']"></JsonParam>
|
||||
|
||||
<value-type-form :name="['output']" v-model:value="value.valueType" key="function"
|
||||
@change-type="changeValueType"></value-type-form>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label="说明" name="description" :rules="[
|
||||
{ max: 200, message: '最多可输入200个字符' },
|
||||
]">
|
||||
|
@ -33,6 +56,7 @@ import ExpandsForm from './ExpandsForm.vue';
|
|||
import ValueTypeForm from './ValueTypeForm.vue'
|
||||
import { useProductStore } from '@/store/product';
|
||||
import { getMetadataConfig } from '@/api/device/product'
|
||||
import JsonParam from '@/components/Metadata/JsonParam/index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
|
@ -43,6 +67,10 @@ const props = defineProps({
|
|||
value: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
modelType: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const productStore = useProductStore()
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<a-button :loading="save.loading" type="primary" @click="save.saveMetadata">保存</a-button>
|
||||
</template>
|
||||
<a-form ref="formRef" :model="form.model" layout="vertical">
|
||||
<PropertyForm v-if="metadataStore.model.type === 'properties'" :type="type" ref="propertyForm" v-model:value="form.model"></PropertyForm>
|
||||
<PropertyForm :model-type="metadataStore.model.type" :type="type" ref="propertyForm" v-model:value="form.model"></PropertyForm>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
@ -33,6 +33,11 @@ const props = defineProps({
|
|||
type: String
|
||||
}
|
||||
})
|
||||
interface Emits {
|
||||
(e: 'refresh'): void;
|
||||
}
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const instanceStore = useInstanceStore()
|
||||
|
@ -95,6 +100,7 @@ const save = reactive({
|
|||
detail.metadata = metadata
|
||||
productStore.setCurrent(detail)
|
||||
}
|
||||
emit('refresh')
|
||||
}
|
||||
const _data = updateMetadata(type, [formValue], _detail, updateStore)
|
||||
const result = await asyncUpdateMetadata(props.type, _data)
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
</template>
|
||||
新增
|
||||
</PermissionButton>
|
||||
<Edit v-if="metadataStore.model.edit" :type="target" :tabs="type"></Edit>
|
||||
<Edit v-if="metadataStore.model.edit" :type="target" :tabs="type" @refresh="refreshMetadata"></Edit>
|
||||
</template>
|
||||
<template #level="slotProps">
|
||||
{{ levelMap[slotProps.expands?.level] || '-' }}
|
||||
|
@ -124,7 +124,7 @@ onMounted(() => {
|
|||
|
||||
})
|
||||
|
||||
watch([route.params.id, type], () => {
|
||||
const refreshMetadata = () => {
|
||||
loading.value = true
|
||||
// const res = target === 'product'
|
||||
// ? await productDetail(route.params.id as string)
|
||||
|
@ -133,7 +133,8 @@ watch([route.params.id, type], () => {
|
|||
const item = JSON.parse(result || '{}') as MetadataItem[]
|
||||
data.value = item[type]?.sort((a: any, b: any) => b?.sortsIndex - a?.sortsIndex)
|
||||
loading.value = false
|
||||
}, { immediate: true })
|
||||
}
|
||||
watch([route.params.id, type], refreshMetadata, { immediate: true })
|
||||
|
||||
const metadataStore = useMetadataStore()
|
||||
const handleAddClick = () => {
|
||||
|
|
|
@ -278,6 +278,7 @@ import { ValidateErrorEntity } from 'ant-design-vue/es/form/interface';
|
|||
import { message } from 'ant-design-vue';
|
||||
import { LocalStore } from '@/utils/comm';
|
||||
import { TOKEN_KEY } from '@/utils/variable';
|
||||
import { SystemConst } from '@/utils/consts'
|
||||
const formRef = ref();
|
||||
const menuRef = ref();
|
||||
const formBasicRef = ref();
|
||||
|
@ -352,6 +353,7 @@ const saveBasicInfo = () =>{
|
|||
const res = await save(item);
|
||||
if (res.status === 200) {
|
||||
resolve(true);
|
||||
localStorage.setItem(SystemConst.AMAP_KEY,form.value.apikey);
|
||||
const ico: any = document.querySelector('link[rel="icon"]');
|
||||
if (ico !== null) {
|
||||
ico.href = form.value.ico;
|
||||
|
|
|
@ -0,0 +1,323 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<Search
|
||||
:columns="columns"
|
||||
target="notice-config"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<JTable
|
||||
ref="listRef"
|
||||
:columns="columns"
|
||||
:request="DeviceApi.list"
|
||||
:defaultParams="{
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
}"
|
||||
:params="params"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<a-button type="primary" @click="handleAdd"> 新增 </a-button>
|
||||
</template>
|
||||
<template #card="slotProps">
|
||||
<CardBox
|
||||
:value="slotProps"
|
||||
:actions="getActions(slotProps, 'card')"
|
||||
v-bind="slotProps"
|
||||
:showStatus="true"
|
||||
:status="
|
||||
slotProps.state.value === 'online' ? 'success' : 'error'
|
||||
"
|
||||
:statusText="slotProps.state.text"
|
||||
:statusNames="{ success: 'success', error: 'error' }"
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img :src="getImage('/device-media.png')" />
|
||||
</slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<h3 class="card-item-content-title">
|
||||
{{ slotProps.name }}
|
||||
</h3>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">厂商</div>
|
||||
<div>{{ slotProps.manufacturer }}</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
通道数量
|
||||
</div>
|
||||
<div>{{ slotProps.channelNumber }}</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">型号</div>
|
||||
<div>{{ slotProps.model }}</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
接入方式
|
||||
</div>
|
||||
<!-- <div>
|
||||
{{ providerType[slotProps.provider] }}
|
||||
</div> -->
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
<template #actions="item">
|
||||
<a-tooltip
|
||||
v-bind="item.tooltip"
|
||||
:title="item.disabled && item.tooltip.title"
|
||||
>
|
||||
<a-popconfirm
|
||||
v-if="item.popConfirm"
|
||||
v-bind="item.popConfirm"
|
||||
:disabled="item.disabled"
|
||||
>
|
||||
<a-button :disabled="item.disabled">
|
||||
<AIcon type="DeleteOutlined" />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<template v-else>
|
||||
<a-button
|
||||
:disabled="item.disabled"
|
||||
@click="item.onClick"
|
||||
>
|
||||
<AIcon :type="item.icon" />
|
||||
<span>{{ item.text }}</span>
|
||||
</a-button>
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
<template #action="slotProps">
|
||||
<a-space :size="16">
|
||||
<a-tooltip
|
||||
v-for="i in getActions(slotProps, 'table')"
|
||||
: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
|
||||
@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>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DeviceApi from '@/api/media/device';
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { PROVIDER_OPTIONS } from '@/views/media/Device/const';
|
||||
|
||||
import { useMenuStore } from 'store/menu';
|
||||
|
||||
const menuStory = useMenuStore();
|
||||
|
||||
const listRef = ref<Record<string, any>>({});
|
||||
const params = ref<Record<string, any>>({});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '上级SIP ID',
|
||||
dataIndex: 'sipConfigs',
|
||||
key: 'sipConfigs',
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '上级SIP 地址',
|
||||
dataIndex: 'sipConfigs',
|
||||
key: 'sipConfigs',
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '通道数量',
|
||||
dataIndex: 'count',
|
||||
key: 'count',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '正常', value: 'enabled' },
|
||||
{ label: '禁用', value: 'disabled' },
|
||||
],
|
||||
handleValue: (v: any) => {
|
||||
return v;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '级联状态',
|
||||
dataIndex: 'onlineStatus',
|
||||
key: 'onlineStatus',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '已连接', value: 'online' },
|
||||
{ label: '未连接', value: 'offline' },
|
||||
],
|
||||
handleValue: (v: any) => {
|
||||
return v;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
scopedSlots: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* @param params
|
||||
*/
|
||||
const handleSearch = (e: any) => {
|
||||
// console.log('handleSearch:', e);
|
||||
params.value = e;
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
const handleAdd = () => {
|
||||
menuStory.jumpPage('media/Device/Save', {
|
||||
id: ':id',
|
||||
});
|
||||
};
|
||||
|
||||
const getActions = (
|
||||
data: Partial<Record<string, any>>,
|
||||
type: 'card' | 'table',
|
||||
): ActionsType[] => {
|
||||
if (!data) return [];
|
||||
const actions = [
|
||||
{
|
||||
key: 'edit',
|
||||
text: '编辑',
|
||||
tooltip: {
|
||||
title: '编辑',
|
||||
},
|
||||
icon: 'EditOutlined',
|
||||
onClick: () => {
|
||||
menuStory.jumpPage(
|
||||
'media/Device/Save',
|
||||
{},
|
||||
{
|
||||
id: data.id,
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'view',
|
||||
text: '查看通道',
|
||||
tooltip: {
|
||||
title: '查看通道',
|
||||
},
|
||||
icon: 'PartitionOutlined',
|
||||
onClick: () => {
|
||||
// router.push(
|
||||
// `/media/device/Channel?id=${data.id}&type=${data.provider}`,
|
||||
// );
|
||||
menuStory.jumpPage(
|
||||
'media/Device/Channel',
|
||||
{},
|
||||
{
|
||||
id: data.id,
|
||||
type: data.provider,
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'debug',
|
||||
text: '更新通道',
|
||||
tooltip: {
|
||||
title:
|
||||
data.provider === 'fixed-media'
|
||||
? '固定地址无法更新通道'
|
||||
: data.state.value === 'offline'
|
||||
? '设备已离线'
|
||||
: data.state.value === 'notActive'
|
||||
? '设备已禁用'
|
||||
: '',
|
||||
},
|
||||
disabled:
|
||||
data.state.value === 'offline' ||
|
||||
data.state.value === 'notActive' ||
|
||||
data.provider === 'fixed-media',
|
||||
icon: 'SyncOutlined',
|
||||
onClick: () => {
|
||||
// updateChannel()
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
text: '删除',
|
||||
tooltip: {
|
||||
title: '在线设备无法删除',
|
||||
},
|
||||
disabled: data.state.value === 'online',
|
||||
popConfirm: {
|
||||
title: '确认删除?',
|
||||
onConfirm: async () => {
|
||||
const resp = await DeviceApi.del(data.id);
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
listRef.value?.reload();
|
||||
} else {
|
||||
message.error('操作失败!');
|
||||
}
|
||||
},
|
||||
},
|
||||
icon: 'DeleteOutlined',
|
||||
},
|
||||
];
|
||||
return actions;
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,41 @@
|
|||
type BaseItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
value: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type SipConfig = {
|
||||
catalogEach: number;
|
||||
charset: string;
|
||||
clusterNodeId: string;
|
||||
domain: string;
|
||||
firmware: string;
|
||||
hostAndPort: string;
|
||||
keepaliveInterval: number;
|
||||
keepaliveTimeoutTimes: number;
|
||||
localAddress: string;
|
||||
localSipId: string;
|
||||
manufacturer: string;
|
||||
model: string;
|
||||
name: string;
|
||||
password: string;
|
||||
port: number;
|
||||
publicAddress: string;
|
||||
publicPort: number;
|
||||
sipId: string;
|
||||
stackName: string;
|
||||
transport: string;
|
||||
user: string;
|
||||
};
|
||||
type CascadeItem = {
|
||||
mediaServerId: string;
|
||||
onlineStatus: State;
|
||||
proxyStream: boolean;
|
||||
sipConfigs: Partial<SipConfig>[];
|
||||
status: State;
|
||||
count?: number;
|
||||
} & BaseItem;
|
|
@ -12,7 +12,18 @@
|
|||
<a-form ref="formRef" :model="formData" layout="vertical">
|
||||
<a-row :gutter="10">
|
||||
<a-col :span="12">
|
||||
<a-form-item name="channelId">
|
||||
<a-form-item
|
||||
name="channelId"
|
||||
:rules="[
|
||||
{
|
||||
max: 64,
|
||||
message: '最多可输入64个字符',
|
||||
},
|
||||
{
|
||||
validator: validateChannelId,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template #label>
|
||||
通道ID
|
||||
<a-tooltip title="若不填写,系统将自动生成唯一ID">
|
||||
|
@ -22,22 +33,35 @@
|
|||
/>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model:value="formData.channelId" />
|
||||
<a-input
|
||||
v-model:value="formData.channelId"
|
||||
:disabled="!!formData.id"
|
||||
placeholder="请输入通道ID"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
name="name"
|
||||
label="通道名称"
|
||||
:rules="{ required: true, message: '请输入通道名称' }"
|
||||
:rules="[
|
||||
{ required: true, message: '请输入通道名称' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
]"
|
||||
>
|
||||
<a-input v-model:value="formData.name" />
|
||||
<a-input
|
||||
v-model:value="formData.name"
|
||||
placeholder="请输入通道名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
name="media_url"
|
||||
:rules="{ required: true, message: '请输入视频地址' }"
|
||||
:rules="[
|
||||
{ required: true, message: '请输入视频地址' },
|
||||
{ max: 128, message: '最多可输入128个字符' },
|
||||
]"
|
||||
>
|
||||
<template #label>
|
||||
视频地址
|
||||
|
@ -50,26 +74,42 @@
|
|||
/>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model:value="formData.others.media_url" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item name="media_username" label="用户名">
|
||||
<a-input
|
||||
v-model:value="formData.others.media_username"
|
||||
v-model:value="formData.media_url"
|
||||
placeholder="请输入视频地址"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item name="media_password" label="密码">
|
||||
<a-form-item
|
||||
name="media_username"
|
||||
label="用户名"
|
||||
:rules="{ max: 64, message: '最多可输入64个字符' }"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formData.media_username"
|
||||
placeholder="请输入用户名"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
name="media_password"
|
||||
label="密码"
|
||||
:rules="{ max: 64, message: '最多可输入64个字符' }"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="formData.others.media_password"
|
||||
v-model:value="formData.media_password"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item name="address" label="安装地址">
|
||||
<a-input v-model:value="formData.address" />
|
||||
<a-input
|
||||
v-model:value="formData.address"
|
||||
placeholder="请输入安装地址"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
|
@ -88,13 +128,18 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import templateApi from '@/api/notice/template';
|
||||
import ChannelApi from '@/api/media/channel';
|
||||
import { PropType } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
type Emits = {
|
||||
(e: 'update:visible', data: boolean): void;
|
||||
(e: 'submit'): void;
|
||||
};
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -112,28 +157,95 @@ const _vis = computed({
|
|||
|
||||
const formRef = ref();
|
||||
const formData = ref({
|
||||
id: '',
|
||||
id: undefined,
|
||||
address: '',
|
||||
channelId: '',
|
||||
description: '',
|
||||
deviceId: '',
|
||||
deviceId: route.query.id,
|
||||
name: '',
|
||||
others: {
|
||||
media_password: '',
|
||||
media_url: '',
|
||||
media_username: '',
|
||||
},
|
||||
// 以下三个字段, 提交时需提取到others字段当中
|
||||
media_password: '',
|
||||
media_url: '',
|
||||
media_username: '',
|
||||
});
|
||||
// const formRules = ref({});
|
||||
|
||||
watch(
|
||||
() => props.channelData,
|
||||
(val: any) => {
|
||||
const {
|
||||
id,
|
||||
address,
|
||||
channelId,
|
||||
description,
|
||||
deviceId,
|
||||
name,
|
||||
others,
|
||||
...extra
|
||||
} = val;
|
||||
formData.value = {
|
||||
id,
|
||||
address,
|
||||
channelId,
|
||||
description,
|
||||
deviceId,
|
||||
name,
|
||||
...others,
|
||||
};
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
/**
|
||||
* 通道ID字段验证是否存在
|
||||
* @param _rule
|
||||
* @param value
|
||||
*/
|
||||
let validateChannelId = async (_rule: Rule, value: string) => {
|
||||
const { result } = await ChannelApi.validateField({
|
||||
deviceId: route.query.id,
|
||||
channelId: value,
|
||||
});
|
||||
|
||||
if (!result.passed) {
|
||||
return Promise.reject('该ID已存在');
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 提交
|
||||
*/
|
||||
const btnLoading = ref<boolean>(false);
|
||||
const handleSubmit = () => {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(async () => {
|
||||
emit('submit');
|
||||
const {
|
||||
media_url,
|
||||
media_password,
|
||||
media_username,
|
||||
...extraFormData
|
||||
} = formData.value;
|
||||
if (media_url || media_password || media_username) {
|
||||
extraFormData.others = {
|
||||
media_url,
|
||||
media_password,
|
||||
media_username,
|
||||
};
|
||||
}
|
||||
btnLoading.value = true;
|
||||
const res = formData.value.id
|
||||
? await ChannelApi.update(formData.value.id, extraFormData)
|
||||
: await ChannelApi.save(extraFormData);
|
||||
btnLoading.value = false;
|
||||
if (res.success) {
|
||||
message.success('操作成功');
|
||||
_vis.value = false;
|
||||
emit('submit');
|
||||
} else {
|
||||
message.error('操作失败');
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.log('err: ', err);
|
||||
|
@ -142,6 +254,14 @@ const handleSubmit = () => {
|
|||
const handleCancel = () => {
|
||||
_vis.value = false;
|
||||
};
|
||||
watch(
|
||||
() => _vis.value,
|
||||
(val) => {
|
||||
if (!val) {
|
||||
formRef.value.resetFields();
|
||||
// 以下字段非表单所填, 重置字段需手动置空
|
||||
formData.value.id = undefined;
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<!-- 视频设备-通道列表 -->
|
||||
<template>
|
||||
<page-container>
|
||||
<Search
|
||||
|
@ -91,6 +92,7 @@ import type { ActionsType } from '@/components/Table/index.vue';
|
|||
import { useMenuStore } from 'store/menu';
|
||||
import { message } from 'ant-design-vue';
|
||||
import Save from './Save.vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
const menuStory = useMenuStore();
|
||||
const route = useRoute();
|
||||
|
@ -158,9 +160,7 @@ const params = ref<Record<string, any>>({});
|
|||
* @param params
|
||||
*/
|
||||
const handleSearch = (e: any) => {
|
||||
// console.log('handleSearch e:', e);
|
||||
params.value = e;
|
||||
// console.log('params.value: ', params.value);
|
||||
};
|
||||
|
||||
const saveVis = ref(false);
|
||||
|
@ -172,6 +172,11 @@ const listRef = ref();
|
|||
const playVis = ref(false);
|
||||
const channelData = ref();
|
||||
|
||||
/**
|
||||
* 表格操作按钮
|
||||
* @param data 表格数据项
|
||||
* @param type 表格展示类型
|
||||
*/
|
||||
const getActions = (
|
||||
data: Partial<Record<string, any>>,
|
||||
type: 'card' | 'table',
|
||||
|
@ -186,8 +191,8 @@ const getActions = (
|
|||
},
|
||||
icon: 'EditOutlined',
|
||||
onClick: () => {
|
||||
channelData.value = cloneDeep(data);
|
||||
saveVis.value = true;
|
||||
channelData.value = data;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -246,5 +251,3 @@ const getActions = (
|
|||
: actions;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
|
@ -70,7 +70,9 @@
|
|||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">说明</div>
|
||||
<div>{{ slotProps.description }}</div>
|
||||
<Ellipsis>
|
||||
{{ slotProps.description }}
|
||||
</Ellipsis>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
|
|
@ -70,7 +70,9 @@
|
|||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">说明</div>
|
||||
<div>{{ slotProps.description }}</div>
|
||||
<Ellipsis>
|
||||
{{ slotProps.description }}
|
||||
</Ellipsis>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,316 @@
|
|||
<template>
|
||||
<div class="alarm-log-card">
|
||||
<Search
|
||||
:columns="columns"
|
||||
target="alarm-log"
|
||||
v-if="['all', 'detail'].includes(props.type)"
|
||||
@search="search"
|
||||
></Search>
|
||||
<Search
|
||||
:columns="produtCol"
|
||||
target="alarm-log"
|
||||
v-if="['product', 'other'].includes(props.type)"
|
||||
@search="search"
|
||||
></Search>
|
||||
<Search
|
||||
:columns="deviceCol"
|
||||
target="alarm-log"
|
||||
v-if="props.type === 'device'"
|
||||
@search="search"
|
||||
></Search>
|
||||
<Search
|
||||
:columns="orgCol"
|
||||
target="alarm-log"
|
||||
v-if="props.type === 'org'"
|
||||
@search="search"
|
||||
></Search>
|
||||
<JTable :columns="columns" :request="handleSearch" :params="params">
|
||||
<template #card="slotProps">
|
||||
<CardBox
|
||||
:value="slotProps"
|
||||
v-bind="slotProps"
|
||||
:statusText="
|
||||
data.defaultLevel.find(
|
||||
(i) => i.level === slotProps.level,
|
||||
)?.title || slotProps.level
|
||||
"
|
||||
>
|
||||
<template #img>
|
||||
<img :src="imgMap.get(slotProps.targetType)" alt="" />
|
||||
</template>
|
||||
<template #content>
|
||||
<Ellipsis>
|
||||
<span style="font-weight: 500">
|
||||
{{ slotProps.alarmName }}
|
||||
</span>
|
||||
</Ellipsis>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="8">
|
||||
<div class="content-des-title">
|
||||
{{ titleMap.get(slotProps.targetType) }}
|
||||
</div>
|
||||
<Ellipsis
|
||||
><div>
|
||||
{{ slotProps?.targetName }}
|
||||
</div></Ellipsis
|
||||
>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="content-des-title">
|
||||
最近告警时间
|
||||
</div>
|
||||
<Ellipsis
|
||||
><div>
|
||||
{{
|
||||
moment(slotProps?.alarmTime).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)
|
||||
}}
|
||||
</div></Ellipsis
|
||||
>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="content-des-title">状态</div>
|
||||
<a-badge
|
||||
:status="
|
||||
slotProps.state.value === 'warning'
|
||||
? 'error'
|
||||
: 'default'
|
||||
"
|
||||
>
|
||||
</a-badge
|
||||
><span
|
||||
:style="
|
||||
slotProps.state.value === 'warning'
|
||||
? 'color: #E50012'
|
||||
: 'color:black'
|
||||
"
|
||||
>
|
||||
{{ slotProps.state.text }}
|
||||
</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
</JTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getImage } from '@/utils/comm';
|
||||
import {
|
||||
getProductList,
|
||||
getDeviceList,
|
||||
getOrgList,
|
||||
query,
|
||||
} from '@/api/rule-engine/log';
|
||||
import { queryLevel } from '@/api/rule-engine/config';
|
||||
import Search from '@/components/Search';
|
||||
import { useAlarmStore } from '@/store/alarm';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { Store } from 'jetlinks-store';
|
||||
import moment from 'moment';
|
||||
const alarmStore = useAlarmStore();
|
||||
const { data } = storeToRefs(alarmStore);
|
||||
const getDefaulitLevel = () => {
|
||||
queryLevel().then((res) => {
|
||||
if (res.status === 200) {
|
||||
Store.set('default-level', res.result?.levels || []);
|
||||
data.value.defaultLevel = res.result?.levels || [];
|
||||
}
|
||||
});
|
||||
};
|
||||
getDefaulitLevel();
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
id?: string;
|
||||
}>();
|
||||
|
||||
const imgMap = new Map();
|
||||
imgMap.set('product', getImage('/alarm/product.png'));
|
||||
imgMap.set('device', getImage('/alarm/device.png'));
|
||||
imgMap.set('other', getImage('/alarm/other.png'));
|
||||
imgMap.set('org', getImage('/alarm/org.png'));
|
||||
|
||||
const titleMap = new Map();
|
||||
titleMap.set('product', '产品');
|
||||
titleMap.set('device', '设备');
|
||||
titleMap.set('other', '其他');
|
||||
titleMap.set('org', '组织');
|
||||
|
||||
const colorMap = new Map();
|
||||
colorMap.set(1, '#E50012');
|
||||
colorMap.set(2, '#FF9457');
|
||||
colorMap.set(3, '#FABD47');
|
||||
colorMap.set(4, '#999999');
|
||||
colorMap.set(5, '#C4C4C4');
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'alarmName',
|
||||
key: 'alarmName',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最近告警事件',
|
||||
dataIndex: 'alarmTime',
|
||||
key: 'alarmTime',
|
||||
search: {
|
||||
type: 'dateTime',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: '告警中',
|
||||
value: 'warning',
|
||||
},
|
||||
{
|
||||
label: '无告警',
|
||||
value: 'normal',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
const produtCol = [
|
||||
...columns,
|
||||
{
|
||||
title: '产品名称',
|
||||
dataIndex: 'targetName',
|
||||
key: 'targetName',
|
||||
search: {
|
||||
type: 'select',
|
||||
options: async () => {
|
||||
const resq = await getProductList();
|
||||
if (resq.status === 200) {
|
||||
return resq.result.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const deviceCol = [
|
||||
...columns,
|
||||
{
|
||||
title: '设备名称',
|
||||
dataIndex: 'targetName',
|
||||
key: 'targetName',
|
||||
search: {
|
||||
type: 'select',
|
||||
opstions: async () => {
|
||||
const res = await getDeviceList();
|
||||
if (res.status === 200) {
|
||||
return res.result.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const orgCol = [
|
||||
...columns,
|
||||
{
|
||||
title: '组织名称',
|
||||
dataIndex: 'targetName',
|
||||
key: 'targetName',
|
||||
search: {
|
||||
type: 'select',
|
||||
options: async () => {
|
||||
const res = await getOrgList();
|
||||
if (res.status === 200) {
|
||||
return res.result.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let params = ref({
|
||||
sorts: [{ name: 'alarmTime', order: 'desc' }],
|
||||
terms: [],
|
||||
});
|
||||
let param = reactive({
|
||||
pageSize: 10,
|
||||
terms: [],
|
||||
});
|
||||
// let dataSource = reactive({
|
||||
// data: [],
|
||||
// pageSize: 10,
|
||||
// pageIndex: 0,
|
||||
// total: 0,
|
||||
// });
|
||||
const handleSearch = async (params: any) => {
|
||||
const resp = await query(params);
|
||||
if (resp.status === 200) {
|
||||
const res = await getOrgList();
|
||||
if (res.status === 200) {
|
||||
resp.result.data.map((item: any) => {
|
||||
if (item.targetType === 'org') {
|
||||
res.result.forEach((item2: any) => {
|
||||
if (item2.id === item.targetId) {
|
||||
item.targetName = item2.name;
|
||||
}
|
||||
//targetName处理之后的
|
||||
if (item.targetId === item.targetName) {
|
||||
item.targetName = '无';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
};
|
||||
watchEffect(() => {
|
||||
if (props.type !== 'all' && !props.id) {
|
||||
params.value.terms.push({
|
||||
termType: 'eq',
|
||||
column: 'targetType',
|
||||
value: props.type,
|
||||
type: 'and',
|
||||
});
|
||||
}
|
||||
if (props.id) {
|
||||
params.value.terms.push({
|
||||
termType: 'eq',
|
||||
column: 'alarmConfigId',
|
||||
value: props.id,
|
||||
type: 'and',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const search = (data: any) => {
|
||||
const dt = {
|
||||
pageSize: 10,
|
||||
terms: [...data?.terms],
|
||||
};
|
||||
};
|
||||
const log = () => {
|
||||
console.log(data.value.defaultLevel);
|
||||
};
|
||||
log();
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
</style>
|
|
@ -1,9 +1,70 @@
|
|||
<template>
|
||||
<div></div>
|
||||
<page-container :tabList="isNoCommunity ? list : noList" :tabActiveKey="data.tab" @tabChange="onTabChange">
|
||||
<TableComponents :type="data.tab"></TableComponents>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { isNoCommunity } from '@/utils/utils';
|
||||
import { useAlarmStore } from '@/store/alarm';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { queryLevel } from '@/api/rule-engine/config';
|
||||
import { Store } from 'jetlinks-store';
|
||||
import TableComponents from './TabComponent/indev.vue';
|
||||
const list = [
|
||||
{
|
||||
key: 'all',
|
||||
tab: '全部',
|
||||
},
|
||||
{
|
||||
key: 'product',
|
||||
tab: '产品',
|
||||
},
|
||||
{
|
||||
key: 'device',
|
||||
tab: '设备',
|
||||
},
|
||||
{
|
||||
key: 'org',
|
||||
tab: '组织',
|
||||
},
|
||||
{
|
||||
key: 'other',
|
||||
tab: '其他',
|
||||
},
|
||||
];
|
||||
const noList = [
|
||||
{
|
||||
key: 'all',
|
||||
tab: '全部',
|
||||
},
|
||||
{
|
||||
key: 'product',
|
||||
tab: '产品',
|
||||
},
|
||||
{
|
||||
key: 'device',
|
||||
tab: '设备',
|
||||
},
|
||||
{
|
||||
key: 'other',
|
||||
tab: '其他',
|
||||
},
|
||||
];
|
||||
const alarmStore = useAlarmStore();
|
||||
const { data } = storeToRefs(alarmStore);
|
||||
const getDefaulitLevel = () => {
|
||||
queryLevel().then((res)=>{
|
||||
if(res.status === 200 ){
|
||||
Store.set('default-level', res.result?.levels || []);
|
||||
data.value.defaultLevel = res.result?.levels || [];
|
||||
}
|
||||
})
|
||||
}
|
||||
getDefaulitLevel();
|
||||
const onTabChange = (key:string) =>{
|
||||
data.value.tab = key;
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
</style>
|
Loading…
Reference in New Issue