fix: 通知订阅

This commit is contained in:
100011797 2023-06-20 16:54:15 +08:00
parent 0edeb9804a
commit 7b6684a9f4
35 changed files with 1558 additions and 858 deletions

View File

@ -25,7 +25,7 @@
"event-source-polyfill": "^1.0.31",
"global": "^4.4.0",
"jetlinks-store": "^0.0.3",
"jetlinks-ui-components": "^1.0.23",
"jetlinks-ui-components": "^1.0.24",
"js-cookie": "^3.0.1",
"less": "^4.1.3",
"less-loader": "^11.1.0",

View File

@ -1,24 +1,26 @@
import server from '@/utils/request'
// 获取记录列表
export const getList_api = (data: object): any => server.post(`/notifications/_query`, data)
export const getList_api = (data: any): any => server.post(`/notifications/_query`, data)
// 获取未读记录列表
export const getListByUnRead_api = (data: object): any => server.post(`/notifications/_query`, data)
// export const getListByUnRead_api = (data: any): any => server.post(`/notifications/_query`, data)
// 修改记录状态
export const changeStatus_api = (type: '_read' | '_unread', data: string[]): any => server.post(`/notifications/${type}`, data)
// 查询告警记录详情
export const getDetail = (id: string): any => server.get(`/alarm/record/${id}`)
const encodeParams = (params: Record<string, any>) => {
let result = {}
for (const key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
const value = params[key];
if (key === 'terms') {
result['terms[0].column:'] = 0
result['terms[0].value'] = JSON.stringify(value[0])
} else result[key] = value
}
}
// const encodeParams = (params: Record<string, any>) => {
// let result = {}
// for (const key in params) {
// if (Object.prototype.hasOwnProperty.call(params, key)) {
// const value = params[key];
// if (key === 'terms') {
// result['terms[0].column:'] = 0
// result['terms[0].value'] = JSON.stringify(value[0])
// } else result[key] = value
// }
// }
return result
};
// return result
// };

View File

