fix: 合并冲突

This commit is contained in:
jackhoo_98 2023-03-01 17:32:29 +08:00
commit 86c15261e8
47 changed files with 3230 additions and 580 deletions

2
.npmrc
View File

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

View File

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

View File

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

View File

@ -485,11 +485,60 @@ export const getPropertiesInfo = (deviceId: string, data: Record<string, unknown
export const getPropertiesList = (deviceId: string, property: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/property/${property}/_query`, data)
/**
*
* @param id
* @param transport
* @returns
*/
export const getProtocal = (id: string, transport: string) => server.get(`/protocol/${id}/transport/${transport}`)
/**
*
* @param productId
* @returns
*/
export const productCode = (productId: string) => server.get(`/device/transparent-codec/${productId}`)
/**
*
* @param productId
* @returns
*/
export const saveProductCode = (productId: string,data: Record<string, unknown>) => server.post(`/device/transparent-codec/${productId}`,data)
/**
*
* @param productId
* @param deviceId
* @returns
*/
export const deviceCode = (productId: string,deviceId:string) => server.get(`device/transparent-codec/${productId}/${deviceId}`)
/**
*
* @param productId
*
* @param deviceId
* @param data
* @returns
*/
export const saveDeviceCode = (productId: string,deviceId:string,data: Record<string, unknown>) => server.post(`/device/transparent-codec/${productId}/${deviceId}`,data)
/**
*
* @param data
* @returns
*/
export const testCode = (data: Record<string, unknown>) => server.post(`/device/transparent-codec/decode-test`,data)
/**
*
* @param productId
* @param deviceId
* @returns
*/
export const delDeviceCode = (productId: string, deviceId: string) => server.remove(`/device/transparent-codec/${productId}/${deviceId}`)
/**
*
* @param productId
* @returns
*/
export const delProductCode = (productId: string) => server.remove(`/device/transparent-codec/${productId}`)
export const queryLog = (deviceId: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/logs`, data)
/**

View File

@ -4,8 +4,6 @@ import type { CascadeItem } from '@/views/media/Cascade/typings'
export default {
// 列表
list: (data: any) => server.post<any>(`/media/gb28181-cascade/_query`, data),
// 列表字段通道数量, 来自下面接口的total
queryCount: (id: string) => server.post<any>(`/media/gb28181-cascade/${id}/bindings/_query`),
// 详情
detail: (id: string): any => server.get(`/media/gb28181-cascade/${id}`),
// 新增
@ -26,4 +24,17 @@ export default {
// SIP本地地址
all: () => server.get<any>(`/network/resources/alive/_all`),
// 查询已绑定的通道, list列表字段通道数量, 来自下面接口的total
queryBindChannel: (id: string, data: any) => server.post<any>(`/media/gb28181-cascade/${id}/bindings/_query`, data),
// 绑定通道
bindChannel: (id: string, data: string[]) => server.post<any>(`/media/gb28181-cascade/${id}/_bind`, data),
// 解绑
unbindChannel: (id: string, data: string[]) => server.post<any>(`/media/gb28181-cascade/${id}/_unbind`, data),
// 验证国标ID是否存在
validateField: (id: string, data: string[]): any => server.post(`/media/gb28181-cascade/${id}/gbChannelId/_validate`, data),
// 更改国标ID
updateGbChannelId: (id: string, data: any): any => server.post(`/media/gb28181-cascade/binding/${id}`, data),
// 查询通道分页列表
queryChannelList: (data: any): any => server.post(`media/channel/_query`, data),
}

View File

@ -19,3 +19,8 @@ export const getOrgList = (parmas?:any) => server.get('/organization/_query/no-p
*
*/
export const query = (data:any) => server.post('/alarm/record/_query/',data);
/**
*
*/
export const handleLog = (data:any) => server.post('/alarm/record/_handle',data)

View File

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

View File

@ -12,7 +12,7 @@ export const delApply_api = (id: string) => server.remove(`/application/${id}`)
// 获取组织列表
export const getDepartmentList_api = () => server.get(`/organization/_all/tree`);
// 获取组织列表
// 获取应用详情
export const getAppInfo_api = (id: string) => server.get(`/application/${id}`);
// 新增应用
export const addApp_api = (data: object) => server.post(`/application`, data);

View File

@ -58,9 +58,12 @@ const iconKeys = [
'PauseOutlined',
'ControlOutlined',
'RedoOutlined',
'ExpandOutlined',
'VideoCameraOutlined',
'HistoryOutlined',
'CalendarOutlined',
'ToolOutlined',
'FileOutlined',
'LikeOutlined',
]
const Icon = (props: {type: string}) => {

View File

@ -75,7 +75,7 @@ watchEffect(() => {
});
const insert = (val) => {
if (!instance) return
if (!instance) return;
const position = instance.getPosition();
instance.executeEdits(instance.getValue(), [
{
@ -88,17 +88,19 @@ const insert = (val) => {
text: val,
},
]);
}
};
watch(() => props.modelValue,
(val) => {
instance.setValue(val)
})
// watch(
// () => props.modelValue,
// (val) => {
// instance.setValue(val);
// },
// );
defineExpose({
editorFormat,
insert,
})
});
</script>
<style lang="less" scoped>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -87,11 +87,11 @@ watchEffect(() => {
</script>
<style lang="less" scoped>
@import 'ant-design-vue/es/style/themes/default.less';
// @import 'ant-design-vue/es/style/themes/default.less';
:root {
--dialog-primary-color: @primary-color;
}
// :root {
// --dialog-primary-color: @primary-color;
// }
.dialog-item {
display: flex;

View File

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

View File

@ -0,0 +1,306 @@
<template>
<a-card>
<div>
<div class="top">
<div class="top-left">
<div>
<AIcon type="ExclamationCircleOutlined" />
<template v-if="topTitle === 'rest'">
当前数据解析内容已脱离产品影响
<PermissionButton type="link" hasPermission="device/Instance:update" @click="rest()">
重置
</PermissionButton>
后将继承产品数据解析内容
</template>
<template v-else>
当前数据解析内容继承自产品,
<PermissionButton type="link" hasPermission="device/Instance:update" @click="readOnly = false"
:style="color">
修改
</PermissionButton>
后将脱离产品影响
</template>
</div>
</div>
<div>
脚本语言:
<a-select :defaultValue="'JavaScript'" style="width: 200;margin-left: 5px;">
<a-select-option value="JavaScript">JavaScript(ECMAScript 5)</a-select-option>
</a-select>
<AIcon type="ExpandOutlined" style="margin-left: 20px;" @click="toggle" />
</div>
</div>
<div class="edit" ref="el">
<div v-show="readOnly" class="edit-only" @click="() => {
message.warning({
key: 1,
content: () => '请点击上方修改字样,用以编辑脚本',
style: {
marginTop: '260px'
}
})
}"></div>
<MonacoEditor language="javascript" style="height: 100%;" theme="vs" v-model:modelValue="editorValue" />
</div>
<div class="bottom">
<div style="width: 49.5%;">
<div class="bottom-title">
<div class="bottom-title-text">模拟输入</div>
<div class="bottom-title-topic">
<template v-if="instanceStore.current.transport === 'MQTT'">
<div style="margin-right: 5px;">Topic:</div>
<a-auto-complete placeholder="请输入Topic" style="width: 300px" :options="topicList"
:allowClear="true" :filterOption="(inputValue: any, option: any) =>
option!.value.indexOf(inputValue) !== -1" v-model:value="topic" />
</template>
<template v-else>
<div style="margin-right: 5px;">URL:</div>
<a-input placeholder="请输入URL" v-model:value="url" style="width: 300px"></a-input>
</template>
</div>
</div>
<a-textarea :rows="5" placeholder="// 二进制数据以0x开头的十六进制输入字符串数据输入原始字符串" style="margin-top: 10px;"
v-model:value="simulation" />
</div>
<div style="width: 49.5%;">
<div class="bottom-title">
<div class="bottom-title-text">运行结果</div>
</div>
<a-textarea :autoSize="{ minRows: 5 }" :style="resStyle" v-model:value="result" />
</div>
</div>
</div>
<div style="margin-top: 10px;margin-left: 10px;">
<PermissionButton type="primary" hasPermission="device/Instance:update" :loading="loading"
:disabled="isDisabled" @click="debug()" :tooltip="{
title: '需输入脚本和模拟数据后再点击',
}">
调试
</PermissionButton>
<PermissionButton hasPermission="device/Instance:update" :loading="loading" :disabled="!isTest" @click="save()"
:style="{ marginLeft: '10px' }" :tooltip="{
title: isTest ? '' : '请先调试',
}">
保存
</PermissionButton>
</div>
</a-card>
</template>
<script setup lang='ts' name="Parsing">
import AIcon from '@/components/AIcon'
import PermissionButton from '@/components/PermissionButton/index.vue'
import MonacoEditor from '@/components/MonacoEditor/index.vue';
import { useFullscreen } from '@vueuse/core'
import { useInstanceStore } from '@/store/instance';
import {
deviceCode,
getProtocal,
testCode,
saveDeviceCode,
delDeviceCode
} from '@/api/device/instance'
import { message } from 'ant-design-vue';
import { isBoolean } from 'lodash';
const defaultValue =
'//解码函数\r\nfunction decode(context) {\r\n //原始报文\r\n var buffer = context.payload();\r\n // 转为json\r\n // var json = context.json();\r\n //mqtt 时通过此方法获取topic\r\n // var topic = context.topic();\r\n\r\n // 提取变量\r\n // var topicVars = context.pathVars("/{deviceId}/**",topic)\r\n //温度属性\r\n var temperature = buffer.getShort(3) * 10;\r\n //湿度属性\r\n var humidity = buffer.getShort(6) * 10;\r\n return {\r\n "temperature": temperature,\r\n "humidity": humidity\r\n };\r\n}\r\n';
const el = ref<HTMLElement | null>(null)
const { toggle } = useFullscreen(el)
const instanceStore = useInstanceStore();
const topTitle = ref<string>('')
const readOnly = ref<boolean>(true)
const url = ref<string>('')
const topic = ref<string>('')
const topicList = ref([])
const simulation = ref<string>('')
const resultValue = ref<any>({})
const loading = ref<boolean>(false)
const isTest = ref<boolean>(false)
const editorValue = ref<string>('')
const color = computed(() => ({
color: readOnly.value ? '#415ed1' : '#a6a6a6'
}))
const resStyle = computed(() => (isBoolean(resultValue.value.success) ? {
'margin-top': '10px',
'border-color': resultValue.value.success ? 'green' : 'red'
} : {
'margin-top': '10px',
}))
const isDisabled = computed(() => simulation.value === '')
const result = computed(() => resultValue.value.success ? JSON.stringify(resultValue.value.outputs?.[0]) : resultValue.value.reason)
//
const rest = async () => {
const res = await delDeviceCode(instanceStore.current.productId, instanceStore.current.id)
if (res.status === 200) {
getDeviceCode();
message.success('操作成功')
}
// service.delDeviceCode(productId, deviceId).then((res) => {
// if (res.status === 200) {
// getDeviceCode(productId, deviceId);
// onlyMessage('');
// }
// });
};
//topic
const getTopic = async () => {
const res: any = await getProtocal(instanceStore.current.protocol, instanceStore.current.transport)
if (res.status === 200) {
const item = res.result.routes?.map((items: any) => ({
value: items.topic,
}));
// setTopicList(item);
topicList.value = item
}
};
//
const getDeviceCode = async () => {
const res: any = await deviceCode(instanceStore.current.productId, instanceStore.current.id)
if (res.status === 200) {
const item = res.result?.configuration?.script ? res.result?.configuration?.script : defaultValue
if (res.result?.deviceId) {
readOnly.value = false
topTitle.value = 'rest'
editorValue.value = item
} else {
readOnly.value = true
topTitle.value = 'edit'
editorValue.value = item
}
}
}
//
const test = async (dataTest: any) => {
loading.value = true
const res = await testCode(dataTest)
if (res.status === 200) {
loading.value = false
resultValue.value = res?.result
} else {
loading.value = false
}
};
//
const save = async () => {
const item = {
provider: 'jsr223',
configuration: {
script: editorValue.value,
lang: 'javascript',
},
}
const res = await saveDeviceCode(instanceStore.current.productId, instanceStore.current.id, item)
if (res.status === 200) {
message.success('保存成功');
getDeviceCode();
}
};
const debug = () => {
if (instanceStore.current.transport === 'MQTT') {
if (topic.value !== '') {
test({
headers: {
topic: topic.value,
},
configuration: {
script: editorValue.value,
lang: 'javascript',
},
provider: 'jsr223',
payload: simulation.value,
})
isTest.value = true
} else {
message.error('请输入topic');
}
} else {
if (url.value !== '') {
test({
headers: {
url: url.value,
},
provider: 'jsr223',
configuration: {
script: editorValue.value,
lang: 'javascript',
},
payload: simulation.value,
});
isTest.value = true
} else {
message.error('请输入url');
}
}
}
onMounted(() => {
getDeviceCode()
getTopic()
})
</script>
<style scoped lang='less'>
.top {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
.top-left {
display: flex;
align-items: center;
}
}
.edit {
height: 550px;
border: 1px solid #dcdcdc;
.edit-only {
height: 550px;
width: 97%;
position: absolute;
z-index: 1;
background-color: #eeeeee70;
cursor: not-allowed;
}
}
.bottom {
display: flex;
justify-content: space-between;
padding: 10px;
background-color: '#f7f7f7';
.bottom-title {
display: flex;
justify-content: space-between;
.bottom-title-text {
font-weight: 600;
font-size: 14px;
margin-top: 10px;
}
.bottom-title-topic {
display: flex;
align-items: center;
}
}
}
</style>

