feat: 告警记录页面

This commit is contained in:
leiqiaochu 2023-02-28 21:50:25 +08:00
commit 459ad37e0d
89 changed files with 5658 additions and 3092 deletions

View File

@ -49,8 +49,10 @@ export const queryProduct = (data?: any) =>
export const queryDevice = () =>
server.get(`/device/instance/_query/no-paging?paging=false`);
export const validateVersion = (productId: string, versionOrder: number) =>
server.get(`/firmware/${productId}/${versionOrder}/exists`);
export const validateVersion = (
productId: string,
versionOrder: number | string,
) => server.get(`/firmware/${productId}/${versionOrder}/exists`);
export const queryDetailList = (data: Record<string, unknown>) =>
server.post(`/device-instance/detail/_query`, data);

View File

@ -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`)

11
src/api/link/dashboard.ts Normal file
View File

@ -0,0 +1,11 @@
import server from '@/utils/request';
export const dashboard = (data: object) =>
server.post(`/dashboard/_multi`, data);
export const productCount = (data: object) =>
server.post(`/device-product/_count`, data);
export const getGeo = (data: object) =>
server.post(`/geo/object/device/_search/geo.json`, data);
export const deviceCount = (data: object) =>
server.get(`/device/instance/_count`, data);
export const serverNode = () => server.get(`/dashboard/cluster/nodes`);

29
src/api/media/cascade.ts Normal file
View File

@ -0,0 +1,29 @@
import server from '@/utils/request'
import type { CascadeItem } from '@/views/media/Cascade/typings'
export default {
// 列表
list: (data: any) => server.post<any>(`/media/gb28181-cascade/_query`, data),
// 列表字段通道数量, 来自下面接口的total
queryCount: (id: string) => server.post<any>(`/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),
update: (data: any) => server.patch(`/media/gb28181-cascade`, data),
// 删除
del: (id: string) => server.remove(`media/gb28181-cascade/${id}`),
// 禁用
disabled: (id: string) => server.post<any>(`/media/gb28181-cascade/${id}/_disabled`),
// 启用
enabled: (id: string) => server.post<any>(`/media/gb28181-cascade/${id}/_enabled`),
// 新增/编辑
// 获取集群节点
clusters: () => server.get<any>(`/network/resources/clusters`),
// SIP本地地址
all: () => server.get<any>(`/network/resources/alive/_all`),
}

View File

@ -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}`),
}

View File

@ -7,4 +7,15 @@ export const save = (data: any) => server.post(`/scene`, data)
export const detail = (id: string) => server.get(`/scene/${id}`)
export const query = (data: any) => server.post('/scene/_query/',data);
export const query = (data: any) => server.post('/scene/_query/',data);
export const _delete = (id: string) => server.remove(`/scene/${id}/`);
export const _action = (id: string, type: '_disable' | '_enable') => server.put(`/scene/${id}/${type}`);
/**
*
* @param id
* @returns
*/
export const _execute = (id: string) => server.post(`/scene/${id}/_execute`);

View File

@ -48,7 +48,7 @@ const iconKeys = [
'ClockCircleOutlined',
'PartitionOutlined',
'ShareAltOutlined',
'playCircleOutlined',
'PlayCircleOutlined',
'RightOutlined',
'FileTextOutlined',
'UploadOutlined',
@ -62,6 +62,7 @@ const iconKeys = [
'HistoryOutlined',
'ToolOutlined',
'FileOutlined',
'LikeOutlined'
]
const Icon = (props: {type: string}) => {

View File

@ -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>

View File

@ -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>

130
src/components/AMapComponent/types.d.ts vendored Normal file
View File

@ -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;
}

View File

@ -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;
};

View File

@ -3,30 +3,29 @@
<template #title>
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="width: 150px;">配置元素</div>
<close-outlined @click="visible = false" />
<AIcon type="CloseOutlined" @click="visible = false" />
</div>
</template>
<template #content>
<div style="max-width: 400px;">
<a-form layout="vertical" :model="_value">
<value-type-form v-model:value="_value" :name="[]" isSub key="sub"></value-type-form>
<a-form-item label="说明" name="description" :rules="[
<div class="ant-form-vertical">
<value-type-form v-model:value="_value" :name="name" isSub key="sub"></value-type-form>
<a-form-item label="说明" :name="name.concat(['description'])" :rules="[
{ max: 200, message: '最多可输入200个字符' },
]">
<a-textarea v-model:value="_value.description" size="small"></a-textarea>
</a-form-item>
</a-form>
</div>
</div>
</template>
<a-button type="dashed" block @click="visible = true">
配置元素<edit-outlined class="item-icon" />
配置元素
<AIcon type="EditOutlined" class="item-icon" />
</a-button>
</a-popover>
</template>
<script setup lang="ts" name="ArrayParam">
import ValueTypeForm from '@/views/device/components/Metadata/Base/Edit/ValueTypeForm.vue';
import { EditOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { PropType } from 'vue';
type ValueType = Record<any, any>;
@ -37,7 +36,7 @@ const props = defineProps({
default: () => ({ extends: {} })
},
name: {
type: Array as PropType<string[]>,
type: Array as PropType<(string | number)[]>,
required: true
}
})

View File

@ -47,7 +47,7 @@ const props = defineProps({
})
},
name: {
type: Array as PropType<string[]>,
type: Array as PropType<(string| number)[]>,
required: true
}
})

View File

@ -16,13 +16,12 @@
</div>
</template>
<a-button type="dashed" block>
存储配置<edit-outlined class="item-icon" />
存储配置<AIcon type="EditOutlined" 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({
@ -31,7 +30,7 @@ const props = defineProps({
default: () => ({})
},
name: {
type: Array as PropType<string[]>,
type: Array as PropType<(string| number)[]>,
default: () => ([]),
required: true
},

View File

@ -2,49 +2,50 @@
<div class="enum-param">
<div class="list-item" v-for="(item, index) in _value" :key="index">
<div class="item-left">
<menu-outlined class="item-drag item-icon" />
<AIcon type="MenuOutlined" class="item-drag item-icon" />
</div>
<div class="item-middle item-editable">
<a-popover :visible="editIndex === index" placement="top">
<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" />
<AIcon type="CloseOutlined" @click="handleClose" />
</div>
</template>
<template #content>
<a-form :model="_value[index]" layout="vertical">
<a-form-item label="Value" name="value" :rules="[
<div class="ant-form-vertical">
<a-form-item label="Value" :name="name.concat([index, 'value'])" :rules="[
{ required: true, message: '请输入Value' },
]">
<a-input v-model:value="_value[index].value" size="small"></a-input>
</a-form-item>
<a-form-item label="Text" name="text" :rules="[
<a-form-item label="Text" :name="name.concat([index, 'text'])" :rules="[
{ required: true, message: '请输入Text' },
]">
<a-input v-model:value="_value[index].text" size="small"></a-input>
</a-form-item>
</a-form>
</div>
</template>
<div class="item-edit" @click="handleEdit(index)">
{{ item.text || '枚举项配置' }}
<edit-outlined class="item-icon" />
<AIcon type="EditOutlined" class="item-icon" />
</div>
</a-popover>
</div>
<div class="item-right">
<delete-outlined @click="handleDelete(index)"/>
<AIcon type="DeleteOutlined" @click="handleDelete(index)" />
</div>
</div>
<a-button type="dashed" block @click="handleAdd">
<template #icon><plus-outlined class="item-icon" /></template>
<template #icon>
<AIcon type="PlusOutlined" class="item-icon" />
</template>
新增枚举型
</a-button>
</div>
</template>
<script setup lang="ts" name="BooleanParam">
import { PropType } from 'vue'
import { MenuOutlined, EditOutlined, DeleteOutlined, PlusOutlined, CloseOutlined } from '@ant-design/icons-vue';
type EnumType = {
text?: string,
@ -58,20 +59,23 @@ const emit = defineEmits<Emits>()
const props = defineProps({
value: {
type: Object as PropType<EnumType[]>,
},
name: {
type: Array as PropType<(string | number)[]>,
default: () => ([])
}
})
const _value = ref<EnumType[]>([])
watchEffect(() => {
_value.value = props.value
_value.value = props.value || ([{}])
})
watch(_value,
() => {
emit('update:value', _value.value)
},
{ deep: true })
() => {
emit('update:value', _value.value)
},
{ deep: true, immediate: true })
const editIndex = ref<number>(-1)
const handleEdit = (index: number) => {

View File

@ -2,59 +2,58 @@
<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" />
<AIcon type="MenuOutlined" class="item-drag item-icon" />
</div>
<div class="item-middle item-editable">
<a-popover :visible="editIndex === index" placement="left">
<template #title>
<div class="edit-title" style="display: flex; justify-content: space-between; align-items: center;">
<div style="width: 150px;">配置参数</div>
<close-outlined @click="handleClose" />
<AIcon type="CloseOutlined" @click="handleClose" />
</div>
</template>
<template #content>
<div style="max-width: 400px;">
<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>
<value-type-form v-model:value="_value[index].valueType" :name="['valueType']" isSub
key="json_sub"></value-type-form>
</a-form>
<div style="max-width: 400px;" class="ant-form-vertical">
<a-form-item label="标识" :name="name.concat([index, '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.concat([index, 'name'])" :rules="[
{ required: true, message: '请输入名称' },
{ max: 64, message: '最多可输入64个字符' },
]">
<a-input v-model:value="_value[index].name" size="small"></a-input>
</a-form-item>
<value-type-form v-model:value="_value[index].valueType" :name="name.concat([index, 'valueType'])" isSub
key="json_sub"></value-type-form>
</div>
</template>
<div class="item-edit" @click="handleEdit(index)">
{{ item.name || '配置参数' }}
<edit-outlined class="item-icon" />
<AIcon type="EditOutlined" class="item-icon" />
</div>
</a-popover>
</div>
<div class="item-right">
<delete-outlined @click="handleDelete(index)" />
<AIcon type="DeleteOutlined" @click="handleDelete(index)" />
</div>
</div>
<a-button type="dashed" block @click="handleAdd">
<template #icon><plus-outlined class="item-icon" /></template>
<template #icon>
<AIcon type="PlusOutlined" class="item-icon" />
</template>
添加参数
</a-button>
</div>
</template>
<script setup lang="ts" name="JsonParam">
import { PropType } from 'vue'
import { MenuOutlined, EditOutlined, DeleteOutlined, PlusOutlined, CloseOutlined } from '@ant-design/icons-vue';
import ValueTypeForm from '@/views/device/components/Metadata/Base/Edit/ValueTypeForm.vue';
type JsonType = Record<any, any>;
@ -66,20 +65,27 @@ const emit = defineEmits<Emits>()
const props = defineProps({
value: {
type: Object as PropType<JsonType[]>,
},
name: {
type: Array as PropType<(string | number)[]>,
default: () => ([])
}
})
const _value = ref<JsonType[]>([])
watchEffect(() => {
_value.value = props.value
_value.value = props.value || [{
valueType: {
expands: {}
},
}]
})
watch(_value,
() => {
emit('update:value', _value.value)
},
{ deep: true })
{ deep: true, immediate: true })
const editIndex = ref<number>(-1)
const handleEdit = (index: number) => {

View File

@ -2,7 +2,7 @@
<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" />
<AIcon type="MenuOutlined" class="item-drag item-icon" />
{{ `#${index + 1}.` }}
</div>
<div class="item-middle item-editable">
@ -10,13 +10,13 @@
<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" />
<AIcon type="CloseOutlined" @click="handleClose" />
</div>
</template>
<template #content>
<div>
<a-form :model="_value[index]" layout="vertical">
<a-form-item label="标识" name="id" :rules="[
<div class="ant-form-vertical">
<a-form-item label="标识" :name="name.concat([index, 'id'])" :rules="[
{ required: true, message: '请输入标识' },
{ max: 64, message: '最多可输入64个字符' },
{
@ -26,40 +26,41 @@
]">
<a-input v-model:value="_value[index].id" size="small"></a-input>
</a-form-item>
<a-form-item label="名称" name="name" :rules="[
<a-form-item label="名称" :name="name.concat([index, '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="[
<a-form-item label="指标值" :name="name.concat([index, 'value'])" :rules="[
{ required: true, message: '请输入指标值' },
{ validator: () => validateIndicator(_value[index]), message: '请输入指标值' }
]">
<JIndicators v-model:value="_value[index]" :type="type" size="small" :enum="enum"/>
<JIndicators v-model:value="_value[index]" :type="type" size="small" :enum="enum" />
</a-form-item>
</a-form>
</div>
</div>
</template>
<div class="item-edit" @click="handleEdit(index)">
{{ item.name || '配置参数' }}
<edit-outlined class="item-icon" />
<AIcon type="EditOutlined" class="item-icon" />
</div>
</a-popover>
</div>
<div class="item-right">
<delete-outlined @click="handleDelete(index)" />
<AIcon type="DeleteOutlined" @click="handleDelete(index)" />
</div>
</div>
<a-button type="dashed" block @click="handleAdd">
<template #icon><plus-outlined class="item-icon" /></template>
<template #icon>
<AIcon type="PlusOutlined" 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 {
@ -78,6 +79,10 @@ const props = defineProps({
enum: {
type: Object,
default: () => ({})
},
name: {
type: Array as PropType<(string | number)[]>,
default: () => ([])
}
})

View File

@ -44,7 +44,7 @@ const props = defineProps({
})
},
name: {
type: Array as PropType<string[]>,
type: Array as PropType<(string| number)[]>,
default: () => ([])
},
id: String,

View File