@ -20,3 +20,22 @@ export const getAlarmList_api = () => server.post(`/alarm/config/_query/no-pagin
sorts: [{ name: 'createTime', order: 'desc' }],
paging: false,
});
// 判断获取当前用户绑定信
export const getIsBindThird = () => server.get(`/user/third-party/me`);
// 生成OAuth2授权URL
export const getWechatOAuth2 = (configId: string, templateId: string, url: string) => server.get(`/notifier/wechat/corp/${configId}/${templateId}/oauth2/binding-user-url?redirectUri=${url}`);
export const getDingTalkOAuth2 = (configId: string, url: string) => server.get(`/notifier/dingtalk/corp/${configId}/oauth2/binding-user-url?authCode=${url}`);
// 获取oauth2授权的用户绑定码
export const getUserBind = (type: 'wechat' | 'dingtalk', params: any) => server.get(`/notifier/${type}/corp/oauth2/user-bind-code`, params);
// 根据绑定码绑定当前用户
export const bindThirdParty = (type: string, provider: string, bindCode: string) => server.post(`/user/third-party/me/${type}/${provider}/${bindCode}/_bind`);

View File

@ -25,4 +25,5 @@ export default {
// 短信获取签名
getSigns: (id: any) => get(`/notifier/sms/aliyun/${id}/signs`),
getListByConfigId: (id: string, data: any): any => post(`/notifier/template/${id}/_query`, data),
getListVariableByConfigId: (id: string, data?: any): any => post(`/notifier/template/${id}/detail/_query`, data),
}

View File

@ -3,18 +3,15 @@
<j-dropdown
v-model:visible="visible"
:trigger="['click']"
:destroyPopupOnHide="true"
@visible-change="visibleChange"
>
<!-- <div class="icon-content">
<AIcon type="BellOutlined" style="font-size: 16px" />
<span class="unread" v-show="total > 0">{{ total }}</span>
</div> -->
<j-badge :count="total" :offset="[3, -3]">
<AIcon type="BellOutlined" style="font-size: 16px" />
</j-badge>
<template #overlay>
<div>
<NoticeInfo :data="list" @on-action="handleRead" />
<NoticeInfo @action="handleRead" />
</div>
</template>
</j-dropdown>
@ -22,7 +19,7 @@
</template>
<script setup lang="tsx">
import { getListByUnRead_api } from '@/api/account/notificationRecord';
import { getList_api } from '@/api/account/notificationRecord';
import NoticeInfo from './NoticeInfo.vue';
import { getWebSocket } from '@/utils/websocket';
import { notification, Button } from 'jetlinks-ui-components';
@ -31,12 +28,13 @@ import { useUserInfo } from '@/store/userInfo';
import { useMenuStore } from '@/store/menu';
const { jumpPage } = useMenuStore();
const updateCount = computed(() => useUserInfo().$state.alarmUpdateCount);
const updateCount = computed(() => useUserInfo().alarmUpdateCount);
const menuStory = useMenuStore();
const total = ref(0);
const list = ref<any[]>([]);
// const list = ref<any[]>([]);
const loading = ref(false);
const visible = ref(false);
const subscribeNotice = () => {
getWebSocket('notification', '/notifications', {})
@ -63,7 +61,6 @@ const subscribeNotice = () => {
)
,
onClick: () => {
// changeStatus_api('_read', [resp.id])
read('', resp);
},
key: resp.payload.id,
@ -93,15 +90,17 @@ const read = (type: string, data: any) => {
notification.close(data.payload.id);
getList();
if (type !== '_read') {
jumpPage('account/NotificationRecord', {
menuStory.routerPush('account/center', {
tabKey: 'StationMessage',
row: data.payload.detail,
});
}
});
};
//
const getList = () => {
loading.value = true;
loading.value = true;
const params = {
sorts: [{
name: 'notifyTime',
@ -120,25 +119,28 @@ const getList = () => {
},
],
};
getListByUnRead_api(params)
getList_api(params)
.then((resp: any) => {
list.value = resp.result.data;
total.value = resp.result.total;
})
.finally(() => (loading.value = false));
};
subscribeNotice();
getList();
watch(updateCount, () => getList());
const visibleChange = (bool: boolean) => {
bool && getList();
};
const visible = ref(false);
const handleRead = () => {
visible.value = false;
getList();
};
watch(updateCount, () => getList());
onMounted(() => {
subscribeNotice();
getList();
})
</script>
<style lang="less" scoped>

View File

@ -1,57 +1,138 @@
<template>
<div class="notice-info-container">
<j-tabs :activeKey="'default'">
<j-tab-pane key="default" tab="未读消息">
<div class="no-data" v-if="props.data.length === 0">
<img
src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
alt=""
/>
</div>
<div v-else class="content">
<j-scrollbar class="list" max-height="400">
<div
class="list-item"
v-for="item in props.data"
@click.stop="read(item.id)"
>
<h5>{{ item.topicName }}</h5>
<p>{{ item.message }}</p>
<j-tabs
v-model:activeKey="activeKey"
:destroyInactiveTabPane="true"
@change="onChange"
>
<j-tab-pane v-for="item in tab" :key="item.key">
<template #tab>
<NoticeTab :refresh="refreshObj[item.key]" :tab="item?.tab" :type="item.type" />
</template>
<j-spin :spinning="loading">
<div class="content">
<j-scrollbar class="list" max-height="400" v-if="total">
<template v-for="i in list" :key="i.id">
<NoticeItem
:data="i"
@action="emits('action')"
@refresh="onRefresh(item.key)"
/>
</template>
</j-scrollbar>
<div class="no-data" v-else>
<j-empty />
</div>
<div class="btns">
<span @click="onMore">查看更多</span>
</div>
</j-scrollbar>
<div class="btns">
<span @click="read()">当前标记为已读</span>
<span @click="jumpPage('account/NotificationRecord')"
>查看更多</span
>
</div>
</div>
</j-spin>
</j-tab-pane>
</j-tabs>
</div>
</template>
<script setup lang="ts">
import { changeStatus_api } from '@/api/account/notificationRecord';
import { getList_api } from '@/api/account/notificationRecord';
import { useMenuStore } from '@/store/menu';
import { cloneDeep } from 'lodash-es';
import NoticeItem from './NoticeItem.vue';
import NoticeTab from './NoticeTab.vue';
const emits = defineEmits(['onAction']);
const props = defineProps<{
data: any[];
}>();
const { jumpPage } = useMenuStore();
const emits = defineEmits(['action']);
const read = (id?: string) => {
const ids = id ? [id] : props.data.map((item) => item.id);
changeStatus_api('_read', ids).then((resp: any) => {
if (resp.status === 200) {
jumpPage('account/NotificationRecord', {
row: props.data.find((f: any) => f.id === id),
});
emits('onAction');
}
type DataType = 'alarm' | 'system-monitor' | 'system-business';
const tab = [
{
key: 'alarm',
tab: '告警',
type: [
'alarm-product',
'alarm-device',
'alarm-other',
'alarm-org',
'alarm',
],
},
{
key: 'system-monitor',
tab: '系统运维',
type: ['system-event'],
},
{
key: 'system-business',
tab: '业务监控',
type: ['device-transparent-codec'],
},
];
const refreshObj = ref({
'alarm': true,
'system-monitor': true,
'system-business': true
})
const loading = ref(false);
const total = ref(0);
const list = ref<any[]>([]);
const activeKey = ref<DataType>('alarm');
const menuStory = useMenuStore();
const getData = (type: string[]) => {
loading.value = true;
const params = {
sorts: [
{
name: 'notifyTime',
order: 'desc',
},
],
pageSize: 12,
terms: [
{
terms: [
{
type: 'or',
value: type,
termType: 'in',
column: 'topicProvider',
},
],
},
],
};
getList_api(params)
.then((resp: any) => {
total.value = resp.result.total;
list.value = resp.result?.data || [];
})
.finally(() => (loading.value = false));
};
const onChange = (_key: string) => {
const type = tab.find((item) => item.key === _key)?.type || [];
getData(type);
};
onMounted(() => {
onChange('alarm');
});
const onRefresh = (id: string) => {
const flag = cloneDeep(refreshObj.value[id])
refreshObj.value = {
...refreshObj.value,
[id]: !flag
}
}
const onMore = () => {
menuStory.routerPush('account/center', {
tabKey: 'StationMessage',
});
emits('action')
};
</script>
@ -89,26 +170,6 @@ const read = (id?: string) => {
//
display: none;
}
.list-item {
padding: 12px 24px;
list-style: none;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
h5 {
color: rgba(0, 0, 0, 0.85);
font-size: 14px;
font-weight: normal;
}
p {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
&:hover {
background: #f0f5ff;
}
}
}
.btns {
display: flex;
@ -116,13 +177,13 @@ const read = (id?: string) => {
line-height: 46px;
span {
display: block;
width: 50%;
width: 100%;
text-align: center;
cursor: pointer;
&:first-child {
border-right: 1px solid #f0f0f0;
}
// &:first-child {
// border-right: 1px solid #f0f0f0;
// }
}
}
}

View File

@ -0,0 +1,159 @@
<template>
<div class="list-items">
<div
class="list-item"
@click="onMove"
:style="{
transform: `translate(${num}px, 0)`,
}"
>
<div class="list-item-left">
<div class="header">
<div class="title">
<div>{{ props.data?.topicName }}</div>
<span :style="{color: state === 'unread' ? 'red' : '#AAAAAA'}">{{ state === 'unread' ? '未读' : '已读' }}</span>
</div>
<div class="time">
{{
dayjs(props.data?.notifyTime).format(
'YYYY-MM-DD HH:mm:ss',
)
}}
</div>
</div>
<p>{{ props.data?.message }}</p>
</div>
<div class="list-item-right">
<j-button @click.stop="detail">查看详情</j-button>
<j-button v-if="state === 'unread'" @click.stop="read('_read')">标为已读</j-button>
<j-button v-else @click.stop="read('_unread')">标为未读</j-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs';
import { changeStatus_api } from '@/api/account/notificationRecord';
import { useMenuStore } from '@/store/menu';
import { useUserInfo } from '@/store/userInfo';
import { onlyMessage } from '@/utils/comm';
const menuStory = useMenuStore();
const route = useRoute();
const userInfo = useUserInfo();
const emits = defineEmits(['action', 'refresh']);
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
const num = ref<-100 | 0>(0);
const state = ref(props.data.state?.value)
watchEffect(() => {
state.value = props.data.state?.value
})
const onMove = () => {
num.value = num.value === 0 ? -100 : 0;
};
const detail = () => {
// /account/center
if (route.path === '/account/center') {
userInfo.tabKey = 'StationMessage';
userInfo.messageInfo = props.data;
} else {
menuStory.routerPush('account/center', {
row: props.data,
tabKey: 'StationMessage',
});
}
emits('action');
};
const read = (type: '_read' | '_unread') => {
changeStatus_api(type, [props.data.id]).then((resp: any) => {
if (resp.status === 200) {
if(type === '_read') {
userInfo.alarmUpdateCount -= 1;
} else {
userInfo.alarmUpdateCount += 1;
}
num.value = 0;
state.value = type === '_read' ? 'read' : 'unread'
onlyMessage('操作成功!');
emits('refresh')
}
});
};
</script>
<style lang="less" scoped>
.list-items {
width: 312px;
overflow: hidden;
height: 100px;
border-bottom: 1px solid #f0f0f0;
margin: 0 24px;
box-sizing: content-box;
}
.list-item {
list-style: none;
cursor: pointer;
display: flex;
width: 412px;
transition: all 0.3s;
gap: 24px;
.list-item-left {
padding: 12px 0;
width: 312px;
height: 100px;
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
.title {
display: flex;
align-items: center;
div {
color: rgba(0, 0, 0, 0.85);
font-size: 14px;
font-weight: bold;
margin-right: 10px;
}
span {
color: red;
font-size: 13px;
}
}
.time {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
}
p {
font-size: 12px;
}
}
.list-item-right {
width: 100px;
padding: 12px 12px 12px 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<j-badge :count="total" :offset="[3, -3]">
{{ tab }}
</j-badge>
</template>
<script setup lang="ts">
import { getList_api } from '@/api/account/notificationRecord';
import { PropType } from 'vue';
const props = defineProps({
tab: {
type: String,
default: '',
},
type: {
type: Array as PropType<string[]>,
default: () => [],
},
refresh: {
type: Boolean
}
});
const total = ref<number>(0);
const getData = (type: string[]) => {
const params = {
sorts: [
{
name: 'notifyTime',
order: 'desc',
},
],
terms: [
{
terms: [
{
type: 'and',
value: type,
termType: 'in',
column: 'topicProvider',
},
{
type: 'and',
value: 'unread',
termType: 'eq',
column: 'state',
},
],
},
],
};
getList_api(params).then((resp: any) => {
total.value = resp.result.total;
});
};
watch(
() => props.refresh,
() => {
getData(props.type);
},
{
immediate: true,
deep: true
}
);
</script>

View File

@ -2,12 +2,13 @@
<div>
<j-dropdown placement="bottomRight">
<div style="cursor: pointer;height: 100%;">
<img
:src="userInfo.avatar"
<j-avatar
:src="userInfo.userInfos?.avatar"
alt=""
style="width: 24px; margin-right: 12px"
:size="24"
style="margin-right: 12px"
/>
<span>{{ userInfo.name }}</span>
<span>{{ userInfo.userInfos?.name }}</span>
</div>
<template #overlay>
<j-menu>
@ -32,8 +33,7 @@ import { LoginPath } from '@/router/menu'
const {push} = useRouter();
const userInfo = useUserInfo().$state.userInfos as any;
const userInfo = useUserInfo() as any;
const logOut = () => {
loginout_api().then(() => {

View File

@ -90,6 +90,12 @@ export const useMenuStore = defineStore({
console.warn(`没有找到对应的页面: ${name}`)
}
},
routerPush(name: string, params?: Record<string, any>, query?: Record<string, any>) {
this.params = { [name]: params || {}}
router.push({
name, params, query, state: { params }
})
},
queryMenuTree(isCommunity = false): Promise<any[]> {
return new Promise(async (res) => {
//过滤非集成的菜单

View File

@ -23,9 +23,12 @@ export const useUserInfo = defineStore('userInfo', {
orgList: [],
roleList: [],
telephone: '',
email: ''
email: '',
avatar: ''
},
alarmUpdateCount: 0
alarmUpdateCount: 0,
tabKey: 'HomeView', // 个人中心的tabKey,
messageInfo: {}, // 站内信的row
}),
actions: {

View File

@ -36,7 +36,7 @@
</div>
</div>
</template>
<j-empty style="margin: 200px 0;" />
<j-empty v-else style="margin: 200px 0;" />
</div>
</div>
</template>

View File

@ -101,6 +101,7 @@
<script setup lang="ts">
import { updateMeInfo_api } from '@/api/account/center';
import { onlyMessage } from '@/utils/comm';
import { cloneDeep } from 'lodash-es';
const emits = defineEmits(['save', 'close']);
const props = defineProps({
@ -110,9 +111,10 @@ const props = defineProps({
},
});
const loading = ref(false);
const form = ref(props.data);
const form = ref<any>(cloneDeep(props.data));
const formRef = ref<any>();
const handleOk = () => {
formRef.value?.validate().then(() => {
loading.value = true;

View File

@ -7,47 +7,71 @@
@cancel="emits('update:visible', false)"
class="view-dialog-container"
>
<j-row v-if="data?.targetType === 'device'">
<j-col :span="4" class="label">告警设备</j-col>
<j-col :span="8" class="value">
{{ data?.targetName || '' }}
</j-col>
<j-col :span="4" class="label">设备ID</j-col>
<j-col :span="8" class="value">
{{ data?.targetId || '' }}
</j-col>
</j-row>
<j-row>
<j-col :span="4" class="label">告警名称</j-col>
<j-col :span="8" class="value">
{{ data?.alarmName || data?.alarmConfigName || '' }}
</j-col>
<j-col :span="4" class="label">告警时间</j-col>
<j-col :span="8" class="value">
{{ moment(data?.alarmTime).format('YYYY-MM-DD HH:mm:ss') }}
</j-col>
<template v-if="type === 'alarm'">
<j-row v-if="data?.topicProvider === 'alarm-device'">
<j-col :span="4" class="label">告警设备</j-col>
<j-col :span="8" class="value">
{{ data?.targetName || '' }}
</j-col>
<j-col :span="4" class="label">设备ID</j-col>
<j-col :span="8" class="value">
{{ data?.targetId || '' }}
</j-col>
</j-row>
<j-row>
<j-col :span="4" class="label">告警名称</j-col>
<j-col :span="8" class="value">
{{ data?.alarmName || data?.alarmConfigName || '' }}
</j-col>
<j-col :span="4" class="label">告警时间</j-col>
<j-col :span="8" class="value">
{{ dayjs(data?.alarmTime).format('YYYY-MM-DD HH:mm:ss') }}
</j-col>
<j-col :span="4" class="label">告警级别</j-col>
<j-col :span="8" class="value">
{{ (levelList.length > 0 && getLevelLabel(data.level)) || '' }}
</j-col>
<j-col :span="4" class="label">告警说明</j-col>
<j-col :span="8" class="value">{{ data?.description || '' }}</j-col>
<j-col :span="4" class="label">告警级别</j-col>
<j-col :span="8" class="value">
{{
(levelList.length > 0 && getLevelLabel(data.level)) ||
''
}}
</j-col>
<j-col :span="4" class="label">告警说明</j-col>
<j-col :span="8" class="value">{{
data?.description || ''
}}</j-col>
<j-col
:span="4"
class="label"
style="display: flex; height: 440px; align-items: center"
>告警流水</j-col
>
<j-col
:span="20"
class="value"
style="max-height: 440px; overflow: auto"
>
<JsonViewer :value="JSON.parse(data?.alarmInfo || '{}')" />
</j-col>
</j-row>
<j-col
:span="4"
class="label"
style="display: flex; height: 440px; align-items: center"
>告警流水</j-col
>
<j-col
:span="20"
class="value"
style="max-height: 440px; overflow: auto"
>
<JsonViewer :value="JSON.parse(data?.alarmInfo || '{}')" />
</j-col>
</j-row>
</template>
<template v-else>
<j-row>
<j-col
:span="4"
class="label"
style="display: flex; height: 440px; align-items: center"
>通知流水</j-col
>
<j-col
:span="20"
class="value"
style="max-height: 440px; overflow: auto"
>
<JsonViewer :value="JSON.parse(data?.alarmInfo || '{}')" />
</j-col>
</j-row>
</template>
</j-modal>
</template>
@ -55,14 +79,17 @@
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
import { queryLevel as queryLevel_api } from '@/api/rule-engine/config';
import moment from 'moment';
import dayjs from 'dayjs';
const emits = defineEmits(['update:visible']);
const props = defineProps<{
visible: boolean;
data: any;
type: string;
}>();
const levelList = ref<any[]>([]);
const data = computed(() => {
if (props.data.detailJson) return JSON.parse(props.data.detailJson);
else return props.data?.detail || props.data;

View File

@ -14,12 +14,8 @@
:request="getList_api"
model="TABLE"
:params="queryParams"
:bodyStyle="{padding: 0}"
:defaultParams="{
sorts: [{
name: 'notifyTime', order: 'desc'
}]
}"
:bodyStyle="{ padding: 0 }"
:defaultParams="defaultParams"
>
<template #headerTitle>
<j-button type="primary">全部已读</j-button>
@ -29,7 +25,7 @@
</template>
<template #notifyTime="slotProps">
{{
moment(slotProps.notifyTime).format(
dayjs(slotProps.notifyTime).format(
'YYYY-MM-DD HH:mm:ss',
)
}}
@ -54,7 +50,7 @@
? '未读'
: '已读'
}`,
onConfirm: () => table.changeStatus(slotProps),
onConfirm: () => changeStatus(slotProps),
}"
:tooltip="{
title:
@ -70,18 +66,18 @@
:tooltip="{
title: '查看',
}"
@click="table.view(slotProps)"
@click="view(slotProps)"
>
<AIcon type="SearchOutlined" />
</PermissionButton>
</j-space>
</template>
</j-pro-table>
<ViewDialog
v-if="viewVisible"
v-model:visible="viewVisible"
:data="viewItem"
:type="type"
/>
</div>
</page-container>
@ -94,16 +90,37 @@ import {
getList_api,
changeStatus_api,
} from '@/api/account/notificationRecord';
import { getTypeList_api } from '@/api/account/notificationSubscription';
import { optionItem } from '@/views/rule-engine/Scene/typings';
import { dictItemType } from '@/views/system/DataSource/typing';
import moment from 'moment';
import dayjs from 'dayjs';
import { message } from 'ant-design-vue';
import { useUserInfo } from '@/store/userInfo';
import { useRouterParams } from '@/utils/hooks/useParams';
import dayjs from 'dayjs'
import { getTypeList_api } from '@/api/account/notificationSubscription';
const user = useUserInfo();
const props = defineProps({
type: {
type: String,
default: '',
},
});
const getType = computed(() => {
if (props.type === 'system-business') {
return ['device-transparent-codec'];
} else if (props.type === 'system-monitor') {
return ['system-event'];
} else {
return [
'alarm-product',
'alarm-device',
'alarm-other',
'alarm-org',
'alarm',
];
}
});
const { updateAlarm } = useUserInfo();
const columns = [
{
title: '类型',
@ -114,11 +131,13 @@ const columns = [
options: () =>
getTypeList_api().then((resp: any) =>
resp.result
.map((item: dictItemType) => ({
.map((item: any) => ({
label: item.name,
value: item.id,
}))
.filter((item: optionItem) => item.value === 'alarm'),
.filter((item: any) =>
[...getType.value].includes(item?.value),
),
),
},
scopedSlots: true,
@ -139,7 +158,7 @@ const columns = [
dataIndex: 'notifyTime',
key: 'notifyTime',
search: {
type: 'date'
type: 'date',
},
scopedSlots: true,
ellipsis: true,
@ -173,37 +192,59 @@ const columns = [
width: '200px',
},
];
const queryParams = ref({});
const tableRef = ref();
const table = {
changeStatus: (row: any) => {
const type = row.state.value === 'read' ? '_unread' : '_read';
changeStatus_api(type, [row.id]).then((resp: any) => {
if (resp.status === 200) {
message.success('操作成功!');
table.refresh();
updateAlarm();
}
});
},
view: (row: any) => {
console.log('row: ', row);
viewItem.value = row;
viewVisible.value = true;
},
refresh: () => {
tableRef.value && tableRef.value.reload();
},
};
const viewVisible = ref<boolean>(false);
const viewItem = ref<any>({});
const routerParams = useRouterParams();
const defaultParams = {
sorts: [{ name: 'notifyTime', order: 'desc' }],
terms: [
{
terms: [
{
column: 'topicProvider',
value: getType.value,
termType: 'in',
},
],
type: 'and',
},
],
};
const queryParams = ref({});
const tableRef = ref();
const view = (row: any) => {
viewItem.value = row;
viewVisible.value = true;
};
const refresh = () => {
tableRef.value && tableRef.value.reload();
};
const changeStatus = (row: any) => {
const type = row.state.value === 'read' ? '_unread' : '_read';
changeStatus_api(type, [row.id]).then((resp: any) => {
if (resp.status === 200) {
message.success('操作成功!');
refresh();
user.updateAlarm();
}
});
};
watchEffect(() => {
if(user.messageInfo?.id) {
view(user.messageInfo)
}
})
onMounted(() => {
if (routerParams.params?.value.row) {
table.view(routerParams.params?.value.row);
view(routerParams.params?.value.row);
}
});
</script>

View File

@ -1,155 +0,0 @@
<template>
<j-modal
visible
:title="props.data.id ? '编辑' : '新增'"
width="865px"
:confirmLoading="loading"
@ok="confirm"
@cancel="emits('update:visible', false)"
>
<j-form :model="form" layout="vertical" ref="formRef">
<j-form-item
label="名称"
name="subscribeName"
:rules="[
{ required: true, message: '请输入名称' },
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<j-input
v-model:value="form.subscribeName"
placeholder="请输入名称"
/>
</j-form-item>
<j-row :gutter="24">
<j-col :span="12">
<j-form-item
label="类型"
name="topicProvider"
:rules="[{ required: true, message: '请选择类型' }]"
>
<j-select
v-model:value="form.topicProvider"
placeholder="请选择类型"
:options="typeList"
/>
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="告警规则"
:name="['topicConfig', 'alarmConfigId']"
:rules="[{ required: true, message: '请选择告警规则' }]"
>
<j-select
:value="form.topicConfig?.alarmConfigId?.split(',')"
:options="alarmList"
placeholder="请选择告警规则"
mode="multiple"
@change="onSelect"
></j-select>
</j-form-item>
</j-col>
</j-row>
<j-form-item
name="notice"
label="通知方式"
:rules="[{ required: true, message: '请选择通知方式' }]"
>
<j-checkbox-group
v-model:value="form.notice"
name="checkboxgroup"
:options="[
{
label: '站内通知',
value: 1,
},
]"
/>
</j-form-item>
</j-form>
</j-modal>
</template>
<script setup lang="ts">
import { rowType } from '../typing';
import {
getTypeList_api,
getAlarmList_api,
save_api,
} from '@/api/account/notificationSubscription';
import { optionsType } from '@/views/system/Department/typing';
import { dictItemType } from '@/views/system/DataSource/typing';
import { optionItem } from '@/views/rule-engine/Scene/typings';
import { FormInstance, message } from 'ant-design-vue';
const emits = defineEmits(['ok', 'update:visible']);
const props = defineProps<{
visible: boolean;
data: rowType;
}>();
const loading = ref(false);
const initForm = {
subscribeName: '',
topicConfig: {},
notice: [1],
};
const formRef = ref<FormInstance>();
const form = ref({
...initForm,
...props.data,
});
const confirm = () => {
formRef.value &&
formRef.value.validate().then(() => {
loading.value = true;
save_api(form.value)
.then((resp) => {
if (resp.status === 200) {
message.success('操作成功');
emits('ok');
emits('update:visible', false);
}
})
.finally(() => (loading.value = false));
});
};
const typeList = ref<optionsType>([]);
const alarmList = ref<optionsType>([]);
init();
function init() {
getTypeList_api().then((resp: any) => {
if (resp.status === 200)
typeList.value = resp.result
.map((item: dictItemType) => ({
label: item.name,
value: item.id,
}))
.filter((item: optionItem) => item.value === 'alarm');
});
getAlarmList_api().then((resp: any) => {
if (resp.status === 200)
alarmList.value = resp.result.map((item: dictItemType) => ({
label: item.name,
value: item.id,
}));
});
}
function onSelect(keys: string[], items: optionsType) {
form.value.topicConfig = {
alarmConfigId: keys.length ? keys.join(',') : undefined,
alarmConfigName: items.length ? items.map((item) => item.label).join(',') : undefined,
};
}
</script>
<style scoped></style>

View File

@ -1,203 +0,0 @@
<template>
<page-container>
<div class="notification-subscription-container">
<pro-search
:columns="columns"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table
ref="tableRef"
:columns="columns"
:request="getNoticeList_api"
model="TABLE"
:params="queryParams"
:defaultParams="{
sorts: [{ name: 'notifyTime', order: 'desc' }],
}"
>
<template #headerTitle>
<PermissionButton type="primary" @click="table.edit()">
<AIcon type="PlusOutlined" />新增
</PermissionButton>
</template>
<template #alarmConfigName="slotProps">
{{ slotProps.topicConfig.alarmConfigName }}
</template>
<template #state="slotProps">
<BadgeStatus
:status="slotProps.state.value"
:text="slotProps.state.text"
:statusNames="{
enabled: 'success',
disabled: 'error',
}"
></BadgeStatus>
</template>
<template #action="slotProps">
<j-space :size="16">
<PermissionButton
type="link"
:tooltip="{
title: '编辑',
}"
@click="table.edit(slotProps)"
>
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton
type="link"
:popConfirm="{
title: `确定${
slotProps.state.value === 'enabled'
? '禁用'
: '启用'
}`,
onConfirm: () => table.changeStatus(slotProps),
}"
:tooltip="{
title:
slotProps.state.value === 'enabled'
? '禁用'
: '启用',
}"
>
<AIcon
:type="
slotProps.state.value === 'enabled'
? 'StopOutlined'
: 'PlayCircleOutlined'
"
/>
</PermissionButton>
<PermissionButton
type="link"
:tooltip="{
title:
slotProps.state.value === 'enabled'
? '请先禁用,再删除'
: '删除',
}"
:popConfirm="{
title: `确认删除?`,
onConfirm: () => table.delete(slotProps),
}"
:disabled="slotProps.state.value === 'enabled'"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</j-space>
</template>
</j-pro-table>
<EditDialog
v-if="dialogVisible"
v-model:visible="dialogVisible"
:data="table.seletctRow"
@ok="table.refresh"
/>
</div>
</page-container>
</template>
<script setup lang="ts" name="NotificationSubscription">
import PermissionButton from '@/components/PermissionButton/index.vue';
import EditDialog from './components/EditDialog.vue';
import {
getNoticeList_api,
changeStatus_api,
remove_api,
} from '@/api/account/notificationSubscription';
import { rowType } from './typing';
import { message } from 'ant-design-vue';
const columns = [
{
title: '名称',
dataIndex: 'subscribeName',
key: 'subscribeName',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '类型',
dataIndex: 'topicName',
key: 'topicName',
scopedSlots: true,
ellipsis: true,
},
{
title: '告警规则',
dataIndex: 'alarmConfigName',
key: 'alarmConfigName',
scopedSlots: true,
ellipsis: true,
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
ellipsis: true,
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
ellipsis: true,
scopedSlots: true,
width: '200px',
},
];
const queryParams = ref({});
const dialogVisible = ref<boolean>(false);
const tableRef = ref();
const table = {
seletctRow: ref<any>({}),
edit: (row?: rowType) => {
table.seletctRow = {
...(row || ({} as any)),
};
dialogVisible.value = true;
},
changeStatus: (row: rowType) => {
const status = row.state.value === 'enabled' ? '_disabled' : '_enabled';
changeStatus_api(row.id as string, status).then((resp) => {
if (resp.status === 200) {
message.success('操作成功!');
table.refresh();
} else message.warning('操作失败!');
});
},
delete: (row: rowType) => {
remove_api(row.id as string).then((resp) => {
if (resp.status === 200) {
message.success('操作成功!');
table.refresh();
} else message.warning('操作失败!');
});
},
refresh: () => {
tableRef.value && tableRef.value.reload();
},
};
</script>
<style lang="less" scoped>
.notification-subscription-container {
:deep(.ant-table-tbody) {
.ant-table-cell {
.ant-space-item {
.ant-btn-link {
padding: 0;
}
}
}
}
}
</style>