View File

@ -116,6 +116,7 @@ import Function from './Function/index.vue';
import Modbus from './Modbus/index.vue';
import OPCUA from './OPCUA/index.vue';
import EdgeMap from './EdgeMap/index.vue';
import Parsing from './Parsing/index.vue'
import Log from './Log/index.vue'
import { _deploy, _disconnect } from '@/api/device/instance';
import { message } from 'ant-design-vue';
@ -172,6 +173,7 @@ const tabs = {
Modbus,
OPCUA,
EdgeMap,
Parsing,
Log
};
@ -281,6 +283,15 @@ watchEffect(() => {
tab: '边缘端映射',
});
}
if (
instanceStore.current.features?.find((item: any) => item.id === 'transparentCodec') &&
!keys.includes('Parsing')
) {
list.value.push({
key: 'Parsing',
tab: '数据解析',
});
}
});
onUnmounted(() => {

View File

@ -1,4 +1,246 @@
<!-- 数据解析 -->
<template></template>
<script></script>
<style></style>
<template>
<a-card>
<div>
<div class="top">
<div>
脚本语言:
<a-select :defaultValue="'JavaScript'" style="width: 200;margin-left: 5px;">
<a-select-option value="JavaScript">JavaScript(ECMAScript 5)</a-select-option>
</a-select>
<AIcon type="ExpandOutlined" style="margin-left: 20px;" @click="toggle" />
</div>
</div>
<div class="edit" ref="el">
<MonacoEditor language="javascript" style="height: 100%;" theme="vs" v-model:modelValue="editorValue" />
</div>
<div class="bottom">
<div style="width: 49.5%;">
<div class="bottom-title">
<div class="bottom-title-text">模拟输入</div>
<div class="bottom-title-topic">
<template v-if="productStore.current.transportProtocol === 'MQTT'">
<div style="margin-right: 5px;">Topic:</div>
<a-auto-complete placeholder="请输入Topic" style="width: 300px" :options="topicList"
:allowClear="true" :filterOption="(inputValue: any, option: any) =>
option!.value.indexOf(inputValue) !== -1" v-model:value="topic" />
</template>
<template v-else>
<div style="margin-right: 5px;">URL:</div>
<a-input placeholder="请输入URL" v-model:value="url" style="width: 300px"></a-input>
</template>
</div>
</div>
<a-textarea :rows="5" placeholder="// 二进制数据以0x开头的十六进制输入字符串数据输入原始字符串" style="margin-top: 10px;"
v-model:value="simulation" />
</div>
<div style="width: 49.5%;">
<div class="bottom-title">
<div class="bottom-title-text">运行结果</div>
</div>
<a-textarea :autoSize="{ minRows: 5 }" :style="resStyle" v-model:value="result" />
</div>
</div>
</div>
<div style="margin-top: 10px;margin-left: 10px;">
<PermissionButton type="primary" hasPermission="device/Instance:update" :loading="loading"
:disabled="isDisabled" @click="debug()" :tooltip="{
title: '需输入脚本和模拟数据后再点击',
}">
调试
</PermissionButton>
<PermissionButton hasPermission="device/Instance:update" :loading="loading" :disabled="!isTest" @click="save()"
:style="{ marginLeft: '10px' }" :tooltip="{
title: isTest ? '' : '请先调试',
}">
保存
</PermissionButton>
</div>
</a-card>
</template>
<script setup lang='ts' name="Parsing">
import AIcon from '@/components/AIcon'
import PermissionButton from '@/components/PermissionButton/index.vue'
import MonacoEditor from '@/components/MonacoEditor/index.vue';
import { useFullscreen } from '@vueuse/core'
import { useProductStore } from '@/store/product';
import {
productCode,
getProtocal,
testCode,
saveProductCode,
} from '@/api/device/instance'
import { message } from 'ant-design-vue';
import { isBoolean } from 'lodash';
const defaultValue =
'//解码函数\r\nfunction decode(context) {\r\n //原始报文\r\n var buffer = context.payload();\r\n // 转为json\r\n // var json = context.json();\r\n //mqtt 时通过此方法获取topic\r\n // var topic = context.topic();\r\n\r\n // 提取变量\r\n // var topicVars = context.pathVars("/{deviceId}/**",topic)\r\n //温度属性\r\n var temperature = buffer.getShort(3) * 10;\r\n //湿度属性\r\n var humidity = buffer.getShort(6) * 10;\r\n return {\r\n "temperature": temperature,\r\n "humidity": humidity\r\n };\r\n}\r\n';
const el = ref<HTMLElement | null>(null)
const { toggle } = useFullscreen(el)
const productStore = useProductStore()
const url = ref<string>('')
const topic = ref<string>('')
const topicList = ref([])
const simulation = ref<string>('')
const resultValue = ref<any>({})
const loading = ref<boolean>(false)
const isTest = ref<boolean>(false)
const editorValue = ref<string>('')
const resStyle = computed(() => (isBoolean(resultValue.value.success) ? {
'margin-top': '10px',
'border-color': resultValue.value.success ? 'green' : 'red'
} : {
'margin-top': '10px',
}))
const isDisabled = computed(() => simulation.value === '')
const result = computed(() => resultValue.value.success ? JSON.stringify(resultValue.value.outputs?.[0]) : resultValue.value.reason)
//topic
const getTopic = async () => {
const res: any = await getProtocal(productStore.current.messageProtocol, productStore.current.transportProtocol)
if (res.status === 200) {
const item = res.result.routes?.map((items: any) => ({
value: items.topic,
}));
topicList.value = item
}
};
//
const getProductCode = async () => {
const res: any = await productCode(productStore.current.id)
if (res.status === 200) {
if(res.result){
editorValue.value = res.result?.configuration?.script
}else{
editorValue.value = defaultValue
}
}
}
//
const test = async (dataTest: any) => {
loading.value = true
const res = await testCode(dataTest)
if (res.status === 200) {
loading.value = false
resultValue.value = res?.result
} else {
loading.value = false
}
};
//
const save = async () => {
const item = {
provider: 'jsr223',
configuration: {
script: editorValue.value,
lang: 'javascript',
},
}
const res = await saveProductCode(productStore.current.id, item)
if (res.status === 200) {
message.success('保存成功');
getProductCode();
}
};
const debug = () => {
if (productStore.current.transportProtocol === 'MQTT') {
if (topic.value !== '') {
test({
headers: {
topic: topic.value,
},
configuration: {
script: editorValue.value,
lang: 'javascript',
},
provider: 'jsr223',
payload: simulation.value,
})
isTest.value = true
} else {
message.error('请输入topic');
}
} else {
if (url.value !== '') {
test({
headers: {
url: url.value,
},
provider: 'jsr223',
configuration: {
script: editorValue.value,
lang: 'javascript',
},
payload: simulation.value,
});
isTest.value = true
} else {
message.error('请输入url');
}
}
}
onMounted(() => {
getProductCode()
getTopic()
})
</script>
<style scoped lang='less'>
.top {
display: flex;
justify-content: flex-end;
margin-bottom: 10px;
}
.edit {
height: 550px;
border: 1px solid #dcdcdc;
.edit-only {
height: 550px;
width: 97%;
position: absolute;
z-index: 1;
background-color: #eeeeee70;
cursor: not-allowed;
}
}
.bottom {
display: flex;
justify-content: space-between;
padding: 10px;
background-color: '#f7f7f7';
.bottom-title {
display: flex;
justify-content: space-between;
.bottom-title-text {
font-weight: 600;
font-size: 14px;
margin-top: 10px;
}
.bottom-title-topic {
display: flex;
align-items: center;
}
}
}
</style>

