Merge branch 'dev' into dev-hub

This commit is contained in:
jackhoo_98 2023-02-28 21:17:00 +08:00
commit 67a5a78db5
88 changed files with 5032 additions and 1449 deletions

View File

@ -482,4 +482,19 @@ export const getPropertiesInfo = (deviceId: string, data: Record<string, unknown
* @param data
* @returns
*/
export const getPropertiesList = (deviceId: string, property: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/property/${property}/_query`, data)
export const getPropertiesList = (deviceId: string, property: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/property/${property}/_query`, data)
/**
*
* @param deviceId
* @param data
* @returns
*/
export const queryLog = (deviceId: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/logs`, data)
/**
*
* @returns
*/
export const queryLogsType = () => server.get(`/dictionary/device-log-type/items`)

View File

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

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,21 @@
import server from '@/utils/request';
/**
*
*/
export const getProductList = (parmas?:any) => server.get('/device/product/_query/no-paging?paging=false',parmas);
/**
*
*/
export const getDeviceList = (parmas?:any) => server.get('/device-instance/_query/no-paging?paging=false',parmas);
/**
*
*/
export const getOrgList = (parmas?:any) => server.get('/organization/_query/no-paging?paging=false',parmas);
/**
*
*/
export const query = (data:any) => server.post('/alarm/record/_query/',data);

View File

@ -4,17 +4,30 @@ import server from '@/utils/request';
// 获取应用管理列表
export const getApplyList_api = (data: any) => server.post(`/application/_query/`, data)
// 修改应用状态
export const changeApplyStatus_api = (id:string,data: any) => server.put(`/application/${id}`, data)
export const changeApplyStatus_api = (id: string, data: any) => server.put(`/application/${id}`, data)
// 删除应用
export const delApply_api = (id:string) => server.remove(`/application/${id}`)
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 getAppInfo_api = (id: string) => server.get(`/application/${id}`);
// 新增应用
export const addApp_api = (data:object) => server.post(`/application`, data);
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 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,6 +58,8 @@ const iconKeys = [
'PauseOutlined',
'ControlOutlined',
'RedoOutlined',
'VideoCameraOutlined',
'HistoryOutlined',
]
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"
@ -53,8 +61,8 @@
delete: item.key === 'delete',
}"
>
<slot name="actions" v-bind="item"></slot>
<!-- <a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
<slot name="actions" v-bind="item"></slot>
<!-- <a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
<a-button :disabled="item.disabled">
<DeleteOutlined v-if="item.key === 'delete'" />
<template v-else>
@ -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 })
() => {
emit('update:value', _value.value)
},
{ 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="[
{ required: true, message: '请输入标识' },
{ max: 64, message: '最多可输入64个字符' },
{
pattern: /^[a-zA-Z0-9_]+$/,
message: '请输入英文或者数字或者-或者_',
},
]">
<a-input v-model:value="_value[index].id" size="small"></a-input>
</a-form-item>
<a-form-item label="名称" name="name" :rules="[
{ required: true, message: '请输入名称' },
{ max: 64, message: '最多可输入64个字符' },
]">
<a-input v-model:value="_value[index].name" size="small"></a-input>
</a-form-item>
<value-type-form v-model:value="_value[index].valueType" :name="['valueType']" isSub
key="json_sub"></value-type-form>
</a-form>
<div style="max-width: 400px;" class="ant-form-vertical">
<a-form-item label="标识" :name="name.concat([index, 'id'])" :rules="[
{ required: true, message: '请输入标识' },
{ max: 64, message: '最多可输入64个字符' },
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: 'ID只能由数字、字母、下划线、中划线组成',
},
]">
<a-input v-model:value="_value[index].id" size="small"></a-input>
</a-form-item>
<a-form-item label="名称" :name="name.concat([index, 'name'])" :rules="[
{ required: true, message: '请输入名称' },
{ max: 64, message: '最多可输入64个字符' },
]">
<a-input v-model:value="_value[index].name" size="small"></a-input>
</a-form-item>
<value-type-form v-model:value="_value[index].valueType" :name="name.concat([index, 'valueType'])" isSub
key="json_sub"></value-type-form>
</div>
</template>
<div class="item-edit" @click="handleEdit(index)">
{{ item.name || '配置参数' }}
<edit-outlined class="item-icon" />
<AIcon type="EditOutlined" class="item-icon" />
</div>
</a-popover>
</div>
<div class="item-right">
<delete-outlined @click="handleDelete(index)" />
<AIcon type="DeleteOutlined" @click="handleDelete(index)" />
</div>
</div>
<a-button type="dashed" block @click="handleAdd">
<template #icon><plus-outlined class="item-icon" /></template>
<template #icon>
<AIcon type="PlusOutlined" class="item-icon" />
</template>
添加参数
</a-button>
</div>
</template>
<script setup lang="ts" name="JsonParam">
import { PropType } from 'vue'
import { MenuOutlined, EditOutlined, DeleteOutlined, PlusOutlined, CloseOutlined } from '@ant-design/icons-vue';
import ValueTypeForm from '@/views/device/components/Metadata/Base/Edit/ValueTypeForm.vue';
type JsonType = Record<any, any>;
@ -66,20 +65,27 @@ const emit = defineEmits<Emits>()
const props = defineProps({
value: {
type: Object as PropType<JsonType[]>,
},
name: {
type: Array as PropType<(string | number)[]>,
default: () => ([])
}
})
const _value = ref<JsonType[]>([])
watchEffect(() => {
_value.value = props.value
_value.value = props.value || [{
valueType: {
expands: {}
},
}]
})
watch(_value,
() => {
emit('update:value', _value.value)
},
{ deep: true })
{ deep: true, immediate: true })
const editIndex = ref<number>(-1)
const handleEdit = (index: number) => {
@ -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

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

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

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

@ -28,7 +28,7 @@
instanceStore.current.firmwareInfo?.version
}}</a-descriptions-item>
<a-descriptions-item label="连接协议">{{
instanceStore.current.protocolName
instanceStore.current?.protocolName
}}</a-descriptions-item>
<a-descriptions-item label="消息协议">{{
instanceStore.current.transport

View File

@ -0,0 +1,153 @@
<template>
<a-card>
<Search
:columns="columns"
target="device-instance-log"
@search="handleSearch"
/>
<JTable
ref="instanceRefLog"
:columns="columns"
:request="(e: Record<string, any>) => queryLog(instanceStore.current.id, e)"
model="TABLE"
:defaultParams="{ sorts: [{ name: 'timestamp', order: 'desc' }] }"
:params="params"
>
<template #type="slotProps">
{{ slotProps?.type?.text }}
</template>
<template #timestamp="slotProps">
{{
slotProps.timestamp
? moment(slotProps.timestamp).format(
'YYYY-MM-DD HH:mm:ss',
)
: ''
}}
</template>
<template #action="slotProps">
<a-space>
<template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
>
<a-button
@click="i.onClick"
type="link"
style="padding: 0px"
>
<template #icon><AIcon :type="i.icon" /></template>
</a-button>
</template>
</a-space>
</template>
</JTable>
</a-card>
</template>
<script lang="ts" setup>
import type { ActionsType } from '@/components/Table';
import { queryLog, queryLogsType } from '@/api/device/instance';
import { useInstanceStore } from '@/store/instance';
import moment from 'moment';
import { Modal, Textarea } from 'ant-design-vue';
const params = ref<Record<string, any>>({});
const instanceStore = useInstanceStore();
const columns = [
{
title: '类型',
dataIndex: 'type',
key: 'type',
scopedSlots: true,
ellipsis: true,
search: {
type: 'select',
options: () =>
new Promise((resolve) => {
queryLogsType().then((resp: any) => {
resolve(
resp.result.map((item: any) => ({
label: item.text,
value: item.value,
})),
);
});
}),
},
},
{
title: '时间',
dataIndex: 'timestamp',
key: 'timestamp',
scopedSlots: true,
ellipsis: true,
search: {
type: 'date',
},
},
{
title: '内容',
ellipsis: true,
dataIndex: 'content',
key: 'content',
scopedSlots: true,
search: {
type: 'string',
},
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true,
},
];
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
return [
{
key: 'view',
text: '查看',
tooltip: {
title: '查看',
},
icon: 'SearchOutlined',
onClick: () => {
let content = '';
try {
content = JSON.stringify(
JSON.parse(data.content),
null,
2,
);
} catch (error) {
content = data.content;
}
Modal.info({
title: '详细信息',
width: 700,
content: h(Textarea, {
bordered: false,
rows: 15,
value: content
}),
});
},
},
];
};
const handleSearch = (_params: any) => {
params.value = _params;
};
</script>
<style lang="less" scoped>
</style>

View File

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

View File

@ -3,12 +3,14 @@
<div style="position: relative">
<div style="position: absolute; right: 0; top: 5px; z-index: 999">
<a-space>
<a-button type="primary">开始动画</a-button>
<a-button type="primary">停止动画</a-button>
<a-button type="primary" @click="onStart">开始动画</a-button>
<a-button type="primary" @click="onStop">停止动画</a-button>
</a-space>
</div>
</div>
<AMap :points="geoList" />
<AMapComponent style="height: 500px">
<PathSimplifier :pathData="geoList" ref="amapPath"></PathSimplifier>
</AMapComponent>
</a-spin>
</template>
@ -16,7 +18,6 @@
import { getPropertyData } from '@/api/device/instance';
import { useInstanceStore } from '@/store/instance';
import encodeQuery from '@/utils/encodeQuery';
import AMap from './AMap.vue';
const instanceStore = useInstanceStore();
@ -33,6 +34,15 @@ const prop = defineProps({
const geoList = ref<any[]>([]);
const loading = ref<boolean>(false);
const amapPath = ref()
const onStart = () => {
amapPath.value.start()
}
const onStop = () => {
amapPath.value.stop()
}
const query = async () => {
loading.value = true;
@ -53,7 +63,10 @@ const query = async () => {
((resp.result as any)?.data || []).forEach((item: any) => {
list.push([item.value.lon, item.value.lat]);
});
geoList.value = list
geoList.value = [{
name: prop?.data?.name,
path: list
}]
}
};

View File

@ -2,7 +2,7 @@
<a-modal title="详情" visible width="50vw" @ok="onCancel" @cancel="onCancel">
<div style="margin-bottom: 10px"><TimeComponent v-model="dateValue" /></div>
<div>
<a-tabs v-model:activeKey="activeKey" style="max-height: 600px; overflow-y: auto">
<a-tabs :destroyInactiveTabPane="true" v-model:activeKey="activeKey" style="max-height: 600px; overflow-y: auto">
<a-tab-pane key="table" tab="列表">
<Table :data="props.data" :time="_getTimes" />
</a-tab-pane>

View File

@ -116,6 +116,7 @@ import Function from './Function/index.vue';
import Modbus from './Modbus/index.vue';
import OPCUA from './OPCUA/index.vue';
import EdgeMap from './EdgeMap/index.vue';
import Log from './Log/index.vue'
import { _deploy, _disconnect } from '@/api/device/instance';
import { message } from 'ant-design-vue';
import { getImage } from '@/utils/comm';
@ -147,6 +148,10 @@ const list = ref([
key: 'Metadata',
tab: '物模型',
},
{
key: 'Log',
tab: '日志管理',
},
{
key: 'Function',
tab: '设备功能',
@ -167,6 +172,7 @@ const tabs = {
Modbus,
OPCUA,
EdgeMap,
Log
};
const getStatus = (id: string) => {

View File

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

View File

@ -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="[
{ required: true, message: '请选择文件类型' },
]">
<a-select v-model:value="value.fileType" :options="FileTypeList" size="small"></a-select>
<a-form-item label="文件类型" :name="name.concat(['fileType'])" v-if="['file'].includes(_value.type)" initialValue="url"
:rules="[
{ required: true, message: '请选择文件类型' },
]">
<a-select v-model:value="_value.fileType" :options="FileTypeList" size="small"></a-select>
</a-form-item>
</template>
<script lang="ts" setup mame="BaseForm">
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,22 +33,24 @@
</a-tag>
</template>
<template #action="slotProps">
<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 />
</PermissionButton>
<PermissionButton :uhas-permission="`${permission}:delete`" type="link" key="delete" style="padding: 0"
:pop-confirm="{
title: '确认删除?', onConfirm: async () => {
await removeItem(slotProps);
},
}" :tooltip="{
title: '删除',
}">
<DeleteOutlined />
</PermissionButton>
<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) ? '当前的存储方式不支持编辑' : '编辑',
}">
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton :uhas-permission="`${permission}:delete`" type="link" key="delete" style="padding: 0"
:pop-confirm="{
title: '确认删除?', onConfirm: async () => {
await removeItem(slotProps);
},
}" :tooltip="{
title: '删除',
}">
<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

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

View File

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

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

View File

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

View File

@ -0,0 +1,430 @@
<template>
<page-container>
<Search
:columns="columns"
target="media-cascade"
@search="handleSearch"
/>
<JTable
ref="listRef"
:columns="columns"
:request="(e:any) => lastValueFrom(e)"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:params="params"
:gridColumn="2"
>
<template #headerTitle>
<a-button type="primary" @click="handleAdd"> 新增 </a-button>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:showStatus="true"
:status="slotProps.status?.value"
:statusText="slotProps.status?.text"
:statusNames="{
enabled: 'success',
disabled: 'error',
}"
>
<template #img>
<slot name="img">
<img
:src="
getImage('/device/instance/device-card.png')
"
/>
</slot>
</template>
<template #content>
<h3 class="card-item-content-title">
{{ slotProps.name }}
</h3>
<p>通道数量{{ slotProps.count }}</p>
<Ellipsis>
<a-badge
:text="`sip:${slotProps.sipConfigs[0]?.sipId}@${slotProps.sipConfigs[0]?.hostAndPort}`"
:status="
slotProps.status?.value === 'enabled'
? 'success'
: 'error'
"
/>
</Ellipsis>
</template>
<template #actions="item">
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
>
<a-button
:disabled="item.disabled"
v-if="item.key === 'delete'"
>
<AIcon type="DeleteOutlined" />
</a-button>
<a-button
:disabled="item.disabled"
@click="item.onClick"
v-else
>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</a-button>
</a-popconfirm>
<template v-else>
<a-button
:disabled="item.disabled"
@click="item.onClick"
>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</a-button>
</template>
</a-tooltip>
<!-- <PermissionButton
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
:hasPermission="`media/Cascade:${item.key}`"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</PermissionButton> -->
</template>
</CardBox>
</template>
<template #sipId="slotProps">
{{ slotProps.sipConfigs[0]?.sipId }}
</template>
<template #publicHost="slotProps">
{{ slotProps.sipConfigs[0]?.publicHost }}
</template>
<template #status="slotProps">
<a-badge
:text="slotProps.status?.text"
:status="
slotProps.status?.value === 'enabled'
? 'success'
: 'error'
"
/>
</template>
<template #onlineStatus="slotProps">
<a-badge
:text="slotProps.onlineStatus?.text"
:status="
slotProps.onlineStatus?.value === 'online'
? 'success'
: 'error'
"
/>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
:disabled="i.disabled"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
<!-- <template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
>
<PermissionButton
:disabled="i.disabled"
:popConfirm="i.popConfirm"
:tooltip="{
...i.tooltip,
}"
@click="i.onClick"
type="link"
style="padding: 0px"
:hasPermission="`device/Instance:${i.key}`"
>
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</template> -->
</a-space>
</template>
</JTable>
</page-container>
</template>
<script setup lang="ts">
import DeviceApi from '@/api/media/device';
import CascadeApi from '@/api/media/cascade';
import type { ActionsType } from '@/components/Table/index.vue';
import { message } from 'ant-design-vue';
import { getImage } from '@/utils/comm';
import { PROVIDER_OPTIONS } from '@/views/media/Device/const';
import { useMenuStore } from 'store/menu';
const menuStory = useMenuStore();
const listRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '上级SIP ID',
dataIndex: 'sipId',
key: 'sipId',
scopedSlots: true,
},
{
title: '上级SIP 地址',
dataIndex: 'publicHost',
key: 'publicHost',
scopedSlots: true,
},
{
title: '通道数量',
dataIndex: 'count',
key: 'count',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '正常', value: 'enabled' },
{ label: '禁用', value: 'disabled' },
],
handleValue: (v: any) => {
return v;
},
},
},
{
title: '级联状态',
dataIndex: 'onlineStatus',
key: 'onlineStatus',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '已连接', value: 'online' },
{ label: '未连接', value: 'offline' },
],
handleValue: (v: any) => {
return v;
},
},
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 200,
scopedSlots: true,
},
];
/**
* 搜索
* @param params
*/
const handleSearch = (e: any) => {
params.value = e;
};
/**
* 处理表格数据
* @param params
*/
const lastValueFrom = async (params: any) => {
const res = await CascadeApi.list(params);
res.result.data.forEach(async (item: any) => {
const resp = await queryBindChannel(item.id);
item.count = resp.result.total;
});
return res;
};
/**
* 查询通道数量
* @param id
*/
const queryBindChannel = async (id: string) => {
return await CascadeApi.queryCount(id);
};
/**
* 新增
*/
const handleAdd = () => {
menuStory.jumpPage('media/Cascade/Save');
};
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
const actions = [
{
key: 'edit',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
menuStory.jumpPage(
'media/Cascade/Save',
{},
{
id: data.id,
},
);
},
},
{
key: 'view',
text: '选择通道',
tooltip: {
title: '选择通道',
},
icon: 'LinkOutlined',
onClick: () => {
menuStory.jumpPage(
'media/Cascade/Channel',
{},
{
id: data.id,
},
);
},
},
{
key: 'debug',
text: '推送',
tooltip: {
title:
data.status?.value === 'disabled'
? '禁用状态下不可推送'
: '推送',
},
disabled: data.status?.value === 'disabled',
icon: 'ShareAltOutlined',
onClick: () => {
// updateChannel()
},
},
{
key: 'action',
text: data.status?.value === 'enabled' ? '禁用' : '启用',
tooltip: {
title: data.status?.value === 'enabled' ? '禁用' : '启用',
},
icon:
data.status?.value === 'enabled'
? 'StopOutlined'
: 'PlayCircleOutlined',
popConfirm: {
title: `确认${
data.status?.value === 'enabled' ? '禁用' : '启用'
}?`,
onConfirm: async () => {
let res =
data.status.value === 'enabled'
? await CascadeApi.disabled(data.id)
: await CascadeApi.enabled(data.id);
if (res.success) {
message.success('操作成功!');
listRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
{
key: 'delete',
text: '删除',
tooltip: {
title:
data.status?.value === 'enabled'
? '请先禁用, 再删除'
: '删除',
},
disabled: data.status?.value === 'enabled',
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const resp = await CascadeApi.del(data.id);
if (resp.status === 200) {
message.success('操作成功!');
listRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
return actions;
};
</script>

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

@ -0,0 +1,41 @@
type BaseItem = {
id: string;
name: string;
};
type State = {
value: string;
text: string;
};
type SipConfig = {
catalogEach: number;
charset: string;
clusterNodeId: string;
domain: string;
firmware: string;
hostAndPort: string;
keepaliveInterval: number;
keepaliveTimeoutTimes: number;
localAddress: string;
localSipId: string;
manufacturer: string;
model: string;
name: string;
password: string;
port: number;
publicAddress: string;
publicPort: number;
sipId: string;
stackName: string;
transport: string;
user: string;
};
export type CascadeItem = {
mediaServerId: string;
onlineStatus: State;
proxyStream: boolean;
sipConfigs: Partial<SipConfig>[];
status: State;
count?: number;
} & BaseItem;

View File

@ -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',
{
style: {
maxHeight: '300px',
overflowY: 'auto',
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
const actions = [
{
key: 'edit',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
channelData.value = cloneDeep(data);
saveVis.value = true;
},
},
{
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('操作失败!');
}
},
},
JSON.stringify(e),
),
});
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

@ -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,33 +670,38 @@
</div>
</a-form-item>
</template>
<a-form-item
<template
v-if="
formData.type !== 'webhook' &&
formData.type !== 'voice'
"
v-bind="validateInfos['template.message']"
>
<template #label>
<span>
模版内容
<a-tooltip title="发送的内容,支持录入变量">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</span>
</template>
<a-textarea
v-model:value="formData.template.message"
:maxlength="200"
:rows="5"
:disabled="formData.type === 'sms'"
placeholder="变量格式:${name};
<a-form-item
v-bind="validateInfos['template.message']"
>
<template #label>
<span>
模版内容
<a-tooltip
title="发送的内容,支持录入变量"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</a-tooltip>
</span>
</template>
<a-textarea
v-model:value="formData.template.message"
:maxlength="200"
:rows="5"
:disabled="formData.type === 'sms'"
placeholder="变量格式:${name};
示例:尊敬的${name},${time}有设备触发告警,请注意处理"
/>
</a-form-item>
/>
</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

@ -0,0 +1,316 @@
<template>
<div class="alarm-log-card">
<Search
:columns="columns"
target="alarm-log"
v-if="['all', 'detail'].includes(props.type)"
@search="search"
></Search>
<Search
:columns="produtCol"
target="alarm-log"
v-if="['product', 'other'].includes(props.type)"
@search="search"
></Search>
<Search
:columns="deviceCol"
target="alarm-log"
v-if="props.type === 'device'"
@search="search"
></Search>
<Search
:columns="orgCol"
target="alarm-log"
v-if="props.type === 'org'"
@search="search"
></Search>
<JTable :columns="columns" :request="handleSearch" :params="params">
<template #card="slotProps">
<CardBox
:value="slotProps"
v-bind="slotProps"
:statusText="
data.defaultLevel.find(
(i) => i.level === slotProps.level,
)?.title || slotProps.level
"
>
<template #img>
<img :src="imgMap.get(slotProps.targetType)" alt="" />
</template>
<template #content>
<Ellipsis>
<span style="font-weight: 500">
{{ slotProps.alarmName }}
</span>
</Ellipsis>
<a-row :gutter="24">
<a-col :span="8">
<div class="content-des-title">
{{ titleMap.get(slotProps.targetType) }}
</div>
<Ellipsis
><div>
{{ slotProps?.targetName }}
</div></Ellipsis
>
</a-col>
<a-col :span="8">
<div class="content-des-title">
最近告警时间
</div>
<Ellipsis
><div>
{{
moment(slotProps?.alarmTime).format(
'YYYY-MM-DD HH:mm:ss',
)
}}
</div></Ellipsis
>
</a-col>
<a-col :span="8">
<div class="content-des-title">状态</div>
<a-badge
:status="
slotProps.state.value === 'warning'
? 'error'
: 'default'
"
>
</a-badge
><span
:style="
slotProps.state.value === 'warning'
? 'color: #E50012'
: 'color:black'
"
>
{{ slotProps.state.text }}
</span>
</a-col>
</a-row>
</template>
</CardBox>
</template>
</JTable>
</div>
</template>
<script lang="ts" setup>
import { getImage } from '@/utils/comm';
import {
getProductList,
getDeviceList,
getOrgList,
query,
} from '@/api/rule-engine/log';
import { queryLevel } from '@/api/rule-engine/config';
import Search from '@/components/Search';
import { useAlarmStore } from '@/store/alarm';
import { storeToRefs } from 'pinia';
import { Store } from 'jetlinks-store';
import moment from 'moment';
const alarmStore = useAlarmStore();
const { data } = storeToRefs(alarmStore);
const getDefaulitLevel = () => {
queryLevel().then((res) => {
if (res.status === 200) {
Store.set('default-level', res.result?.levels || []);
data.value.defaultLevel = res.result?.levels || [];
}
});
};
getDefaulitLevel();
const props = defineProps<{
type: string;
id?: string;
}>();
const imgMap = new Map();
imgMap.set('product', getImage('/alarm/product.png'));
imgMap.set('device', getImage('/alarm/device.png'));
imgMap.set('other', getImage('/alarm/other.png'));
imgMap.set('org', getImage('/alarm/org.png'));
const titleMap = new Map();
titleMap.set('product', '产品');
titleMap.set('device', '设备');
titleMap.set('other', '其他');
titleMap.set('org', '组织');
const colorMap = new Map();
colorMap.set(1, '#E50012');
colorMap.set(2, '#FF9457');
colorMap.set(3, '#FABD47');
colorMap.set(4, '#999999');
colorMap.set(5, '#C4C4C4');
const columns = [
{
title: '名称',
dataIndex: 'alarmName',
key: 'alarmName',
search: {
type: 'string',
},
},
{
title: '最近告警事件',
dataIndex: 'alarmTime',
key: 'alarmTime',
search: {
type: 'dateTime',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
search: {
type: 'select',
options: [
{
label: '告警中',
value: 'warning',
},
{
label: '无告警',
value: 'normal',
},
],
},
},
];
const produtCol = [
...columns,
{
title: '产品名称',
dataIndex: 'targetName',
key: 'targetName',
search: {
type: 'select',
options: async () => {
const resq = await getProductList();
if (resq.status === 200) {
return resq.result.map((item: any) => ({
label: item.name,
value: item.name,
}));
}
return [];
},
},
},
];
const deviceCol = [
...columns,
{
title: '设备名称',
dataIndex: 'targetName',
key: 'targetName',
search: {
type: 'select',
opstions: async () => {
const res = await getDeviceList();
if (res.status === 200) {
return res.result.map((item: any) => ({
label: item.name,
value: item.name,
}));
}
return [];
},
},
},
];
const orgCol = [
...columns,
{
title: '组织名称',
dataIndex: 'targetName',
key: 'targetName',
search: {
type: 'select',
options: async () => {
const res = await getOrgList();
if (res.status === 200) {
return res.result.map((item: any) => ({
label: item.name,
value: item.name,
}));
}
return [];
},
},
},
];
let params = ref({
sorts: [{ name: 'alarmTime', order: 'desc' }],
terms: [],
});
let param = reactive({
pageSize: 10,
terms: [],
});
// let dataSource = reactive({
// data: [],
// pageSize: 10,
// pageIndex: 0,
// total: 0,
// });
const handleSearch = async (params: any) => {
const resp = await query(params);
if (resp.status === 200) {
const res = await getOrgList();
if (res.status === 200) {
resp.result.data.map((item: any) => {
if (item.targetType === 'org') {
res.result.forEach((item2: any) => {
if (item2.id === item.targetId) {
item.targetName = item2.name;
}
//targetName
if (item.targetId === item.targetName) {
item.targetName = '无';
}
});
}
});
return resp;
}
}
};
watchEffect(() => {
if (props.type !== 'all' && !props.id) {
params.value.terms.push({
termType: 'eq',
column: 'targetType',
value: props.type,
type: 'and',
});
}
if (props.id) {
params.value.terms.push({
termType: 'eq',
column: 'alarmConfigId',
value: props.id,
type: 'and',
});
}
});
const search = (data: any) => {
const dt = {
pageSize: 10,
terms: [...data?.terms],
};
};
const log = () => {
console.log(data.value.defaultLevel);
};
log();
</script>
<style lang="less" scoped>
</style>

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

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

View File

@ -0,0 +1,81 @@
<template>
<a-row :gutter='[24]'>
<a-col :span='10'>
<a-form-item
name='readProperties'
:rules="[{ required: true, message: '请选择属性' }]"
>
<a-select
show-search
mode='multiple'
max-tag-count='responsive'
placeholder='请选择属性'
style='width: 100%'
v-model:value='readProperties'
:options='properties'
:filter-option='filterSelectNode'
@change='change'
/>
</a-form-item>
</a-col>
<a-col :span='14'>
<a-form-item>定时读取所选属性值</a-form-item>
</a-col>
</a-row>
</template>
<script setup lang='ts' name='ReadProperties'>
import { filterSelectNode } from '@/utils/comm'
import type { PropType } from 'vue'
type Emit = {
(e: 'update:value', data: Array<string>): void
(e: 'update:action', data: string): void
}
const props = defineProps({
value: {
type: Array as PropType<Array<string>>,
default: () => []
},
action: {
type: String,
default: ''
},
properties: {
type: Array,
default: () => []
}
})
const emit = defineEmits<Emit>()
const readProperties = ref<string[]>(props.value)
const change = (values: string[], optionItems: any[]) => {
const names = optionItems.map((item) => item.name);
let extraStr = '';
let isLimit = false;
let indexOf = 0;
extraStr = names.reduce((_prev, next, index) => {
if (_prev.length <= 30) {
indexOf = index;
return index === 0 ? next : _prev + '、' + next;
} else {
isLimit = true;
}
return _prev;
}, '');
if (isLimit && names.length - 1 > indexOf) {
extraStr += `${optionItems.length}个属性`;
}
emit('update:value', values)
emit('update:action', `读取 ${extraStr}`)
}
</script>
<style scoped>
</style>

View File

@ -7,23 +7,46 @@
>
<TopCard
:label-bottom='true'
:options='options'
:options='topOptions'
v-model:value='formModel.operator'
/>
</a-form-item>
<Timer v-if='showTimer' />
<Timer v-if='showTimer' v-model:value='formModel.timer' />
<ReadProperties v-if='showReadProperty' v-model:value='formModel.readProperties' v-model:action='optionCache.action' :properties='readProperties' />
<a-form-item
v-if='showWriteProperty'
name='writeProperties'
:rules="[{ required: true, message: '请输入修改值' }]"
>
<WriteProperty v-model:value='formModel.writeProperties' v-model:action='optionCache.action' :properties='writeProperties' />
</a-form-item>
<a-form-item
v-if='showReportEvent'
name='eventId'
:rules="[{ required: true, message: '请选择事件' }]"
>
<a-select
v-model:value='formModel.eventId'
:filter-option='filterSelectNode'
:options='eventOptions'
placeholder='请选择事件'
style='width: 100%'
@select='eventSelect'
/>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang='ts'>
import TopCard from '@/views/rule-engine/Scene/Save/components/TopCard.vue'
import { filterSelectNode } from '@/utils/comm'
import { TopCard, Timer } from '@/views/rule-engine/Scene/Save/components'
import { getImage } from '@/utils/comm'
import { metadataType } from '@/views/rule-engine/Scene/typings'
import type { PropType } from 'vue'
import { TypeEnum } from '@/views/rule-engine/Scene/Save/Device/util'
import Timer from '../components/Timer.vue'
import ReadProperties from './ReadProperties.vue'
import WriteProperty from './WriteProperty.vue'
const props = defineProps({
metadata: {
@ -34,12 +57,23 @@ const props = defineProps({
const formModel = reactive({
operator: 'online',
timer: {},
readProperties: [],
writeProperties: {},
eventId: undefined,
functionId: undefined,
functionParameters: []
})
const optionCache = reactive({
action: ''
})
const readProperties = ref<any[]>([])
const writeProperties = ref<any[]>([])
const eventOptions = ref<any[]>([])
const options = computed(() => {
const topOptions = computed(() => {
const baseOptions = [
{
label: '设备上线',
@ -55,13 +89,14 @@ const options = computed(() => {
if (props.metadata.events?.length) {
baseOptions.push(TypeEnum.reportEvent)
eventOptions.value = props.metadata.events.map(item => ({ ...item, label: item.name, value: item.id }))
}
if (props.metadata.properties?.length) {
const _properties = props.metadata.properties
readProperties.value = _properties.filter((item: any) => item.expands.type?.includes('read'))
writeProperties.value = _properties.filter((item: any) => item.expands.type?.includes('write'))
const reportProperties = _properties.filter((item: any) => item.expands.type?.includes('report'))
readProperties.value = _properties.filter((item: any) => item.expands.type?.includes('read')).map(item => ({...item, label: item.name, value: item.id }))
writeProperties.value = _properties.filter((item: any) => item.expands.type?.includes('write')).map(item => ({...item, label: item.name, value: item.id }))
const reportProperties = _properties.filter((item: any) => item.expands.type?.includes('report')).map(item => ({...item, label: item.name, value: item.id }))
if (readProperties.value.length) {
baseOptions.push(TypeEnum.readProperty)
@ -84,10 +119,34 @@ const options = computed(() => {
return baseOptions
})
const showTimer = computed(() => {
return ['readProperty', 'writeProperty', 'invokeFunction'].includes(formModel.operator)
const showReadProperty = computed(() => {
return formModel.operator === TypeEnum.readProperty.value
})
const showWriteProperty = computed(() => {
return formModel.operator === TypeEnum.writeProperty.value
})
const showReportEvent = computed(() => {
return formModel.operator === TypeEnum.reportEvent.value
})
const showInvokeFunction = computed(() => {
return formModel.operator === TypeEnum.invokeFunction.value
})
const showTimer = computed(() => {
return [
TypeEnum.readProperty.value,
TypeEnum.writeProperty.value,
TypeEnum.invokeFunction.value
].includes(formModel.operator)
})
const eventSelect = (_: string, eventItem: any) => {
optionCache.action = `${eventItem.name}上报`
}
</script>
<style scoped lang='less'>

View File

@ -0,0 +1,121 @@
<template>
<a-row :futter='[24, 24]'>
<a-col :span='10'>
<a-select
showSearch
style='width: 100%'
placeholder='请选择属性'
v-model:value='reportKey'
:options='properties'
:filter-option='filterSelectNode'
@change='change'
/>
</a-col>
<a-col :span='14'>
<span style='line-height: 32px;padding-left: 24px'>
定时调用所选属性
</span>
</a-col>
<a-col :span='24' v-if='showTable'>
<div style='margin-top: 24px'>
<FunctionCall
:value='_value'
:data='callDataOptions'
@change='callDataChange'
/>
</div>
</a-col>
</a-row>
</template>
<script setup lang='ts' name='WriteProperties'>
import { filterSelectNode } from '@/utils/comm'
import { FunctionCall } from '../components'
import type { PropType } from 'vue'
type Emit = {
(e: 'update:value', data: Record<string, any>): void
(e: 'update:action', data: string): void
}
const props = defineProps({
value: {
type: Object as PropType<Record<string, any>>,
default: () => []
},
action: {
type: String,
default: ''
},
properties: {
type: Array,
default: () => []
}
})
const emit = defineEmits<Emit>()
const reportKey = ref<string>()
const callData = ref<Array<{ id: string, value: string | undefined }>>()
const _value = ref([])
const callDataOptions = computed(() => {
const _valueKeys = Object.keys(props.value)
if (_valueKeys.length) {
return _valueKeys.map(key => {
const item: any = props.properties.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 showTable = computed(() => {
return !!reportKey.value
})
const change = (v: string, option: any) => {
console.log(v, option)
const _data = {
[v]: undefined
}
callData.value = [{ id: v, value: undefined }]
emit('update:value', _data)
emit('update:action', `修改${option.name}`)
}
const callDataChange = (v: any[]) => {
emit('update:value', {
[reportKey.value!]: v[0]?.value
})
}
const initRowKey = () => {
if (props.value.length) {
const keys = Object.keys(props.value)
reportKey.value = keys[0]
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,114 @@
<template>
<a-table
:data-source='dataSource.value'
:columns='columns'
>
<template #bodyCell="{ column, record, index }">
<template v-if='column.dataIndex === "name"'>
{{ record.name }}
</template>
<template v-if='column.dataIndex === "type"'>
{{ record.type }}
<a-tooltip
v-if="record.type === 'object'"
>
<template slot="title">
请按照json格式输入
</template>
<AIcon
type="QuestionCircleOutlined"
:style="{
marginLeft: '5px',
cursor: 'help',
}"
/>
</a-tooltip>
</template>
<template v-if='column.dataIndex === "value"'>
<ValueItem
v-model:modelValue='record.value'
:itemType="record.type"
:options="handleOptions(record)"
/>
</template>
</template>
</a-table>
</template>
<script setup lang='ts' name='FunctionCall'>
import type { PropType } from 'vue'
type Emit = {
(e: 'change', data: Array<{ name: string, value: any}>): void
(e: 'update:value', data: Array<{ name: string, value: any}>): void
}
const emit = defineEmits<Emit>()
const props = defineProps({
value: {
type: Array as PropType<Array<{ label: string, value: any}>>,
default: () => []
},
data: {
type: Array,
default: () => []
}
})
const dataSource = reactive<{value: any[]}>({
value: []
})
watch(() => props.data, () => {
dataSource.value = props.data.map((item: any) => {
const oldValue = props.value.find((oldItem: any) => oldItem.name === item.id)
return oldValue ? { ...item, value: oldValue.value } : item
})
}, { immediate: true })
const columns = [
{
title: '参数名称',
dataIndex: 'name'
},
{
title: '类型',
dataIndex: 'type'
},
{
title: '值',
dataIndex: 'value',
align: 'center',
width: 260
},
]
const handleOptions = (record: any) => {
switch(record.type) {
case 'enum':
return (record?.options?.elements || []).map((item: any) => ({ label: item.text, value: item.value }))
case 'boolean':
return [
{ label: '是', value: true },
{ label: '否', value: false },
]
default:
return undefined
}
}
watch(() => dataSource.value, () => {
const _value = dataSource.value.map(item => ({
name: item.id, value: item.value
}))
emit('change', _value)
emit('update:value', _value)
}, { deep: true })
</script>
<style scoped>
</style>

View File

@ -0,0 +1 @@
export { default as FunctionCall } from './FunctionCall.vue'

View File

@ -0,0 +1,98 @@
<template>
<div class='timer-when-warp'>
<div :class='["when-item-option", allActive ? "active" : ""]' @click='() => change(0)'>每天</div>
<div
v-for='item in timeOptions'
:class='["when-item-option", rowKeys.includes(item.value) ? "active" : ""]'
@click='() => change(item.value)'
>
{{ item.label }}
</div>
</div>
</template>
<script setup lang='ts' name='WhenOption'>
import type { PropType } from 'vue'
import { numberToString } from './util'
type Emit = {
(e: 'update:value', data: Array<number>):void
}
const props = defineProps({
value: {
type: Array as PropType<Array<number>>,
default: []
},
type: {
type: String,
default: ''
}
})
const emit = defineEmits<Emit>()
const timeOptions = ref<Array<{ label: string, value: number}>>([])
const rowKeys = ref<Array<number>>(props.value)
const change = (number: number) => {
const _keys = new Set(rowKeys.value)
if (number === 0) { //
_keys.clear()
} else {
if (_keys.has(number)) {
_keys.delete(number)
} else {
_keys.add(number)
}
}
rowKeys.value = [..._keys.values()]
emit('update:value', rowKeys.value)
}
const allActive = computed(() => {
return !rowKeys.value.length
})
watch(() => props.type, () => {
const isMonth = props.type === 'month'
const day = isMonth ? 31 : 7
change(0)
timeOptions.value = new Array(day)
.fill(1)
.map((_, index) => {
const _value = index + 1
return {
label: isMonth ? `${_value}` : numberToString[_value],
value: _value
}
})
}, { immediate: true })
</script>
<style scoped lang='less'>
.timer-when-warp {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
padding: 16px;
background: #fafafa;
.when-item-option {
width: 76px;
padding: 6px 0;
text-align: center;
background: #fff;
border: 1px solid #e6e6e6;
border-radius: 2px;
cursor: pointer;
}
.active {
color: #233dd7;
border-color: #233dd7;
}
}
</style>

View File

@ -0,0 +1,3 @@
import Timer from './index.vue'
export default Timer

View File

@ -17,12 +17,26 @@
button-style='solid'
/>
</a-form-item>
<a-form-item v-if='showCron' name='cron'>
<a-input placeholder='corn表达式' v-model='formModel.cron' />
<a-form-item v-if='showCron' name='cron' :rules="[
{ max: 64, message: '最多可输入64个字符' },
{
validator: async (_, v) => {
if (v) {
if (!isCron(v)) {
return Promise.reject(new Error('请输入正确的cron表达式'));
}
} else {
return Promise.reject(new Error('请输入cron表达式'));
}
return Promise.resolve();
}
}
]">
<a-input placeholder='corn表达式' v-model:value='formModel.cron' />
</a-form-item>
<template v-else>
<a-form-item name='when'>
<WhenOption v-model:value='formModel.when' :type='formModel.trigger' />
</a-form-item>
<a-form-item name='mod'>
<a-radio-group
@ -38,9 +52,10 @@
</template>
<a-space v-if='showOnce' style='display: flex;gap: 24px'>
<a-form-item :name="['once', 'time']">
<a-time-picker valueFormat='HH:mm:ss' v-model:value='formModel.once.time' style='width: 100%' format='HH:mm:ss' />
<a-time-picker valueFormat='HH:mm:ss' v-model:value='formModel.once.time' style='width: 100%'
format='HH:mm:ss' />
</a-form-item>
<a-form-item> 执行一次 </a-form-item>
<a-form-item> 执行一次</a-form-item>
</a-space>
<a-space v-if='showPeriod' style='display: flex;gap: 24px'>
<a-form-item>
@ -89,9 +104,17 @@
<script setup lang='ts' name='Timer'>
import type { PropType } from 'vue'
import moment from 'moment'
import WhenOption from './WhenOption.vue'
import { cloneDeep } from 'lodash-es'
import type { OperationTimer } from '../../../typings'
import { isCron } from '@/utils/regular'
type NameType = string[] | string
type Emit = {
(e: 'update:value', data: OperationTimer): void
}
const props = defineProps({
name: {
type: [String, Array] as PropType<NameType>,
@ -103,13 +126,15 @@ const props = defineProps({
}
})
const formModel = reactive({
const emit = defineEmits<Emit>()
const formModel = reactive<OperationTimer>({
trigger: 'week',
when: [],
mod: 'period',
cron: undefined,
once: {
time: ''
time: moment(new Date()).format('HH:mm:ss')
},
period: {
from: moment(new Date()).startOf('day').format('HH:mm:ss'),
@ -119,6 +144,8 @@ const formModel = reactive({
}
})
Object.assign(formModel, props.value)
const showCron = computed(() => {
return formModel.trigger === 'cron'
})
@ -131,6 +158,22 @@ const showPeriod = computed(() => {
return formModel.trigger !== 'cron' && formModel.mod === 'period'
})
watch(() => formModel, () => {
const cloneValue = cloneDeep(formModel)
if (cloneValue.trigger === 'cron') {
delete cloneValue.when
} else {
delete cloneValue.cron
}
if (cloneValue.mod === 'period') {
delete cloneValue.once
} else {
delete cloneValue.period
}
emit('update:value', cloneValue)
}, { deep: true })
</script>
<style scoped lang='less'>

View File

@ -0,0 +1,9 @@
export const numberToString = {
1: '星期一',
2: '星期二',
3: '星期三',
4: '星期四',
5: '星期五',
6: '星期六',
7: '星期日',
};

View File

@ -0,0 +1,4 @@
export { default as Timer } from './Timer'
export { default as TopCard } from './TopCard.vue'
export { default as TriggerWay } from './TriggerWay.vue'
export { default as FunctionCall } from './FunctionCall/FunctionCall.vue'

View File

@ -93,7 +93,7 @@ export enum ActionAlarmMode {
export interface OperationTimerPeriod {
from: string;
to: string;
every: string[];
every: number;
unit: keyof typeof TimeUnit;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -710,6 +710,7 @@
<a-select
v-model:value="form.data.apiServer.roleIdList"
:options="form.roleIdList"
mode="multiple"
placeholder="请选中角色"
></a-select>
<PermissionButton
@ -869,7 +870,7 @@
'sso',
'configuration',
'oauth2',
'client_id',
'clientId',
]"
:rules="[
{
@ -1003,7 +1004,6 @@
v-model:file-list="form.fileList"
accept=".jpg,.png,.jfif,.pjp,.pjpeg,.jpeg"
:maxCount="1"
name="avatar"
list-type="picture-card"
:show-upload-list="false"
:headers="{
@ -1022,8 +1022,9 @@
.logoUrl
"
alt="avatar"
style="width: 150px;"
/>
<div v-else>
<div v-else style="width: 150px;">
<AIcon
:type="
form.uploadLoading
@ -1295,6 +1296,7 @@
<a-form-item label="角色">
<a-select
v-model:value="form.data.sso.roleIdList"
mode="multiple"
:options="form.roleIdList"
placeholder="请选中角色"
></a-select>
@ -1372,7 +1374,10 @@
</a-button>
<div class="dialog">
<MenuDialog ref="dialogRef" />
<MenuDialog
ref="dialogRef"
:mode="routeQuery.id ? 'edit' : 'add'"
/>
</div>
</div>
</template>
@ -1389,7 +1394,7 @@ import {
} from '@/api/system/apply';
import FormLabel from './FormLabel.vue';
import RequestTable from './RequestTable.vue';
import MenuDialog from './MenuDialog.vue';
import MenuDialog from '../../componenets/MenuDialog.vue';
import { getImage } from '@/utils/comm';
import type { formType, dictType, optionsType } from '../typing';
import { getRoleList_api } from '@/api/system/user';
@ -1400,32 +1405,34 @@ import {
UploadFile,
} from 'ant-design-vue';
import { randomString } from '@/utils/utils';
import { cloneDeep } from 'lodash';
import { cloneDeep, difference } from 'lodash';
import { useMenuStore } from '@/store/menu';
const emit = defineEmits(['changeApplyType']);
const routeQuery = useRoute().query;
const menuStory = useMenuStore();
const deptPermission = 'system/Department';
const rolePermission = 'system/Role';
const dialogRef = ref();
//
const initForm: formType = {
name: '',
provider: 'internal-standalone',
integrationModes: [],
config: '',
description: '',
page: {
//
baseUrl: '',
routeType: 'hash',
parameters: [{ label: '', value: '' }],
parameters: [],
},
apiClient: {
// API
baseUrl: '',
headers: [{ label: '', value: '' }], //
parameters: [{ label: '', value: '' }], //
headers: [], //
parameters: [], //
authConfig: {
// API
type: 'oauth2', // , none, bearer, oauth2, basic, other
@ -1447,13 +1454,12 @@ const initForm: formType = {
},
apiServer: {
// API
appId: '',
appId: randomString(16),
secureKey: randomString(), //
redirectUri: '', // URL
roleIdList: [], //
orgIdList: [], //
ipWhiteList: '', // IP
signature: '', // , MD5SHA256
enableOAuth2: false, // OAuth2
},
sso: {
@ -1497,9 +1503,9 @@ const initForm: formType = {
const formRef = ref<FormInstance>();
const form = reactive({
data: { ...initForm },
integrationModesISO: [] as string[],
roleIdList: [] as optionsType,
orgIdList: [] as dictType,
integrationModesISO: [] as string[], // 使
roleIdList: [] as optionsType, //
orgIdList: [] as dictType, //
errorNumInfo: {
page: new Set(),
@ -1509,7 +1515,6 @@ const form = reactive({
},
fileList: [] as any[],
fileUrlList: [] as string[],
uploadLoading: false,
});
//
@ -1591,6 +1596,7 @@ function init() {
() => form.data.provider,
(n) => {
emit('changeApplyType', n);
if (routeQuery.id) return;
if (n === 'wechat-webapp' || n === 'dingtalk-ent-app') {
form.data.integrationModes = ['ssoClient'];
form.integrationModesISO = ['ssoClient'];
@ -1616,6 +1622,7 @@ function getInfo(id: string) {
(item: any) => item.value,
),
} as formType;
form.data.apiServer && (form.data.apiServer.appId = id);
});
}
//
@ -1651,6 +1658,23 @@ function clickAddItem(data: string[], target: string) {
function clickSave() {
formRef.value?.validate().then(() => {
const params = cloneDeep(form.data);
//
const list = ['page', 'apiClient', 'apiServer', 'ssoClient'];
difference(list, params.integrationModes).forEach((item) => {
if (item === 'ssoClient') {
// @ts-ignore
delete params['sso'];
}
delete params[item];
});
clearNullProp(params);
if (
params.provider === 'internal-standalone' &&
params.integrationModes.includes('page')
) {
// @ts-ignore
delete params.page.parameters;
}
if (
params.provider === 'internal-standalone' &&
@ -1674,15 +1698,19 @@ function clickSave() {
const request = routeQuery.id
? updateApp_api(routeQuery.id as string, params)
: addApp_api(params);
request.then((resp) => {
request.then((resp: any) => {
if (resp.status === 200) {
const isPage = params.integrationModes.includes('page');
if (isPage) {
form.data = params;
dialogRef.value && dialogRef.value.openDialog();
dialogRef.value &&
dialogRef.value.openDialog(
routeQuery.id || resp.result.id,
form.data.provider,
);
} else {
message.success('保存成功');
jumpPage('system/Apply');
menuStory.jumpPage('system/Apply');
}
}
});
@ -1722,9 +1750,15 @@ function changeBackUpload(info: UploadChangeParam<UploadFile<any>>) {
function test(...args: any[]) {
console.log('test:', args);
}
function jumpPage(arg0: string) {
throw new Error('Function not implemented.');
function clearNullProp(obj: object) {
if (typeof obj !== 'object') return;
for (const prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
const val = obj[prop];
if (val === '') delete obj[prop];
else if (typeof val === 'object') clearNullProp(obj[prop]);
}
}
}
</script>
@ -1743,6 +1777,16 @@ function jumpPage(arg0: string) {
:deep(.ant-form-item-control) {
.ant-form-item-control-input-content {
display: flex;
.ant-upload-select-picture-card {
width: auto;
height: auto;
max-width: 150px;
max-height: 150px;
>.ant-upload {
height: 150px;
}
}
}
}
}

View File

@ -1,49 +0,0 @@
<template>
<a-modal
v-model:visible="dialog.visible"
title="集成菜单"
width="600px"
@ok="dialog.handleOk"
class="edit-dialog-container"
:confirmLoading="dialog.loading"
cancelText="取消"
okText="确定"
>
</a-modal>
</template>
<script setup lang="ts">
const emits = defineEmits(['confirm']);
//
const dialog = reactive({
visible: false,
loading: false,
handleOk: () => {
emits('confirm');
},
/**
* 设置表单类型
* @param type 弹窗类型
* @param defaultForm 表单回显对象
*/
changeVisible: () => {
dialog.visible = true;
}
});
//
defineExpose({
openDialog: dialog.changeVisible,
});
</script>
<style scoped>
</style>

View File

@ -88,6 +88,7 @@ const tableData = computed(() => {
return props.value.slice((current.value - 1) * 10, current.value * 10);
});
if(props.value.length < 1) addRow()
watch(
() => props.value,
(n, o) => {

View File

@ -19,7 +19,7 @@ export type formType = {
name: string;
provider: applyType;
integrationModes: string[];
config: string;
config?: string;
description: string;
page: { // 页面集成
baseUrl: string,
@ -54,7 +54,7 @@ export type formType = {
roleIdList: string[], // 角色列表
orgIdList: string[], // 部门列表
ipWhiteList: string, // IP白名单
signature: 'MD5' | 'SHA256' | '', // 签名方式, 可选值MD5SHA256
signature?: 'MD5' | 'SHA256' | '', // 签名方式, 可选值MD5SHA256
enableOAuth2: boolean, // 是否启用OAuth2
},
sso: { // 统一单点登陆集成

View File

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

View File

@ -0,0 +1,196 @@
<template>
<a-modal
v-model:visible="dialog.visible"
title="集成菜单"
width="600px"
@ok="dialog.handleOk"
@cancel="dialog.cancel"
class="edit-dialog-container"
:confirmLoading="dialog.loading"
cancelText="取消"
okText="确定"
>
<a-select
v-model:value="form.checkedSystem"
@change="(value) => value && getTree(value as string)"
style="width: 200px"
placeholder="请选择集成系统"
>
<a-select-option
v-for="item in form.systemList"
:value="item.value"
>{{ item.label }}</a-select-option
>
</a-select>
<p style="margin: 20px 0 0 0" v-show="form.menuTree.length > 0">当前集成菜单</p>
<a-tree
v-model:checkedKeys="form.checkedMenu"
v-model:expandedKeys="form.expandedKeys"
checkable
:tree-data="form.menuTree"
:fieldNames="{ key: 'id', title: 'name' }"
@check="treeCheck"
>
<template #title="{ name }">
<span>{{ name }}</span>
</template>
</a-tree>
</a-modal>
</template>
<script setup lang="ts">
import { optionItemType } from '@/views/system/DataSource/typing';
import { applyType } from '../Save/typing';
import {
getOwner_api,
getOwnerStandalone_api,
getOwnerTree_api,
getOwnerTreeStandalone_api,
saveOwnerMenu_api,
} from '@/api/system/apply';
import { CheckInfo } from 'ant-design-vue/lib/vc-tree/props';
import { useMenuStore } from '@/store/menu';
import { message } from 'ant-design-vue';
import { getMenuTree_api } from '@/api/system/menu';
const menuStory = useMenuStore();
const props = defineProps<{
mode: 'add' | 'edit';
}>();
//
const dialog = reactive({
visible: false,
loading: false,
handleOk: () => {
const items = filterTree(form.menuTree, [
...form.checkedMenu,
...form.half,
]);
if (form.checkedSystem) {
if (items && items.length !== 0) {
saveOwnerMenu_api('iot', form.id, items).then((resp) => {
if (resp.status === 200) {
message.success('操作成功');
dialog.visible = false;
}
});
} else {
message.warning('请勾选配置菜单');
}
} else {
message.warning('请选择所属系统');
}
},
cancel: () => {
if (props.mode === 'add')
menuStory.jumpPage('system/Apply/Save', {}, { id: form.id });
dialog.visible = false;
},
changeVisible: (id: string, provider: applyType) => {
form.id = id;
form.provider = provider;
form.checkedSystem = undefined;
form.checkedMenu = [];
dialog.visible = true;
if (id) {
getSystemList();
getMenus();
}
},
});
//
defineExpose({
openDialog: dialog.changeVisible,
});
const form = reactive({
id: '',
checkedSystem: '' as undefined | string,
checkedMenu: [] as string[],
expandedKeys: [] as string[],
half: [] as string[],
provider: '' as applyType,
systemList: [] as optionItemType[],
menuTree: [] as any[],
});
/**
* 与集成系统关联的菜单
* @param params
*/
function getTree(params: string) {
const api =
form.provider === 'internal-standalone'
? getOwnerTreeStandalone_api(form.id, params)
: getOwnerTree_api(params);
api.then((resp: any) => {
form.menuTree = resp.result;
form.expandedKeys = resp.result.map((item: any) => item.id);
});
}
/**
* 获取当前用户可访问菜单
*/
function getMenus() {
const params = {
terms: [
{
column: 'appId',
value: form.id,
},
],
};
getMenuTree_api(params).then((resp: any) => {
if (resp.status === 200) {
form.menuTree = resp.result;
const keys = resp.result.map((item: any) => item.id) as string[];
form.expandedKeys = keys;
form.checkedMenu = keys;
}
});
}
/**
* 获取集成系统选项
*/
function getSystemList() {
const api =
form.provider === 'internal-standalone'
? getOwnerStandalone_api(form.id, ['iot'])
: getOwner_api(['iot']);
api.then((resp: any) => {
if (resp.status === 200) {
form.systemList = resp.result.map((item: string) => ({
label: item,
value: item,
}));
}
});
}
//
function treeCheck(checkedKeys: any, e: CheckInfo) {
form.checkedMenu = checkedKeys;
form.half = e.halfCheckedKeys as string[];
}
//-
function filterTree(nodes: any[], list: any[]) {
if (!nodes?.length) {
return nodes;
}
return nodes.filter((it) => {
//
if (list.indexOf(it.id) <= -1) {
return false;
}
//
it.children = filterTree(it.children, list);
return true;
});
}
</script>
<style scoped></style>

View File

@ -1,5 +1,5 @@
<template>
<page-container class="apply-container">
<page-container>
<div class="apply-container">
<Search :columns="columns" @search="search" />
@ -42,6 +42,7 @@
enabled: 'success',
disabled: 'error',
}"
hasMark
>
<template #img>
<slot name="img">
@ -118,6 +119,14 @@
</PermissionButton>
</a-tooltip>
</template>
<template #mark>
<AIcon
type="EyeOutlined"
style="font-size: 24px"
@click="() => table.toSave(slotProps.id, true)"
/>
</template>
</CardBox>
</template>
@ -151,11 +160,15 @@
</template>
</JTable>
</div>
<div class="dialogs">
<MenuDialog ref="dialogRef" mode="edit" />
</div>
</page-container>
</template>
<script setup lang="ts" name="Apply">
import PermissionButton from '@/components/PermissionButton/index.vue';
import MenuDialog from './componenets/MenuDialog.vue';
import {
getApplyList_api,
changeApplyStatus_api,
@ -251,9 +264,10 @@ const columns = [
},
];
const params = ref({});
const search = (newParams: any) => (params.value = {...newParams});
const search = (newParams: any) => (params.value = { ...newParams });
const tableRef = ref();
const dialogRef = ref();
const table = {
refresh: () => {
tableRef.value.reload();
@ -344,7 +358,10 @@ const table = {
title: '集成菜单',
},
icon: 'MenuUnfoldOutlined',
onClick: () => {},
onClick: () => {
dialogRef.value &&
dialogRef.value.openDialog(data.id, data.provider);
},
});
// api
if (otherServers.includes('apiServer'))
@ -357,7 +374,13 @@ const table = {
title: '赋权',
},
icon: 'icon-fuquan',
onClick: () => {},
onClick: () => {
menuStory.jumpPage(
'system/Apply/Api',
{},
{ code: data.id },
);
},
},
{
permission: true,
@ -367,7 +390,13 @@ const table = {
title: '查看API',
},
icon: 'icon-chakanAPI',
onClick: () => {},
onClick: () => {
menuStory.jumpPage(
'system/Apply/View',
{},
{ code: data.id },
);
},
},
);
//

View File

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

View File

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

View File

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

View File

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