View File

@ -1,14 +0,0 @@
export type rowType = {
id?: string;
locale: string;
state: { text: string, value: "enabled" | 'disabled' };
subscribeName: string;
subscriber: string;
subscriberType: string;
topicConfig: { alarmConfigId?: string, alarmConfigName?: string };
alarmConfigId: string;
alarmConfigName: stirng;
topicName: string;
topicProvider: string| undefined;
notice?:any[]
}

View File

@ -1,29 +1,59 @@
<template>
<div style="margin-top: 24px;">
<j-tabs tab-position="left" v-if="tabs.length">
<j-tab-pane v-for="item in tabs" :key="item.key" :tab="item.tab">
<NotificationRecord />
<div style="margin-top: 24px">
<j-tabs
tab-position="left"
v-if="tabs.length"
:destroyInactiveTabPane="true"
>
<j-tab-pane v-for="item in tabs" :key="item.provider" :tab="item.name">
<NotificationRecord :type="item.provider" />
</j-tab-pane>
</j-tabs>
<j-empty v-else />
<j-empty v-else style="margin: 200px 0" />
</div>
</template>
<script lang="ts" setup>
import NotificationRecord from './components/NotificationRecord/index.vue'
import NotificationRecord from './components/NotificationRecord/index.vue';
import { initData } from '../data';
import { getAllNotice } from '@/api/account/center';
const tabs = [
{
key: '1',
tab: '告警'
},
{
key: '2',
tab: '系统运维'
},
{
key: '3',
tab: '业务监控'
}
]
const tabs = ref<any[]>([]);
const queryTypeList = () => {
getAllNotice().then((resp: any) => {
if (resp.status === 200) {
const arr = initData
.map((item: any) => {
const _child = item.children.map((i: any) => {
const _item = (resp.result || []).find(
(t: any) => t?.provider === i?.provider,
);
return {
...i,
..._item,
};
});
return {
...item,
children: _child,
};
})
.filter((it: any) => {
return it.children.filter((lt: any) => lt?.id)?.length;
})
.map((item) => {
return {
...item,
children: item.children.filter((lt: any) => lt?.id),
};
});
tabs.value = arr
}
});
};
onMounted(() => {
queryTypeList();
});
</script>

