Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
7d9cd9c5c0
|
@ -0,0 +1,53 @@
|
|||
import server from '@/utils/request';
|
||||
import { BASE_API_PATH } from '@/utils/variable';
|
||||
|
||||
export const FIRMWARE_UPLOAD = `${BASE_API_PATH}/file/upload`;
|
||||
|
||||
export const save = (data: object) => server.post(`/firmware`, data);
|
||||
|
||||
export const update = (data: object) => server.patch(`/firmware`, data);
|
||||
|
||||
export const remove = (id: string) => server.remove(`/firmware/${id}`);
|
||||
|
||||
export const query = (data: object) => server.post(`/firmware/_query/`, data);
|
||||
|
||||
export const querySystemApi = (data?: object) =>
|
||||
server.post(`/system/config/scopes`, data);
|
||||
|
||||
export const task = (data: Record<string, unknown>) =>
|
||||
server.post(`/firmware/upgrade/task/detail/_query`, data);
|
||||
|
||||
export const taskById = (id: string) =>
|
||||
server.get(`/firmware/upgrade/task/${id}`);
|
||||
|
||||
export const saveTask = (data: Record<string, unknown>) =>
|
||||
server.post(`/firmware/upgrade/task`, data);
|
||||
|
||||
export const deleteTask = (id: string) =>
|
||||
server.remove(`/firmware/upgrade/task/${id}`);
|
||||
|
||||
export const history = (data: Record<string, unknown>) =>
|
||||
server.post(`/firmware/upgrade/history/_query`, data);
|
||||
|
||||
export const historyCount = (data: Record<string, unknown>) =>
|
||||
server.post(`/firmware/upgrade/history/_count`, data);
|
||||
|
||||
export const startTask = (id: string, data: string[]) =>
|
||||
server.post(`/firmware/upgrade/task/${id}/_start`, data);
|
||||
|
||||
export const stopTask = (id: string) =>
|
||||
server.post(`/firmware/upgrade/task/${id}/_stop`);
|
||||
|
||||
export const startOneTask = (data: string[]) =>
|
||||
server.post(`/firmware/upgrade/task/_start`, data);
|
||||
|
||||
// export const queryProduct = (data?: any) =>
|
||||
// server.post(`/device-product/_query/no-paging`, data);
|
||||
export const queryProduct = (data?: any) =>
|
||||
server.post(`/device-product/detail/_query/no-paging`, data);
|
||||
|
||||
export const queryDevice = () =>
|
||||
server.get(`/device/instance/_query/no-paging?paging=false`);
|
||||
|
||||
export const validateVersion = (productId: string, versionOrder: number) =>
|
||||
server.get(`/firmware/${productId}/${versionOrder}/exists`);
|
|
@ -83,22 +83,22 @@ export const batchDeleteDevice = (data: string[]) => server.put(`/device-instanc
|
|||
* @param type 文件类型
|
||||
* @returns
|
||||
*/
|
||||
export const deviceTemplateDownload = (productId: string, type: string) => `${BASE_API_PATH}/device-instance/${productId}/template.${type}`
|
||||
export const deviceTemplateDownload = (productId: string, type: string) => `${BASE_API_PATH}/device-instance/${productId}/template.${type}`
|
||||
|
||||
/**
|
||||
* 设备导入
|
||||
* @param productId 产品id
|
||||
* @param type 文件类型
|
||||
* @returns
|
||||
*/
|
||||
export const deviceImport = (productId: string, fileUrl: string, autoDeploy: boolean) => `${BASE_API_PATH}/device-instance/${productId}/import?fileUrl=${fileUrl}&autoDeploy=${autoDeploy}&:X_Access_Token=${LocalStore.get(TOKEN_KEY)}`
|
||||
|
||||
/**
|
||||
* 设备导出
|
||||
* @param productId 产品id
|
||||
* @param type 文件类型
|
||||
* @returns
|
||||
*/
|
||||
/**
|
||||
* 设备导入
|
||||
* @param productId 产品id
|
||||
* @param type 文件类型
|
||||
* @returns
|
||||
*/
|
||||
export const deviceImport = (productId: string, fileUrl: string, autoDeploy: boolean) => `${BASE_API_PATH}/device-instance/${productId}/import?fileUrl=${fileUrl}&autoDeploy=${autoDeploy}&:X_Access_Token=${LocalStore.get(TOKEN_KEY)}`
|
||||
|
||||
/**
|
||||
* 设备导出
|
||||
* @param productId 产品id
|
||||
* @param type 文件类型
|
||||
* @returns
|
||||
*/
|
||||
export const deviceExport = (productId: string, type: string) => `${BASE_API_PATH}/device-instance${!!productId ? '/' + productId : ''}/export.${type}`
|
||||
|
||||
/**
|
||||
|
@ -143,7 +143,7 @@ export const _disconnect = (id: string) => server.post(`/device-instance/${id}/d
|
|||
*/
|
||||
export const queryUserListNoPaging = () => server.post(`/user/_query/no-paging`, {
|
||||
paging: false,
|
||||
sorts: [{name: 'name', order: "asc"}]
|
||||
sorts: [{ name: 'name', order: "asc" }]
|
||||
})
|
||||
|
||||
/**
|
||||
|
@ -347,4 +347,59 @@ export const settingProperties = (deviceId: string, data: any) => server.put(`/d
|
|||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const execute = (id: string, action: string, data: any) => server.post(`/device/invoked/${id}/function/${action}`, data)
|
||||
export const execute = (id: string, action: string, data: any) => server.post(`/device/invoked/${id}/function/${action}`, data)
|
||||
|
||||
/**
|
||||
* 查询通道列表不分页
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const queryChannelNoPaging = (data: any) => server.post(`data-collect/channel/_query/no-paging`, data)
|
||||
|
||||
/**
|
||||
* 查询采集器列表不分页
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const queryCollectorNoPaging = (data: any) => server.post(`/data-collect/collector/_query/no-paging`, data)
|
||||
|
||||
/**
|
||||
* 查询点位列表不分页
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const queryPointNoPaging = (data: any) => server.post(`/data-collect/point/_query/no-paging`, data)
|
||||
|
||||
/**
|
||||
* 查询映射列表
|
||||
* @param thingType
|
||||
* @param thingId
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export const queryMapping = (thingType: string, thingId: any, params?: any) => server.get(`/things/collector/${thingType}/${thingId}/_query`, params)
|
||||
|
||||
/**
|
||||
* 删除映射
|
||||
* @param thingType
|
||||
* @param thingId
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const removeMapping = (thingType: string, thingId: any, data?: any) => server.post(`/things/collector/${thingType}/${thingId}/_delete`, data)
|
||||
|
||||
/**
|
||||
* 映射树
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const treeMapping = (data?: any) => server.post(`/data-collect/channel/_all/tree`, data)
|
||||
|
||||
/**
|
||||
* 保存映射
|
||||
* @param thingId
|
||||
* @param provider
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const saveMapping = (thingId: any, provider: string, data?: any) => server.patch(`/things/collector/device/${thingId}/${provider}`, data)
|
|
@ -1,5 +1,18 @@
|
|||
import server from '@/utils/request';
|
||||
import { LevelItem } from '@/views/rule-engine/Alarm/Config/typing';
|
||||
/**
|
||||
* 查询等级
|
||||
*/
|
||||
export const queryLevel = () => server.get('/alarm/config/default/level');
|
||||
export const queryLevel = () => server.get('/alarm/config/default/level');
|
||||
/**
|
||||
* 保存告警等级
|
||||
*/
|
||||
export const saveLevel = (data:LevelItem[]) => server.patch('/alarm/config/default/level',data);
|
||||
/**
|
||||
* 获取数据流转数据
|
||||
*/
|
||||
export const getDataExchange = (type:'consume' | 'producer') => server.get(`/alarm/config/${type}/data-exchange`);
|
||||
/**
|
||||
* 保存告警数据输出
|
||||
*/
|
||||
export const saveOutputData = (data:any) => server.patch('/alarm/config/data-exchange',data);
|
|
@ -3,4 +3,24 @@ import server from '@/utils/request';
|
|||
// 获取tree数据-第一层
|
||||
export const getTreeOne_api = () => server.get(`/v3/api-docs/swagger-config`);
|
||||
// 获取tree数据-第二层
|
||||
export const getTreeTwo_api = (name:string) => server.get(`/v3/api-docs/${name}`);
|
||||
export const getTreeTwo_api = (name: string) => server.get(`/v3/api-docs/${name}`);
|
||||
|
||||
|
||||
/**
|
||||
* 获取已授权的接口ID
|
||||
* @param id 第三方平台的ID
|
||||
*/
|
||||
export const getApiGranted_api = (id: string) => server.get(`/application/${id}/granted`);
|
||||
/**
|
||||
* 获取可授权的接口ID
|
||||
*/
|
||||
export const apiOperations_api = () => server.get(`/application/operations`);
|
||||
|
||||
/**
|
||||
* 新增可授权的接口ID
|
||||
*/
|
||||
export const addOperations_api = (data:object) => server.patch(`/application/operations/_batch`,data);
|
||||
/**
|
||||
* 删除可授权的接口ID
|
||||
*/
|
||||
export const delOperations_api = (data:object) => server.remove(`/application/operations/_batch`,{},{data});
|
|
@ -49,6 +49,9 @@ const iconKeys = [
|
|||
'PartitionOutlined',
|
||||
'ShareAltOutlined',
|
||||
'playCircleOutlined',
|
||||
'RightOutlined',
|
||||
'FileTextOutlined',
|
||||
'UploadOutlined'
|
||||
]
|
||||
|
||||
const Icon = (props: {type: string}) => {
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<a-modal :mask-closable="false" visible width="70vw" title="设置属性规则" @cancel="handleCancel" @ok="handleOk">
|
||||
<div class="advance-box">
|
||||
<div class="left">
|
||||
<Editor
|
||||
mode="advance"
|
||||
key="advance"
|
||||
v-model:value="_value"
|
||||
/>
|
||||
<Debug
|
||||
:virtualRule="{
|
||||
...virtualRule,
|
||||
script: _value,
|
||||
}"
|
||||
:id="id"
|
||||
/>
|
||||
</div>
|
||||
<div class="right">
|
||||
<Operator :id="id" />
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script setup lang="ts" name="Advance">
|
||||
import Editor from '../Editor/index.vue'
|
||||
import Debug from '../Debug/index.vue'
|
||||
import Operator from '../Operator/index.vue'
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', data: string | undefined): void;
|
||||
(e: 'change', data: string): void;
|
||||
}
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const props = defineProps({
|
||||
value: String,
|
||||
id: String,
|
||||
virtualRule: Object
|
||||
})
|
||||
|
||||
const _value = ref<string | undefined>(props.value)
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('change', 'simple')
|
||||
}
|
||||
const handleOk = () => {
|
||||
emit('update:value', _value.value)
|
||||
emit('change', 'simple')
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.advance-box {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
|
||||
.left {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 30%;
|
||||
margin-left: 10px;
|
||||
padding-left: 10px;
|
||||
border-left: 1px solid lightgray;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,266 @@
|
|||
<template>
|
||||
<div class="debug-container">
|
||||
<div class="left">
|
||||
<div class="header">
|
||||
<div>
|
||||
<div class="title">
|
||||
属性赋值
|
||||
<div class="description">请对上方规则使用的属性进行赋值</div>
|
||||
</div>
|
||||
<div v-if="!isBeginning && virtualRule?.type === 'window'" class="action" @click="runScriptAgain">
|
||||
<a style="margin-left: 75px;">发送数据</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-table :columns="columns" :data-source="property" :pagination="false" bordered size="small">
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'id'">
|
||||
<a-input v-model:value="record.id" size="small"></a-input>
|
||||
</template>
|
||||
<template v-if="column.key === 'current'">
|
||||
<a-input v-model:value="record.current" size="small"></a-input>
|
||||
</template>
|
||||
<template v-if="column.key === 'last'">
|
||||
<a-input v-model:value="record.last" size="small"></a-input>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<delete-outlined @click="deleteItem(index)" />
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-button type="dashed" block style="margin-top: 5px" @click="addItem">
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
添加条目
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<div>运行结果</div>
|
||||
</div>
|
||||
<div class="action">
|
||||
<div>
|
||||
<a v-if="isBeginning" @click="beginAction">
|
||||
开始运行
|
||||
</a>
|
||||
<a v-else @click="stopAction">
|
||||
停止运行
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a @click="clearAction">
|
||||
清空
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log">
|
||||
<a-descriptions>
|
||||
<a-descriptions-item v-for="item in ruleEditorStore.state.log" :label="moment(item.time).format('HH:mm:ss')" :key="item.time"
|
||||
:span="3">
|
||||
<a-tooltip placement="top" :title="item.content">
|
||||
{{ item.content }}
|
||||
</a-tooltip>
|
||||
</a-descriptions-item>
|
||||
))}
|
||||
</a-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="Debug">
|
||||
import { PropType } from 'vue';
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { useProductStore } from '@/store/product';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useRuleEditorStore } from '@/store/ruleEditor';
|
||||
import moment from 'moment';
|
||||
import { getWebSocket } from '@/utils/websocket';
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
virtualRule: Object as PropType<Record<any, any>>,
|
||||
id: String,
|
||||
})
|
||||
|
||||
const isBeginning = ref(true)
|
||||
|
||||
const runScriptAgain = () => { }
|
||||
|
||||
type propertyType = {
|
||||
id?: string,
|
||||
current?: string,
|
||||
last?: string
|
||||
}
|
||||
const property = ref<propertyType[]>([])
|
||||
|
||||
const columns = [{
|
||||
title: '属性ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id'
|
||||
}, {
|
||||
title: '当前值',
|
||||
dataIndex: 'current',
|
||||
key: 'current'
|
||||
}, {
|
||||
title: '上一值',
|
||||
dataIndex: 'last',
|
||||
key: 'last'
|
||||
}, {
|
||||
title: '',
|
||||
key: 'action'
|
||||
}]
|
||||
|
||||
const addItem = () => {
|
||||
property.value.push({})
|
||||
}
|
||||
const deleteItem = (index: number) => {
|
||||
property.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const ws = ref()
|
||||
|
||||
const virtualIdRef = ref(new Date().getTime());
|
||||
|
||||
const productStore = useProductStore()
|
||||
const ruleEditorStore = useRuleEditorStore()
|
||||
const runScript = () => {
|
||||
const metadata = productStore.current.metadata || '{}';
|
||||
const propertiesList = JSON.parse(metadata).properties || [];
|
||||
const _properties = property.value.map((item: any) => {
|
||||
const _item = propertiesList.find((i: any) => i.id === item.id);
|
||||
return { ...item, type: _item?.valueType?.type };
|
||||
});
|
||||
|
||||
if (ws.value) {
|
||||
ws.value.unsubscribe();
|
||||
}
|
||||
if (!props.virtualRule?.script) {
|
||||
isBeginning.value = true;
|
||||
message.warning('请编辑规则');
|
||||
return;
|
||||
}
|
||||
ws.value = getWebSocket(`virtual-property-debug-${ruleEditorStore.state.property}-${new Date().getTime()}`,
|
||||
'/virtual-property-debug',
|
||||
{
|
||||
virtualId: `${virtualIdRef.value}-virtual-id`,
|
||||
property: ruleEditorStore.state.property,
|
||||
virtualRule: {
|
||||
...props.virtualRule,
|
||||
},
|
||||
properties: _properties || [],
|
||||
})
|
||||
ws.value.subscribe((data: any) => {
|
||||
ruleEditorStore.state.log.push({ time: new Date().getTime(), content: JSON.stringify(data.payload) });
|
||||
})
|
||||
}
|
||||
const beginAction = () => {
|
||||
isBeginning.value = false;
|
||||
runScript();
|
||||
}
|
||||
const stopAction = () => {
|
||||
isBeginning.value = true;
|
||||
if (ws.value) {
|
||||
ws.value.unsubscribe();
|
||||
}
|
||||
}
|
||||
const clearAction = () => {
|
||||
ruleEditorStore.set('log', []);
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ws.value) {
|
||||
ws.value.unsubscribe();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.debug-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 340px;
|
||||
margin-top: 20px;
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 550px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid lightgray;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border-bottom: 1px solid lightgray;
|
||||
//justify-content: space-around;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
//width: 100%;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
height: 100%;
|
||||
|
||||
.title {
|
||||
margin: 0 10px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-left: 10px;
|
||||
color: lightgray;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
width: 150px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: 1px solid lightgray;
|
||||
border-left: none;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border-bottom: 1px solid lightgray;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
|
||||
div {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
|
||||
div {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log {
|
||||
height: 290px;
|
||||
padding: 5px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,211 @@
|
|||
<template>
|
||||
<div class="editor-box">
|
||||
<div class="top">
|
||||
<div class="left">
|
||||
<span v-for="item in symbolList.filter((t: SymbolType, i: number) => i <= 3)" :key="item.key"
|
||||
@click="handleInsertCode(item.value)">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
<span>
|
||||
<a-dropdown>
|
||||
<more-outlined />
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item v-for="item in symbolList.filter((t: SymbolType, i: number) => i > 6)" :key="item.key"
|
||||
@click="handleInsertCode(item.value)">
|
||||
{{ item.value }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span v-if="mode !== 'advance'">
|
||||
<a-tooltip :title="!id ? '请先输入标识' : '设置属性规则'">
|
||||
<fullscreen-outlined :class="!id ? 'disabled' : ''" @click="fullscreenClick" />
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor">
|
||||
<MonacoEditor v-if="loading" v-model:model-value="_value" theme="vs" ref="editor" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="Editor">
|
||||
import { FullscreenOutlined, MoreOutlined } from '@ant-design/icons-vue';
|
||||
import MonacoEditor from '@/components/MonacoEditor/index.vue';
|
||||
|
||||
interface Props {
|
||||
mode?: 'advance' | 'simple';
|
||||
id?: string;
|
||||
value?: string;
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
interface Emits {
|
||||
(e: 'change', data: string): void;
|
||||
(e: 'update:value', data: string): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
type editorType = {
|
||||
insert(val: string): void
|
||||
}
|
||||
const editor = ref<editorType>()
|
||||
|
||||
type SymbolType = {
|
||||
key: string,
|
||||
value: string
|
||||
}
|
||||
const symbolList = [
|
||||
{
|
||||
key: 'add',
|
||||
value: '+',
|
||||
},
|
||||
{
|
||||
key: 'subtract',
|
||||
value: '-',
|
||||
},
|
||||
{
|
||||
key: 'multiply',
|
||||
value: '*',
|
||||
},
|
||||
{
|
||||
key: 'divide',
|
||||
value: '/',
|
||||
},
|
||||
{
|
||||
key: 'parentheses',
|
||||
value: '()',
|
||||
},
|
||||
{
|
||||
key: 'cubic',
|
||||
value: '^',
|
||||
},
|
||||
{
|
||||
key: 'dayu',
|
||||
value: '>',
|
||||
},
|
||||
{
|
||||
key: 'dayudengyu',
|
||||
value: '>=',
|
||||
},
|
||||
{
|
||||
key: 'dengyudengyu',
|
||||
value: '==',
|
||||
},
|
||||
{
|
||||
key: 'xiaoyudengyu',
|
||||
value: '<=',
|
||||
},
|
||||
{
|
||||
key: 'xiaoyu',
|
||||
value: '<',
|
||||
},
|
||||
{
|
||||
key: 'jiankuohao',
|
||||
value: '<>',
|
||||
},
|
||||
{
|
||||
key: 'andand',
|
||||
value: '&&',
|
||||
},
|
||||
{
|
||||
key: 'huohuo',
|
||||
value: '||',
|
||||
},
|
||||
{
|
||||
key: 'fei',
|
||||
value: '!',
|
||||
},
|
||||
{
|
||||
key: 'and',
|
||||
value: '&',
|
||||
},
|
||||
{
|
||||
key: 'huo',
|
||||
value: '|',
|
||||
},
|
||||
{
|
||||
key: 'bolang',
|
||||
value: '~',
|
||||
},
|
||||
] as SymbolType[];
|
||||
|
||||
const _value = computed({
|
||||
get: () => props.value || '',
|
||||
set: (data: string) => {
|
||||
emit('update:value', data);
|
||||
}
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
loading.value = true;
|
||||
}, 100);
|
||||
})
|
||||
|
||||
const handleInsertCode = (val: string) => {
|
||||
editor.value?.insert(val)
|
||||
}
|
||||
|
||||
const fullscreenClick = () => {
|
||||
if (props.id) {
|
||||
emit('change', 'advance');
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.editor-box {
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid lightgray;
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid lightgray;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 60%;
|
||||
margin: 0 5px;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
height: 40px;
|
||||
margin: 0 10px;
|
||||
line-height: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 10%;
|
||||
margin: 0 5px;
|
||||
|
||||
span {
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: rgba(#000, 0.5);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.editor {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<div class="operator-box">
|
||||
<a-input-search @search="search" allow-clear placeholder="搜索关键字" />
|
||||
<a-tree class="tree" @select="selectTree" :field-names="{ title: 'name', key: 'id', }" auto-expand-parent
|
||||
:tree-data="data">
|
||||
<template #title="node">
|
||||
<div class="node">
|
||||
<div>{{ node.name }}</div>
|
||||
<div :class="node.children?.length > 0 ? 'parent' : 'add'">
|
||||
<a-popover v-if="node.type === 'property'" placement="right" title="请选择使用值" @visibleChange="setVisible">
|
||||
<template #content>
|
||||
<a-space direction="vertical">
|
||||
<a-tooltip placement="right" title="实时值为空时获取上一有效值补齐,实时值不为空则使用实时值">
|
||||
<a-button type="text" @click="recentClick(node)">
|
||||
$recent实时值
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip placement="right" title="实时值的上一有效值">
|
||||
<a-button @click="lastClick(node)" type="text">
|
||||
上一值
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
<a @click="setVisible(true)">添加</a>
|
||||
</a-popover>
|
||||
|
||||
<a v-else @click="addClick(node)">
|
||||
添加
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-tree>
|
||||
<div class="explain">
|
||||
<ReactMarkdown>{{ item?.description || '' }}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="Operator">
|
||||
import type { OperatorItem } from './typings';
|
||||
import { treeFilter } from '@/utils/tree'
|
||||
import { Store } from 'jetlinks-store';
|
||||
|
||||
const item = ref<Partial<OperatorItem>>()
|
||||
const data = ref<OperatorItem[]>([])
|
||||
const dataRef = ref<OperatorItem[]>([])
|
||||
const visible = ref(false)
|
||||
|
||||
const search = (value: string) => {
|
||||
if (value) {
|
||||
const nodes = treeFilter(dataRef.value, value, 'name') as OperatorItem[];
|
||||
data.value = nodes;
|
||||
} else {
|
||||
data.value = dataRef.value;
|
||||
}
|
||||
};
|
||||
|
||||
const selectTree = (k: any, info: any) => {
|
||||
item.value = info.node as unknown as OperatorItem;
|
||||
}
|
||||
|
||||
const setVisible = (_visible: boolean) => {
|
||||
visible.value = !!visible
|
||||
}
|
||||
|
||||
const recentClick = (node: OperatorItem) => {
|
||||
Store.set('add-operator-value', `$recent("${node.id}")`);
|
||||
setVisible(!visible.value);
|
||||
}
|
||||
const lastClick = (node: OperatorItem) => {
|
||||
Store.set('add-operator-value', `$lastState("${node.id}")`);
|
||||
setVisible(!visible.value);
|
||||
}
|
||||
const addClick = (node: OperatorItem) => {
|
||||
Store.set('add-operator-value', node.code);
|
||||
setVisible(true);
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.border {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border-top: 1px solid lightgray;
|
||||
}
|
||||
|
||||
.operator-box {
|
||||
width: 100%;
|
||||
|
||||
.explain {
|
||||
.border;
|
||||
}
|
||||
|
||||
.tree {
|
||||
.border;
|
||||
|
||||
height: 350px;
|
||||
overflow-y: auto;
|
||||
|
||||
.node {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 220px;
|
||||
|
||||
//.add {
|
||||
// display: none;
|
||||
//}
|
||||
//
|
||||
//&:hover .add {
|
||||
// display: block;
|
||||
//}
|
||||
|
||||
.parent {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,10 @@
|
|||
import type { TreeNode } from '@/utils/tree';
|
||||
|
||||
interface OperatorItem extends TreeNode {
|
||||
id: string;
|
||||
name: string;
|
||||
key: string;
|
||||
description: string;
|
||||
code: string;
|
||||
children: OperatorItem[];
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<Editor key="simple" @change="change" v-model:value="_value" :id="id" />
|
||||
{{ ruleEditorStore.state.model }}
|
||||
<Advance v-if="ruleEditorStore.state.model === 'advance'" :model="ruleEditorStore.state.model"
|
||||
:virtualRule="virtualRule" :id="id" @change="change" />
|
||||
</template>
|
||||
<script setup lang="ts" name="FRuleEditor">
|
||||
import { useRuleEditorStore } from '@/store/ruleEditor'
|
||||
import Editor from './Editor/index.vue'
|
||||
import Advance from './Advance/index.vue'
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
property?: string;
|
||||
virtualRule?: any;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', data: string): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const _value = computed({
|
||||
get: () => props.value,
|
||||
set: (val: string) => {
|
||||
emit('update:value', val)
|
||||
}
|
||||
})
|
||||
|
||||
const ruleEditorStore = useRuleEditorStore()
|
||||
|
||||
const change = (v: string) => {
|
||||
ruleEditorStore.set('model', v);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
ruleEditorStore.set('property', props.property)
|
||||
ruleEditorStore.set('code', props.value);
|
||||
})
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<a-form-item :name="name.concat(['script'])">
|
||||
<f-rule-editor v-model:value="value.script" :id="id" ></f-rule-editor>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<script setup lang="ts" name="VirtualRuleParam">
|
||||
import { PropType } from 'vue';
|
||||
import FRuleEditor from '@/components/FRuleEditor/index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
type: 'script',
|
||||
})
|
||||
},
|
||||
name: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => ([])
|
||||
},
|
||||
id: String
|
||||
})
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', data: Record<any, any>): void;
|
||||
}
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
onMounted(() => {
|
||||
emit('update:value', {
|
||||
...props.value,
|
||||
type: 'script'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
|
@ -73,6 +73,27 @@ watchEffect(() => {
|
|||
editorFormat();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
const insert = (val) => {
|
||||
if (!instance) return
|
||||
const position = instance.getPosition();
|
||||
instance.executeEdits(instance.getValue(), [
|
||||
{
|
||||
range: new monaco.Range(
|
||||
position?.lineNumber,
|
||||
position?.column,
|
||||
position?.lineNumber,
|
||||
position?.column,
|
||||
),
|
||||
text: val,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
editorFormat,
|
||||
insert,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<a-table rowKey="id" :rowSelection="rowSelection" :columns="[..._columns]" :dataSource="_dataSource" :pagination="false">
|
||||
<a-table rowKey="operationId" :rowSelection="rowSelection" :columns="[..._columns]" :dataSource="_dataSource" :pagination="false">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- <template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
|
|
|
@ -29,7 +29,7 @@ export default [
|
|||
},
|
||||
{
|
||||
path: '/system/Api',
|
||||
component: () => import('@/views/system/Apply/Api/index.vue')
|
||||
component: () => import('@/views/system/Platforms/index.vue')
|
||||
},
|
||||
|
||||
// end: 测试用, 可删除
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
type RuleEditorType = {
|
||||
model: 'simple' | 'advance';
|
||||
code: string;
|
||||
property?: string;
|
||||
log: {
|
||||
content: string;
|
||||
time: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const useRuleEditorStore = defineStore({
|
||||
id: 'ruleEditor',
|
||||
state: () => ({
|
||||
state: {
|
||||
model: 'simple',
|
||||
code: '',
|
||||
log: [],
|
||||
} as RuleEditorType
|
||||
}),
|
||||
actions: {
|
||||
set(key: string, value: any) {
|
||||
this.state[key] = value
|
||||
}
|
||||
}
|
||||
})
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
场景
|
||||
树形数据过滤, 并保留原有树形结构不变, 即如果有子集被选中,父级同样保留。
|
||||
思路
|
||||
对数据进行处理,根据过滤标识对匹配的数据添加标识。如visible:true
|
||||
对有标识的子集的父级添加标识visible:true
|
||||
根据visible标识对数据进行递归过滤,得到最后的数据
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
export type TreeNode = {
|
||||
id: string;
|
||||
name: string;
|
||||
children: TreeNode[];
|
||||
visible?: boolean;
|
||||
} & Record<string, any>;
|
||||
|
||||
/*
|
||||
* 对表格数据进行处理
|
||||
* data 树形数据数组
|
||||
* filter 过滤参数值
|
||||
* filterType 过滤参数名
|
||||
*/
|
||||
export function treeFilter(data: TreeNode[], filter: string, filterType: string): TreeNode[] {
|
||||
const _data = _.cloneDeep(data);
|
||||
const traverse = (item: TreeNode[]) => {
|
||||
item.forEach((child) => {
|
||||
child.visible = filterMethod(filter, child, filterType);
|
||||
if (child.children) traverse(child.children);
|
||||
if (!child.visible && child.children?.length) {
|
||||
const visible = !child.children.some((c) => c.visible);
|
||||
child.visible = !visible;
|
||||
}
|
||||
});
|
||||
};
|
||||
traverse(_data);
|
||||
return filterDataByVisible(_data);
|
||||
}
|
||||
|
||||
// 根据传入的值进行数据匹配, 并返回匹配结果
|
||||
function filterMethod(val: string, data: TreeNode, filterType: string | number) {
|
||||
return data[filterType].includes(val);
|
||||
}
|
||||
|
||||
// 递归过滤符合条件的数据
|
||||
function filterDataByVisible(data: TreeNode[]) {
|
||||
return data.filter((item) => {
|
||||
if (item.children) {
|
||||
item.children = filterDataByVisible(item.children);
|
||||
}
|
||||
return item.visible;
|
||||
});
|
||||
}
|
||||
|
||||
const mockData = [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [],
|
||||
name: '加',
|
||||
id: 'operator-1',
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
name: '减',
|
||||
id: 'operator-2',
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
name: '乘',
|
||||
id: 'operator-3',
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
name: '除',
|
||||
id: 'operator-4',
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
name: '括号',
|
||||
id: 'operator-5',
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
name: '按位异或',
|
||||
id: 'operator-6',
|
||||
},
|
||||
],
|
||||
name: '操作符',
|
||||
id: 'operator',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [],
|
||||
name: 'if',
|
||||
id: 'if',
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
name: 'for',
|
||||
id: 'for',
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
name: 'while',
|
||||
id: 'while',
|
||||
},
|
||||
],
|
||||
name: '控制语句',
|
||||
id: 'control',
|
||||
},
|
||||
];
|
||||
const myTree = treeFilter(mockData, '操作', 'name');
|
||||
|
||||
console.log(JSON.stringify(myTree), 'mytree');
|
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<a-spin :spinning="loading">
|
||||
<a-input
|
||||
placeholder="请上传文件"
|
||||
v-model:value="fileValue"
|
||||
style="width: calc(100% - 110px)"
|
||||
:disabled="true"
|
||||
/>
|
||||
<a-upload
|
||||
name="file"
|
||||
:multiple="true"
|
||||
:action="FIRMWARE_UPLOAD"
|
||||
:headers="{
|
||||
[TOKEN_KEY]: LocalStore.get(TOKEN_KEY),
|
||||
}"
|
||||
@change="handleChange"
|
||||
:showUploadList="false"
|
||||
class="upload-box"
|
||||
>
|
||||
<a-button type="primary">
|
||||
<div>
|
||||
<AIcon type="UploadOutlined" /><span class="upload-text"
|
||||
>上传文件</span
|
||||
>
|
||||
</div>
|
||||
</a-button>
|
||||
</a-upload>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="FileUpload">
|
||||
import { LocalStore } from '@/utils/comm';
|
||||
import { TOKEN_KEY } from '@/utils/variable';
|
||||
import { FIRMWARE_UPLOAD, querySystemApi } from '@/api/device/firmware';
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { UploadChangeParam, UploadProps } from 'ant-design-vue';
|
||||
import { notification as Notification } from 'ant-design-vue';
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'update:extraValue', 'change']);
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: () => '',
|
||||
},
|
||||
});
|
||||
|
||||
const fileValue = ref(props.modelValue);
|
||||
const loading = ref(false);
|
||||
|
||||
const handleChange = async (info: UploadChangeParam) => {
|
||||
loading.value = true;
|
||||
if (info.file.status === 'done') {
|
||||
loading.value = false;
|
||||
const result = info.file.response?.result;
|
||||
const api = await querySystemApi(['paths']);
|
||||
const path = api.result[0]?.properties
|
||||
? api.result[0]?.properties['base-path']
|
||||
: '';
|
||||
const f = `${path}/file/${result.id}?accessKey=${result.others.accessKey}`;
|
||||
message.success('上传成功!');
|
||||
fileValue.value = f;
|
||||
emit('update:modelValue', f);
|
||||
emit('update:extraValue', result);
|
||||
} else {
|
||||
if (info.file.error) {
|
||||
Notification.error({
|
||||
// key: '403',
|
||||
message: '系统提示',
|
||||
description: '系统未知错误,请反馈给管理员',
|
||||
});
|
||||
loading.value = false;
|
||||
} else if (info.file.response) {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
fileValue.value = value;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.upload-box {
|
||||
:deep(.ant-btn) {
|
||||
width: 110px;
|
||||
}
|
||||
.upload-text {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,388 @@
|
|||
<template lang="">
|
||||
<a-modal
|
||||
:title="data.id ? '编辑' : '新增'"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
:visible="true"
|
||||
width="700px"
|
||||
:confirm-loading="loading"
|
||||
@cancel="handleCancel"
|
||||
@ok="handleOk"
|
||||
>
|
||||
<a-form
|
||||
class="form"
|
||||
layout="vertical"
|
||||
:model="formData"
|
||||
name="basic"
|
||||
autocomplete="off"
|
||||
>
|
||||
<a-row :gutter="[24, 0]">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="名称" v-bind="validateInfos.name">
|
||||
<a-input
|
||||
placeholder="请输入名称"
|
||||
v-model:value="formData.name"
|
||||
/></a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24"
|
||||
><a-form-item
|
||||
label="所属产品"
|
||||
v-bind="validateInfos.productId"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="formData.productId"
|
||||
:options="productOptions"
|
||||
placeholder="请选择所属产品"
|
||||
allowClear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
/> </a-form-item
|
||||
></a-col>
|
||||
<a-col :span="12"
|
||||
><a-form-item label="版本号" v-bind="validateInfos.version">
|
||||
<a-input
|
||||
placeholder="请输入版本号"
|
||||
v-model:value="formData.version" /></a-form-item
|
||||
></a-col>
|
||||
<a-col :span="12"
|
||||
><a-form-item
|
||||
label="版本序号"
|
||||
v-bind="validateInfos.versionOrder"
|
||||
>
|
||||
<a-input-number
|
||||
placeholder="请输入版本序号"
|
||||
style="width: 100%"
|
||||
:min="1"
|
||||
:max="99999"
|
||||
v-model:value="
|
||||
formData.versionOrder
|
||||
" /></a-form-item
|
||||
></a-col>
|
||||
<a-col :span="12"
|
||||
><a-form-item
|
||||
label="签名方式"
|
||||
v-bind="validateInfos.signMethod"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="formData.signMethod"
|
||||
:options="[
|
||||
{ label: 'MD5', value: 'md5' },
|
||||
{ label: 'SHA256', value: 'sha256' },
|
||||
]"
|
||||
placeholder="请选择签名方式"
|
||||
allowClear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
@change="changeSignMethod"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12"
|
||||
><a-form-item label="签名" v-bind="validateInfos.sign">
|
||||
<a-input
|
||||
placeholder="请输入签名"
|
||||
v-model:value="formData.sign" /></a-form-item
|
||||
></a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="固件上传" v-bind="validateInfos.url">
|
||||
<FileUpload
|
||||
v-model:modelValue="formData.url"
|
||||
v-model:extraValue="extraValue"
|
||||
/> </a-form-item
|
||||
></a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="其他配置"
|
||||
v-bind="validateInfos.properties"
|
||||
>
|
||||
<a-form
|
||||
:class="
|
||||
dynamicValidateForm.properties.length !== 0 &&
|
||||
'formRef'
|
||||
"
|
||||
ref="formRef"
|
||||
name="dynamic_form_nest_item"
|
||||
:model="dynamicValidateForm"
|
||||
>
|
||||
<div
|
||||
class="formRef-content"
|
||||
v-for="(
|
||||
propertie, index
|
||||
) in dynamicValidateForm.properties"
|
||||
:key="propertie.keyid"
|
||||
>
|
||||
<a-form-item
|
||||
:label="index === 0 && 'Key'"
|
||||
class="formRef-form-item"
|
||||
:name="['properties', index, 'id']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '请输入KEY',
|
||||
}"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="propertie.id"
|
||||
placeholder="请输入KEY"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label="index === 0 && 'Value'"
|
||||
class="formRef-form-item"
|
||||
:name="['properties', index, 'value']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '请输入VALUE',
|
||||
}"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="propertie.value"
|
||||
placeholder="请输入VALUE"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label="index === 0 && '操作'"
|
||||
class="formRef-form-item"
|
||||
style="width: 10%"
|
||||
>
|
||||
<a-popconfirm
|
||||
title="确认删除吗?"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="removeUser(propertie)"
|
||||
>
|
||||
<AIcon type="DeleteOutlined" />
|
||||
</a-popconfirm>
|
||||
</a-form-item>
|
||||
</div>
|
||||
<a-form-item class="formRef-form-item-add">
|
||||
<a-button type="dashed" block @click="addUser">
|
||||
<AIcon type="PlusOutlined" />
|
||||
添加
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-form-item></a-col
|
||||
>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="说明"
|
||||
v-bind="validateInfos.description"
|
||||
>
|
||||
<a-textarea
|
||||
placeholder="请输入说明"
|
||||
v-model:value="formData.description"
|
||||
:maxlength="200"
|
||||
:rows="3"
|
||||
showCount
|
||||
/> </a-form-item
|
||||
></a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { message, Form } from 'ant-design-vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import type { UploadChangeParam } from 'ant-design-vue';
|
||||
import FileUpload from './FileUpload.vue';
|
||||
import { save, update, queryProduct } from '@/api/device/firmware';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import type { Properties } from '../type';
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const dynamicValidateForm = reactive<{ properties: Properties[] }>({
|
||||
properties: [],
|
||||
});
|
||||
|
||||
const removeUser = (item: Properties) => {
|
||||
let index = dynamicValidateForm.properties.indexOf(item);
|
||||
if (index !== -1) {
|
||||
dynamicValidateForm.properties.splice(index, 1);
|
||||
}
|
||||
};
|
||||
const addUser = () => {
|
||||
dynamicValidateForm.properties.push({
|
||||
id: '',
|
||||
value: '',
|
||||
keyid: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
const loading = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
const productOptions = ref([]);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const id = props.data.id;
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
productId: undefined,
|
||||
version: '',
|
||||
versionOrder: '',
|
||||
signMethod: undefined,
|
||||
sign: '',
|
||||
url: '',
|
||||
properties: [],
|
||||
description: '',
|
||||
});
|
||||
|
||||
const extraValue = ref({});
|
||||
|
||||
const validatorSign = async (_: Record<string, any>, value: string) => {
|
||||
const { signMethod, url } = formData.value;
|
||||
if (value && !!signMethod && !!url && !extraValue.value) {
|
||||
return extraValue.value[signMethod] !== value
|
||||
? Promise.reject('签名不一致,请检查文件是否上传正确')
|
||||
: Promise.resolve();
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const { resetFields, validate, validateInfos } = useForm(
|
||||
formData,
|
||||
reactive({
|
||||
name: [
|
||||
{ required: true, message: '请输入名称' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
],
|
||||
productId: [{ required: true, message: '请选择所属产品' }],
|
||||
version: [
|
||||
{ required: true, message: '请输入版本号' },
|
||||
{ max: 64, message: '最多可输入64个字符', trigger: 'change' },
|
||||
],
|
||||
versionOrder: [{ required: true, message: '请输入版本号' }],
|
||||
signMethod: [{ required: true, message: '请选择签名方式' }],
|
||||
sign: [
|
||||
{ required: true, message: '请输入签名' },
|
||||
{ validator: validatorSign },
|
||||
],
|
||||
url: [{ required: true, message: '请上传文件' }],
|
||||
description: [{ max: 200, message: '最多可输入200个字符' }],
|
||||
}),
|
||||
);
|
||||
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
const { properties } = await formRef.value?.validate();
|
||||
|
||||
validate()
|
||||
.then(async (res) => {
|
||||
const product = productOptions.value.find(
|
||||
(item) => item.value === res.productId,
|
||||
);
|
||||
const productName = product.label || props.data?.url;
|
||||
const size = extraValue.value.length || props.data?.size;
|
||||
|
||||
const params = {
|
||||
...toRaw(formData.value),
|
||||
properties: !!properties ? properties : [],
|
||||
productName,
|
||||
size,
|
||||
};
|
||||
loading.value = true;
|
||||
const response = !id
|
||||
? await save(params)
|
||||
: await update({ ...props.data, ...params });
|
||||
if (response.status === 200) {
|
||||
message.success('操作成功');
|
||||
emit('change', true);
|
||||
}
|
||||
loading.value = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
onSubmit();
|
||||
};
|
||||
const handleCancel = () => {
|
||||
emit('change', false);
|
||||
};
|
||||
|
||||
const changeSignMethod = () => {
|
||||
formData.value.sign = '';
|
||||
formData.value.url = '';
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
queryProduct({
|
||||
paging: false,
|
||||
terms: [{ column: 'state', value: 1 }],
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
}).then((resp) => {
|
||||
productOptions.value = resp.result.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
}));
|
||||
});
|
||||
});
|
||||
watch(
|
||||
() => props.data,
|
||||
(value) => {
|
||||
if (value.id) {
|
||||
formData.value = value;
|
||||
dynamicValidateForm.properties = value.properties;
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
watch(
|
||||
() => extraValue.value,
|
||||
() => validate('sign'),
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.form {
|
||||
.form-radio-button {
|
||||
width: 148px;
|
||||
height: 80px;
|
||||
padding: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.form-url-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.form-submit {
|
||||
background-color: @primary-color !important;
|
||||
}
|
||||
}
|
||||
.formRef {
|
||||
border: 1px dashed #d9d9d9;
|
||||
.formRef-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.formRef-content {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
.formRef-form-item {
|
||||
width: 47%;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
.formRef-form-item-add {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,268 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<div>
|
||||
<Search :columns="columns" target="search" @search="handleSearch" />
|
||||
<JTable
|
||||
ref="tableRef"
|
||||
model="TABLE"
|
||||
:columns="columns"
|
||||
:request="query"
|
||||
:defaultParams="{
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
}"
|
||||
:params="params"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<a-button type="primary" @click="handlAdd"
|
||||
><plus-outlined />新增</a-button
|
||||
>
|
||||
</template>
|
||||
<template #productId="slotProps">
|
||||
<span>{{ slotProps.productName }}</span>
|
||||
</template>
|
||||
<template #createTime="slotProps">
|
||||
<span>{{
|
||||
moment(slotProps.createTime).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)
|
||||
}}</span>
|
||||
</template>
|
||||
<template #action="slotProps">
|
||||
<a-space :size="16">
|
||||
<a-tooltip
|
||||
v-for="i in getActions(slotProps)"
|
||||
:key="i.key"
|
||||
v-bind="i.tooltip"
|
||||
>
|
||||
<a-popconfirm
|
||||
v-if="i.popConfirm"
|
||||
v-bind="i.popConfirm"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<Save v-if="visible" :data="current" @change="saveChange" />
|
||||
</page-container>
|
||||
</template>
|
||||
<script lang="ts" setup name="CertificatePage">
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
// import { save, query, remove } from '@/api/link/certificate';
|
||||
import { query, queryProduct, remove } from '@/api/device/firmware';
|
||||
import { message } from 'ant-design-vue';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import Save from './Save/index.vue';
|
||||
|
||||
const tableRef = ref<Record<string, any>>({});
|
||||
const router = useRouter();
|
||||
const params = ref<Record<string, any>>({});
|
||||
|
||||
const productOptions = ref([]);
|
||||
const visible = ref(false);
|
||||
const current = ref({});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '固件名称',
|
||||
key: 'name',
|
||||
dataIndex: 'name',
|
||||
fixed: 'left',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '固件版本',
|
||||
dataIndex: 'version',
|
||||
key: 'version',
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '所属产品',
|
||||
dataIndex: 'productId',
|
||||
key: 'productId',
|
||||
ellipsis: true,
|
||||
width: 200,
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: productOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '签名方式',
|
||||
dataIndex: 'signMethod',
|
||||
key: 'signMethod',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: 'MD5',
|
||||
value: 'md5',
|
||||
},
|
||||
{
|
||||
label: 'SHA256',
|
||||
value: 'sha256',
|
||||
},
|
||||
],
|
||||
},
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createTime',
|
||||
dataIndex: 'createTime',
|
||||
search: {
|
||||
type: 'time',
|
||||
},
|
||||
width: 200,
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
scopedSlots: true,
|
||||
},
|
||||
];
|
||||
|
||||
const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
key: 'FileTextOutlined',
|
||||
text: '升级任务',
|
||||
tooltip: {
|
||||
title: '升级任务',
|
||||
},
|
||||
icon: 'FileTextOutlined',
|
||||
onClick: async () => {
|
||||
handlUpdate(data.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'edit',
|
||||
text: '编辑',
|
||||
tooltip: {
|
||||
title: '编辑',
|
||||
},
|
||||
icon: 'EditOutlined',
|
||||
onClick: async () => {
|
||||
handlEdit(data);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
text: '删除',
|
||||
popConfirm: {
|
||||
title: '确认删除?',
|
||||
okText: ' 确定',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
handlDelete(data.id);
|
||||
},
|
||||
},
|
||||
icon: 'DeleteOutlined',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const handlUpdate = (id: string) => {
|
||||
// router.push({
|
||||
// path: `/iot/link/certificate/detail/${id}`,
|
||||
// query: { view: true },
|
||||
// });
|
||||
};
|
||||
|
||||
const handlAdd = () => {
|
||||
current.value = {};
|
||||
visible.value = true;
|
||||
};
|
||||
const handlEdit = (data: object) => {
|
||||
current.value = _.cloneDeep(data);
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const saveChange = (value: object) => {
|
||||
visible.value = false;
|
||||
current.value = {};
|
||||
if (value) {
|
||||
message.success('操作成功');
|
||||
tableRef.value.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const handlDelete = async (id: string) => {
|
||||
const res = await remove(id);
|
||||
if (res.success) {
|
||||
message.success('操作成功');
|
||||
tableRef.value.reload();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
queryProduct({
|
||||
paging: false,
|
||||
sorts: [{ name: 'name', order: 'desc' }],
|
||||
}).then((resp) => {
|
||||
const list = resp.result.filter((it) => {
|
||||
return _.map(it?.features || [], 'id').includes('supportFirmware');
|
||||
});
|
||||
productOptions.value = list.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* @param params
|
||||
*/
|
||||
const handleSearch = (e: any) => {
|
||||
params.value = e;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,23 @@
|
|||
export type FormDataType = {
|
||||
description: string;
|
||||
name: string;
|
||||
productId: string | undefined;
|
||||
version: undefined;
|
||||
versionOrder: undefined;
|
||||
signMethod: string | undefined;
|
||||
sign: string;
|
||||
url: string;
|
||||
size: number;
|
||||
properties: Array<Properties>;
|
||||
id?: string;
|
||||
format?: string;
|
||||
mode?: object;
|
||||
creatorId?: string;
|
||||
createTime?: number;
|
||||
};
|
||||
|
||||
export interface Properties {
|
||||
id: string;
|
||||
value: any;
|
||||
keyid: number;
|
||||
}
|
|
@ -1,20 +1,47 @@
|
|||
<template>
|
||||
<div class="dialog-item" :key="data.key" :class="{'dialog-active' : !data?.upstream}">
|
||||
<div
|
||||
class="dialog-item"
|
||||
:key="data.key"
|
||||
:class="{ 'dialog-active': !data?.upstream }"
|
||||
>
|
||||
<div class="dialog-card">
|
||||
<div class="dialog-list" v-for="item in data.list" :key="item.key">
|
||||
<div class="dialog-icon">
|
||||
<AIcon :type="visible.includes(item.key) ? 'DownOutlined' : 'RightOutlined'" />
|
||||
<div class="dialog-icon" @click="getDetail(item)">
|
||||
<AIcon
|
||||
v-if="visible.includes(item.key)"
|
||||
type="DownOutlined"
|
||||
/>
|
||||
<AIcon v-else type="RightOutlined" />
|
||||
</div>
|
||||
<div class="dialog-box">
|
||||
<div class="dialog-header">
|
||||
<div class="dialog-title">
|
||||
<a-badge :color="statusColor.get(item.error ? 'error' : 'success')" style="margin-right: 5px" />
|
||||
{{operationMap.get(item.operation) || item?.operation}}
|
||||
<a-badge
|
||||
:color="
|
||||
statusColor.get(
|
||||
item.error ? 'error' : 'success',
|
||||
)
|
||||
"
|
||||
style="margin-right: 5px"
|
||||
/>
|
||||
{{
|
||||
operationMap.get(item.operation) ||
|
||||
item?.operation
|
||||
}}
|
||||
</div>
|
||||
<div class="dialog-time">
|
||||
{{
|
||||
moment(item.endTime).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class="dialog-item">{{moment(item.endTime).format('YYYY-MM-DD HH:mm:ss')}}</div>
|
||||
</div>
|
||||
<div class="dialog-editor" v-if="visible.includes(item.key)">
|
||||
<a-textarea :bordered="false" :value="item?.detail" />
|
||||
<div
|
||||
class="dialog-editor"
|
||||
v-if="visible.includes(item.key)"
|
||||
>
|
||||
<a-textarea autoSize :bordered="false" :value="item?.detail" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -24,7 +51,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
const operationMap = new Map();
|
||||
import moment from 'moment'
|
||||
import moment from 'moment';
|
||||
operationMap.set('connection', '连接');
|
||||
operationMap.set('auth', '权限验证');
|
||||
operationMap.set('decode', '解码');
|
||||
|
@ -41,102 +68,113 @@ statusColor.set('success', '#24B276');
|
|||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
const visible = ref<string[]>([]);
|
||||
const getDetail = (item: any) => {
|
||||
const index = visible.value.indexOf(item.key);
|
||||
if (index === -1) {
|
||||
visible.value.push(item.key);
|
||||
} else {
|
||||
visible.value.splice(index, 1);
|
||||
}
|
||||
})
|
||||
const visible = ref<string[]>([])
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
console.log(props.data)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import 'ant-design-vue/es/style/themes/default.less';
|
||||
|
||||
:root {
|
||||
--dialog-primary-color: @primary-color;
|
||||
--dialog-primary-color: @primary-color;
|
||||
}
|
||||
|
||||
.dialog-item {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
padding-bottom: 12px;
|
||||
|
||||
.dialog-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 60%;
|
||||
padding: 24px;
|
||||
background-color: #fff;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
padding-bottom: 12px;
|
||||
|
||||
.dialog-list {
|
||||
display: flex;
|
||||
|
||||
.dialog-icon {
|
||||
margin-right: 10px;
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
.dialog-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
width: 60%;
|
||||
padding: 24px;
|
||||
background-color: #fff;
|
||||
|
||||
.dialog-header {
|
||||
.dialog-title {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
.dialog-list {
|
||||
display: flex;
|
||||
|
||||
.dialog-time {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 12px;
|
||||
}
|
||||
.dialog-icon {
|
||||
margin-right: 10px;
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.dialog-header {
|
||||
.dialog-title {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dialog-time {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-editor {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
|
||||
textarea::-webkit-scrollbar {
|
||||
width: 5px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-editor {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
|
||||
textarea::-webkit-scrollbar {
|
||||
width: 5px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-active {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
.dialog-card {
|
||||
background-color: @primary-color;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
.dialog-card {
|
||||
background-color: @primary-color;
|
||||
|
||||
.dialog-list {
|
||||
.dialog-icon {
|
||||
color: #fff;
|
||||
}
|
||||
.dialog-list {
|
||||
.dialog-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
.dialog-header {
|
||||
.dialog-title,
|
||||
.dialog-time {
|
||||
color: #fff;
|
||||
}
|
||||
.dialog-box {
|
||||
.dialog-header {
|
||||
.dialog-title,
|
||||
.dialog-time {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-editor {
|
||||
textarea {
|
||||
color: #fff !important;
|
||||
background-color: @primary-color !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-editor {
|
||||
textarea {
|
||||
color: #fff !important;
|
||||
background-color: @primary-color !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -67,7 +67,7 @@
|
|||
message: '请输入值',
|
||||
}"
|
||||
>
|
||||
<a-input v-model:value="propertyValue" />
|
||||
<a-input v-model:value="modelRef.propertyValue" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6" v-if="modelRef.type === 'INVOKE_FUNCTION'">
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
<template>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="16">
|
||||
<a-row :gutter="24" style="margin-bottom: 20px;">
|
||||
<a-row :gutter="24" style="margin-bottom: 20px">
|
||||
<a-col :span="12" v-for="item in messageArr" :key="item">
|
||||
<div :style="messageStyleMap.get(item.status)" class="message-status">
|
||||
<a-badge :status="messageStatusMap.get(item.status)" style="margin-right: 5px;" />
|
||||
<span>{{item.text}}</span>
|
||||
<div
|
||||
:style="messageStyleMap.get(item.status)"
|
||||
class="message-status"
|
||||
>
|
||||
<a-badge
|
||||
:status="messageStatusMap.get(item.status)"
|
||||
style="margin-right: 5px"
|
||||
/>
|
||||
<span>{{ item.text }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
@ -26,7 +32,11 @@
|
|||
<TitleComponent data="日志" />
|
||||
<div :style="{ marginTop: '10px' }">
|
||||
<template v-if="logList.length">
|
||||
<Log v-for="item in logList" :data="item" :key="item.key" />
|
||||
<Log
|
||||
v-for="item in logList"
|
||||
:data="item"
|
||||
:key="item.key"
|
||||
/>
|
||||
</template>
|
||||
<a-empty v-else />
|
||||
</div>
|
||||
|
@ -36,58 +46,146 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MessageType } from './util'
|
||||
import { messageStatusMap, messageStyleMap } from './util'
|
||||
import Dialog from './Dialog/index.vue'
|
||||
import Function from './Function/index.vue'
|
||||
import Log from './Log/index.vue'
|
||||
import type { MessageType } from './util';
|
||||
import { messageStatusMap, messageStyleMap } from './util';
|
||||
import Dialog from './Dialog/index.vue';
|
||||
import Function from './Function/index.vue';
|
||||
import Log from './Log/index.vue';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
import { getWebSocket } from '@/utils/websocket';
|
||||
import { randomString } from '@/utils/utils';
|
||||
import _ from 'lodash';
|
||||
|
||||
const message = reactive<MessageType>({
|
||||
up: {
|
||||
text: '上行消息诊断中',
|
||||
status: 'loading',
|
||||
text: '上行消息诊断中',
|
||||
status: 'loading',
|
||||
},
|
||||
down: {
|
||||
text: '下行消息诊断中',
|
||||
status: 'loading',
|
||||
text: '下行消息诊断中',
|
||||
status: 'loading',
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const dialogList = ref<Record<string, any>>([])
|
||||
const logList = ref<Record<string, any>>([])
|
||||
const instanceStore = useInstanceStore();
|
||||
|
||||
const allDialogList = ref<Record<string, any>[]>([]);
|
||||
const dialogList = ref<Record<string, any>[]>([]);
|
||||
const logList = ref<Record<string, any>[]>([]);
|
||||
|
||||
const diagnoseRef = ref();
|
||||
|
||||
const messageArr = computed(() => {
|
||||
const arr = Object.keys(message) || []
|
||||
return arr.map(i => { return {...message[i], key: i}})
|
||||
})
|
||||
const arr = Object.keys(message) || [];
|
||||
return arr.map((i) => {
|
||||
return { ...message[i], key: i };
|
||||
});
|
||||
});
|
||||
|
||||
const subscribeLog = () => {
|
||||
const id = `device-debug-${instanceStore.current?.id}`;
|
||||
const topic = `/debug/device/${instanceStore.current?.id}/trace`;
|
||||
diagnoseRef.value = getWebSocket(id, topic, {})
|
||||
?.pipe(map((res: any) => res.payload))
|
||||
.subscribe((payload) => {
|
||||
if (payload.type === 'log') {
|
||||
logList.value.push({
|
||||
key: randomString(),
|
||||
...payload,
|
||||
});
|
||||
} else {
|
||||
const data = { key: randomString(), ...payload };
|
||||
allDialogList.value.push(data);
|
||||
const flag = allDialogList.value
|
||||
.filter(
|
||||
(i: any) =>
|
||||
i.traceId === data.traceId &&
|
||||
(data.downstream === i.downstream ||
|
||||
data.upstream === i.upstream),
|
||||
)
|
||||
.every((item: any) => {
|
||||
return !item.error;
|
||||
});
|
||||
if (!data.upstream) {
|
||||
message.down = {
|
||||
text: !flag ? '下行消息通信异常' : '下行消息通信正常',
|
||||
status: !flag ? 'error' : 'success',
|
||||
};
|
||||
} else {
|
||||
message.up = {
|
||||
text: !flag ? '上行消息通信异常' : '上行消息通信正常',
|
||||
status: !flag ? 'error' : 'success',
|
||||
};
|
||||
}
|
||||
const list: any[] = _.cloneDeep(dialogList.value);
|
||||
const t = list.find(
|
||||
(item) =>
|
||||
item.traceId === data.traceId &&
|
||||
data.downstream === item.downstream &&
|
||||
data.upstream === item.upstream,
|
||||
);
|
||||
if (t) {
|
||||
const arr = list.map((item) => {
|
||||
if (item.traceId === data.traceId) {
|
||||
item.list.push(data);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
dialogList.value = _.cloneDeep(arr);
|
||||
} else {
|
||||
list.push({
|
||||
key: randomString(),
|
||||
traceId: data.traceId,
|
||||
downstream: data.downstream,
|
||||
upstream: data.upstream,
|
||||
list: [data],
|
||||
});
|
||||
dialogList.value = _.cloneDeep(list);
|
||||
}
|
||||
}
|
||||
const chatBox = document.getElementById('dialog');
|
||||
if (chatBox) {
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
const topState: any = inject('topState') || '';
|
||||
|
||||
watchEffect(() => {
|
||||
if (topState && topState?.value === 'success') {
|
||||
subscribeLog();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (diagnoseRef.value) {
|
||||
diagnoseRef.value.unsubscribe();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.message-status {
|
||||
padding: 8px 24px;
|
||||
padding: 8px 24px;
|
||||
}
|
||||
.content {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
max-height: 500px;
|
||||
padding: 24px;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: #f2f5f7;
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
max-height: 500px;
|
||||
padding: 24px;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: #f2f5f7;
|
||||
}
|
||||
.right-log {
|
||||
padding-left: 20px;
|
||||
border-left: 1px solid rgba(0, 0, 0, .09);
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.09);
|
||||
overflow: hidden;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
|
|
|
@ -34,8 +34,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Message v-if="activeKey === 'message'" />
|
||||
<Status v-else :providerType="providerType" @countChange="countChange" @percentChange="percentChange" @stateChange="stateChange" />
|
||||
<Message v-show="activeKey === 'message'" />
|
||||
<Status v-show="activeKey !== 'message'" :providerType="providerType" @countChange="countChange" @percentChange="percentChange" @stateChange="stateChange" />
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
@ -70,6 +70,7 @@ const percent = ref<number>(0)
|
|||
const activeKey = ref<'status' | 'message'>('status')
|
||||
const providerType = ref()
|
||||
|
||||
provide('topState', topState)
|
||||
|
||||
const onTabChange = (key: 'status' | 'message') => {
|
||||
if(topState.value === 'success'){
|
||||
|
|
|
@ -0,0 +1,289 @@
|
|||
<template>
|
||||
<a-spin :spinning="loading">
|
||||
<a-card>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="visible = true">批量映射</a-button>
|
||||
<a-button type="primary" @click="onSave">保存</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-form ref="formRef" :model="modelRef">
|
||||
<a-table :dataSource="modelRef.dataSource" :columns="columns">
|
||||
<template #headerCell="{ column }">
|
||||
<template v-if="column.key === 'collectorId'">
|
||||
采集器
|
||||
<a-tooltip title="边缘网关代理的真实物理设备">
|
||||
<AIcon type="QuestionCircleOutlined" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.dataIndex === 'channelId'">
|
||||
<a-form-item
|
||||
:name="['dataSource', index, 'channelId']"
|
||||
>
|
||||
<a-select
|
||||
style="width: 100%"
|
||||
v-model:value="record[column.dataIndex]"
|
||||
placeholder="请选择"
|
||||
allowClear
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in channelList"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
>{{ item.label }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'collectorId'">
|
||||
<a-form-item
|
||||
:name="['dataSource', index, 'collectorId']"
|
||||
:rules="[
|
||||
{
|
||||
required: !!record.channelId,
|
||||
message: '请选择采集器',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MSelect
|
||||
v-model="record[column.dataIndex]"
|
||||
:id="record.channelId"
|
||||
type="COLLECTOR"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'pointId'">
|
||||
<a-form-item
|
||||
:name="['dataSource', index, 'pointId']"
|
||||
:rules="[
|
||||
{
|
||||
required: !!record.channelId,
|
||||
message: '请选择点位',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MSelect
|
||||
v-model="record[column.dataIndex]"
|
||||
:id="record.collectorId"
|
||||
type="POINT"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'id'">
|
||||
<a-badge
|
||||
v-if="record[column.dataIndex]"
|
||||
status="success"
|
||||
text="已绑定"
|
||||
/>
|
||||
<a-badge v-else status="error" text="未绑定" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-tooltip title="解绑">
|
||||
<a-popconfirm
|
||||
title="确认解绑"
|
||||
@confirm="unbind(record.id)"
|
||||
>
|
||||
<a-button type="link" :disabled="!record.id"
|
||||
><AIcon type="icon-jiebang"
|
||||
/></a-button>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-form>
|
||||
</a-card>
|
||||
<!-- <PatchMapping
|
||||
:deviceId="instanceStore.current.id"
|
||||
v-if="visible"
|
||||
@close="visible = false"
|
||||
@save="onPatchBind"
|
||||
:type="provider"
|
||||
:metaData="modelRef.dataSource"
|
||||
/> -->
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
import {
|
||||
queryMapping,
|
||||
saveMapping,
|
||||
removeMapping,
|
||||
queryChannelNoPaging,
|
||||
} from '@/api/device/instance';
|
||||
import MSelect from '../components/MSelect.vue';
|
||||
// import PatchMapping from '../components/PatchMapping.vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'metadataName',
|
||||
key: 'metadataName',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: '通道',
|
||||
dataIndex: 'channelId',
|
||||
key: 'channelId',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: '采集器',
|
||||
dataIndex: 'collectorId',
|
||||
key: 'collectorId',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: '点位',
|
||||
key: 'pointId',
|
||||
dataIndex: 'pointId',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'id',
|
||||
dataIndex: 'id',
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: '10%',
|
||||
},
|
||||
];
|
||||
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
provider: {
|
||||
type: String,
|
||||
default: 'MODBUS_TCP',
|
||||
},
|
||||
});
|
||||
|
||||
const instanceStore = useInstanceStore();
|
||||
const metadata = JSON.parse(instanceStore.current?.metadata || '{}');
|
||||
const loading = ref<boolean>(false);
|
||||
const channelList = ref([]);
|
||||
|
||||
const modelRef = reactive({
|
||||
dataSource: [],
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const visible = ref<boolean>(false);
|
||||
|
||||
const getChannel = async () => {
|
||||
const resp: any = await queryChannelNoPaging({
|
||||
paging: false,
|
||||
terms: [
|
||||
{
|
||||
terms: [
|
||||
{
|
||||
column: 'provider',
|
||||
value: props.provider,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
channelList.value = resp.result?.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
provider: item.provider,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
loading.value = true;
|
||||
getChannel();
|
||||
const _metadata = metadata.properties.map((item: any) => ({
|
||||
metadataId: item.id,
|
||||
metadataName: `${item.name}(${item.id})`,
|
||||
metadataType: 'property',
|
||||
name: item.name,
|
||||
}));
|
||||
if (_metadata && _metadata.length) {
|
||||
const resp: any = await queryMapping(
|
||||
'device',
|
||||
instanceStore.current.id,
|
||||
);
|
||||
if (resp.status === 200) {
|
||||
const array = resp.result.reduce((x: any, y: any) => {
|
||||
const metadataId = _metadata.find(
|
||||
(item: any) => item.metadataId === y.metadataId,
|
||||
);
|
||||
if (metadataId) {
|
||||
Object.assign(metadataId, y);
|
||||
} else {
|
||||
x.push(y);
|
||||
}
|
||||
return x;
|
||||
}, _metadata);
|
||||
modelRef.dataSource = array;
|
||||
}
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const unbind = async (id: string) => {
|
||||
if (id) {
|
||||
const resp = await removeMapping('device', instanceStore.current.id, [
|
||||
id,
|
||||
]);
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
handleSearch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onPatchBind = () => {
|
||||
visible.value = false;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
handleSearch();
|
||||
});
|
||||
|
||||
const onSave = () => {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(async () => {
|
||||
const arr = toRaw(modelRef).dataSource.filter(
|
||||
(i: any) => i.channelId,
|
||||
);
|
||||
if (arr && arr.length !== 0) {
|
||||
const resp = await saveMapping(
|
||||
instanceStore.current.id,
|
||||
props.provider,
|
||||
arr,
|
||||
);
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
handleSearch();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.log('error', err);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.ant-form-item) {
|
||||
margin: 0 !important;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<EditTable provider="MODBUS_TCP" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import EditTable from '../components/EditTable/index.vue'
|
||||
</script>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<EditTable provider="OPC_UA" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import EditTable from '../components/EditTable/index.vue'
|
||||
</script>
|
|
@ -24,7 +24,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="value">
|
||||
<ValueRender :data="data" :value="_props.data" />
|
||||
<ValueRender :data="data" :value="_props.data" type="card" />
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div style="color: rgba(0, 0, 0, .65); font-size: 12px">更新时间</div>
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:maskClosable="false"
|
||||
width="600px"
|
||||
:visible="true"
|
||||
title="详情"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
@ok="handleCancel"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<template v-if="['.jpg', '.png'].includes(type)">
|
||||
<a-image :src="value?.formatValue" />
|
||||
</template>
|
||||
<template v-else-if="['.flv', '.m3u8', '.mp4'].includes(type)">
|
||||
<!-- TODO 视频组件缺失 -->
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- <json-viewer
|
||||
:value="{
|
||||
'id': '123'
|
||||
}"
|
||||
copyable
|
||||
boxed
|
||||
sort
|
||||
></json-viewer> -->
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// import JsonViewer from 'vue3-json-viewer';
|
||||
|
||||
const _data = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: [Object, String],
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
const _emit = defineEmits(['close']);
|
||||
const handleCancel = () => {
|
||||
_emit('close');
|
||||
};
|
||||
|
||||
// watchEffect(() => {
|
||||
// console.log(_data.value?.formatValue)
|
||||
// })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
|
@ -1,11 +1,58 @@
|
|||
<template>
|
||||
<div class="value">
|
||||
{{value?.value || '--'}}
|
||||
<div v-if="value?.formatValue !== 0 && !value?.formatValue" :class="valueClass">--</div>
|
||||
<div v-else-if="data?.valueType?.type === 'file'">
|
||||
<template v-if="data?.valueType?.fileType === 'base64'">
|
||||
<div :class="valueClass" v-if="!!getType(value?.formatValue)">
|
||||
<img :src="imgMap.get(_type)" @error="onError" />
|
||||
</div>
|
||||
<div v-else :class="valueClass">
|
||||
<img :src="imgMap.get('other')" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="data?.valueType?.fileType === 'Binary(二进制)'" :class="valueClass">
|
||||
<img :src="imgMap.get('other')" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<template v-if="imgList.some((item) => value?.formatValue.includes(item))">
|
||||
<div :class="valueClass" @click="getDetail('img')">
|
||||
<img :src="value?.formatValue" @error="imgError" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="videoList.some((item) => value?.formatValue.includes(item))">
|
||||
<div :class="valueClass" @click="getDetail('video')">
|
||||
<img :src="imgMap.get('video')" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="fileList.some((item) => value?.formatValue.includes(item))">
|
||||
<div :class="valueClass">
|
||||
<img :src="imgMap.get(fileList.find((item) => value?.formatValue.includes(item)).slice(1))" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div :class="valueClass">
|
||||
<img :src="imgMap.get('other')" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="data?.valueType?.type === 'object'" @click="getDetail('obj')" :class="valueClass">
|
||||
<img :src="imgMap.get('obj')" />
|
||||
</div>
|
||||
<div v-else-if="data?.valueType?.type === 'geoPoint' || data?.valueType?.type === 'array'" :class="valueClass">
|
||||
{{JSON.stringify(value?.formatValue)}}
|
||||
</div>
|
||||
<div v-else :class="valueClass">
|
||||
{{String(value?.formatValue)}}
|
||||
</div>
|
||||
<ValueDetail v-if="visible" :type="_types" :value="value" @close="visible = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getImage } from "@/utils/comm";
|
||||
import { message } from "ant-design-vue";
|
||||
import ValueDetail from './ValueDetail.vue'
|
||||
|
||||
const _data = defineProps({
|
||||
data: {
|
||||
|
@ -22,6 +69,10 @@ const _data = defineProps({
|
|||
}
|
||||
});
|
||||
|
||||
const valueClass = computed(() => {
|
||||
return _data.type === 'card' ? 'cardValue' : 'otherValue'
|
||||
})
|
||||
|
||||
const imgMap = new Map<any, any>();
|
||||
imgMap.set('txt', getImage('/running/txt.png'));
|
||||
imgMap.set('doc', getImage('/running/doc.png'));
|
||||
|
@ -41,6 +92,64 @@ const imgList = ['.jpg', '.png', '.swf', '.tiff'];
|
|||
const videoList = ['.m3u8', '.flv', '.mp4', '.rmvb', '.mvb'];
|
||||
const fileList = ['.txt', '.doc', '.xls', '.pdf', '.ppt', '.docx', '.xlsx', '.pptx'];
|
||||
|
||||
const isHttps = document.location.protocol === 'https:';
|
||||
|
||||
const _types = ref<string>('')
|
||||
const visible = ref<boolean>(false)
|
||||
const temp = ref<boolean>(false)
|
||||
|
||||
const getType = (url: string) => {
|
||||
let t: string = '';
|
||||
[...imgList, ...videoList, ...fileList].map((item) => {
|
||||
const str = item.slice(1, item.length);
|
||||
if (url && String(url).indexOf(str) !== -1) {
|
||||
if (imgList.includes(item)) {
|
||||
t = 'img';
|
||||
} else if (videoList.includes(item)) {
|
||||
t = 'video';
|
||||
} else {
|
||||
t = str;
|
||||
}
|
||||
}
|
||||
});
|
||||
return t;
|
||||
};
|
||||
|
||||
const onError = (e: any) => {
|
||||
e.target.src = imgMap.get('other')
|
||||
}
|
||||
|
||||
const imgError = (e: any) => {
|
||||
e.target.src = imgMap.get('error')
|
||||
temp.value = true
|
||||
}
|
||||
|
||||
const getDetail = (_type: string) => {
|
||||
const value = _data.value
|
||||
let flag: string = ''
|
||||
if(_type === 'img'){
|
||||
if (isHttps && value?.formatValue.indexOf('http:') !== -1) {
|
||||
message.error('域名为https时,不支持访问http地址');
|
||||
} else if (temp.value) {
|
||||
message.error('该图片无法访问');
|
||||
} else {
|
||||
flag = ['.jpg', '.png'].find((item) => value?.formatValue.includes(item)) || '--';
|
||||
}
|
||||
} else if(_type === 'video'){
|
||||
if (isHttps && value?.formatValue.indexOf('http:') !== -1) {
|
||||
message.error('域名为https时,不支持访问http地址');
|
||||
} else if (['.rmvb', '.mvb'].some((item) => value?.formatValue.includes(item))) {
|
||||
message.error('当前仅支持播放.mp4,.flv,.m3u8格式的视频');
|
||||
} else {
|
||||
flag = ['.m3u8', '.flv', '.mp4'].find((item) => value?.formatValue.includes(item)) || '--';
|
||||
}
|
||||
}else if(_type === 'obj'){
|
||||
flag = 'obj'
|
||||
}
|
||||
_types.value = flag
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
</template>
|
||||
<template #value="slotProps">
|
||||
<ValueRender
|
||||
type="table"
|
||||
:data="slotProps"
|
||||
:value="propertyValue[slotProps?.id]"
|
||||
/>
|
||||
|
@ -332,6 +333,10 @@ watch(
|
|||
const onSearch = () => {
|
||||
query(0, 8, value.value);
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
subRef.value && subRef.value?.unsubscribe()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
|
|
@ -109,6 +109,7 @@ const tabChange = (key: string) => {
|
|||
<style lang="less" scoped>
|
||||
.property-box {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
.property-box-left {
|
||||
width: 200px;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
<template>
|
||||
<a-modal
|
||||
width="900px"
|
||||
title="批量映射"
|
||||
visible
|
||||
@ok="handleClick"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<div class="map-tree">
|
||||
<div class="map-tree-top">
|
||||
采集器的点位名称与属性名称一致时将自动映射绑定;有多个采集器点位名称与属性名称一致时以第1个采集器的点位数据进行绑定
|
||||
</div>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="map-tree-content">
|
||||
<a-card class="map-tree-content-card" title="源数据">
|
||||
<a-tree
|
||||
checkable
|
||||
:height="300"
|
||||
:tree-data="dataSource"
|
||||
:checkedKeys="checkedKeys"
|
||||
@check="onCheck"
|
||||
/>
|
||||
</a-card>
|
||||
<div style="width: 100px">
|
||||
<a-button
|
||||
:disabled="rightList.length >= leftList.length"
|
||||
@click="onRight"
|
||||
>加入右侧</a-button
|
||||
>
|
||||
</div>
|
||||
<a-card class="map-tree-content-card" title="采集器">
|
||||
<a-list
|
||||
size="small"
|
||||
:data-source="rightList"
|
||||
class="map-tree-content-card-list"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
{{ item.title }}
|
||||
<template #actions>
|
||||
<a-popconfirm
|
||||
title="确定删除?"
|
||||
@confirm="_delete(item.key)"
|
||||
>
|
||||
<AIcon type="DeleteOutlined" />
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { treeMapping, saveMapping } from '@/api/device/instance';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
const _props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'MODBUS_TCP',
|
||||
},
|
||||
metaData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
deviceId: {
|
||||
type: String,
|
||||
default: '',
|
||||
}
|
||||
});
|
||||
const _emits = defineEmits(['close', 'save']);
|
||||
|
||||
const checkedKeys = ref<string[]>([]);
|
||||
|
||||
const leftList = ref<any[]>([]);
|
||||
const rightList = ref<any[]>([]);
|
||||
|
||||
const dataSource = ref<any[]>([]);
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
const handleData = (data: any[], type: string) => {
|
||||
data.forEach((item) => {
|
||||
item.key = item.id;
|
||||
item.title = item.name;
|
||||
item.checkable = type === 'collectors';
|
||||
if (
|
||||
item.collectors &&
|
||||
Array.isArray(item.collectors) &&
|
||||
item.collectors.length
|
||||
) {
|
||||
item.children = handleData(item.collectors, 'collectors');
|
||||
}
|
||||
if (item.points && Array.isArray(item.points) && item.points.length) {
|
||||
item.children = handleData(item.points, 'points');
|
||||
}
|
||||
});
|
||||
return data as any[];
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
loading.value = true;
|
||||
const resp = await treeMapping({
|
||||
terms: [
|
||||
{
|
||||
column: 'provider',
|
||||
value: _props.type,
|
||||
},
|
||||
],
|
||||
});
|
||||
loading.value = false;
|
||||
if (resp.status === 200) {
|
||||
dataSource.value = handleData(resp.result as any[], 'channel');
|
||||
}
|
||||
};
|
||||
|
||||
const onCheck = (keys: string[], e: any) => {
|
||||
checkedKeys.value = [...keys];
|
||||
leftList.value = e?.checkedNodes || [];
|
||||
};
|
||||
|
||||
const onRight = () => {
|
||||
rightList.value = leftList.value;
|
||||
};
|
||||
|
||||
const _delete = (_key: string) => {
|
||||
const _index = rightList.value.findIndex((i) => i.key === _key);
|
||||
rightList.value.splice(_index, 1);
|
||||
checkedKeys.value = rightList.value.map((i) => i.key);
|
||||
leftList.value = rightList.value;
|
||||
};
|
||||
|
||||
const handleClick = async () => {
|
||||
if (!rightList.value.length) {
|
||||
message.warning('请选择采集器');
|
||||
} else {
|
||||
const params: any[] = [];
|
||||
rightList.value.map((item: any) => {
|
||||
const array = (item.children || []).map((element: any) => ({
|
||||
channelId: item.parentId,
|
||||
collectorId: element.collectorId,
|
||||
pointId: element.id,
|
||||
metadataType: 'property',
|
||||
metadataId: (_props.metaData as any[]).find((i: any) => i.name === element.name)
|
||||
?.metadataId,
|
||||
provider: _props.type
|
||||
}));
|
||||
params.push(...array);
|
||||
});
|
||||
const filterParms = params.filter((item) => !!item.metadataId);
|
||||
if (filterParms && filterParms.length !== 0) {
|
||||
const res = await saveMapping(_props.deviceId, _props.type, filterParms);
|
||||
if (res.status === 200) {
|
||||
message.success('操作成功');
|
||||
_emits('save');
|
||||
}
|
||||
} else {
|
||||
message.error('暂无对应属性的映射');
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleClose = () => {
|
||||
_emits('close');
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (_props.type) {
|
||||
handleSearch();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.map-tree-content {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.map-tree-content-card {
|
||||
width: 350px;
|
||||
height: 400px;
|
||||
|
||||
.map-tree-content-card-list {
|
||||
overflow-y: auto;
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,289 @@
|
|||
<template>
|
||||
<a-spin :spinning="loading">
|
||||
<a-card>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="visible = true">批量映射</a-button>
|
||||
<a-button type="primary" @click="onSave">保存</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-form ref="formRef" :model="modelRef">
|
||||
<a-table :dataSource="modelRef.dataSource" :columns="columns">
|
||||
<template #headerCell="{ column }">
|
||||
<template v-if="column.key === 'collectorId'">
|
||||
采集器
|
||||
<a-tooltip title="数据采集中配置的真实物理设备">
|
||||
<AIcon type="QuestionCircleOutlined" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.dataIndex === 'channelId'">
|
||||
<a-form-item
|
||||
:name="['dataSource', index, 'channelId']"
|
||||
>
|
||||
<a-select
|
||||
style="width: 100%"
|
||||
v-model:value="record[column.dataIndex]"
|
||||
placeholder="请选择"
|
||||
allowClear
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in channelList"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
>{{ item.label }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'collectorId'">
|
||||
<a-form-item
|
||||
:name="['dataSource', index, 'collectorId']"
|
||||
:rules="[
|
||||
{
|
||||
required: !!record.channelId,
|
||||
message: '请选择采集器',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MSelect
|
||||
v-model="record[column.dataIndex]"
|
||||
:id="record.channelId"
|
||||
type="COLLECTOR"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'pointId'">
|
||||
<a-form-item
|
||||
:name="['dataSource', index, 'pointId']"
|
||||
:rules="[
|
||||
{
|
||||
required: !!record.channelId,
|
||||
message: '请选择点位',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MSelect
|
||||
v-model="record[column.dataIndex]"
|
||||
:id="record.collectorId"
|
||||
type="POINT"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'id'">
|
||||
<a-badge
|
||||
v-if="record[column.dataIndex]"
|
||||
status="success"
|
||||
text="已绑定"
|
||||
/>
|
||||
<a-badge v-else status="error" text="未绑定" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-tooltip title="解绑">
|
||||
<a-popconfirm
|
||||
title="确认解绑"
|
||||
@confirm="unbind(record.id)"
|
||||
>
|
||||
<a-button type="link" :disabled="!record.id"
|
||||
><AIcon type="icon-jiebang"
|
||||
/></a-button>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-form>
|
||||
</a-card>
|
||||
<PatchMapping
|
||||
:deviceId="instanceStore.current.id"
|
||||
v-if="visible"
|
||||
@close="visible = false"
|
||||
@save="onPatchBind"
|
||||
:type="provider"
|
||||
:metaData="modelRef.dataSource"
|
||||
/>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
import {
|
||||
queryMapping,
|
||||
saveMapping,
|
||||
removeMapping,
|
||||
queryChannelNoPaging,
|
||||
} from '@/api/device/instance';
|
||||
import MSelect from '../MSelect.vue';
|
||||
import PatchMapping from './PatchMapping.vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'metadataName',
|
||||
key: 'metadataName',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: '通道',
|
||||
dataIndex: 'channelId',
|
||||
key: 'channelId',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: '采集器',
|
||||
dataIndex: 'collectorId',
|
||||
key: 'collectorId',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: '点位',
|
||||
key: 'pointId',
|
||||
dataIndex: 'pointId',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'id',
|
||||
dataIndex: 'id',
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: '10%',
|
||||
},
|
||||
];
|
||||
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
provider: {
|
||||
type: String,
|
||||
default: 'MODBUS_TCP',
|
||||
},
|
||||
});
|
||||
|
||||
const instanceStore = useInstanceStore();
|
||||
const metadata = JSON.parse(instanceStore.current?.metadata || '{}');
|
||||
const loading = ref<boolean>(false);
|
||||
const channelList = ref([]);
|
||||
|
||||
const modelRef = reactive({
|
||||
dataSource: [],
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const visible = ref<boolean>(false);
|
||||
|
||||
const getChannel = async () => {
|
||||
const resp: any = await queryChannelNoPaging({
|
||||
paging: false,
|
||||
terms: [
|
||||
{
|
||||
terms: [
|
||||
{
|
||||
column: 'provider',
|
||||
value: props.provider,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
channelList.value = resp.result?.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
provider: item.provider,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
loading.value = true;
|
||||
getChannel();
|
||||
const _metadata = metadata.properties.map((item: any) => ({
|
||||
metadataId: item.id,
|
||||
metadataName: `${item.name}(${item.id})`,
|
||||
metadataType: 'property',
|
||||
name: item.name,
|
||||
}));
|
||||
if (_metadata && _metadata.length) {
|
||||
const resp: any = await queryMapping(
|
||||
'device',
|
||||
instanceStore.current.id,
|
||||
);
|
||||
if (resp.status === 200) {
|
||||
const array = resp.result.reduce((x: any, y: any) => {
|
||||
const metadataId = _metadata.find(
|
||||
(item: any) => item.metadataId === y.metadataId,
|
||||
);
|
||||
if (metadataId) {
|
||||
Object.assign(metadataId, y);
|
||||
} else {
|
||||
x.push(y);
|
||||
}
|
||||
return x;
|
||||
}, _metadata);
|
||||
modelRef.dataSource = array;
|
||||
}
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const unbind = async (id: string) => {
|
||||
if (id) {
|
||||
const resp = await removeMapping('device', instanceStore.current.id, [
|
||||
id,
|
||||
]);
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
handleSearch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onPatchBind = () => {
|
||||
visible.value = false;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
handleSearch();
|
||||
});
|
||||
|
||||
const onSave = () => {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(async () => {
|
||||
const arr = toRaw(modelRef).dataSource.filter(
|
||||
(i: any) => i.channelId,
|
||||
);
|
||||
if (arr && arr.length !== 0) {
|
||||
const resp = await saveMapping(
|
||||
instanceStore.current.id,
|
||||
props.provider,
|
||||
arr,
|
||||
);
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
handleSearch();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.log('error', err);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.ant-form-item) {
|
||||
margin: 0 !important;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<a-select allowClear v-model:value="_value" @change="onChange" placeholder="请选择" style="width: 100%">
|
||||
<a-select-option
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name"
|
||||
:filter-option="filterOption"
|
||||
>{{ item.name }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
queryCollectorNoPaging,
|
||||
queryPointNoPaging,
|
||||
} from '@/api/device/instance';
|
||||
|
||||
const _props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'POINT',
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
type Emits = {
|
||||
(e: 'update:modelValue', data: string | undefined): void;
|
||||
};
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const list = ref<any[]>([]);
|
||||
const _value = ref<string | undefined>(undefined);
|
||||
|
||||
watchEffect(() => {
|
||||
_value.value = _props.modelValue;
|
||||
});
|
||||
|
||||
const onChange = (_val: string) => {
|
||||
emit('update:modelValue', _val);
|
||||
};
|
||||
|
||||
const getCollector = async (_val: string) => {
|
||||
if (!_val) {
|
||||
return [];
|
||||
} else {
|
||||
const resp = await queryCollectorNoPaging({
|
||||
terms: [
|
||||
{
|
||||
terms: [
|
||||
{
|
||||
column: 'channelId',
|
||||
value: _val,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
list.value = resp.result as any[];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getPoint = async (_val: string) => {
|
||||
if (!_val) {
|
||||
return [];
|
||||
} else {
|
||||
const resp = await queryPointNoPaging({
|
||||
terms: [
|
||||
{
|
||||
terms: [
|
||||
{
|
||||
column: 'collectorId',
|
||||
value: _val,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
list.value = resp.result as any[];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (_props.id) {
|
||||
if (_props.type === 'POINT') {
|
||||
getPoint(_props.id);
|
||||
} else {
|
||||
getCollector(_props.id);
|
||||
}
|
||||
} else {
|
||||
list.value = [];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
|
@ -1,146 +1,261 @@
|
|||
<template>
|
||||
<page-container :tabList="list" @back="onBack" :tabActiveKey="instanceStore.active" @tabChange="onTabChange">
|
||||
<page-container
|
||||
:tabList="list"
|
||||
@back="onBack"
|
||||
:tabActiveKey="instanceStore.active"
|
||||
@tabChange="onTabChange"
|
||||
>
|
||||
<template #title>
|
||||
<div>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div>{{instanceStore.current.name}}</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<div>{{ instanceStore.current.name }}</div>
|
||||
<a-divider type="vertical" />
|
||||
<a-space>
|
||||
<a-badge :text="instanceStore.current.state?.text" :status="statusMap.get(instanceStore.current.state?.value)" />
|
||||
<a-popconfirm title="确认启用设备" @confirm="handleAction" v-if="instanceStore.current.state?.value === 'notActive'">
|
||||
<a-badge
|
||||
:text="instanceStore.current.state?.text"
|
||||
:status="
|
||||
statusMap.get(
|
||||
instanceStore.current.state?.value,
|
||||
)
|
||||
"
|
||||
/>
|
||||
<a-popconfirm
|
||||
title="确认启用设备"
|
||||
@confirm="handleAction"
|
||||
v-if="
|
||||
instanceStore.current.state?.value ===
|
||||
'notActive'
|
||||
"
|
||||
>
|
||||
<a-button type="link">启用设备</a-button>
|
||||
</a-popconfirm>
|
||||
<a-popconfirm title="确认断开连接" @confirm="handleDisconnect" v-if="instanceStore.current.state?.value === 'online'">
|
||||
<a-popconfirm
|
||||
title="确认断开连接"
|
||||
@confirm="handleDisconnect"
|
||||
v-if="
|
||||
instanceStore.current.state?.value === 'online'
|
||||
"
|
||||
>
|
||||
<a-button type="link">断开连接</a-button>
|
||||
</a-popconfirm>
|
||||
<a-tooltip v-if="instanceStore.current?.accessProvider === 'child-device' &&
|
||||
instanceStore.current?.state?.value === 'offline'" :title="instanceStore.current?.features?.find((item) => item.id === 'selfManageState')
|
||||
? '该设备的在线状态与父设备(网关设备)保持一致'
|
||||
: '该设备在线状态由设备自身运行状态决定,不继承父设备(网关设备)的在线状态'">
|
||||
<AIcon type="QuestionCircleOutlined" style="font-size: 14px" />
|
||||
<a-tooltip
|
||||
v-if="
|
||||
instanceStore.current?.accessProvider ===
|
||||
'child-device' &&
|
||||
instanceStore.current?.state?.value ===
|
||||
'offline'
|
||||
"
|
||||
:title="
|
||||
instanceStore.current?.features?.find(
|
||||
(item) => item.id === 'selfManageState',
|
||||
)
|
||||
? '该设备的在线状态与父设备(网关设备)保持一致'
|
||||
: '该设备在线状态由设备自身运行状态决定,不继承父设备(网关设备)的在线状态'
|
||||
"
|
||||
>
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="font-size: 14px"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</div>
|
||||
<div style="padding-top: 10px">
|
||||
<a-descriptions size="small" :column="4">
|
||||
<a-descriptions-item label="ID">{{ instanceStore.current.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="ID">{{
|
||||
instanceStore.current.id
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="所属产品">
|
||||
<a-button style="margin-top: -5px; padding: 0" type="link" @click="jumpProduct">{{ instanceStore.current.productName }}</a-button>
|
||||
<a-button
|
||||
style="margin-top: -5px; padding: 0"
|
||||
type="link"
|
||||
@click="jumpProduct"
|
||||
>{{
|
||||
instanceStore.current.productName
|
||||
}}</a-button
|
||||
>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<img @click="handleRefresh" :src="getImage('/device/button.png')" style="margin-right: 20px; cursor: pointer;" />
|
||||
<img
|
||||
@click="handleRefresh"
|
||||
:src="getImage('/device/button.png')"
|
||||
style="margin-right: 20px; cursor: pointer"
|
||||
/>
|
||||
</template>
|
||||
<component :is="tabs[instanceStore.tabActiveKey]" v-bind="{ type: 'device' }" @onJump="onTabChange" />
|
||||
<component
|
||||
:is="tabs[instanceStore.tabActiveKey]"
|
||||
v-bind="{ type: 'device' }"
|
||||
@onJump="onTabChange"
|
||||
/>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
import Info from './Info/index.vue';
|
||||
import Running from './Running/index.vue'
|
||||
import Running from './Running/index.vue';
|
||||
import Metadata from '../../components/Metadata/index.vue';
|
||||
import ChildDevice from './ChildDevice/index.vue';
|
||||
import Diagnose from './Diagnose/index.vue'
|
||||
import Function from './Function/index.vue'
|
||||
import { _deploy, _disconnect } from '@/api/device/instance'
|
||||
import Diagnose from './Diagnose/index.vue';
|
||||
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 { _deploy, _disconnect } from '@/api/device/instance';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { getWebSocket } from '@/utils/websocket';
|
||||
|
||||
const route = useRoute();
|
||||
const instanceStore = useInstanceStore()
|
||||
const instanceStore = useInstanceStore();
|
||||
|
||||
const statusMap = new Map();
|
||||
statusMap.set('online', 'success');
|
||||
statusMap.set('offline', 'error');
|
||||
statusMap.set('notActive', 'warning');
|
||||
|
||||
const list = [
|
||||
const statusRef = ref();
|
||||
|
||||
const list = ref([
|
||||
{
|
||||
key: 'Info',
|
||||
tab: '实例信息'
|
||||
tab: '实例信息',
|
||||
},
|
||||
{
|
||||
key: 'Running',
|
||||
tab: '运行状态'
|
||||
tab: '运行状态',
|
||||
},
|
||||
{
|
||||
key: 'Metadata',
|
||||
tab: '物模型'
|
||||
tab: '物模型',
|
||||
},
|
||||
{
|
||||
key: 'Function',
|
||||
tab: '设备功能'
|
||||
tab: '设备功能',
|
||||
},
|
||||
{
|
||||
key: 'ChildDevice',
|
||||
tab: '子设备'
|
||||
tab: '子设备',
|
||||
},
|
||||
{
|
||||
key: 'Diagnose',
|
||||
tab: '设备诊断'
|
||||
},
|
||||
]
|
||||
]);
|
||||
|
||||
const tabs = {
|
||||
Info,
|
||||
Metadata,
|
||||
Running,
|
||||
ChildDevice,
|
||||
Diagnose,
|
||||
Function
|
||||
}
|
||||
Info,
|
||||
Metadata,
|
||||
Running,
|
||||
ChildDevice,
|
||||
Diagnose,
|
||||
Function,
|
||||
Modbus,
|
||||
OPCUA,
|
||||
EdgeMap,
|
||||
};
|
||||
|
||||
const getStatus = (id: string) => {
|
||||
statusRef.value = getWebSocket(
|
||||
`instance-editor-info-status-${id}`,
|
||||
`/dashboard/device/status/change/realTime`,
|
||||
{
|
||||
deviceId: id,
|
||||
},
|
||||
).subscribe(() => {
|
||||
instanceStore.refresh(id);
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => route.params.id,
|
||||
(newId) => {
|
||||
if(newId){
|
||||
instanceStore.tabActiveKey = 'Info'
|
||||
instanceStore.refresh(newId as string)
|
||||
if (newId) {
|
||||
instanceStore.tabActiveKey = 'Info';
|
||||
instanceStore.refresh(newId as string);
|
||||
|
||||
getStatus(String(newId));
|
||||
}
|
||||
},
|
||||
{immediate: true, deep: true}
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
const onBack = () => {
|
||||
|
||||
}
|
||||
const onBack = () => {};
|
||||
|
||||
const onTabChange = (e: string) => {
|
||||
instanceStore.tabActiveKey = e
|
||||
}
|
||||
instanceStore.tabActiveKey = e;
|
||||
};
|
||||
|
||||
const handleAction = async () => {
|
||||
if(instanceStore.current.id){
|
||||
const resp = await _deploy(instanceStore.current.id)
|
||||
if(resp.status === 200){
|
||||
message.success('操作成功!')
|
||||
instanceStore.refresh(instanceStore.current.id)
|
||||
if (instanceStore.current.id) {
|
||||
const resp = await _deploy(instanceStore.current.id);
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
instanceStore.refresh(instanceStore.current.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
if(instanceStore.current.id){
|
||||
const resp = await _disconnect(instanceStore.current.id)
|
||||
if(resp.status === 200){
|
||||
message.success('操作成功!')
|
||||
instanceStore.refresh(instanceStore.current.id)
|
||||
if (instanceStore.current.id) {
|
||||
const resp = await _disconnect(instanceStore.current.id);
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
instanceStore.refresh(instanceStore.current.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
if(instanceStore.current.id){
|
||||
await instanceStore.refresh(instanceStore.current.id)
|
||||
message.success('操作成功')
|
||||
if (instanceStore.current.id) {
|
||||
await instanceStore.refresh(instanceStore.current.id);
|
||||
message.success('操作成功');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const jumpProduct = () => {
|
||||
message.warn('暂未开发')
|
||||
}
|
||||
message.warn('暂未开发');
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
const keys = list.value.map((i) => i.key);
|
||||
if (instanceStore.current.protocol && !(['modbus-tcp', 'opc-ua'].includes(instanceStore.current.protocol)) && !keys.includes('Diagnose')) {
|
||||
list.value.push({
|
||||
key: 'Diagnose',
|
||||
tab: '设备诊断',
|
||||
});
|
||||
}
|
||||
if (
|
||||
instanceStore.current.protocol === 'modbus-tcp' &&
|
||||
!keys.includes('Modbus')
|
||||
) {
|
||||
list.value.push({
|
||||
key: 'Modbus',
|
||||
tab: 'Modbus TCP',
|
||||
});
|
||||
}
|
||||
if (
|
||||
instanceStore.current.protocol === 'opc-ua' &&
|
||||
!keys.includes('OPCUA')
|
||||
) {
|
||||
list.value.push({
|
||||
key: 'OPCUA',
|
||||
tab: 'OPC UA',
|
||||
});
|
||||
}
|
||||
if (
|
||||
instanceStore.current.accessProvider === 'edge-child-device' &&
|
||||
instanceStore.current.parentId &&
|
||||
!keys.includes('EdgeMap')
|
||||
) {
|
||||
list.value.push({
|
||||
key: 'EdgeMap',
|
||||
tab: '边缘端映射',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
statusRef.value && statusRef.value.unsubscribe();
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<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-form-item>
|
||||
<virtual-rule-param v-if="_value.source === 'rule'" v-model:value="_value.virtualRule" :name="name.concat(['virtualRule'])" :id="id"></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-form-item>
|
||||
</template>
|
||||
<script setup lang="ts" name="ExpandsForm">
|
||||
import { useMetadataStore } from '@/store/metadata';
|
||||
import { PropertySource } from '@/views/device/data';
|
||||
import { PropType } from 'vue';
|
||||
import VirtualRuleParam from '@/components/Metadata/VirtualRuleParam/index.vue';
|
||||
|
||||
type ValueType = Record<any, any>;
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object as PropType<ValueType>,
|
||||
default: () => ({})
|
||||
},
|
||||
type: {
|
||||
type: String
|
||||
},
|
||||
name: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => ([]),
|
||||
required: true
|
||||
},
|
||||
id: {
|
||||
type: String
|
||||
},
|
||||
})
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', data: ValueType): void;
|
||||
}
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const _value = computed({
|
||||
get: () => props.value,
|
||||
set: val => {
|
||||
emit('update:value', val)
|
||||
}
|
||||
})
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '读',
|
||||
value: 'read',
|
||||
},
|
||||
{
|
||||
label: '写',
|
||||
value: 'write',
|
||||
},
|
||||
{
|
||||
label: '上报',
|
||||
value: 'report',
|
||||
},
|
||||
]
|
||||
|
||||
const metadataStore = useMetadataStore()
|
||||
|
||||
onMounted(() => {
|
||||
if (props.type === 'product' || !props.value.source) {
|
||||
emit('update:value', { ...props.value, source: 'device' })
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
|
@ -16,12 +16,10 @@
|
|||
]">
|
||||
<a-input v-model:value="form.model.name" size="small"></a-input>
|
||||
</a-form-item>
|
||||
<ValueTypeForm :name="['valueType']" v-model:value="form.model.valueType" key="property"></ValueTypeForm>
|
||||
<a-form-item label="读写类型" :name="['expands', 'type']" :rules="[
|
||||
{ required: true, message: '请选择读写类型' },
|
||||
]">
|
||||
<a-select v-model:value="form.model.expands.type" :options="form.expandsType" mode="multiple" size="small"></a-select>
|
||||
</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个字符' },
|
||||
]">
|
||||
|
@ -30,8 +28,18 @@
|
|||
</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: {
|
||||
|
@ -39,20 +47,6 @@ const form = reactive({
|
|||
},
|
||||
expands: {}
|
||||
} as any,
|
||||
expandsType: [
|
||||
{
|
||||
label: '读',
|
||||
value: 'read',
|
||||
},
|
||||
{
|
||||
label: '写',
|
||||
value: 'write',
|
||||
},
|
||||
{
|
||||
label: '上报',
|
||||
value: 'report',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
</script>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<template #extra>
|
||||
<a-button :loading="save.loading" type="primary" @click="save.saveMetadata">保存</a-button>
|
||||
</template>
|
||||
<PropertyForm v-if="metadataStore.model.type === 'properties'"></PropertyForm>
|
||||
<PropertyForm v-if="metadataStore.model.type === 'properties'" :type="type"></PropertyForm>
|
||||
</a-drawer>
|
||||
</template>
|
||||
<script lang="ts" setup name="Edit">
|
||||
|
@ -20,12 +20,18 @@ import { SystemConst } from '@/utils/consts';
|
|||
import { detail } from '@/api/device/instance';
|
||||
import { DeviceInstance } from '@/views/device/Instance/typings';
|
||||
import PropertyForm from './PropertyForm.vue';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
interface Props {
|
||||
type: 'product' | 'device';
|
||||
tabs?: string;
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String as PropType<'product' | 'device'>,
|
||||
required: true,
|
||||
default: 'product'
|
||||
},
|
||||
tabs: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
const route = useRoute()
|
||||
|
||||
const instanceStore = useInstanceStore()
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
<script setup lang="ts" name="BaseMetadata">
|
||||
import type { MetadataItem, MetadataType } from '@/views/device/Product/typings'
|
||||
import MetadataMapping from './columns'
|
||||
import JTable, { JColumnProps } from '@/components/Table'
|
||||
import JTable from '@/components/Table'
|
||||
import { useInstanceStore } from '@/store/instance'
|
||||
import { useProductStore } from '@/store/product'
|
||||
import { useMetadataStore } from '@/store/metadata'
|
||||
|
@ -97,7 +97,7 @@ const expandsType = ref({
|
|||
write: '写',
|
||||
report: '上报',
|
||||
});
|
||||
const actions: JColumnProps[] = [
|
||||
const actions = [
|
||||
{
|
||||
title: '操作',
|
||||
align: 'left',
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
</template>
|
||||
<Basic ref="basicRef" />
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel key="2" forceRender>
|
||||
<a-collapse-panel key="2" >
|
||||
<template #header>
|
||||
<span class="title">菜单初始化</span>
|
||||
<span class="sub-title"
|
||||
|
@ -36,7 +36,7 @@
|
|||
</template>
|
||||
<Role ref="roleRef"></Role>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel key="4">
|
||||
<a-collapse-panel key="4" forceRender>
|
||||
<template #header>
|
||||
<span class="title">初始化数据</span>
|
||||
<span class="sub-title"
|
||||
|
|
|
@ -153,6 +153,7 @@ import { getImage } from '@/utils/comm';
|
|||
import { list, remove } from '@/api/link/protocol';
|
||||
import { message } from 'ant-design-vue';
|
||||
import Save from './Save/index.vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
const tableRef = ref<Record<string, any>>({});
|
||||
const router = useRouter();
|
||||
|
@ -261,7 +262,7 @@ const handlAdd = () => {
|
|||
visible.value = true;
|
||||
};
|
||||
const handlEdit = (data: object) => {
|
||||
current.value = data;
|
||||
current.value = _.cloneDeep(data);
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ const _value = computed({
|
|||
|
||||
const options = ref([]);
|
||||
const queryData = async () => {
|
||||
if (!props.configId) return;
|
||||
const { result } = await templateApi.getDept(props.type, props.configId);
|
||||
options.value = result.map((item: any) => ({
|
||||
label: item.name,
|
||||
|
|
|
@ -30,6 +30,7 @@ const _value = computed({
|
|||
|
||||
const options = ref([]);
|
||||
const queryData = async () => {
|
||||
if (!props.configId) return;
|
||||
const { result } = await templateApi.getTags(props.configId);
|
||||
options.value = result.map((item: any) => ({
|
||||
label: item.name,
|
||||
|
|
|
@ -30,6 +30,7 @@ const _value = computed({
|
|||
|
||||
const options = ref([]);
|
||||
const queryData = async () => {
|
||||
if (!props.configId) return;
|
||||
const { result } = await templateApi.getUser(props.type, props.configId);
|
||||
options.value = result.map((item: any) => ({
|
||||
label: item.name,
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
v-model:value="formData.type"
|
||||
placeholder="请选择通知方式"
|
||||
:disabled="!!formData.id"
|
||||
@change="handleTypeChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(item, index) in NOTICE_METHOD"
|
||||
|
@ -40,7 +41,7 @@
|
|||
<RadioCard
|
||||
:options="msgType"
|
||||
v-model="formData.provider"
|
||||
@change="getConfigList"
|
||||
@change="handleProviderChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
|
@ -227,7 +228,8 @@
|
|||
}"
|
||||
:showUploadList="false"
|
||||
@change="
|
||||
(e) => handleChange(e)
|
||||
(e) =>
|
||||
handleLinkChange(e)
|
||||
"
|
||||
>
|
||||
<AIcon
|
||||
|
@ -791,21 +793,46 @@ const formData = ref<TemplateFormData>({
|
|||
configId: '',
|
||||
});
|
||||
|
||||
/**
|
||||
* 重置公用字段值
|
||||
*/
|
||||
const resetPublicFiles = () => {
|
||||
formData.value.template.message = '';
|
||||
formData.value.configId = undefined;
|
||||
|
||||
if (
|
||||
formData.value.type === 'dingTalk' ||
|
||||
formData.value.type === 'weixin'
|
||||
) {
|
||||
formData.value.template.toTag = undefined;
|
||||
formData.value.template.toUser = undefined;
|
||||
formData.value.template.agentId = undefined;
|
||||
}
|
||||
if (formData.value.type === 'weixin')
|
||||
formData.value.template.toParty = undefined;
|
||||
if (formData.value.type === 'email')
|
||||
formData.value.template.toParty = undefined;
|
||||
// formData.value.description = '';
|
||||
};
|
||||
|
||||
// 根据通知方式展示对应的字段
|
||||
watch(
|
||||
() => formData.value.type,
|
||||
(val) => {
|
||||
// formData.value.template = TEMPLATE_FIELD_MAP[val];
|
||||
msgType.value = MSG_TYPE[val];
|
||||
|
||||
formData.value.provider = msgType.value[0].value;
|
||||
formData.value.provider =
|
||||
route.params.id !== ':id'
|
||||
? formData.value.provider
|
||||
: msgType.value[0].value;
|
||||
// formData.value.provider = formData.value.provider || msgType.value[0].value;
|
||||
// console.log('formData.value.template: ', formData.value.template);
|
||||
|
||||
formData.value.template =
|
||||
TEMPLATE_FIELD_MAP[val][formData.value.provider];
|
||||
// formData.value.template =
|
||||
// TEMPLATE_FIELD_MAP[val][formData.value.provider];
|
||||
|
||||
if (val !== 'email') getConfigList();
|
||||
clearValid();
|
||||
// clearValid();
|
||||
// console.log('formData.value: ', formData.value);
|
||||
|
||||
if (val === 'sms') {
|
||||
getTemplateList();
|
||||
|
@ -814,14 +841,14 @@ watch(
|
|||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => formData.value.provider,
|
||||
(val) => {
|
||||
formData.value.template = TEMPLATE_FIELD_MAP[formData.value.type][val];
|
||||
// watch(
|
||||
// () => formData.value.provider,
|
||||
// (val) => {
|
||||
// formData.value.template = TEMPLATE_FIELD_MAP[formData.value.type][val];
|
||||
|
||||
clearValid();
|
||||
},
|
||||
);
|
||||
// clearValid();
|
||||
// },
|
||||
// );
|
||||
|
||||
// 验证规则
|
||||
const formRules = ref({
|
||||
|
@ -884,12 +911,12 @@ watch(
|
|||
{ deep: true },
|
||||
);
|
||||
|
||||
const clearValid = () => {
|
||||
setTimeout(() => {
|
||||
formData.value.variableDefinitions = [];
|
||||
clearValidate();
|
||||
}, 200);
|
||||
};
|
||||
// const clearValid = () => {
|
||||
// setTimeout(() => {
|
||||
// formData.value.variableDefinitions = [];
|
||||
// clearValidate();
|
||||
// }, 200);
|
||||
// };
|
||||
|
||||
/**
|
||||
* 获取详情
|
||||
|
@ -917,10 +944,32 @@ const getConfigList = async () => {
|
|||
configList.value = result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 通知方式改变
|
||||
*/
|
||||
const handleTypeChange = () => {
|
||||
setTimeout(() => {
|
||||
formData.value.template =
|
||||
TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider];
|
||||
resetPublicFiles();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* 通知类型改变
|
||||
*/
|
||||
const handleProviderChange = () => {
|
||||
formData.value.template =
|
||||
TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider];
|
||||
console.log('formData.value.template: ', formData.value.template);
|
||||
getConfigList();
|
||||
resetPublicFiles();
|
||||
};
|
||||
|
||||
/**
|
||||
* link消息类型 图片链接
|
||||
*/
|
||||
const handleChange = (info: UploadChangeParam) => {
|
||||
const handleLinkChange = (info: UploadChangeParam) => {
|
||||
if (info.file.status === 'done') {
|
||||
formData.value.template.link.picUrl = info.file.response?.result;
|
||||
}
|
||||
|
|
|
@ -184,11 +184,11 @@ export const TEMPLATE_FIELD_MAP = {
|
|||
},
|
||||
voice: {
|
||||
aliyun: {
|
||||
templateType: '',
|
||||
templateType: 'tts',
|
||||
templateCode: '',
|
||||
ttsCode: '',
|
||||
message: '',
|
||||
playTimes: undefined,
|
||||
playTimes: 1,
|
||||
calledShowNumbers: '',
|
||||
calledNumber: '',
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:maskClosable="false"
|
||||
width="45vw"
|
||||
title="编辑"
|
||||
@cancel="close"
|
||||
@ok="save"
|
||||
visible
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
>
|
||||
<a-form layout="vertical" :model="inputData">
|
||||
<a-form-item
|
||||
label="kafka地址"
|
||||
name="address"
|
||||
:rules="[
|
||||
{
|
||||
max: 64,
|
||||
message: '最多输入64个字符',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="inputData.address"
|
||||
placeholder="请输入kafka地址"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="topic"
|
||||
name="topic"
|
||||
:rules="[
|
||||
{
|
||||
max: 64,
|
||||
message: '最多输入64个字符',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input v-model:value="inputData.topic"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-switch
|
||||
checked-children="启用"
|
||||
un-checked-children="启用"
|
||||
v-model:checked="inputData.status"
|
||||
></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Form } from 'ant-design-vue';
|
||||
import { saveOutputData } from '@/api/rule-engine/config';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
const useForm = Form.useForm;
|
||||
const Myprops = defineProps({
|
||||
data: {
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
let inputData = reactive({
|
||||
status: false,
|
||||
address: '',
|
||||
topic: '',
|
||||
});
|
||||
watchEffect(() => {
|
||||
inputData.status =
|
||||
Myprops.data?.data?.state?.value === 'enabled' ? true : false;
|
||||
inputData.address = Myprops.data?.data?.config?.config?.address;
|
||||
inputData.topic = Myprops.data?.data?.config?.config?.topic;
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
emit('closeModel');
|
||||
};
|
||||
const save = () => {
|
||||
saveOutputData({
|
||||
config: {
|
||||
sourceType: 'kafka',
|
||||
config: {
|
||||
...inputData,
|
||||
state: inputData?.status ? 'enabled' : 'disable',
|
||||
},
|
||||
},
|
||||
state: inputData?.status ? 'enabled' : 'disable',
|
||||
id: Myprops?.data?.data?.id,
|
||||
sourceType: 'kafka',
|
||||
exchangeType: 'consume',
|
||||
}).then((res) => {
|
||||
if (res.status === 200) {
|
||||
message.success('操作成功');
|
||||
emit('saveSuc');
|
||||
}
|
||||
});
|
||||
};
|
||||
const emit = defineEmits(['closeModel', 'saveSuc']);
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
</style>
|
|
@ -0,0 +1,109 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:maskClosable="false"
|
||||
width="45vw"
|
||||
title="编辑"
|
||||
@cancel="close"
|
||||
@ok="save"
|
||||
visible
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
>
|
||||
<a-form layout="vertical" :model="outputData">
|
||||
<a-form-item label="状态">
|
||||
<a-switch
|
||||
checked-children="启用"
|
||||
un-checked-children="启用"
|
||||
v-model:checked="outputData.status"
|
||||
></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="outputData.status"
|
||||
label="kafka地址"
|
||||
name="address"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入kafka地址',
|
||||
},
|
||||
{
|
||||
max: 64,
|
||||
message: '最多输入64个字符',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="outputData.address"
|
||||
placeholder="请输入kafka地址"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="outputData.status"
|
||||
label="topic"
|
||||
name="topic"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入topic',
|
||||
},
|
||||
{
|
||||
max: 64,
|
||||
message: '最多输入64个字符',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input v-model:value="outputData.topic"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Form } from 'ant-design-vue';
|
||||
import { saveOutputData } from '@/api/rule-engine/config';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
const useForm = Form.useForm;
|
||||
const Myprops = defineProps({
|
||||
data: {
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
let outputData = reactive({
|
||||
status: false,
|
||||
address: '',
|
||||
topic: '',
|
||||
});
|
||||
watchEffect(() => {
|
||||
outputData.status =
|
||||
Myprops.data?.data?.state?.value === 'enabled' ? true : false;
|
||||
outputData.address = Myprops.data?.data?.config?.config?.address;
|
||||
outputData.topic = Myprops.data?.data?.config?.config?.topic;
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
emit('closeModel');
|
||||
};
|
||||
const save = () => {
|
||||
saveOutputData({
|
||||
config: {
|
||||
sourceType: 'kafka',
|
||||
config: {
|
||||
...outputData,
|
||||
state: outputData?.status ? 'enabled' : 'disable',
|
||||
},
|
||||
},
|
||||
state: outputData?.status ? 'enabled' : 'disable',
|
||||
id: Myprops?.data?.data?.id,
|
||||
sourceType: 'kafka',
|
||||
exchangeType: 'producer',
|
||||
}).then((res) => {
|
||||
if (res.status === 200) {
|
||||
message.success('操作成功');
|
||||
emit('saveSuc');
|
||||
}
|
||||
});
|
||||
};
|
||||
const emit = defineEmits(['closeModel', 'saveSuc']);
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
</style>
|
|
@ -0,0 +1,532 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="14">
|
||||
<div class="alarmFlow-left">
|
||||
<a-card
|
||||
:head-style="{ borderBottom: 'none', height: '30px' }"
|
||||
:bordered="false"
|
||||
>
|
||||
<template #title>
|
||||
<div class="alarmTitle">
|
||||
<span>告警数据输出</span>
|
||||
<a-tooltip
|
||||
title="将告警数据输出到其他第三方系统"
|
||||
>
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="
|
||||
margin-left: 6px;
|
||||
line-height: 35px;
|
||||
"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<div
|
||||
style="
|
||||
margin: 0 0px 0 4px;
|
||||
color: #1d39c4;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
<edit-outlined @click="showOutput"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-descriptions
|
||||
bordered
|
||||
:labelStyle="{ width: 112 + 'px' }"
|
||||
:contentStyle="{ minWidth: 100 + 'px' }"
|
||||
:column="2"
|
||||
>
|
||||
<a-descriptions-item
|
||||
label="kafka地址"
|
||||
:content-style="{ minWidth: '200px' }"
|
||||
><a-badge
|
||||
:status="
|
||||
output?.running ? 'success' : 'error'
|
||||
"
|
||||
:text="
|
||||
output?.data?.config?.config?.address ||
|
||||
''
|
||||
"
|
||||
></a-badge
|
||||
></a-descriptions-item>
|
||||
<a-descriptions-item label="topic">{{
|
||||
output?.data?.config?.config?.topic || ''
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态" :span="2"
|
||||
><a-badge
|
||||
:status="
|
||||
output?.data?.state?.value === 'enabled'
|
||||
? 'success'
|
||||
: 'error'
|
||||
"
|
||||
:text="output?.data?.state?.text || ''"
|
||||
></a-badge
|
||||
></a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
<a-card
|
||||
:head-style="{ borderBottom: 'none', height: '30px' }"
|
||||
:bordered="false"
|
||||
>
|
||||
<template #title>
|
||||
<div class="alarmTitle">
|
||||
<span>告警处理结果输入</span>
|
||||
<a-tooltip title="接收第三方系统处理的告警结果">
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="
|
||||
margin-left: 6px;
|
||||
line-height: 35px;
|
||||
"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<div
|
||||
style="
|
||||
margin: 0 0px 0 4px;
|
||||
color: #1d39c4;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
<edit-outlined @click="showInput"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-descriptions
|
||||
bordered
|
||||
:labelStyle="{ width: 112 + 'px' }"
|
||||
:contentStyle="{ minWidth: 150 + 'px' }"
|
||||
:column="2"
|
||||
>
|
||||
<a-descriptions-item label="kafka地址"
|
||||
><a-badge
|
||||
:status="
|
||||
input?.running ? 'success' : 'error'
|
||||
"
|
||||
:text="
|
||||
input?.data?.config?.config?.address ||
|
||||
''
|
||||
"
|
||||
></a-badge
|
||||
></a-descriptions-item>
|
||||
<a-descriptions-item label="topic">{{
|
||||
input?.data?.config?.config?.topic || ''
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态" :span="2"
|
||||
><a-badge
|
||||
:status="
|
||||
input?.data?.state?.value === 'enabled'
|
||||
? 'success'
|
||||
: 'error'
|
||||
"
|
||||
:text="input?.data?.state?.text || ''"
|
||||
></a-badge
|
||||
></a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="10">
|
||||
<div class="alarmFlow-right">
|
||||
<div class="doc">
|
||||
<h1>功能图示</h1>
|
||||
<div class="image">
|
||||
<a-image
|
||||
width="100%"
|
||||
:src="getImage('/alarm/io.png')"
|
||||
></a-image>
|
||||
</div>
|
||||
<h1>功能说明</h1>
|
||||
<div>
|
||||
1、平台支持将告警数据输出到kafka,第三方系统可订阅kafka中的告警数据,进行业务处理。
|
||||
</div>
|
||||
<h2>输出参数</h2>
|
||||
<div>
|
||||
<a-table
|
||||
:dataSource="outputData"
|
||||
:pagination="false"
|
||||
:columns="outputColumns"
|
||||
></a-table>
|
||||
</div>
|
||||
<h2>示例</h2>
|
||||
<div v-html="markdownOutputText" class="code"></div>
|
||||
<div>
|
||||
2、平台支持订阅kafka中告警处理数据,并更新告警记录状态。
|
||||
</div>
|
||||
<h2>订阅参数</h2>
|
||||
<div>
|
||||
<a-table
|
||||
:dataSource="subData"
|
||||
:pagination="false"
|
||||
:columns="subColumns"
|
||||
></a-table>
|
||||
</div>
|
||||
<h2>示例</h2>
|
||||
<div class="code" v-html="markdownSubText"></div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<InputSave :data="input" v-if="inputVisible" @closeModel="closeInput" @saveSuc="saveInput"/>
|
||||
<OutputSave :data="output" v-if="outputVisible" @closeModel="closeOutput" @saveSuc="saveOutput"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import InputSave from './Save/input.vue'
|
||||
import OutputSave from './save/output.vue'
|
||||
import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { getDataExchange } from '@/api/rule-engine/config';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { marked } from 'marked';
|
||||
let input = ref<any>();
|
||||
let output = ref<any>();
|
||||
const outputData = [
|
||||
{
|
||||
key: 'alarmConfigName',
|
||||
name: '告警配置名称',
|
||||
type: 'string',
|
||||
desc: '推送的告警配置名称',
|
||||
example: '烟感告警',
|
||||
},
|
||||
{
|
||||
key: 'alarmConfigId',
|
||||
name: '告警配置ID',
|
||||
type: 'string',
|
||||
desc: '推送的告警配置ID',
|
||||
example: '1605111722418597888',
|
||||
},
|
||||
{
|
||||
key: 'Id',
|
||||
name: '告警数据ID',
|
||||
type: 'string',
|
||||
desc: '告警唯一性标识',
|
||||
example: '1515992841393119232',
|
||||
},
|
||||
{
|
||||
key: 'alarmRecordId',
|
||||
name: '告警记录ID',
|
||||
type: 'string',
|
||||
desc: '告警记录的唯一标识,可根据此ID处理告警',
|
||||
example: 'ba33a59ca5ebe3dccfcd75fd0575be4e',
|
||||
},
|
||||
{
|
||||
key: 'targetType',
|
||||
name: '告警目标类型',
|
||||
type: 'string',
|
||||
desc: '告警所属的业务类型,具体有产品、设备、部门、其他',
|
||||
example: '产品',
|
||||
},
|
||||
{
|
||||
key: 'targetId',
|
||||
name: '告警目标ID',
|
||||
type: 'string',
|
||||
desc: '告警目标唯一性标识',
|
||||
example: '1583300346713661440',
|
||||
},
|
||||
{
|
||||
key: 'targetName',
|
||||
name: '告警目标名称',
|
||||
type: 'string',
|
||||
desc: '告警目标实例名称',
|
||||
example: '海康烟感',
|
||||
},
|
||||
{
|
||||
key: 'alarmTime',
|
||||
name: '告警时间',
|
||||
type: 'long',
|
||||
desc: '告警触发时间',
|
||||
example: '1651233650840',
|
||||
},
|
||||
{
|
||||
key: 'sourceType',
|
||||
name: '告警源类型',
|
||||
type: 'string',
|
||||
desc: '触发告警的源类型。当前只有device',
|
||||
example: 'device',
|
||||
},
|
||||
{
|
||||
key: 'sourceId',
|
||||
name: '告警源ID',
|
||||
type: 'string',
|
||||
desc: '触发告警的源Id。如设备Id',
|
||||
example: '1605138218826821632',
|
||||
},
|
||||
{
|
||||
key: 'sourceName',
|
||||
name: '告警源名称',
|
||||
type: 'string',
|
||||
desc: '触发告警的源名称。如设备名称',
|
||||
example: '1楼烟感S01',
|
||||
},
|
||||
{
|
||||
key: 'level',
|
||||
name: '告警级别',
|
||||
type: 'int',
|
||||
desc: '告警严重程度指标',
|
||||
example: 1,
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
name: '告警说明',
|
||||
type: 'string',
|
||||
desc: '告警规则说明',
|
||||
example: '1楼烟感统一告警规则设置',
|
||||
},
|
||||
];
|
||||
const subData = [
|
||||
{
|
||||
key: 'alarmRecordId',
|
||||
name: '告警记录ID',
|
||||
type: 'string',
|
||||
require: '是',
|
||||
desc: '告警记录的唯一标识,可根据此ID处理告警',
|
||||
example: 'ba33a59ca5ebe3dccfcd75fd0575be4e',
|
||||
},
|
||||
{
|
||||
key: 'alarmConfigId',
|
||||
name: '告警配置ID',
|
||||
type: 'string',
|
||||
require: '是',
|
||||
desc: '推送的告警配置ID',
|
||||
example: '1605111722418597888',
|
||||
},
|
||||
{
|
||||
key: 'alarmTime',
|
||||
name: '告警时间',
|
||||
type: 'long',
|
||||
require: '是',
|
||||
desc: '告警触发时间',
|
||||
example: '1651233650840',
|
||||
},
|
||||
{
|
||||
key: 'handleTime',
|
||||
name: '处理时间',
|
||||
type: 'long',
|
||||
require: '是',
|
||||
desc: '告警处理时间,不填是默认为消息处理时间',
|
||||
example: '1651233650840',
|
||||
},
|
||||
{
|
||||
key: 'describe',
|
||||
name: '处理说明',
|
||||
type: 'string',
|
||||
require: '是',
|
||||
desc: '告警处理内容详细描述说明',
|
||||
example: '已联系第三方人员进行告警处理,现告警已恢复',
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
name: '处理类型',
|
||||
type: 'enum',
|
||||
require: '是',
|
||||
desc: '支持system、user',
|
||||
example: 'user',
|
||||
},
|
||||
{
|
||||
key: 'state',
|
||||
name: '处理后的状态',
|
||||
type: 'enum',
|
||||
require: '是',
|
||||
desc: 'warning、normal',
|
||||
example: 'normal',
|
||||
},
|
||||
];
|
||||
const outputColumns = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '标识',
|
||||
dataIndex: 'key',
|
||||
key: 'key',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'desc',
|
||||
key: 'desc',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '示例值',
|
||||
dataIndex: 'example',
|
||||
key: 'example',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
];
|
||||
const subColumns = [...outputColumns];
|
||||
subColumns.splice(3, 0, {
|
||||
title: '必填',
|
||||
dataIndex: 'require',
|
||||
key: 'require',
|
||||
ellipsis: true,
|
||||
});
|
||||
const subText = `
|
||||
~~~json
|
||||
{
|
||||
"alarmRecordId": "ba33a59ca5ebe3dccfcd75fd0575be4e",
|
||||
"alarmConfigId": "1605111722418597888",
|
||||
"alarmTime": "1651233650840",
|
||||
"handleTime": "1651233650841",
|
||||
"describe": "已联系第三方人员进行告警处理,现告警已恢复",
|
||||
"type": "user",
|
||||
"state": "normal"
|
||||
}
|
||||
~~~
|
||||
`;
|
||||
const outputText = `
|
||||
~~~json
|
||||
{
|
||||
"alarmConfigId": "1605111722418597888",
|
||||
"id": "1515992841393119232",
|
||||
"alarmConfigId": "1586989804257853441",
|
||||
"alarmConfigName": "烟感告警",
|
||||
"alarmRecordId": "ba33a59ca5ebe3dccfcd75fd0575be4e",
|
||||
"level": "3",
|
||||
"description": "设备温度过高",
|
||||
"alarmTime": "1667202964007",
|
||||
"sourceType": "device",
|
||||
"sourceId": "1605138218826821632",
|
||||
"sourceName": "1楼烟感S01",
|
||||
"targetType": "device",
|
||||
"targetName": "温度探测设备",
|
||||
"targetId": "1583300346713661440"
|
||||
}
|
||||
~~~
|
||||
`;
|
||||
const render = new marked.Renderer();
|
||||
const markdownSubText = shallowRef(marked(subText));
|
||||
const markdownOutputText = shallowRef(marked(outputText));
|
||||
let inputVisible = ref(false);
|
||||
let outputVisible = ref(false);
|
||||
marked.setOptions({
|
||||
renderer: render,
|
||||
gfm: true,
|
||||
pedantic: false,
|
||||
});
|
||||
const handleOutputSearch = () => {
|
||||
getDataExchange('producer').then((res) => {
|
||||
if (res.status === 200) {
|
||||
output.value = res.result;
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleInputSearch = () => {
|
||||
getDataExchange('consume').then((res) => {
|
||||
if (res.status === 200) {
|
||||
input.value = res.result;
|
||||
}
|
||||
});
|
||||
};
|
||||
handleInputSearch();
|
||||
handleOutputSearch();
|
||||
const showInput = () => {
|
||||
inputVisible.value = true;
|
||||
}
|
||||
const closeInput = () =>{
|
||||
inputVisible.value = false;
|
||||
}
|
||||
const saveInput = () =>{
|
||||
inputVisible.value = false;
|
||||
handleInputSearch();
|
||||
}
|
||||
const showOutput = () =>{
|
||||
outputVisible.value = true;
|
||||
}
|
||||
const closeOutput = () =>{
|
||||
outputVisible.value = false;
|
||||
}
|
||||
const saveOutput = () =>{
|
||||
outputVisible.value = false;
|
||||
handleOutputSearch();
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.alarmTitle {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
margin-bottom: 16px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.alarmTitle::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background-color: #1d39c4;
|
||||
border-radius: 0 3px 3px 0;
|
||||
content: ' ';
|
||||
}
|
||||
.alarmFlow-left,
|
||||
.alarmFlow-right {
|
||||
height: 780px;
|
||||
background-color: white;
|
||||
}
|
||||
.alarmFlow-right {
|
||||
margin-left: 20px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
.doc {
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
color: rgba(#000, 0.8);
|
||||
font-size: 14px;
|
||||
background-color: #fff;
|
||||
|
||||
.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 10px;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.image {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.code {
|
||||
padding: 16px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,10 +1,14 @@
|
|||
<template>
|
||||
<page-container :tabList="list" @tabChange="onTabChange">
|
||||
<div v-if="true">
|
||||
<page-container :tabList="list" @tabChange="onTabChange" :tabActiveKey="tab">
|
||||
<div v-if="tab=='config'">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="14">
|
||||
<div class="alarm-level">
|
||||
<a-card :headStyle="{ borderBottom: 'none' }" :bodyStyle="{paddingTop:0}">
|
||||
<a-card
|
||||
:headStyle="{ borderBottom: 'none', padding: 0 }"
|
||||
:bodyStyle="{ padding: 0 }"
|
||||
:bordered="false"
|
||||
>
|
||||
<template #title>
|
||||
<div class="alarmLevelTitle">告警级别配置</div>
|
||||
</template>
|
||||
|
@ -23,21 +27,44 @@
|
|||
<span>{{ `级别${i + 1}` }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<a-input type="text" v-model:value="item.title" :maxlength=64></a-input>
|
||||
<a-input
|
||||
type="text"
|
||||
v-model:value="item.title"
|
||||
:maxlength="64"
|
||||
></a-input>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="middle"
|
||||
@click="handleSaveLevel"
|
||||
>保存</a-button
|
||||
>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="10">
|
||||
<div class="description">
|
||||
<h1>功能说明</h1>
|
||||
<div>
|
||||
1、告警级别用于描述告警的严重程度,请根据业务管理方式进行自定义。
|
||||
</div>
|
||||
<div>2、告警级别将会在告警配置中被引用。</div>
|
||||
<div>3、最多可配置5个级别。</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="10">123</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<Io v-else></Io>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { queryLevel } from '@/api/rule-engine/config';
|
||||
import { queryLevel, saveLevel } from '@/api/rule-engine/config';
|
||||
import { LevelItem } from './typing';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import Io from './Io/index.vue'
|
||||
const list = ref([
|
||||
{
|
||||
key: 'config',
|
||||
|
@ -48,11 +75,8 @@ const list = ref([
|
|||
tab: '数据流转',
|
||||
},
|
||||
]);
|
||||
interface levelsObj {
|
||||
level: number;
|
||||
title?: string;
|
||||
}
|
||||
let levels = ref<levelsObj[]>([]);
|
||||
let levels = ref<LevelItem[]>([]);
|
||||
let tab = ref<'io'|'config'|string>('config');
|
||||
const getAlarmLevel = () => {
|
||||
queryLevel().then((res: any) => {
|
||||
if (res.status == 200) {
|
||||
|
@ -61,11 +85,22 @@ const getAlarmLevel = () => {
|
|||
});
|
||||
};
|
||||
getAlarmLevel();
|
||||
const onTabChange = (e: string) => {};
|
||||
const handleSaveLevel = async () => {
|
||||
saveLevel(levels.value).then((res) => {
|
||||
if (res.status === 200) {
|
||||
message.success('操作成功');
|
||||
}
|
||||
});
|
||||
};
|
||||
const onTabChange = (e: string) => {
|
||||
tab.value = e;
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.alarm-level {
|
||||
padding: 24px;
|
||||
background-color: white;
|
||||
height: 700px;
|
||||
}
|
||||
.alarmLevelTitle {
|
||||
position: relative;
|
||||
|
@ -88,4 +123,18 @@ const onTabChange = (e: string) => {};
|
|||
.alarmInputItem {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.description {
|
||||
height: 700px;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
color: rgba(#000, 0.8);
|
||||
font-size: 14px;
|
||||
background-color: #fff;
|
||||
h1 {
|
||||
margin: 16px 0;
|
||||
color: rgba(#000, 0.85);
|
||||
font-weight: bold;
|
||||
font-size: 14px;}
|
||||
}
|
||||
</style>
|
|
@ -84,6 +84,7 @@ 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>,
|
||||
|
@ -216,6 +217,7 @@ const respParamsCard = reactive<tableCardType>({
|
|||
const tableData = findData(schemaName);
|
||||
const codeText = getCodeText(tableData, 3);
|
||||
|
||||
emit('update:paramsTable', tableData)
|
||||
respParamsCard.tableData = tableData;
|
||||
respParamsCard.codeText = JSON.stringify(codeText);
|
||||
|
||||
|
|
|
@ -12,16 +12,16 @@
|
|||
<div class="api-card">
|
||||
<h5>请求参数</h5>
|
||||
<div class="content">
|
||||
<VueJsoneditor
|
||||
<!-- <VueJsoneditor
|
||||
height="400"
|
||||
mode="tree"
|
||||
v-model:text="requestBody.paramsText"
|
||||
/>
|
||||
<!-- <MonacoEditor
|
||||
/> -->
|
||||
<MonacoEditor
|
||||
v-model:modelValue="requestBody.paramsText"
|
||||
style="height: 300px; width: 100%"
|
||||
theme="vs"
|
||||
/> -->
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-card">
|
||||
|
@ -47,21 +47,34 @@ import VueJsoneditor from 'vue3-ts-jsoneditor';
|
|||
import MonacoEditor from '@/components/MonacoEditor/index.vue';
|
||||
import type { apiDetailsType } from '../typing';
|
||||
import InputCard from './InputCard.vue';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
selectApi: {
|
||||
type: Object as PropType<apiDetailsType>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const props = defineProps<{
|
||||
selectApi: apiDetailsType;
|
||||
paramsTable: any[];
|
||||
}>();
|
||||
|
||||
const requestBody = reactive({
|
||||
paramsTable: [],
|
||||
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>
|
||||
|
|
|
@ -25,10 +25,11 @@
|
|||
<ApiDoes
|
||||
:select-api="selectedApi"
|
||||
:schemas="schemas"
|
||||
v-model:params-table="paramsTable"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="test" tab="调试">
|
||||
<ApiTest :select-api="selectedApi" />
|
||||
<ApiTest :select-api="selectedApi" :params-table="paramsTable" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
|
@ -66,8 +67,9 @@ const treeSelect = (node: treeNodeTpye, nodeSchemas: object = {}) => {
|
|||
tableData.value = table;
|
||||
};
|
||||
|
||||
const activeKey = ref('does');
|
||||
const activeKey = ref<'does' | 'test'>('does');
|
||||
const schemas = ref({});
|
||||
const paramsTable = ref([])
|
||||
const initSelectedApi: apiDetailsType = {
|
||||
url: '',
|
||||
method: '',
|
||||
|
@ -78,7 +80,10 @@ const initSelectedApi: apiDetailsType = {
|
|||
};
|
||||
const selectedApi = ref<apiDetailsType>(initSelectedApi);
|
||||
|
||||
watch(tableData, () => (selectedApi.value = initSelectedApi));
|
||||
watch(tableData, () => {
|
||||
activeKey.value = 'does';
|
||||
selectedApi.value = initSelectedApi;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<div>
|
||||
应用管理
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,378 @@
|
|||
<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 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);
|
||||
|
||||
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>
|
|
@ -0,0 +1,299 @@
|
|||
<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" @click="send">发送</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-card">
|
||||
<h5>请求参数</h5>
|
||||
<div class="content">
|
||||
<!-- <VueJsoneditor
|
||||
height="400"
|
||||
mode="tree"
|
||||
v-model:text="requestBody.paramsText"
|
||||
/> -->
|
||||
<div class="table" v-if="paramsTable.length">
|
||||
<a-form :model="requestBody.params" ref="formRef" >
|
||||
<a-table
|
||||
:columns="requestBody.tableColumns"
|
||||
:dataSource="paramsTable"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<a-form-item
|
||||
:name="[
|
||||
'paramsTable',
|
||||
index +
|
||||
(requestBody.pageNum - 1) *
|
||||
requestBody.pageSize,
|
||||
'name',
|
||||
]"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '该字段是必填字段',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="record.name"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<a-form-item
|
||||
:name="[
|
||||
'paramsTable',
|
||||
index +
|
||||
(requestBody.pageNum - 1) *
|
||||
requestBody.pageSize,
|
||||
'value',
|
||||
]"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '该字段是必填字段',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="record.value"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<PermissionButton
|
||||
type="link"
|
||||
:uhasPermission="`{permission}:delete`"
|
||||
:popConfirm="{
|
||||
title: `确定删除`,
|
||||
onConfirm: () =>
|
||||
requestBody.clickDel(index),
|
||||
}"
|
||||
>
|
||||
<AIcon type="DeleteOutlined" />
|
||||
</PermissionButton>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-form>
|
||||
|
||||
<a-pagination
|
||||
:pageSize="requestBody.pageSize"
|
||||
v-model:current="requestBody.pageNum"
|
||||
:total="requestBody.params.paramsTable.length"
|
||||
hideOnSinglePage
|
||||
style="text-align: center"
|
||||
/>
|
||||
<a-button
|
||||
@click="requestBody.addRow"
|
||||
style="width: 100%; text-align: center"
|
||||
>
|
||||
<AIcon type="PlusOutlined" />新增
|
||||
</a-button>
|
||||
</div>
|
||||
<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"
|
||||
:disabled="true"
|
||||
/>
|
||||
<!-- <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';
|
||||
import { cloneDeep, toLower } from 'lodash';
|
||||
import { FormInstance } from 'ant-design-vue';
|
||||
import server from '@/utils/request'
|
||||
|
||||
const props = defineProps<{
|
||||
selectApi: apiDetailsType;
|
||||
}>();
|
||||
const formRef = ref<FormInstance>();
|
||||
const requestBody = reactive({
|
||||
tableColumns: [
|
||||
{
|
||||
title: '参数名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '参数值',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: '80px',
|
||||
scopedSlots: true,
|
||||
},
|
||||
],
|
||||
pageSize: 10,
|
||||
pageNum: 1,
|
||||
params: {
|
||||
paramsTable: cloneDeep(props.selectApi.parameters || []) as requestObj[],
|
||||
},
|
||||
|
||||
paramsText: '',
|
||||
|
||||
addRow: () => {
|
||||
if (paramsTable.value.length === 10)
|
||||
requestBody.pageNum = requestBody.pageNum + 1;
|
||||
requestBody.params.paramsTable.push({
|
||||
name: '',
|
||||
value: '',
|
||||
});
|
||||
},
|
||||
clickDel: (index: number) => {
|
||||
if (paramsTable.value.length === 1 && requestBody.pageNum > 1)
|
||||
requestBody.pageNum = requestBody.pageNum - 1;
|
||||
requestBody.params.paramsTable.splice(index, 1);
|
||||
},
|
||||
});
|
||||
const paramsTable = computed(() => {
|
||||
const startIndex = (requestBody.pageNum - 1) * requestBody.pageSize;
|
||||
const endIndex = requestBody.pageNum * requestBody.pageSize;
|
||||
return requestBody.params.paramsTable.slice(startIndex, endIndex);
|
||||
});
|
||||
|
||||
const responsesContent = ref('{"a":123}');
|
||||
|
||||
const send = () => {
|
||||
formRef.value &&
|
||||
formRef.value.validate().then(() => {
|
||||
const methodName = toLower(props.selectApi.method)
|
||||
const methodObj = {
|
||||
get: 'get',
|
||||
post: 'post',
|
||||
patch: 'patch',
|
||||
put: 'put',
|
||||
delete: 'remove'
|
||||
}
|
||||
|
||||
let url = props.selectApi?.url;
|
||||
const urlParams = {}
|
||||
requestBody.params.paramsTable.forEach(item=>{
|
||||
if(methodName === 'get')
|
||||
urlParams[item.name] = item.value
|
||||
if(url.includes(`{${item.name}}`))
|
||||
url = url.replace(`{${item.name}}`, item.value)
|
||||
})
|
||||
const params = {
|
||||
...JSON.parse(requestBody.paramsText || '{}'),
|
||||
...urlParams
|
||||
}
|
||||
|
||||
|
||||
|
||||
server[methodObj[methodName]](url,params).then((resp:any)=>{
|
||||
responsesContent.value = JSON.stringify(resp)
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
type requestObj = {
|
||||
name: 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;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.table {
|
||||
:deep(.ant-table-cell) {
|
||||
padding: 0 8px;
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,98 @@
|
|||
<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" @click="save">保存</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { addOperations_api, delOperations_api } from '@/api/system/apiPage';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { modeType } from '../typing';
|
||||
const emits = defineEmits(['update:clickApi', 'update:selectedRowKeys']);
|
||||
const props = defineProps<{
|
||||
tableData: any[];
|
||||
clickApi: any;
|
||||
selectedRowKeys: string[];
|
||||
sourceKeys: string[];
|
||||
mode: modeType;
|
||||
}>();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'API',
|
||||
dataIndex: 'url',
|
||||
key: 'url',
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'summary',
|
||||
key: 'summary',
|
||||
},
|
||||
];
|
||||
const rowSelection = {
|
||||
onSelect: (record: any) => {
|
||||
let newKeys = [...props.selectedRowKeys];
|
||||
|
||||
if (props.selectedRowKeys.includes(record.id)) {
|
||||
newKeys = newKeys.filter((id) => id !== record.id);
|
||||
} else newKeys.push(record.id);
|
||||
|
||||
emits('update:selectedRowKeys', newKeys);
|
||||
},
|
||||
selectedRowKeys: ref<string[]>([]),
|
||||
};
|
||||
const save = () => {
|
||||
const keys = props.selectedRowKeys;
|
||||
|
||||
const removeKeys = props.sourceKeys.filter((key) => !keys.includes(key));
|
||||
const addKeys = keys.filter((key) => !props.sourceKeys.includes(key));
|
||||
|
||||
if (props.mode === 'api') {
|
||||
// 此时是api配置
|
||||
removeKeys.length &&
|
||||
delOperations_api(removeKeys)
|
||||
.finally(() => addOperations_api(addKeys))
|
||||
.then(() => message.success('操作成功'));
|
||||
}
|
||||
};
|
||||
const jump = (row: any) => {
|
||||
emits('update:clickApi', row);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.selectedRowKeys,
|
||||
(n) => {
|
||||
rowSelection.selectedRowKeys.value = n;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.choose-api-container {
|
||||
height: 100%;
|
||||
|
||||
:deep(.jtable-body-header) {
|
||||
display: none !important;
|
||||
}
|
||||
:deep(.ant-alert-info) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,35 @@
|
|||
<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>
|
|
@ -0,0 +1,100 @@
|
|||
<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 type { modeType, treeNodeTpye } from '../typing';
|
||||
|
||||
const emits = defineEmits(['select']);
|
||||
const props = defineProps<{
|
||||
mode:modeType
|
||||
}>()
|
||||
|
||||
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>
|
|
@ -0,0 +1,124 @@
|
|||
<template>
|
||||
<a-card class="api-page-container">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="5">
|
||||
<LeftTree @select="treeSelect" :mode="props.mode" />
|
||||
</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"
|
||||
/>
|
||||
|
||||
<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"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="test" tab="调试">
|
||||
<ApiTest :select-api="selectedApi" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="apiPage">
|
||||
|
||||
import { getApiGranted_api, apiOperations_api } from '@/api/system/apiPage';
|
||||
import type {
|
||||
treeNodeTpye,
|
||||
apiObjType,
|
||||
apiDetailsType,
|
||||
modeType,
|
||||
} 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 route = useRoute();
|
||||
const props = defineProps<{
|
||||
mode: modeType;
|
||||
}>();
|
||||
|
||||
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 as any;
|
||||
for (const key in method) {
|
||||
if (Object.prototype.hasOwnProperty.call(method, key)) {
|
||||
table.push({
|
||||
...method[key],
|
||||
url,
|
||||
method: key,
|
||||
id: method[key].operationId,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
tableData.value = table;
|
||||
};
|
||||
|
||||
const activeKey = ref<'does' | 'test'>('does');
|
||||
const schemas = ref({});
|
||||
const initSelectedApi: apiDetailsType = {
|
||||
url: '',
|
||||
method: '',
|
||||
summary: '',
|
||||
parameters: [],
|
||||
responses: {},
|
||||
requestBody: {},
|
||||
};
|
||||
const selectedApi = ref<apiDetailsType>(initSelectedApi);
|
||||
|
||||
const canSelectKeys = ref<string[]>([]); // 左侧可展示的项
|
||||
const selectedKeys = ref<string[]>([]); // 右侧默认勾选的项
|
||||
let selectSourceKeys = ref<string[]>([])
|
||||
init();
|
||||
|
||||
function init() {
|
||||
const code = route.query.code;
|
||||
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[]]
|
||||
})
|
||||
}
|
||||
watch(tableData, () => {
|
||||
activeKey.value = 'does';
|
||||
selectedApi.value = initSelectedApi;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.api-page-container {
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,27 @@
|
|||
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;
|
||||
}
|
||||
|
||||
export type modeType = 'api'| 'appManger' | 'home'
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<div>
|
||||
<Api mode="api" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Platforms">
|
||||
import Api from './Api/index.vue';
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -86,8 +86,8 @@ export default defineConfig(({ mode}) => {
|
|||
// target: 'http://192.168.32.244:8881',
|
||||
// target: 'http://47.112.135.104:5096', // opcua
|
||||
// target: 'http://120.77.179.54:8844', // 120测试
|
||||
// target: 'http://47.108.63.174:8845', // 测试
|
||||
target: 'http://120.77.179.54:8844',
|
||||
target: 'http://47.108.63.174:8845', // 测试
|
||||
// target: 'http://120.77.179.54:8844',
|
||||
ws: 'ws://120.77.179.54:8844',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
|
|
Loading…
Reference in New Issue