View File

@ -123,6 +123,7 @@ const tabs = {
Info,
Metadata,
Device,
DataAnalysis
};
watch(
@ -188,7 +189,7 @@ const handleUndeploy = async () => {
*/
const getProtocol = async () => {
if (productStore.current?.messageProtocol) {
const res = await getProtocolDetail(
const res:any = await getProtocolDetail(
productStore.current?.messageProtocol,
);
if (res.status === 200) {

View File

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

View File

@ -6,8 +6,26 @@
<DevOpsHome v-else-if="currentView === 'ops'" />
<ComprehensiveHome v-else-if="currentView === 'comprehensive'" />
<Api :mode="'home'" hasHome showTitle>
<template #top> </template>
<Api
v-else-if="currentView === 'api'"
:mode="'home'"
hasHome
showTitle
:code="clientId"
>
<template #top>
<div class="card">
<h3 style="margin: 0 0 24px 0">基本信息</h3>
<p>
<span style="font-weight: bold">clientId: </span>
<span>{{ clientId }}</span>
</p>
<p>
<span style="font-weight: bold">secureKey:</span>
<span>{{ secureKey }}</span>
</p>
</div>
</template>
</Api>
</div>
</page-container>
@ -19,14 +37,16 @@ import DeviceHome from './components/DeviceHome/index.vue';
import DevOpsHome from './components/DevOpsHome/index.vue';
import ComprehensiveHome from './components/ComprehensiveHome/index.vue';
import Api from '@/views/system/Platforms/Api/index.vue';
import { useUserInfo } from '@/store/userInfo';
import { isNoCommunity } from '@/utils/utils';
import { getMe_api, getView_api } from '@/api/home';
const router = useRouter();
import { getAppInfo_api } from '@/api/system/apply';
const currentView = ref<string>('');
const loading = ref<boolean>(true);
const clientId = useUserInfo().$state.userInfos.id;
const secureKey = ref<string>('');
//
const setCurrentView = () => {
@ -49,7 +69,12 @@ if (isNoCommunity) {
item.type === 'api-client' || item.type.id === 'api-client',
);
isApiUser ? router.push('/system/api') : setCurrentView();
if (isApiUser) {
currentView.value = 'api';
getAppInfo_api(clientId).then((resp: any) => {
secureKey.value = resp.result.apiServer.secureKey;
});
} else setCurrentView();
}
});
} else setCurrentView();
@ -57,7 +82,15 @@ if (isNoCommunity) {
<style lang="less" scoped>
.iot-home-container {
background: #f0f2f5;
overflow: hidden;
.card {
background-color: #fff;
padding: 24px;
margin-bottom: 24px;
p {
margin: 0;
font-size: 16px;
}
}
}
</style>

View File

@ -0,0 +1,211 @@
<!-- 国标级联-绑定通道 -->
<template>
<a-modal
v-model:visible="_vis"
title="绑定通道"
cancelText="取消"
okText="确定"
width="80%"
@ok="handleSave"
@cancel="_vis = false"
:confirmLoading="loading"
>
<Search
type="simple"
:columns="columns"
target="media"
@search="handleSearch"
/>
<JTable
ref="listRef"
model="table"
:columns="columns"
:request="CascadeApi.queryChannelList"
:defaultParams="{
sorts: [{ name: 'name', order: 'desc' }],
terms: [
{
column: 'id',
termType: 'cascade_channel$not',
type: 'and',
value: route.query.id,
},
{
column: 'catalogType',
termType: 'eq',
type: 'and',
value: 'device',
},
],
}"
:params="params"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onSelect: onSelectChange,
onSelectAll: onSelectAllChange,
}"
@cancelSelect="_selectedRowKeys = []"
>
<template #headerTitle>
<h3>通道列表</h3>
</template>
<template #status="slotProps">
<a-space>
<a-badge
:status="
slotProps.status.value === 'online'
? 'success'
: 'error'
"
:text="slotProps.status.text"
></a-badge>
</a-space>
</template>
</JTable>
</a-modal>
</template>
<script setup lang="ts">
import CascadeApi from '@/api/media/cascade';
import { message } from 'ant-design-vue';
import { PropType } from 'vue';
const route = useRoute();
type Emits = {
(e: 'update:visible', data: boolean): void;
(e: 'submit'): void;
};
const emit = defineEmits<Emits>();
const props = defineProps({
visible: { type: Boolean, default: false },
data: {
type: Object as PropType<Partial<Record<string, any>>>,
default: () => ({}),
},
});
const _vis = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val),
});
watch(
() => _vis.value,
(val) => {
if (val) handleSearch({ terms: [] });
},
);
const columns = [
{
title: '设备名称',
dataIndex: 'deviceName',
key: 'deviceName',
search: {
type: 'string',
},
},
{
title: '通道名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'string',
},
},
{
title: '安装地址',
dataIndex: 'address',
key: 'address',
search: {
type: 'string',
},
},
{
title: '厂商',
dataIndex: 'manufacturer',
key: 'manufacturer',
search: {
type: 'string',
},
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '已连接', value: 'online' },
{ label: '未连接', value: 'offline' },
],
handleValue: (v: any) => {
return v;
},
},
},
];
const params = ref<Record<string, any>>({});
/**
* 搜索
* @param params
*/
const handleSearch = (e: any) => {
params.value = e;
console.log('params.value: ', params.value);
};
const listRef = ref();
const _selectedRowKeys = ref<string[]>([]);
const onSelectChange = (
record: any[],
selected: boolean,
selectedRows: any[],
) => {
_selectedRowKeys.value = selected
? [...getSetRowKey(selectedRows)]
: _selectedRowKeys.value.filter((item: any) => item !== record?.id);
};
const onSelectAllChange = (
selected: boolean,
selectedRows: any[],
changeRows: any[],
) => {
const unRowsKeys = getSelectedRowsKey(changeRows);
_selectedRowKeys.value = selected
? [...getSetRowKey(selectedRows)]
: _selectedRowKeys.value
.concat(unRowsKeys)
.filter((item) => !unRowsKeys.includes(item));
};
const getSelectedRowsKey = (selectedRows: any[]) =>
selectedRows.map((item) => item?.id).filter((i) => !!i);
const getSetRowKey = (selectedRows: any[]) =>
new Set([..._selectedRowKeys.value, ...getSelectedRowsKey(selectedRows)]);
const loading = ref(false);
const handleSave = async () => {
if (!_selectedRowKeys.value.length) message.error('请勾选数据');
loading.value = true;
const resp = await CascadeApi.bindChannel(
route.query.id as string,
_selectedRowKeys.value,
);
loading.value = false;
if (resp.success) {
message.success('操作成功!');
_vis.value = false;
emit('submit');
} else {
message.error('操作失败!');
}
};
</script>

