Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev

This commit is contained in:
easy 2023-03-14 16:04:42 +08:00
commit 73bb4bbc5f
35 changed files with 7678 additions and 8671 deletions

2977
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,13 @@ export const detail = (id: string) => server.get<DeviceInstance>(`/device-instan
*/ */
export const query = (data?: Record<string, any>) => server.post('/device-instance/_query', data) export const query = (data?: Record<string, any>) => server.post('/device-instance/_query', data)
/**
*
* @param data
* @returns
*/
export const queryNoPagingPost = (data?: Record<string, any>) => server.post('/device-instance/_query/no-paging?paging=false', data)
/** /**
* *
* @param id ID * @param id ID

9
src/api/edge/device.ts Normal file
View File

@ -0,0 +1,9 @@
import server from '@/utils/request'
export const restPassword = (id: string) => server.post(`/edge/operations/${id}/auth-user-password-reset/invoke`)
export const _control = (deviceId: string) => server.get(`/edge/remote/${deviceId}/url`)
export const _stopControl = (deviceId: string) => server.get(`/edge/remote/${deviceId}/stop`, {})

14
src/api/edge/resource.ts Normal file
View File

@ -0,0 +1,14 @@
import server from '@/utils/request'
export const query = (data: Record<string, any>) => server.post(`/entity/template/_query`, data)
export const modify = (id: string, data: Record<string, any>) => server.put(`/entity/template/${id}`, data)
export const _delete = (id: string) => server.remove(`/entity/template/${id}`)
export const _start = (data: Record<string, any>) => server.post(`/entity/template/start/_batch`, data)
export const _stop = (data: Record<string, any>) => server.post(`/entity/template/stop/_batch`, data)
export const queryDeviceList = (data: Record<string, any>) => server.post(`/device-instance/detail/_query`, data)

View File

@ -55,7 +55,7 @@
allowClear allowClear
> >
<template #addonAfter> <template #addonAfter>
<j-upload <a-upload
name="file" name="file"
:action="FILE_UPLOAD" :action="FILE_UPLOAD"
:headers="headers" :headers="headers"
@ -63,7 +63,7 @@
@change="handleFileChange" @change="handleFileChange"
> >
<AIcon type="UploadOutlined" /> <AIcon type="UploadOutlined" />
</j-upload> </a-upload>
</template> </template>
</j-input> </j-input>
<j-input <j-input

View File

@ -145,6 +145,9 @@ const extraRouteObj = {
{ code: 'Save', name: '详情' }, { code: 'Save', name: '详情' },
], ],
}, },
'edge/Device': {
children: [{ code: 'Remote', name: '远程控制' }],
},
}; };

View File

@ -1,12 +1,29 @@
<template> <template>
<j-modal :maskClosable="false" width="800px" :visible="true" title="导入" @ok="handleSave" @cancel="handleCancel"> <j-modal
:maskClosable="false"
width="800px"
:visible="true"
title="导入"
@ok="handleSave"
@cancel="handleCancel"
>
<div style="margin-top: 10px"> <div style="margin-top: 10px">
<j-form :layout="'vertical'"> <j-form :layout="'vertical'">
<j-row> <j-row>
<j-col span="24"> <j-col span="24">
<j-form-item label="产品" required> <j-form-item label="产品" required>
<j-select showSearch v-model:value="modelRef.product" placeholder="请选择产品"> <j-select
<j-select-option :value="item.id" v-for="item in productList" :key="item.id" :label="item.name">{{ item.name }}</j-select-option> showSearch
v-model:value="modelRef.product"
placeholder="请选择产品"
>
<j-select-option
:value="item.id"
v-for="item in productList"
:key="item.id"
:label="item.name"
>{{ item.name }}</j-select-option
>
</j-select> </j-select>
</j-form-item> </j-form-item>
</j-col> </j-col>
@ -17,7 +34,11 @@
</j-col> </j-col>
<j-col span="12"> <j-col span="12">
<j-form-item label="文件上传" v-if="modelRef.product"> <j-form-item label="文件上传" v-if="modelRef.product">
<NormalUpload :product="modelRef.product" v-model="modelRef.upload" :file="modelRef.file" /> <NormalUpload
:product="modelRef.product"
v-model="modelRef.upload"
:file="modelRef.file"
/>
</j-form-item> </j-form-item>
</j-col> </j-col>
</j-row> </j-row>
@ -27,16 +48,17 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { queryNoPagingPost } from '@/api/device/product' import { queryNoPagingPost } from '@/api/device/product';
const emit = defineEmits(['close', 'save']) const emit = defineEmits(['close', 'save']);
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object, type: Object,
default: undefined default: undefined,
} },
}) type: String,
const productList = ref<Record<string, any>[]>([]) });
const productList = ref<Record<string, any>[]>([]);
const modelRef = reactive({ const modelRef = reactive({
product: undefined, product: undefined,
@ -44,26 +66,40 @@ const modelRef = reactive({
file: { file: {
fileType: 'xlsx', fileType: 'xlsx',
autoDeploy: false, autoDeploy: false,
} },
}); });
watch( watch(
() => props.data, () => props.data,
() => { () => {
queryNoPagingPost({paging: false}).then(resp => { queryNoPagingPost({
if(resp.status === 200){ paging: false,
productList.value = resp.result as Record<string, any>[] terms: [
} {
}) column: 'state',
value: '1',
type: 'and'
}, },
{immediate: true, deep: true} {
) column: 'accessProvider',
value: props?.type
}
],
sorts: [{ name: 'createTime', order: 'desc' }]
}).then((resp) => {
if (resp.status === 200) {
productList.value = resp.result as Record<string, any>[];
}
});
},
{ immediate: true, deep: true },
);
const handleCancel = () => { const handleCancel = () => {
emit('close') emit('close');
} };
const handleSave = () => { const handleSave = () => {
emit('save') emit('save');
} };
</script> </script>

View File

@ -289,7 +289,6 @@ import { queryTree } from '@/api/device/category';
import { useMenuStore } from '@/store/menu'; import { useMenuStore } from '@/store/menu';
import type { ActionsType } from './typings'; import type { ActionsType } from './typings';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { throttle } from 'lodash-es';
const instanceRef = ref<Record<string, any>>({}); const instanceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({}); const params = ref<Record<string, any>>({});

View File

@ -0,0 +1,49 @@
<template>
<page-container>
<div class="box">
<iframe :src="url" class="box-iframe"></iframe>
</div>
</page-container>
</template>
<script setup lang="ts">
import { _control, _stopControl } from '@/api/edge/device';
const url = ref<string>('');
const deviceId = ref<string>('');
watch(
() => history.state?.params?.id,
(newId) => {
if (newId) {
deviceId.value = newId as string;
_control(newId).then((resp: any) => {
if (resp.status === 200) {
const item = `http://${resp.result?.url}/#/login?token=${resp.result.token}`;
url.value = item;
}
});
}
},
{ immediate: true },
);
onUnmounted(() => {
if (deviceId.value) {
_stopControl(unref(deviceId));
}
});
</script>
<style lang="less" scoped>
.box {
width: 100%;
height: 85vh;
background-color: #fff;
}
.box-iframe {
width: 100%;
height: 100%;
border: none;
}
</style>

View File

@ -0,0 +1,265 @@
<template>
<j-modal
:maskClosable="false"
width="650px"
:visible="true"
:title="!!data?.id ? '编辑' : '新增'"
@ok="handleSave"
@cancel="handleCancel"
:confirmLoading="loading"
>
<div style="margin-top: 10px">
<j-form :layout="'vertical'" ref="formRef" :model="modelRef">
<j-row type="flex">
<j-col flex="180px">
<j-form-item name="photoUrl">
<JProUpload v-model="modelRef.photoUrl" />
</j-form-item>
</j-col>
<j-col flex="auto">
<j-form-item
name="id"
:rules="[
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: '请输入英文或者数字或者-或者_',
},
{
max: 64,
message: '最多输入64个字符',
},
{
validator: vailId,
trigger: 'blur',
},
]"
>
<template #label>
<span>
ID
<j-tooltip
title="若不填写系统将自动生成唯一ID"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</j-tooltip>
</span>
</template>
<j-input
v-model:value="modelRef.id"
placeholder="请输入ID"
:disabled="!!data?.id"
/>
</j-form-item>
<j-form-item
label="名称"
name="name"
:rules="[
{
required: true,
message: '请输入名称',
},
{
max: 64,
message: '最多输入64个字符',
},
]"
>
<j-input
v-model:value="modelRef.name"
placeholder="请输入名称"
/>
</j-form-item>
</j-col>
</j-row>
<j-row>
<j-col :span="22">
<j-form-item
name="productId"
:rules="[
{
required: true,
message: '请选择所属产品',
},
]"
>
<template #label>
<span
>所属产品
<j-tooltip title="只能选择“正常”状态的产品">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</j-tooltip>
</span>
</template>
<j-select
showSearch
v-model:value="modelRef.productId"
:disabled="!!data?.id"
placeholder="请选择所属产品"
>
<j-select-option
:value="item.id"
v-for="item in productList"
:key="item.id"
:label="item.name"
>{{ item.name }}</j-select-option
>
</j-select>
</j-form-item>
</j-col>
<j-col :span="2" style="margin-top: 30px">
<PermissionButton
type="link"
:disabled="data.id"
@click="visible = true"
hasPermission="device/Product:add"
>
<AIcon type="PlusOutlined" />
</PermissionButton>
</j-col>
</j-row>
<j-form-item
label="说明"
name="describe"
:rules="[
{
max: 200,
message: '最多输入200个字符',
},
]"
>
<j-textarea
v-model:value="modelRef.describe"
placeholder="请输入说明"
showCount
:maxlength="200"
/>
</j-form-item>
</j-form>
</div>
</j-modal>
<SaveProduct
v-model:visible="visible"
v-model:productId="modelRef.productId"
:channel="'official-edge-gateway'"
@close="onClose"
:deviceType="'gateway'"
@save="onSave"
/>
</template>
<script lang="ts" setup>
import { queryNoPagingPost } from '@/api/device/product';
import { isExists, update } from '@/api/device/instance';
import { getImage } from '@/utils/comm';
import { message } from 'jetlinks-ui-components';
import SaveProduct from '@/views/media/Device/Save/SaveProduct.vue';
const emit = defineEmits(['close', 'save']);
const props = defineProps({
data: {
type: Object,
default: undefined,
},
});
const productList = ref<Record<string, any>[]>([]);
const loading = ref<boolean>(false);
const visible = ref<boolean>(false);
const formRef = ref();
const modelRef = reactive({
productId: undefined,
id: undefined,
name: '',
describe: '',
photoUrl: getImage('/device/instance/device-card.png'),
});
const vailId = async (_: Record<string, any>, value: string) => {
if (!props?.data?.id && value) {
const resp = await isExists(value);
if (resp.status === 200 && resp.result) {
return Promise.reject('ID重复');
} else {
return Promise.resolve();
}
} else {
return Promise.resolve();
}
};
watch(
() => props.data,
(newValue) => {
queryNoPagingPost({
paging: false,
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [
{
terms: [
{
termType: 'eq',
column: 'state',
value: 1,
type: 'and',
},
{
termType: 'eq',
column: 'accessProvider',
value: 'official-edge-gateway',
},
],
},
],
}).then((resp) => {
if (resp.status === 200) {
productList.value = resp.result as Record<string, any>[];
}
});
Object.assign(modelRef, newValue);
},
{ immediate: true, deep: true },
);
const handleCancel = () => {
emit('close');
formRef.value.resetFields();
};
const onClose = () => {
visible.value = false;
};
const onSave = (_data: any) => {
productList.value.push(_data)
}
const handleSave = () => {
formRef.value
.validate()
.then(async (_data: any) => {
loading.value = true;
const obj = { ..._data };
if (!obj.id) {
delete obj.id;
}
const resp = await update(obj).finally(() => {
loading.value = false;
});
if (resp.status === 200) {
message.success('操作成功!');
emit('save');
formRef.value.resetFields();
}
})
.catch((err: any) => {
console.log('error', err);
});
};
</script>