View File

@ -18,49 +18,85 @@
<template #card="slotProps">
<div class="box-item">
<div class="box-item-img">
<j-popover
trigger="click"
:visible="show?.[slotProps?.id]"
@visibleChange="onVisibleChange(slotProps)"
>
<img
:src="
getImage(
`/notice/${noticeType.get(
<j-dropdown placement="top" :trigger="['click']">
<!-- :visible="show?.[slotProps?.id]"
@visibleChange="onVisibleChange(slotProps)" -->
<div>
<img
:src="
iconMap.get(
slotProps?.channelProvider,
)}.png`,
)
"
/>
<template
#content
v-if="
)
"
/>
<div
:class="{
disabled: !notifyChannels?.includes(
slotProps?.id,
),
}"
></div>
</div>
<!-- v-if="
notifyChannels?.includes(
slotProps?.id,
) &&
slotProps?.channelProvider !==
'inside-mail'
"
>
<div>
<PermissionButton
type="link"
:hasPermission="true"
@click="onUnSubscribe(slotProps)"
" -->
<template #overlay>
<j-menu>
<j-menu-item
v-if="
!notifyChannels?.includes(
slotProps?.id,
)
"
>
取消订阅
</PermissionButton>
</div>
<div>
<PermissionButton
type="link"
:hasPermission="true"
>
更换接收账号
</PermissionButton>
</div>
<PermissionButton
type="link"
:hasPermission="true"
@click="
onCheckChange(slotProps)
"
>
订阅
</PermissionButton>
</j-menu-item>
<template v-else>
<j-menu-item>
<PermissionButton
type="link"
:hasPermission="true"
@click="
onUnSubscribe(slotProps)
"
>
取消订阅
</PermissionButton>
</j-menu-item>
<j-menu-item
v-if="
slotProps.channelProvider !==
'inside-mail'
"
>
<PermissionButton
type="link"
:hasPermission="true"
@click="
onAccountChange(
slotProps,
)
"
>
更换接收账号
</PermissionButton>
</j-menu-item>
</template>
</j-menu>
</template>
</j-popover>
</j-dropdown>
<div class="box-item-checked">
<j-checkbox
:checked="
@ -68,13 +104,6 @@
"
></j-checkbox>
</div>
<div
:class="{
disabled: !notifyChannels?.includes(
slotProps?.id,
),
}"
></div>
</div>
<div class="box-item-text">
{{ slotProps?.name }}
@ -91,26 +120,36 @@
:current="current"
@close="visible = false"
/>
<EditInfo
v-if="editInfoVisible"
:data="user.userInfos"
@close="editInfoVisible = false"
@save="onSave"
/>
</template>
<script lang="ts" setup>
import { getImage, onlyMessage } from '@/utils/comm';
import MCarousel from '@/components/MCarousel/index.vue';
import Unsubscribe from './Unsubscribe.vue';
import { remove_api, save_api } from '@/api/account/notificationSubscription';
import {
getIsBindThird,
save_api,
} from '@/api/account/notificationSubscription';
import { useUserInfo } from '@/store/userInfo';
import EditInfo from '../../EditInfo/index.vue';
const noticeType = new Map();
noticeType.set('notifier-dingTalk', 'dingtalk');
noticeType.set('notifier-weixin', 'wechat');
noticeType.set('notifier-email', 'email');
noticeType.set('notifier-voice', 'voice');
noticeType.set('notifier-sms', 'sms');
noticeType.set('inside-mail', 'inside-mail');
const iconMap = new Map();
iconMap.set('notifier-dingTalk', getImage('/notice/dingtalk.png'));
iconMap.set('notifier-weixin', getImage('/notice/wechat.png'));
iconMap.set('notifier-email', getImage('/notice/email.png'));
iconMap.set('notifier-voice', getImage('/notice/voice.png'));
iconMap.set('notifier-sms', getImage('/notice/sms.png'));
iconMap.set('inside-mail', getImage('/notice/inside-mail.png'));
const current = ref<any>({});
const visible = ref<boolean>(false);
const show = ref<any>()
const editInfoVisible = ref<boolean>(false);
const user = useUserInfo();
@ -154,8 +193,8 @@ const onUnSubscribe = async (obj: any) => {
// onlyMessage('');
// emits('refresh');
// }
const _set = new Set(props.subscribe?.notifyChannels || [])
_set.delete(obj?.id)
const _set = new Set(props.subscribe?.notifyChannels || []);
_set.delete(obj?.id);
const _obj = {
subscribeName: obj.name,
topicProvider: props.data?.provider,
@ -188,6 +227,18 @@ const onCheckChange = async (_data: any) => {
}
} else {
//
const resp: any = await getIsBindThird();
if (resp.status === 200) {
const _item = (resp?.result || []).find((item: any) => {
return (
_data?.channelConfiguration?.notifierId ===
item?.provider
);
});
if (_item) {
_bind = true;
}
}
}
}
if (_data?.channelProvider === 'inside-mail' || _bind) {
@ -198,18 +249,25 @@ const onCheckChange = async (_data: any) => {
}
};
const onVisibleChange = (_data: any) => {
show.value = {}
if (notifyChannels.value?.includes(_data?.id)) {
if (_data?.channelProvider === 'inside-mail') {
onUnSubscribe(_data);
} else {
show.value[_data.id] = true
}
//
const onAccountChange = (_data: any) => {
current.value = _data;
if (
['notifier-voice', 'notifier-sms', 'notifier-email'].includes(
_data?.channelProvider,
)
) {
editInfoVisible.value = true;
} else {
onCheckChange(_data)
visible.value = true;
}
};
const onSave = () => {
editInfoVisible.value = false;
//
onUnSubscribe(current.value);
};
</script>
<style lang="less" scoped>

View File

@ -1,10 +1,34 @@
<template>
<j-modal visible @cancel="emit('close')">
<j-modal width="900px" visible @cancel="emit('close')">
<template v-if="getType === 'notifier-dingTalk'">
<div class="tip">请先绑定钉钉账号</div>
<!-- <div class="tip">请先绑定钉钉账号</div> -->
<j-spin :spinning="loading">
<div class="code">
<iframe
id="notifier_iframe"
class="code-item"
width="100%"
height="100%"
:src="url"
v-if="!loading"
></iframe>
</div>
</j-spin>
</template>
<template v-else-if="getType === 'notifier-weixin'">
<div class="tip">请先绑定企业微信账号</div>
<!-- <div class="tip">请先绑定企业微信账号</div> -->
<j-spin :spinning="loading">
<div class="code">
<iframe
id="notifier_iframe"
class="code-item"
width="100%"
height="100%"
:src="url"
v-if="!loading"
></iframe>
</div>
</j-spin>
</template>
<template v-else-if="getType === 'notifier-email'">
<div class="tip">请先绑定邮箱</div>
@ -14,45 +38,184 @@
</template>
<template #footer>
<j-button @click="emit('close')">取消</j-button>
<j-button @click="onBind" type="primary" v-if="['notifier-email', 'notifier-voice', 'notifier-sms'].includes(getType)">立即绑定</j-button>
<j-button
@click="onBind"
type="primary"
v-if="
[
'notifier-email',
'notifier-voice',
'notifier-sms',
].includes(getType)
"
>立即绑定</j-button
>
<j-button v-else @click="emit('close')">确定</j-button>
</template>
<EditInfo v-if="editInfoVisible" :data="user.userInfos" @close="editInfoVisible = false" @save="onSave" />
<EditInfo
v-if="editInfoVisible"
:data="user.userInfos"
@close="editInfoVisible = false"
@save="onSave"
/>
</j-modal>
</template>
<script lang="ts" setup>
import {
bindThirdParty,
getDingTalkOAuth2,
getUserBind,
getWechatOAuth2,
} from '@/api/account/notificationSubscription';
import { useUserInfo } from '@/store/userInfo';
import EditInfo from '../../EditInfo/index.vue'
import EditInfo from '../../EditInfo/index.vue';
const user = useUserInfo();
const emit = defineEmits(['close', 'save']);
const props = defineProps({
data: { //
data: {
//
type: Object,
default: () => {},
},
current: { //
current: {
//
type: Object,
default: () => {},
},
});
const editInfoVisible = ref<boolean>(false)
const editInfoVisible = ref<boolean>(false);
const url = ref<string>('');
const loading = ref<boolean>(false);
const getType = computed(() => {
return props.current?.channelProvider
})
return props.current?.channelProvider;
});
const onBind = () => {
editInfoVisible.value = true
}
editInfoVisible.value = true;
};
const onSave = () => {
editInfoVisible.value = false
emit('save', props.current)
emit('close')
}
editInfoVisible.value = false;
emit('save', props.current);
emit('close');
};
const onBindHandle = (
type: 'notifier-weixin' | 'notifier-dingTalk',
code: string,
) => {
getUserBind(type === 'notifier-dingTalk' ? 'dingtalk' : 'wechat', {
authCode: code,
configId: props.current?.channelConfiguration.notifierId,
})
.then((resp) => {
if (resp.status === 200) {
const _bindCode = (resp?.result || '') as string;
if (_bindCode) {
bindThirdParty(
type,
props.current?.channelConfiguration.notifierId,
_bindCode,
)
.then((response) => {
if (response.status === 200) {
//
emit('save', props.current);
emit('close');
}
})
.finally(() => {
loading.value = false;
});
}
}
})
.catch(() => {
loading.value = false;
});
};
const updateIframeStyle = () => {
const iframe = document.querySelector(
'#notifier_iframe',
) as HTMLIFrameElement;
iframe.onload = () => {
const currentUrl = iframe?.contentWindow?.location?.search || '';
let authCode = '';
if (currentUrl.startsWith('?')) {
currentUrl
.substring(1)
.split('&')
.map((item) => {
if (
props.current?.channelProvider === 'notifier-dingTalk'
) {
if (item.split('=')?.[0] === 'authCode') {
authCode = item.split('=')?.[1] || '';
}
} else {
if (item.split('=')?.[0] === 'code') {
authCode = item.split('=')?.[1] || '';
}
}
});
}
if (authCode) {
loading.value = true;
onBindHandle(props.current?.channelProvider, authCode);
}
};
};
const handleSearch = async () => {
if (props.current?.channelProvider === 'notifier-weixin') {
loading.value = true;
const resp = await getWechatOAuth2(
props.current?.channelConfiguration.notifierId,
props.current?.channelConfiguration.templateId,
location.href
).finally(() => {
loading.value = false;
});
if (resp.status === 200) {
url.value = resp.result as string;
nextTick(() => {
updateIframeStyle();
});
}
}
if (props.current?.channelProvider === 'notifier-dingTalk') {
loading.value = true;
const resp = await getDingTalkOAuth2(
props.current?.channelConfiguration.notifierId,
location.href,
).finally(() => {
loading.value = false;
});
if (resp.status === 200) {
url.value = resp.result as string;
nextTick(() => {
updateIframeStyle();
});
}
}
};
watch(
() => props.current,
() => {
handleSearch();
},
{
immediate: true,
deep: true,
},
);
</script>
<style lang="less" scoped>
@ -61,6 +224,17 @@ const onSave = () => {
margin: 80px 0;
text-align: center;
font-size: 14px;
color: #7F7F7F;
color: #7f7f7f;
}
.code {
width: 100%;
height: 500px;
display: flex;
justify-content: center;
.code-item {
border: none;
}
}
</style>

