fix: merge

This commit is contained in:
wzyyy 2023-03-01 14:15:55 +08:00
commit 1f4bdc274e
138 changed files with 9324 additions and 3791 deletions

2
.npmrc
View File

@ -1,2 +1,2 @@
always-auth=true
registry=http://47.108.170.157:9013/
registry=https://registry.jetlinks.cn/

View File

@ -23,14 +23,14 @@
"event-source-polyfill": "^1.0.31",
"global": "^4.4.0",
"jetlinks-store": "^0.0.3",
"jetlinks-ui-components": "^1.0.0",
"js-cookie": "^3.0.1",
"less": "^4.1.3",
"less-loader": "^11.1.0",
"lodash-es": "^4.17.21",
"marked": "^4.2.12",
"mavon-editor": "^2.10.4",
"moment": "^2.29.4",
"monaco-editor": "^0.24.0",
"monaco-editor": "^0.36.0",
"nrm": "^1.2.5",
"pinia": "^2.0.28",
"unplugin-auto-import": "^0.12.1",

View File

@ -0,0 +1,3 @@
import server from '@/utils/request'
export const getSsoBinds_api = (): any =>server.get(`/application/sso/me/bindings`)

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

@ -514,6 +514,7 @@ export const deviceCode = (productId: string,deviceId:string) => server.get(`dev
/**
*
* @param productId
*
* @param deviceId
* @param data
* @returns
@ -538,3 +539,11 @@ export const delDeviceCode = (productId: string, deviceId: string) => server.rem
* @returns
*/
export const delProductCode = (productId: string) => server.remove(`/device/transparent-codec/${productId}`)
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`)

View File