View File

@ -0,0 +1,438 @@
<template>
<page-container>
<pro-search
:columns="columns"
target="edge-device"
@search="handleSearch"
/>
<JProTable
ref="edgeDeviceRef"
:columns="columns"
:request="query"
:defaultParams="defaultParams"
:params="params"
:gridColumn="3"
>
<template #headerTitle>
<j-space>
<PermissionButton
type="primary"
@click="handleAdd"
hasPermission="edge/Device:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
<PermissionButton
@click="importVisible = true"
hasPermission="edge/Device:import"
>
<template #icon
><AIcon type="ImportOutlined"
/></template>
导入
</PermissionButton>
</j-space>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
:actions="getActions(slotProps, 'card')"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
online: 'success',
offline: 'error',
notActive: 'warning',
}"
>
<template #img>
<img
:src="getImage('/device/instance/device-card.png')"
/>
</template>
<template #content>
<Ellipsis style="width: calc(100% - 100px)">
<span
style="font-size: 16px; font-weight: 600"
@click.stop="handleView(slotProps.id)"
>
{{ slotProps.name }}
</span>
</Ellipsis>
<j-row style="margin-top: 20px">
<j-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>{{ slotProps.deviceType?.text }}</div>
</j-col>
<j-col :span="12">
<div class="card-item-content-text">
产品名称
</div>
<Ellipsis style="width: 100%">
{{ slotProps.productName }}
</Ellipsis>
</j-col>
</j-row>
</template>
<template #actions="item">
<PermissionButton
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
:hasPermission="'edge/Device:' + item.key"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</PermissionButton>
</template>
</CardBox>
</template>
<template #state="slotProps">
<j-badge
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
/>
</template>
<template #createTime="slotProps">
<span>{{
dayjs(slotProps.createTime).format('YYYY-MM-DD HH:mm:ss')
}}</span>
</template>
<template #action="slotProps">
<j-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: 0 5px"
:hasPermission="'edge/Device:' + i.key"
>
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</template>
</j-space>
</template>
</JProTable>
<Save
v-if="visible"
:data="current"
@close="visible = false"
@save="saveBtn"
/>
<Import @save="onRefresh" @close="importVisible = false" v-if="importVisible" type="official-edge-gateway" />
</page-container>
</template>
<script lang="ts" setup>
import { queryNoPagingPost } from '@/api/device/product';
import { queryTree } from '@/api/device/category';
import { message } from 'jetlinks-ui-components';
import { ActionsType } from '@/views/device/Instance/typings';
import { useMenuStore } from '@/store/menu';
import { getImage } from '@/utils/comm';
import dayjs from 'dayjs';
import { query, _delete, _deploy, _undeploy } from '@/api/device/instance';
import { restPassword } from '@/api/edge/device';
import Save from './Save/index.vue';
import Import from '@/views/device/Instance/Import/index.vue';
const menuStory = useMenuStore();
const defaultParams = {
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [
{
terms: [
{
column: 'productId$product-info',
value: 'accessProvider is official-edge-gateway',
},
],
type: 'and',
},
],
};
const statusMap = new Map();
statusMap.set('online', 'success');
statusMap.set('offline', 'error');
statusMap.set('notActive', 'warning');
const params = ref<Record<string, any>>({});
const edgeDeviceRef = ref<Record<string, any>>({});
const importVisible = ref<boolean>(false);
const visible = ref<boolean>(false);
const current = ref<Record<string, any>>({});
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
search: {
type: 'string',
defaultTermType: 'eq',
},
},
{
title: '设备名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'string',
first: true,
},
},
{
title: '产品名称',
dataIndex: 'productName',
key: 'productName',
search: {
type: 'select',
options: () =>
new Promise((resolve) => {
queryNoPagingPost({ paging: false }).then((resp: any) => {
resolve(
resp.result.map((item: any) => ({
label: item.name,
value: item.id,
})),
);
});
}),
},
},
{
title: '注册时间',
dataIndex: 'registryTime',
key: 'registryTime',
scopedSlots: true,
search: {
type: 'date',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '禁用', value: 'notActive' },
{ label: '离线', value: 'offline' },
{ label: '在线', value: 'online' },
],
},
},
{
key: 'classifiedId',
dataIndex: 'classifiedId',
title: '产品分类',
hideInTable: true,
search: {
type: 'treeSelect',
options: () =>
new Promise((resolve) => {
queryTree({ paging: false }).then((resp: any) => {
resolve(resp.result);
});
}),
},
},
{
dataIndex: 'deviceType',
title: '设备类型',
valueType: 'select',
hideInTable: true,
search: {
type: 'select',
options: [
{ label: '直连设备', value: 'device' },
{ label: '网关子设备', value: 'childrenDevice' },
{ label: '网关设备', value: 'gateway' },
],
},
},
{
title: '说明',
dataIndex: 'describe',
key: 'describe',
search: {
type: 'string',
},
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true,
},
];
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
const actions = [
{
key: 'view',
text: '查看',
tooltip: {
title: '查看',
},
icon: 'EyeOutlined',
onClick: () => {
handleView(data.id);
},
},
{
key: 'update',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
visible.value = true;
current.value = data;
},
},
{
key: 'setting',
text: '远程控制',
tooltip: {
title: '远程控制',
},
icon: 'ControlOutlined',
onClick: () => {
menuStory.jumpPage('edge/Device/Remote', { id: data.id });
},
},
{
key: 'password',
text: '重置密码',
tooltip: {
title: '重置密码',
},
icon: 'RedoOutlined',
popConfirm: {
title: '确认重置密码为P@ssw0rd',
onConfirm: async () => {
restPassword(data.id).then((resp: any) => {
if (resp.status === 200) {
message.success('操作成功!');
edgeDeviceRef.value?.reload();
}
});
},
},
},
{
key: 'action',
text: data.state?.value !== 'notActive' ? '禁用' : '启用',
tooltip: {
title: data.state?.value !== 'notActive' ? '禁用' : '启用',
},
icon:
data.state.value !== 'notActive'
? 'StopOutlined'
: 'CheckCircleOutlined',
popConfirm: {
title: `确认${
data.state.value !== 'notActive' ? '禁用' : '启用'
}?`,
onConfirm: async () => {
let response = undefined;
if (data.state.value !== 'notActive') {
response = await _undeploy(data.id);
} else {
response = await _deploy(data.id);
}
if (response && response.status === 200) {
message.success('操作成功!');
edgeDeviceRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
{
key: 'delete',
text: '删除',
disabled: data.state?.value !== 'notActive',
tooltip: {
title:
data.state.value !== 'notActive'
? '已启用的设备不能删除'
: '删除',
},
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const resp = await _delete(data.id);
if (resp.status === 200) {
message.success('操作成功!');
edgeDeviceRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
if (type === 'card')
return actions.filter((i: ActionsType) => i.key !== 'view');
return actions;
};
const handleSearch = (_params: any) => {
params.value = _params;
};
const handleView = (id: string) => {
menuStory.jumpPage('device/Instance/Detail', { id });
};
const handleAdd = () => {
visible.value = true;
current.value = {};
};
const saveBtn = () => {
visible.value = false;
edgeDeviceRef.value?.reload();
};
const onRefresh = () => {
importVisible.value = false
edgeDeviceRef.value?.reload();
}
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,132 @@
<template>
<j-modal
visible
title="下发结果"
:width="900"
@ok="emit('close')"
@cancel="emit('close')"
>
<j-row>
<j-col :span="8">
<div>成功{{ count }}</div>
<div>
失败{{ countErr }}
<j-button @click="_download(errMessage || '', '下发失败原因')" v-if="errMessage.length" type="link"
>下载</j-button
>
</div>
</j-col>
<j-col :span="8">下发设备数量{{ list.length || 0 }}</j-col>
<j-col :span="8">已下发数量{{ countErr + count }}</j-col>
</j-row>
<div v-if="!flag">
<j-textarea :rows="20" :value="JSON.stringify(errMessage)" />
</div>
</j-modal>
</template>
<script setup lang="ts">
import { LocalStore } from '@/utils/comm';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import dayjs from 'dayjs';
import { EventSourcePolyfill } from 'event-source-polyfill';
const props = defineProps({
data: {
type: Object,
default: () => {},
},
list: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['close']);
const count = ref<number>(0);
const countErr = ref<number>(0);
const flag = ref<boolean>(true);
const errMessage = ref<any[]>([]);
const getData = () => {
let dt = 0;
let et = 0;
const errMessages: any[] = [];
const _terms = {
deviceId: (props.list || []).map((item: any) => item?.id),
params: JSON.stringify({
name: props.data.name,
targetId: props.data.targetId,
targetType: props.data.targetType,
category: props.data.category,
metadata: props.data?.metadata,
}),
};
const url = new URLSearchParams();
Object.keys(_terms).forEach((key) => {
if (Array.isArray(_terms[key]) && _terms[key].length) {
_terms[key].map((item: string) => {
url.append(key, item);
});
} else {
url.append(key, _terms[key]);
}
});
const source = new EventSourcePolyfill(
`${BASE_API_PATH}/edge/operations/entity-template-save/invoke/_batch?:X_Access_Token=${LocalStore.get(
TOKEN_KEY,
)}&${url}`,
);
source.onmessage = (e: any) => {
const res = JSON.parse(e.data);
if (res.successful) {
dt += 1;
count.value = dt;
} else {
et += 1;
countErr.value = et;
flag.value = false;
if (errMessages.length <= 5) {
errMessages.push({ ...res });
errMessage.value = [...errMessages];
}
}
};
source.onerror = () => {
source.close();
};
source.onopen = () => {};
};
const _download = (record: Record<string, any>, fileName: string, format?: string) => {
//
const ghostLink = document.createElement('a');
ghostLink.download = `${fileName ? '' : record?.name}${fileName}_${dayjs(new Date()).format(
format || 'YYYY_MM_DD',
)}.txt`;
ghostLink.style.display = 'none';
//Blob
const blob = new Blob([JSON.stringify(record)]);
ghostLink.href = URL.createObjectURL(blob);
//
document.body.appendChild(ghostLink);
ghostLink.click();
//
document.body.removeChild(ghostLink);
}
watch(
() => props.data.id,
(newId) => {
if(newId){
getData()
}
},
{
immediate: true,
},
);
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,181 @@
<template>
<j-modal
visible
title="下发设备"
:width="1000"
@ok="onSave"
@cancel="onCancel"
>
<div class="alert">
<AIcon
type="InfoCircleOutlined"
style="margin-right: 10px"
/>线
</div>
<pro-search
:columns="columns"
target="edge-resource-issue"
@search="handleSearch"
type="simple"
class="search"
/>
<JProTable
ref="edgeResourceIssueRef"
:columns="columns"
:request="queryDeviceList"
:defaultParams="defaultParams"
:params="params"
model="TABLE"
:bodyStyle="{ padding: 0 }"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange,
}"
>
<template #state="slotProps">
<j-badge
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
/>
</template>
<template #sourceId="slotProps">
{{ slotProps.sourceName }}
</template>
<template #registerTime="slotProps">
<span>{{
dayjs(slotProps.registerTime).format('YYYY-MM-DD HH:mm:ss')
}}</span>
</template>
</JProTable>
<Result v-if="visible" :data="props.data" :list="_data" @close="onCancel" />
</j-modal>
</template>
<script setup lang="ts">
import { onlyMessage } from '@/utils/comm';
import { queryDeviceList } from '@/api/edge/resource';
import dayjs from 'dayjs';
import Result from './Result.vue';
const defaultParams = {
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [
{
terms: [
{
termType: 'eq',
column: 'productId$product-info',
value: 'accessProvider is official-edge-gateway',
},
],
type: 'and',
},
],
};
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['close']);
const params = ref({});
const edgeResourceIssueRef = ref();
const _selectedRowKeys = ref<string[]>([]);
const _data = ref<any[]>([]);
const visible = ref<boolean>(false);
const statusMap = new Map();
statusMap.set('online', 'success');
statusMap.set('offline', 'error');
statusMap.set('notActive', 'warning');
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
width: 200,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '产品名称',
dataIndex: 'productName',
key: 'productName',
ellipsis: true,
search: {
type: 'select',
},
},
{
title: '设备名称',
ellipsis: true,
dataIndex: 'name',
key: 'name',
},
{
title: '注册时间',
dataIndex: 'registerTime',
key: 'registerTime',
width: 200,
scopedSlots: true,
search: {
type: 'date',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '禁用', value: 'notActive' },
{ label: '离线', value: 'offline' },
{ label: '在线', value: 'online' },
],
},
},
];
const onSelectChange = (keys: string[], _options: any[]) => {
_selectedRowKeys.value = [...keys];
_data.value = _options;
};
const handleSearch = (v: any) => {
params.value = v;
};
const onSave = () => {
if(_data.value.length){
visible.value = true
} else {
onlyMessage('请选择设备', 'error')
}
};
const onCancel = () => {
emit('close');
};
</script>
<style lang="less" scoped>
.search {
padding: 0 0 0 24px;
}
.alert {
height: 40px;
padding-left: 10px;
color: rgba(0, 0, 0, 0.55);
line-height: 40px;
background-color: #f6f6f6;
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<j-modal visible title="编辑" :width="700" @ok="onSave" @cancel="onCancel">
<MonacoEditor
style="width: 100%; height: 370px"
theme="vs"
v-model="monacoValue"
language="json"
/>
</j-modal>
</template>
<script setup lang="ts">
import MonacoEditor from '@/components/MonacoEditor/index.vue';
import { modify } from '@/api/edge/resource';
import { onlyMessage } from '@/utils/comm';
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['close', 'save']);
const monacoValue = ref<string>('{}');
watchEffect(() => {
monacoValue.value = props.data?.metadata || '{}';
});
const onSave = async () => {
const resp = await modify(props.data.id, { metadata: unref(monacoValue) });
if (resp.status === 200) {
emit('save');
onlyMessage('操作成功', 'success');
}
};
const onCancel = () => {
emit('close');
};
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,383 @@
<template>
<page-container>
<pro-search
:columns="columns"
target="edge-resource"
@search="handleSearch"
/>
<JProTable
ref="edgeResourceRef"
:columns="columns"
:request="query"
:defaultParams="defaultParams"
:params="params"
>
<template #card="slotProps">
<CardBox
:value="slotProps"
:actions="getActions(slotProps, 'card')"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
enabled: 'success',
disabled: 'error',
}"
>
<template #img>
<img
:src="getImage('/device/instance/device-card.png')"
/>
</template>
<template #content>
<Ellipsis style="width: calc(100% - 100px)">
<span
style="font-size: 16px; font-weight: 600"
@click.stop="handleView(slotProps.id)"
>
{{ slotProps.name }}
</span>
</Ellipsis>
<j-row style="margin-top: 20px">
<j-col :span="12">
<div class="card-item-content-text">
通讯协议
</div>
<Ellipsis>{{
options.find(
(i) => i.value === slotProps.category,
)?.label || slotProps.category
}}</Ellipsis>
</j-col>
<j-col :span="12">
<div class="card-item-content-text">
所属边缘网关
</div>
<Ellipsis style="width: 100%">
{{ slotProps.sourceName }}
</Ellipsis>
</j-col>
</j-row>
</template>
<template #actions="item">
<PermissionButton
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
:hasPermission="'edge/Resource:' + item.key"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</PermissionButton>
</template>
</CardBox>
</template>
<template #state="slotProps">
<j-badge
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
/>
</template>
<template #sourceId="slotProps">
{{ slotProps.sourceName }}
</template>
<template #category="slotProps">
{{
options.find((i) => i.value === slotProps.category)
?.label || slotProps.category
}}
</template>
<template #createTime="slotProps">
<span>{{
dayjs(slotProps.createTime).format('YYYY-MM-DD HH:mm:ss')
}}</span>
</template>
<template #action="slotProps">
<j-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: 0 5px"
:hasPermission="'edge/Resource:' + i.key"
>
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</template>
</j-space>
</template>
</JProTable>
<Save
v-if="visible"
:data="current"
@close="visible = false"
@save="saveBtn"
/>
<Issue
v-if="settingVisible"
:data="current"
@close="settingVisible = false"
/>
</page-container>
</template>
<script lang="ts" setup>
import { queryNoPagingPost } from '@/api/device/instance';
import { message } from 'jetlinks-ui-components';
import { ActionsType } from '@/views/device/Instance/typings';
import { useMenuStore } from '@/store/menu';
import { getImage } from '@/utils/comm';
import dayjs from 'dayjs';
import { query, _delete, _start, _stop } from '@/api/edge/resource';
import Save from './Save/index.vue';
import Issue from './Issue/index.vue';
const menuStory = useMenuStore();
const defaultParams = { sorts: [{ name: 'createTime', order: 'desc' }] };
const statusMap = new Map();
statusMap.set('enabled', 'success');
statusMap.set('disabled', 'error');
const options = [
{ label: 'UA接入', value: 'OPC_UA' },
{ label: 'Modbus TCP接入', value: 'MODBUS_TCP' },
{ label: 'S7-200接入', value: 'snap7' },
{ label: 'BACnet接入', value: 'BACNetIp' },
{ label: 'MODBUS_RTU接入', value: 'MODBUS_RTU' },
];
const params = ref<Record<string, any>>({});
const edgeResourceRef = ref<Record<string, any>>({});
const settingVisible = ref<boolean>(false);
const visible = ref<boolean>(false);
const current = ref<Record<string, any>>({});
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
},
{
dataIndex: 'category',
title: '通信协议',
valueType: 'select',
scopedSlots: true,
key: 'category',
search: {
type: 'select',
options: options,
},
},
{
title: '所属边缘网关',
dataIndex: 'sourceId',
key: 'sourceId',
scopedSlots: true,
search: {
type: 'select',
options: () =>
new Promise((resolve) => {
queryNoPagingPost({
paging: false,
terms: [
{
terms: [
{
column: 'productId$product-info',
value: 'accessProvider is official-edge-gateway',
},
],
type: 'and',
},
],
sorts: [
{
name: 'createTime',
order: 'desc',
},
],
}).then((resp: any) => {
resolve(
resp.result.map((item: any) => ({
label: item.name,
value: item.id,
})),
);
});
}),
},
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
scopedSlots: true,
search: {
type: 'date',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '禁用', value: 'disabled' },
{ label: '正常', value: 'enabled' },
],
},
},
{
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 = [
{
key: 'update',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
visible.value = true;
current.value = data;
},
},
{
key: 'setting',
text: '下发',
disabled: data.state?.value === 'disabled',
tooltip: {
title:
data.state.value === 'disabled'
? '请先启用,再下发'
: '下发',
},
icon: 'DownSquareOutlined',
onClick: () => {
settingVisible.value = true;
current.value = data;
},
},
{
key: 'action',
text: data.state?.value !== 'disabled' ? '禁用' : '启用',
tooltip: {
title: data.state?.value !== 'disabled' ? '禁用' : '启用',
},
icon:
data.state.value !== 'disabled'
? 'StopOutlined'
: 'CheckCircleOutlined',
popConfirm: {
title: `确认${
data.state.value !== 'disabled' ? '禁用' : '启用'
}?`,
onConfirm: async () => {
let response = undefined;
if (data.state.value !== 'disabled') {
response = await _stop([data.id]);
} else {
response = await _start([data.id]);
}
if (response && response.status === 200) {
message.success('操作成功!');
edgeResourceRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
{
key: 'delete',
text: '删除',
disabled: data.state?.value !== 'disabled',
tooltip: {
title:
data.state.value !== 'disabled'
? '请先禁用,再删除。'
: '删除',
},
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const resp = await _delete(data.id);
if (resp.status === 200) {
message.success('操作成功!');
edgeResourceRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
if (type === 'card')
return actions.filter((i: ActionsType) => i.key !== 'view');
return actions;
};
const handleSearch = (_params: any) => {
params.value = _params;
};
const handleView = (id: string) => {
menuStory.jumpPage('device/Instance/Detail', { id });
};
const saveBtn = () => {
visible.value = false;
edgeResourceRef.value?.reload();
};
const onRefresh = () => {
settingVisible.value = false;
edgeResourceRef.value?.reload();
};
</script>
<style lang="less" scoped>
</style>

View File

@ -9,32 +9,52 @@
:confirmLoading="btnLoading" :confirmLoading="btnLoading"
width="660px" width="660px"
> >
<j-form layout="vertical"> <j-form ref="formRef" :model="formData" layout="vertical">
<j-form-item label="产品名称" v-bind="validateInfos.name"> <j-form-item
label="产品名称"
name="name"
:rules="{ required: true, message: '请输入产品名称' }"
>
<j-input <j-input
v-model:value="formData.name" v-model:value="formData.name"
placeholder="请输入名称" placeholder="请输入名称"
/> />
</j-form-item> </j-form-item>
<template v-if="channel === 'gb28181-2016' && formData.accessId"> <template v-if="deviceType !== 'gateway'">
<template v-for="(item, index) in extendFormItem" :key="index">
<j-form-item <j-form-item
label="接入密码" :name="item.name"
v-bind="validateInfos['configuration.access_pwd']" :label="item.label"
:rules="{
required: item.required,
message: item.message,
trigger: 'change',
}"
> >
<j-input-password
v-model:value="formData.configuration.access_pwd"
placeholder="请输入接入密码"
/>
</j-form-item>
<j-form-item label="流传输模式">
<j-select <j-select
v-model:value="formData.configuration.stream_mode" v-if="item.type === 'enum'"
placeholder="请选择流传输模式" v-model:value="formData[item.name[0]][item.name[1]]"
:options="streamMode" :options="item.options"
:placeholder="item.message"
/>
<j-input-password
v-else-if="item.type === 'password'"
v-model:value="formData[item.name[0]][item.name[1]]"
:placeholder="item.message"
/>
<j-input
v-else
v-model:value="formData[item.name[0]][item.name[1]]"
:placeholder="item.message"
/> />
</j-form-item> </j-form-item>
</template> </template>
<j-form-item label="接入网关" v-bind="validateInfos.accessId"> </template>
<j-form-item
label="接入网关"
name="accessId"
:rules="{ required: true, message: '请选择接入网关' }"
>
<div class="gateway-box"> <div class="gateway-box">
<div v-if="!gatewayList.length"> <div v-if="!gatewayList.length">
暂无数据请先 暂无数据请先
@ -119,20 +139,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Form, message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { PropType } from 'vue';
import { streamMode } from '@/views/media/Device/const';
import DeviceApi from '@/api/media/device'; import DeviceApi from '@/api/media/device';
import { getImage } from '@/utils/comm'; import { getImage } from '@/utils/comm';
import { gatewayType } from '@/views/media/Device/typings'; import { gatewayType } from '@/views/media/Device/typings';
import { providerType } from '../const'; import { providerType } from '../const';
const useForm = Form.useForm;
type Emits = { type Emits = {
(e: 'update:visible', data: boolean): void; (e: 'update:visible', data: boolean): void;
(e: 'update:productId', data: string): void; (e: 'update:productId', data: string): void;
(e: 'close'): void; (e: 'close'): void;
(e: 'save', data: Record<string, any>): void;
}; };
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
@ -140,6 +157,7 @@ const props = defineProps({
visible: { type: Boolean, default: false }, visible: { type: Boolean, default: false },
productId: { type: String, default: '' }, productId: { type: String, default: '' },
channel: { type: String, default: '' }, channel: { type: String, default: '' },
deviceType: { type: String, default: 'device' },
}); });
const _vis = computed({ const _vis = computed({
@ -166,20 +184,36 @@ const getGatewayList = async () => {
* @param e * @param e
*/ */
const _selectedRowKeys = ref<string[]>([]); const _selectedRowKeys = ref<string[]>([]);
const extendFormItem = ref<any[]>();
const handleClick = async (e: any) => { const handleClick = async (e: any) => {
_selectedRowKeys.value = [e.id]; _selectedRowKeys.value = [e.id];
formData.value.accessId = e.id; formData.value.accessId = e.id;
formData.value.accessName = e.name; formData.value.accessName = e.name;
formData.value.accessProvider = e.provider; formData.value.accessProvider = e.provider;
formData.value.messageProtocol = e.provider; formData.value.messageProtocol = e.protocolDetail.id;
formData.value.protocolName = e.protocolDetail.name; formData.value.protocolName = e.protocolDetail.name;
formData.value.transportProtocol = e.transport; formData.value.transportProtocol = e.transport;
const { result } = await DeviceApi.getConfiguration( const { result } = await DeviceApi.getConfiguration(
props.channel, e.protocol,
e.transport, e.transport,
); );
console.log('result: ', result);
extendFormItem.value = result.properties.map((item: any) => ({
name: ['configuration', item.property],
label: item.name,
type: item.type?.type,
value: item.type.expands?.defaultValue,
options: item.type.elements?.map((e: any) => ({
label: e.text,
value: e.value,
})),
required: !!item.type.expands?.required,
message:
item.type?.type === 'enum'
? `请选择${item.name}`
: `请输入${item.name}`,
}));
}; };
watch( watch(
@ -187,10 +221,6 @@ watch(
(val) => { (val) => {
if (val) { if (val) {
getGatewayList(); getGatewayList();
formRules.value['configuration.access_pwd'][0].required =
props.channel === 'gb28181-2016';
validate();
} else { } else {
emit('close'); emit('close');
} }
@ -198,6 +228,7 @@ watch(
); );
// //
const formRef = ref();
const formData = ref({ const formData = ref({
accessId: '', accessId: '',
accessName: '', accessName: '',
@ -206,32 +237,20 @@ const formData = ref({
access_pwd: '', access_pwd: '',
stream_mode: 'UDP', stream_mode: 'UDP',
}, },
deviceType: 'device', deviceType: props.deviceType,
messageProtocol: '', messageProtocol: '',
name: '', name: '',
protocolName: '', protocolName: '',
transportProtocol: '', transportProtocol: '',
}); });
//
const formRules = ref({
name: [{ required: true, message: '请输入产品名称' }],
'configuration.access_pwd': [{ required: true, message: '请输入接入密码' }],
accessId: [{ required: true, message: '请选择接入网关' }],
});
const { resetFields, validate, validateInfos, clearValidate } = useForm(
formData.value,
formRules.value,
);
/** /**
* 提交 * 提交
*/ */
const btnLoading = ref(false); const btnLoading = ref(false);
const handleOk = () => { const handleOk = () => {
// console.log('formData.value: ', formData.value); formRef.value
validate() ?.validate()
.then(async () => { .then(async () => {
btnLoading.value = true; btnLoading.value = true;
const res = await DeviceApi.saveProduct(formData.value); const res = await DeviceApi.saveProduct(formData.value);
@ -241,20 +260,21 @@ const handleOk = () => {
res.result.id, res.result.id,
); );
if (deployResp.success) { if (deployResp.success) {
emit('save', {...res.result})
message.success('操作成功'); message.success('操作成功');
handleCancel(); handleCancel();
} }
} }
btnLoading.value = false; btnLoading.value = false;
}) })
.catch((err) => { .catch((err: any) => {
console.log('err: ', err); console.log('err: ', err);
}); });
}; };
const handleCancel = () => { const handleCancel = () => {
_vis.value = false; _vis.value = false;
resetFields(); formRef.value.resetFields();
}; };
</script> </script>

View File

@ -392,53 +392,53 @@ const formRules = ref({
provider: [{ required: true, message: '请选择类型' }], provider: [{ required: true, message: '请选择类型' }],
// //
'configuration.appKey': [ 'configuration.appKey': [
{ required: true, message: '请输入AppKey' }, { required: true, message: '请输入AppKey', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符', trigger: 'change' },
], ],
'configuration.appSecret': [ 'configuration.appSecret': [
{ required: true, message: '请输入AppSecret' }, { required: true, message: '请输入AppSecret', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符', trigger: 'change' },
], ],
// 'configuration.url': [{ required: true, message: 'WebHook' }], // 'configuration.url': [{ required: true, message: 'WebHook' }],
// //
'configuration.corpId': [ 'configuration.corpId': [
{ required: true, message: '请输入corpId' }, { required: true, message: '请输入corpId', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
], ],
'configuration.corpSecret': [ 'configuration.corpSecret': [
{ required: true, message: '请输入corpSecret' }, { required: true, message: '请输入corpSecret', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
], ],
// / // /
'configuration.regionId': [ 'configuration.regionId': [
{ required: true, message: '请输入RegionId' }, { required: true, message: '请输入RegionId', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
], ],
'configuration.accessKeyId': [ 'configuration.accessKeyId': [
{ required: true, message: '请输入AccessKeyId' }, { required: true, message: '请输入AccessKeyId', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
], ],
'configuration.secret': [ 'configuration.secret': [
{ required: true, message: '请输入Secret' }, { required: true, message: '请输入Secret', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
], ],
// //
'configuration.host': [{ required: true, message: '请输入服务器地址' }], 'configuration.host': [{ required: true, message: '请输入服务器地址', trigger: 'blur' }],
'configuration.sender': [ 'configuration.sender': [
{ required: true, message: '请输入发件人' }, { required: true, message: '请输入发件人', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
], ],
'configuration.username': [ 'configuration.username': [
{ required: true, message: '请输入用户名' }, { required: true, message: '请输入用户名', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
], ],
'configuration.password': [ 'configuration.password': [
{ required: true, message: '请输入密码' }, { required: true, message: '请输入密码', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
], ],
// webhook // webhook
'configuration.url': [ 'configuration.url': [
{ required: true, message: '请输入Webhook' }, { required: true, message: '请输入Webhook', trigger: 'blur' },
// { // {
// pattern: // pattern:
// /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[j-z]{2,6}\/?/, // /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[j-z]{2,6}\/?/,
@ -458,8 +458,6 @@ const getDetail = async () => {
const res = await configApi.detail(route.params.id as string); const res = await configApi.detail(route.params.id as string);
// formData.value = res.result; // formData.value = res.result;
Object.assign(formData.value, res.result); Object.assign(formData.value, res.result);
// console.log('res.result: ', res.result);
// console.log('formData.value: ', formData.value);
}; };
getDetail(); getDetail();
@ -537,7 +535,6 @@ const btnLoading = ref<boolean>(false);
const handleSubmit = () => { const handleSubmit = () => {
validate() validate()
.then(async () => { .then(async () => {
// console.log('formData.value: ', formData.value);
btnLoading.value = true; btnLoading.value = true;
let res; let res;
if (!formData.value.id) { if (!formData.value.id) {
@ -545,7 +542,6 @@ const handleSubmit = () => {
} else { } else {
res = await configApi.update(formData.value); res = await configApi.update(formData.value);
} }
// console.log('res: ', res);
if (res?.success) { if (res?.success) {
message.success('保存成功'); message.success('保存成功');
router.back(); router.back();

View File

@ -147,6 +147,19 @@
</template> </template>
</CardBox> </CardBox>
</template> </template>
<template #type="slotProps">
<span> {{ getMethodTxt(slotProps.type) }}</span>
</template>
<template #provider="slotProps">
<span>
{{ getProviderTxt(slotProps.type, slotProps.provider) }}
</span>
</template>
<!-- <template #description="slotProps">
<Ellipsis>
{{ slotProps.description }}
</Ellipsis>
</template> -->
<template #action="slotProps"> <template #action="slotProps">
<j-space :size="16"> <j-space :size="16">
<template <template
@ -205,6 +218,7 @@ const columns = [
title: '配置名称', title: '配置名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 100,
search: { search: {
type: 'string', type: 'string',
}, },
@ -214,6 +228,7 @@ const columns = [
dataIndex: 'type', dataIndex: 'type',
key: 'type', key: 'type',
scopedSlots: true, scopedSlots: true,
width: 100,
search: { search: {
type: 'select', type: 'select',
options: NOTICE_METHOD, options: NOTICE_METHOD,
@ -227,6 +242,7 @@ const columns = [
dataIndex: 'provider', dataIndex: 'provider',
key: 'provider', key: 'provider',
scopedSlots: true, scopedSlots: true,
width: 200,
search: { search: {
type: 'select', type: 'select',
options: providerList, options: providerList,
@ -239,6 +255,8 @@ const columns = [
title: '说明', title: '说明',
dataIndex: 'description', dataIndex: 'description',
key: 'description', key: 'description',
scopedSlots: true,
ellipsis: true,
search: { search: {
type: 'string', type: 'string',
}, },
@ -272,6 +290,14 @@ const getLogo = (type: string, provider: string) => {
const getMethodTxt = (type: string) => { const getMethodTxt = (type: string) => {
return NOTICE_METHOD.find((f) => f.value === type)?.label; return NOTICE_METHOD.find((f) => f.value === type)?.label;
}; };
/**
* 根据类型展示对应文案
* @param type
* @param provider
*/
const getProviderTxt = (type: string, provider: string) => {
return MSG_TYPE[type].find((f: any) => f.value === provider)?.label;
};
/** /**
* 新增 * 新增

View File

@ -50,10 +50,19 @@
<template v-else> <template v-else>
<j-form-item <j-form-item
:name="['templateDetailTable', index, 'value']" :name="['templateDetailTable', index, 'value']"
:rules="{ :rules="[
{
required: record.required, required: record.required,
message: '该字段为必填字段', message: '该字段为必填字段',
}" },
...record.otherRules,
]"
>
<template
v-if="
data.type === 'dingTalk' ||
data.type === 'weixin'
"
> >
<ToUser <ToUser
v-if="record.type === 'user'" v-if="record.type === 'user'"
@ -78,6 +87,13 @@
v-model:modelValue="record.value" v-model:modelValue="record.value"
:itemType="record.type" :itemType="record.type"
/> />
</template>
<template v-else>
<ValueItem
v-model:modelValue="record.value"
:itemType="record.type"
/>
</template>
</j-form-item> </j-form-item>
</template> </template>
</template> </template>
@ -100,6 +116,8 @@ import { message } from 'ant-design-vue';
import ToUser from '../Detail/components/ToUser.vue'; import ToUser from '../Detail/components/ToUser.vue';
import ToOrg from '../Detail/components/ToOrg.vue'; import ToOrg from '../Detail/components/ToOrg.vue';
import ToTag from '../Detail/components/ToTag.vue'; import ToTag from '../Detail/components/ToTag.vue';
import type { Rule } from 'ant-design-vue/es/form';
import { phoneRegEx } from '@/utils/validate';
type Emits = { type Emits = {
(e: 'update:visible', data: boolean): void; (e: 'update:visible', data: boolean): void;
@ -154,8 +172,28 @@ const getTemplateDetail = async () => {
formData.value.templateDetailTable = result.variableDefinitions.map( formData.value.templateDetailTable = result.variableDefinitions.map(
(m: any) => ({ (m: any) => ({
...m, ...m,
type: m.expands ? m.expands.businessType : m.type, type: m.expands?.businessType ? m.expands.businessType : m.type,
value: undefined, value: undefined,
//
otherRules:
m.id === 'calledNumber' || m.id === 'phoneNumber'
? [
{
max: 64,
message: '最多可输入64个字符',
trigger: 'change',
},
{
trigger: 'change',
validator(_rule: Rule, value: string) {
if (!value) return Promise.resolve();
if (!phoneRegEx(value))
return Promise.reject('请输入有效号码');
return Promise.resolve();
},
},
]
: '',
}), }),
); );
}; };

View File

@ -85,7 +85,7 @@
> >
<template #label> <template #label>
<span> <span>
AgentID AgentId
<j-tooltip title="应用唯一标识"> <j-tooltip title="应用唯一标识">
<AIcon <AIcon
type="QuestionCircleOutlined" type="QuestionCircleOutlined"
@ -98,7 +98,7 @@
v-model:value=" v-model:value="
formData.template.agentId formData.template.agentId
" "
placeholder="请输入AppSecret" placeholder="请输入AgentId"
/> />
</j-form-item> </j-form-item>
<j-row :gutter="10"> <j-row :gutter="10">
@ -271,7 +271,7 @@
</template> </template>
<j-input <j-input
v-model:value="formData.template.agentId" v-model:value="formData.template.agentId"
placeholder="请输入agentId" placeholder="请输入AgentId"
/> />
</j-form-item> </j-form-item>
<j-row :gutter="10"> <j-row :gutter="10">
@ -664,7 +664,6 @@
<j-radio :value="false">自定义</j-radio> <j-radio :value="false">自定义</j-radio>
</j-radio-group> </j-radio-group>
<j-textarea <j-textarea
v-model:value="formData.template.body"
placeholder="请求体中的数据来自于发送通知时指定的所有变量" placeholder="请求体中的数据来自于发送通知时指定的所有变量"
v-if="formData.template.contextAsBody" v-if="formData.template.contextAsBody"
disabled disabled
@ -896,28 +895,33 @@ watch(
const formRules = ref({ const formRules = ref({
type: [{ required: true, message: '请选择通知方式' }], type: [{ required: true, message: '请选择通知方式' }],
name: [ name: [
{ required: true, message: '请输入名称' }, { required: true, message: '请输入名称', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
], ],
provider: [{ required: true, message: '请选择类型' }], provider: [{ required: true, message: '请选择类型' }],
configId: [{ required: true, message: '请选择绑定配置' }], configId: [{ required: true, message: '请选择绑定配置', trigger: 'blur' }],
// //
'template.agentId': [{ required: true, message: '请输入agentId' }], 'template.agentId': [
'template.messageType': [{ required: true, message: '请选择消息类型' }], { required: true, message: '请输入AgentId', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符', trigger: 'change' },
],
'template.messageType': [
{ required: true, message: '请选择消息类型', trigger: 'blur' },
],
'template.markdown.title': [ 'template.markdown.title': [
{ required: true, message: '请输入标题', trigger: 'change' }, { required: true, message: '请输入标题', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符', trigger: 'change' }, { max: 64, message: '最多可输入64个字符', trigger: 'change' },
], ],
'template.link.title': [ 'template.link.title': [
{ required: true, message: '请输入标题', trigger: 'change' }, { required: true, message: '请输入标题', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符', trigger: 'change' }, { max: 64, message: '最多可输入64个字符', trigger: 'change' },
], ],
// 'template.url': [{ required: true, message: 'WebHook' }], // 'template.url': [{ required: true, message: 'WebHook' }],
// //
// 'template.agentId': [{ required: true, message: 'agentId' }], // 'template.agentId': [{ required: true, message: 'AgentId' }],
// //
'template.subject': [ 'template.subject': [
{ required: true, message: '请输入标题' }, { required: true, message: '请输入标题', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符', trigger: 'change' }, { max: 64, message: '最多可输入64个字符', trigger: 'change' },
], ],
'template.sendTo': [ 'template.sendTo': [
@ -944,7 +948,9 @@ const formRules = ref({
], ],
// //
'template.templateType': [{ required: true, message: '请选择类型' }], 'template.templateType': [{ required: true, message: '请选择类型' }],
'template.templateCode': [{ required: true, message: '请输入模板ID' }], 'template.templateCode': [
{ required: true, message: '请输入模板ID', trigger: 'blur' },
],
'template.calledNumber': [ 'template.calledNumber': [
{ max: 64, message: '最多可输入64个字符', trigger: 'change' }, { max: 64, message: '最多可输入64个字符', trigger: 'change' },
{ {
@ -978,14 +984,19 @@ const formRules = ref({
}, },
], ],
// //
'template.code': [{ required: true, message: '请选择模板' }], 'template.code': [
'template.signName': [{ required: true, message: '请输入签名' }], { required: true, message: '请选择模板', trigger: 'blur' },
],
'template.signName': [
{ required: true, message: '请输入签名', trigger: 'blur' },
],
// webhook // webhook
description: [{ max: 200, message: '最多可输入200个字符' }], description: [{ max: 200, message: '最多可输入200个字符' }],
'template.message': [ 'template.message': [
{ {
required: true, required: true,
message: '请输入模板内容', message: '请输入模板内容',
trigger: 'blur',
}, },
{ max: 500, message: '最多可输入500个字符', trigger: 'change' }, { max: 500, message: '最多可输入500个字符', trigger: 'change' },
], ],
@ -1071,7 +1082,7 @@ const spliceStr = () => {
variableFieldsStr += formData.value.template.body as string; variableFieldsStr += formData.value.template.body as string;
if (formData.value.provider === 'aliyun') if (formData.value.provider === 'aliyun')
variableFieldsStr += formData.value.template.ttsmessage as string; variableFieldsStr += formData.value.template.ttsmessage as string;
// console.log('variableFieldsStr: ', variableFieldsStr);
return variableFieldsStr || ''; return variableFieldsStr || '';
}; };
@ -1128,7 +1139,6 @@ const handleMessageTypeChange = () => {
}; };
} }
formData.value.variableDefinitions = []; formData.value.variableDefinitions = [];
// formData.value.template.message = '';
}; };
/** /**
@ -1139,7 +1149,6 @@ const getDetail = async () => {
const res = await templateApi.detail(route.params.id as string); const res = await templateApi.detail(route.params.id as string);
// formData.value = res.result; // formData.value = res.result;
Object.assign(formData.value, res.result); Object.assign(formData.value, res.result);
// console.log('formData.value: ', formData.value);
} }
}; };
getDetail(); getDetail();
@ -1174,8 +1183,7 @@ const handleTypeChange = () => {
const handleProviderChange = () => { const handleProviderChange = () => {
formData.value.template = formData.value.template =
TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider]; TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider];
// console.log('formData.value: ', formData.value);
// console.log('formData.value.template: ', formData.value.template);
getConfigList(); getConfigList();
resetPublicFiles(); resetPublicFiles();
}; };
@ -1243,7 +1251,6 @@ const handleSubmit = () => {
delete formData.value.template.link; delete formData.value.template.link;
if (formData.value.template.messageType === 'link') if (formData.value.template.messageType === 'link')
delete formData.value.template.markdown; delete formData.value.template.markdown;
// console.log('formData.value: ', formData.value);
// , , , : // , , , :
setTimeout(() => { setTimeout(() => {
validate() validate()
@ -1259,13 +1266,11 @@ const handleSubmit = () => {
} }
btnLoading.value = true; btnLoading.value = true;
let res;
if (!formData.value.id) { const res = formData.value.id
res = await templateApi.save(formData.value); ? await templateApi.update(formData.value)
} else { : await templateApi.save(formData.value);
res = await templateApi.update(formData.value);
}
// console.log('res: ', res);
if (res?.success) { if (res?.success) {
message.success('保存成功'); message.success('保存成功');
router.back(); router.back();
@ -1279,14 +1284,4 @@ const handleSubmit = () => {
}); });
}, 200); }, 200);
}; };
// test
// watch(
// () => formData.value,
// (val) => {
// console.log('formData.value: ', val);
// },
// { deep: true },
// );
// test
</script> </script>

View File

@ -110,14 +110,19 @@
</template> </template>
</CardBox> </CardBox>
</template> </template>
<template #bodyCell="{ column, text, record }"> <template #type="slotProps">
<span v-if="column.dataIndex === 'type'"> <span> {{ getMethodTxt(slotProps.type) }}</span>
{{ getMethodTxt(record.type) }} </template>
</span> <template #provider="slotProps">
<span v-if="column.dataIndex === 'provider'"> <span>
{{ getProviderTxt(record.type, record.provider) }} {{ getProviderTxt(slotProps.type, slotProps.provider) }}
</span> </span>
</template> </template>
<!-- <template #description="slotProps">
<Ellipsis>
{{ slotProps.description }}
</Ellipsis>
</template> -->
<template #action="slotProps"> <template #action="slotProps">
<j-space :size="16"> <j-space :size="16">
<template <template
@ -150,12 +155,8 @@
<script setup lang="ts"> <script setup lang="ts">
import TemplateApi from '@/api/notice/template'; import TemplateApi from '@/api/notice/template';
import type { ActionsType } from '@/components/Table/index.vue'; import type { ActionsType } from '@/components/Table/index.vue';
// import { getImage, LocalStore } from '@/utils/comm';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
// import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { NOTICE_METHOD, MSG_TYPE } from '@/views/notice/const'; import { NOTICE_METHOD, MSG_TYPE } from '@/views/notice/const';
import Debug from './Debug/index.vue'; import Debug from './Debug/index.vue';
import Log from './Log/index.vue'; import Log from './Log/index.vue';
import { downloadObject } from '@/utils/utils'; import { downloadObject } from '@/utils/utils';
@ -210,6 +211,8 @@ const columns = [
title: '说明', title: '说明',
dataIndex: 'description', dataIndex: 'description',
key: 'description', key: 'description',
scopedSlots: true,
ellipsis: true,
search: { search: {
type: 'string', type: 'string',
}, },

View File

@ -82,7 +82,7 @@ export const MSG_TYPE = {
], ],
email: [ email: [
{ {
label: 'email', label: '邮件',
value: 'embedded', value: 'embedded',
logo: getImage('/notice/email.png'), logo: getImage('/notice/email.png'),
}, },

View File

@ -1,27 +1,27 @@
<template> <template>
<div> <div>
<a-form layout="vertical" :rules="rule" :model="form" ref="formRef"> <j-form layout="vertical" :rules="rule" :model="form" ref="formRef">
<a-row :gutter="24"> <j-row :gutter="24">
<a-col :span="12"> <j-col :span="12">
<a-form-item label="名称" name="name"> <j-form-item label="名称" name="name">
<a-input <j-input
placeholder="请输入名称" placeholder="请输入名称"
v-model:value="form.name" v-model:value="form.name"
></a-input> </a-form-item ></j-input> </j-form-item
></a-col> ></j-col>
<a-col :span="12"> <j-col :span="12">
<a-form-item label="类型" name="targetType"> <j-form-item label="类型" name="targetType">
<a-select <j-select
:options="options" :options="options"
v-model:value="form.targetType" v-model:value="form.targetType"
:disabled="selectDisable" :disabled="selectDisable"
></a-select> ></j-select>
</a-form-item> </j-form-item>
</a-col> </j-col>
</a-row> </j-row>
<a-form-item label="级别" name="level"> <j-form-item label="级别" name="level">
<a-radio-group v-model:value="form.level"> <j-radio-group v-model:value="form.level">
<a-radio-button <j-radio-button
v-for="(item, index) in levelOption" v-for="(item, index) in levelOption"
:key="index" :key="index"
:value="item.value" :value="item.value"
@ -40,14 +40,14 @@
alt="" alt=""
/>{{ item.label }} />{{ item.label }}
</div> </div>
</a-radio-button> </j-radio-button>
</a-radio-group> </j-radio-group>
</a-form-item> </j-form-item>
<a-form-item label="说明" name="description"> <j-form-item label="说明" name="description">
<a-textarea v-model:value="form.description"></a-textarea> <j-textarea v-model:value="form.description"></j-textarea>
</a-form-item> </j-form-item>
<PermissionButton type="primary" @click="handleSave" :hasPermission="['rule-engine/Alarm/Configuration:add','rule-engine/Alarm/Configuration:update']">保存</PermissionButton> <PermissionButton type="primary" @click="handleSave" :hasPermission="['rule-engine/Alarm/Configuration:add','rule-engine/Alarm/Configuration:update']">保存</PermissionButton>
</a-form> </j-form>
</div> </div>
</template> </template>
@ -178,7 +178,7 @@ const handleSave = async () => {
const res = await save(form); const res = await save(form);
loading.value = false; loading.value = false;
if (res.status === 200) { if (res.status === 200) {
message.success('操作成功'); message.success('操作成功,请配置关联的场景联动');
menuStory.jumpPage( menuStory.jumpPage(
'rule-engine/Alarm/Configuration/Save', 'rule-engine/Alarm/Configuration/Save',
{}, {},

View File

@ -1,5 +1,5 @@
<template> <template>
<a-modal <j-modal
visible visible
title="新增" title="新增"
okText="确定" okText="确定"
@ -8,7 +8,7 @@
@cancel="closeModal" @cancel="closeModal"
@ok="saveCorrelation" @ok="saveCorrelation"
> >
<Search :columns="columns" @search="handleSearch"></Search> <pro-search :columns="columns" @search="handleSearch"/>
<div style="height: 500px; overflow-y: auto"> <div style="height: 500px; overflow-y: auto">
<JProTable <JProTable
model="CARD" model="CARD"
@ -78,7 +78,7 @@
</template> </template>
</JProTable> </JProTable>
</div> </div>
</a-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -213,7 +213,7 @@ const saveCorrelation = async () => {
const list = _selectedRowKeys.value.map((item: any) => { const list = _selectedRowKeys.value.map((item: any) => {
return { return {
alarmId: props.id, alarmId: props.id,
releId: item, ruleId: item,
}; };
}); });
const res = await bindScene([...list]); const res = await bindScene([...list]);

View File

@ -10,12 +10,12 @@
ref="actionRef" ref="actionRef"
> >
<template #headerTitle> <template #headerTitle>
<a-space> <j-space>
<PermissionButton type="primary" @click="showModal" hasPermission="rule-engine/Alarm/Configuration:add"> <PermissionButton type="primary" @click="showModal" hasPermission="rule-engine/Alarm/Configuration:add">
<template #icon><AIcon type="PlusOutlined" /></template> <template #icon><AIcon type="PlusOutlined" /></template>
新增 新增
</PermissionButton> </PermissionButton>
</a-space> </j-space>
</template> </template>
<template #card="slotProps"> <template #card="slotProps">
<SceneCard <SceneCard

View File

@ -3,13 +3,13 @@
<j-card> <j-card>
<j-tabs :activeKey="activeKey" @change="changeTabs"> <j-tabs :activeKey="activeKey" @change="changeTabs">
<j-tab-pane key="1" tab="基础配置"> <j-tab-pane key="1" tab="基础配置">
<Base/> <Base />
</j-tab-pane> </j-tab-pane>
<j-tab-pane key="2" tab="关联场景联动"> <j-tab-pane key="2" tab="关联场景联动">
<Scene></Scene> <Scene></Scene>
</j-tab-pane> </j-tab-pane>
<j-tab-pane key="3" tab="告警记录"> <j-tab-pane key="3" tab="告警记录">
<Log/> <Log v-if="activeKey === '3'" />
</j-tab-pane> </j-tab-pane>
</j-tabs> </j-tabs>
</j-card> </j-card>
@ -23,13 +23,13 @@ import Log from './Log/indev.vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
const route = useRoute(); const route = useRoute();
const changeTabs = (e:any) =>{ const changeTabs = (e: any) => {
if(route.query?.id){ if (route.query?.id) {
activeKey.value = e; activeKey.value = e;
}else{ } else {
message.error('请先保存基础配置'); message.error('请先保存基础配置');
} }
} };
const activeKey = ref('1'); const activeKey = ref('1');
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -56,8 +56,8 @@
{{ slotProps.name }} {{ slotProps.name }}
</span> </span>
</Ellipsis> </Ellipsis>
<a-row> <j-row>
<a-col :span="12"> <j-col :span="12">
<div class="content-des-title"> <div class="content-des-title">
关联场景联动 关联场景联动
</div> </div>
@ -66,8 +66,8 @@
{{ (slotProps?.scene || []).map((item: any) => item?.name).join(',') || '' }} {{ (slotProps?.scene || []).map((item: any) => item?.name).join(',') || '' }}
</div></Ellipsis </div></Ellipsis
> >
</a-col> </j-col>
<a-col :span="12"> <j-col :span="12">
<div class="content-des-title"> <div class="content-des-title">
告警级别 告警级别
</div> </div>
@ -75,8 +75,8 @@
{{ (Store.get('default-level') || []).find((item: any) => item?.level === slotProps.level)?.title || {{ (Store.get('default-level') || []).find((item: any) => item?.level === slotProps.level)?.title ||
slotProps.level }} slotProps.level }}
</div> </div>
</a-col> </j-col>
</a-row> </j-row>
</template> </template>
<template #actions="item"> <template #actions="item">
<PermissionButton <PermissionButton
@ -109,7 +109,7 @@
<span>{{ map[slotProps.targetType] }}</span> <span>{{ map[slotProps.targetType] }}</span>
</template> </template>
<template #level="slotProps"> <template #level="slotProps">
<a-tooltip <j-tooltip
placement="topLeft" placement="topLeft"
:title="(Store.get('default-level') || []).find((item: any) => item?.level === slotProps.level)?.title || :title="(Store.get('default-level') || []).find((item: any) => item?.level === slotProps.level)?.title ||
slotProps.level" slotProps.level"
@ -118,7 +118,7 @@
{{ (Store.get('default-level') || []).find((item: any) => item?.level === slotProps.level)?.title || {{ (Store.get('default-level') || []).find((item: any) => item?.level === slotProps.level)?.title ||
slotProps.level }} slotProps.level }}
</div> </div>
</a-tooltip> </j-tooltip>
</template> </template>
<template #sceneId="slotProps"> <template #sceneId="slotProps">
<span <span
@ -126,7 +126,7 @@
> >
</template> </template>
<template #state="slotProps"> <template #state="slotProps">
<a-badge <j-badge
:text=" :text="
slotProps.state?.value === 'enabled' slotProps.state?.value === 'enabled'
? '正常' ? '正常'
@ -140,7 +140,7 @@
/> />
</template> </template>
<template #action="slotProps"> <template #action="slotProps">
<a-space :size="16"> <j-space :size="16">
<template <template
v-for="i in getActions(slotProps, 'table')" v-for="i in getActions(slotProps, 'table')"
:key="i.key" :key="i.key"
@ -168,7 +168,7 @@
/></template> /></template>
</PermissionButton> </PermissionButton>
</template> </template>
</a-space> </j-space>
</template> </template>
</JProTable> </JProTable>
</div> </div>

View File

@ -1,10 +1,10 @@
<template> <template>
<page-container> <page-container>
<Search <pro-search
:columns="columns" :columns="columns"
target="alarm-log-detail" target="alarm-log-detail"
@search="handleSearch" @search="handleSearch"
></Search> />
<JProTable <JProTable
:columns="columns" :columns="columns"
model="TABLE" model="TABLE"
@ -19,7 +19,7 @@
moment(slotProps.alarmTime).format('YYYY-MM-DD HH:mm:ss') moment(slotProps.alarmTime).format('YYYY-MM-DD HH:mm:ss')
}}</template> }}</template>
<template #action="slotProps"> <template #action="slotProps">
<a-space <j-space
><template ><template
v-for="i in getActions(slotProps, 'table')" v-for="i in getActions(slotProps, 'table')"
:key="i.key" :key="i.key"
@ -37,7 +37,7 @@
<template #icon><AIcon :type="i.icon"/></template> <template #icon><AIcon :type="i.icon"/></template>
</PermissionButton> </PermissionButton>
</template> </template>
</a-space> </j-space>
</template> </template>
</JProTable> </JProTable>
<Info v-if="visiable" :data="current" @close="close"/> <Info v-if="visiable" :data="current" @close="close"/>

View File

@ -1,16 +1,16 @@
<template> <template>
<a-modal visible title="详情" okText="确定" cancelText="取消" :width="1000" @ok="closeModal" @cancel="closeModal"> <j-modal visible title="详情" okText="确定" cancelText="取消" :width="1000" @ok="closeModal" @cancel="closeModal">
<a-descriptions bordered :column="2"> <j-descriptions bordered :column="2">
<a-descriptions-item v-if="props.data.targetType==='device'" label="告警设备" :span="1">{{props.data?.targetName || ''}}</a-descriptions-item> <j-descriptions-item v-if="props.data.targetType==='device'" label="告警设备" :span="1">{{props.data?.targetName || ''}}</j-descriptions-item>
<a-descriptions-item v-if="props.data.targetType==='device'" label="设备ID" :span="1">{{props.data?.targetId || ''}}</a-descriptions-item> <j-descriptions-item v-if="props.data.targetType==='device'" label="设备ID" :span="1">{{props.data?.targetId || ''}}</j-descriptions-item>
<a-descriptions-item label="告警名称" :span="1">{{ <j-descriptions-item label="告警名称" :span="1">{{
props.data?.alarmConfigName props.data?.alarmConfigName
}}</a-descriptions-item> }}</j-descriptions-item>
<a-descriptions-item label="告警时间" :span="1">{{ <j-descriptions-item label="告警时间" :span="1">{{
moment(data?.alarmTime).format('YYYY-MM-DD HH:mm:ss') moment(data?.alarmTime).format('YYYY-MM-DD HH:mm:ss')
}}</a-descriptions-item> }}</j-descriptions-item>
<a-descriptions-item label="告警级别" :span="1"> <j-descriptions-item label="告警级别" :span="1">
<a-tooltip <j-tooltip
placement="topLeft" placement="topLeft"
:title="(Store.get('default-level') || []).find((item: any) => item?.level === data?.level) :title="(Store.get('default-level') || []).find((item: any) => item?.level === data?.level)
?.title || props.data?.level" ?.title || props.data?.level"
@ -21,10 +21,10 @@
?.title || props.data?.level}} ?.title || props.data?.level}}
</span> </span>
</Ellipsis> </Ellipsis>
</a-tooltip> </j-tooltip>
</a-descriptions-item> </j-descriptions-item>
<a-descriptions-item label="告警说明" :span="1" <j-descriptions-item label="告警说明" :span="1"
><a-tooltip ><j-tooltip
placement="topLeft" placement="topLeft"
:title="data?.description || ''" :title="data?.description || ''"
> >
@ -33,14 +33,14 @@
{{ data?.description || '' }} {{ data?.description || '' }}
</span> </Ellipsis </span> </Ellipsis
> >
</a-tooltip></a-descriptions-item </j-tooltip></j-descriptions-item
> >
<a-descriptions-item <j-descriptions-item
label="告警流水" label="告警流水"
:span="2" :span="2"
><div style="max-height: 500px; overflow-y: auto;"><JsonViewer :value="JSON.parse(data?.alarmInfo || '{}')" :expand-depth="5"></JsonViewer></div></a-descriptions-item> ><div style="max-height: 500px; overflow-y: auto;"><JsonViewer :value="JSON.parse(data?.alarmInfo || '{}')" :expand-depth="5"></JsonViewer></div></j-descriptions-item>
</a-descriptions> </j-descriptions>
</a-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -1,5 +1,5 @@
<template> <template>
<a-modal <j-modal
title="告警处理" title="告警处理"
okText="确定" okText="确定"
cancelText="取消" cancelText="取消"
@ -9,18 +9,18 @@
destroyOnClose destroyOnClose
:confirmLoading="loading" :confirmLoading="loading"
> >
<a-form :rules="rules" layout="vertical" ref="formRef" :model="form"> <j-form :rules="rules" layout="vertical" ref="formRef" :model="form">
<a-form-item label="处理结果" name="describe"> <j-form-item label="处理结果" name="describe">
<a-textarea <j-textarea
:rows="8" :rows="8"
:maxlength="200" :maxlength="200"
showCount showCount
placeholder="请输入处理结果" placeholder="请输入处理结果"
v-model:value="form.describe" v-model:value="form.describe"
></a-textarea> ></j-textarea>
</a-form-item> </j-form-item>
</a-form> </j-form>
</a-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -64,6 +64,7 @@ const handleSave = () => {
}); });
if (res.status === 200) { if (res.status === 200) {
onlyMessage('操作成功!'); onlyMessage('操作成功!');
emit('closeSolve');
} else { } else {
onlyMessage('操作失败!', 'error'); onlyMessage('操作失败!', 'error');
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<a-modal <j-modal
visible visible
title="处理记录" title="处理记录"
:width="1200" :width="1200"
@ -8,11 +8,11 @@
@ok="clsoeModal" @ok="clsoeModal"
@cancel="clsoeModal" @cancel="clsoeModal"
> >
<Search <pro-search
:columns="columns" :columns="columns"
target="bind-channel" target="bind-channel"
@search="handleSearch" @search="handleSearch"
></Search> />
<JProTable <JProTable
model="TABLE" model="TABLE"
:columns="columns" :columns="columns"
@ -48,7 +48,7 @@
</span> </span>
</template> </template>
</JProTable> </JProTable>
</a-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -1,29 +1,29 @@
<template> <template>
<div class="alarm-log-card"> <div class="alarm-log-card">
<Search <pro-search
:columns="columns" :columns="columns"
target="alarm-log" target="alarm-log"
v-if="['all', 'detail'].includes(props.type)" v-if="['all', 'detail'].includes(props.type)"
@search="search" @search="search"
></Search> />
<Search <pro-search
:columns="produtCol" :columns="produtCol"
target="alarm-log" target="alarm-log"
v-if="['product', 'other'].includes(props.type)" v-if="['product', 'other'].includes(props.type)"
@search="search" @search="search"
></Search> />
<Search <pro-search
:columns="deviceCol" :columns="deviceCol"
target="alarm-log" target="alarm-log"
v-if="props.type === 'device'" v-if="props.type === 'device'"
@search="search" @search="search"
></Search> />
<Search <pro-search
:columns="orgCol" :columns="orgCol"
target="alarm-log" target="alarm-log"
v-if="props.type === 'org'" v-if="props.type === 'org'"
@search="search" @search="search"
></Search> />
<JProTable <JProTable
:columns="columns" :columns="columns"
:request="handleSearch" :request="handleSearch"
@ -31,6 +31,7 @@
:gridColumns="[1, 1, 2]" :gridColumns="[1, 1, 2]"
:gridColumn="2" :gridColumn="2"
model="CARD" model="CARD"
ref="tableRef"
> >
<template #card="slotProps"> <template #card="slotProps">
<CardBox <CardBox
@ -52,8 +53,8 @@
{{ slotProps.alarmName }} {{ slotProps.alarmName }}
</span> </span>
</Ellipsis> </Ellipsis>
<a-row :gutter="24"> <j-row :gutter="24">
<a-col :span="8"> <j-col :span="8">
<div class="content-des-title"> <div class="content-des-title">
{{ titleMap.get(slotProps.targetType) }} {{ titleMap.get(slotProps.targetType) }}
</div> </div>
@ -62,8 +63,8 @@
{{ slotProps?.targetName }} {{ slotProps?.targetName }}
</div></Ellipsis </div></Ellipsis
> >
</a-col> </j-col>
<a-col :span="8"> <j-col :span="8">
<div class="content-des-title"> <div class="content-des-title">
最近告警时间 最近告警时间
</div> </div>
@ -76,17 +77,17 @@
}} }}
</div></Ellipsis </div></Ellipsis
> >
</a-col> </j-col>
<a-col :span="8"> <j-col :span="8">
<div class="content-des-title">状态</div> <div class="content-des-title">状态</div>
<a-badge <j-badge
:status=" :status="
slotProps.state.value === 'warning' slotProps.state.value === 'warning'
? 'error' ? 'error'
: 'default' : 'default'
" "
> >
</a-badge </j-badge
><span ><span
:style=" :style="
slotProps.state.value === 'warning' slotProps.state.value === 'warning'
@ -96,8 +97,8 @@
> >
{{ slotProps.state.text }} {{ slotProps.state.text }}
</span> </span>
</a-col> </j-col>
</a-row> </j-row>
</template> </template>
<template #actions="item"> <template #actions="item">
<PermissionButton <PermissionButton
@ -105,7 +106,6 @@
item.key === 'solve' && item.key === 'solve' &&
slotProps.state.value === 'normal' slotProps.state.value === 'normal'
" "
:popConfirm="item.popConfirm"
:tooltip="{ :tooltip="{
...item.tooltip, ...item.tooltip,
}" }"
@ -152,11 +152,11 @@ import { Store } from 'jetlinks-store';
import moment from 'moment'; import moment from 'moment';
import type { ActionsType } from '@/components/Table'; import type { ActionsType } from '@/components/Table';
import SolveComponent from '../SolveComponent/index.vue'; import SolveComponent from '../SolveComponent/index.vue';
import SolveLog from '../SolveLog/index.vue' import SolveLog from '../SolveLog/index.vue';
import { useMenuStore } from '@/store/menu'; import { useMenuStore } from '@/store/menu';
import { usePermissionStore } from '@/store/permission'; import { usePermissionStore } from '@/store/permission';
const menuStory = useMenuStore(); const menuStory = useMenuStore();
const tableRef = ref();
const alarmStore = useAlarmStore(); const alarmStore = useAlarmStore();
const { data } = storeToRefs(alarmStore); const { data } = storeToRefs(alarmStore);
const getDefaulitLevel = () => { const getDefaulitLevel = () => {
@ -339,7 +339,7 @@ watchEffect(() => {
}, },
]; ];
} }
if(props.type === 'all'){ if (props.type === 'all') {
params.value.terms = []; params.value.terms = [];
} }
}); });
@ -347,24 +347,20 @@ watchEffect(() => {
const search = (data: any) => { const search = (data: any) => {
params.value.terms = [...data?.terms]; params.value.terms = [...data?.terms];
if (props.type !== 'all' && !props.id) { if (props.type !== 'all' && !props.id) {
params.value.terms.push( params.value.terms.push({
{
termType: 'eq', termType: 'eq',
column: 'targetType', column: 'targetType',
value: props.type, value: props.type,
type: 'and', type: 'and',
}, });
);
} }
if (props.id) { if (props.id) {
params.value.terms.push ( params.value.terms.push({
{
termType: 'eq', termType: 'eq',
column: 'alarmConfigId', column: 'alarmConfigId',
value: props.id, value: props.id,
type: 'and', type: 'and',
}, });
);
} }
}; };
@ -381,17 +377,19 @@ const getActions = (
title: '告警处理', title: '告警处理',
}, },
icon: 'ToolOutlined', icon: 'ToolOutlined',
onClick: () =>{ onClick: () => {
data.value.current = currentData; data.value.current = currentData;
data.value.solveVisible = true; data.value.solveVisible = true;
}, },
popConfirm:{ // popConfirm: {
title: !usePermissionStore().hasPermission('rule-engine/Alarm/Log:action') // title: !usePermissionStore().hasPermission(
? '暂无权限,请联系管理员' // 'rule-engine/Alarm/Log:action',
: data.state?.value === 'normal' // )
? '无告警' // ? ''
: '' // : data.state?.value === 'normal'
} // ? ''
// : '',
// },
}, },
{ {
key: 'log', key: 'log',
@ -400,9 +398,11 @@ const getActions = (
title: '告警日志', title: '告警日志',
}, },
icon: 'FileOutlined', icon: 'FileOutlined',
onClick: () =>{ onClick: () => {
menuStory.jumpPage(`rule-engine/Alarm/Log/Detail`,{id:currentData.id}); menuStory.jumpPage(`rule-engine/Alarm/Log/Detail`, {
} id: currentData.id,
});
},
}, },
{ {
key: 'detail', key: 'detail',
@ -411,10 +411,10 @@ const getActions = (
title: '处理记录', title: '处理记录',
}, },
icon: 'FileTextOutlined', icon: 'FileTextOutlined',
onClick:() =>{ onClick: () => {
data.value.current = currentData; data.value.current = currentData;
data.value.logVisible = true; data.value.logVisible = true;
} },
}, },
]; ];
return actions; return actions;
@ -422,15 +422,16 @@ const getActions = (
/** /**
* 关闭告警日志 * 关闭告警日志
*/ */
const closeSolve = () =>{ const closeSolve = () => {
data.value.solveVisible = false; data.value.solveVisible = false;
} tableRef.value.reload(params.value);
};
/** /**
* 关闭处理记录 * 关闭处理记录
*/ */
const closeLog = () =>{ const closeLog = () => {
data.value.logVisible = false; data.value.logVisible = false;
} };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<a-modal <j-modal
:maskClosable="false" :maskClosable="false"
width="650px" width="650px"
destroyOnClose destroyOnClose
@ -12,30 +12,30 @@
:confirmLoading="loading" :confirmLoading="loading"
> >
<div style="margin-top: 10px"> <div style="margin-top: 10px">
<a-form <j-form
:layout="'vertical'" :layout="'vertical'"
ref="formRef" ref="formRef"
:rules="rules" :rules="rules"
:model="modelRef" :model="modelRef"
> >
<a-form-item label="名称" name="name"> <j-form-item label="名称" name="name">
<a-input <j-input
v-model:value="modelRef.name" v-model:value="modelRef.name"
placeholder="请输入名称" placeholder="请输入名称"
/> />
</a-form-item> </j-form-item>
<a-form-item label="说明" name="describe"> <j-form-item label="说明" name="describe">
<a-textarea <j-textarea
v-model:value="modelRef.description" v-model:value="modelRef.description"
placeholder="请输入说明" placeholder="请输入说明"
showCount showCount
:maxlength="200" :maxlength="200"
:rows="4" :rows="4"
/> />
</a-form-item> </j-form-item>
</a-form> </j-form>
</div> </div>
</a-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -1,11 +1,11 @@
<template> <template>
<page-container> <page-container>
<div> <div>
<Search <pro-search
:columns="query.columns" :columns="query.columns"
target="device-instance" target="device-instance"
@search="handleSearch" @search="handleSearch"
></Search> />
<JProTable <JProTable
:columns="columns" :columns="columns"
:request="queryList" :request="queryList"
@ -53,15 +53,15 @@
{{ slotProps.name }} {{ slotProps.name }}
</span> </span>
</Ellipsis> </Ellipsis>
<a-row> <j-row>
<a-col :span="12"> <j-col :span="12">
<Ellipsis> <Ellipsis>
<div> <div>
{{ slotProps.description }} {{ slotProps.description }}
</div> </div>
</Ellipsis> </Ellipsis>
</a-col> </j-col>
</a-row> </j-row>
</template> </template>
<template #actions="item"> <template #actions="item">
<PermissionButton <PermissionButton
@ -88,7 +88,7 @@
</CardBox> </CardBox>
</template> </template>
<template #state="slotProps"> <template #state="slotProps">
<a-badge <j-badge
:text=" :text="
slotProps.state?.value === 'started' slotProps.state?.value === 'started'
? '正常' ? '正常'
@ -102,7 +102,7 @@
/> />
</template> </template>
<template #action="slotProps"> <template #action="slotProps">
<a-space :size="16"> <j-space :size="16">
<template <template
v-for="i in getActions(slotProps, 'table')" v-for="i in getActions(slotProps, 'table')"
:key="i.key" :key="i.key"
@ -123,7 +123,7 @@
/></template> /></template>
</PermissionButton> </PermissionButton>
</template> </template>
</a-space> </j-space>
</template> </template>
</JProTable> </JProTable>
<!-- 新增编辑 --> <!-- 新增编辑 -->

11055
yarn.lock

File diff suppressed because it is too large Load Diff