View File

@ -56,60 +56,9 @@
<script lang="ts" setup>
import { getAllNotice } from '@/api/account/center';
import { getNoticeList_api } from '@/api/account/notificationSubscription';
import { initData } from '../data';
import Item from './components/Item.vue';
const initData = [
{
provider: 'alarm',
name: '告警',
children: [
{
provider: 'alarm-product',
name: '产品告警',
description:
'当产品类型的告警被触发时,你将在已订阅的方式中收到通知',
},
{
provider: 'alarm-device',
name: '设备告警',
description:
'当设备类型的告警被触发时,你将在已订阅的方式中收到通知',
},
{
provider: 'alarm-org',
name: '部门告警',
description:
'当部门类型的告警被触发时,你将在已订阅的方式中收到通知',
},
{
provider: 'alarm-other',
name: '其他告警',
description:
'当其他类型的告警被触发时,你将在已订阅的方式中收到通知',
},
],
},
{
provider: 'system-monitor',
name: '系统监控',
children: [
{
provider: 'system-event',
name: '系统运行异常',
},
],
},
{
provider: 'system-business',
name: '业务监控',
children: [
{
provider: 'device-transparent-codec',
name: '透传消息解析异常',
},
],
},
];
const subscribe = ref<any[]>([]);
const dataSource = ref<any[]>([]);
const activeKey = ref<string[]>(['alarm', 'system-monitor', 'system-business']);
@ -120,8 +69,8 @@ const handleSearch = () => {
getAllNotice().then((resp: any) => {
if (resp.status === 200) {
const arr = initData
.map((item) => {
const _child = item.children.map((i) => {
.map((item: any) => {
const _child = item.children.map((i: any) => {
const _item = (resp.result || []).find(
(t: any) => t?.provider === i?.provider,
);

View File

@ -0,0 +1,246 @@
<template>
<div class="upload-image-warp">
<div class="upload-image-border" :style="borderStyle">
<j-upload
name="file"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:before-upload="beforeUpload"
@change="handleChange"
:action="FILE_UPLOAD"
:headers="{
'X-Access-Token': LocalStore.get(TOKEN_KEY),
}"
v-bind="props"
>
<div class="upload-image-content">
<template v-if="imageUrl">
<img
:src="imageUrl"
height="100%"
class="upload-image"
/>
</template>
<template v-else>
<AIcon type="UserOutlined" style="font-size: 20px" />
</template>
<div class="upload-image-mask">更换</div>
</div>
</j-upload>
<div class="upload-loading-mask" v-if="props.disabled"></div>
<div class="upload-loading-mask" v-if="imageUrl && loading">
<AIcon type="LoadingOutlined" style="font-size: 20px" />
</div>
</div>
</div>
<ImageCropper
v-if="cropperVisible"
:img="cropperImg"
@cancel="cropperVisible = false"
@ok="saveImage"
/>
</template>
<script lang="ts" setup name='JProUpload'>
import { UploadChangeParam, UploadProps } from 'ant-design-vue';
import { message } from 'jetlinks-ui-components';
import { FILE_UPLOAD } from '@/api/comm';
import { TOKEN_KEY } from '@/utils/variable';
import { getBase64, LocalStore } from '@/utils/comm';
import { CSSProperties } from 'vue';
import ImageCropper from '@/components/Upload/Cropper.vue';
type Emits = {
(e: 'update:modelValue', data: string): void;
(e: 'change', data: string): void;
};
interface JUploadProps extends UploadProps {
modelValue: string;
disabled?: boolean;
types?: string[];
errorMessage?: string;
size?: number;
borderStyle?: CSSProperties;
}
const emit = defineEmits<Emits>();
const props: JUploadProps = defineProps({
modelValue: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
accept: {
type: String,
default: undefined,
},
borderStyle: {
type: Object,
default: undefined,
},
});
const loading = ref<boolean>(false);
const imageUrl = ref<string>(props?.modelValue || '');
const imageTypes = props.types ? props.types : ['image/jpeg', 'image/png'];
const cropperImg = ref();
const cropperVisible = ref(false);
watch(
() => props.modelValue,
(newValue) => {
imageUrl.value = newValue;
},
{
deep: true,
immediate: true,
},
);
const handleChange = (info: UploadChangeParam) => {
if (info.file.status === 'uploading') {
loading.value = true;
}
if (info.file.status === 'done') {
imageUrl.value = info.file.response?.result;
loading.value = false;
emit('update:modelValue', info.file.response?.result);
emit('change', info.file.response?.result);
}
if (info.file.status === 'error') {
loading.value = false;
message.error('上传失败');
}
};
const beforeUpload = (file: UploadProps['fileList'][number]) => {
const isType = imageTypes.includes(file.type);
const maxSize = props.size || 2; //
if (!isType) {
if (props.errorMessage) {
message.error(props.errorMessage);
} else {
message.error(`请上传正确格式的图片`);
}
return false;
}
const isSize = file.size / 1024 / 1024 < maxSize;
if (!isSize) {
message.error(`图片大小必须小于${maxSize}M`);
}
getBase64(file, (base64Url) => {
cropperImg.value = base64Url;
cropperVisible.value = true;
});
return false;
};
const saveImage = (url: string) => {
cropperVisible.value = false;
imageUrl.value = url;
emit('update:modelValue', url);
emit('change', url);
};
</script>
<style lang="less" scoped>
@border: 1px dashed @border-color-base;
@mask-color: rgba(#000, 0.35);
@with: 66px;
@height: 66px;
.flex-center() {
align-items: center;
justify-content: center;
}
.upload-image-warp {
display: flex;
justify-content: flex-start;
.upload-image-border {
position: relative;
width: @with;
height: @height;
overflow: hidden;
transition: all 0.3s;
:deep(.ant-upload-picture-card-wrapper) {
width: 100%;
height: 100%;
}
:deep(.ant-upload) {
width: 100%;
height: 100%;
}
:deep(.ant-upload-select-picture-card) {
background: none !important;
border: none !important;
}
.upload-image-content {
.flex-center();
border-radius: 50%;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background-color: rgba(#000, 0.06);
cursor: pointer;
// padding: 8px;
.upload-image-mask {
.flex-center();
position: absolute;
top: 0;
left: 0;
display: none;
width: 100%;
height: 100%;
color: #fff;
border-radius: 50%;
font-size: 16px;
background-color: @mask-color;
}
.upload-image {
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
&:hover .upload-image-mask {
display: flex;
}
}
}
.upload-loading-mask {
.flex-center();
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
color: #fff;
background-color: @mask-color;
}
}
</style>

View File

@ -0,0 +1,54 @@
const initData: any[] = [
{
provider: 'alarm',
name: '告警',
children: [
{
provider: 'alarm-product',
name: '产品告警',
description:
'当产品类型的告警被触发时,你将在已订阅的方式中收到通知',
},
{
provider: 'alarm-device',
name: '设备告警',
description:
'当设备类型的告警被触发时,你将在已订阅的方式中收到通知',
},
{
provider: 'alarm-org',
name: '部门告警',
description:
'当部门类型的告警被触发时,你将在已订阅的方式中收到通知',
},
{
provider: 'alarm-other',
name: '其他告警',
description:
'当其他类型的告警被触发时,你将在已订阅的方式中收到通知',
},
],
},
{
provider: 'system-monitor',
name: '系统监控',
children: [
{
provider: 'system-event',
name: '系统运行异常',
},
],
},
{
provider: 'system-business',
name: '业务监控',
children: [
{
provider: 'device-transparent-codec',
name: '透传消息解析异常',
},
],
},
];
export { initData };

View File

@ -138,7 +138,7 @@
<div class="card" v-if="isNoCommunity">
<h3>绑定三方账号</h3>
<div class="content">
<div class="account-card" v-for="item in bindList">
<div class="account-card" v-for="item in bindList" :key="item.id">
<img
:src="item.logoUrl || getImage(bindIcon[item.provider])"
style="height: 50px;width: 50px"

View File

@ -4,19 +4,23 @@
<div class="person-header-item">
<div class="person-header-item-info">
<div class="person-header-item-info-left">
<j-avatar :size="64">
<template #icon
><AIcon type="UserOutlined"
/></template>
</j-avatar>
<UploadAvatar
:accept="
imageTypes && imageTypes.length
? imageTypes.toString()
: ''
"
:modelValue="user.userInfos?.avatar"
@change="onAvatarChange"
/>
</div>
<div class="person-header-item-info-right">
<div class="person-header-item-info-right-top">
<span>xx部门 · xx角色</span>
<span>{{ _org }}部门 · {{ _role }}角色</span>
</div>
<div class="person-header-item-info-right-info">
<div>用户名 {{user.userInfos?.username}}</div>
<div>账号ID {{user.userInfos?.id}}</div>
<div>用户名 {{ user.userInfos?.username }}</div>
<div>账号ID {{ user.userInfos?.id }}</div>
</div>
</div>
</div>
@ -27,7 +31,7 @@
@click="onActivated(item.key)"
v-for="item in list"
:type="
activeKey === item.key
user.tabKey === item.key
? 'primary'
: 'default'
"
@ -39,21 +43,25 @@
<div class="person-header-item-action-right">
<j-space :size="24">
<j-tooltip title="查看详情"
><j-button @click="visible = true" shape="circle"
><j-button
@click="visible = true"
shape="circle"
><AIcon
style="font-size: 24px"
type="FileSearchOutlined" /></j-button
></j-tooltip>
<j-tooltip title="编辑资料"
><j-button shape="circle"
@click="editInfoVisible = true"
><j-button
shape="circle"
@click="editInfoVisible = true"
><AIcon
style="font-size: 24px"
type="FormOutlined" /></j-button
></j-tooltip>
<j-tooltip title="修改密码"
><j-button shape="circle"
@click="editPasswordVisible = true"
><j-button
shape="circle"
@click="editPasswordVisible = true"
><AIcon
style="font-size: 24px"
type="LockOutlined" /></j-button
@ -67,14 +75,23 @@
<div class="person-content-item">
<FullPage>
<div class="person-content-item-content">
<component :is="tabs[activeKey]" />
<component :is="tabs[user.tabKey]" />
</div>
</FullPage>
</div>
</div>
<Detail v-if="visible" @close="visible = false"/>
<EditInfo v-if="editInfoVisible" :data="user.userInfos" @close="editInfoVisible = false" @save="onSave" />
<EditPassword v-if="editPasswordVisible" @close="editPasswordVisible = false" @save="onPasswordSave" />
<Detail v-if="visible" @close="visible = false" />
<EditInfo
v-if="editInfoVisible"
:data="user.userInfos"
@close="editInfoVisible = false"
@save="onSave"
/>
<EditPassword
v-if="editPasswordVisible"
@close="editPasswordVisible = false"
@save="onPasswordSave"
/>
</div>
</template>
@ -87,6 +104,19 @@ import Detail from './components/Detail/index.vue';
import EditInfo from './components/EditInfo/index.vue';
import EditPassword from './components/EditPassword/index.vue';
import { useUserInfo } from '@/store/userInfo';
import UploadAvatar from './components/UploadAvatar/index.vue';
import { updateMeInfo_api } from '@/api/account/center';
import { onlyMessage } from '@/utils/comm';
import { useRouterParams } from '@/utils/hooks/useParams';
const imageTypes = reactive([
'image/jpeg',
'image/png',
'image/jpg',
'image/jfif',
'image/pjp',
'image/pjpeg',
]);
const user = useUserInfo();
@ -117,23 +147,59 @@ const tabs = {
StationMessage,
};
const activeKey = ref<KeyType>('HomeView');
const router = useRouterParams()
// const activeKey = ref<KeyType>('HomeView');
const visible = ref<boolean>(false);
const editInfoVisible = ref<boolean>(false);
const editPasswordVisible = ref<boolean>(false);
const onActivated = (_key: KeyType) => {
activeKey.value = _key;
user.tabKey = _key;
};
const _org = computed(() => {
return user.userInfos?.orgList
?.map((item: any) => {
return item?.name;
})
.join(',');
});
const _role = computed(() => {
return user.userInfos?.roleList
?.map((item: any) => {
return item?.name;
})
.join(',');
});
const onSave = () => {
user.getUserInfo()
editInfoVisible.value = false
}
user.getUserInfo();
editInfoVisible.value = false;
};
const onPasswordSave = () => {
editPasswordVisible.value = false
}
editPasswordVisible.value = false;
};
const onAvatarChange = (url: string) => {
updateMeInfo_api({
...user.userInfos,
avatar: url,
}).then((resp) => {
if (resp.status === 200) {
onlyMessage('操作成功', 'success');
user.getUserInfo();
}
});
};
watchEffect(() => {
if(router.params.value?.tabKey) {
user.tabKey = router.params.value?.tabKey
}
})
</script>
<style lang="less" scoped>

View File

@ -3924,91 +3924,101 @@ export default [
},
{
code: 'system/NoticeRule',
name: '通知规则',
name: '订阅管理',
owner: 'iot',
id: 'system/NoticeRule',
sortIndex: 11,
id: '522f790d4422a608d491bc9e2fa12b4e',
sortIndex: 12,
url: '/system/NoticeRule',
icon: 'icon-yingyongguanli',
showPage: ['application'],
icon: 'CopyOutlined',
showPage: ['notify-channel'],
permissions: [],
buttons: [
{
id: 'delete',
name: '删除',
permissions: [
// {
// permission: 'application',
// actions: ['query', 'delete'],
// },
{
permission: 'role',
actions: ['query'],
},
{
permission: 'notify-channel',
actions: ['save', 'delete'],
},
{
permission: 'notifier',
actions: ['query'],
},
{
permission: 'template',
actions: ['query'],
},
],
},
{
id: 'add',
name: '新增',
permissions: [
// {
// permission: 'role',
// actions: ['query'],
// },
// {
// permission: 'menu',
// actions: ['query'],
// },
// {
// permission: 'application',
// actions: ['query', 'save'],
// },
// {
// permission: 'open-api',
// actions: ['query', 'save', 'delete'],
// },
{
permission: 'role',
actions: ['query'],
},
{
permission: 'notify-channel',
actions: ['save'],
},
{
permission: 'notifier',
actions: ['query'],
},
{
permission: 'template',
actions: ['query'],
},
],
},
{
id: 'update',
name: '编辑',
permissions: [
// {
// permission: 'role',
// actions: ['query'],
// },
// {
// permission: 'menu',
// actions: ['query'],
// },
// {
// permission: 'application',
// actions: ['query', 'save'],
// },
// {
// permission: 'open-api',
// actions: ['query', 'save', 'delete'],
// },
],
},
{
id: 'view',
name: '查看',
permissions: [
// {
// permission: 'application',
// actions: ['query'],
// },
// {
// permission: 'role',
// actions: ['query'],
// },
{
permission: 'role',
actions: ['query'],
},
{
permission: 'notify-channel',
actions: ['save', 'query'],
},
{
permission: 'notifier',
actions: ['query'],
},
{
permission: 'template',
actions: ['query'],
},
],
},
{
id: 'action',
name: '启/禁用',
permissions: [
// {
// permission: 'application',
// actions: ['save'],
// },
{
permission: 'role',
actions: ['query'],
},
{
permission: 'notify-channel',
actions: ['save'],
},
{
permission: 'notifier',
actions: ['query'],
},
{
permission: 'template',
actions: ['query'],
},
],
},
],

View File

@ -5,7 +5,13 @@
{{ data?.name }}
</div>
<div>
<j-switch @change="onSwitchChange" :checked="checked" />
<j-tooltip :title="!action ? '暂无权限,请联系管理员' : ''">
<j-switch
:disabled="!action"
@change="onSwitchChange"
:checked="checked"
/>
</j-tooltip>
</div>
</div>
<div class="child-item-right" v-if="checked">
@ -17,15 +23,16 @@
<img
style="width: 100%"
:src="
getImage(
`/notice/${noticeType.get(
slotProps?.channelProvider,
)}.png`,
)
iconMap.get(slotProps?.channelProvider)
"
/>
</div>
<template #overlay v-if="slotProps?.channelProvider !== 'inside-mail'">
<template
#overlay
v-if="
slotProps?.channelProvider !== 'inside-mail'
"
>
<j-menu mode="">
<j-menu-item>
<PermissionButton
@ -40,7 +47,9 @@
<PermissionButton
@click="onEdit(slotProps)"
type="link"
:hasPermission="true"
:hasPermission="[
'system/NoticeRule:update',
]"
>
编辑
</PermissionButton>
@ -50,7 +59,9 @@
@click="onDelete(slotProps.id)"
danger
type="link"
:hasPermission="true"
:hasPermission="[
'system/NoticeRule:delete',
]"
>
删除
</PermissionButton>
@ -65,29 +76,51 @@
</template>
<template #add>
<div class="box-item">
<div @click="onAdd" class="box-item-img">
<AIcon
style="font-size: 20px"
type="PlusOutlined"
/>
<div class="box-item-img">
<j-tooltip
:title="!add ? '暂无权限,请联系管理员' : ''"
>
<j-button
:disabled="!add"
type="text"
@click="onAdd"
>
<AIcon
style="font-size: 20px"
type="PlusOutlined"
/>
</j-button>
</j-tooltip>
</div>
<div class="box-item-text"></div>
</div>
</template>
</MCarousel>
<div
class="child-item-right-auth"
:class="{ active: auth.length }"
@click="onAuth"
>
<AIcon type="UserOutlined" />
<span>权限控制</span>
<div class="child-item-right-auth" :class="{ active: auth.length }">
<j-tooltip :title="!update ? '暂无权限,请联系管理员' : ''">
<j-button :disabled="!update" type="text" @click="onAuth">
<div class="child-item-right-auth-btn">
<AIcon type="UserOutlined" />
<span>权限控制</span>
</div>
</j-button>
</j-tooltip>
</div>
</div>
</div>
<Save :data="current" v-if="visible" @close="visible = false" @save="onSave" />
<Detail :data="current" v-if="detailVisible" @close="detailVisible = false" />
<Save
:data="current"
v-if="visible"
@close="visible = false"
@save="onSave"
:loading="loading"
/>
<Detail
:data="current"
v-if="detailVisible"
@close="detailVisible = false"
/>
<Auth
v-if="authVisible"
:data="data?.grant?.role?.idList"
@ -101,7 +134,7 @@ import MCarousel from '@/components/MCarousel/index.vue';
import Save from '../Save/index.vue';
import Detail from '../Detail/index.vue';
import Auth from '../Auth/index.vue';
import { noticeType } from '../../data';
import { iconMap } from '../../data';
import { getImage, onlyMessage } from '@/utils/comm';
import {
saveChannelConfig,
@ -111,6 +144,7 @@ import {
updateChannelConfig,
} from '@/api/system/noticeRule';
import { Modal } from 'jetlinks-ui-components';
import { usePermissionStore } from '@/store/permission';
const props = defineProps({
data: {
@ -128,6 +162,13 @@ const current = ref<any>({});
const checked = ref<boolean>(false);
const auth = ref<string[]>([]);
const loading = ref<boolean>(false);
const permission = usePermissionStore();
const action = permission.hasPermission('system/NoticeRule:action');
const add = permission.hasPermission('system/NoticeRule:add');
const update = permission.hasPermission('system/NoticeRule:update');
watchEffect(() => {
checked.value = props.data?.state?.value === 'enabled';
@ -137,17 +178,17 @@ watchEffect(() => {
const onAdd = () => {
visible.value = true;
current.value = {
providerId: props.data.id
}
providerId: props.data.id,
};
};
const onView = (dt: any) => {
current.value = dt
current.value = dt;
detailVisible.value = true;
};
const onEdit = (dt: any) => {
current.value = dt
current.value = dt;
visible.value = true;
};
@ -241,13 +282,16 @@ const onSwitchChange = (e: boolean) => {
};
const onSave = (_data: any) => {
updateChannelConfig(props.data.id, {...props.data, ..._data}).then((resp) => {
loading.value = true
updateChannelConfig(props.data.id, [_data]).then((resp) => {
if (resp.status === 200) {
onlyMessage('操作成功!', 'success');
visible.value = false;
emits('refresh');
}
});
}).finally(() => {
loading.value = false
})
};
</script>
@ -297,17 +341,19 @@ const onSave = (_data: any) => {
.child-item-right-auth {
margin-left: 20px;
height: 78px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
color: @primary-color-hover;
}
&.active {
color: @primary-color;
.child-item-right-auth-btn {
height: 78px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
color: @primary-color-hover;
}
&.active {
color: @primary-color;
}
}
}
}

View File

@ -6,13 +6,17 @@
@search="handleSearch"
class="action-search"
/>
<div class="alert">
<AIcon type="InfoCircleOutlined" />
已规定固定收信人的模板在当前页面将被过滤
</div>
<div style="height: 400px; overflow-y: auto">
<JProTable
:columns="columns"
:request="(e) => handleData(e)"
model="CARD"
:bodyStyle="{
padding: 0
padding: 0,
}"
:params="params"
:gridColumn="2"
@ -74,6 +78,7 @@
<script lang="ts" setup>
import TemplateApi from '@/api/notice/template';
import { MSG_TYPE, NOTICE_METHOD } from '@/views/notice/const';
import { _variableMap } from '../../../data';
const props = defineProps({
notifierId: {
type: String,
@ -152,14 +157,19 @@ const handleData = async (e: any) => {
{ name: 'id', value: props.value },
{ name: 'createTime', order: 'desc' },
];
const resp = await TemplateApi.getListByConfigId(props.notifierId, {
const resp = await TemplateApi.getListVariableByConfigId(props.notifierId, {
...e,
sorts: sorts,
});
const result = (resp?.result || []).filter((item: any) => {
const _variable = _variableMap.get(item.type);
const arr = item?.variableDefinitions?.map((i: any) => i?.id) || [];
return arr.includes(_variable);
});
return {
code: resp.message,
result: {
data: resp.result ? resp.result : [],
data: result,
pageIndex: 0,
pageSize: resp.result.length,
total: resp.result.length,
@ -193,4 +203,12 @@ watch(
width: 88px;
height: 88px;
}
.alert {
height: 40px;
padding-left: 10px;
margin-bottom: 10px;
color: rgba(0, 0, 0, 0.55);
line-height: 40px;
background-color: #f6f6f6;
}
</style>

View File

@ -72,7 +72,6 @@ const formRef = ref();
const modelRef = reactive({});
watchEffect(() => {
console.log(props.notify, '123')
Object.assign(modelRef, props?.value);
});

View File

@ -110,7 +110,13 @@ watch(
if (v === 'upper') {
queryConfigVariables(props.providerId).then(resp => {
if (resp.status === 200) {
builtInList.value = (resp.result as any[]).map(item => {
// id
const _set = new Set((resp.result as any[]).map(item => item?.id))
const arr = [..._set.values()].map(item => {
const _arr = (resp.result as any[]).reverse()
return _arr.find(i => i.id === item)
})
builtInList.value = arr.map(item => {
return {
...item,
id: 'detail.' + item.id // 便

View File

@ -43,7 +43,7 @@ watch(
);
watch(
() => props.notify.notifierId,
() => props.notify.channelConfiguration?.notifierId,
(newVal) => {
if (newVal) {
getDepartment(newVal);

View File

@ -52,7 +52,7 @@
v-if="current !== stepList.length - 1"
>下一步</j-button
>
<j-button type="primary" @click="onSave" v-else>确认</j-button>
<j-button :loading="loading" type="primary" @click="onSave" v-else>确认</j-button>
</j-space>
</template>
</j-modal>
@ -87,6 +87,10 @@ const props = defineProps({
type: Object,
default: () => {},
},
loading: {
type: Boolean,
default: false
}
});
const stepList = [

View File

@ -6,11 +6,11 @@ iconMap.set('notifier-weixin', getImage('/notice/wechat.png'));
iconMap.set('notifier-email', getImage('/notice/email.png'));
iconMap.set('notifier-voice', getImage('/notice/voice.png'));
iconMap.set('notifier-sms', getImage('/notice/sms.png'));
iconMap.set('inside-mail', getImage('/notice/sms.png'));
iconMap.set('inside-mail', getImage('/notice/inside-mail.png'));
const noticeType = new Map();
noticeType.set('notifier-dingTalk', 'dingtalk');
noticeType.set('notifier-weixin', 'wechat');
noticeType.set('notifier-dingTalk', 'dingTalk');
noticeType.set('notifier-weixin', 'weixin');
noticeType.set('notifier-email', 'email');
noticeType.set('notifier-voice', 'voice');
noticeType.set('notifier-sms', 'sms');
@ -23,4 +23,11 @@ variableMap.set('notifier-email', 'sendTo');
variableMap.set('notifier-voice', 'calledNumber');
variableMap.set('notifier-sms', 'phoneNumber');
export { iconMap, noticeType, variableMap }
const _variableMap = new Map();
_variableMap.set('dingTalk', 'userIdList');
_variableMap.set('weixin', 'toUser');
_variableMap.set('email', 'sendTo');
_variableMap.set('voice', 'calledNumber');
_variableMap.set('sms', 'phoneNumber');
export { iconMap, noticeType, variableMap, _variableMap }

View File

@ -3823,10 +3823,10 @@ jetlinks-store@^0.0.3:
resolved "https://registry.npmjs.org/jetlinks-store/-/jetlinks-store-0.0.3.tgz"
integrity sha512-AZf/soh1hmmwjBZ00fr1emuMEydeReaI6IBTGByQYhTmK1Zd5pQAxC7WLek2snRAn/HHDgJfVz2hjditKThl6Q==
jetlinks-ui-components@^1.0.23:
version "1.0.23"
resolved "https://registry.jetlinks.cn/jetlinks-ui-components/-/jetlinks-ui-components-1.0.23.tgz#029f45a61316e3bf3b4c75959d41d7d76068fcbe"
integrity sha512-6OGDn8/kAmjlHMoeIp5B4L6EeucbqKFxqnYys4MqmtEvco/d5Pr8rzmJEav6bZmGkN21YWNHpwkhX3yrQt2F+g==
jetlinks-ui-components@^1.0.24:
version "1.0.24"
resolved "http://registry.jetlinks.cn/jetlinks-ui-components/-/jetlinks-ui-components-1.0.24.tgz#97580bed720526b50b3244440c7ae16d3d0a26c0"
integrity sha512-7ccv/eu9moZZFzCRuBa8Pe4NLd/knDARWwJaivH+qgkPSIIdij0Wax27zFwoRqivsDzbOAs2iRButcwSNvR9AQ==
dependencies:
"@vueuse/core" "^9.12.0"
"@vueuse/router" "^9.13.0"
@ -3834,6 +3834,7 @@ jetlinks-ui-components@^1.0.23:
colorpicker-v3 "^2.10.2"
lodash-es "^4.17.21"
monaco-editor "^0.35.0"
vuedraggable "^4.1.0"
js-cookie@^3.0.1:
version "3.0.1"
@ -6162,6 +6163,11 @@ socks@^2.6.2:
ip "^2.0.0"
smart-buffer "^4.2.0"
sortablejs@1.14.0:
version "1.14.0"
resolved "http://registry.jetlinks.cn/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8"
integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
@ -7048,6 +7054,13 @@ vue@^3.2.37, vue@^3.2.45:
"@vue/server-renderer" "3.2.45"
"@vue/shared" "3.2.45"
vuedraggable@^4.1.0:
version "4.1.0"
resolved "http://registry.jetlinks.cn/vuedraggable/-/vuedraggable-4.1.0.tgz#edece68adb8a4d9e06accff9dfc9040e66852270"
integrity sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==
dependencies:
sortablejs "1.14.0"
walk-up-path@^1.0.0:
version "1.0.0"
resolved "https://registry.jetlinks.cn/walk-up-path/-/walk-up-path-1.0.0.tgz"