@ -185,3 +185,12 @@ export const getOperator = () => server.get<OperatorItem>('/property-calculate-r
*
*/
export const getStreamingAggType = () => server.get<Record<string, string>[]>('/dictionary/streaming-agg-type/items')
export const getMetadataConfig = (params: {
deviceId: string;
metadata: {
type: MetadataType | 'property';
id: string;
dataType: string;
};
}) => server.get<Record<any, any>[]>(`/device/product/${params.deviceId}/config-metadata/${params.metadata.type}/${params.metadata.id}/${params.metadata.dataType}`)

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

16
src/api/media/channel.ts Normal file
View File

@ -0,0 +1,16 @@
import server from '@/utils/request'
export default {
// 列表
list: (data: any, id: string) => server.post(`/media/device/${id}/channel/_query`, data),
// 详情
detail: (id: string): any => server.get(`/media/channel/${id}`),
// 验证通道ID是否存在
validateField: (params: any): any => server.get(`/media/channel/channelId/_validate`, params),
// 新增
save: (data: any) => server.post(`/media/channel`, data),
// 修改
update: (id: string, data: any) => server.put(`/media/channel/${id}`, data),
// 删除
del: (id: string) => server.remove(`media/channel/${id}`),
}

View File

@ -0,0 +1,26 @@
import server from '@/utils/request';
/**
*
*/
export const getProductList = (parmas?:any) => server.get('/device/product/_query/no-paging?paging=false',parmas);
/**
*
*/
export const getDeviceList = (parmas?:any) => server.get('/device-instance/_query/no-paging?paging=false',parmas);
/**
*
*/
export const getOrgList = (parmas?:any) => server.get('/organization/_query/no-paging?paging=false',parmas);
/**
*
*/
export const query = (data:any) => server.post('/alarm/record/_query/',data);
/**
*
*/
export const handleLog = (data:any) => server.post('/alarm/record/_handle',data)

View File

@ -8,3 +8,14 @@ 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 _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

@ -12,9 +12,22 @@ export const delApply_api = (id:string) => server.remove(`/application/${id}`)
// 获取组织列表
export const getDepartmentList_api = () => server.get(`/organization/_all/tree`);
// 获取组织列表
// 获取应用详情
export const getAppInfo_api = (id: string) => server.get(`/application/${id}`);
// 新增应用
export const addApp_api = (data: object) => server.post(`/application`, data);
// 更新应用
export const updateApp_api = (id: string, data: object) => server.put(`/application/${id}`, data);
// ---------集成菜单-----------
// 获取所属系统
export const getOwner_api = (data: object) => server.post(`/menu/owner`, data);
export const getOwnerStandalone_api = (appId: string, data: object) => server.post(`/application/${appId}/_/api/menu/owner`, data);
// 获取对应系统菜单树
export const getOwnerTree_api = (owner: string) => server.post(`/menu/owner/tree/${owner}`, {});
export const getOwnerTreeStandalone_api = (appId: string, owner: string) => server.post(`/application/${appId}/_/api/menu/owner/tree/${owner}`, {});
// 保存集成菜单
export const saveOwnerMenu_api = (owner: string, appId: string, data: object) => server.patch(`/menu/owner/${owner}/${appId}/_all`, data);

View File

@ -48,7 +48,7 @@ const iconKeys = [
'ClockCircleOutlined',
'PartitionOutlined',
'ShareAltOutlined',
'playCircleOutlined',
'PlayCircleOutlined',
'RightOutlined',
'FileTextOutlined',
'UploadOutlined',
@ -58,7 +58,12 @@ const iconKeys = [
'PauseOutlined',
'ControlOutlined',
'RedoOutlined',
'ExpandOutlined'
'ExpandOutlined',
'VideoCameraOutlined',
'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

@ -40,11 +40,19 @@
</div>
</div>
</div>
<div class="card-mask" v-if="props.hasMark">
<div class="mask-content">
<slot name="mark" />
</div>
</div>
</div>
<!-- 按钮 -->
<slot name="bottom-tool">
<div v-if="showTool && actions && actions.length" class="card-tools">
<div
v-if="showTool && actions && actions.length"
class="card-tools"
>
<div
v-for="item in actions"
:key="item.key"
@ -79,10 +87,14 @@
</template>
<script setup lang="ts">
import { SearchOutlined, CheckOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import {
SearchOutlined,
CheckOutlined,
DeleteOutlined,
} from '@ant-design/icons-vue';
import BadgeStatus from '@/components/BadgeStatus/index.vue';
import { StatusColorEnum } from '@/utils/consts.ts';
import type { ActionsType } from '@/components/Table/index.vue'
import type { ActionsType } from '@/components/Table/index.vue';
import { PropType } from 'vue';
type EmitProps = {
@ -90,14 +102,14 @@ type EmitProps = {
(e: 'click', data: Record<string, any>): void;
};
type TableActionsType = Partial<ActionsType>
type TableActionsType = Partial<ActionsType>;
const emit = defineEmits<EmitProps>();
const props = defineProps({
value: {
type: Object as PropType<Record<string, any>>,
default: () => {}
default: () => {},
},
showStatus: {
type: Boolean,
@ -124,8 +136,12 @@ const props = defineProps({
},
active: {
type: Boolean,
default: false
}
default: false,
},
hasMark: {
type: Boolean,
default: false,
},
});
const handleClick = () => {
@ -167,9 +183,13 @@ const handleClick = () => {
position: relative;
border: 1px solid #e6e6e6;
&.hover {
&:hover {
cursor: pointer;
box-shadow: 0 0 24px rgba(#000, 0.1);
.card-mask {
visibility: visible;
}
}
&.active {
@ -269,12 +289,12 @@ const handleClick = () => {
width: 100%;
height: 100%;
color: #fff;
background-color: rgba(#000, 0);
background-color: rgba(#000, .5);
visibility: hidden;
cursor: pointer;
transition: all 0.3s;
> div {
.mask-content {
display: flex;
align-items: center;
justify-content: center;
@ -282,11 +302,6 @@ const handleClick = () => {
height: 100%;
padding: 0 !important;
}
&.show {
background-color: rgba(#000, 0.5);
visibility: visible;
}
}
}

View File

@ -0,0 +1,115 @@
<template>
<div class="indicator-box">
<template v-if="['int', 'long', 'double', 'float'].includes(type)">
<template v-if="value.range">
<a-input-number v-model:value="value.value[0]" :max="value.value[1]" size="small"
style="width: 100%;"></a-input-number>
~
<a-input-number v-model:value="value.value[1]" :min="value.value[0]" size="small"
style="width: 100%;"></a-input-number>
</template>
<a-input-number v-else v-model:value="value.value" size="small" style="width: 100%;"></a-input-number>
</template>
<template v-else-if="type === 'date'">
<a-range-picker v-if="value.range" show-time v-model:value="value.value" size="small" />
<a-date-picker v-else show-time v-model:value="value.value" size="small" />
</template>
<template v-else-if="type === 'boolean'">
<a-select v-model:value="value.value[0]" :options="list" size="small" placeholder="请选择"></a-select>
</template>
<template v-else-if="type === 'string'">
<a-input v-model:value="value.value" size="small" placeholder="请输入"></a-input>
</template>
<template v-else>
<template v-if="value.range">
<a-input v-model:value="value.value[0]" :max="value.value[1]" size="small" placeholder="请输入"></a-input>
~
<a-input v-model:value="value.value[1]" :min="value.value[0]" size="small" placeholder="请输入"></a-input>
</template>
<a-input-number v-else v-model:value="value.value" size="small" placeholder="请输入"></a-input-number>
</template>
<div v-if="type !== 'boolean' && type !== 'string'">
<a-checkbox style="min-width: 60px; margin-left: 5px;" v-model:checked="value.range" @change="changeChecked">
范围
</a-checkbox>
</div>
</div>
</template>
<script setup lang="ts" name="JIndicators">
import { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';
import { Form } from 'ant-design-vue'
const props = defineProps({
type: {
type: String,
required: true
},
range: {
type: Boolean,
default: false
},
value: {
type: [String, Number, Array] as any
},
enum: {
type: Object,
default: () => ({})
}
})
Form.useInjectFormItemContext()
const changeChecked = (e: CheckboxChangeEvent) => {
if (e.target.checked) {
props.value.value = []
} else {
delete props.value.value
}
}
const list = ref<{ label: any; value: any; }[]>([])
watch(() => props.enum,
() => {
const arr = [];
if (!!props.enum?.falseText && props.enum?.falseValue !== undefined) {
arr.push({ label: props.enum?.falseText, value: props.enum?.falseValue });
}
if (!!props.enum?.trueText && props.enum?.trueValue !== undefined) {
arr.push({ label: props.enum?.trueText, value: props.enum?.trueValue });
}
list.value = arr
},
{ immediate: true, deep: true })
watch(() => props.type,
(value) => {
if (value === 'boolean') {
if (!props.value.value) props.value.value = []
}
},
{ immediate: true })
</script>
<style lang="less" scoped>
.indicator-box {
display: flex;
justify-content: space-between;
align-items: center;
}
:deep(.ant-form-item-label) {
line-height: 1;
>label {
font-size: 12px;
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
font-size: 12px;
}
}
}
:deep(input) {
height: 22px;
}
</style>

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
}
})
@ -77,5 +77,8 @@ onMounted(() => {
}
}
}
:deep(input) {
height: 22px;
}
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<a-popover placement="left" trigger="click">
<template #title>
<div class="edit-title" style="display: flex; justify-content: space-between; align-items: center;">
<div style="width: 150px;">{{ config.name }}</div>
</div>
</template>
<template #content>
<div style="max-width: 400px;" class="ant-form-vertical">
<a-form-item v-for="item in config.properties" :name="name.concat([item.property])" :label="item.name">
<a-select v-model:value="value[item.property]" :options="item.type?.elements?.map((e: { 'text': string, 'value': string }) => ({
label: e.text,
value: e.value,
}))" size="small"></a-select>
</a-form-item>
</div>
</template>
<a-button type="dashed" block>
存储配置<AIcon type="EditOutlined" class="item-icon"/>
</a-button>
</a-popover>
</template>
<script setup lang="ts" name="ConfigParam">
import { PropType } from 'vue';
type ValueType = Record<any, any>;
const props = defineProps({
value: {
type: Object,
default: () => ({})
},
name: {
type: Array as PropType<(string| number)[]>,
default: () => ([]),
required: true
},
config: {
type: Array as PropType<ValueType>,
default: () => ({ properties: [] })
}
})
// interface Emits {
// (e: 'update:value', data: string | undefined): void;
// }
// const emit = defineEmits<Emits>()
// const _value = computed({
// get: () => props.value,
// set: (val: string | undefined) => {
// emit('update:value', val)
// }
// })
</script>
<style lang="less" scoped>
.item-icon {
color: rgb(136, 136, 136);
font-size: 12px;
}
:deep(.ant-form-item-label) {
>label {
font-size: 12px;
}
}
:deep(.ant-select) {
font-size: 12px;
}
:deep(input) {
height: 22px;
}
</style>

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 })
{ deep: true, immediate: true })
const editIndex = ref<number>(-1)
const handleEdit = (index: number) => {
@ -157,4 +161,8 @@ const handleAdd = () => {
:deep(.ant-select) {
font-size: 12px;
}
:deep(input) {
height: 22px;
}
</style>

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="[
<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: '请输入英文或者数字或者-或者_',
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="[
<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="['valueType']" isSub
<value-type-form v-model:value="_value[index].valueType" :name="name.concat([index, 'valueType'])" isSub
key="json_sub"></value-type-form>
</a-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) => {
@ -169,4 +175,8 @@ const handleAdd = () => {
:deep(.ant-select) {
font-size: 12px;
}
:deep(input) {
height: 22px;
}
</style>

View File

@ -0,0 +1,207 @@
<template>
<div class="json-param">
<div class="list-item" v-for="(item, index) in _value" :key="`object_${index}`">
<div class="item-left">
<AIcon type="MenuOutlined" class="item-drag item-icon" />
{{ `#${index + 1}.` }}
</div>
<div class="item-middle item-editable">
<a-popover :visible="editIndex === index" placement="top" @visible-change="change" trigger="click">
<template #title>
<div class="edit-title" style="display: flex; justify-content: space-between; align-items: center;">
<div style="width: 150px;">配置参数</div>
<AIcon type="CloseOutlined" @click="handleClose" />
</div>
</template>
<template #content>
<div>
<div 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>
<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" />
</a-form-item>
</div>
</div>
</template>
<div class="item-edit" @click="handleEdit(index)">
{{ item.name || '配置参数' }}
<AIcon type="EditOutlined" class="item-icon" />
</div>
</a-popover>
</div>
<div class="item-right">
<AIcon type="DeleteOutlined" @click="handleDelete(index)" />
</div>
</div>
<a-button type="dashed" block @click="handleAdd">
<template #icon>
<AIcon type="PlusOutlined" class="item-icon" />
</template>
添加指标
</a-button>
</div>
</template>
<script setup lang="ts" name="MetricsParam">
import { PropType } from 'vue'
import JIndicators from '@/components/JIndicators/index.vue';
interface Emits {
(e: 'update:value', data: Record<any, any>[]): void;
}
const emit = defineEmits<Emits>()
const props = defineProps({
value: {
type: Object as PropType<Record<any, any>[]>,
default: () => ([])
},
type: {
type: String,
default: ''
},
enum: {
type: Object,
default: () => ({})
},
name: {
type: Array as PropType<(string | number)[]>,
default: () => ([])
}
})
const _value = ref<Record<any, any>[]>([])
watchEffect(() => {
_value.value = props.value
})
watch(_value,
() => {
emit('update:value', _value.value)
},
{ deep: true })
const editIndex = ref<number>(-1)
const handleEdit = (index: number) => {
editIndex.value = index
}
const handleDelete = (index: number) => {
editIndex.value = -1
_value.value.splice(index, 1)
}
const handleClose = () => {
editIndex.value = -1
}
const handleAdd = () => {
_value.value.push({})
emit('update:value', _value.value)
}
const validateIndicator = (value: any) => {
if (value?.range) {
if (!value?.value || !value?.value[0] || !value?.value[1]) {
return Promise.reject(new Error('请输入指标值'));
}
} else {
if (value?.value === '' || value?.value === undefined) {
return Promise.reject(new Error('请输入指标值'));
}
}
return Promise.resolve();
}
const change = (visible: boolean) => {
if (!visible) {
editIndex.value = -1
}
}
</script>
<style lang="less" scoped>
.json-param {
.list-item {
border: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
color: rgba(0, 0, 0, 0.85);
padding: 3px 6px;
margin-bottom: 10px;
background-color: #fff;
line-height: 26px;
font-size: 14px;
// .item-left {
// .item-drag {
// cursor: move;
// }
// }
.item-edit {
cursor: pointer;
}
.item-icon {
color: rgb(136, 136, 136);
font-size: 12px;
}
}
}
:deep(.ant-form-item-label) {
line-height: 1;
>label {
font-size: 12px;
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
font-size: 12px;
}
}
}
:deep(.ant-form-item-explain) {
font-size: 12px;
}
:deep(.ant-form-item-with-help) {
.ant-form-item-explain {
min-height: 20px;
line-height: 20px;
}
}
:deep(.ant-form-item) {
margin-bottom: 20px;
&.ant-form-item-with-help {
margin-bottom: 0;
}
input {
font-size: 12px;
}
}
:deep(.ant-input),
:deep(.ant-select) {
font-size: 12px;
}
:deep(input) {
height: 22px;
}
</style>

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

@ -4,14 +4,14 @@
<a-popconfirm v-bind="popConfirm" :disabled="!isPermission || props.disabled">
<a-tooltip v-if="tooltip" v-bind="tooltip">
<slot v-if="noButton"></slot>
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" :style="props.style">
<a-button v-else v-bind="props" :disabled="_isPermission" :style="props.style">
<slot></slot>
<template #icon>
<slot name="icon"></slot>
</template>
</a-button>
</a-tooltip>
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" >
<a-button v-else v-bind="props" :disabled="_isPermission" >
<slot></slot>
<template #icon>
<slot name="icon"></slot>
@ -22,7 +22,7 @@
<template v-else-if="tooltip">
<a-tooltip v-bind="tooltip">
<slot v-if="noButton"></slot>
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" :style="props.style">
<a-button v-else v-bind="props" :disabled="_isPermission" :style="props.style">
<slot></slot>
<template #icon>
<slot name="icon"></slot>
@ -32,7 +32,7 @@
</template>
<template v-else>
<slot v-if="noButton"></slot>
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" :style="props.style">
<a-button v-else v-bind="props" :disabled="_isPermission" :style="props.style">
<slot></slot>
<template #icon>
<slot name="icon"></slot>
@ -42,7 +42,7 @@
</template>
<a-tooltip v-else title="没有权限">
<slot v-if="noButton"></slot>
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" :style="props.style">
<a-button v-else v-bind="props" :disabled="_isPermission" :style="props.style">
<slot></slot>
<template #icon>
<slot name="icon"></slot>
@ -91,7 +91,7 @@ const props = defineProps({
...buttonProps()
})
const { tooltip, popConfirm, hasPermission, noButton, ..._buttonProps } = props;
// const { tooltip, popConfirm, hasPermission, noButton, ..._buttonProps } = props;
const permissionStore = usePermissionStore()
@ -103,8 +103,8 @@ const isPermission = computed(() => {
})
const _isPermission = computed(() =>
'hasPermission' in props && isPermission.value
? 'disabled' in _buttonProps
? _buttonProps.disabled as boolean
? 'disabled' in props
? props.disabled as boolean
: false
: true
)

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

@ -4,14 +4,14 @@ import store from './store'
import components from './components'
import router from './router'
import './style.less'
import 'ant-design-vue/es/notification/style/css';
// import jConmonents from 'jetlinks-ui-components'
// import 'jetlinks-ui-components/lib/style'
import jComponents from 'jetlinks-ui-components'
import 'jetlinks-ui-components/es/style.js'
import 'jetlinks-ui-components/es/style/variable.less'
const app = createApp(App)
app.use(store)
.use(router)
.use(components)
// .use(jConmonents)
.use(jComponents)
.mount('#app')

View File

@ -31,6 +31,10 @@ export default [
path: '/system/Api',
component: () => import('@/views/system/Platforms/index.vue')
},
{
path: '/account/center',
component: () => import('@/views/account/Center/index.vue')
},
// end: 测试用, 可删除

37
src/store/alarm.ts Normal file
View File

@ -0,0 +1,37 @@
import { defineStore } from "pinia";
export const useAlarmStore = defineStore('alarm',()=>{
const data = reactive({
tab: 'all',
current: {},
solveVisible: false,
logVisible: false,
defaultLevel: [],
columns: [
{
dataIndex: 'alarmConfigName',
title: '告警名称',
// hideInSearch: true,
},
{
dataIndex: 'alarmTime',
title: '告警时间',
valueType: 'dateTime',
},
{
dataIndex: 'description',
title: '说明',
// hideInSearch: true,
},
{
dataIndex: 'action',
title: '操作',
hideInSearch: true,
valueType: 'option',
},
],
})
return {
data
}
})

View File

@ -1,4 +1,5 @@
@import 'ant-design-vue/es/style/themes/default.less';
//@import 'jetlinks-ui-components/es/style/default.less';
.ellipsisFn(@num: 1, @width: 100%) {
display: -webkit-box;

View File

@ -44,6 +44,5 @@ export const SystemConst = {
REFRESH_METADATA_TABLE: 'refresh_metadata_table',
GET_METADATA: 'get_metadata',
REFRESH_DEVICE: 'refresh_device',
AMAP_KEY: 'amap_key',
VERSION_CODE: 'version_code',
}

View File

@ -0,0 +1,266 @@
<template>
<page-container>
<div class="center-container">
<div class="card">
<div class="content" style="margin-top: 0">
<div
class="content-item flex-item"
style="width: 350px; justify-content: center"
>
<img
:src="userInfo.avatar"
style="width: 140px; border-radius: 70px"
alt=""
/>
<div
style="
width: 100%;
text-align: center;
margin-top: 20px;
"
>
<a-upload
v-model:file-list="upload.fileList"
accept=".jpg,.png,.jfif,.pjp,.pjpeg,.jpeg"
:maxCount="1"
:show-upload-list="false"
:headers="{
[TOKEN_KEY]: LocalStore.get(TOKEN_KEY),
}"
:action="`${BASE_API_PATH}/file/static`"
@change="upload.changeBackUpload"
>
<a-button>
<AIcon type="UploadOutlined" />
更换头像
</a-button>
</a-upload>
</div>
</div>
<div
class="content-item flex-item"
style="flex: 1; padding: 15px 0"
>
<div class="info-card">
<p>用户名</p>
<p>{{ userInfo.username }}</p>
</div>
<div class="info-card">
<p>账号ID</p>
<p>{{ userInfo.id }}</p>
</div>
<div class="info-card">
<p>注册时间</p>
<p>{{ userInfo.createTime }}</p>
</div>
<div class="info-card">
<p>电话</p>
<p>{{ userInfo.telephone }}</p>
</div>
<div class="info-card">
<p>姓名</p>
<p>{{ userInfo.name }}</p>
</div>
<div class="info-card">
<p>角色</p>
<p>{{ userInfo.roleList.join(',') || '-' }}</p>
</div>
<div class="info-card">
<p>组织</p>
<p>{{ userInfo.orgList.join(',') || '-' }}</p>
</div>
<div class="info-card">
<p>邮箱</p>
<p>{{ userInfo.email || '-' }}</p>
</div>
</div>
<AIcon
type="EditOutlined"
class="edit"
style="right: 40px"
/>
</div>
</div>
<div class="card">
<h3>修改密码</h3>
<div class="content">
<div class="content" style="align-items: flex-end">
<lock-outlined
style="color: #1d39c4; font-size: 70px"
/>
<!-- <AIcon type="LockOutlined" /> -->
<span
style="margin-left: 5px; color: rgba(0, 0, 0, 0.55)"
>安全性高的密码可以使帐号更安全建议您定期更换密码,设置一个包含字母,符号或数字中至少两项且长度超过8位的密码</span
>
</div>
<AIcon type="EditOutlined" class="edit" />
</div>
</div>
<div class="card">
<h3>绑定三方账号</h3>
<div class="content">
<div class="account-card" v-for="item in bindList">
<img
:src="getImage(bindIcon[item.provider])"
style="height: 50px"
alt=""
/>
<div class="text">
<div v-if="item.bound">
<div>绑定名{{ item.others.name }}</div>
<div>
绑定时间{{
moment(item.bindTime).format(
'YYYY-MM-DD HH:mm:ss',
)
}}
</div>
</div>
<div v-else>{{ item.name }}未绑定</div>
</div>
<a-button v-if="item.bound">解除绑定</a-button>
<a-button v-else type="primary">立即绑定</a-button>
</div>
</div>
</div>
<div class="card">
<h3>首页视图</h3>
</div>
</div>
</page-container>
</template>
<script setup lang="ts">
import { LockOutlined } from '@ant-design/icons-vue';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { LocalStore, getImage } from '@/utils/comm';
import { useUserInfo } from '@/store/userInfo';
import { message, UploadChangeParam, UploadFile } from 'ant-design-vue';
import { getSsoBinds_api } from '@/api/account/center';
import moment from 'moment';
const userInfo = useUserInfo().$state.userInfos as any as userInfoType;
const bindList = ref<any[]>([]);
const bindIcon = {
'dingtalk-ent-app': '/notice/dingtalk.png',
'wechat-webapp': '/notice/wechat.png',
'internal-standalone': '/apply/provider1.png',
'third-party': '/apply/provider5.png',
};
const upload = reactive({
fileList: [] as any[],
uploadLoading: false,
changeBackUpload: (info: UploadChangeParam<UploadFile<any>>) => {
if (info.file.status === 'uploading') {
upload.uploadLoading = true;
} else if (info.file.status === 'done') {
info.file.url = info.file.response?.result;
upload.uploadLoading = false;
userInfo.avatar = info.file.response?.result;
} else if (info.file.status === 'error') {
console.log(info.file);
upload.uploadLoading = false;
message.error('logo上传失败请稍后再试');
}
},
});
init();
function init() {
getSsoBinds_api().then((resp: any) => {
if (resp.status === 200) bindList.value = resp.result;
});
}
type userInfoType = {
avatar: string;
createTime: number;
email: string;
id: string;
name: string;
orgList: string[];
roleList: string[];
status: number;
telephone: string;
tenantDisabled: boolean;
type: { name: string; id: string };
username: string;
};
</script>
<style lang="less" scoped>
.center-container {
background-color: #f0f2f5;
min-height: 100vh;
.card {
margin: 24px;
padding: 24px;
background-color: #fff;
position: relative;
h3 {
font-size: 22px;
&::before {
display: inline-block;
width: 3px;
height: 0.7em;
content: '';
background-color: #2f54eb;
margin: 0 8px;
}
}
.content {
display: flex;
margin-top: 24px;
.content-item {
margin-right: 24px;
.info-card {
width: 25%;
:first-child {
font-weight: bold;
}
:last-child {
color: #666363d9;
}
}
&.flex-item {
display: flex;
flex-wrap: wrap;
}
}
.edit {
position: absolute;
cursor: pointer;
top: 30px;
right: 24px;
color: #1d39c4;
}
.account-card {
margin-right: 24px;
width: 415px;
background-image: url(/images/notice/dingtalk-background.png);
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px;
.text {
display: -webkit-box;
font-size: 22px;
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
}
}
}
}
}
</style>

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

@ -1,12 +1,13 @@
<template>
<a-card>
<a-empty
v-if="!metadata || (metadata && !metadata.functions)"
style="margin-top: 100px"
v-if="!metadata || (metadata && !metadata.functions.length)"
style="margin-top: 50px"
>
<template #description>
暂无数据请配置
<a @click="emits('onJump', 'Metadata')">物模型</a>
请配置对应产品的
<!-- <a @click="emits('onJump', 'Metadata')">物模型属性功能</a> -->
<a @click="onJump">物模型属性功能</a>
</template>
</a-empty>
<template v-else>
@ -23,9 +24,12 @@
import { useInstanceStore } from '@/store/instance';
import Simple from './components/Simple.vue';
import Advance from './components/Advance.vue';
import { useMenuStore } from 'store/menu';
const menuStory = useMenuStore();
const instanceStore = useInstanceStore();
const emits = defineEmits(['onJump']);
// const emits = defineEmits(['onJump']);
const metadata = computed(() => JSON.parse(instanceStore.detail.metadata));
@ -34,6 +38,14 @@ const tabs = {
Simple,
Advance,
};
</script>
<style lang="less" scoped></style>
const onJump = () => {
menuStory.jumpPage(
'device/Product/Detail',
{
id: instanceStore.current.productId,
},
{ key: 'metadata' },
);
};
</script>

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

@ -117,6 +117,7 @@ import Modbus from './Modbus/index.vue';
import OPCUA from './OPCUA/index.vue';
import EdgeMap from './EdgeMap/index.vue';
import Parsing from './Parsing/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';
@ -148,6 +149,10 @@ const list = ref([
key: 'Metadata',
tab: '物模型',
},
{
key: 'Log',
tab: '日志管理',
},
{
key: 'Function',
tab: '设备功能',
@ -168,7 +173,8 @@ const tabs = {
Modbus,
OPCUA,
EdgeMap,
Parsing
Parsing,
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

@ -35,7 +35,6 @@
<template #card="slotProps">
<CardBox
:value="slotProps"
@click="handleClick"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:active="_selectedRowKeys.includes(slotProps.id)"
@ -272,14 +271,14 @@ const cancelSelect = () => {
_selectedRowKeys.value = [];
};
const handleClick = (dt: any) => {
if (_selectedRowKeys.value.includes(dt.id)) {
const _index = _selectedRowKeys.value.findIndex((i) => i === dt.id);
_selectedRowKeys.value.splice(_index, 1);
} else {
_selectedRowKeys.value = [..._selectedRowKeys.value, dt.id];
}
};
// const handleClick = (dt: any) => {
// if (_selectedRowKeys.value.includes(dt.id)) {
// const _index = _selectedRowKeys.value.findIndex((i) => i === dt.id);
// _selectedRowKeys.value.splice(_index, 1);
// } else {
// _selectedRowKeys.value = [..._selectedRowKeys.value, dt.id];
// }
// };
const getActions = (
data: Partial<Record<string, any>>,

View File

@ -0,0 +1,174 @@
<template>
<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.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: '请输入名称' },
{ max: 64, message: '最多可输入64个字符' },
]">
<a-input v-model:value="value.name" size="small"></a-input>
</a-form-item>
<template v-if="modelType === 'properties'">
<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>
<template v-if="modelType === 'functions'">
<a-form-item label="是否异步" name="async" :rules="[
{ required: true, message: '请选择是否异步' },
]">
<a-radio-group v-model:value="value.async">
<a-radio :value="true"></a-radio>
<a-radio :value="false"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="输入参数" name="inputs" :rules="[
{ required: true, message: '请输入输入参数' },
]">
<JsonParam v-model:value="value.inputs" :name="['inputs']"></JsonParam>
</a-form-item>
<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="[
{ max: 200, message: '最多可输入200个字符' },
]">
<a-textarea v-model:value="value.description" size="small"></a-textarea>
</a-form-item>
</template>
<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: {
type: String as PropType<'product' | 'device'>,
required: true,
default: 'product'
},
value: {
type: Object,
default: () => ({})
},
modelType: {
type: String,
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, id } = props.value
const { type } = valueType || {}
const productId = productStore.current?.id
if (!productId || !id || !type) return
const resp = await getMetadataConfig({
deviceId: productId,
metadata: {
id,
type: 'property',
dataType: type,
},
})
if (resp.status === 200) {
config.value = resp.result
}
}
onMounted(() => {
if (props.modelType === 'properties') {
asyncOtherConfig()
}
})
const changeValueType = (val: string) => {
if (props.type === 'product' && ['int', 'float', 'double', 'long', 'date', 'string', 'boolean'].includes(val)) {
props.value.expands.metrics = []
} else {
delete props.value.expands?.metrics
}
asyncOtherConfig()
}
</script>
<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

@ -2,26 +2,38 @@
<a-form-item label="来源" :name="name.concat(['source'])" v-if="type === 'product'" :rules="[
{ required: true, message: '请选择来源' },
]">
<a-select v-model:value="_value.source" :options="PropertySource" size="small" :disabled="metadataStore.model.action === 'edit'"></a-select>
<a-select v-model:value="_value.source" :options="PropertySource" size="small"
:disabled="metadataStore.model.action === 'edit'"></a-select>
</a-form-item>
<virtual-rule-param v-if="_value.source === 'rule'" v-model:value="_value.virtualRule" :name="name.concat(['virtualRule'])" :id="id" :showWindow="_value.source === 'rule'"></virtual-rule-param>
<virtual-rule-param v-if="_value.source === 'rule'" v-model:value="_value.virtualRule"
:name="name.concat(['virtualRule'])" :id="id" :showWindow="_value.source === 'rule'"></virtual-rule-param>
<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">
<config-param v-model:value="_value" :config="item" :name="name"></config-param>
</a-form-item>
</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" :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'
import MetricsParam from '@/components/Metadata/MetricsParam/index.vue'
type ValueType = Record<any, any>;
const props = defineProps({
value: {
type: Object as PropType<ValueType>,
default: () => ({})
},
type: {
type: String
@ -34,6 +46,14 @@ const props = defineProps({
id: {
type: String
},
config: {
type: Array as PropType<ValueType[]>,
default: () => ([])
},
valueType: {
type: Object,
default: () => ({})
}
})
interface Emits {
@ -41,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,93 +0,0 @@
<template>
<a-form ref="addFormRef" :model="form.model" layout="vertical">
<a-form-item label="标识" name="id" :rules="[
{ required: true, message: '请输入标识' },
{ max: 64, message: '最多可输入64个字符' },
{
pattern: /^[a-zA-Z0-9_]+$/,
message: '请输入英文或者数字或者-或者_',
},
]">
<a-input v-model:value="form.model.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="form.model.name" size="small"></a-input>
</a-form-item>
<value-type-form :name="['valueType']" v-model:value="form.model.valueType" key="property"></value-type-form>
<expands-form :name="['expands']" v-model:value="form.model.expands" :type="type" :id="form.model.id"></expands-form>
<a-form-item label="说明" name="description" :rules="[
{ max: 200, message: '最多可输入200个字符' },
]">
<a-textarea v-model:value="form.model.description" size="small"></a-textarea>
</a-form-item>
</a-form>
</template>
<script setup lang="ts" name="PropertyForm">
import { PropType } from 'vue';
import ExpandsForm from './ExpandsForm.vue';
import ValueTypeForm from './ValueTypeForm.vue'
const props = defineProps({
type: {
type: String as PropType<'product' | 'device'>,
required: true,
default: 'product'
}
})
const form = reactive({
model: {
valueType: {
expands: {}
},
expands: {}
} as any,
})
</script>
<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

@ -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"></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,24 +28,25 @@
</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="[
<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-select v-model:value="_value.fileType" :options="FileTypeList" size="small"></a-select>
</a-form-item>
</template>
<script lang="ts" setup mame="BaseForm">
import { DataTypeList, FileTypeList } from '@/views/device/data';
import { DefaultOptionType } from 'ant-design-vue/es/select';
import { DefaultOptionType, SelectValue } from 'ant-design-vue/es/select';
import { PropType } from 'vue'
import { getUnit } from '@/api/device/instance';
import { Store } from 'jetlinks-store';
@ -57,36 +55,63 @@ 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: '数据类型'
}
})
interface Emits {
(e: 'update:value', data: ValueType): void;
(e: 'changeType', data: string): void;
}
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: {}
}
}
})
@ -107,6 +132,16 @@ 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)
}
</script>
<style lang="less" scoped>
:deep(.ant-form-item-label) {

View File

@ -4,22 +4,23 @@
<template #extra>
<a-button :loading="save.loading" type="primary" @click="save.saveMetadata">保存</a-button>
</template>
<PropertyForm v-if="metadataStore.model.type === 'properties'" :type="type"></PropertyForm>
<a-form ref="formRef" :model="form.model" layout="vertical">
<BaseForm :model-type="metadataStore.model.type" :type="type" v-model:value="form.model"></BaseForm>
</a-form>
</a-drawer>
</template>
<script lang="ts" setup name="Edit">
import { useInstanceStore } from '@/store/instance';
import { useMetadataStore } from '@/store/metadata';
import { useProductStore } from '@/store/product';
import { MetadataItem, ProductItem } from '@/views/device/Product/typings';
import { ProductItem } from '@/views/device/Product/typings';
import { message } from 'ant-design-vue/es';
import type { FormInstance } from 'ant-design-vue/es';
import { updateMetadata, asyncUpdateMetadata } from '../../metadata'
import { Store } from 'jetlinks-store';
import { SystemConst } from '@/utils/consts';
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({
@ -32,6 +33,11 @@ const props = defineProps({
type: String
}
})
interface Emits {
(e: 'refresh'): void;
}
const emit = defineEmits<Emits>()
const route = useRoute()
const instanceStore = useInstanceStore()
@ -50,7 +56,14 @@ const close = () => {
const title = computed(() => metadataStore.model.action === 'add' ? '新增' : '修改')
const addFormRef = ref<FormInstance>()
const form = reactive({
model: {} as any,
})
if (metadataStore.model.action === 'edit') {
form.model = metadataStore.model.item
}
const formRef = ref<FormInstance>()
/**
* 保存按钮
*/
@ -58,7 +71,7 @@ const save = reactive({
loading: false,
saveMetadata: (deploy?: boolean) => {
save.loading = true
addFormRef.value?.validateFields().then(async (formValue) => {
formRef.value?.validateFields().then(async (formValue) => {
const type = metadataStore.model.type
const _detail: ProductItem | DeviceInstance = props.type === 'device' ? instanceStore.detail : productStore.current
const _metadata = JSON.parse(_detail?.metadata || '{}')
@ -80,6 +93,7 @@ const save = reactive({
detail.metadata = metadata
productStore.setCurrent(detail)
}
emit('refresh')
}
const _data = updateMetadata(type, [formValue], _detail, updateStore)
const result = await asyncUpdateMetadata(props.type, _data)
@ -90,8 +104,9 @@ const save = reactive({
setTimeout(() => window.close(), 300);
}
} else {
Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
// Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
if (deploy) {
// TODO
Store.set('product-deploy', deploy);
} else {
save.resetMetadata();
@ -111,6 +126,7 @@ const save = reactive({
}
save.loading = false
})
save.loading = false
},
resetMetadata: async () => {
const { id } = route.params
@ -121,10 +137,45 @@ const save = reactive({
}
})
const form = reactive({
model: {} as Record<string, any>
})
</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,11 +9,11 @@
title: operateLimits('add', type) ? '当前的存储方式不支持新增' : '新增',
}">
<template #icon>
<PlusOutlined />
<AIcon type="PlusOutlined" />
</template>
新增
</PermissionButton>
<Edit v-if="metadataStore.model.edit" :type="target" :tabs="type"></Edit>
<Edit v-if="metadataStore.model.edit" :type="target" :tabs="type" @refresh="refreshMetadata"></Edit>
</template>
<template #level="slotProps">
{{ levelMap[slotProps.expands?.level] || '-' }}
@ -33,11 +33,12 @@
</a-tag>
</template>
<template #action="slotProps">
<a-space>
<PermissionButton :uhas-permission="`${permission}:update`" type="link" key="edit" style="padding: 0"
: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="{
@ -47,8 +48,9 @@
}" :tooltip="{
title: '删除',
}">
<DeleteOutlined />
<Aicon type="DeleteOutlined" />
</PermissionButton>
</a-space>
</template>
</JTable>
</template>
@ -60,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'
@ -122,7 +123,8 @@ onMounted(() => {
})
watch([route.params.id, type], () => {
const refreshMetadata = () => {
loading.value = true
// const res = target === 'product'
// ? await productDetail(route.params.id as string)
// : await detail(route.params.id as string);
@ -130,7 +132,8 @@ watch([route.params.id, type], () => {
const item = JSON.parse(result || '{}') as MetadataItem[]
data.value = item[type]?.sort((a: any, b: any) => b?.sortsIndex - a?.sortsIndex)
loading.value = false
}, { immediate: true })
}
watch([route.params.id, type], refreshMetadata, { immediate: true })
const metadataStore = useMetadataStore()
const handleAddClick = () => {

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

@ -10,7 +10,7 @@
:class="{ selected: selectValue === 'device' }"
@click="selectValue = 'device'"
>
<img src="/images/home/device.png" alt="" />
<img :src="getImage('/home/device.png')" alt="" />
</a-col>
<a-col
:span="8"
@ -18,7 +18,7 @@
:class="{ selected: selectValue === 'ops' }"
@click="selectValue = 'ops'"
>
<img src="/images/home/ops.png" alt="" />
<img :src="getImage('/home/ops.png')" alt="" />
</a-col>
<a-col
:span="8"
@ -26,7 +26,7 @@
:class="{ selected: selectValue === 'comprehensive' }"
@click="selectValue = 'comprehensive'"
>
<img src="/images/home/comprehensive.png" alt="" />
<img :src="getImage('/home/comprehensive.png')" alt="" />
</a-col>
</a-row>
<a-button type="primary" class="btn" @click="confirm"
@ -38,6 +38,7 @@
<script lang="ts" setup>
import { setView_api } from '@/api/home';
import { getImage } from '@/utils/comm';
const emits = defineEmits(['refresh']);
const selectValue = ref('device');

View File

@ -1,25 +1,52 @@
<template>
<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
v-else-if="currentView === 'api'"
:mode="'home'"
hasHome
showTitle
:code="clientId"
>
<template #top>
<div class="card">
<h3 style="margin: 0 0 24px 0">基本信息</h3>
<p>
<span style="font-weight: bold">clientId: </span>
<span>{{ clientId }}</span>
</p>
<p>
<span style="font-weight: bold">secureKey:</span>
<span>{{ secureKey }}</span>
</p>
</div>
</template>
</Api>
</div>
</page-container>
</template>
<script lang="ts" setup>
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 { useUserInfo } from '@/store/userInfo';
import { isNoCommunity } from '@/utils/utils';
import { getMe_api, getView_api } from '@/api/home';
const router = useRouter();
import { getAppInfo_api } from '@/api/system/apply';
const currentView = ref<string>('');
const loading = ref<boolean>(true);
const clientId = useUserInfo().$state.userInfos.id;
const secureKey = ref<string>('');
//
const setCurrentView = () => {
@ -42,7 +69,12 @@ if (isNoCommunity) {
item.type === 'api-client' || item.type.id === 'api-client',
);
isApiUser ? router.push('/system/api') : setCurrentView();
if (isApiUser) {
currentView.value = 'api';
getAppInfo_api(clientId).then((resp: any) => {
secureKey.value = resp.result.apiServer.secureKey;
});
} else setCurrentView();
}
});
} else setCurrentView();
@ -50,7 +82,15 @@ if (isNoCommunity) {
<style lang="less" scoped>
.iot-home-container {
background: #f0f2f5;
overflow: hidden;
.card {
background-color: #fff;
padding: 24px;
margin-bottom: 24px;
p {
margin: 0;
font-size: 16px;
}
}
}
</style>

View File

@ -278,6 +278,7 @@ import { ValidateErrorEntity } from 'ant-design-vue/es/form/interface';
import { message } from 'ant-design-vue';
import { LocalStore } from '@/utils/comm';
import { TOKEN_KEY } from '@/utils/variable';
import { SystemConst } from '@/utils/consts'
const formRef = ref();
const menuRef = ref();
const formBasicRef = ref();
@ -352,6 +353,7 @@ const saveBasicInfo = () =>{
const res = await save(item);
if (res.status === 200) {
resolve(true);
localStorage.setItem(SystemConst.AMAP_KEY,form.value.apikey);
const ico: any = document.querySelector('link[rel="icon"]');
if (ico !== null) {
ico.href = form.value.ico;

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,7 +110,6 @@
<a-row :gutter="[24, 24]">
<a-col :span="12">
<title-component data="基本信息" />
<div>
<a-form
:model="formState"
name="basic"
@ -139,15 +145,18 @@
/>
</a-form-item>
<a-form-item>
<a-button
<PermissionButton
v-if="current !== 1 && view === 'false'"
type="primary"
html-type="submit"
>保存</a-button
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</PermissionButton>
</a-form-item>
</a-form>
</div>
</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,7 +341,7 @@ onMounted(() => {
};
networkCurrent.value = props.data.channelId;
}
}),
});
watch(
current,
(v) => {

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,24 +47,6 @@ 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);
};
@ -73,6 +55,8 @@ const click = (value: object) => {
<style lang="less" scoped>
.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,30 +134,14 @@
</template>
<template #actions="item">
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
>
<a-button :disabled="item.disabled">
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</template>
</a-button>
</a-popconfirm>
<template v-else>
<a-button
<PermissionButton
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
:hasPermission="'link/AccessConfig:' + item.key"
>
<AIcon
type="DeleteOutlined"
@ -169,11 +149,9 @@
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
<span>{{ item?.text }}</span>
</template>
</a-button>
</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,761 @@
<!-- 国标级联新增/编辑 -->
<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 地址',
},
{
validator: checkSIP,
},
]"
>
<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本地地址',
},
{
validator: checkLocalSIP,
},
]"
>
<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="请选择端口"
:options="allListPorts"
/>
</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远程地址',
},
{
validator: checkPublicSIP,
},
]"
>
<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"
@change="setPorts"
>
<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 { message } from 'ant-design-vue';
import CascadeApi from '@/api/media/cascade';
const router = useRouter();
const route = useRoute();
//
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<any[]>([]);
const getAllList = async () => {
const { result } = await CascadeApi.all();
allList.value = result.map((m: any) => ({
label: m.host,
value: m.host,
}));
setPorts();
};
getAllList();
/**
* 传输协议改变, 获取对应的端口
*/
const allListPorts = ref([]);
const setPorts = () => {
allListPorts.value = allList.value.find(
(f: any) => f.host === formData.value.host,
)?.ports[formData.value.transport || ''];
};
/**
* 获取详情
*/
const getDetail = async () => {
if (!route.query.id) return;
const res = await CascadeApi.detail(route.query.id as string);
const { id, name, proxyStream, sipConfigs, ...others } = res.result;
Object.keys(formData.value).forEach((key: string) => {
if (key === 'id') formData.value[key] = id;
else if (key === 'cascadeName') formData.value[key] = name;
else if (key === 'proxyStream') formData.value[key] = proxyStream;
else formData.value[key] = sipConfigs[0][key];
});
// console.log('formData.value: ', formData.value);
};
onMounted(() => {
getDetail();
});
const regDomain =
/[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?/;
/**
* 上级SIP地址 字段验证
* @param _
* @param value 此处绑定的是 remoteAddress
*/
const checkSIP = (_: any, value: string) => {
return checkHost(value, formData.value.remotePort);
};
/**
* SIP远程地址 字段验证
* @param _
* @param value 此处绑定的是 publicHost
*/
const checkPublicSIP = (_: any, value: string) => {
return checkHost(value, formData.value.publicPort);
};
/**
* 字段验证
* @param host ip
* @param port 端口
*/
const checkHost = (host: string, port: string | number | undefined) => {
if (!host) {
return Promise.resolve();
} else if (!host) {
return Promise.reject(new Error('请输入IP 地址'));
} else if (host && !regDomain.test(host)) {
return Promise.reject(new Error('请输入正确的IP地址'));
} else if (!port) {
return Promise.reject(new Error('请输入端口'));
} else if ((host && Number(host) < 1) || Number(host) > 65535) {
return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
}
return Promise.resolve();
};
/**
* SIP本地地址 字段验证
* @param _
* @param value
*/
const checkLocalSIP = (_: any, value: string) => {
if (!value) {
return Promise.resolve();
} else if (!value) {
return Promise.reject(new Error('请选择IP地址'));
} else if (!formData.value.port) {
return Promise.reject(new Error('请选择端口'));
}
return Promise.resolve();
};
/**
* 表单提交
*/
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

@ -0,0 +1,267 @@
<!-- Modal 弹窗用于新增修改数据 -->
<template>
<a-modal
v-model:visible="_vis"
:title="!!formData.id ? '编辑' : '新增'"
width="650px"
cancelText="取消"
okText="确定"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form ref="formRef" :model="formData" layout="vertical">
<a-row :gutter="10">
<a-col :span="12">
<a-form-item
name="channelId"
:rules="[
{
max: 64,
message: '最多可输入64个字符',
},
{
validator: validateChannelId,
},
]"
>
<template #label>
通道ID
<a-tooltip title="若不填写系统将自动生成唯一ID">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</template>
<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: '请输入通道名称' },
{ max: 64, message: '最多可输入64个字符' },
]"
>
<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: '请输入视频地址' },
{ max: 128, message: '最多可输入128个字符' },
]"
>
<template #label>
视频地址
<a-tooltip
title="不同厂家的RTSP固定地址规则不同请按对应厂家的规则填写"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</template>
<a-input
v-model:value="formData.media_url"
placeholder="请输入视频地址"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<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.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"
placeholder="请输入安装地址"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item name="description" label="说明">
<a-textarea
v-model:value="formData.description"
:rows="4"
:maxlength="200"
showCount
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
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({
visible: { type: Boolean, default: false },
channelData: {
type: Object as PropType<Partial<Record<string, any>>>,
default: () => ({}),
},
});
const _vis = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val),
});
const formRef = ref();
const formData = ref({
id: undefined,
address: '',
channelId: '',
description: '',
deviceId: route.query.id,
name: '',
// , others
media_password: '',
media_url: '',
media_username: '',
});
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 () => {
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);
});
};
const handleCancel = () => {
_vis.value = false;
};
watch(
() => _vis.value,
(val) => {
if (!val) {
formRef.value.resetFields();
// ,
formData.value.id = undefined;
}
},
);
</script>