View File

@ -0,0 +1,267 @@
<!-- 国标级联-通道列表 -->
<template>
<page-container>
<Search
type="simple"
:columns="columns"
target="media"
@search="handleSearch"
/>
<JTable
ref="listRef"
model="table"
:columns="columns"
:request="(e:any) => CascadeApi.queryBindChannel(route?.query.id as string, e)"
:defaultParams="{
sorts: [{ name: 'name', order: 'desc' }],
}"
:params="params"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onSelect: onSelectChange,
onSelectAll: onSelectAllChange,
}"
@cancelSelect="_selectedRowKeys = []"
>
<template #headerTitle>
<h3>通道列表</h3>
</template>
<template #rightExtraRender>
<a-space>
<a-button type="primary" @click="bindVis = true">
绑定通道
</a-button>
<a-popconfirm
title="确认解绑?"
@confirm="handleMultipleUnbind"
>
<a-button> 批量解绑 </a-button>
</a-popconfirm>
</a-space>
</template>
<template #status="slotProps">
<a-space>
<a-badge
:status="
slotProps.status.value === 'online'
? 'success'
: 'error'
"
:text="slotProps.status.text"
></a-badge>
</a-space>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
:disabled="i.disabled"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
</template>
</JTable>
<BindChannel v-model:visible="bindVis" @submit="listRef.reload()" />
</page-container>
</template>
<script setup lang="ts">
import CascadeApi from '@/api/media/cascade';
import type { ActionsType } from '@/components/Table/index.vue';
import { message } from 'ant-design-vue';
import BindChannel from './BindChannel/index.vue';
const route = useRoute();
const columns = [
{
title: '设备名称',
dataIndex: 'deviceName',
key: 'deviceName',
search: {
type: 'string',
},
},
{
title: '通道名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'string',
},
},
{
title: '国标ID',
dataIndex: 'gbChannelId',
key: 'gbChannelId',
scopedSlots: true,
search: {
type: 'string',
},
},
{
title: '安装地址',
dataIndex: 'address',
key: 'address',
search: {
type: 'string',
},
},
{
title: '厂商',
dataIndex: 'manufacturer',
key: 'manufacturer',
search: {
type: 'string',
},
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '已连接', value: 'online' },
{ label: '未连接', value: 'offline' },
],
handleValue: (v: any) => {
return v;
},
},
},
{
title: '操作',
key: 'action',
scopedSlots: true,
},
];
const params = ref<Record<string, any>>({});
/**
* 搜索
* @param params
*/
const handleSearch = (e: any) => {
params.value = e;
console.log('params.value: ', params.value);
};
const listRef = ref();
const _selectedRowKeys = ref<string[]>([]);
const bindVis = ref(false);
const onSelectChange = (
record: any[],
selected: boolean,
selectedRows: any[],
) => {
_selectedRowKeys.value = selected
? [...getSetRowKey(selectedRows)]
: _selectedRowKeys.value.filter((item: any) => item !== record?.id);
};
const onSelectAllChange = (
selected: boolean,
selectedRows: any[],
changeRows: any[],
) => {
const unRowsKeys = getSelectedRowsKey(changeRows);
_selectedRowKeys.value = selected
? [...getSetRowKey(selectedRows)]
: _selectedRowKeys.value
.concat(unRowsKeys)
.filter((item) => !unRowsKeys.includes(item));
};
const getSelectedRowsKey = (selectedRows: any[]) =>
selectedRows.map((item) => item?.id).filter((i) => !!i);
const getSetRowKey = (selectedRows: any[]) =>
new Set([..._selectedRowKeys.value, ...getSelectedRowsKey(selectedRows)]);
/**
* 表格操作按钮
* @param data 表格数据项
* @param type 表格展示类型
*/
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
const actions = [
{
key: 'delete',
text: '解绑',
tooltip: {
title: '解绑',
},
icon: 'DisconnectOutlined',
popConfirm: {
title: '确认解绑?',
onConfirm: async () => {
const resp = await CascadeApi.unbindChannel(
route.query.id as string,
[data.channelId],
);
if (resp.success) {
message.success('操作成功!');
listRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
];
return actions;
};
/**
* 批量解绑
*/
const handleMultipleUnbind = async () => {
const channelIds = listRef.value?._dataSource
.filter((f: any) => _selectedRowKeys.value.includes(f.id))
.map((m: any) => m.channelId);
const resp = await CascadeApi.unbindChannel(
route.query.id as string,
channelIds,
);
if (resp.success) {
message.success('操作成功!');
listRef.value?.reload();
} else {
message.error('操作失败!');
}
};
</script>

View File

@ -157,8 +157,7 @@
message: '请输入上级SIP 地址',
},
{
max: 64,
message: '最多可输入64个字符',
validator: checkSIP,
},
]"
>
@ -213,7 +212,10 @@
:rules="[
{
required: true,
message: '请输入SIP本地地址',
message: '请选择SIP本地地址',
},
{
validator: checkLocalSIP,
},
]"
>
@ -242,11 +244,8 @@
<a-select
v-model:value="formData.port"
placeholder="请选择端口"
>
<a-select-option value="1">
1
</a-select-option>
</a-select>
:options="allListPorts"
/>
</a-col>
</a-row>
</a-form-item>
@ -261,8 +260,7 @@
message: '请输入SIP远程地址',
},
{
max: 64,
message: '最多可输入64个字符',
validator: checkPublicSIP,
},
]"
>
@ -303,6 +301,7 @@
<a-radio-group
button-style="solid"
v-model:value="formData.transport"
@change="setPorts"
>
<a-radio-button value="UDP">
UDP
@ -558,14 +557,11 @@
<script setup lang="ts">
import { getImage } from '@/utils/comm';
import { Form } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import CascadeApi from '@/api/media/cascade';
const router = useRouter();
const route = useRoute();
const useForm = Form.useForm;
//
const formData = ref({
@ -614,59 +610,102 @@ getClustersList();
/**
* SIP本地地址
*/
const allList = ref([]);
const allList = ref<any[]>([]);
const getAllList = async () => {
const { result } = await CascadeApi.all();
allList.value = result.map((m: any) => ({
label: m.host,
value: m.host,
}));
setPorts();
};
getAllList();
/**
* 传输协议改变, 获取对应的端口
*/
const allListPorts = ref([]);
const setPorts = () => {
allListPorts.value = allList.value.find(
(f: any) => f.host === formData.value.host,
)?.ports[formData.value.transport || ''];
};
/**
* 获取详情
*/
const getDetail = async () => {
if (!route.query.id) return;
const res = await CascadeApi.detail(route.query.id as string);
// console.log('res: ', res);
// formData.value = res.result;
// Object.assign(formData.value, res.result);
const { id, name, proxyStream, sipConfigs } = res.result;
formData.value = {
id,
cascadeName: name,
proxyStream,
clusterNodeId: sipConfigs[0]?.clusterNodeId,
name: sipConfigs[0]?.name,
sipId: sipConfigs[0]?.sipId,
domain: sipConfigs[0]?.domain,
remoteAddress: sipConfigs[0]?.remoteAddress,
remotePort: sipConfigs[0]?.remotePort,
localSipId: sipConfigs[0]?.localSipId,
host: sipConfigs[0]?.host,
port: sipConfigs[0]?.port,
publicHost: sipConfigs[0]?.publicHost,
publicPort: sipConfigs[0]?.publicPort,
transport: sipConfigs[0]?.transport,
user: sipConfigs[0]?.user,
password: sipConfigs[0]?.password,
manufacturer: sipConfigs[0]?.manufacturer,
model: sipConfigs[0]?.model,
firmware: sipConfigs[0]?.firmware,
keepaliveInterval: sipConfigs[0]?.keepaliveInterval,
registerInterval: sipConfigs[0]?.registerInterval,
};
console.log('formData.value: ', formData.value);
const { id, name, proxyStream, sipConfigs, ...others } = res.result;
Object.keys(formData.value).forEach((key: string) => {
if (key === 'id') formData.value[key] = id;
else if (key === 'cascadeName') formData.value[key] = name;
else if (key === 'proxyStream') formData.value[key] = proxyStream;
else formData.value[key] = sipConfigs[0][key];
});
// console.log('formData.value: ', formData.value);
};
onMounted(() => {
getDetail();
});
const regDomain =
/[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?/;
/**
* 上级SIP地址 字段验证
* @param _
* @param value 此处绑定的是 remoteAddress
*/
const checkSIP = (_: any, value: string) => {
return checkHost(value, formData.value.remotePort);
};
/**
* SIP远程地址 字段验证
* @param _
* @param value 此处绑定的是 publicHost
*/
const checkPublicSIP = (_: any, value: string) => {
return checkHost(value, formData.value.publicPort);
};
/**
* 字段验证
* @param host ip
* @param port 端口
*/
const checkHost = (host: string, port: string | number | undefined) => {
if (!host) {
return Promise.resolve();
} else if (!host) {
return Promise.reject(new Error('请输入IP 地址'));
} else if (host && !regDomain.test(host)) {
return Promise.reject(new Error('请输入正确的IP地址'));
} else if (!port) {
return Promise.reject(new Error('请输入端口'));
} else if ((host && Number(host) < 1) || Number(host) > 65535) {
return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
}
return Promise.resolve();
};
/**
* SIP本地地址 字段验证
* @param _
* @param value
*/
const checkLocalSIP = (_: any, value: string) => {
if (!value) {
return Promise.resolve();
} else if (!value) {
return Promise.reject(new Error('请选择IP地址'));
} else if (!formData.value.port) {
return Promise.reject(new Error('请选择端口'));
}
return Promise.resolve();
};
/**
* 表单提交
*/

View File

@ -197,12 +197,10 @@
</template>
<script setup lang="ts">
import DeviceApi from '@/api/media/device';
import CascadeApi from '@/api/media/cascade';
import type { ActionsType } from '@/components/Table/index.vue';
import { message } from 'ant-design-vue';
import { getImage } from '@/utils/comm';
import { PROVIDER_OPTIONS } from '@/views/media/Device/const';
import { useMenuStore } from 'store/menu';
@ -295,7 +293,7 @@ const handleSearch = (e: any) => {
const lastValueFrom = async (params: any) => {
const res = await CascadeApi.list(params);
res.result.data.forEach(async (item: any) => {
const resp = await queryBindChannel(item.id);
const resp = await queryChannelCount(item.id);
item.count = resp.result.total;
});
return res;
@ -305,8 +303,8 @@ const lastValueFrom = async (params: any) => {
* 查询通道数量
* @param id
*/
const queryBindChannel = async (id: string) => {
return await CascadeApi.queryCount(id);
const queryChannelCount = async (id: string) => {
return await CascadeApi.queryBindChannel(id, {});
};
/**

View File

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

View File

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

View File

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

View File

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

View File

@ -24,11 +24,18 @@
v-if="props.type === 'org'"
@search="search"
></Search>
<JTable :columns="columns" :request="handleSearch" :params="params">
<JTable
:columns="columns"
:request="handleSearch"
:params="params"
:gridColumn="2"
model="CARD"
>
<template #card="slotProps">
<CardBox
:value="slotProps"
v-bind="slotProps"
:actions="getActions(slotProps, 'card')"
:statusText="
data.defaultLevel.find(
(i) => i.level === slotProps.level,
@ -39,7 +46,7 @@
<img :src="imgMap.get(slotProps.targetType)" alt="" />
</template>
<template #content>
<Ellipsis>
<Ellipsis style="width: calc(100% - 100px)">
<span style="font-weight: 500">
{{ slotProps.alarmName }}
</span>
@ -90,10 +97,25 @@
</span>
</a-col>
</a-row>
</template>
<template #actions="item">
<PermissionButton
:disabled="item.key === 'solve' && slotProps.state.value ==='normal'"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</PermissionButton>
</template>
</CardBox>
</template>
</JTable>
<SolveLog :data="data" v-if="data.solveVisible" @closeSolve="closeSolve"/>
</div>
</template>
@ -111,6 +133,11 @@ import { useAlarmStore } from '@/store/alarm';
import { storeToRefs } from 'pinia';
import { Store } from 'jetlinks-store';
import moment from 'moment';
import type { ActionsType } from '@/components/Table';
import SolveLog from '../SolveLog/index.vue'
import { useMenuStore } from '@/store/menu';
const menuStory = useMenuStore();
const alarmStore = useAlarmStore();
const { data } = storeToRefs(alarmStore);
const getDefaulitLevel = () => {
@ -156,11 +183,11 @@ const columns = [
},
},
{
title: '最近告警事件',
title: '最近告警时间',
dataIndex: 'alarmTime',
key: 'alarmTime',
search: {
type: 'dateTime',
type: 'date',
},
},
{
@ -254,12 +281,7 @@ let param = reactive({
pageSize: 10,
terms: [],
});
// let dataSource = reactive({
// data: [],
// pageSize: 10,
// pageIndex: 0,
// total: 0,
// });
const handleSearch = async (params: any) => {
const resp = await query(params);
if (resp.status === 200) {
@ -284,33 +306,97 @@ const handleSearch = async (params: any) => {
};
watchEffect(() => {
if (props.type !== 'all' && !props.id) {
params.value.terms.push({
params.value.terms = [
{
termType: 'eq',
column: 'targetType',
value: props.type,
type: 'and',
});
},
];
}
if (props.id) {
params.value.terms.push({
params.value.terms = [
{
termType: 'eq',
column: 'alarmConfigId',
value: props.id,
type: 'and',
});
},
];
}
if(props.type === 'all'){
params.value.terms = [];
}
});
const search = (data: any) => {
const dt = {
pageSize: 10,
terms: [...data?.terms],
};
params.value.terms = [...data?.terms];
if (props.type !== 'all' && !props.id) {
params.value.terms.push(
{
termType: 'eq',
column: 'targetType',
value: props.type,
type: 'and',
},
);
}
if (props.id) {
params.value.terms.push (
{
termType: 'eq',
column: 'alarmConfigId',
value: props.id,
type: 'and',
},
);
}
};
const log = () => {
console.log(data.value.defaultLevel);
const getActions = (
currentData: Partial<Record<string, any>>,
type: 'card',
): ActionsType[] => {
if (!currentData) return [];
const actions = [
{
key: 'solve',
text: '告警处理',
tooltip: {
title: '告警处理',
},
icon: 'ToolOutlined',
onClick: () =>{
data.value.current = currentData;
data.value.solveVisible = true;
}
},
{
key: 'log',
text: '告警日志',
tooltip: {
title: '告警日志',
},
icon: 'FileOutlined',
onClick: () =>{
menuStory.jumpPage(`rule-engine/Alarm/Log/Detail`,{id:currentData.id});
}
},
{
key: 'detail',
text: '处理记录',
tooltip: {
title: '处理记录',
},
icon: 'FileTextOutlined',
},
];
return actions;
};
log();
const closeSolve = () =>{
data.value.solveVisible = false
}
</script>
<style lang="less" scoped>
</style>

View File

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

View File

@ -28,7 +28,7 @@ type Emit = {
const options = [
{ value: 'device', label: '设备触发', tip: '适用于设备数据或行为满足触发条件时,执行指定的动作', image: getImage('/device-trigger.png') },
{ value: 'manual', label: '手动触发', tip: '适用于第三方平台向物联网平台下发指令控制设备', image: getImage('/manual-trigger.png') },
{ value: 'timing', label: '定时触发', tip: '适用于定期执行固定任务', image: getImage('/timing-trigger.png') },
{ value: 'timer', label: '定时触发', tip: '适用于定期执行固定任务', image: getImage('/timing-trigger.png') },
]
const props = defineProps({
@ -63,7 +63,7 @@ const handleClick = (type: string) => {
</script>
<style scoped lang='less'>
@import 'ant-design-vue/es/style/themes/default.less';
// @import 'ant-design-vue/es/style/themes/default.less';
.scene-trigger-way-warp {display: flex;
flex-wrap: wrap;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,45 @@
<template>
<page-container>
<Api :mode="'appManger'" hasHome showTitle :code="code">
<Api :mode="'home'" hasHome showTitle :code="clientId">
<template #top>
<div class="card">
<h3 style="margin: 0 0 24px 0">基本信息</h3>
<p>
<span style="font-weight: bold">clientId: </span>
<span>{{ clientId }}</span>
</p>
<p>
<span style="font-weight: bold">secureKey:</span>
<span>{{ secureKey }}</span>
</p>
</div>
</template>
</Api>
</page-container>
</template>
<script setup lang="ts" name="apiPage">
import { getAppInfo_api } from '@/api/system/apply';
import Api from '@/views/system/Platforms/Api/index.vue';
const route = useRoute()
const code = route.query.code as string
const route = useRoute();
const clientId = route.query.code as string;
const secureKey = ref<string>('');
getAppInfo_api(clientId).then((resp: any) => {
secureKey.value = resp.result.apiServer.secureKey;
});
</script>
<style scoped></style>
<style lang="less" scoped>
.card {
background-color: #fff;
padding: 24px;
margin-bottom: 24px;
p {
margin: 0;
font-size: 16px;
}
}
</style>

View File

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

View File

@ -3,7 +3,7 @@
<JTable
:columns="columns"
:dataSource="props.tableData"
:rowSelection="rowSelection"
:rowSelection="props.mode !== 'home' ? rowSelection : undefined"
noPagination
model="TABLE"
>
@ -16,7 +16,9 @@
</template>
</JTable>
<a-button type="primary" @click="save">保存</a-button>
<a-button type="primary" @click="save" v-if="props.mode !== 'home'"
>保存</a-button
>
</div>
</template>

View File

@ -132,11 +132,9 @@ const filterPath = (path: object, filterArr: string[]) => {
delete value[prop];
}
}
if(Object.keys(value).length === 0) delete path[key]
if (Object.keys(value).length === 0) delete path[key];
}
}
console.log(path, filterArr);
return path;
};
</script>

View File

@ -3,7 +3,7 @@
<div class="top">
<slot name="top" />
</div>
<a-row :gutter="24" style="background-color: #fff; padding: 20px">
<a-row :gutter="24" style="background-color: #fff; padding: 20px;margin: 0;">
<a-col
:span="24"
v-if="props.showTitle"
@ -16,6 +16,7 @@
:mode="props.mode"
:has-home="props.hasHome"
:filter-array="treeFilter"
:code="props.code"
/>
</a-col>
<a-col :span="19">
@ -71,7 +72,6 @@ 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;
showTitle?: boolean;
@ -117,15 +117,17 @@ const initSelectedApi: apiDetailsType = {
};
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') {
getApiGranted_api(props.code as string).then((resp) => {
selectedKeys.value = resp.result as string[];
selectSourceKeys.value = [...(resp.result as string[])];
})
} else if (props.mode === 'api') {
apiOperations_api().then((resp) => {
selectedKeys.value = resp.result as string[];

View File

@ -115,7 +115,7 @@
>
</a-form-item>
<a-form-item>
<a-button
<j-button
:loading="loading"
type="primary"
html-type="submit"
@ -123,7 +123,7 @@
block
>
登录
</a-button>
</j-button>
</a-form-item>
</a-form>
<div class="other">
@ -133,14 +133,14 @@
</div>
</a-divider>
<div class="other-button">
<a-button
<j-button
v-for="(item, index) in bindings"
:key="index"
type="link"
@Click="handleClickOther(item)"
>
<img
style="width: 32px, height: 33px"
style="width: 32px; height: 33px"
:alt="item.name"
:src="
iconMap.get(
@ -148,7 +148,7 @@
) || defaultImg
"
/>
</a-button>
</j-button>
</div>
</div>
</div>
@ -443,6 +443,7 @@ screenRotation(screenWidth.value, screenHeight.value);
position: relative;
bottom: 10px;
text-align: center;
}
}
@ -463,6 +464,10 @@ screenRotation(screenWidth.value, screenHeight.value);
// vertical-align: middle;
}
}
.login-form-button {
width: 100%;
}
}
}
}

View File

@ -98,8 +98,8 @@ export default defineConfig(({ mode}) => {
preprocessorOptions: {
less: {
modifyVars: {
'root-entry-name': 'variable',
hack: `true; @import (reference) "${path.resolve('src/style/variable.less')}";`,
...Config.theme,
},
javascriptEnabled: true,
}

755
yarn.lock

File diff suppressed because it is too large Load Diff