@ -90,7 +90,7 @@ const insert = (val) => {
]);
}
watch(() => props.value,
watch(() => props.modelValue,
(val) => {
instance.setValue(val)
})

View File

@ -158,7 +158,6 @@ const JTable = defineComponent<JTableProps>({
const pageSize = ref<number>(6)
const total = ref<number>(0)
const loading = ref<boolean>(true)
const loading1 = ref<boolean>(true)
const _columns = computed(() => props.columns.filter(i => !(i?.hideInTable)))
@ -240,6 +239,7 @@ const JTable = defineComponent<JTableProps>({
)
onMounted(() => {
windowChange() // 初始化
window.onresize = () => {
windowChange()
}

View File

@ -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)
}
}

View File

@ -184,7 +184,12 @@
import { message, Form } from 'ant-design-vue';
import { getImage } from '@/utils/comm';
import FileUpload from './FileUpload.vue';
import { save, update, queryProduct } from '@/api/device/firmware';
import {
save,
update,
queryProduct,
validateVersion,
} from '@/api/device/firmware';
import type { FormInstance } from 'ant-design-vue';
import type { Properties } from '../type';
@ -246,6 +251,21 @@ const validatorSign = async (_: Record<string, any>, value: string) => {
return Promise.resolve();
}
};
const validatorVersionOrder = async (_: Record<string, any>, value: string) => {
const { signMethod, productId } = formData.value;
if (value && !!signMethod && productId) {
const res = await validateVersion(productId, value);
if (res.status === 200) {
if (id && props.data.versionOrder === value) {
formData.value.versionOrder = '';
} else {
Promise.reject(res.result ? ['版本序号已存在'] : '');
}
}
} else {
return Promise.resolve();
}
};
const { resetFields, validate, validateInfos } = useForm(
formData,
@ -258,8 +278,12 @@ const { resetFields, validate, validateInfos } = useForm(
version: [
{ required: true, message: '请输入版本号' },
{ max: 64, message: '最多可输入64个字符', trigger: 'change' },
{ validator: validatorVersionOrder, trigger: 'blur' },
],
versionOrder: [
{ required: true, message: '请输入版本序号' },
{ validator: validatorVersionOrder, trigger: 'blur' },
],
versionOrder: [{ required: true, message: '请输入版本序号' }],
signMethod: [{ required: true, message: '请选择签名方式' }],
sign: [
{ required: true, message: '请输入签名' },
@ -280,10 +304,10 @@ const onSubmit = async () => {
validate()
.then(async (res) => {
const product = productOptions.value.find(
(item) => item.value === res.productId,
(item) => item?.value === res.productId,
);
const productName = product.label || props.data?.url;
const size = extraValue.value.length || props.data?.size;
const productName = product?.label || props.data?.url;
const size = extraValue.value?.length || props.data?.size;
const params = {
...toRaw(formData.value),

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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
}]
}
};

View File

@ -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>

View File

@ -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) => {

View File

@ -694,7 +694,6 @@ const saveBtn = () => {
};
const handleSearch = (_params: any) => {
console.log(_params);
params.value = _params;
};
</script>

View File

@ -7,7 +7,7 @@
message: 'ID只能由数字、字母、下划线、中划线组成',
},
]">
<a-input v-model:value="value.id" size="small" @change="asyncOtherConfig"></a-input>
<a-input v-model:value="value.id" size="small" @change="asyncOtherConfig" :disabled="metadataStore.model.action === 'edit'"></a-input>
</a-form-item>
<a-form-item label="名称" name="name" :rules="[
{ required: true, message: '请输入名称' },
@ -15,11 +15,9 @@
]">
<a-input v-model:value="value.name" size="small"></a-input>
</a-form-item>
{{ modelType }}
<template v-if="modelType === 'properties'">
<value-type-form :name="['valueType']" v-model:value="value.valueType" key="property"
<value-type-form :name="['valueType']" v-model:value="value.valueType" key="property" title="数据类型"
@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>
</template>
@ -37,11 +35,22 @@
]">
<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>
<value-type-form :name="['output']" v-model:value="value.output" key="function" title="输出参数"></value-type-form>
</template>
<template v-if="modelType === 'events'">
<a-form-item label="级别" :name="['expands', 'level']" :rules="[
{ required: true, message: '请选择级别' },
]">
<a-select v-model:value="value.expands.level" :options="EventLevel" size="small"></a-select>
</a-form-item>
<value-type-form :name="['valueType']" v-model:value="value.valueType" key="function" title="输出参数"></value-type-form>
</template>
<template v-if="modelType === 'tags'">
<value-type-form :name="['valueType']" v-model:value="value.valueType" key="property" title="数据类型"></value-type-form>
<a-form-item label="读写类型" :name="['expands', 'type']" :rules="[
{ required: true, message: '请选择读写类型' },
]">
<a-select v-model:value="value.expands.type" :options="ExpandsTypeList" mode="multiple" size="small"></a-select>
</a-form-item>
</template>
<a-form-item label="说明" name="description" :rules="[
@ -50,13 +59,15 @@
<a-textarea v-model:value="value.description" size="small"></a-textarea>
</a-form-item>
</template>
<script setup lang="ts" name="PropertyForm">
<script setup lang="ts" name="BaseForm">
import { PropType } from 'vue';
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'
import { EventLevel, ExpandsTypeList } from '@/views/device/data';
import { useMetadataStore } from '@/store/metadata';
const props = defineProps({
type: {
@ -73,12 +84,22 @@ const props = defineProps({
default: ''
}
})
const metadataStore = useMetadataStore()
if (props.modelType === 'events' || props.modelType === 'tags') {
if (!props.value.expands) {
props.value.expands = {}
}
}
const productStore = useProductStore()
const config = ref<Record<any, any>[]>([])
const asyncOtherConfig = async () => {
if (props.type !== 'product') return
const { valueType: { type }, id } = props.value
const { valueType, id } = props.value
const { type } = valueType || {}
const productId = productStore.current?.id
if (!productId || !id || !type) return
const resp = await getMetadataConfig({
@ -93,8 +114,11 @@ const asyncOtherConfig = async () => {
config.value = resp.result
}
}
onMounted(() => {
asyncOtherConfig()
if (props.modelType === 'properties') {
asyncOtherConfig()
}
})
const changeValueType = (val: string) => {

View File

@ -1,2 +0,0 @@
<template></template>
<script setup lang="ts" name="CommonForm"></script>

View File

@ -10,7 +10,7 @@
<a-form-item label="读写类型" :name="name.concat(['type'])" :rules="[
{ required: true, message: '请选择读写类型' },
]">
<a-select v-model:value="_value.type" :options="options" mode="multiple" size="small"></a-select>
<a-select v-model:value="_value.type" :options="ExpandsTypeList" mode="multiple" size="small"></a-select>
</a-form-item>
<a-form-item label="其他配置" v-if="config.length > 0">
<a-form-item v-for="(item, index) in config" :key="index">
@ -19,12 +19,12 @@
</a-form-item>
<a-form-item v-if="type === 'product' && ['int', 'float', 'double', 'long', 'date', 'string', 'boolean'].includes(valueType.type)"
label="指标配置" :name="name.concat(['metrics'])">
<metrics-param v-model:value="_value.metrics" :type="valueType.type" :enum="valueType"></metrics-param>
<metrics-param v-model:value="_value.metrics" :type="valueType.type" :enum="valueType" :name="name.concat(['metrics'])"></metrics-param>
</a-form-item>
</template>
<script setup lang="ts" name="ExpandsForm">
import { useMetadataStore } from '@/store/metadata';
import { PropertySource } from '@/views/device/data';
import { ExpandsTypeList, PropertySource } from '@/views/device/data';
import { PropType } from 'vue';
import VirtualRuleParam from '@/components/Metadata/VirtualRuleParam/index.vue';
import ConfigParam from '@/components/Metadata/ConfigParam/index.vue'
@ -34,7 +34,6 @@ type ValueType = Record<any, any>;
const props = defineProps({
value: {
type: Object as PropType<ValueType>,
default: () => ({})
},
type: {
type: String
@ -62,32 +61,27 @@ interface Emits {
}
const emit = defineEmits<Emits>()
const _value = computed({
get: () => props.value,
set: val => {
emit('update:value', val)
}
// const _value = computed({
// get: () => props.value || {},
// set: val => {
// emit('update:value', val)
// }
// })
const _value = ref<ValueType>({})
watchEffect(() => {
_value.value = props.value || {}
})
const options = [
{
label: '读',
value: 'read',
watch(_value,
() => {
emit('update:value', _value.value)
},
{
label: '写',
value: 'write',
},
{
label: '上报',
value: 'report',
},
]
{ deep: true, immediate: true })
const metadataStore = useMetadataStore()
onMounted(() => {
if (props.type === 'product' || !props.value.source) {
if (props.type === 'product' || !props.value?.source) {
emit('update:value', { ...props.value, source: 'device' })
}
})

View File

@ -1,28 +1,25 @@
<template>
<a-form-item label="数据类型" :name="name.concat(['type'])" :rules="[
{ required: true, message: '请选择数据类型' },
<a-form-item :label="title" :name="name.concat(['type'])" :rules="[
metadataStore.model.type !== 'functions' ? { required: true, message: `请选择${title}` } : {},
]">
<a-select v-model:value="value.type" :options="_dataTypeList" size="small" @change="changeType"></a-select>
<a-select v-model:value="_value.type" :options="metadataStore.model.type === 'events' ? eventDataTypeList : _dataTypeList" size="small" @change="changeType"></a-select>
</a-form-item>
<a-form-item label="单位" :name="name.concat(['unit'])" v-if="['int', 'float', 'long', 'double'].includes(value.type)">
<InputSelect v-model:value="value.unit" :options="unit.unitOptions" size="small"></InputSelect>
<a-form-item label="单位" :name="name.concat(['unit'])" v-if="['int', 'float', 'long', 'double'].includes(_value.type)">
<InputSelect v-model:value="_value.unit" :options="unit.unitOptions" size="small"></InputSelect>
</a-form-item>
<a-form-item label="精度" :name="name.concat(['scale'])" v-if="['float', 'double'].includes(value.type)">
<a-input-number v-model:value="value.scale" size="small" :min="0" :max="2147483647" :precision="0"
:default-value="2" style="width: 100%"></a-input-number>
<a-form-item label="精度" :name="name.concat(['scale'])" v-if="['float', 'double'].includes(_value.type)">
<a-input-number v-model:value="_value.scale" size="small" :min="0" :max="2147483647" :precision="0" :default-value="2"
style="width: 100%"></a-input-number>
</a-form-item>
<a-form-item label="布尔值" name="booleanConfig" v-if="['boolean'].includes(value.type)">
<BooleanParam
:name="name"
v-model:value="_value"
></BooleanParam>
<a-form-item label="布尔值" name="booleanConfig" v-if="['boolean'].includes(_value.type)">
<BooleanParam :name="name" v-model:value="_value"></BooleanParam>
</a-form-item>
<a-form-item label="枚举项" :name="name.concat(['elements'])" v-if="['enum'].includes(value.type)" :rules="[
<a-form-item label="枚举项" :name="name.concat(['elements'])" v-if="['enum'].includes(_value.type)" :rules="[
{ required: true, message: '请配置枚举项' }
]">
<EnumParam v-model:value="value.elements"></EnumParam>
<EnumParam v-model:value="_value.elements" :name="name.concat(['elements'])"></EnumParam>
</a-form-item>
<a-form-item :name="name.concat(['expands', 'maxLength'])" v-if="['string', 'password'].includes(value.type)">
<a-form-item :name="name.concat(['expands', 'maxLength'])" v-if="['string', 'password'].includes(_value.type)">
<template #label>
<a-space>
最大长度
@ -31,19 +28,20 @@
</a-tooltip>
</a-space>
</template>
<a-input-number v-model:value="value.expands.maxLength" size="small" :max="2147483647" :min="1" :precision="0"
<a-input-number v-model:value="_value.expands.maxLength" size="small" :max="2147483647" :min="1" :precision="0"
style="width: 100%;"></a-input-number>
</a-form-item>
<a-form-item label="元素配置" :name="name.concat(['elementType'])" v-if="['array'].includes(value.type)">
<ArrayParam v-model:value="value.elementType" :name="name.concat(['elementType'])"></ArrayParam>
<a-form-item label="元素配置" :name="name.concat(['elementType'])" v-if="['array'].includes(_value.type)">
<ArrayParam v-model:value="_value.elementType" :name="name.concat(['elementType'])"></ArrayParam>
</a-form-item>
<a-form-item label="JSON对象" :name="name.concat(['properties'])" v-if="['object'].includes(value.type)">
<JsonParam v-model:value="value.jsonConfig" :name="name.concat(['properties'])"></JsonParam>
<a-form-item label="JSON对象" :name="name.concat(['properties'])" v-if="['object'].includes(_value.type)" :rules="[]">
<JsonParam v-model:value="_value.properties" :name="name.concat(['properties'])"></JsonParam>
</a-form-item>
<a-form-item label="文件类型" :name="name.concat(['fileType'])" v-if="['file'].includes(value.type)" initialValue="url" :rules="[
{ required: true, message: '请选择文件类型' },
]">
<a-select v-model:value="value.fileType" :options="FileTypeList" size="small"></a-select>
<a-form-item label="文件类型" :name="name.concat(['fileType'])" v-if="['file'].includes(_value.type)" initialValue="url"
:rules="[
{ required: true, message: '请选择文件类型' },
]">
<a-select v-model:value="_value.fileType" :options="FileTypeList" size="small"></a-select>
</a-form-item>
</template>
<script lang="ts" setup mame="BaseForm">
@ -57,23 +55,26 @@ import BooleanParam from '@/components/Metadata/BooleanParam/index.vue'
import EnumParam from '@/components/Metadata/EnumParam/index.vue'
import ArrayParam from '@/components/Metadata/ArrayParam/index.vue'
import JsonParam from '@/components/Metadata/JsonParam/index.vue'
import { useMetadataStore } from '@/store/metadata';
type ValueType = Record<any, any>;
const props = defineProps({
value: {
type: Object as PropType<ValueType>,
default: () => ({
extends: {}
})
// default: () => ({})
},
isSub: {
type: Boolean,
default: false
},
name: {
type: Array as PropType<string[]>,
type: Array as PropType<(string | number)[]>,
default: () => ([]),
required: true
},
title: {
String,
default: '数据类型'
}
})
@ -83,11 +84,34 @@ interface Emits {
}
const emit = defineEmits<Emits>()
// emit('update:value', { extends: {}, ...props.value })
const _value = computed({
get: () => props.value,
set: val => {
emit('update:value', val)
const metadataStore = useMetadataStore()
// const _value = computed({
// get: () => props.value,
// set: val => {
// emit('update:value', val)
// }
// })
const _value = ref<ValueType>({})
watchEffect(() => {
_value.value = props.value || {
expands: {}
}
})
watch(_value,
() => {
emit('update:value', _value.value)
},
{ deep: true, immediate: true })
onMounted(() => {
if (metadataStore.model.type === 'events') {
_value.value = {
type: 'object',
expands: {}
}
}
})
@ -108,6 +132,12 @@ const unit = {
unit.getUnit()
const _dataTypeList = computed(() => props.isSub ? DataTypeList.filter(item => item.value !== 'array' && item.value !== 'object') : DataTypeList)
const eventDataTypeList = [
{
value: 'object',
label: 'object(结构体)',
},
]
const changeType = (val: SelectValue) => {
emit('changeType', val as string)

View File

@ -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 :model-type="metadataStore.model.type" :type="type" ref="propertyForm" v-model:value="form.model"></PropertyForm>
<BaseForm :model-type="metadataStore.model.type" :type="type" v-model:value="form.model"></BaseForm>
</a-form>
</a-drawer>
</template>
@ -20,7 +20,7 @@ import { updateMetadata, asyncUpdateMetadata } from '../../metadata'
import { Store } from 'jetlinks-store';
import { detail } from '@/api/device/instance';
import { DeviceInstance } from '@/views/device/Instance/typings';
import PropertyForm from './PropertyForm.vue';
import BaseForm from './BaseForm.vue';
import { PropType } from 'vue';
const props = defineProps({
@ -56,15 +56,8 @@ const close = () => {
const title = computed(() => metadataStore.model.action === 'add' ? '新增' : '修改')
const propertyForm = ref()
const form = reactive({
model: {
valueType: {
expands: {}
},
expands: {}
} as any,
model: {} as any,
})
if (metadataStore.model.action === 'edit') {
form.model = metadataStore.model.item
@ -133,6 +126,7 @@ const save = reactive({
}
save.loading = false
})
save.loading = false
},
resetMetadata: async () => {
const { id } = route.params
@ -144,6 +138,44 @@ const save = reactive({
})
</script>
<style lang="less" scoped>
<style scoped lang="less">
: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;
}
</style>

View File

@ -9,7 +9,7 @@
title: operateLimits('add', type) ? '当前的存储方式不支持新增' : '新增',
}">
<template #icon>
<PlusOutlined />
<AIcon type="PlusOutlined" />
</template>
新增
</PermissionButton>
@ -38,7 +38,7 @@
:udisabled="operateLimits('updata', type)" @click="handleEditClick(slotProps)" :tooltip="{
title: operateLimits('updata', type) ? '当前的存储方式不支持编辑' : '编辑',
}">
<EditOutlined />
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton :uhas-permission="`${permission}:delete`" type="link" key="delete" style="padding: 0"
:pop-confirm="{
@ -48,7 +48,7 @@
}" :tooltip="{
title: '删除',
}">
<DeleteOutlined />
<Aicon type="DeleteOutlined" />
</PermissionButton>
</a-space>
</template>
@ -62,7 +62,6 @@ import { useInstanceStore } from '@/store/instance'
import { useProductStore } from '@/store/product'
import { useMetadataStore } from '@/store/metadata'
import PermissionButton from '@/components/PermissionButton/index.vue'
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue/es'
import { SystemConst } from '@/utils/consts'
import { Store } from 'jetlinks-store'

View File

@ -122,7 +122,7 @@ watch(
)
watch(
[props.visible, props.type],
() => [props.visible, props.type],
() => {
if (props.visible) {
loading.value = true
@ -136,7 +136,7 @@ watch(
} else {
productDetail(id as string).then((resp) => {
loading.value = false
// productStore.setCurrent(resp.result)
productStore.setCurrent(resp.result)
value.value = resp.result.metadata
});
}

View File

@ -3,7 +3,7 @@
@ok="handleImport" :confirm-loading="loading">
<div class="import-content">
<p class="import-tip">
<exclamation-circle-outlined style="margin-right: 5px" />
<AIcon type="ExclamationCircleOutlined" style="margin-right: 5px" />
导入的物模型会覆盖原来的属性功能事件标签请谨慎操作
</p>
</div>
@ -37,8 +37,7 @@
<a-upload v-model:file-list="fileList" :before-upload="beforeUpload" accept=".json"
:show-upload-list="false" :action="FILE_UPLOAD" @change="fileChange"
:headers="{ 'X-Access-Token': getToken()}">
<upload-outlined class="upload-button"/>
<!-- <button id="uploadFile" style="display: none;"></button> -->
<AIcon type="UploadOutlined" class="upload-button" />
</a-upload>
</template>
</a-input>
@ -62,9 +61,8 @@ import { Store } from 'jetlinks-store';
import { SystemConst } from '@/utils/consts';
import { useInstanceStore } from '@/store/instance'
import { useProductStore } from '@/store/product';
import { UploadOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { FILE_UPLOAD } from '@/api/comm';
import { LocalStore, getToken } from '@/utils/comm';
import { getToken } from '@/utils/comm';
import MonacoEditor from '@/components/MonacoEditor/index.vue'
const route = useRoute()
@ -258,13 +256,15 @@ const handleImport = async () => {
if (resp.status === 200) {
if (props?.type === 'device') {
const metadata: DeviceMetadata = JSON.parse(paramsDevice || '{}')
// TODO
// MetadataAction.insert(metadata);
instanceStore.setCurrent(metadata)
// instanceStore.setCurrent(metadata)
message.success('导入成功')
} else {
const metadata: ProductItem = JSON.parse(params?.metadata || '{}')
// TODO
// MetadataAction.insert(metadata);
productStore.setCurrent(metadata)
// productStore.setCurrent(metadata)
message.success('导入成功')
}
}

View File

@ -6,7 +6,7 @@
? '该设备已脱离产品物模型,修改产品物模型对该设备无影响'
: '设备会默认继承产品的物模型,修改设备物模型后将脱离产品物模型'">
<div class="ellipsis">
<info-circle-outlined style="margin-right: 3px" />
<AIcon type="InfoCircleOutlined" style="margin-right: 3px" />
{{
instanceStore.detail?.independentMetadata && type === 'device'
? '该设备已脱离产品物模型,修改产品物模型对该设备无影响'
@ -47,7 +47,6 @@
</a-card>
</template>
<script setup lang="ts" name="Metadata">
import { InfoCircleOutlined } from '@ant-design/icons-vue';
import PermissionButton from '@/components/PermissionButton/index.vue'
import { deleteMetadata } from '@/api/device/instance.js'
import { message } from 'ant-design-vue'

View File

@ -133,3 +133,18 @@ export const DateTypeList = [
// value: 'yyyy-MM-dd HH:mm:ss zzz',
// },
];
export const ExpandsTypeList = [
{
label: '读',
value: 'read',
},
{
label: '写',
value: 'write',
},
{
label: '上报',
value: 'report',
},
]

View File

@ -1,10 +1,16 @@
<template>
<div class="iot-home-container" v-loading="loading">
<InitHome v-if="currentView === 'init'" @refresh="setCurrentView" />
<DeviceHome v-else-if="currentView === 'device'" />
<DevOpsHome v-else-if="currentView === 'ops'" />
<ComprehensiveHome v-else-if="currentView === 'comprehensive'" />
</div>
<page-container>
<div class="iot-home-container" v-loading="loading">
<InitHome v-if="currentView === 'init'" @refresh="setCurrentView" />
<DeviceHome v-else-if="currentView === 'device'" />
<DevOpsHome v-else-if="currentView === 'ops'" />
<ComprehensiveHome v-else-if="currentView === 'comprehensive'" />
<Api :mode="'home'" hasHome showTitle>
<template #top> </template>
</Api>
</div>
</page-container>
</template>
<script lang="ts" setup>
@ -12,6 +18,7 @@ import InitHome from './components/InitHome/index.vue';
import DeviceHome from './components/DeviceHome/index.vue';
import DevOpsHome from './components/DevOpsHome/index.vue';
import ComprehensiveHome from './components/ComprehensiveHome/index.vue';
import Api from '@/views/system/Platforms/Api/index.vue';
import { isNoCommunity } from '@/utils/utils';
import { getMe_api, getView_api } from '@/api/home';

View File

@ -9,8 +9,10 @@
></Provider>
</div>
<div v-else>
<div v-if="!id"><a @click="goBack">返回</a></div>
<AccessNetwork
<div class="go-back" v-if="id === ':id'">
<a @click="goBack">返回</a>
</div>
<Network
v-if="showType === 'network'"
:provider="provider"
:data="data"
@ -42,18 +44,15 @@
</template>
<script lang="ts" setup name="AccessConfigDetail">
import { getImage } from '@/utils/comm';
import AccessNetwork from '../components/Network.vue';
import Network from '../components/Network/index.vue';
import Provider from '../components/Provider/index.vue';
import { getProviders, detail } from '@/api/link/accessConfig';
import Media from '../components/Media/index.vue';
import Channel from '../components/Channel/index.vue';
import Edge from '../components/Edge/index.vue';
import Cloud from '../components/Cloud/index.vue';
import { getProviders, detail } from '@/api/link/accessConfig';
const route = useRoute();
const view = route.query.view as string;
const id = route.params.id as string;
const dataSource = ref([]);
@ -74,7 +73,7 @@ const goBack = () => {
type.value = true;
};
const getTypeList = (result: any[]) => {
const getTypeList = (result: Record<string, any>) => {
const list = [];
const media: any[] = [];
const network: any[] = [];
@ -184,76 +183,7 @@ onMounted(() => {
</script>
<style lang="less" scoped>
.provider {
position: relative;
width: 100%;
padding: 20px;
background: url('/public/images/access/background.png') no-repeat;
background-size: 100% 100%;
border: 1px solid #e6e6e6;
&::before {
position: absolute;
top: 0;
left: 40px;
display: block;
width: 15%;
min-width: 64px;
height: 2px;
background-image: url('/public/images/access/rectangle.png');
background-repeat: no-repeat;
background-size: 100% 100%;
// border: 1px #8da1f4 solid;
// border-bottom-left-radius: 10%;
// border-bottom-right-radius: 10%;
content: ' ';
}
&:hover {
box-shadow: 0 0 24px rgba(#000, 0.1);
}
}
.box {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.left {
display: flex;
width: calc(100% - 70px);
.images {
width: 64px;
height: 64px;
img {
width: 100%;
}
}
.context {
width: calc(100% - 84px);
margin: 10px;
.title {
font-weight: 600;
}
.desc {
width: 100%;
margin-top: 10px;
overflow: hidden;
color: rgba(0, 0, 0, 0.55);
font-weight: 400;
font-size: 13px;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.right {
width: 70px;
}
.go-back {
margin: 0 0 20px 0;
}
</style>

View File

@ -39,12 +39,16 @@
/>
</a-form-item>
<a-form-item>
<a-button
<PermissionButton
v-if="view === 'false'"
type="primary"
html-type="submit"
>保存</a-button
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</PermissionButton>
</a-form-item>
</a-form>
</div>
@ -86,10 +90,9 @@
</template>
<script lang="ts" setup name="AccessChannel">
import { message, Form } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { update, save } from '@/api/link/accessConfig';
import { ProtocolMapping } from '../../Detail/data';
import { ProtocolMapping } from '../../data';
interface FormState {
name: string;
@ -129,16 +132,7 @@ const onFinish = async (values: any) => {
id === ':id' ? await save(params) : await update({ ...params, id });
if (resp.status === 200) {
message.success('操作成功!');
// if (params.get('save')) {
// if ((window as any).onTabSaveSuccess) {
// if (resp.result) {
// (window as any).onTabSaveSuccess(resp.result);
// setTimeout(() => window.close(), 300);
// }
// }
// } else {
history.back();
// }
}
};
@ -164,8 +158,6 @@ onMounted(() => {
}
.config-right {
padding: 20px;
// color: rgba(0, 0, 0, 0.8);
// background: rgba(0, 0, 0, 0.04);
.config-right-item {
margin-bottom: 10px;

View File

@ -6,7 +6,7 @@
<div class="steps-content">
<div class="steps-box" v-if="current === 0">
<div class="alert">
<info-circle-outlined />
<AIcon type="InfoCircleOutlined" />
通过CTWing平台的HTTP推送服务进行数据接入
</div>
<div style="margin-top: 15px">
@ -160,7 +160,7 @@
<div class="steps-content">
<div class="steps-box" v-if="current === 1">
<div class="alert">
<info-circle-outlined />
<AIcon type="InfoCircleOutlined" />
只能选择HTTP通信方式的协议
</div>
<div class="search">
@ -170,9 +170,14 @@
style="width: 300px"
@search="procotolSearch"
/>
<a-button type="primary" @click="addProcotol"
>新增</a-button
<PermissionButton
type="primary"
@click="addProcotol"
hasPermission="link/Protocol:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
</div>
<div class="card-item">
<a-row :gutter="[24, 24]" v-if="procotolList.length > 0">
@ -282,96 +287,39 @@
>
下一步
</a-button>
<a-button
<PermissionButton
v-if="current === 2 && view === 'false'"
type="primary"
style="margin-right: 8px"
@click="saveData"
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</a-button>
</PermissionButton>
<a-button v-if="current > 0" @click="prev"> 上一步 </a-button>
</div>
</div>
</template>
<script lang="ts" setup name="AccessCloudCtwing">
import { message, Form } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue';
import { update, save, getNetworkList } from '@/api/link/accessConfig';
import { ProtocolMapping, NetworkTypeMapping } from '../../Detail/data';
import { InfoCircleOutlined } from '@ant-design/icons-vue';
import { update, save, getProtocolList } from '@/api/link/accessConfig';
import { ProtocolMapping } from '../../data';
import AccessCard from '../AccessCard/index.vue';
import { randomString } from '@/utils/utils';
import { getImage } from '@/utils/comm';
import { useMenuStore } from 'store/menu';
const menuStory = useMenuStore();
const origin = window.location.origin;
const img1 = getImage('/network/01.png');
const img2 = getImage('/network/02.jpg');
const img3 = getImage('/network/03.png');
const img4 = getImage('/network/04.jpg');
//1{
const resultList1 = [
{
id: '1612354213444087808',
name: '大华烟感协议',
},
{
id: '1610475299002855424',
name: '宇视摄像头协议',
},
{
id: '1610466717670780928',
name: '官方协议',
},
{
id: '1610205217785524224',
name: 'demo协议',
},
{
id: '1610204985806958592',
name: '水压协议',
},
{
id: '1605459961693745152',
name: '测试设备诊断日志显示',
},
{
id: '1582302200020783104',
name: 'demo',
},
{
id: '1581839391887794176',
name: '海康闸机协议',
},
{
id: '1567062365030637568',
name: '协议20220906160914',
},
{
id: '1561650927208628224',
name: 'local',
},
{
id: '1552881998413754368',
name: '官方协议V3-支持固件升级3',
},
{
id: '2b283b28a16d61e5fc2bdf39ceff34f8',
name: 'JetLinks官方协议',
description: 'JetLinks官方协议包',
},
{
id: '1551510679466844160',
name: '官方协议3.1',
},
{
id: '1551509716811161600',
name: '官方协议3.0',
},
];
interface FormState {
apiAddress: string;
appKey: string;
@ -397,7 +345,6 @@ const props = defineProps({
},
});
const channel = ref(props.provider.channel);
const formRef1 = ref<FormInstance>();
const formRef2 = ref<FormInstance>();
@ -459,37 +406,24 @@ const saveData = async () => {
});
if (resp.status === 200) {
message.success('操作成功!');
//
// if (window.onTabSaveSuccess) {
// window.onTabSaveSuccess(resp);
// setTimeout(() => window.close(), 300);
// } else {
// // this.$store.dispatch('jumpPathByKey', { key: MenuKeys['Link/AccessConfig'] })
// }
history.back();
}
// onFinish(data);
};
const queryProcotolList = async (id: string, params = {}) => {
// const resp = await getProtocolList(ProtocolMapping.get(id), {
// ...params,
// 'sorts[0].name': 'createTime',
// 'sorts[0].order': 'desc',
// });
// if (resp.status === 200) {
// procotolList.value = resp.result;
// allProcotolList.value = resp.result;
// }
//使1
procotolList.value = resultList1;
allProcotolList.value = resultList1;
const resp = await getProtocolList(ProtocolMapping.get(id), {
...params,
'sorts[0].name': 'createTime',
'sorts[0].order': 'desc',
});
if (resp.status === 200) {
procotolList.value = resp.result;
allProcotolList.value = resp.result;
}
};
const addProcotol = () => {
// const url = this.$store.state.permission.routes['Link/Protocol']
const url = '/iot/link/protocol';
const url = menuStory.menus['link/Protocol']?.path;
const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?save=true`,
);
@ -503,7 +437,7 @@ const addProcotol = () => {
const next = async () => {
if (current.value === 0) {
let data1: any = await formRef1.value?.validate();
await formRef1.value?.validate();
queryProcotolList(props.provider.id);
current.value = current.value + 1;
} else if (current.value === 1) {
@ -514,9 +448,11 @@ const next = async () => {
}
}
};
const prev = () => {
current.value = current.value - 1;
};
onMounted(() => {
if (id !== ':id') {
formState.value = props.data.configuration;
@ -527,6 +463,7 @@ onMounted(() => {
};
}
});
watch(
current,
(v) => {
@ -590,9 +527,6 @@ watch(
}
.config-right {
padding: 20px;
// color: rgba(0, 0, 0, 0.8);
// background: rgba(0, 0, 0, 0.04);
.config-right-item {
margin-bottom: 10px;

View File

@ -6,7 +6,7 @@
<div class="steps-content">
<div class="steps-box" v-if="current === 0">
<div class="alert">
<info-circle-outlined />
<AIcon type="InfoCircleOutlined" />
通过OneNet平台的HTTP推送服务进行数据接入
</div>
<div style="margin-top: 15px">
@ -41,7 +41,9 @@
同步物联网平台设备数据到OneNet
</p>
</template>
<question-circle-outlined />
<AIcon
type="QuestionCircleOutlined"
/>
</a-tooltip>
</div>
<a-input
@ -105,7 +107,9 @@
接收OneNet推送的Token地址
</p>
</template>
<question-circle-outlined />
<AIcon
type="QuestionCircleOutlined"
/>
</a-tooltip>
</div>
<a-input
@ -136,7 +140,9 @@
端生成的消息加密key
</p>
</template>
<question-circle-outlined />
<AIcon
type="QuestionCircleOutlined"
/>
</a-tooltip>
</div>
<a-input
@ -253,7 +259,7 @@
<div class="steps-content">
<div class="steps-box" v-if="current === 1">
<div class="alert">
<info-circle-outlined />
<AIcon type="InfoCircleOutlined" />
只能选择HTTP通信方式的协议
</div>
<div class="search">
@ -263,9 +269,14 @@
style="width: 300px"
@search="procotolSearch"
/>
<a-button type="primary" @click="addProcotol"
>新增</a-button
<PermissionButton
type="primary"
@click="addProcotol"
hasPermission="link/Protocol:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
</div>
<div class="card-item">
<a-row :gutter="[24, 24]" v-if="procotolList.length > 0">
@ -375,98 +386,38 @@
>
下一步
</a-button>
<a-button
<PermissionButton
style="margin-right: 8px"
v-if="current === 2 && view === 'false'"
type="primary"
@click="saveData"
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</a-button>
</PermissionButton>
<a-button v-if="current > 0" @click="prev"> 上一步 </a-button>
</div>
</div>
</template>
<script lang="ts" setup name="AccessCloudOneNet">
import { message, Form } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue';
import { update, save, getNetworkList } from '@/api/link/accessConfig';
import { ProtocolMapping, NetworkTypeMapping } from '../../Detail/data';
import {
InfoCircleOutlined,
QuestionCircleOutlined,
} from '@ant-design/icons-vue';
import { update, save, getProtocolList } from '@/api/link/accessConfig';
import AccessCard from '../AccessCard/index.vue';
import { randomString } from '@/utils/utils';
import { getImage } from '@/utils/comm';
import { ProtocolMapping } from '../../data';
import { useMenuStore } from 'store/menu';
const menuStory = useMenuStore();
const origin = window.location.origin;
const img5 = getImage('/network/05.jpg');
const img6 = getImage('/network/06.jpg');
const img = getImage('/network/OneNet.jpg');
//1{
const resultList1 = [
{
id: '1612354213444087808',
name: '大华烟感协议',
},
{
id: '1610475299002855424',
name: '宇视摄像头协议',
},
{
id: '1610466717670780928',
name: '官方协议',
},
{
id: '1610205217785524224',
name: 'demo协议',
},
{
id: '1610204985806958592',
name: '水压协议',
},
{
id: '1605459961693745152',
name: '测试设备诊断日志显示',
},
{
id: '1582302200020783104',
name: 'demo',
},
{
id: '1581839391887794176',
name: '海康闸机协议',
},
{
id: '1567062365030637568',
name: '协议20220906160914',
},
{
id: '1561650927208628224',
name: 'local',
},
{
id: '1552881998413754368',
name: '官方协议V3-支持固件升级3',
},
{
id: '2b283b28a16d61e5fc2bdf39ceff34f8',
name: 'JetLinks官方协议',
description: 'JetLinks官方协议包',
},
{
id: '1551510679466844160',
name: '官方协议3.1',
},
{
id: '1551509716811161600',
name: '官方协议3.0',
},
];
interface FormState {
apiAddress: string;
apiKey: string;
@ -478,9 +429,6 @@ interface Form {
name: string;
description: string;
}
const route = useRoute();
const view = route.query.view as string;
const id = route.params.id as string;
const props = defineProps({
provider: {
@ -493,7 +441,10 @@ const props = defineProps({
},
});
const channel = ref(props.provider.channel);
const route = useRoute();
const view = route.query.view as string;
const id = route.params.id as string;
const formRef1 = ref<FormInstance>();
const formRef2 = ref<FormInstance>();
@ -557,36 +508,24 @@ const saveData = async () => {
if (resp.status === 200) {
message.success('操作成功!');
//
// if (window.onTabSaveSuccess) {
// window.onTabSaveSuccess(resp);
// setTimeout(() => window.close(), 300);
// } else {
// // this.$store.dispatch('jumpPathByKey', { key: MenuKeys['Link/AccessConfig'] })
// }
history.back();
}
};
const queryProcotolList = async (id: string, params = {}) => {
// const resp = await getProtocolList(ProtocolMapping.get(id), {
// ...params,
// 'sorts[0].name': 'createTime',
// 'sorts[0].order': 'desc',
// });
// if (resp.status === 200) {
// procotolList.value = resp.result;
// allProcotolList.value = resp.result;
// }
//使1
procotolList.value = resultList1;
allProcotolList.value = resultList1;
const resp = await getProtocolList(ProtocolMapping.get(id), {
...params,
'sorts[0].name': 'createTime',
'sorts[0].order': 'desc',
});
if (resp.status === 200) {
procotolList.value = resp.result;
allProcotolList.value = resp.result;
}
};
const addProcotol = () => {
// const url = this.$store.state.permission.routes['Link/Protocol']
const url = '/iot/link/protocol';
const url = menuStory.menus['link/Protocol']?.path;
const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?save=true`,
);
@ -689,9 +628,6 @@ watch(
}
.config-right {
padding: 20px;
// color: rgba(0, 0, 0, 0.8);
// background: rgba(0, 0, 0, 0.04);
.config-right-item {
margin-bottom: 10px;

View File

@ -10,7 +10,7 @@
<div v-if="channel !== 'edge-child-device'" class="steps-content">
<div class="steps-box" v-if="current === 0">
<div class="alert">
<question-circle-outlined />
<AIcon type="InfoCircleOutlined" />
选择与设备通信的网络组件
</div>
<div class="search">
@ -20,7 +20,14 @@
style="width: 300px"
@search="networkSearch"
/>
<a-button type="primary" @click="addNetwork">新增</a-button>
<PermissionButton
type="primary"
@click="addNetwork"
hasPermission="link/Type:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
</div>
<div class="card-item">
<a-row :gutter="[24, 24]" v-if="networkList.length > 0">
@ -103,51 +110,53 @@
<a-row :gutter="[24, 24]">
<a-col :span="12">
<title-component data="基本信息" />
<div>
<a-form
:model="formState"
name="basic"
autocomplete="off"
layout="vertical"
@finish="onFinish"
ref="formRef"
<a-form
:model="formState"
name="basic"
autocomplete="off"
layout="vertical"
@finish="onFinish"
ref="formRef"
>
<a-form-item
label="名称"
name="name"
:rules="[
{
required: true,
message: '请输入名称',
trigger: 'blur',
},
{ max: 64, message: '最多可输入64个字符' },
]"
>
<a-form-item
label="名称"
name="name"
:rules="[
{
required: true,
message: '请输入名称',
trigger: 'blur',
},
{ max: 64, message: '最多可输入64个字符' },
]"
<a-input
placeholder="请输入名称"
v-model:value="formState.name"
/>
</a-form-item>
<a-form-item label="说明" name="description">
<a-textarea
placeholder="请输入说明"
:rows="4"
v-model:value="formState.description"
show-count
:maxlength="200"
/>
</a-form-item>
<a-form-item>
<PermissionButton
v-if="current !== 1 && view === 'false'"
type="primary"
html-type="submit"
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
<a-input
placeholder="请输入名称"
v-model:value="formState.name"
/>
</a-form-item>
<a-form-item label="说明" name="description">
<a-textarea
placeholder="请输入说明"
:rows="4"
v-model:value="formState.description"
show-count
:maxlength="200"
/>
</a-form-item>
<a-form-item>
<a-button
v-if="current !== 1 && view === 'false'"
type="primary"
html-type="submit"
>保存</a-button
>
</a-form-item>
</a-form>
</div>
保存
</PermissionButton>
</a-form-item>
</a-form>
</a-col>
<a-col :span="12">
<div class="config-right">
@ -178,119 +187,35 @@
>
下一步
</a-button>
<a-button
<PermissionButton
v-if="current === 1 && view === 'false'"
type="primary"
style="margin-right: 8px"
@click="saveData"
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</a-button>
</PermissionButton>
<a-button v-if="current > 0" @click="prev"> 上一步 </a-button>
</div>
</div>
</template>
<script lang="ts" setup name="AccessEdge">
import { message, Form } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue';
import { update, save, getNetworkList } from '@/api/link/accessConfig';
import {
descriptionList,
ProtocolMapping,
NetworkTypeMapping,
} from '../../Detail/data';
import { QuestionCircleOutlined } from '@ant-design/icons-vue';
} from '../../data';
import AccessCard from '../AccessCard/index.vue';
import { useMenuStore } from 'store/menu';
//1
const networkListTest = {
message: 'success',
result: [
{
id: '1585192878304051200',
name: 'MQTT网络组件',
addresses: [
{
address: 'mqtt://120.77.179.54:8101',
health: 1,
ok: true,
bad: false,
disabled: false,
},
],
},
{
id: '1583268266806009856',
name: '我的第一个MQTT服务组件',
description: '',
addresses: [
{
address: 'mqtt://120.77.179.54:8100',
health: 1,
ok: true,
bad: false,
disabled: false,
},
],
},
{
id: '1570335308902912000',
name: '0915MQTT网络组件_勿动',
description: '测试,勿动!',
addresses: [
{
address: 'mqtt://120.77.179.54:8083',
health: 1,
ok: true,
bad: false,
disabled: false,
},
],
},
{
id: '1567062350140858368',
name: '网络组件20220906160907',
addresses: [
{
address: 'mqtt://120.77.179.54:8083',
health: 1,
ok: true,
bad: false,
disabled: false,
},
],
},
{
id: '1556563257890742272',
name: 'MQTT网络组件',
addresses: [
{
address: 'mqtt://0.0.0.0:8104',
health: 1,
ok: true,
bad: false,
disabled: false,
},
],
},
{
id: '1534774770408108032',
name: 'MQTT',
addresses: [
{
address: 'mqtt://120.77.179.54:8088',
health: 1,
ok: true,
bad: false,
disabled: false,
},
],
},
],
status: 200,
timestamp: 1674960624150,
};
const menuStory = useMenuStore();
interface FormState {
name: string;
@ -327,6 +252,7 @@ const stepCurrent = ref(0);
const steps = ref(['网络组件', '完成']);
const networkCurrent = ref('');
const networkList = ref([]);
const allNetworkList = ref([]);
const onFinish = async (values: any) => {
const providerId = props.provider.id;
@ -341,16 +267,7 @@ const onFinish = async (values: any) => {
id === ':id' ? await save(params) : await update({ ...params, id });
if (resp.status === 200) {
message.success('操作成功!');
// if (params.get('save')) {
// if ((window as any).onTabSaveSuccess) {
// if (resp.result) {
// (window as any).onTabSaveSuccess(resp.result);
// setTimeout(() => window.close(), 300);
// }
// }
// } else {
history.back();
// }
}
};
@ -359,28 +276,27 @@ const checkedChange = (id: string) => {
};
const queryNetworkList = async (id: string, include: string, data = {}) => {
// const resp = await getNetworkList(
// NetworkTypeMapping.get(id),
// include,
// data,
// );
// if (resp.status === 200) {
// networkList.value = resp.result;
// }
//使1
networkList.value = networkListTest.result;
const resp = await getNetworkList(
NetworkTypeMapping.get(id),
include,
data,
);
if (resp.status === 200) {
networkList.value = resp.result;
allNetworkList.value = resp.result;
}
};
const networkSearch = (value: string) => {
queryNetworkList(props.provider.id, networkCurrent.value || '', {
terms: [
{
column: 'name$LIKE',
value: `%${value}%`,
},
],
});
if (value) {
networkList.value = allNetworkList.value.filter(
(i: any) =>
i.name &&
i.name.toLocaleLowerCase().includes(value.toLocaleLowerCase()),
);
} else {
networkList.value = allNetworkList.value;
}
};
const saveData = async () => {
@ -389,8 +305,7 @@ const saveData = async () => {
};
const addNetwork = () => {
// const url = this.$store.state.permission.routes['Link/Type/Detail']
const url = '/iot/link/type/detail/:id';
const url = menuStory.menus['link/Type/Detail']?.path;
const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?type=${
NetworkTypeMapping.get(props.provider?.id) || ''
@ -426,17 +341,17 @@ onMounted(() => {
};
networkCurrent.value = props.data.channelId;
}
}),
watch(
current,
(v) => {
stepCurrent.value = v;
},
{
deep: true,
immediate: true,
},
);
});
watch(
current,
(v) => {
stepCurrent.value = v;
},
{
deep: true,
immediate: true,
},
);
</script>
<style lang="less" scoped>

View File

@ -6,7 +6,7 @@
<div class="steps-content">
<div class="steps-box" v-if="current === 0">
<div class="alert">
<info-circle-outlined />
<AIcon type="InfoCircleOutlined" />
配置设备信令参数
</div>
<div>
@ -85,7 +85,7 @@
独立配置:集群下不同节点使用不同配置
</p>
</template>
<question-circle-outlined />
<AIcon type="QuestionCircleOutlined" />
</a-tooltip>
</div>
@ -218,9 +218,7 @@
:header="`#${index + 1}.节点`"
>
<template #extra>
<delete-outlined
@click="removeCluster(cluster)"
/>
<AIcon type="DeleteOutlined" />
</template>
<a-row :gutter="[24, 24]">
<a-col :span="8">
@ -274,7 +272,9 @@
绑定到服务器上的网卡地址,绑定到所有网卡:0.0.0.0
</p>
</template>
<question-circle-outlined />
<AIcon
type="QuestionCircleOutlined"
/>
</a-tooltip>
</div>
@ -363,7 +363,9 @@
监听指定端口的请求
</p>
</template>
<question-circle-outlined />
<AIcon
type="QuestionCircleOutlined"
/>
</a-tooltip>
</div>
<a-input
@ -417,7 +419,7 @@
block
@click="addCluster"
>
<PlusOutlined />
<AIcon type="PlusOutlined" />
新增
</a-button>
</a-form-item>
@ -502,14 +504,17 @@
>
下一步
</a-button>
<a-button
<PermissionButton
v-if="current === 1 && view === 'false'"
type="primary"
style="margin-right: 8px"
@click="saveData"
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</a-button>
</PermissionButton>
<a-button v-if="current > 0" @click="prev"> 上一步 </a-button>
</div>
</div>
@ -519,12 +524,6 @@
import { message, Form } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue';
import { getResourcesCurrent, getClusters } from '@/api/link/accessConfig';
import {
DeleteOutlined,
PlusOutlined,
QuestionCircleOutlined,
InfoCircleOutlined,
} from '@ant-design/icons-vue';
import { update, save } from '@/api/link/accessConfig';
interface Form2 {

View File

@ -39,12 +39,16 @@
/>
</a-form-item>
<a-form-item>
<a-button
<PermissionButton
v-if="view === 'false'"
type="primary"
html-type="submit"
>保存</a-button
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</PermissionButton>
</a-form-item>
</a-form>
</div>
@ -122,16 +126,7 @@ const onFinish = async (values: any) => {
id === ':id' ? await save(params) : await update({ ...params, id });
if (resp.status === 200) {
message.success('操作成功!');
// if (params.get('save')) {
// if ((window as any).onTabSaveSuccess) {
// if (resp.result) {
// (window as any).onTabSaveSuccess(resp.result);
// setTimeout(() => window.close(), 300);
// }
// }
// } else {
history.back();
// }
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,701 @@
<template>
<div>
<a-steps :current="stepCurrent">
<a-step v-for="item in steps" :key="item" :title="item" />
</a-steps>
<div class="steps-content">
<div class="steps-box" v-if="current === 0">
<div class="alert">
<AIcon type="InfoCircleOutlined" />
选择与设备通信的网络组件
</div>
<div class="search">
<a-input-search
allowClear
placeholder="请输入"
style="width: 300px"
@search="networkSearch"
/>
<PermissionButton
type="primary"
@click="addNetwork"
hasPermission="link/Type:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
</div>
<div class="card-item">
<a-row :gutter="[24, 24]" v-if="networkList.length > 0">
<a-col
:span="8"
v-for="item in networkList"
:key="item.id"
>
<access-card
@checkedChange="checkedChange"
:checked="networkCurrent"
:data="{
...item,
description: item.description
? item.description
: descriptionList[provider.id],
}"
>
<template #other>
<div class="other">
<a-tooltip placement="topLeft">
<div
v-if="
(item.addresses || [])
.length > 1
"
>
<div
v-for="i in item.addresses ||
[]"
:key="i.address"
class="item"
>
<a-badge
:color="getColor(i)"
/>{{ i.address }}
</div>
</div>
<div
v-for="i in (
item.addresses || []
).slice(0, 1)"
:key="i.address"
class="item"
>
<a-badge
:color="getColor(i)"
:text="i.address"
/>
<span
v-if="
(item.addresses || [])
.length > 1
"
>...</span
>
</div>
</a-tooltip>
</div>
</template>
</access-card>
</a-col>
</a-row>
<a-empty v-else description="暂无数据" />
</div>
</div>
<div class="steps-box" v-else-if="current === 1">
<div class="alert">
<AIcon type="InfoCircleOutlined" />
使用选择的消息协议对网络组件通信数据进行编解码认证等操作
</div>
<div class="search">
<a-input-search
allowClear
placeholder="请输入"
style="width: 300px"
@search="procotolSearch"
/>
<PermissionButton
type="primary"
@click="addProcotol"
hasPermission="link/Protocol:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
</div>
<div class="card-item">
<a-row :gutter="[24, 24]" v-if="procotolList.length > 0">
<a-col
:span="8"
v-for="item in procotolList"
:key="item?.id"
>
<access-card
@checkedChange="procotolChange"
:checked="procotolCurrent"
:data="item"
>
</access-card>
</a-col>
</a-row>
<a-empty v-else description="暂无数据" />
</div>
</div>
<div class="steps-box" v-else>
<div
class="card-last"
:style="`max-height:${
clientHeight > 900 ? 750 : clientHeight * 0.7
}px`"
>
<a-row :gutter="[24, 24]">
<a-col :span="12">
<title-component data="基本信息" />
<div>
<a-form
ref="formRef"
:model="formData"
layout="vertical"
>
<a-form-item
label="名称"
v-bind="validateInfos.name"
>
<a-input
v-model:value="formData.name"
allowClear
placeholder="请输入名称"
/>
</a-form-item>
<a-form-item
label="说明"
v-bind="validateInfos.description"
>
<a-textarea
placeholder="请输入说明"
:rows="4"
v-model:value="formData.description"
show-count
:maxlength="200"
/>
</a-form-item>
</a-form>
</div>
</a-col>
<a-col :span="12">
<div class="config-right">
<div class="config-right-item">
<div class="config-right-item-title">
接入方式
</div>
<div class="config-right-item-context">
{{ provider.name }}
</div>
<div class="config-right-item-context">
{{ provider.description }}
</div>
</div>
<div class="config-right-item">
<div class="config-right-item-title">
消息协议
</div>
<div class="config-right-item-context">
{{
procotolList.find(
(i) => i.id === procotolCurrent,
).name
}}
</div>
<div
class="config-right-item-context"
v-if="config.document"
>
<Markdown :source="config.document" />
</div>
</div>
<div
class="config-right-item"
v-if="getNetworkCurrent()"
>
<div class="config-right-item-title">
网络组件
</div>
<div
v-for="i in getNetworkCurrentData()"
:key="i.address"
>
<a-badge
:color="getColor(i)"
:text="i.address"
/>
</div>
</div>
<div
class="config-right-item"
v-if="
config.routes &&
config.routes.length > 0
"
>
<div class="config-right-item-title">
{{
data.provider ===
'mqtt-server-gateway' ||
data.provider ===
'mqtt-client-gateway'
? 'topic'
: 'URL信息'
}}
</div>
<a-table
:pagination="false"
:rowKey="generateUUID()"
:data-source="config.routes || []"
bordered
:columns="
config.id === 'MQTT'
? columnsMQTT
: columnsHTTP
"
:scroll="{ y: 300 }"
>
<template
#bodyCell="{ column, text, record }"
>
<template
v-if="
column.dataIndex ===
'stream'
"
>
<span>{{
getStream(record)
}}</span>
</template>
</template>
</a-table>
</div>
</div>
</a-col>
</a-row>
</div>
</div>
</div>
<div class="steps-action">
<a-button
v-if="[0, 1].includes(current)"
type="primary"
style="margin-right: 8px"
@click="next"
>
下一步
</a-button>
<PermissionButton
v-if="current === 2 && view === 'false'"
type="primary"
style="margin-right: 8px"
@click="saveData"
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</PermissionButton>
<a-button
v-if="type === 'child-device' ? current > 1 : current > 0"
@click="prev"
>
上一步
</a-button>
</div>
</div>
</template>
<script lang="ts" setup name="AccessNetwork">
import {
getNetworkList,
getProtocolList,
getConfigView,
save,
update,
getChildConfigView,
} from '@/api/link/accessConfig';
import {
descriptionList,
NetworkTypeMapping,
ProtocolMapping,
ColumnsMQTT,
ColumnsHTTP,
} from '../../data';
import AccessCard from '../AccessCard/index.vue';
import { message, Form } from 'ant-design-vue';
import type { FormInstance, TableColumnType } from 'ant-design-vue';
import Markdown from 'vue3-markdown-it';
import { useMenuStore } from 'store/menu';
const menuStory = useMenuStore();
function generateUUID() {
var d = new Date().getTime();
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
/[xy]/g,
function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
},
);
}
const props = defineProps({
provider: {
type: Object,
default: () => {},
},
data: {
type: Object,
default: () => {},
},
});
const clientHeight = document.body.clientHeight;
const type = props.provider.channel;
const route = useRoute();
const view = route.query.view as string;
const id = route.params.id as string;
const formRef = ref<FormInstance>();
const useForm = Form.useForm;
const current = ref(0);
const stepCurrent = ref(0);
const steps = ref(['网络组件', '消息协议', '完成']);
const networkList: any = ref([]);
const allNetworkList: any = ref([]);
const procotolList = ref([]);
const allProcotolList = ref([]);
const networkCurrent: any = ref('');
const procotolCurrent: any = ref('');
const config: any = ref({});
const columnsMQTT = ref(<TableColumnType>[]);
const columnsHTTP = ref(<TableColumnType>[]);
const formData = ref({
name: '',
description: '',
});
const { resetFields, validate, validateInfos } = useForm(
formData,
reactive({
name: [
{ required: true, message: '请输入名称', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
],
description: [{ max: 200, message: '最多可输入200个字符' }],
}),
);
const queryNetworkList = async (id: string, include: string, data = {}) => {
const resp = await getNetworkList(
NetworkTypeMapping.get(id),
include,
data,
);
if (resp.status === 200) {
networkList.value = resp.result;
}
};
const queryProcotolList = async (id: string, params = {}) => {
const resp = await getProtocolList(ProtocolMapping.get(id), {
...params,
'sorts[0].name': 'createTime',
'sorts[0].order': 'desc',
});
if (resp.status === 200) {
procotolList.value = resp.result;
allProcotolList.value = resp.result;
}
};
const addNetwork = () => {
const url = menuStory.menus['link/Type/Detail']?.path;
const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?type=${
NetworkTypeMapping.get(props.provider?.id) || ''
}`,
);
tab.onTabSaveSuccess = (value) => {
if (value.success) {
networkCurrent.value = value.result.id;
queryNetworkList(props.provider?.id, networkCurrent.value || '');
}
};
};
const addProcotol = () => {
const url = menuStory.menus['link/Protocol']?.path;
const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?save=true`,
);
tab.onTabSaveSuccess = (value) => {
if (value.success) {
procotolCurrent.value = value.result?.id;
queryProcotolList(props.provider?.id);
}
};
};
const getNetworkCurrent = () =>
networkList.value.find((i) => i.id === networkCurrent) &&
(networkList.value.find((i) => i.id === networkCurrent).addresses || [])
.length > 0;
const getNetworkCurrentData = () =>
getNetworkCurrent()
? networkList.value.find((i) => i.id === networkCurrent).addresses
: [];
const getColor = (i) => (i.health === -1 ? 'red' : 'green');
const getStream = (record: any) => {
let stream = '';
if (record.upstream && record.downstream) stream = '上行、下行';
else if (record.upstream) stream = '上行';
else if (record.downstream) stream = '下行';
return stream;
};
const checkedChange = (id: string) => {
networkCurrent.value = id;
};
const networkSearch = (value: string) => {
if (value) {
networkList.value = allNetworkList.value.filter(
(i: any) =>
i.name &&
i.name.toLocaleLowerCase().includes(value.toLocaleLowerCase()),
);
} else {
networkList.value = allNetworkList.value;
}
};
const procotolChange = (id: string) => {
if (!props.data.id) {
procotolCurrent.value = id;
}
};
const procotolSearch = (value: string) => {
if (value) {
const list = allProcotolList.value.filter((i: any) => {
return (
i.name &&
i.name.toLocaleLowerCase().includes(value.toLocaleLowerCase())
);
});
procotolList.value = list;
} else {
procotolList.value = allProcotolList.value;
}
};
const saveData = () => {
validate()
.then(async (values) => {
const params = {
...props.data,
...values,
protocol: procotolCurrent.value,
channel: 'network', //
channelId: networkCurrent.value,
};
const resp =
id === ':id'
? await save(params)
: await update({
...params,
id,
provider: props.provider.id,
transport:
props.provider?.id === 'child-device'
? 'Gateway'
: ProtocolMapping.get(props.provider.id),
});
if (resp.status === 200) {
message.success('操作成功!');
history.back();
}
})
.catch((err) => {});
};
const next = async () => {
if (current.value === 0) {
if (!networkCurrent.value) {
message.error('请选择网络组件!');
} else {
queryProcotolList(props.provider.id);
current.value = current.value + 1;
}
} else if (current.value === 1) {
if (!procotolCurrent.value) {
message.error('请选择消息协议!');
} else {
const resp =
type !== 'child-device'
? await getConfigView(
procotolCurrent.value,
ProtocolMapping.get(props.provider.id),
)
: await getChildConfigView(procotolCurrent.value);
if (resp.status === 200) {
config.value = resp.result;
console.log(222, config.value);
current.value = current.value + 1;
const Group = {
title: '分组',
dataIndex: 'group',
key: 'group',
ellipsis: true,
align: 'center',
width: 100,
customCell: (record: any, rowIndex: number) => {
const obj = {
children: record,
rowSpan: 0,
};
const list = config.value?.routes || [];
const arr = list.filter(
(res: any) => res.group === record.group,
);
const isRowIndex =
rowIndex === 0 ||
list[rowIndex - 1].group !== record.group;
isRowIndex && (obj.rowSpan = arr.length);
return obj;
},
};
columnsMQTT.value = [Group, ...ColumnsMQTT];
columnsHTTP.value = [Group, ...ColumnsHTTP];
}
}
}
};
const prev = () => {
current.value = current.value - 1;
};
onMounted(() => {
if (props.data && props.data.id) {
if (props.data.provider !== 'child-device') {
procotolCurrent.value = props.data.protocol;
current.value = 0;
networkCurrent.value = props.data.channelId;
queryNetworkList(props.provider.id, networkCurrent.value);
procotolCurrent.value = props.data.protocol;
steps.value = ['网络组件', '消息协议', '完成'];
} else {
steps.value = ['消息协议', '完成'];
current.value = 1;
queryProcotolList(props.provider.id);
}
} else {
if (props.provider?.id) {
if (type !== 'child-device') {
queryNetworkList(props.provider.id, '');
steps.value = ['网络组件', '消息协议', '完成'];
current.value = 0;
} else {
steps.value = ['消息协议', '完成'];
current.value = 1;
queryProcotolList(props.provider.id);
}
}
}
});
onMounted(() => {
if (id !== ':id') {
procotolCurrent.value = props.data.protocol;
formData.value = {
name: props.data.name,
description: props.data.description,
};
}
});
watch(
current,
(v) => {
stepCurrent.value = type === 'child-device' ? v - 1 : v;
},
{
deep: true,
immediate: true,
},
);
</script>
<style lang="less" scoped>
.steps-content {
margin: 20px;
}
.steps-box {
min-height: 400px;
.card-item {
padding-right: 5px;
max-height: 480px;
overflow-y: auto;
overflow-x: hidden;
}
.card-last {
padding-right: 5px;
overflow-y: auto;
overflow-x: hidden;
}
}
.steps-action {
width: 100%;
margin-top: 24px;
margin-left: 20px;
}
.alert {
height: 40px;
padding-left: 10px;
color: rgba(0, 0, 0, 0.55);
line-height: 40px;
background-color: #f6f6f6;
}
.search {
display: flex;
margin: 15px 0;
justify-content: space-between;
}
.other {
width: 100%;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
.item {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.config-right {
padding: 20px;
color: rgba(0, 0, 0, 0.8);
background: rgba(0, 0, 0, 0.04);
.config-right-item {
margin-bottom: 10px;
.config-right-item-title {
width: 100%;
margin-bottom: 10px;
font-weight: 600;
}
.config-right-item-context {
margin: 5px 0;
color: rgba(0, 0, 0, 0.8);
}
}
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div v-for="items in dataSource" :key="items.type">
<a-card class="card-items">
<div v-for="items in dataSource" :key="items.type" class="card-items">
<div class="card-items-container">
<TitleComponent :data="items.title"></TitleComponent>
<a-row :gutter="[24, 24]">
<a-col :span="12" v-for="item in items.list" :key="item.id">
@ -8,7 +8,7 @@
<div class="box">
<div class="left">
<div class="images">
<img :src="backMap.get(item.id)" />
<img :src="BackMap.get(item.id)" />
</div>
<div class="context">
<div class="title">
@ -30,13 +30,13 @@
</div>
</a-col>
</a-row>
</a-card>
</div>
</div>
</template>
<script lang="ts" setup name="AccessConfigProvider">
import TitleComponent from '@/components/TitleComponent/index.vue';
import { getImage } from '@/utils/comm';
import { BackMap } from '../../data';
const props = defineProps({
dataSource: {
@ -47,32 +47,16 @@ const props = defineProps({
const emit = defineEmits(['onClick']);
const backMap = new Map();
backMap.set('mqtt-server-gateway', getImage('/access/mqtt.png'));
backMap.set('websocket-server', getImage('/access/websocket.png'));
backMap.set('modbus-tcp', getImage('/access/modbus.png'));
backMap.set('coap-server-gateway', getImage('/access/coap.png'));
backMap.set('tcp-server-gateway', getImage('/access/tcp.png'));
backMap.set('Ctwing', getImage('/access/ctwing.png'));
backMap.set('child-device', getImage('/access/child-device.png'));
backMap.set('opc-ua', getImage('/access/opc-ua.png'));
backMap.set('http-server-gateway', getImage('/access/http.png'));
backMap.set('fixed-media', getImage('/access/video-device.png'));
backMap.set('udp-device-gateway', getImage('/access/udp.png'));
backMap.set('OneNet', getImage('/access/onenet.png'));
backMap.set('gb28181-2016', getImage('/access/gb28181.png'));
backMap.set('mqtt-client-gateway', getImage('/access/mqtt-broke.png'));
backMap.set('edge-child-device', getImage('/access/child-device.png'));
backMap.set('official-edge-gateway', getImage('/access/edge.png'));
const click = (value: object) => {
emit('onClick', value);
};
</script>
<style lang="less" scoped>
.card-items{
.card-items {
margin-bottom: 24px;
.card-items-container {
}
}
.provider {
position: relative;
@ -93,9 +77,6 @@ const click = (value: object) => {
background-image: url('/public/images/access/rectangle.png');
background-repeat: no-repeat;
background-size: 100% 100%;
// border: 1px #8da1f4 solid;
// border-bottom-left-radius: 10%;
// border-bottom-right-radius: 10%;
content: ' ';
}
@ -109,7 +90,6 @@ const click = (value: object) => {
align-items: center;
justify-content: space-between;
width: 100%;
.left {
display: flex;
width: calc(100% - 70px);

View File

@ -1,3 +1,4 @@
import { getImage } from '@/utils/comm';
const ProtocolMapping = new Map();
ProtocolMapping.set('websocket-server', 'WebSocket');
@ -25,6 +26,23 @@ NetworkTypeMapping.set('mqtt-server-gateway', 'MQTT_SERVER');
NetworkTypeMapping.set('tcp-server-gateway', 'TCP_SERVER');
NetworkTypeMapping.set('official-edge-gateway', 'MQTT_SERVER');
const BackMap = new Map();
BackMap.set('mqtt-server-gateway', getImage('/access/mqtt.png'));
BackMap.set('websocket-server', getImage('/access/websocket.png'));
BackMap.set('modbus-tcp', getImage('/access/modbus.png'));
BackMap.set('coap-server-gateway', getImage('/access/coap.png'));
BackMap.set('tcp-server-gateway', getImage('/access/tcp.png'));
BackMap.set('Ctwing', getImage('/access/ctwing.png'));
BackMap.set('child-device', getImage('/access/child-device.png'));
BackMap.set('opc-ua', getImage('/access/opc-ua.png'));
BackMap.set('http-server-gateway', getImage('/access/http.png'));
BackMap.set('fixed-media', getImage('/access/video-device.png'));
BackMap.set('udp-device-gateway', getImage('/access/udp.png'));
BackMap.set('OneNet', getImage('/access/onenet.png'));
BackMap.set('gb28181-2016', getImage('/access/gb28181.png'));
BackMap.set('mqtt-client-gateway', getImage('/access/mqtt-broke.png'));
BackMap.set('edge-child-device', getImage('/access/child-device.png'));
BackMap.set('official-edge-gateway', getImage('/access/edge.png'));
const descriptionList = {
'udp-device-gateway':
@ -43,21 +61,21 @@ const descriptionList = {
'CoAP是针对只有少量的内存空间和有限的计算能力提供的一种基于UDP的协议。便于低功耗或网络受限的设备与平台通信仅支持设备和平台之间单对单的请求-响应模式。',
};
const columnsMQTT = [
{
title: '分组',
dataIndex: 'group',
key: 'group',
ellipsis: true,
align: 'center',
width: 100,
scopedSlots: { customRender: 'group' },
},
const ColumnsMQTT = [
// {
// title: '分组',
// dataIndex: 'group',
// key: 'group',
// ellipsis: true,
// align: 'center',
// width: 100,
// scopedSlots: { customRender: 'group' },
// },
{
title: 'topic',
dataIndex: 'topic',
key: 'topic',
scopedSlots: { customRender: 'topic' },
ellipsis: true,
},
{
title: '上下行',
@ -72,37 +90,58 @@ const columnsMQTT = [
title: '说明',
dataIndex: 'description',
key: 'description',
scopedSlots: { customRender: 'description' },
},
]
const columnsHTTP = [
{
title: '分组',
dataIndex: 'group',
key: 'group',
ellipsis: true,
width: 100,
scopedSlots: { customRender: 'group' },
},
{
},
];
const ColumnsHTTP = [
// {
// title: '分组',
// dataIndex: 'group',
// key: 'group',
// ellipsis: true,
// width: 100,
// scopedSlots: { customRender: 'group' },
// },
{
title: '地址',
dataIndex: 'address',
key: 'address',
scopedSlots: { customRender: 'address' },
},
{
ellipsis: true,
// scopedSlots: { customRender: 'address' },
},
{
title: '示例',
dataIndex: 'example',
key: 'example',
scopedSlots: { customRender: 'example' },
},
{
ellipsis: true,
// scopedSlots: { customRender: 'example' },
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
scopedSlots: { customRender: 'description' }
},
]
ellipsis: true,
// scopedSlots: { customRender: 'description' },
},
];
export { NetworkTypeMapping, ProtocolMapping, descriptionList, columnsMQTT, columnsHTTP };
const TiTlePermissionButtonStyle = {
padding: 0,
color: ' #1890ff !important',
'font-weight': 700,
'font-size': '16px',
overflow: 'hidden',
'text-overflow': 'ellipsis',
'white-space': 'nowrap',
};
export {
NetworkTypeMapping,
ProtocolMapping,
BackMap,
descriptionList,
ColumnsMQTT,
ColumnsHTTP,
TiTlePermissionButtonStyle,
};

View File

@ -11,12 +11,23 @@
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
gridColumn="2"
gridColumns="[2]"
:params="params"
>
<template #headerTitle>
<a-button type="primary" @click="handlAdd"
><AIcon type="PlusOutlined" />新增</a-button
>
<a-space>
<PermissionButton
type="primary"
@click="handlAdd"
hasPermission="link/AccessConfig:add"
>
<template #icon
><AIcon type="PlusOutlined"
/></template>
新增
</PermissionButton>
</a-space>
</template>
<template #card="slotProps">
<CardBox
@ -43,12 +54,15 @@
</template>
<template #content>
<div class="card-item-content">
<h3
<PermissionButton
type="link"
@click="handlEye(slotProps.id)"
class="card-item-content-title card-item-content-title-a"
hasPermission="link/AccessConfig:view"
:style="TiTlePermissionButtonStyle"
>
{{ slotProps.name }}
</h3>
</PermissionButton>
<a-row class="card-item-content-box">
<a-col
:span="12"
@ -65,13 +79,7 @@
"
>
<a-badge
:status="
slotProps.channelInfo
.addresses[0].health ===
-1
? 'error'
: 'success'
"
:status="getStatus(slotProps)"
/>
<a-tooltip>
<template #title>{{
@ -112,24 +120,12 @@
<a-tooltip>
<template #title>
{{
slotProps.description
? slotProps.description
: providersList.find(
(item) =>
item.id ===
slotProps.provider,
)?.description
getDescription(
slotProps,
)
}}
</template>
{{
slotProps.description
? slotProps.description
: providersList.find(
(item) =>
item.id ===
slotProps.provider,
)?.description
}}
{{ getDescription(slotProps) }}
</a-tooltip>
</div>
</a-col>
@ -138,42 +134,24 @@
</template>
<template #actions="item">
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
<PermissionButton
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
:hasPermission="'link/AccessConfig:' + item.key"
>
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
>
<a-button :disabled="item.disabled">
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</template>
</a-button>
</a-popconfirm>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<a-button
:disabled="item.disabled"
@click="item.onClick"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</template>
</a-button>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-tooltip>
</PermissionButton>
</template>
</CardBox>
</template>
@ -198,12 +176,14 @@ import {
deploy,
} from '@/api/link/accessConfig';
import { message } from 'ant-design-vue';
import { useMenuStore } from 'store/menu';
import { TiTlePermissionButtonStyle } from './data';
const menuStory = useMenuStore();
const tableRef = ref<Record<string, any>>({});
const router = useRouter();
const params = ref<Record<string, any>>({});
let providersList = ref([]);
let providersList = ref<Record<string, any>>([]);
const statusMap = new Map();
statusMap.set('enabled', 'success');
@ -225,8 +205,6 @@ const columns = [
key: 'provider',
search: {
type: 'select',
// options: providersList,
// options: getProvidersList
options: async () => {
const res = await getProviders();
return (res?.result || []).map((item) => ({
@ -275,9 +253,10 @@ const columns = [
const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
if (!data) return [];
const state = data.state.value;
const stateText = state === 'enabled' ? '禁用' : '启用';
return [
{
key: 'edit',
key: 'update',
text: '编辑',
tooltip: {
title: '编辑',
@ -289,13 +268,13 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
},
{
key: 'action',
text: state === 'enabled' ? '禁用' : '启用',
text: stateText,
tooltip: {
title: state === 'enabled' ? '禁用' : '启用',
title: stateText,
},
icon: state === 'enabled' ? 'StopOutlined' : 'CheckCircleOutlined',
popConfirm: {
title: `确认${state === 'enabled' ? '禁用' : '启用'}?`,
title: `确认${stateText}?`,
onConfirm: async () => {
let res =
state === 'enabled'
@ -342,27 +321,29 @@ const getProvidersList = async () => {
getProvidersList();
const handlAdd = () => {
// router.push('/link/accessConfig/detail/add/new');
router.push({
path: `/iot/link/accessConfig/detail/:id`,
query: { view: false },
});
menuStory.jumpPage(
`link/AccessConfig/Detail`,
{ id: ':id' },
{ view: false },
);
};
const handlEdit = (id: string) => {
// router.push(`/link/accessConfig/detail/edit/${id}`);
router.push({
path: `/iot/link/accessConfig/detail/${id}`,
query: { view: false },
});
menuStory.jumpPage(`link/AccessConfig/Detail`, { id }, { view: false });
};
const handlEye = (id: string) => {
// router.push(`/link/accessConfig/detail/view/${id}`);
router.push({
path: `/iot/link/accessConfig/detail/${id}`,
query: { view: true },
});
menuStory.jumpPage(`link/AccessConfig/Detail`, { id }, { view: true });
};
const getDescription = (slotProps: Record<string, any>) =>
slotProps.description
? slotProps.description
: providersList?.find(
(item: Record<string, any>) => item.id === slotProps.provider,
)?.description;
const getStatus = (slotProps: Record<string, any>) =>
slotProps.channelInfo.addresses[0].health === -1 ? 'error' : 'success';
/**
* 搜索
* @param params
@ -370,18 +351,6 @@ const handlEye = (id: string) => {
const handleSearch = (e: any) => {
params.value = e;
};
// const handlAdd = () => {
// router.push({
// path: '/link/accessConfig/detail/add',
// query: {
// id: '1610475400026861568',
// },
// });
// };
// const handlAdd = () => {
// router.push('/link/accessConfig/detail/add');
// }
</script>
<style lang="less" scoped>
.tableCardDisabled {

View File

@ -0,0 +1,189 @@
<template>
<a-spin :spinning="loading">
<div class="dash-board">
<div class="header">
<h3>CPU使用率趋势</h3>
<a-range-picker
@change="pickerTimeChange"
:allowClear="false"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
v-model="data.time"
>
<template #suffixIcon><a-icon type="calendar" /></template>
<template #renderExtraFooter>
<a-radio-group
default-value="a"
button-style="solid"
style="margin-right: 10px"
v-model:value="data.type"
>
<a-radio-button value="hour">
最近1小时
</a-radio-button>
<a-radio-button value="today">
今日
</a-radio-button>
<a-radio-button value="week">
近一周
</a-radio-button>
</a-radio-group></template
>
</a-range-picker>
</div>
<div ref="chartRef" style="width: 100%; height: 300px"></div>
</div>
</a-spin>
</template>
m
<script lang="ts" setup name="Cpu">
import * as echarts from 'echarts';
import { dashboard } from '@/api/link/dashboard';
import moment from 'moment';
import {
getTimeFormat,
getTimeByType,
arrayReverse,
defulteParamsData,
areaStyleCpu,
typeDataLine,
} from './tool.ts';
const chartRef = ref<Record<string, any>>({});
const loading = ref(false);
const data = ref({
type: 'hour',
time: [null, null],
});
const pickerTimeChange = () => {
data.value.type = undefined;
};
const getCPUEcharts = async (val) => {
loading.value = true;
const res = await dashboard(defulteParamsData('cpu', val));
if (res.success) {
const _cpuOptions = {};
const _cpuXAxis = new Set();
if (res.result?.length) {
res.result.forEach((item) => {
const value = item.data.value;
const nodeID = item.data.clusterNodeId;
_cpuXAxis.add(
moment(value.timestamp).format(
getTimeFormat(data.value.type),
),
);
if (!_cpuOptions[nodeID]) {
_cpuOptions[nodeID] = [];
}
_cpuOptions[nodeID].push(
Number(value.cpuSystemUsage).toFixed(2),
);
});
}
handleCpuOptions(_cpuOptions, [..._cpuXAxis.keys()]);
}
setTimeout(() => {
loading.value = false;
}, 300);
};
const setOptions = (optionsData, key) => ({
data: arrayReverse(optionsData[key]),
name: key,
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: areaStyleCpu,
});
const handleCpuOptions = (optionsData, xAxis) => {
const chart = chartRef.value;
if (chart) {
const myChart = echarts.init(chart);
const dataKeys = Object.keys(optionsData);
const options = {
xAxis: {
type: 'category',
boundaryGap: false,
data: arrayReverse(xAxis),
},
tooltip: {
trigger: 'axis',
valueFormatter: (value) => `${value}%`,
},
yAxis: {
type: 'value',
},
grid: {
left: '50px',
right: '50px',
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
color: ['#2CB6E0'],
series: dataKeys.length
? dataKeys.map((key) => setOptions(optionsData, key))
: typeDataLine,
};
myChart.setOption(options);
window.addEventListener('resize', function () {
myChart.resize();
});
}
};
watch(
() => data.value.type,
(val) => {
const endTime = moment(new Date());
const startTime = getTimeByType(val);
data.value.time = [startTime, endTime];
},
{ immediate: true, deep: true },
);
watch(
() => data.value,
(val) => {
const { time } = val;
if (time && Array.isArray(time) && time.length === 2 && time[0]) {
getCPUEcharts(val);
}
},
{ immediate: true, deep: true },
);
</script>
<style lang="less" scoped>
.dash-board {
display: flex;
flex-direction: column;
height: 100%;
padding: 24px;
background-color: #fff;
box-shadow: 0px 2.73036px 5.46071px rgba(31, 89, 245, 0.2);
border-radius: 2px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
width: 200px;
margin-top: 8px;
}
}
</style>

View File

@ -0,0 +1,193 @@
<template>
<a-spin :spinning="loading">
<div class="dash-board">
<div class="header">
<h3>JVM内存使用率趋势</h3>
<a-range-picker
@change="pickerTimeChange"
:allowClear="false"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
v-model="data.time"
>
<template #suffixIcon><a-icon type="calendar" /></template>
<template #renderExtraFooter>
<a-radio-group
default-value="a"
button-style="solid"
style="margin-right: 10px"
v-model:value="data.type"
>
<a-radio-button value="hour">
最近1小时
</a-radio-button>
<a-radio-button value="today">
今日
</a-radio-button>
<a-radio-button value="week">
近一周
</a-radio-button>
</a-radio-group></template
>
</a-range-picker>
</div>
<div ref="chartRef" style="width: 100%; height: 300px"></div>
</div>
</a-spin>
</template>
<script lang="ts" setup name="Jvm">
import * as echarts from 'echarts';
import { dashboard } from '@/api/link/dashboard';
import moment from 'moment';
import {
getTimeFormat,
getTimeByType,
arrayReverse,
typeDataLine,
areaStyleJvm,
defulteParamsData,
} from './tool.ts';
const chartRef = ref<Record<string, any>>({});
const loading = ref(false);
const data = ref({
type: 'hour',
time: [null, null],
});
const pickerTimeChange = () => {
data.value.type = undefined;
};
const getJVMEcharts = async (val) => {
loading.value = true;
const res = await dashboard(defulteParamsData('jvm', val));
if (res.success) {
const _jvmOptions = {};
const _jvmXAxis = new Set();
if (res.result?.length) {
res.result.forEach((item) => {
const value = item.data.value;
const memoryJvmHeapFree = value.memoryJvmHeapFree;
const memoryJvmHeapTotal = value.memoryJvmHeapTotal;
const nodeID = item.data.clusterNodeId;
const _value = (
((memoryJvmHeapTotal - memoryJvmHeapFree) /
memoryJvmHeapTotal) *
100
).toFixed(2);
if (!_jvmOptions[nodeID]) {
_jvmOptions[nodeID] = [];
}
_jvmXAxis.add(
moment(value.timestamp).format(
getTimeFormat(data.value.type),
),
);
_jvmOptions[nodeID].push(_value);
});
}
handleJVMOptions(_jvmOptions, [..._jvmXAxis.keys()]);
}
setTimeout(() => {
loading.value = false;
}, 300);
};
const setOptions = (optionsData, key) => ({
data: arrayReverse(optionsData[key]),
name: key,
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: areaStyleJvm,
});
const handleJVMOptions = (optionsData, xAxis) => {
const chart = chartRef.value;
if (chart) {
const myChart = echarts.init(chart);
const dataKeys = Object.keys(optionsData);
const options = {
xAxis: {
type: 'category',
boundaryGap: false,
data: arrayReverse(xAxis),
},
tooltip: {
trigger: 'axis',
valueFormatter: (value: any) => `${value}%`,
},
yAxis: {
type: 'value',
},
grid: {
left: '50px',
right: '50px',
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
color: ['#60DFC7'],
series: dataKeys.length
? dataKeys.map((key) => setOptions(optionsData, key))
: typeDataLine,
};
myChart.setOption(options);
window.addEventListener('resize', function () {
myChart.resize();
});
}
};
watch(
() => data.value.type,
(val) => {
const endTime = moment(new Date());
const startTime = getTimeByType(val);
data.value.time = [startTime, endTime];
},
{ immediate: true, deep: true },
);
watch(
() => data.value,
(val) => {
const { time } = val;
if (time && Array.isArray(time) && time.length === 2 && time[0]) {
getJVMEcharts(val);
}
},
{ immediate: true, deep: true },
);
</script>
<style lang="less" scoped>
.dash-board {
display: flex;
flex-direction: column;
height: 100%;
padding: 24px;
background-color: #fff;
box-shadow: 0px 2.73036px 5.46071px rgba(31, 89, 245, 0.2);
border-radius: 2px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
width: 200px;
margin-top: 8px;
}
}
</style>

View File

@ -0,0 +1,219 @@
<template>
<a-spin :spinning="loading">
<div class="dash-board">
<div class="header">
<div class="left">
<h3 style="width: 80px">网络流量</h3>
<a-radio-group
button-style="solid"
v-model:value="data.type"
>
<a-radio-button value="bytesRead">
上行
</a-radio-button>
<a-radio-button value="bytesSent">
下行
</a-radio-button>
</a-radio-group>
</div>
<div class="right">
<a-radio-group
default-value="a"
button-style="solid"
style="margin-right: 10px"
v-model:value="data.time.type"
>
<a-radio-button value="hour">
最近1小时
</a-radio-button>
<a-radio-button value="today"> 今日 </a-radio-button>
<a-radio-button value="week"> 近一周 </a-radio-button>
</a-radio-group>
<a-range-picker
:allowClear="false"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
v-model="data.time.time"
@change="pickerTimeChange"
>
<template #suffixIcon
><a-icon type="calendar"
/></template>
</a-range-picker>
</div>
</div>
<div>
<div
ref="chartRef"
v-if="flag"
style="width: 100%; height: 350px"
></div>
<a-empty v-else style="height: 300px; margin-top: 120px" />
</div>
</div>
</a-spin>
</template>
<script lang="ts" setup name="Network">
import { dashboard } from '@/api/link/dashboard';
import {
getTimeByType,
typeDataLine,
areaStyle,
networkParams,
arrayReverse,
} from './tool.ts';
import moment from 'moment';
import * as echarts from 'echarts';
const chartRef = ref<Record<string, any>>({});
const flag = ref(true);
const loading = ref(false);
const myChart = ref(null);
const data = ref({
type: 'bytesRead',
time: {
type: 'today',
time: [null, null],
},
});
const pickerTimeChange = () => {
data.value.time.type = undefined;
};
const getNetworkEcharts = async (val) => {
loading.value = true;
const resp = await dashboard(networkParams(val));
if (resp.success) {
const _networkOptions = {};
const _networkXAxis = new Set();
if (resp.result.length) {
resp.result.forEach((item) => {
const value = item.data.value;
const _data = [];
const nodeID = item.data.clusterNodeId;
value.forEach((item) => {
_data.push(item.value);
_networkXAxis.add(item.timeString);
});
_networkOptions[nodeID] = {
_data: _networkOptions[nodeID]
? _networkOptions[nodeID]._data.concat(_data)
: _data,
};
});
handleNetworkOptions(_networkOptions, [..._networkXAxis.keys()]);
} else {
handleNetworkOptions([], []);
}
}
setTimeout(() => {
loading.value = false;
}, 300);
};
const networkValueRender = (obj) => {
const { value } = obj;
let _data = '';
if (value >= 1024 && value < 1024 * 1024) {
_data = `${Number((value / 1024).toFixed(2))}KB`;
} else if (value >= 1024 * 1024) {
_data = `${Number((value / 1024 / 1024).toFixed(2))}M`;
} else {
_data = `${value}B`;
}
return `${obj?.axisValueLabel}<br />${obj?.marker}${obj?.seriesName}: ${_data}`;
};
const setOptions = (data, key) => ({
data: data[key]._data, // .map((item) => Number((item / 1024 / 1024).toFixed(2))),
name: key,
type: 'line',
smooth: true,
areaStyle,
});
const handleNetworkOptions = (optionsData, xAxis) => {
const chart = chartRef.value;
if (chart) {
const myChart = echarts.init(chart);
const dataKeys = Object.keys(optionsData);
const options = {
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxis,
},
yAxis: {
type: 'value',
},
grid: {
left: '80px',
right: '50px',
},
tooltip: {
trigger: 'axis',
formatter: (_value) => networkValueRender(_value[0]),
},
color: ['#979AFF'],
series: dataKeys.length
? dataKeys.map((key) => setOptions(optionsData, key))
: typeDataLine,
};
myChart.setOption(options);
window.addEventListener('resize', function () {
myChart.resize();
});
}
};
watch(
() => data.value.time.type,
(value) => {
const endTime = moment(new Date());
const startTime = getTimeByType(value);
data.value.time.time = [startTime, endTime];
},
{ immediate: true, deep: true },
);
watch(
() => data.value,
(value) => {
const {
time: { time },
} = value;
if (time && Array.isArray(time) && time.length === 2 && time[0]) {
getNetworkEcharts(value);
}
},
{ immediate: true, deep: true },
);
</script>
<style lang="less" scoped>
.dash-board {
display: flex;
flex-direction: column;
height: 100%;
padding: 24px;
background-color: #fff;
box-shadow: 0px 2.73036px 5.46071px rgba(31, 89, 245, 0.2);
border-radius: 2px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
.left h3 {
width: 200px;
margin-top: 8px;
}
}
.left,
.right {
display: flex;
align-items: center;
}
</style>

View File

@ -0,0 +1,141 @@
<template>
<div>
<a-select
style="width: 300px; margin-bottom: 20px"
@change="serverIdChange"
:value="serverId"
:options="serverNodeOptions"
v-if="serverNodeOptions.length > 1"
></a-select>
<div class="dash-board">
<div class="dash-board-item">
<TopEchartsItemNode title="CPU使用率" :value="topValues.cpu" />
</div>
<div class="dash-board-item">
<TopEchartsItemNode
title="JVM内存"
:max="topValues.jvmTotal"
:bottom="`总JVM内存 ${topValues.jvmTotal}G`"
formatter="G"
:value="topValues.jvm"
/>
</div>
<div class="dash-board-item">
<TopEchartsItemNode
title="磁盘占用"
:max="topValues.usageTotal"
:bottom="`总磁盘大小 ${topValues.usageTotal}G`"
formatter="G"
:value="topValues.usage"
/>
</div>
<div class="dash-board-item">
<TopEchartsItemNode
title="系统内存"
:max="topValues.systemUsageTotal"
:bottom="`系统内存 ${topValues.systemUsageTotal}G`"
formatter="G"
:value="topValues.systemUsage"
/>
</div>
</div>
</div>
</template>
<script lang="ts" setup name="TopCard">
import { serverNode } from '@/api/link/dashboard';
import TopEchartsItemNode from './TopEchartsItemNode.vue';
import { getWebSocket } from '@/utils/websocket';
import { map } from 'rxjs/operators';
const serverId = ref();
const serverNodeOptions = ref([]);
const topValues = ref({
cpu: 0,
jvm: 0,
jvmTotal: 0,
usage: 0,
usageTotal: 0,
systemUsage: 0,
systemUsageTotal: 0,
});
const serverIdChange = (val: string) => {
serverId.value = val;
};
const getData = () => {
const id = 'operations-statistics-system-info-realTime';
const topic = '/dashboard/systemMonitor/stats/info/realTime';
getWebSocket(id, topic, {
type: 'all',
serverNodeId: serverId.value,
interval: '1s',
agg: 'avg',
})
.pipe(map((res) => res.payload))
.subscribe((payload) => {
const {
value: { cpu, memory, disk },
} = payload;
topValues.value = {
cpu: cpu.systemUsage,
jvm: Number(
(
(memory.jvmHeapUsage / 100) *
(memory.jvmHeapTotal / 1024)
).toFixed(1),
),
jvmTotal: Math.ceil(memory.jvmHeapTotal / 1024),
usage: Number(
((disk.total / 1024) * (disk.usage / 100)).toFixed(1),
),
usageTotal: Math.ceil(disk.total / 1024),
systemUsage: Number(
(
(memory.systemTotal / 1024) *
(memory.systemUsage / 100)
).toFixed(1),
),
systemUsageTotal: Math.ceil(memory.systemTotal / 1024),
};
});
};
onMounted(() => {
serverNode().then((resp) => {
if (resp.success) {
serverNodeOptions.value = resp.result.map((item) => ({
label: item.name,
value: item.id,
}));
if (serverNodeOptions.value.length) {
serverId.value = serverNodeOptions.value[0]?.value;
}
}
});
});
watch(
() => serverId.value,
(val) => {
val && getData();
},
);
</script>
<style lang="less" scoped>
.dash-board {
display: flex;
flex-wrap: wrap;
height: 100%;
background-color: #fff;
box-shadow: 0px 2.73036px 5.46071px rgba(31, 89, 245, 0.2);
border-radius: 2px;
justify-content: space-between;
.dash-board-item {
flex: 1;
margin: 24px 12px;
min-width: 250px;
}
}
</style>

View File

@ -0,0 +1,174 @@
<template>
<div class="echarts-item">
<div class="echarts-item-left">
<div class="echarts-item-title">{{ title }}</div>
<div class="echarts-item-value">
{{ value || 0 }} {{ formatter || '%' }}
</div>
<div v-if="!!bottom" class="echarts-item-bottom">{{ bottom }}</div>
</div>
<div class="echarts-item-right">
<div ref="chartRef" style="width: 100%; height: 100px"></div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts';
import { topOptionsSeries } from './tool';
export default {
name: 'TopEchartsItemNode',
props: {
title: {
type: String,
default: '',
},
value: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 0,
},
bottom: {
type: String,
default: '',
},
formatter: {
type: String,
default: '%',
},
},
data() {
return {
options: {},
};
},
methods: {
createChart(val) {
const chart = this.$refs.chartRef;
if (chart && Object.keys(val).length > 0) {
const myChart = echarts.init(chart);
myChart.setOption(val);
window.addEventListener('resize', function () {
myChart.resize();
});
}
},
getOptions(max, formatter, val) {
let formatterCount = 0;
this.options = {
series: [
{
...topOptionsSeries,
max: max || 100,
axisLabel: {
distance: -22,
color: 'auto',
fontSize: 12,
width: 30,
padding: [6, 10, 0, 10],
formatter: (value) => {
formatterCount += 1;
if ([1, 3, 6, 9, 11].includes(formatterCount)) {
return value + (formatter || '%');
}
return '';
},
},
data: [{ value: val || 0 }],
},
],
};
},
},
watch: {
options: {
handler(val) {
this.createChart(val);
},
immediate: true,
deep: true,
},
max: {
handler(val) {
this.getOptions(val, this.formatter, this.value);
},
immediate: true,
deep: true,
},
value: {
handler(val) {
this.getOptions(this.max, this.formatter, val);
},
immediate: true,
deep: true,
},
formatter: {
handler(val) {
this.getOptions(this.max, val, this.value);
},
immediate: true,
deep: true,
},
},
};
</script>
<style lang="less" scoped>
.echarts-item {
display: flex;
height: 150px;
padding: 16px;
background-color: #fff;
box-shadow: 0px 2.73036px 5.46071px rgba(31, 89, 245, 0.2);
.echarts-item-left {
display: flex;
flex-direction: column;
width: 45%;
}
.echarts-item-right {
width: 55%;
}
.echarts-item-title {
margin-bottom: 8px;
color: rgba(#000, 0.6);
font-size: 16px;
}
.echarts-item-value {
font-weight: bold;
font-size: 36px;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-align: left;
text-overflow: ellipsis;
}
.echarts-item-bottom {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
height: 0;
padding-left: 12px;
&::before {
position: absolute;
top: 50%;
left: 0;
width: 4px;
height: 12px;
background-color: #ff595e;
transform: translateY(-50%);
content: ' ';
}
}
}
</style>

View File

@ -0,0 +1,220 @@
import moment from 'moment';
import * as echarts from 'echarts';
export const getInterval = (type) => {
switch (type) {
case 'year':
return '30d';
case 'month':
case 'week':
return '1d';
case 'hour':
return '1m';
default:
return '1h';
}
};
export const getTimeFormat = (type) => {
switch (type) {
case 'year':
return 'YYYY-MM-DD';
case 'month':
case 'week':
return 'MM-DD';
case 'hour':
return 'HH:mm';
default:
return 'HH';
}
};
export const getTimeByType = (type) => {
switch (type) {
case 'hour':
return moment().subtract(1, 'hours');
case 'week':
return moment().subtract(6, 'days');
case 'month':
return moment().subtract(29, 'days');
case 'year':
return moment().subtract(365, 'days');
default:
return moment().startOf('day');
}
};
export const arrayReverse = (data) => {
const newArray = [];
for (let i = data.length - 1; i >= 0; i--) {
newArray.push(data[i]);
}
return newArray;
};
export const networkParams = (val) => [
{
dashboard: 'systemMonitor',
object: 'network',
measurement: 'traffic',
dimension: 'agg',
group: 'network',
params: {
type: val.type,
interval: getInterval(val.time.type),
from: moment(val.time.time[0]).valueOf(),
to: moment(val.time.time[1]).valueOf(),
},
},
];
export const defulteParamsData = (group, val) => [
{
dashboard: 'systemMonitor',
object: 'stats',
measurement: 'info',
dimension: 'history',
group,
params: {
from: moment(val.time[0]).valueOf(),
to: moment(val.time[1]).valueOf(),
},
},
];
export const areaStyle = {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 1,
color: 'rgba(151, 154, 255, 0)',
},
{
offset: 0,
color: 'rgba(151, 154, 255, .24)',
},
]),
};
export const areaStyleCpu = {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 1,
color: 'rgba(44, 182, 224, 0)',
},
{
offset: 0,
color: 'rgba(44, 182, 224, .24)',
},
]),
};
export const areaStyleJvm = {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 1,
color: 'rgba(96, 223, 199, 0)',
},
{
offset: 0,
color: 'rgba(96, 223, 199, .24)',
},
]),
};
export const typeDataLine = [
{
data: [],
type: 'line',
},
];
export const topOptionsSeries = {
type: 'gauge',
min: 0,
startAngle: 200,
endAngle: -20,
center: ['50%', '67%'],
title: {
show: false,
},
axisTick: {
distance: -20,
lineStyle: {
width: 1,
color: 'rgba(0,0,0,0.15)',
},
},
splitLine: {
distance: -22,
length: 9,
lineStyle: {
width: 1,
color: '#000',
},
},
pointer: {
length: '80%',
width: 4,
itemStyle: {
color: 'auto',
},
},
anchor: {
show: true,
showAbove: true,
size: 20,
itemStyle: {
borderWidth: 3,
borderColor: '#fff',
shadowBlur: 20,
shadowColor: 'rgba(0, 0, 0, .25)',
color: 'auto',
},
},
axisLine: {
lineStyle: {
width: 10,
color: [
[0.25, 'rgba(36, 178, 118, 1)'],
[
0.4,
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(66, 147, 255, 1)',
},
{
offset: 1,
color: 'rgba(36, 178, 118, 1)',
},
]),
],
[
0.5,
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(250, 178, 71, 1)',
},
{
offset: 1,
color: 'rgba(66, 147, 255, 1)',
},
]),
],
[
1,
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(250, 178, 71, 1)',
},
{
offset: 1,
color: 'rgba(247, 111, 93, 1)',
},
]),
],
],
},
},
detail: {
show: false,
},
};

View File

@ -0,0 +1,21 @@
<template>
<page-container>
<div>
<a-row :gutter="[24, 24]">
<a-col :span="24"><TopCard /> </a-col>
<a-col :span="24"><Network /></a-col>
<a-col :span="12"><Cpu /></a-col>
<a-col :span="12"><Jvm /></a-col>
</a-row>
</div>
</page-container>
</template>
<script lang="ts" setup name="DashBoardPage">
import TopCard from './components/TopCard.vue';
import Network from './components/Network.vue';
import Cpu from './components/Cpu.vue';
import Jvm from './components/Jvm.vue';
</script>
<style lang="less" scoped></style>

View File

@ -159,7 +159,7 @@ import _ from 'lodash';
const tableRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const route = useRoute();
const visible = ref(false);
const current = ref({});
@ -276,6 +276,14 @@ const saveChange = (value: object) => {
}
};
watch(
() => route.query?.save,
(value) => {
value === 'true' && handlAdd();
},
{ deep: true, immediate: true },
);
/**
* 搜索
* @param params

View File

@ -1032,11 +1032,10 @@ import { Store } from 'jetlinks-store';
import MonacoEditor from '@/components/MonacoEditor/index.vue';
const route = useRoute();
const view = route.query.view as string;
const NetworkType = route.query.type as string;
const view = NetworkType ? 'false' : (route.query.view as string);
const id = route.params.id as string;
const activeKey = ref(['1']);
const loading = ref(false);
const formRef1 = ref<FormInstance>();
const formRef2 = ref<FormInstance>();
@ -1250,7 +1249,6 @@ watch(
}
},
{ deep: true },
// { deep: true, immediate: true },
);
watch(
@ -1263,7 +1261,6 @@ watch(
updateClustersListIndex();
},
{ deep: true },
// { deep: true, immediate: true },
);
watch(
() => dynamicValidateForm.cluster?.length,
@ -1272,6 +1269,17 @@ watch(
},
{ deep: true, immediate: true },
);
watch(
() => NetworkType,
(value) => {
if (value) {
const { cluster } = dynamicValidateForm;
formData.value.type = value;
cluster[0].configuration.host = '0.0.0.0';
}
},
{ deep: true, immediate: true },
);
</script>
<style lang="less" scoped>

View File

@ -0,0 +1,35 @@
.doc {
height: 1050px;
padding: 24px;
overflow-y: auto;
color: rgba(#000, 0.8);
font-size: 14px;
background-color: #fafafa;
.url {
padding: 8px 16px;
color: #2f54eb;
background-color: rgba(#a7bdf7, 0.2);
}
h1 {
margin: 16px 0;
color: rgba(#000, 0.85);
font-weight: bold;
font-size: 14px;
&:first-child {
margin-top: 0;
}
}
h2 {
margin: 6px 0;
color: rgba(0, 0, 0, 0.8);
font-size: 14px;
}
.image {
margin: 16px 0;
}
}

View File

@ -0,0 +1,722 @@
<!-- 国标级联新增/编辑 -->
<template>
<page-container>
<a-card>
<a-row :gutter="24">
<a-col :span="12">
<a-form ref="formRef" layout="vertical" :model="formData">
<a-row :gutter="24">
<TitleComponent data="基本信息" />
<a-col :span="12">
<a-form-item
label="名称"
name="cascadeName"
:rules="[
{
required: true,
message: '请输入名称',
},
{
max: 84,
message: '最多可输入84个字符',
},
]"
>
<a-input
v-model:value="formData.cascadeName"
placeholder="请输入名称"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="代理视频流"
name="proxyStream"
:rules="[
{
required: true,
message: '请选择代理视频流',
},
]"
>
<a-radio-group
button-style="solid"
v-model:value="formData.proxyStream"
>
<a-radio-button :value="true">
启用
</a-radio-button>
<a-radio-button :value="false">
禁用
</a-radio-button>
</a-radio-group>
</a-form-item>
</a-col>
<TitleComponent data="信令服务配置" />
<a-col :span="12">
<a-form-item
name="clusterNodeId"
:rules="[
{
required: true,
message: '请选择集群节点',
},
]"
>
<template #label>
<span>
集群节点
<a-tooltip
title="使用此集群节点级联到上级平台"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</span>
</template>
<a-select
v-model:value="formData.clusterNodeId"
placeholder="请选择集群节点"
:options="clustersList"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="信令名称"
name="name"
:rules="[
{
required: true,
message: '请输入信令名称',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input
v-model:value="formData.name"
placeholder="请输入信令名称"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
label="上级SIP ID"
name="sipId"
:rules="[
{
required: true,
message: '请输入上级SIP ID',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input
v-model:value="formData.sipId"
placeholder="请输入上级SIP ID"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="上级SIP域"
name="domain"
:rules="[
{
required: true,
message: '请输入上级平台SIP域',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input
v-model:value="formData.domain"
placeholder="请输入上级平台SIP域"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="上级SIP 地址"
name="remoteAddress"
:rules="[
{
required: true,
message: '请输入上级SIP 地址',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-row :gutter="10">
<a-col :span="14">
<a-input
v-model:value="
formData.remoteAddress
"
placeholder="请输入IP地址"
/>
</a-col>
<a-col :span="10">
<a-input-number
:min="1"
:max="65535"
v-model:value="
formData.remotePort
"
placeholder="请输入端口"
style="width: 100%"
/>
</a-col>
</a-row>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
label="本地SIP ID"
name="localSipId"
:rules="[
{
required: true,
message: '请输入网关侧的SIP ID',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input
v-model:value="formData.localSipId"
placeholder="网关侧的SIP ID"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
name="host"
:rules="[
{
required: true,
message: '请输入SIP本地地址',
},
]"
>
<template #label>
<span>
SIP本地地址
<a-tooltip
title="使用指定的网卡和端口进行请求"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</span>
</template>
<a-row :gutter="10">
<a-col :span="14">
<a-select
v-model:value="formData.host"
placeholder="请选择IP地址"
:options="allList"
/>
</a-col>
<a-col :span="10">
<a-select
v-model:value="formData.port"
placeholder="请选择端口"
>
<a-select-option value="1">
1
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="SIP远程地址"
name="publicHost"
:rules="[
{
required: true,
message: '请输入SIP远程地址',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-row :gutter="10">
<a-col :span="14">
<a-input
v-model:value="
formData.publicHost
"
placeholder="请输入IP地址"
/>
</a-col>
<a-col :span="10">
<a-input-number
:min="1"
:max="65535"
v-model:value="
formData.publicPort
"
placeholder="请输入端口"
style="width: 100%"
/>
</a-col>
</a-row>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item
label="传输协议"
name="transport"
:rules="[
{
required: true,
message: '请选择传输协议',
},
]"
>
<a-radio-group
button-style="solid"
v-model:value="formData.transport"
>
<a-radio-button value="UDP">
UDP
</a-radio-button>
<a-radio-button value="TCP">
TCP
</a-radio-button>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="用户"
name="user"
:rules="[
{
required: true,
message: '请输入用户',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input
v-model:value="formData.user"
placeholder="请输入用户"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="接入密码"
name="password"
:rules="[
{
required: true,
message: '请输入接入密码',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input-password
v-model:value="formData.password"
placeholder="请输入接入密码"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="厂商"
name="manufacturer"
:rules="[
{
required: true,
message: '请输入厂商',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input
v-model:value="formData.manufacturer"
placeholder="请输入厂商"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="型号"
name="model"
:rules="[
{
required: true,
message: '请输入型号',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input
v-model:value="formData.model"
placeholder="请输入型号"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="版本号"
name="firmware"
:rules="[
{
required: true,
message: '请输入版本号',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<a-input
v-model:value="formData.firmware"
placeholder="请输入版本号"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="心跳周期(秒)"
name="keepaliveInterval"
:rules="[
{
required: true,
message: '请输入心跳周期',
},
]"
>
<a-input-number
:min="1"
:max="10000"
v-model:value="
formData.keepaliveInterval
"
placeholder="请输入心跳周期"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="注册间隔(秒)"
name="registerInterval"
:rules="[
{
required: true,
message: '请输入注册间隔',
},
]"
>
<a-input-number
:min="1"
:max="10000"
v-model:value="
formData.registerInterval
"
placeholder="请输入注册间隔"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item>
<a-button
type="primary"
@click="handleSubmit"
:loading="btnLoading"
>
保存
</a-button>
</a-form-item>
</a-form>
</a-col>
<a-col :span="12">
<div class="doc">
<h1>1.概述</h1>
<div>
配置国标级联平台可以将已经接入到自身的摄像头共享给第三方调用播放
</div>
<div>
<a-alert
message="注该配置只用于将本平台向上级联至第三方平台如需第三方平台向上级联至本平台请在“视频设备”页面新增设备时选择“GB/T28181”接入方式。"
type="info"
show-icon
/>
</div>
<h1>2.配置说明</h1>
<div>
以下配置说明以将本平台数据级联到LiveGBS平台为例
</div>
<h2>1上级SIP ID</h2>
<div>请填写第三方平台中配置的<b>SIP ID</b></div>
<div class="image">
<a-image
width="100%"
:src="getImage('/northbound/doc2.png')"
/>
</div>
<h2>2上级SIP </h2>
<div>请填写第三方平台中配置的<b>SIP ID域</b></div>
<div class="image">
<a-image
width="100%"
:src="getImage('/northbound/doc1.png')"
/>
</div>
<h2>3上级SIP 地址</h2>
<div>请填写第三方平台中配置的<b>SIP ID地址</b></div>
<div class="image">
<a-image
width="100%"
:src="getImage('/northbound/doc3.png')"
/>
</div>
<h2>4本地SIP ID</h2>
<div>
请填写本地的<b>SIP ID地址</b>
地址由中心编码(8)行业编码(2)类型编码(3)和序号(7)四个码段共20位十
进制数字字符构成详细规则请参见GB/T28181-2016中附录D部分
</div>
<h2>5SIP本地地址</h2>
<div>
请选择<b>指定的网卡和端口</b>如有疑问请联系系统运维人员
</div>
<h2>6用户</h2>
<div>
部分平台有基于用户和接入密码的特殊认证通常情况下,请填写<b
>本地SIP ID</b
>
</div>
<h2>7接入密码</h2>
<div>
需与上级平台设置的接入密码一致用于身份认证
</div>
<h2>8厂商/型号/版本号</h2>
<div>
本平台将以设备的身份级联到上级平台请设置本平台在上级平台中显示的厂商型号版本号
</div>
<h2>9心跳周期</h2>
<div>
需与上级平台设置的心跳周期保持一致通常默认60秒
</div>
<h2>10注册间隔</h2>
<div>
若SIP代理通过注册方式校时,其注册间隔时间宜设置为小于
SIP代理与 SIP服务器出现1s误 差所经过的运行时间
</div>
</div>
</a-col>
</a-row>
</a-card>
</page-container>
</template>
<script setup lang="ts">
import { getImage } from '@/utils/comm';
import { Form } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import CascadeApi from '@/api/media/cascade';
const router = useRouter();
const route = useRoute();
const useForm = Form.useForm;
//
const formData = ref({
id: route.query.id || undefined,
// name: '',
cascadeName: '',
proxyStream: false,
// , sipConfigs[{}]
clusterNodeId: '',
name: '',
sipId: '',
domain: '',
remoteAddress: '',
remotePort: undefined,
localSipId: '',
host: '',
port: undefined,
// remotePublic: {
// host: '',
// port: undefined,
// },
publicHost: '',
publicPort: undefined,
transport: 'UDP',
user: '',
password: '',
manufacturer: '',
model: '',
firmware: '',
keepaliveInterval: '60',
registerInterval: '3600',
});
/**
* 获取集群节点
*/
const clustersList = ref([]);
const getClustersList = async () => {
const { result } = await CascadeApi.clusters();
clustersList.value = result.map((m: any) => ({
label: m.name,
value: m.id,
}));
};
getClustersList();
/**
* SIP本地地址
*/
const allList = ref([]);
const getAllList = async () => {
const { result } = await CascadeApi.all();
allList.value = result.map((m: any) => ({
label: m.host,
value: m.host,
}));
};
getAllList();
/**
* 获取详情
*/
const getDetail = async () => {
if (!route.query.id) return;
const res = await CascadeApi.detail(route.query.id as string);
// console.log('res: ', res);
// formData.value = res.result;
// Object.assign(formData.value, res.result);
const { id, name, proxyStream, sipConfigs } = res.result;
formData.value = {
id,
cascadeName: name,
proxyStream,
clusterNodeId: sipConfigs[0]?.clusterNodeId,
name: sipConfigs[0]?.name,
sipId: sipConfigs[0]?.sipId,
domain: sipConfigs[0]?.domain,
remoteAddress: sipConfigs[0]?.remoteAddress,
remotePort: sipConfigs[0]?.remotePort,
localSipId: sipConfigs[0]?.localSipId,
host: sipConfigs[0]?.host,
port: sipConfigs[0]?.port,
publicHost: sipConfigs[0]?.publicHost,
publicPort: sipConfigs[0]?.publicPort,
transport: sipConfigs[0]?.transport,
user: sipConfigs[0]?.user,
password: sipConfigs[0]?.password,
manufacturer: sipConfigs[0]?.manufacturer,
model: sipConfigs[0]?.model,
firmware: sipConfigs[0]?.firmware,
keepaliveInterval: sipConfigs[0]?.keepaliveInterval,
registerInterval: sipConfigs[0]?.registerInterval,
};
console.log('formData.value: ', formData.value);
};
onMounted(() => {
getDetail();
});
/**
* 表单提交
*/
const formRef = ref();
const btnLoading = ref<boolean>(false);
const handleSubmit = () => {
// console.log('formData.value: ', formData.value);
formRef.value
.validate()
.then(async () => {
const {
id,
cascadeName,
proxyStream,
publicHost,
publicPort,
...extraFormData
} = formData.value;
const params = {
id,
name: cascadeName,
proxyStream,
sipConfigs: [
{
...extraFormData,
remotePublic: {
host: publicHost,
port: publicPort,
},
},
],
};
btnLoading.value = true;
const res = formData.value.id
? await CascadeApi.update(params)
: await CascadeApi.save(params);
btnLoading.value = false;
if (res.success) {
message.success('操作成功');
router.back();
} else {
message.error('操作失败');
}
})
.catch((err: any) => {
console.log('err: ', err);
});
};
</script>
<style lang="less" scoped>
@import './index.less';
</style>

View File

@ -0,0 +1,430 @@
<template>
<page-container>
<Search
:columns="columns"
target="media-cascade"
@search="handleSearch"
/>
<JTable
ref="listRef"
:columns="columns"
:request="(e:any) => lastValueFrom(e)"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:params="params"
:gridColumn="2"
>
<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.status?.value"
:statusText="slotProps.status?.text"
:statusNames="{
enabled: 'success',
disabled: 'error',
}"
>
<template #img>
<slot name="img">
<img
:src="
getImage('/device/instance/device-card.png')
"
/>
</slot>
</template>
<template #content>
<h3 class="card-item-content-title">
{{ slotProps.name }}
</h3>
<p>通道数量{{ slotProps.count }}</p>
<Ellipsis>
<a-badge
:text="`sip:${slotProps.sipConfigs[0]?.sipId}@${slotProps.sipConfigs[0]?.hostAndPort}`"
:status="
slotProps.status?.value === 'enabled'
? 'success'
: 'error'
"
/>
</Ellipsis>
</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"
v-if="item.key === 'delete'"
>
<AIcon type="DeleteOutlined" />
</a-button>
<a-button
:disabled="item.disabled"
@click="item.onClick"
v-else
>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</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>
<!-- <PermissionButton
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
:hasPermission="`media/Cascade:${item.key}`"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</PermissionButton> -->
</template>
</CardBox>
</template>
<template #sipId="slotProps">
{{ slotProps.sipConfigs[0]?.sipId }}
</template>
<template #publicHost="slotProps">
{{ slotProps.sipConfigs[0]?.publicHost }}
</template>
<template #status="slotProps">
<a-badge
:text="slotProps.status?.text"
:status="
slotProps.status?.value === 'enabled'
? 'success'
: 'error'
"
/>
</template>
<template #onlineStatus="slotProps">
<a-badge
:text="slotProps.onlineStatus?.text"
:status="
slotProps.onlineStatus?.value === 'online'
? 'success'
: 'error'
"
/>
</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>
<!-- <template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
>
<PermissionButton
:disabled="i.disabled"
:popConfirm="i.popConfirm"
:tooltip="{
...i.tooltip,
}"
@click="i.onClick"
type="link"
style="padding: 0px"
:hasPermission="`device/Instance:${i.key}`"
>
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</template> -->
</a-space>
</template>
</JTable>
</page-container>
</template>
<script setup lang="ts">
import DeviceApi from '@/api/media/device';
import CascadeApi from '@/api/media/cascade';
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: 'sipId',
key: 'sipId',
scopedSlots: true,
},
{
title: '上级SIP 地址',
dataIndex: 'publicHost',
key: 'publicHost',
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) => {
params.value = e;
};
/**
* 处理表格数据
* @param params
*/
const lastValueFrom = async (params: any) => {
const res = await CascadeApi.list(params);
res.result.data.forEach(async (item: any) => {
const resp = await queryBindChannel(item.id);
item.count = resp.result.total;
});
return res;
};
/**
* 查询通道数量
* @param id
*/
const queryBindChannel = async (id: string) => {
return await CascadeApi.queryCount(id);
};
/**
* 新增
*/
const handleAdd = () => {
menuStory.jumpPage('media/Cascade/Save');
};
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/Cascade/Save',
{},
{
id: data.id,
},
);
},
},
{
key: 'view',
text: '选择通道',
tooltip: {
title: '选择通道',
},
icon: 'LinkOutlined',
onClick: () => {
menuStory.jumpPage(
'media/Cascade/Channel',
{},
{
id: data.id,
},
);
},
},
{
key: 'debug',
text: '推送',
tooltip: {
title:
data.status?.value === 'disabled'
? '禁用状态下不可推送'
: '推送',
},
disabled: data.status?.value === 'disabled',
icon: 'ShareAltOutlined',
onClick: () => {
// updateChannel()
},
},
{
key: 'action',
text: data.status?.value === 'enabled' ? '禁用' : '启用',
tooltip: {
title: data.status?.value === 'enabled' ? '禁用' : '启用',
},
icon:
data.status?.value === 'enabled'
? 'StopOutlined'
: 'PlayCircleOutlined',
popConfirm: {
title: `确认${
data.status?.value === 'enabled' ? '禁用' : '启用'
}?`,
onConfirm: async () => {
let res =
data.status.value === 'enabled'
? await CascadeApi.disabled(data.id)
: await CascadeApi.enabled(data.id);
if (res.success) {
message.success('操作成功!');
listRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
{
key: 'delete',
text: '删除',
tooltip: {
title:
data.status?.value === 'enabled'
? '请先禁用, 再删除'
: '删除',
},
disabled: data.status?.value === 'enabled',
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const resp = await CascadeApi.del(data.id);
if (resp.status === 200) {
message.success('操作成功!');
listRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
return actions;
};
</script>

41
src/views/media/Cascade/typings.d.ts vendored Normal file
View File

@ -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;
};
export type CascadeItem = {
mediaServerId: string;
onlineStatus: State;
proxyStream: boolean;
sipConfigs: Partial<SipConfig>[];
status: State;
count?: number;
} & BaseItem;

View File

@ -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>

View File

@ -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>

View File

@ -1,4 +1,4 @@
<!-- 通知模板详情 -->
<!-- 视频设备新增/编辑 -->
<template>
<page-container>
<a-card>

View File

@ -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>

View File

@ -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>

View File

@ -28,7 +28,7 @@ type Emit = {
const options = [
{ value: 'device', label: '设备触发', tip: '适用于设备数据或行为满足触发条件时,执行指定的动作', image: getImage('/device-trigger.png') },
{ value: 'manual', label: '手动触发', tip: '适用于第三方平台向物联网平台下发指令控制设备', image: getImage('/manual-trigger.png') },
{ value: 'timing', label: '定时触发', tip: '适用于定期执行固定任务', image: getImage('/timing-trigger.png') },
{ value: 'timer', label: '定时触发', tip: '适用于定期执行固定任务', image: getImage('/timing-trigger.png') },
]
const props = defineProps({

View File

@ -66,6 +66,10 @@ const props = defineProps({
}
})
watchEffect(() => {
Object.assign(formModel, props.data)
})
const emit = defineEmits<Emit>()
const title = computed(() => {

View File

@ -0,0 +1,370 @@
<template>
<div class="card">
<div
class="card-warp"
:class="{ active: active ? 'active' : '' }"
@click="handleClick"
>
<div class="card-type">
<div class="card-type-text"><slot name="type"></slot></div>
</div>
<div class="card-content">
<div style="display: flex">
<!-- 图片 -->
<div class="card-item-avatar">
<slot name="img"> </slot>
</div>
<!-- 内容 -->
<div class="card-item-body">
<slot name="title"></slot>
<span class="subTitle">
<slot name="subTitle"></slot>
</span>
</div>
</div>
<!-- 勾选 -->
<div v-if="active" class="checked-icon">
<div>
<AIcon type="CheckOutlined" />
</div>
</div>
<!-- 状态 -->
<div
v-if="showStatus"
class="card-state"
:class="statusNames ? statusNames[status] : ''"
>
<div class="card-state-content">
<BadgeStatus
:status="status"
:text="statusText"
:statusNames="statusNames"
></BadgeStatus>
</div>
</div>
</div>
</div>
<!-- 按钮 -->
<slot name="bottom-tool">
<div
v-if="showTool && actions && actions.length"
class="card-tools"
>
<div
v-for="item in actions"
:key="item.key"
class="card-button"
:class="{
delete: item.key === 'delete',
}"
>
<slot name="actions" v-bind="item"></slot>
</div>
</div>
</slot>
</div>
</template>
<script setup lang="ts">
import BadgeStatus from '@/components/BadgeStatus/index.vue';
import { StatusColorEnum } from '@/utils/consts.ts';
import type { ActionsType } from '@/components/Table/index.vue';
import { PropType } from 'vue';
type EmitProps = {
(e: 'click', data: Record<string, any>): void;
};
type TableActionsType = Partial<ActionsType>;
const emit = defineEmits<EmitProps>();
const props = defineProps({
value: {
type: Object as PropType<Record<string, any>>,
default: () => {},
},
showStatus: {
type: Boolean,
default: true,
},
showTool: {
type: Boolean,
default: true,
},
statusText: {
type: String,
default: '正常',
},
status: {
type: [String, Number],
default: 'default',
},
statusNames: {
type: Object,
},
actions: {
type: Array as PropType<TableActionsType[]>,
default: () => [],
},
active: {
type: Boolean,
default: false,
},
});
const handleClick = () => {
emit('click', props.value);
};
</script>
<style lang="less" scoped>
.card {
width: 100%;
background-color: #fff;
.checked-icon {
position: absolute;
right: -22px;
bottom: -22px;
z-index: 2;
width: 44px;
height: 44px;
color: #fff;
background-color: red;
background-color: #2f54eb;
transform: rotate(-45deg);
> div {
position: relative;
height: 100%;
transform: rotate(45deg);
> span {
position: absolute;
top: 6px;
left: 6px;
font-size: 12px;
}
}
}
.card-warp {
position: relative;
border: 1px solid #e6e6e6;
overflow: hidden;
&:hover {
cursor: pointer;
box-shadow: 0 0 24px rgba(#000, 0.1);
.card-mask {
visibility: visible;
}
}
&.active {
position: relative;
border: 1px solid #2f54eb;
}
.card-type {
position: absolute;
top: 0;
left: -14px;
height: 32px;
padding: 0 30px;
color: rgba(0, 0, 0, 0.65);
line-height: 32px;
background-color: rgba(0, 0, 0, 0.06);
transform: skewX(-45deg);
.card-type-text {
display: flex;
align-items: center;
justify-content: center;
transform: skewX(45deg);
}
}
.card-content {
position: relative;
padding: 43px 12px 19px 30px;
overflow: hidden;
.card-item-avatar {
margin-right: 16px;
}
.card-item-body {
display: flex;
flex-direction: column;
flex-grow: 1;
width: 0;
.subTitle {
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
margin-top: 10px;
}
}
.card-state {
position: absolute;
top: 40px;
right: -12px;
display: flex;
justify-content: center;
width: 100px;
padding: 2px 0;
background-color: rgba(#5995f5, 0.15);
transform: skewX(45deg);
&.success {
background-color: @success-color-deprecated-bg;
}
&.warning {
background-color: rgba(#ff9000, 0.1);
}
&.error {
background-color: rgba(#e50012, 0.1);
}
.card-state-content {
transform: skewX(-45deg);
}
}
:deep(.card-item-content-title) {
cursor: pointer;
font-size: 16px;
font-weight: 700;
color: @primary-color;
width: calc(100% - 100px);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
:deep(.card-item-heard-name) {
font-weight: 700;
font-size: 16px;
margin-bottom: 12px;
}
:deep(.card-item-content-text) {
color: rgba(0, 0, 0, 0.75);
font-size: 12px;
}
}
}
&.item-active {
position: relative;
color: #2f54eb;
.checked-icon {
display: block;
}
.card-warp {
border: 1px solid #2f54eb;
}
}
.card-tools {
display: flex;
margin-top: 8px;
.card-button {
display: flex;
flex-grow: 1;
& > :deep(span, button) {
width: 100%;
border-radius: 0;
}
:deep(button) {
width: 100%;
border-radius: 0;
background: #f6f6f6;
border: 1px solid #e6e6e6;
color: #2f54eb;
&:hover {
background-color: @primary-color-hover;
border-color: @primary-color-hover;
span {
color: #fff !important;
}
}
&:active {
background-color: @primary-color-active;
border-color: @primary-color-active;
span {
color: #fff !important;
}
}
}
&:not(:last-child) {
margin-right: 8px;
}
&.delete {
flex-basis: 60px;
flex-grow: 0;
:deep(button) {
background: @error-color-deprecated-bg;
border: 1px solid @error-color-outline;
span {
color: @error-color !important;
}
&:hover {
background-color: @error-color-hover;
span {
color: #fff !important;
}
}
&:active {
background-color: @error-color-active;
span {
color: #fff !important;
}
}
}
}
:deep(button[disabled]) {
background: @disabled-bg;
border-color: @disabled-color;
span {
color: @disabled-color !important;
}
&:hover {
background-color: @disabled-active-bg;
}
&:active {
background-color: @disabled-active-bg;
}
}
}
}
}
</style>

View File

@ -1,68 +1,340 @@
<template>
<page-container>
<search
:columns='columns'
/>
<j-table
:columns='columns'
>
<template #headerTitle>
<a-space>
<a-button type="primary" @click="visible = true">新增</a-button>
</a-space>
</template>
</j-table>
<SaveModal v-if='visible' @close='visible = false'/>
</page-container>
<page-container>
<Search :columns="columns" target="scene" @search="handleSearch" />
<JTable
ref="sceneRef"
:columns="columns"
:request="query"
:defaultParams="{ sorts: [{ name: 'createTime', order: 'desc' }] }"
:params="params"
>
<template #headerTitle>
<a-space>
<PermissionButton
type="primary"
@click="handleAdd"
hasPermission="device/Instance:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
</a-space>
</template>
<template #card="slotProps">
<SceneCard
:value="slotProps"
@click="handleClick"
:actions="getActions(slotProps, 'card')"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
started: 'success',
disable: 'error',
}"
>
<template #type>
<span
><img
:height="16"
:src="typeMap.get(slotProps.triggerType)?.icon"
style="margin-right: 5px"
/>{{
typeMap.get(slotProps.triggerType)?.text
}}</span
>
</template>
<template #img>
<img :src="typeMap.get(slotProps.triggerType)?.img" />
</template>
<template #title>
<Ellipsis style="width: calc(100% - 100px)">
<span
style="font-size: 16px; font-weight: 600"
@click.stop="handleView(slotProps.id)"
>
{{ slotProps.name }}
</span>
</Ellipsis>
</template>
<template #subTitle>
<Ellipsis :lineClamp="2">
说明{{
slotProps?.description ||
typeMap.get(slotProps.triggerType)?.tip
}}
</Ellipsis>
</template>
<template #actions="item">
<PermissionButton
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
:hasPermission="'rule-engine/Scene:' + item.key"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</PermissionButton>
</template>
</SceneCard>
</template>
<template #triggerType="slotProps">
{{ typeMap.get(slotProps.triggerType)?.text }}
</template>
<template #state="slotProps">
<a-badge
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
/>
</template>
<template #action="slotProps">
<a-space>
<template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
>
<PermissionButton
:disabled="i.disabled"
:popConfirm="i.popConfirm"
:tooltip="{
...i.tooltip,
}"
@click="i.onClick"
type="link"
style="padding: 0px"
:hasPermission="'rule-engine/Scene:' + i.key"
>
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</template>
</a-space>
</template>
</JTable>
<SaveModal v-if="visible" @close="visible = false" :data="current" />
</page-container>
</template>
<script setup lang='ts'>
import SaveModal from './Save/save.vue'
import type { SceneItem } from './typings'
import { useMenuStore } from 'store/menu'
import SaveModal from './Save/save.vue';
import type { SceneItem } from './typings';
import { useMenuStore } from 'store/menu';
import { query, _delete, _action } from '@/api/rule-engine/scene';
import { message } from 'ant-design-vue';
import type { ActionsType } from '@/components/Table';
import { getImage } from '@/utils/comm';
import SceneCard from './SceneCard.vue';
const menuStory = useMenuStore()
const visible = ref<boolean>(false)
const menuStory = useMenuStore();
const visible = ref<boolean>(false);
const current = ref<Record<string, any>>({});
const statusMap = new Map();
statusMap.set('started', 'success');
statusMap.set('disable', 'error');
const params = ref<Record<string, any>>({});
const sceneRef = ref<Record<string, any>>({});
const typeMap = new Map();
typeMap.set('manual', {
text: '手动触发',
img: getImage('/scene/scene-hand.png'),
icon: getImage('/scene/trigger-type-icon/manual.png'),
tip: '适用于第三方平台向物联网平台下发指令控制设备',
});
typeMap.set('timer', {
text: '定时触发',
img: getImage('/scene/scene-timer.png'),
icon: getImage('/scene/trigger-type-icon/timing.png'),
tip: '适用于定期执行固定任务',
});
typeMap.set('device', {
text: '设备触发',
img: getImage('/scene/scene-device.png'),
icon: getImage('/scene/trigger-type-icon/device.png'),
tip: '适用于设备数据或行为满足触发条件时,执行指定的动作',
});
const columns = [
{
dataIndex: 'name',
fixed: 'left',
ellipsis: true,
width: 300,
title: '名称',
search: {
type: 'string'
{
dataIndex: 'name',
fixed: 'left',
ellipsis: true,
width: 300,
title: '名称',
search: {
type: 'string',
},
},
{
dataIndex: 'triggerType',
title: '触发方式',
scopedSlots: true,
search: {
type: 'select',
options: Array.from(typeMap).map((item) => ({
label: item[1],
value: item[0],
})),
},
},
{
dataIndex: 'state',
title: '状态',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '正常', value: 'started' },
{ label: '禁用', value: 'disable' },
],
},
},
{
dataIndex: 'description',
title: '说明',
search: {
type: 'string',
},
scopedSlots: true,
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true,
},
];
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
const actions: ActionsType[] = [
{
key: 'update',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
visible.value = true;
current.value = data;
},
},
{
key: 'action',
text: data.state?.value !== 'disable' ? '禁用' : '启用',
tooltip: {
title: !(!!data.triggerType && (data.branches || [])?.length)
? '未配置规则的不能启用'
: data.state?.value !== 'disable'
? '禁用'
: '启用',
},
disabled: !(!!data?.triggerType && (data?.branches || [])?.length),
icon:
data.state.value !== 'disable'
? 'StopOutlined'
: 'CheckCircleOutlined',
popConfirm: {
title: `确认${
data.state.value !== 'disable' ? '禁用' : '启用'
}?`,
onConfirm: async () => {
let response = undefined;
if (data.state.value !== 'disable') {
response = await _action(data.id, '_disable');
} else {
response = await _action(data.id, '_enable');
}
if (response && response.status === 200) {
message.success('操作成功!');
sceneRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
{
key: 'delete',
text: '删除',
disabled: data.state?.value !== 'disable',
tooltip: {
title:
data.state.value !== 'disable'
? '请先禁用该场景,再删除'
: '删除',
},
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const resp = await _delete(data.id);
if (resp.status === 200) {
message.success('操作成功!');
sceneRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
if (data.triggerType === 'manual') {
const _item: ActionsType = {
key: 'trigger',
text: '手动触发',
disabled: data.state?.value === 'disable',
tooltip: {
title:
data.state.value !== 'disable'
? '手动触发'
: '未启用,不能手动触发',
},
icon: 'LikeOutlined',
onClick: () => {
// handleView(data.id, data.triggerType);
},
};
actions.splice(1, 0, _item);
}
},
{
dataIndex: 'triggerType',
title: '触发方式',
search: {
type: 'select',
options: [
{ label: '手动触发', value: 'manual'},
{ label: '定时触发', value: 'timer'},
{ label: '设备触发', value: 'device'}
]
if (type === 'table') {
actions.splice(0, 0, {
key: 'view',
text: '查看',
tooltip: {
title: '查看',
},
icon: 'EyeOutlined',
onClick: () => {
handleView(data.id, data.triggerType);
},
});
}
},
{
dataIndex: 'description',
title: '说明',
},
{
dataIndex: 'state',
title: '状态',
search: {
type: 'select',
options: [
{ label: '正常', value: 'started'},
{ label: '禁用', value: 'disable'},
]
}
}
]
return actions;
};
const handleSearch = (_params: any) => {
params.value = _params;
};
const handleAdd = () => {
visible.value = true;
current.value = {};
};
/**
* 编辑
@ -70,8 +342,12 @@ const columns = [
* @param triggerType 触发类型
*/
const handleEdit = (id: string, triggerType: string) => {
menuStory.jumpPage('Scene/Save', { }, { triggerType: triggerType, id, type: 'edit' })
}
menuStory.jumpPage(
'rule-engine/Scene/Save',
{},
{ triggerType: triggerType, id, type: 'edit' },
);
};
/**
* 查看
@ -79,10 +355,13 @@ const handleEdit = (id: string, triggerType: string) => {
* @param triggerType 触发类型
*/
const handleView = (id: string, triggerType: string) => {
menuStory.jumpPage('Scene/Save', { }, { triggerType: triggerType, id, type: 'view' })
menuStory.jumpPage(
'rule-engine/Scene/Save',
{},
{ triggerType: triggerType, id, type: 'view' },
);
};
</script>
<style scoped>
</style>

View File

@ -1,380 +0,0 @@
<template>
<div class="api-does-container">
<div class="top">
<h5>{{ selectApi.summary }}</h5>
<div class="input">
<InputCard :value="selectApi.method" />
<a-input :value="selectApi?.url" disabled />
</div>
</div>
<p>
<span class="label">请求数据类型</span>
<span>{{
getContent(selectApi.requestBody) ||
'application/x-www-form-urlencoded'
}}</span>
<span class="label">响应数据类型</span>
<span>{{ `["/"]` }}</span>
</p>
<div class="api-card">
<h5>请求参数</h5>
<div class="content">
<JTable
:columns="requestCard.columns"
:dataSource="requestCard.tableData"
noPagination
model="TABLE"
>
<template #required="slotProps">
<span>{{ Boolean(slotProps.required) + '' }}</span>
</template>
<template #type="slotProps">
<span>{{ slotProps.schema.type }}</span>
</template>
</JTable>
</div>
</div>
<div class="api-card">
<h5>响应状态</h5>
<div class="content">
<JTable
:columns="responseStatusCard.columns"
:dataSource="responseStatusCard.tableData"
noPagination
model="TABLE"
>
</JTable>
<a-tabs v-model:activeKey="responseStatusCard.activeKey">
<a-tab-pane
:key="key"
:tab="key"
v-for="key in tabs"
></a-tab-pane>
</a-tabs>
</div>
</div>
<div class="api-card">
<h5>响应参数</h5>
<div class="content">
<JTable
:columns="respParamsCard.columns"
:dataSource="respParamsCard.tableData"
noPagination
model="TABLE"
>
</JTable>
</div>
<MonacoEditor
v-model:modelValue="codeText"
style="height: 300px; width: 100%"
theme="vs"
/>
</div>
</div>
</template>
<script setup lang="ts">
import MonacoEditor from '@/components/MonacoEditor/index.vue';
import type { apiDetailsType } from '../typing';
import InputCard from './InputCard.vue';
import { PropType } from 'vue';
const emit = defineEmits(['update:paramsTable'])
const props = defineProps({
selectApi: {
type: Object as PropType<apiDetailsType>,
required: true,
},
schemas: {
type: Object,
required: true,
},
});
const { selectApi } = toRefs(props);
type tableCardType = {
columns: object[];
tableData: object[];
codeText?: any;
activeKey?: any;
getData?: any;
};
const requestCard = reactive<tableCardType>({
columns: [
{
title: '参数名',
dataIndex: 'name',
key: 'name',
},
{
title: '参数说明',
dataIndex: 'description',
key: 'description',
},
{
title: '请求类型',
dataIndex: 'in',
key: 'in',
},
{
title: '是否必须',
dataIndex: 'required',
key: 'required',
scopedSlots: true,
},
{
title: '参数类型',
dataIndex: 'type',
key: 'type',
scopedSlots: true,
},
],
tableData: [],
getData: () => {
requestCard.tableData = props.selectApi.parameters;
},
});
const responseStatusCard = reactive<tableCardType>({
activeKey: '',
columns: [
{
title: '状态码',
dataIndex: 'code',
key: 'code',
},
{
title: '说明',
dataIndex: 'desc',
key: 'desc',
},
{
title: 'schema',
dataIndex: 'schema',
key: 'schema',
},
],
tableData: [],
getData: () => {
if (!Object.keys(props.selectApi.responses).length)
return (responseStatusCard.tableData = []);
const tableData = <any>[];
Object.entries(props.selectApi.responses || {}).forEach((item: any) => {
const desc = item[1].description;
const schema = item[1].content['*/*'].schema.$ref?.split('/') || '';
tableData.push({
code: item[0],
desc,
schema: schema && schema.pop(),
});
});
responseStatusCard.activeKey = tableData[0]?.code;
responseStatusCard.tableData = tableData;
},
});
const tabs = computed(() =>
responseStatusCard.tableData
.map((item: any) => item.code + '')
.filter((code: string) => code !== '400'),
);
const respParamsCard = reactive<tableCardType>({
columns: [
{
title: '参数名称',
dataIndex: 'paramsName',
},
{
title: '参数说明',
dataIndex: 'desc',
},
{
title: '类型',
dataIndex: 'paramsType',
},
],
tableData: [],
codeText: '',
getData: (code: string) => {
type schemaObjType = {
paramsName: string;
paramsType: string;
desc?: string;
children?: schemaObjType[];
};
const schemaName = responseStatusCard.tableData.find(
(item: any) => item.code === code,
)?.schema;
const schemas = toRaw(props.schemas);
const basicType = ['string', 'integer', 'boolean'];
const tableData = findData(schemaName);
const codeText = getCodeText(tableData, 3);
emit('update:paramsTable', tableData)
respParamsCard.tableData = tableData;
respParamsCard.codeText = JSON.stringify(codeText);
function findData(schemaName: string) {
if (!schemaName || !schemas[schemaName]) {
return [];
}
const result: schemaObjType[] = [];
const schema = schemas[schemaName];
Object.entries(schema.properties).forEach((item: [string, any]) => {
const paramsType =
item[1].type ||
(item[1].$ref && item[1].$ref.split('/').pop()) ||
(item[1].items && item[1].items.$ref.split('/').pop()) ||
'';
const schemaObj: schemaObjType = {
paramsName: item[0],
paramsType,
desc: item[1].description || '',
};
if (!basicType.includes(paramsType))
schemaObj.children = findData(paramsType);
result.push(schemaObj);
});
return result;
}
function getCodeText(arr: schemaObjType[], level: number): object {
const result = {};
arr.forEach((item) => {
switch (item.paramsType) {
case 'string':
result[item.paramsName] = '';
break;
case 'integer':
result[item.paramsName] = 0;
break;
case 'boolean':
result[item.paramsName] = true;
break;
case 'array':
result[item.paramsName] = [];
break;
case 'object':
result[item.paramsName] = {};
break;
default: {
const properties = schemas[item.paramsType]
.properties as object;
const newArr = Object.entries(properties).map(
(item: [string, any]) => ({
paramsName: item[0],
paramsType: level
? (item[1].$ref &&
item[1].$ref.split('/').pop()) ||
(item[1].items &&
item[1].items.$ref
.split('/')
.pop()) ||
item[1].type ||
''
: item[1].type,
}),
);
result[item.paramsName] = getCodeText(
newArr,
level - 1,
);
}
}
});
return result;
}
},
});
const { codeText } = toRefs(requestCard);
const getContent = (data: any) => {
if (data && data.content) {
return Object.keys(data.content || {})[0];
}
return '';
};
onMounted(() => {
requestCard.getData();
responseStatusCard.getData();
});
watch(
() => props.selectApi,
() => {
requestCard.getData();
responseStatusCard.getData();
},
);
watch([() => responseStatusCard.activeKey, () => props.selectApi], (n) => {
n[0] && respParamsCard.getData(n[0]);
});
</script>
<style lang="less" scoped>
.api-does-container {
.top {
width: 100%;
h5 {
font-weight: bold;
font-size: 16px;
}
.input {
display: flex;
margin: 24px 0;
}
}
p {
display: flex;
justify-content: space-between;
font-size: 14px;
.label {
font-weight: bold;
}
}
.api-card {
margin-top: 24px;
h5 {
position: relative;
padding-left: 10px;
font-weight: 600;
font-size: 16px;
&::before {
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background-color: #1d39c4;
border-radius: 0 3px 3px 0;
content: ' ';
}
}
.content {
padding-left: 10px;
:deep(.jtable-body) {
padding: 0;
.jtable-body-header {
display: none;
}
}
}
}
}
</style>

View File

@ -1,134 +0,0 @@
<template>
<div class="api-test-container">
<div class="top">
<h5>{{ props.selectApi.summary }}</h5>
<div class="input">
<InputCard :value="props.selectApi.method" />
<a-input :value="props.selectApi?.url" disabled />
<span class="send">发送</span>
</div>
</div>
<div class="api-card">
<h5>请求参数</h5>
<div class="content">
<!-- <VueJsoneditor
height="400"
mode="tree"
v-model:text="requestBody.paramsText"
/> -->
<MonacoEditor
v-model:modelValue="requestBody.paramsText"
style="height: 300px; width: 100%"
theme="vs"
/>
</div>
</div>
<div class="api-card">
<h5>响应参数</h5>
<div class="content">
<VueJsoneditor
height="400"
mode="tree"
v-model:text="responsesContent"
/>
<!-- <MonacoEditor
v-model:modelValue="responsesContent"
style="height: 300px; width: 100%"
theme="vs"
/> -->
</div>
</div>
</div>
</template>
<script setup lang="ts">
import VueJsoneditor from 'vue3-ts-jsoneditor';
import MonacoEditor from '@/components/MonacoEditor/index.vue';
import type { apiDetailsType } from '../typing';
import InputCard from './InputCard.vue';
const props = defineProps<{
selectApi: apiDetailsType;
paramsTable: any[];
}>();
const requestBody = reactive({
paramsTable: [] as requestObj[],
paramsText: '',
});
const responsesContent = ref('{"a":123}');
watch(
() => props.paramsTable,
(n) => {
const table = n?.map((item: any) => ({
paramsName: item.paramsName,
value: '',
}));
requestBody.paramsTable = table;
},
);
type requestObj = {
paramsName: string;
value: string;
};
</script>
<style lang="less" scoped>
.api-test-container {
.top {
width: 100%;
h5 {
font-weight: bold;
font-size: 16px;
}
.input {
display: flex;
.send {
width: 65px;
padding: 4px 15px;
font-size: 14px;
color: #fff;
background-color: #1890ff;
}
}
}
.api-card {
margin-top: 24px;
h5 {
position: relative;
padding-left: 10px;
font-weight: 600;
font-size: 16px;
&::before {
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background-color: #1d39c4;
border-radius: 0 3px 3px 0;
content: ' ';
}
}
.content {
padding-left: 10px;
:deep(.jtable-body) {
padding: 0;
.jtable-body-header {
display: none;
}
}
}
}
}
</style>

View File

@ -1,66 +0,0 @@
<template>
<div class="choose-api-container">
<JTable
:columns="columns"
:dataSource="props.tableData"
:rowSelection="rowSelection"
noPagination
model="TABLE"
>
<template #url="slotProps">
<span
style="color: #1d39c4; cursor: pointer"
@click="jump(slotProps)"
>{{ slotProps.url}}</span
>
</template>
</JTable>
<a-button type="primary">保存</a-button>
</div>
</template>
<script setup lang="ts">
import { TableProps } from 'ant-design-vue';
const emits = defineEmits(['update:clickApi'])
const props = defineProps({
tableData: Array,
clickApi: Object
});
const columns = [
{
title: 'API',
dataIndex: 'url',
key: 'url',
scopedSlots: true,
},
{
title: '说明',
dataIndex: 'summary',
key: 'summary',
},
];
const rowSelection: TableProps['rowSelection'] = {
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
};
const jump = (row:any) => {
emits('update:clickApi',row)
};
</script>
<style lang="less" scoped>
.choose-api-container {
height: 100%;
:deep(.jtable-body-header) {
display: none !important;
}
}
</style>

View File

@ -1,35 +0,0 @@
<template>
<span class="input-card-container" :class="props.value">
{{ props.value?.toLocaleUpperCase() }}
</span>
</template>
<script setup lang="ts">
const props = defineProps({
value: String,
});
</script>
<style lang="less" scoped>
.input-card-container {
padding: 4px 15px;
font-size: 14px;
color: #fff;
&.get {
background-color: #1890ff;
}
&.put {
background-color: #fa8c16;
}
&.post {
background-color: #52c41a;
}
&.delete {
background-color: #f5222d;
}
&.patch {
background-color: #a0d911;
}
}
</style>

View File

@ -1,97 +0,0 @@
<template>
<a-tree
:tree-data="treeData"
@select="clickSelectItem"
showLine
class="left-tree-container"
>
<template #title="{ name }">
{{ name }}
</template>
</a-tree>
</template>
<script setup lang="ts">
import { TreeProps } from 'ant-design-vue';
import { getTreeOne_api, getTreeTwo_api } from '@/api/system/apiPage';
import { treeNodeTpye } from '../typing';
const emits = defineEmits(['select']);
const treeData = ref<TreeProps['treeData']>([]);
const getTreeData = () => {
let tree: treeNodeTpye[] = [];
getTreeOne_api().then((resp: any) => {
tree = resp.urls.map((item: any) => ({
...item,
key: item.url,
}));
const allPromise = tree.map((item) => getTreeTwo_api(item.name));
Promise.all(allPromise).then((values) => {
values.forEach((item: any, i) => {
tree[i].children = combData(item?.paths);
tree[i].schemas = item.components.schemas
});
treeData.value = tree;
});
});
};
const clickSelectItem: TreeProps['onSelect'] = (key, node: any) => {
if(!node.node.parent) return
emits('select', node.node.dataRef, node.node?.parent.node.schemas);
};
onMounted(() => {
getTreeData();
});
const combData = (dataSource: object) => {
const apiList: treeNodeTpye[] = [];
const keys = Object.keys(dataSource);
keys.forEach((key) => {
const method = Object.keys(dataSource[key] || {})[0];
const name = dataSource[key][method].tags[0];
let apiObj: treeNodeTpye | undefined = apiList.find(
(item) => item.name === name,
);
if (apiObj) {
apiObj.apiList?.push({
url: key,
method: dataSource[key],
});
} else {
apiObj = {
name,
key: name,
apiList: [
{
url: key,
method: dataSource[key],
},
],
};
apiList.push(apiObj);
}
});
return apiList;
};
</script>
<style lang="less">
.left-tree-container {
border-right: 1px solid #e9e9e9;
height: calc(100vh - 150px);
overflow-y: auto;
.ant-tree-list {
.ant-tree-list-holder-inner {
.ant-tree-switcher-noop {
display: none !important;
}
}
}
}
</style>

View File

@ -1,95 +1,12 @@
<template>
<a-card class="api-page-container">
<a-row :gutter="24">
<a-col :span="5">
<LeftTree @select="treeSelect" />
</a-col>
<a-col :span="19">
<ChooseApi
v-show="!selectedApi.url"
v-model:click-api="selectedApi"
:table-data="tableData"
/>
<div
class="api-details"
v-if="selectedApi.url && tableData.length > 0"
>
<a-button
@click="selectedApi = initSelectedApi"
style="margin-bottom: 24px"
>返回</a-button
>
<a-tabs v-model:activeKey="activeKey" type="card">
<a-tab-pane key="does" tab="文档">
<ApiDoes
:select-api="selectedApi"
:schemas="schemas"
v-model:params-table="paramsTable"
/>
</a-tab-pane>
<a-tab-pane key="test" tab="调试">
<ApiTest :select-api="selectedApi" :params-table="paramsTable" />
</a-tab-pane>
</a-tabs>
</div>
</a-col>
</a-row>
</a-card>
<page-container>
<Api :mode="'appManger'" hasHome>
</Api>
</page-container>
</template>
<script setup lang="ts" name="apiPage">
import type { treeNodeTpye, apiObjType, apiDetailsType } from './typing';
import LeftTree from './components/LeftTree.vue';
import ChooseApi from './components/ChooseApi.vue';
import ApiDoes from './components/ApiDoes.vue';
import ApiTest from './components/ApiTest.vue';
const tableData = ref([]);
const treeSelect = (node: treeNodeTpye, nodeSchemas: object = {}) => {
schemas.value = nodeSchemas;
if (!node.apiList) return;
const apiList: apiObjType[] = node.apiList as apiObjType[];
const table: any = [];
//
apiList?.forEach((apiItem) => {
const { method, url } = apiItem;
for (const key in method) {
if (Object.prototype.hasOwnProperty.call(method, key)) {
table.push({
...method[key],
url,
method: key,
});
}
}
});
tableData.value = table;
};
const activeKey = ref<'does' | 'test'>('does');
const schemas = ref({});
const paramsTable = ref([])
const initSelectedApi: apiDetailsType = {
url: '',
method: '',
summary: '',
parameters: [],
responses: {},
requestBody: {},
};
const selectedApi = ref<apiDetailsType>(initSelectedApi);
watch(tableData, () => {
activeKey.value = 'does';
selectedApi.value = initSelectedApi;
});
import Api from '@/views/system/Platforms/Api/index.vue';
</script>
<style scoped>
.api-page-container {
padding: 24px;
height: 100%;
background-color: transparent;
}
</style>
<style scoped></style>

View File

@ -1,25 +0,0 @@
export type treeNodeTpye = {
name: string;
key: string;
schemas?:object;
link?: string;
apiList?: object[];
children?: treeNodeTpye[];
};
export type methodType = {
[key: string]: object
}
export type apiObjType = {
url: string,
method: methodType
}
export type apiDetailsType = {
url: string;
method: string;
summary: string;
parameters: any[];
requestBody?: any;
responses:object;
}

View File

@ -1022,9 +1022,9 @@
.logoUrl
"
alt="avatar"
width="100%"
style="width: 150px;"
/>
<div v-else>
<div v-else style="width: 150px;">
<AIcon
:type="
form.uploadLoading
@ -1777,6 +1777,16 @@ function clearNullProp(obj: object) {
:deep(.ant-form-item-control) {
.ant-form-item-control-input-content {
display: flex;
.ant-upload-select-picture-card {
width: auto;
height: auto;
max-width: 150px;
max-height: 150px;
>.ant-upload {
height: 150px;
}
}
}
}
}

View File

@ -0,0 +1,14 @@
<template>
<page-container>
<Api :mode="'appManger'" hasHome showTitle :code="code">
</Api>
</page-container>
</template>
<script setup lang="ts" name="apiPage">
import Api from '@/views/system/Platforms/Api/index.vue';
const route = useRoute()
const code = route.query.code as string
</script>
<style scoped></style>

View File

@ -1,5 +1,5 @@
<template>
<page-container class="apply-container">
<page-container>
<div class="apply-container">
<Search :columns="columns" @search="search" />
@ -374,7 +374,13 @@ const table = {
title: '赋权',
},
icon: 'icon-fuquan',
onClick: () => {},
onClick: () => {
menuStory.jumpPage(
'system/Apply/Api',
{},
{ code: data.id },
);
},
},
{
permission: true,
@ -384,7 +390,13 @@ const table = {
title: '查看API',
},
icon: 'icon-chakanAPI',
onClick: () => {},
onClick: () => {
menuStory.jumpPage(
'system/Apply/View',
{},
{ code: data.id },
);
},
},
);
//

View File

@ -1906,7 +1906,7 @@ export default [
],
},
{
id: 'tigger',
id: 'trigger',
name: '手动触发',
permissions: [
{
@ -2323,7 +2323,7 @@ export default [
],
},
{
id: 'tigger',
id: 'trigger',
name: '手动触发',
permissions: [
{

View File

@ -0,0 +1,229 @@
<template>
<div class="home">
<h1>第三方接入说明</h1>
<div style="color: #666666">
第三方平台接口请求基于数据签名调用方式使用签名来校验客户端请求的完整性以及合法性您可以参看如下文档来构造
HTTP 接口以调用对应的第三方平台接口
</div>
<h2>签名示例说明</h2>
<div class="h2-text">1. 签名方式,支持MD5和Sha256两种方式.</div>
<div class="h2-text">
2. 发起请求的签名信息都需要放到请求头中,而不是请求体.
</div>
<div
style="
display: flex;
border: 1px solid #e6e6e6;
padding: 15;
justify-content: space-between;
"
>
<div>
<h3>签名规则</h3>
<p>
注意签名时间戳与服务器时间不能相差五分钟以上否则服务器将拒绝本次请求
</p>
<div class="div-border">
<div class="h3-text">
将参数key按ascii排序得到: pageIndex=0&pageSize=20
</div>
<div class="h3-text">
使用拼接时间戳以及密钥得到:
pageIndex=0&pageSize=201574993804802testSecure
</div>
<div class="h3-text">
使用md5(pageIndex=0&pageSize=201574993804802testSecure)得到837fe7fa29e7a5e4852d447578269523
</div>
</div>
<h3>请求头示例</h3>
<div class="div-border">
<div class="h3-text">
GET /api/device?pageIndex=0&amp;pageSize=20
</div>
<div class="h3-text">X-Client-Id: testId</div>
<div class="h3-text">X-Timestamp: 1574993804802</div>
<div class="h3-text">
X-Sign: 837fe7fa29e7a5e4852d447578269523
</div>
</div>
<h3>响应结果示例</h3>
<div class="div-border">
<div class="h3-text">xxx</div>
<div class="h3-text">HTTP/1.1 200 OK</div>
<div class="h3-text">X-Timestamp: 1574994269075</div>
<div class="h3-text">
X-Sign: c23faa3c46784ada64423a8bba433f25
</div>
<div class="h3-text">status:200,result:[ ]</div>
</div>
</div>
<div style="width: 50%">
<h3>示例数据</h3>
<div>
<JTable
:dataSource="data"
model="TABLE"
noPagination
:columns="[
{
title: '示例数据类型',
dataIndex: 'type',
},
{
title: '示例数据',
dataIndex: 'data',
},
]"
/>
</div>
</div>
</div>
<div
:style="{
display: 'flex',
border: '1px solid #e6e6e6',
padding: 15,
justifyContent: 'space-between',
marginTop: 20,
}"
>
<div>
<h3>服务器验签流程</h3>
<div>
<img :src="getImage('/apiHome.png')" style="width: 80%" />
</div>
</div>
<div style="width: 505px">
<h3>验签说明</h3>
<div>
<p>使用和签名相同的算法(不需要对响应结果排序)</p>
<div>
<MonacoEditor
style="width: 100%; height: 620px"
theme="vs-dark"
language="java"
v-model="javaStr1"
/>
</div>
</div>
</div>
</div>
<div>
<h2>java SDK接入说明</h2>
<div class="div-border">
<div class="h3-text">
JetLinks平台java SDK基于java 8版本开发
</div>
</div>
<h3>添加 SDK 依赖</h3>
<div class="h3-text">将以下Maven依赖加入到pom.xml文件中</div>
<div>
<MonacoEditor
style="width: 100%; height: 100px"
theme="vs-dark"
v-model="javaStr2"
language="java"
/>
</div>
<h3>SDK 客户端的初始化和请求方式</h3>
<div>
<MonacoEditor
style="width: 100%; height: 370px"
theme="vs-dark"
v-model="javaStr"
language="java"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { getImage } from '@/utils/comm';
import MonacoEditor from '@/components/MonacoEditor/index.vue';
const data = [
{
key: '1',
type: 'clientId',
data: 'testId',
},
{
key: '2',
type: 'secureKey',
data: 'testSecure',
},
{
key: '3',
type: '请求URI',
data: '/api/v1/device/dev0001/log/_query',
},
{
key: '4',
type: '请求方式',
data: 'GET',
},
{
key: '5',
type: '请求参数',
data: 'pageSize=20&pageIndex=0',
},
{
key: '6',
type: '签名方式',
data: 'MD5',
},
{
key: '7',
type: '签名示例时间戳',
data: '1574993804802 ',
},
];
const javaStr1 = `String secureKey = ...; //密钥\r\nString responseBody = ...;//服务端响应结果\r\nString timestampHeader = ...;//响应头: X-Timestamp\r\nString signHeader = ...; //响应头: X-Sign\r\n\r\nString sign = DigestUtils.md5Hex(responseBody+timestampHeader+secureKey);\r\nif(sign.equalsIgnoreCase(signHeader)){\r\n //验签通过\r\n}`;
const javaStr2 =
'<dependency>\r\n <groupId>org.jetlinks.sdk</groupId>\r\n <artifactId>api-sdk</artifactId>\r\n <version>1.0.0</version>\r\n</dependency>';
const javaStr =
'\r\n //服务器的baseUrl\r\n String baseUrl = "http://localhost:9000/jetlinks";\r\n //客户端Id\r\n String clientId = "aSoq98aAxzP";\r\n //访问秘钥\r\n String secureKey = "DaYsxpiWSfdTAPJyKW8rP2WAGyWErnsR";\r\n\r\n ClientConfig clientConfig = new ClientConfig(baseUrl, clientId, secureKey);\r\n\r\n ApiClient client = new WebApiClient(clientConfig);\r\n\r\nApiResponse < PagerResult < DeviceInfo >> response = client\r\n .request(QueryDeviceRequest\r\n .of(query -> query\r\n .where("productId", "demo-device")\r\n .doPaging(0, 100)));';
</script>
<style lang="less" scoped>
.home {
padding: 20px;
h1 {
font-weight: 600;
font-size: 20px;
}
h2 {
font-weight: 600;
font-size: 18px;
}
.h2-text {
color: #999;
}
h3 {
margin-top: 10px;
font-weight: 600;
font-size: 16px;
}
.h3-text {
max-width: 530px;
margin-top: 3px;
color: #999;
}
p {
color: #666;
}
.div-border {
padding: 10px;
border-left: 10px solid #eee;
}
}
</style>

View File

@ -2,6 +2,7 @@
<a-tree
:tree-data="treeData"
@select="clickSelectItem"
v-model:selected-keys="selectedKeys"
showLine
class="left-tree-container"
>
@ -14,16 +15,23 @@
<script setup lang="ts">
import { TreeProps } from 'ant-design-vue';
import { getTreeOne_api, getTreeTwo_api } from '@/api/system/apiPage';
import {
apiOperations_api,
getApiGranted_api,
getTreeOne_api,
getTreeTwo_api,
} from '@/api/system/apiPage';
import type { modeType, treeNodeTpye } from '../typing';
const emits = defineEmits(['select']);
const props = defineProps<{
mode:modeType
}>()
mode: modeType;
hasHome?: boolean;
code?: string;
}>();
const treeData = ref<TreeProps['treeData']>([]);
const selectedKeys = ref<string[]>([]);
const getTreeData = () => {
let tree: treeNodeTpye[] = [];
getTreeOne_api().then((resp: any) => {
@ -32,17 +40,42 @@ const getTreeData = () => {
key: item.url,
}));
const allPromise = tree.map((item) => getTreeTwo_api(item.name));
// api
if (props.mode === 'appManger') allPromise.push(apiOperations_api());
else if (props.mode === 'home')
allPromise.push(getApiGranted_api(props.code as string));
Promise.all(allPromise).then((values) => {
values.forEach((item: any, i) => {
tree[i].children = combData(item?.paths);
tree[i].schemas = item.components.schemas
if (props.mode === 'api') {
tree[i].schemas = item.components.schemas;
tree[i].children = combData(item.paths);
} else if (i < values.length - 2) {
const paths = filterPath(
item.paths,
values[values.length - 1].result as string[],
);
tree[i].children = combData(paths);
tree[i].schemas = item.components.schemas;
}
});
if (props.hasHome) {
tree.unshift({
key: 'home',
name: '首页',
schemas: {},
children: [],
});
selectedKeys.value = ['home'];
}
treeData.value = tree;
});
});
};
const clickSelectItem: TreeProps['onSelect'] = (key, node: any) => {
if(!node.node.parent) return
const clickSelectItem: TreeProps['onSelect'] = (key: any[], node: any) => {
if (key[0] === 'home') return emits('select', node.node.dataRef, {});
if (!node.node.parent && key[0] !== 'home') return;
emits('select', node.node.dataRef, node.node?.parent.node.schemas);
};
@ -76,12 +109,36 @@ const combData = (dataSource: object) => {
},
],
};
apiList.push(apiObj);
}
});
return apiList;
};
/**
* 过滤能展示的接口 模式mode为api时不需要过滤
* @param path 源数据
* @param filterArr 过滤数组
*/
const filterPath = (path: object, filterArr: string[]) => {
for (const key in path) {
if (Object.prototype.hasOwnProperty.call(path, key)) {
const value = path[key];
for (const prop in value) {
if (Object.prototype.hasOwnProperty.call(value, prop)) {
const item = value[prop];
if (!filterArr.includes(item.operationId))
delete value[prop];
}
}
if(Object.keys(value).length === 0) delete path[key]
}
}
console.log(path, filterArr);
return path;
};
</script>
<style lang="less">

View File

@ -1,49 +1,64 @@
<template>
<a-card class="api-page-container">
<p>
<AIcon type="ExclamationCircleOutlined" style="margin-right: 12px;" />配置系统支持API赋权的范围
</p>
<a-row :gutter="24">
<div class="api-page-container">
<div class="top">
<slot name="top" />
</div>
<a-row :gutter="24" style="background-color: #fff; padding: 20px">
<a-col
:span="24"
v-if="props.showTitle"
style="font-size: 16px; margin-bottom: 48px"
>API文档</a-col
>
<a-col :span="5">
<LeftTree @select="treeSelect" :mode="props.mode" />
<LeftTree
@select="treeSelect"
:mode="props.mode"
:has-home="props.hasHome"
:filter-array="treeFilter"
/>
</a-col>
<a-col :span="19">
<ChooseApi
v-show="!selectedApi.url"
v-model:click-api="selectedApi"
:table-data="tableData"
v-model:selectedRowKeys="selectedKeys"
:source-keys="selectSourceKeys" :mode="props.mode"
/>
<HomePage v-show="showHome" />
<div class="url-page" v-show="!showHome">
<ChooseApi
v-show="!selectedApi.url"
v-model:click-api="selectedApi"
:table-data="tableData"
v-model:selectedRowKeys="selectedKeys"
:source-keys="selectSourceKeys"
:mode="props.mode"
/>
<div
class="api-details"
v-if="selectedApi.url && tableData.length > 0"
>
<a-button
@click="selectedApi = initSelectedApi"
style="margin-bottom: 24px"
>返回</a-button
<div
class="api-details"
v-if="selectedApi.url && tableData.length > 0"
>
<a-tabs v-model:activeKey="activeKey" type="card">
<a-tab-pane key="does" tab="文档">
<ApiDoes
:select-api="selectedApi"
:schemas="schemas"
/>
</a-tab-pane>
<a-tab-pane key="test" tab="调试">
<ApiTest :select-api="selectedApi" />
</a-tab-pane>
</a-tabs>
<a-button
@click="selectedApi = initSelectedApi"
style="margin-bottom: 24px"
>返回</a-button
>
<a-tabs v-model:activeKey="activeKey" type="card">
<a-tab-pane key="does" tab="文档">
<ApiDoes
:select-api="selectedApi"
:schemas="schemas"
/>
</a-tab-pane>
<a-tab-pane key="test" tab="调试">
<ApiTest :select-api="selectedApi" />
</a-tab-pane>
</a-tabs>
</div>
</div>
</a-col>
</a-row>
</a-card>
</div>
</template>
<script setup lang="ts" name="apiPage">
import HomePage from './components/HomePage.vue';
import { getApiGranted_api, apiOperations_api } from '@/api/system/apiPage';
import type {
treeNodeTpye,
@ -59,12 +74,18 @@ import ApiTest from './components/ApiTest.vue';
const route = useRoute();
const props = defineProps<{
mode: modeType;
showTitle?: boolean;
hasHome?: boolean;
code?: string
}>();
const showHome = ref<boolean>(Boolean(props.hasHome));
const tableData = ref([]);
const treeFilter = ref([]);
const treeSelect = (node: treeNodeTpye, nodeSchemas: object = {}) => {
if (node.key === 'home') return (showHome.value = true);
schemas.value = nodeSchemas;
if (!node.apiList) return;
showHome.value = false;
const apiList: apiObjType[] = node.apiList as apiObjType[];
const table: any = [];
//
@ -98,7 +119,7 @@ const selectedApi = ref<apiDetailsType>(initSelectedApi);
const canSelectKeys = ref<string[]>([]); //
const selectedKeys = ref<string[]>([]); //
let selectSourceKeys = ref<string[]>([])
let selectSourceKeys = ref<string[]>([]);
init();
function init() {
@ -106,10 +127,10 @@ function init() {
if (props.mode === 'appManger') {
} else if (props.mode === 'home') {
} else if (props.mode === 'api') {
apiOperations_api().then(resp=>{
selectedKeys.value = resp.result as string[]
selectSourceKeys.value = [...resp.result as string[]]
})
apiOperations_api().then((resp) => {
selectedKeys.value = resp.result as string[];
selectSourceKeys.value = [...(resp.result as string[])];
});
}
watch(tableData, () => {
activeKey.value = 'does';

View File

@ -1,6 +1,15 @@
<template>
<page-container>
<Api mode="api" />
<Api mode="api">
<template #top>
<p>
<AIcon
type="ExclamationCircleOutlined"
style="margin-right: 12px; font-size: 14px"
/>API
</p>
</template>
</Api>
</page-container>
</template>