View File

@ -1,3 +1,4 @@
<!-- 视频设备-通道列表 -->
<template>
<page-container>
<Search
@ -8,106 +9,137 @@
/>
<JTable
ref="instanceRef"
ref="listRef"
:columns="columns"
:request="(e:any) => templateApi.getHistory(e, data.id)"
:request="(e:any) => ChannelApi.list(e, route?.query.id as string)"
:defaultParams="{
sorts: [{ name: 'notifyTime', order: 'desc' }],
terms: [{ column: 'notifyType$IN', value: data.type }],
}"
:params="params"
model="table"
>
<template #notifyTime="slotProps">
{{ moment(slotProps.notifyTime).format('YYYY-MM-DD HH:mm:ss') }}
<template #headerTitle>
<a-tooltip
v-if="route?.query.type === 'gb28181-2016'"
title="接入方式为GB/T28281时不支持新增"
>
<a-button type="primary" disabled> 新增 </a-button>
</a-tooltip>
<a-button type="primary" @click="handleAdd" v-else>
新增
</a-button>
</template>
<template #state="slotProps">
<template #status="slotProps">
<a-space>
<a-badge
:status="slotProps.state.value"
:text="slotProps.state.text"
:status="
slotProps.status.value === 'online'
? 'success'
: 'error'
"
:text="slotProps.status.text"
></a-badge>
<AIcon
v-if="slotProps.state.value === 'error'"
type="ExclamationCircleOutlined"
style="color: #1d39c4; cursor: pointer"
@click="handleError(slotProps.errorStack)"
/>
</a-space>
</template>
<template #action="slotProps">
<AIcon
type="ExclamationCircleOutlined"
style="color: #1d39c4; cursor: pointer"
@click="handleDetail(slotProps.context)"
/>
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
:disabled="i.disabled"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
</template>
</JTable>
<Save
v-model:visible="saveVis"
:channelData="channelData"
@submit="listRef.reload()"
/>
</page-container>
</template>
<script setup lang="ts">
import templateApi from '@/api/notice/template';
import { PropType } from 'vue';
import moment from 'moment';
import { Modal } from 'ant-design-vue';
import ChannelApi from '@/api/media/channel';
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';
type Emits = {
(e: 'update:visible', data: boolean): void;
};
// const emit = defineEmits<Emits>();
const props = defineProps({
visible: { type: Boolean, default: false },
data: {
type: Object as PropType<Partial<Record<string, any>>>,
default: () => ({}),
},
});
// const _vis = computed({
// get: () => props.visible,
// set: (val) => emit('update:visible', val),
// });
// watch(
// () => _vis.value,
// (val) => {
// if (val) handleSearch({ terms: [] });
// },
// );
const menuStory = useMenuStore();
const route = useRoute();
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
title: '通道ID',
dataIndex: 'channelId',
key: 'channelId',
search: {
type: 'string',
},
},
{
title: '发送时间',
dataIndex: 'notifyTime',
key: 'notifyTime',
scopedSlots: true,
title: '名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'date',
handleValue: (v: any) => {
return v;
type: 'string',
},
},
{
title: '厂商',
dataIndex: 'manufacturer',
key: 'manufacturer',
search: {
type: 'string',
},
},
{
title: '安装地址',
dataIndex: 'address',
key: 'address',
search: {
type: 'string',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
dataIndex: 'status',
key: 'status',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '成功', value: 'success' },
{ label: '失败', value: 'error' },
{ label: '已连接', value: 'online' },
{ label: '未连接', value: 'offline' },
],
handleValue: (v: any) => {
return v;
@ -128,47 +160,94 @@ 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 handleError = (e: any) => {
Modal.info({
title: '错误信息',
content: h(
'p',
{
style: {
maxHeight: '300px',
overflowY: 'auto',
},
},
JSON.stringify(e),
),
});
const saveVis = ref(false);
const handleAdd = () => {
saveVis.value = true;
};
const listRef = ref();
const playVis = ref(false);
const channelData = ref();
/**
* 查看详情
* 表格操作按钮
* @param data 表格数据项
* @param type 表格展示类型
*/
const handleDetail = (e: any) => {
Modal.info({
title: '详情信息',
content: h(
'p',
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
const actions = [
{
style: {
maxHeight: '300px',
overflowY: 'auto',
key: 'edit',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
channelData.value = cloneDeep(data);
saveVis.value = true;
},
},
JSON.stringify(e),
),
});
{
key: 'play',
text: '播放',
tooltip: {
title: '播放',
},
icon: 'VideoCameraOutlined',
onClick: () => {
playVis.value = true;
},
},
{
key: 'backPlay',
text: '回放',
tooltip: {
title: '回放',
},
icon: 'HistoryOutlined',
onClick: () => {
menuStory.jumpPage(
'media/Device/Playback',
{},
{
id: route.query.id,
type: route.query.type,
channelId: data.channelId,
},
);
},
},
{
key: 'delete',
text: '删除',
tooltip: {
title: '删除',
},
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const resp = await ChannelApi.del(data.id);
if (resp.status === 200) {
message.success('操作成功!');
listRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
return route?.query.type === 'gb28181-2016'
? actions.filter((f) => f.key !== 'delete')
: actions;
};
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<page-container> 回放 </page-container>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,15 @@
export type recordsItemType = {
channelId: string;
deviceId: string;
endTime: number;
fileSize: number;
name: string;
secrecy: string;
startTime: number;
mediaEndTime: number;
mediaStartTime: number;
filePath: string;
type: string;
id: string;
isServer?: boolean;
};

View File

@ -55,8 +55,8 @@
:status="item.state?.value"
:statusText="item.state?.text"
:statusNames="{
online: 'enabled',
offline: 'disabled',
enabled: 'processing',
disabled: 'error',
}"
>
<template #img>

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

@ -670,17 +670,21 @@
</div>
</a-form-item>
</template>
<a-form-item
<template
v-if="
formData.type !== 'webhook' &&
formData.type !== 'voice'
"
>
<a-form-item
v-bind="validateInfos['template.message']"
>
<template #label>
<span>
模版内容
<a-tooltip title="发送的内容,支持录入变量">
<a-tooltip
title="发送的内容,支持录入变量"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
@ -697,6 +701,7 @@
示例:尊敬的${name},${time}有设备触发告警,请注意处理"
/>
</a-form-item>
</template>
<a-form-item
label="变量列表"
v-if="
@ -804,6 +809,7 @@ const formData = ref<TemplateFormData>({
* 重置字段值
*/
const resetPublicFiles = () => {
formData.value.template = {};
switch (formData.value.provider) {
case 'dingTalkMessage':
formData.value.template.agentId = '';
@ -854,6 +860,7 @@ const resetPublicFiles = () => {
formData.value.configId = undefined;
formData.value.variableDefinitions = [];
handleMessageTypeChange();
// console.log('formData.value.template: ', formData.value.template);
};
//
@ -1049,7 +1056,7 @@ const handleMessageTypeChange = () => {
};
}
formData.value.variableDefinitions = [];
formData.value.template.message = '';
// formData.value.template.message = '';
};
/**
@ -1085,7 +1092,6 @@ const handleTypeChange = () => {
setTimeout(() => {
formData.value.template =
TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider];
// console.log('formData.value.template: ', formData.value.template);
resetPublicFiles();
}, 0);
};

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

@ -9,6 +9,7 @@
<JTable
:columns="columns"
:request="queryList"
:gridColumn="3"
ref="tableRef"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
@ -42,7 +43,7 @@
</slot>
</template>
<template #content>
<Ellipsis>
<Ellipsis style="width: calc(100% - 100px)">
<span style="font-weight: 600; font-size: 16px">
{{ slotProps.name }}
</span>
@ -70,35 +71,14 @@
</a-row>
</template>
<template #actions="item">
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
<PermissionButton
v-if="
item.key != 'trigger' ||
slotProps.sceneTriggerType == 'manual'
"
>
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
okText="确定"
cancelText="取消"
>
<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>
<template v-else>
<a-button
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{ ...item.tootip }"
@click="item.onClick"
>
<AIcon
@ -109,9 +89,7 @@
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</template>
</a-tooltip>
</PermissionButton>
</template>
</CardBox>
</template>
@ -151,45 +129,29 @@
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps)"
<template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
v-bind="i.tooltip"
>
<span
<PermissionButton
v-if="
i.key != 'trigger' ||
slotProps.sceneTriggerType == 'manual'
"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
okText="确定"
cancelText="取消"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
:popConfirm="i.popConfirm"
:tooltip="{
...i.tooltip,
}"
@click="i.onClick"
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)"
style="padding: 0px"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
<template #icon
><AIcon :type="i.icon"
/></a-button>
</a-button>
</span>
</a-tooltip>
/></template>
</PermissionButton>
</template>
</a-space>
</template>
</JTable>
@ -214,7 +176,6 @@ import { message } from 'ant-design-vue';
import { getImage } from '@/utils/comm';
import { useMenuStore } from '@/store/menu';
import encodeQuery from '@/utils/encodeQuery';
import { useStorage } from '@vueuse/core';
const params = ref<Record<string, any>>({});
let isAdd = ref<number>(0);
let title = ref<string>('');
@ -291,7 +252,10 @@ const columns = [
}),
);
if (res.status === 200) {
return res.result.map((item:any) => ({label:item.name, value:item.id}))
return res.result.map((item: any) => ({
label: item.name,
value: item.id,
}));
}
return [];
},
@ -322,7 +286,7 @@ const columns = [
key: 'description',
search: {
type: 'string',
}
},
},
{
title: '操作',
@ -396,7 +360,11 @@ const getActions = (
icon: 'EditOutlined',
onClick: () => {
menuStory.jumpPage('rule-engine/Alarm/Configuration/Save',{},{id:data.id});
menuStory.jumpPage(
'rule-engine/Alarm/Configuration/Save',
{},
{ id: data.id },
);
},
},
{
@ -456,8 +424,6 @@ const getActions = (
icon: 'DeleteOutlined',
},
];
if (type === 'card')
return actions.filter((i: ActionsType) => i.key !== 'view');
return actions;
};
const add = () => {

View File

@ -0,0 +1,38 @@
<template>
<page-container>
<Search :columns="columns" target="alarm-log-detail"></Search>
<JTable :columns="columns" model="TABLE" :request="queryList"></JTable>
</page-container>
</template>
<script lang="ts" setup>
const columns = [{
title:'告警时间',
dataIndex:'alarmTime',
key:'alarmTime',
search:{
type:'date'
}
},{
title:'告警名称',
dataIndex:'alarmConfigName',
key:'alarmConfigName',
},{
title:'说明',
dataIndex:'description',
key:'description'
},{
title:'操作',
dataIndex:'action',
key:'action'
}]
/**
* 获取详情列表
*/
const queryList = () =>{
}
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,79 @@
<template>
<a-modal
title="告警处理"
okText="确定"
cancelText="取消"
visible
@cancel="handleCancel"
@ok="handleSave"
destroyOnClose
:confirmLoading="loading"
>
<a-form :rules="rules" layout="vertical" ref="formRef" :model="form">
<a-form-item label="处理结果" name="describe">
<a-textarea
:rows="8"
:maxlength="200"
showCount
placeholder="请输入处理结果"
v-model:value="form.describe"
></a-textarea>
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { handleLog } from '@/api/rule-engine/log';
import { onlyMessage } from '@/utils/comm';
const props = defineProps({
data: {
type: Object,
},
});
const loading = ref<boolean>(false);
const formRef = ref();
const rules = {
describe: [
{
required: true,
message: '请输入处理结果',
},
],
};
const form = reactive({
describe: '',
});
let visible = ref(true);
const emit = defineEmits(['closeSolve'])
const handleCancel = () => {
emit('closeSolve');
};
const handleSave = () => {
loading.value = true;
formRef.value
.validate()
.then(async () => {
const res = await handleLog({
describe: form.describe,
type: 'user',
state: 'normal',
alarmRecordId: props.data?.current?.id || '',
alarmConfigId: props.data?.current?.alarmConfigId || '',
alarmTime: props?.data?.current?.alarmTime || '',
});
if (res.status === 200) {
onlyMessage('操作成功!');
} else {
onlyMessage('操作失败!', 'error');
}
loading.value = false;
})
.catch((error) => {
console.log(error);
loading.value = false;
});
};
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,402 @@
<template>
<div class="alarm-log-card">
<Search
:columns="columns"
target="alarm-log"
v-if="['all', 'detail'].includes(props.type)"
@search="search"
></Search>
<Search
:columns="produtCol"
target="alarm-log"
v-if="['product', 'other'].includes(props.type)"
@search="search"
></Search>
<Search
:columns="deviceCol"
target="alarm-log"
v-if="props.type === 'device'"
@search="search"
></Search>
<Search
:columns="orgCol"
target="alarm-log"
v-if="props.type === 'org'"
@search="search"
></Search>
<JTable
:columns="columns"
:request="handleSearch"
:params="params"
:gridColumn="2"
model="CARD"
>
<template #card="slotProps">
<CardBox
:value="slotProps"
v-bind="slotProps"
:actions="getActions(slotProps, 'card')"
:statusText="
data.defaultLevel.find(
(i) => i.level === slotProps.level,
)?.title || slotProps.level
"
>
<template #img>
<img :src="imgMap.get(slotProps.targetType)" alt="" />
</template>
<template #content>
<Ellipsis style="width: calc(100% - 100px)">
<span style="font-weight: 500">
{{ slotProps.alarmName }}
</span>
</Ellipsis>
<a-row :gutter="24">
<a-col :span="8">
<div class="content-des-title">
{{ titleMap.get(slotProps.targetType) }}
</div>
<Ellipsis
><div>
{{ slotProps?.targetName }}
</div></Ellipsis
>
</a-col>
<a-col :span="8">
<div class="content-des-title">
最近告警时间
</div>
<Ellipsis
><div>
{{
moment(slotProps?.alarmTime).format(
'YYYY-MM-DD HH:mm:ss',
)
}}
</div></Ellipsis
>
</a-col>
<a-col :span="8">
<div class="content-des-title">状态</div>
<a-badge
:status="
slotProps.state.value === 'warning'
? 'error'
: 'default'
"
>
</a-badge
><span
:style="
slotProps.state.value === 'warning'
? 'color: #E50012'
: 'color:black'
"
>
{{ slotProps.state.text }}
</span>
</a-col>
</a-row>
</template>
<template #actions="item">
<PermissionButton
:disabled="item.key === 'solve' && slotProps.state.value ==='normal'"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</PermissionButton>
</template>
</CardBox>
</template>
</JTable>
<SolveLog :data="data" v-if="data.solveVisible" @closeSolve="closeSolve"/>
</div>
</template>
<script lang="ts" setup>
import { getImage } from '@/utils/comm';
import {
getProductList,
getDeviceList,
getOrgList,
query,
} from '@/api/rule-engine/log';
import { queryLevel } from '@/api/rule-engine/config';
import Search from '@/components/Search';
import { useAlarmStore } from '@/store/alarm';
import { storeToRefs } from 'pinia';
import { Store } from 'jetlinks-store';
import moment from 'moment';
import type { ActionsType } from '@/components/Table';
import SolveLog from '../SolveLog/index.vue'
import { useMenuStore } from '@/store/menu';
const menuStory = useMenuStore();
const alarmStore = useAlarmStore();
const { data } = storeToRefs(alarmStore);
const getDefaulitLevel = () => {
queryLevel().then((res) => {
if (res.status === 200) {
Store.set('default-level', res.result?.levels || []);
data.value.defaultLevel = res.result?.levels || [];
}
});
};
getDefaulitLevel();
const props = defineProps<{
type: string;
id?: string;
}>();
const imgMap = new Map();
imgMap.set('product', getImage('/alarm/product.png'));
imgMap.set('device', getImage('/alarm/device.png'));
imgMap.set('other', getImage('/alarm/other.png'));
imgMap.set('org', getImage('/alarm/org.png'));
const titleMap = new Map();
titleMap.set('product', '产品');
titleMap.set('device', '设备');
titleMap.set('other', '其他');
titleMap.set('org', '组织');
const colorMap = new Map();
colorMap.set(1, '#E50012');
colorMap.set(2, '#FF9457');
colorMap.set(3, '#FABD47');
colorMap.set(4, '#999999');
colorMap.set(5, '#C4C4C4');
const columns = [
{
title: '名称',
dataIndex: 'alarmName',
key: 'alarmName',
search: {
type: 'string',
},
},
{
title: '最近告警时间',
dataIndex: 'alarmTime',
key: 'alarmTime',
search: {
type: 'date',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
search: {
type: 'select',
options: [
{
label: '告警中',
value: 'warning',
},
{
label: '无告警',
value: 'normal',
},
],
},
},
];
const produtCol = [
...columns,
{
title: '产品名称',
dataIndex: 'targetName',
key: 'targetName',
search: {
type: 'select',
options: async () => {
const resq = await getProductList();
if (resq.status === 200) {
return resq.result.map((item: any) => ({
label: item.name,
value: item.name,
}));
}
return [];
},
},
},
];
const deviceCol = [
...columns,
{
title: '设备名称',
dataIndex: 'targetName',
key: 'targetName',
search: {
type: 'select',
opstions: async () => {
const res = await getDeviceList();
if (res.status === 200) {
return res.result.map((item: any) => ({
label: item.name,
value: item.name,
}));
}
return [];
},
},
},
];
const orgCol = [
...columns,
{
title: '组织名称',
dataIndex: 'targetName',
key: 'targetName',
search: {
type: 'select',
options: async () => {
const res = await getOrgList();
if (res.status === 200) {
return res.result.map((item: any) => ({
label: item.name,
value: item.name,
}));
}
return [];
},
},
},
];
let params = ref({
sorts: [{ name: 'alarmTime', order: 'desc' }],
terms: [],
});
let param = reactive({
pageSize: 10,
terms: [],
});
const handleSearch = async (params: any) => {
const resp = await query(params);
if (resp.status === 200) {
const res = await getOrgList();
if (res.status === 200) {
resp.result.data.map((item: any) => {
if (item.targetType === 'org') {
res.result.forEach((item2: any) => {
if (item2.id === item.targetId) {
item.targetName = item2.name;
}
//targetName
if (item.targetId === item.targetName) {
item.targetName = '无';
}
});
}
});
return resp;
}
}
};
watchEffect(() => {
if (props.type !== 'all' && !props.id) {
params.value.terms = [
{
termType: 'eq',
column: 'targetType',
value: props.type,
type: 'and',
},
];
}
if (props.id) {
params.value.terms = [
{
termType: 'eq',
column: 'alarmConfigId',
value: props.id,
type: 'and',
},
];
}
if(props.type === 'all'){
params.value.terms = [];
}
});
const search = (data: any) => {
params.value.terms = [...data?.terms];
if (props.type !== 'all' && !props.id) {
params.value.terms.push(
{
termType: 'eq',
column: 'targetType',
value: props.type,
type: 'and',
},
);
}
if (props.id) {
params.value.terms.push (
{
termType: 'eq',
column: 'alarmConfigId',
value: props.id,
type: 'and',
},
);
}
};
const getActions = (
currentData: Partial<Record<string, any>>,
type: 'card',
): ActionsType[] => {
if (!currentData) return [];
const actions = [
{
key: 'solve',
text: '告警处理',
tooltip: {
title: '告警处理',
},
icon: 'ToolOutlined',
onClick: () =>{
data.value.current = currentData;
data.value.solveVisible = true;
}
},
{
key: 'log',
text: '告警日志',
tooltip: {
title: '告警日志',
},
icon: 'FileOutlined',
onClick: () =>{
menuStory.jumpPage(`rule-engine/Alarm/Log/Detail`,{id:currentData.id});
}
},
{
key: 'detail',
text: '处理记录',
tooltip: {
title: '处理记录',
},
icon: 'FileTextOutlined',
},
];
return actions;
};
const closeSolve = () =>{
data.value.solveVisible = false
}
</script>
<style lang="less" scoped>
</style>

View File

@ -1,9 +1,70 @@
<template>
<div></div>
<page-container :tabList="isNoCommunity ? list : noList" :tabActiveKey="data.tab" @tabChange="onTabChange">
<TableComponents :type="data.tab"></TableComponents>
</page-container>
</template>
<script lang="ts" setup>
import { isNoCommunity } from '@/utils/utils';
import { useAlarmStore } from '@/store/alarm';
import { storeToRefs } from 'pinia';
import { queryLevel } from '@/api/rule-engine/config';
import { Store } from 'jetlinks-store';
import TableComponents from './TabComponent/indev.vue';
const list = [
{
key: 'all',
tab: '全部',
},
{
key: 'product',
tab: '产品',
},
{
key: 'device',
tab: '设备',
},
{
key: 'org',
tab: '组织',
},
{
key: 'other',
tab: '其他',
},
];
const noList = [
{
key: 'all',
tab: '全部',
},
{
key: 'product',
tab: '产品',
},
{
key: 'device',
tab: '设备',
},
{
key: 'other',
tab: '其他',
},
];
const alarmStore = useAlarmStore();
const { data } = storeToRefs(alarmStore);
const getDefaulitLevel = () => {
queryLevel().then((res)=>{
if(res.status === 200 ){
Store.set('default-level', res.result?.levels || []);
data.value.defaultLevel = res.result?.levels || [];
}
})
}
getDefaulitLevel();
const onTabChange = (key:string) =>{
data.value.tab = key;
}
</script>
<style lang="less" scoped>
</style>

View File

@ -40,7 +40,7 @@
</slot>
</template>
<template #content>
<Ellipsis>
<Ellipsis style="width: calc(100% - 100px)">
<span style="font-weight: 600; font-size: 16px">
{{ slotProps.name }}
</span>
@ -56,31 +56,12 @@
</a-row>
</template>
<template #actions="item">
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
okText="确定"
cancelText="取消"
>
<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>
<template v-else>
<a-button
<PermissionButton
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
>
<AIcon
@ -91,9 +72,7 @@
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</template>
</a-tooltip>
</PermissionButton>
</template>
</CardBox>
</template>
@ -113,38 +92,26 @@
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps)"
<template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
okText="确定"
cancelText="取消"
>
<a-button
<PermissionButton
:disabled="i.disabled"
style="padding: 0"
:popConfirm="i.popConfirm"
:tooltip="{
...i.tooltip,
}"
@click="i.onClick"
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)"
style="padding: 0px"
:hasPermission="'device/Instance:' + i.key"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
<template #icon
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
/></template>
</PermissionButton>
</template>
</a-space>
</template>
</JTable>

View File

@ -0,0 +1,106 @@
<template>
<a-row :gutter='24'>
<a-col :span='10'>
<a-form-item
name='functionId'
:rules="[{ required: true, message: '请选择功能' }]"
>
<a-select
showSearch
allowClear
v-model='functionId'
style='width: 100%'
placeholder='请选择功能'
:filterOption='filterSelectNode'
@select='onSelect'
/>
</a-form-item>
</a-col>
<a-col :span='14'>
<a-form-item>定时调用所选功能</a-form-item>
</a-col>
<a-col :span='24'>
<a-form-item
style='margin-top: 24px'
name='functionParameters'
>
<FunctionCall
v-model:value='_value'
:data='callDataOptions'
@change='callDataChange'
/>
</a-form-item>
</a-col>
</a-row>
</template>
<script setup lang='ts' name='InvokeFunction'>
import { filterSelectNode } from '@/utils/comm'
import { FunctionCall } from '../components'
type Emit = {
(e: 'update:value', data: Record<string, any>): void
(e: 'update:action', data: string): void
}
const props = defineProps({
value: {
type: Object,
default: () => ({})
},
action: {
type: String,
default: ''
},
functions: {
type: Array,
default: () => []
}
})
const emit = defineEmits<Emit>()
const functionId = ref()
const _value = ref([])
const callDataOptions = computed(() => {
const _valueKeys = Object.keys(props.value)
if (_valueKeys.length) {
return _valueKeys.map(key => {
const item: any = props.functions.find((p: any) => p.id === key)
if (item) {
return {
id: item.id,
name: item.name,
type: item.valueType ? item.valueType.type : '-',
format: item.valueType ? item.valueType.format : undefined,
options: item.valueType ? item.valueType.element : undefined,
value: props.value[key]
}
}
return {
id: key,
name: key,
type: '',
format: undefined,
options: undefined,
value: props.value[key]
}
})
}
return []
})
const onSelect = (v: string, item: any) => {
emit('update:action', `执行${item.name}`)
}
const callDataChange = () => {
}
</script>
<style scoped>
</style>

Some files were not shown because too many files have changed in this diff Show More