feat: 订阅管理+个人中心

This commit is contained in:
100011797 2023-06-08 15:06:42 +08:00
parent bb62983d6e
commit 4aa8a2dbe6
37 changed files with 3489 additions and 528 deletions

View File

@ -0,0 +1,4 @@
import server from '@/utils/request';
// 获取角色列表
export const queryRoleList = (data: any): Promise<any> => server.post(`/role/_query/`, data);

View File

@ -0,0 +1,87 @@
<template>
<div class="box">
<div class="box-item" v-if="data.length > showLength">
<j-button
@click="onLeft"
type="primary"
:disabled="!(pageIndex > 0)"
shape="circle"
class="box-item-action"
><AIcon type="LeftOutlined"
/></j-button>
</div>
<div class="box-item" v-for="item in getData" :key="item.id">
<slot name="card" v-bind="item"></slot>
</div>
<div class="box-item">
<slot name="add"></slot>
</div>
<div class="box-item" v-if="data.length > showLength">
<j-button
:disabled="!(pageIndex + showLength < data.length)"
type="primary"
shape="circle"
class="box-item-action"
@click="onRight"
><AIcon type="RightOutlined"
/></j-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { PropType } from 'vue';
const props = defineProps({
data: {
type: Array as PropType<any[]>,
default: () => [],
},
showLength: {
type: Number,
default: 8,
},
});
// const emit = defineEmits(['add']);
const pageIndex = ref<number>(0);
const getData = computed(() => {
const start = pageIndex.value >= 0 ? pageIndex.value : 0;
const end =
props.showLength + pageIndex.value < props.data.length
? props.showLength + pageIndex.value
: props.data.length;
return props.data.slice(start, end);
});
const onRight = () => {
const flag = pageIndex.value + 1;
if (flag < props.data.length) {
pageIndex.value = flag;
}
};
const onLeft = () => {
const flag = pageIndex.value - 1;
if (flag >= 0) {
pageIndex.value -= 1;
}
};
// const onAdd = () => {
// emit('add');
// };
</script>
<style scoped lang="less">
.box {
display: flex;
align-items: center;
margin: 5px;
.box-item {
margin-left: 10px;
}
}
</style>

View File

@ -23,34 +23,34 @@ export const AccountMenu = {
name: 'account/center',
code: 'account/center',
meta: {
title: '基本设置',
title: '个人中心',
icon: '',
hideInMenu: false
hideInMenu: true
},
component: () => import('@/views/account/Center/index.vue')
},
{
path: '/account/NotificationSubscription',
name: 'account/NotificationSubscription',
code: 'account/NotificationSubscription',
meta: {
title: '通知订阅',
icon: '',
hideInMenu: false
},
component: () => import('@/views/account/NotificationSubscription/index.vue')
},
{
path: '/account/NotificationRecord',
name: 'account/NotificationRecord',
code: 'account/NotificationRecord',
meta: {
title: '通知记录',
icon: '',
hideInMenu: false
},
component: () => import('@/views/account/NotificationRecord/index.vue')
},
// {
// path: '/account/NotificationSubscription',
// name: 'account/NotificationSubscription',
// code: 'account/NotificationSubscription',
// meta: {
// title: '通知订阅',
// icon: '',
// hideInMenu: false
// },
// component: () => import('@/views/account/NotificationSubscription/index.vue')
// },
// {
// path: '/account/NotificationRecord',
// name: 'account/NotificationRecord',
// code: 'account/NotificationRecord',
// meta: {
// title: '通知记录',
// icon: '',
// hideInMenu: false
// },
// component: () => import('@/views/account/NotificationRecord/index.vue')
// },
]
}
@ -78,6 +78,6 @@ export default [
title: '授权页'
},
component: () => import('@/views/oauth/index.vue')
}
},
AccountMenu
]

View File

@ -5,7 +5,7 @@ import { cloneDeep, isArray } from 'lodash-es'
import { usePermissionStore } from './permission'
import router from '@/router'
import { onlyMessage } from '@/utils/comm'
import { AccountMenu, NotificationRecordCode, NotificationSubscriptionCode } from '@/router/menu'
// import { AccountMenu, NotificationRecordCode, NotificationSubscriptionCode } from '@/router/menu'
import { MESSAGE_SUBSCRIBE_MENU_CODE, USER_CENTER_MENU_CODE } from '@/utils/consts'
import {isNoCommunity} from "@/utils/utils";
@ -104,11 +104,11 @@ export const useMenuStore = defineStore({
const { menusData, silderMenus } = filterAsyncRouter(resultData)
// 是否存在通知订阅
const hasMessageSub = resultData.some((item: { code: string }) => item.code === MESSAGE_SUBSCRIBE_MENU_CODE)
if (!hasMessageSub) {
AccountMenu.children = AccountMenu.children.filter((item: { code: string }) => ![NotificationSubscriptionCode, NotificationRecordCode].includes(item.code) )
}
this.menus = findCodeRoute([...resultData, AccountMenu])
// const hasMessageSub = resultData.some((item: { code: string }) => item.code === MESSAGE_SUBSCRIBE_MENU_CODE)
// if (!hasMessageSub) {
// AccountMenu.children = AccountMenu.children.filter((item: { code: string }) => ![NotificationSubscriptionCode, NotificationRecordCode].includes(item.code) )
// }
this.menus = findCodeRoute([...resultData]) // AccountMenu
Object.keys(this.menus).forEach((item) => {
const _item = this.menus[item]
if (_item.buttons?.length) {
@ -123,7 +123,7 @@ export const useMenuStore = defineStore({
hideInMenu: true
}
})
menusData.push(AccountMenu)
// menusData.push(AccountMenu)
this.siderMenus = silderMenus.filter((item: { name: string }) => ![USER_CENTER_MENU_CODE, MESSAGE_SUBSCRIBE_MENU_CODE].includes(item.name))
res(menusData)
}

View File

@ -19,6 +19,11 @@ export const useUserInfo = defineStore('userInfo', {
roles: [],
token: '',
user: {},
name: '',
orgList: [],
roleList: [],
telephone: '',
email: ''
},
alarmUpdateCount: 0
}),

View File

@ -0,0 +1,114 @@
<template>
<div class="box">
<div class="content">
<div class="content-item" v-for="item in bindList" :key="item.id">
<div class="content-item-left">
<img
:src="item.logoUrl || getImage(bindIcon[item.provider])"
style="height: 50px; width: 50px"
width="50px"
height="50px"
alt=""
/>
<Ellipsis style="max-width: 200px; font-size: 22px">{{
item?.name
}}</Ellipsis>
<div>
<j-tag v-if="item.bound">已绑定</j-tag>
<j-tag v-else>未绑定</j-tag>
</div>
<div v-if="item.others?.name">
绑定名{{ item.others?.name }}
</div>
</div>
<div>
<j-popconfirm
v-if="item.bound"
title="确认解除绑定嘛?"
@confirm="() => unBind(item.id)"
>
<j-button>解除绑定</j-button>
</j-popconfirm>
<j-button v-else type="primary" @click="clickBind(item.id)"
>立即绑定</j-button
>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { BASE_API_PATH } from '@/utils/variable';
import { getSsoBinds_api } from '@/api/account/center';
import { unBind_api } from '@/api/account/center';
import { onlyMessage, getImage } from '@/utils/comm';
const bindList = ref<any[]>([]);
const bindIcon = {
'dingtalk-ent-app': '/notice/dingtalk.png',
'wechat-webapp': '/notice/wechat.png',
'internal-standalone': '/apply/provider1.png',
'third-party': '/apply/provider5.png',
};
const unBind = (id: string) => {
unBind_api(id).then((resp) => {
if (resp.status === 200) {
onlyMessage('解绑成功', 'success');
getSsoBinds();
}
});
};
const clickBind = (id: string) => {
window.open(
`${BASE_API_PATH}/application/sso/${id}/login?autoCreateUser=false`,
);
localStorage.setItem('onBind', 'false');
localStorage.setItem('onLogin', 'yes');
window.onstorage = (e) => {
if (e.newValue) {
getSsoBinds();
}
};
};
/**
* 获取绑定第三方账号
*/
function getSsoBinds() {
getSsoBinds_api().then((resp: any) => {
if (resp.status === 200) bindList.value = resp.result;
});
}
onMounted(() => {
getSsoBinds();
});
</script>
<style lang="less" scoped>
.box {
display: flex;
justify-content: center;
.content {
margin-top: 24px;
width: 80%;
.content-item {
width: 100%;
margin: 10px 0;
padding: 15px;
border: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
.content-item-left {
display: flex;
gap: 24px;
align-items: center;
}
}
}
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<j-modal visible title="查看详情" @cancel="emit('close')">
<j-descriptions :column="1">
<j-descriptions-item label="用户名">{{ userInfos?.username }}</j-descriptions-item>
<j-descriptions-item label="账号ID">{{ userInfos?.id }}</j-descriptions-item>
<j-descriptions-item label="姓名">{{ userInfos.name }}</j-descriptions-item>
<j-descriptions-item label="角色">{{ role }}</j-descriptions-item>
<j-descriptions-item label="组织">{{ org }}</j-descriptions-item>
<j-descriptions-item label="手机号">{{ userInfos?.telephone || '--' }}</j-descriptions-item>
<j-descriptions-item label="邮箱">{{ userInfos?.email || '--' }}</j-descriptions-item>
</j-descriptions>
<template #footer>
<j-button type="primary" @click="emit('close')">关闭</j-button>
</template>
</j-modal>
</template>
<script lang="ts" setup>
import { useUserInfo } from '@/store/userInfo';
const { userInfos } = useUserInfo();
const emit = defineEmits(['close', 'save']);
const role = computed(() => {
const _role = userInfos?.roleList.map((item: any) => item?.name).join(',')
return _role || '暂无角色'
})
const org = computed(() => {
const _role = userInfos?.orgList.map((item: any) => item?.name).join(',')
return _role || '暂无组织'
})
</script>

View File

@ -4,7 +4,7 @@
title="编辑"
@ok="handleOk"
width="770px"
@cancel="emits('update:visible', false)"
@cancel="emits('close')"
:confirmLoading="loading"
>
<j-form :model="form" layout="vertical" ref="formRef">
@ -80,7 +80,12 @@
<j-form-item
label="邮箱"
name="email"
:rules="[{ type: 'email',message:'邮箱不是一个有效的email' }]"
:rules="[
{
type: 'email',
message: '邮箱不是一个有效的email',
},
]"
>
<j-input
v-model:value="form.email"
@ -95,28 +100,30 @@
<script setup lang="ts">
import { updateMeInfo_api } from '@/api/account/center';
import { message } from 'ant-design-vue';
import { FormInstance } from 'ant-design-vue/es';
import { userInfoType } from '../typing';
import { onlyMessage } from '@/utils/comm';
const emits = defineEmits(['ok', 'update:visible']);
const props = defineProps<{
visible: boolean;
data: userInfoType;
}>();
const loading = ref(false)
const emits = defineEmits(['save', 'close']);
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
const loading = ref(false);
const form = ref(props.data);
const formRef = ref<FormInstance>();
const formRef = ref<any>();
const handleOk = () => {
formRef.value?.validate().then(() => {
loading.value = true
updateMeInfo_api(form.value).then((resp) => {
if (resp.status === 200) {
message.success('保存成功');
emits('ok');
emits('update:visible', false);
}
}).finally(()=>loading.value = false)
loading.value = true;
updateMeInfo_api(form.value)
.then((resp) => {
if (resp.status === 200) {
onlyMessage('保存成功', 'success');
emits('save');
}
})
.finally(() => (loading.value = false));
});
};
</script>

View File

@ -5,7 +5,7 @@
@ok="handleOk"
width="520px"
:confirmLoading="loading"
@cancel="emits('update:visible', false)"
@cancel="emits('close')"
>
<j-form :model="form" layout="vertical" ref="formRef">
<j-form-item
@ -24,7 +24,6 @@
<j-form-item
label="密码"
name="newPassword"
:rules="[
{ required: true, message: '请输入密码' },
{ validator: checkMethods.new, trigger: 'blur' },
@ -58,15 +57,18 @@ import {
checkOldPassword_api,
validateField_api,
} from '@/api/account/center';
import { FormInstance, message } from 'ant-design-vue';
import { Rule } from 'ant-design-vue/lib/form';
import { onlyMessage } from '@/utils/comm';
type formType = {
oldPassword: string;
newPassword: string;
confirmPassword: string;
};
const emits = defineEmits(['save', 'close']);
const emits = defineEmits(['ok', 'update:visible']);
const props = defineProps<{
visible: boolean;
}>();
const loading = ref(false);
const formRef = ref<FormInstance>();
const formRef = ref<any>();
const form = ref<formType>({
oldPassword: '',
newPassword: '',
@ -74,7 +76,7 @@ const form = ref<formType>({
});
const checkMethods = {
old: async (_rule: Rule, value: string) => {
old: async (_rule: any, value: string) => {
if (!value) return Promise.reject('请输入密码');
try {
const resp: any = await checkOldPassword_api(value);
@ -85,7 +87,7 @@ const checkMethods = {
return Promise.reject('验证失败');
}
},
new: async (_rule: Rule, value: string) => {
new: async (_rule: any, value: string) => {
if (!value) return Promise.reject('请输入密码');
else if (
form.value.confirmPassword &&
@ -101,7 +103,7 @@ const checkMethods = {
return Promise.reject('验证失败');
}
},
confirm: async (_rule: Rule, value: string) => {
confirm: async (_rule: any, value: string) => {
if (!value) return Promise.reject();
else if (form.value.newPassword && value !== form.value.newPassword) {
formRef.value?.validate('newPassword');
@ -127,20 +129,13 @@ const handleOk = () => {
updateMepsd_api(params)
.then((resp) => {
if (resp.status === 200) {
message.success('保存成功');
emits('ok');
emits('update:visible', false);
onlyMessage('保存成功', 'success');
emits('save');
}
})
.finally(() => (loading.value = false));
});
};
type formType = {
oldPassword: string;
newPassword: string;
confirmPassword: string;
};
</script>
<style scoped></style>

View File

@ -0,0 +1,184 @@
<template>
<j-modal
visible
title="重置密码"
width="520px"
:confirmLoading="loading"
@cancel="emits('close')"
>
<j-steps :current="current" size="small" progress-dot @change="onChange">
<j-step title="验证密码" />
<j-step title="设置密码" />
<j-step title="二次确认" />
</j-steps>
<div class="content">
<j-form :model="form" layout="vertical" ref="formRef">
<j-form-item
label="请输入当前密码"
name="oldPassword"
v-show="current === 0"
:rules="[
{ required: true, message: '请输入当前密码' },
{ validator: checkMethods.old, trigger: 'blur' },
]"
>
<j-input
v-model:value="form.oldPassword"
placeholder="请输入当前密码"
/>
</j-form-item>
<j-form-item
label="请输入新密码"
name="newPassword"
v-show="current === 1"
:rules="[
{ required: true, message: '请输入新密码' },
{ validator: checkMethods.new, trigger: 'blur' },
]"
>
<j-input-password
v-model:value="form.newPassword"
placeholder="请输入新密码"
/>
</j-form-item>
<j-form-item
label="请确认新密码"
v-show="current === 2"
name="confirmPassword"
:rules="[
{ required: true, message: '请确认新密码' },
{ validator: checkMethods.confirm, trigger: 'blur' },
]"
>
<j-input-password
v-model:value="form.confirmPassword"
placeholder="请确认新密码"
/>
</j-form-item>
</j-form>
</div>
<template #footer>
<j-button v-if="current === 0" @click="emits('close')">取消</j-button>
<j-button v-if="current === 2" @click="onPrev">上一步</j-button>
<j-button type="primary" v-else @click="onNext">下一步</j-button>
<j-button v-if="current === 2" type="primary" @click="handleOk">完成</j-button>
</template>
</j-modal>
</template>
<script setup lang="ts">
import {
updateMepsd_api,
checkOldPassword_api,
validateField_api,
} from '@/api/account/center';
import { onlyMessage } from '@/utils/comm';
type formType = {
oldPassword: string;
newPassword: string;
confirmPassword: string;
};
const emits = defineEmits(['save', 'close']);
const loading = ref(false);
const formRef = ref<any>();
const form = ref<formType>({
oldPassword: '',
newPassword: '',
confirmPassword: '',
});
const current = ref<number>(0);
const onPrev = () => {
current.value -= 1
}
const jumpStep = (val: number) => {
if(val === 1) {
formRef.value?.validate('oldPassword').then(() => {
current.value += 1
})
} else if(val === 2) {
formRef.value?.validate('newPassword').then(() => {
current.value += 1
})
}
}
const onNext = () => {
jumpStep(current.value + 1)
}
const onChange = (cur: number) => {
jumpStep(cur)
}
const checkMethods = {
old: async (_rule: any, value: string) => {
if (!value) return Promise.resolve();
try {
const resp: any = await checkOldPassword_api(value);
if (resp.status === 200 && !resp.result.passed)
return Promise.reject(resp.result.reason);
else return Promise.resolve();
} catch (error) {
return Promise.reject('验证失败');
}
},
new: async (_rule: any, value: string) => {
if (!value) return Promise.resolve();
try {
const resp: any = await validateField_api('password', value);
if (resp.status === 200 && !resp.result.passed)
return Promise.reject(resp.result.reason);
else return Promise.resolve();
} catch (error) {
return Promise.reject('验证失败');
}
},
confirm: async (_rule: any, value: string) => {
if (!value) return Promise.resolve();
else if (
form.value.newPassword &&
value !== form.value.newPassword
) {
return Promise.reject('两次密码输入不一致');
}
try {
const resp: any = await validateField_api('password', value);
if (resp.status === 200 && !resp.result.passed)
return Promise.reject(resp.result.reason);
else return Promise.resolve();
} catch (error) {
return Promise.reject('验证失败');
}
},
};
const handleOk = () => {
formRef.value?.validate().then(() => {
loading.value = true;
const params = {
oldPassword: form.value.oldPassword,
newPassword: form.value.newPassword,
};
updateMepsd_api(params)
.then((resp) => {
if (resp.status === 200) {
onlyMessage('保存成功', 'success');
emits('save');
}
})
.finally(() => (loading.value = false));
});
};
</script>
<style scoped>
.content {
padding: 20px 50px;
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<div class="choose-view">
<j-row class="view-content" :gutter="24">
<j-col
:span="8"
class="select-item"
:class="{ selected: currentView === 'device' }"
@click="currentView = 'device'"
>
<img :src="getImage('/home/device.png')" alt="" />
</j-col>
<j-col
:span="8"
class="select-item"
:class="{ selected: currentView === 'ops' }"
@click="currentView = 'ops'"
>
<img :src="getImage('/home/ops.png')" alt="" />
</j-col>
<j-col
:span="8"
class="select-item"
:class="{
selected: currentView === 'comprehensive',
}"
@click="currentView = 'comprehensive'"
>
<img :src="getImage('/home/comprehensive.png')" alt="" />
</j-col>
</j-row>
<j-button type="primary" class="btn" @click="confirm">确定</j-button>
</div>
</template>
<script lang="ts" setup>
import { getMe_api, getView_api, setView_api } from '@/api/home';
import { getImage, onlyMessage } from '@/utils/comm';
const currentView = ref<string>('');
const isApiUser = ref<boolean>();
function getViews() {
// api
getMe_api()
.then((resp: any) => {
if (resp && resp.status === 200) {
isApiUser.value = resp.result.dimensions.find(
(item: any) =>
item.type === 'api-client' ||
item.type.id === 'api-client',
);
if (!isApiUser.value) return getView_api();
}
})
.then((resp: any) => {
if (resp?.status === 200) {
if (resp.result) currentView.value = resp.result?.content;
else if (resp.result?.username === 'admin') {
currentView.value = 'comprehensive';
} else currentView.value = 'init';
}
});
}
const confirm = () => {
setView_api({
name: 'view',
content: currentView.value,
}).then(() => onlyMessage('保存成功', 'success'));
};
onMounted(() => {
getViews();
});
</script>
<style lang="less" scoped>
.choose-view {
width: 100%;
margin-top: 30px;
padding: 48px 150px;
box-sizing: border-box;
.view-content {
display: flex;
flex-flow: row wrap;
.select-item {
border: 2px solid transparent;
img {
width: 100%;
background-size: cover;
}
&.selected {
border-color: #10239e;
}
}
}
.btn {
display: block;
margin: 48px auto;
}
}
</style>

View File

@ -4,6 +4,7 @@
<pro-search
:columns="columns"
target="category"
style="padding: 0"
@search="(params:any)=>queryParams = {...params}"
/>
@ -13,12 +14,16 @@
:request="getList_api"
model="TABLE"
:params="queryParams"
:bodyStyle="{padding: 0}"
:defaultParams="{
sorts: [{
name: 'notifyTime', order: 'desc'
}]
}"
>
<template #headerTitle>
<j-button type="primary">全部已读</j-button>
</template>
<template #topicProvider="slotProps">
{{ slotProps.topicName }}
</template>

View File

@ -0,0 +1,26 @@
<template>
<div style="margin-top: 24px;">
<j-tabs tab-position="left">
<j-tab-pane v-for="item in tabs" :key="item.key" :tab="item.tab"><NotificationRecord /></j-tab-pane>
</j-tabs>
</div>
</template>
<script lang="ts" setup>
import NotificationRecord from './components/NotificationRecord/index.vue'
const tabs = [
{
key: '1',
tab: '告警'
},
{
key: '2',
tab: '系统运维'
},
{
key: '3',
tab: '业务监控'
}
]
</script>

View File

@ -0,0 +1,23 @@
<template>
<div>
<div><j-button type="link">取消订阅</j-button></div>
<template v-if="['dingTalk', 'weixin'].includes(type)">
<div><j-button type="link">更换接收账号</j-button></div>
<p>当前绑定的账号名称</p>
</template>
<template v-else>
<div><j-button type="link">更换接收账号</j-button></div>
</template>
</div>
</template>
<script lang="ts" setup>
import { PropType } from "vue";
type Type = 'dingTalk' | 'weixin' | 'email' | 'voice' | 'sms';
const props = defineProps({
type: {
type: String as PropType<Type>,
default: 'dingTalk',
},
});
</script>

View File

@ -0,0 +1,39 @@
<template>
<j-modal visible @cancel="emit('close')">
<template v-if="type === 'dingTalk'">
<p>请先绑定钉钉账号</p>
</template>
<template v-else-if="type === 'weixin'">
<p>请先绑定企业微信账号</p>
</template>
<template v-else-if="type === 'email'">
<p>请先绑定邮箱</p>
</template>
<template v-else>
<p>请先绑定手机号</p>
</template>
<template #footer>
<j-button @click="emit('close')">确定</j-button>
<j-button @click="onBind" type="primary" v-if="['voice', 'sms'].includes(type)">立即绑定</j-button>
</template>
</j-modal>
</template>
<script lang="ts" setup>
import { onlyMessage } from '@/utils/comm';
import { PropType } from 'vue';
type Type = 'dingTalk' | 'weixin' | 'email' | 'voice' | 'sms';
const emit = defineEmits(['close']);
const props = defineProps({
type: {
type: String as PropType<Type>,
default: 'dingTalk',
},
});
const onBind = () => {
onlyMessage('打开详情编辑框')
}
</script>

View File

@ -0,0 +1,300 @@
<template>
<div style="margin-top: 24px">
<div class="alert">
<AIcon type="InfoCircleOutlined" />
你可以在该页面选择需要订阅的主题及接收通知的方式
</div>
<div style="margin-top: 20px">
<j-collapse :bordered="false" v-model:activeKey="activeKey">
<template #expandIcon="{ isActive }">
<AIcon
type="CaretRightOutlined"
:rotate="isActive ? 90 : 0"
/>
</template>
<j-collapse-panel
v-for="item in dataSource"
:key="item.id"
class="custom"
>
<template #header
><h3>{{ item.name }}</h3></template
>
<div class="child">
<template
v-for="child in item.children"
:key="child.id"
>
<div class="child-item">
<div class="child-item-left">
<div style="font-weight: 600">
{{ child.name }}
</div>
<div class="child-item-left-auth">
<j-tooltip
title="当产品类型的告警被触发时,你将在已订阅的方式中收到通知"
>
<AIcon
type="ExclamationCircleOutlined"
/>
</j-tooltip>
</div>
</div>
<div class="child-item-right">
<MCarousel :data="child.children">
<template #card="slotProps">
<div class="box-item">
<j-popover>
<div class="box-item-img">
<img
style="width: 100%"
:src="
getImage(
`/notice/${slotProps?.type}.png`,
)
"
/>
<div
class="box-item-checked"
>
<j-checkbox
:checked="
!slotProps?.type
"
></j-checkbox>
</div>
</div>
<template #content>
<Detail />
<!-- <Error v-else /> -->
</template>
</j-popover>
<div class="box-item-text">
{{ item.name }}
</div>
</div>
</template>
</MCarousel>
</div>
</div>
</template>
</div>
</j-collapse-panel>
</j-collapse>
</div>
</div>
</template>
<script lang="ts" setup>
import { getImage } from '@/utils/comm';
import MCarousel from '@/components/MCarousel/index.vue';
import Detail from './components/Detail.vue';
import Error from './components/Error.vue'
const dataSource = ref([
{
id: 'a',
name: '告警',
children: [
{
id: 'product',
name: '产品告警',
active: true,
children: [
{
id: 'sms9',
name: '站内信',
type: 'sms',
},
{
id: 'dingtalk8',
name: '钉钉',
type: 'dingtalk',
},
{
id: 'wechat7',
name: '微信',
type: 'wechat',
},
{
id: 'email6',
name: '邮箱',
type: 'email',
},
{
id: 'dingtalk5',
name: '钉钉',
type: 'dingtalk',
},
{
id: 'wechat4',
name: '微信',
type: 'wechat',
},
{
id: 'email3',
name: '邮箱',
type: 'email',
},
{
id: 'email2',
name: '邮箱',
type: 'email',
},
{
id: 'email1',
name: '邮箱',
type: 'email',
},
],
},
{
id: 'device',
name: '设备告警',
active: false,
children: [
{
id: 'sms11',
name: '站内信',
type: 'sms',
},
{
id: 'wechat11',
name: '微信',
type: 'wechat',
},
{
id: 'voice11',
name: '语音',
type: 'voice',
},
],
},
],
},
{
id: 'b',
name: '系统监控',
children: [
{
id: 'cache',
name: '缓冲区数据丢弃',
active: false,
children: [
{
id: 'message111',
name: '站内信',
type: 'sms',
},
],
},
{
id: 'mqtt',
name: 'MQTT并发限制',
active: false,
children: [
{
id: 'message22',
name: '站内信',
type: 'sms',
},
],
},
],
},
{
id: 'c',
name: '业务监控',
children: [
{
id: 'error',
name: '透传消息解析异常',
active: false,
children: [
{
id: 'message333',
name: '站内信',
type: 'sms',
},
],
},
],
},
]);
const activeKey = ['a', 'b', 'c'];
</script>
<style lang="less" scoped>
.alert {
height: 40px;
padding-left: 10px;
margin-bottom: 10px;
color: rgba(0, 0, 0, 0.55);
line-height: 40px;
background-color: #f6f6f6;
}
.custom {
background: #f7f7f7;
border-radius: 4px;
border: 0;
overflow: hidden;
}
.child {
background-color: white;
padding: 10px;
.child-item {
padding: 10px 20px;
margin: 5px;
background: #f7f7f7;
display: flex;
justify-content: space-between;
align-items: center;
.child-item-left {
display: flex;
align-items: center;
div {
display: flex;
margin-right: 30px;
flex-direction: column;
justify-content: center;
align-items: center;
}
.child-item-left-auth {
cursor: pointer;
}
}
.child-item-right {
display: flex;
.box-item {
margin-left: 10px;
.box-item-img {
background-color: #fff;
width: 48px;
height: 48px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
.box-item-checked {
position: absolute;
top: -10px;
right: -10px;
}
}
.box-item-text {
width: 100%;
text-align: center;
height: 20px;
}
}
}
}
}
</style>

View File

@ -0,0 +1,482 @@
<template>
<page-container>
<div class="center-container">
<div class="card">
<div class="content" style="margin-top: 0">
<div
class="content-item flex-item"
style="width: 350px; justify-content: center"
>
<img
v-if="userInfo.avatar"
:src="userInfo.avatar"
style="width: 140px; border-radius: 70px"
alt=""
/>
<div class="default-avatar" v-else>
<AIcon type="UserOutlined" />
</div>
<div
style="
width: 100%;
text-align: center;
margin-top: 20px;
"
>
<j-upload
v-model:file-list="upload.fileList"
accept=".jpg,.png,.jfif,.pjp,.pjpeg,.jpeg"
:maxCount="1"
:show-upload-list="false"
:headers="{
[TOKEN_KEY]: LocalStore.get(TOKEN_KEY),
}"
:action="`${BASE_API_PATH}/file/static`"
@change="upload.changeBackUpload"
:beforeUpload="upload.beforeUpload"
>
<j-button>
<AIcon type="UploadOutlined" />
更换头像
</j-button>
</j-upload>
</div>
</div>
<div
class="content-item flex-item"
style="flex: 1; padding: 15px 0"
>
<div class="info-card">
<p>用户名</p>
<p>{{ userInfo.username }}</p>
</div>
<div class="info-card">
<p>账号ID</p>
<p>{{ userInfo.id }}</p>
</div>
<div class="info-card">
<p>注册时间</p>
<p>
{{
userInfo.createTime ? moment(userInfo.createTime).format(
'YYYY-MM-DD HH:mm:ss',
) : '-'
}}
</p>
</div>
<div class="info-card">
<p>电话</p>
<p>{{ userInfo.telephone || '-' }}</p>
</div>
<div class="info-card">
<p>姓名</p>
<p>{{ userInfo.name }}</p>
</div>
<div class="info-card">
<p>角色</p>
<p>
{{
(userInfo.roleList &&
userInfo.roleList
.map((item) => item.name)
.join(',')) ||
'-'
}}
</p>
</div>
<div class="info-card">
<p>组织</p>
<p>
{{
(userInfo.orgList &&
userInfo.orgList
.map((item) => item.name)
.join(',')) ||
'-'
}}
</p>
</div>
<div class="info-card">
<p>邮箱</p>
<p>{{ userInfo.email || '-' }}</p>
</div>
</div>
<AIcon
type="EditOutlined"
class="edit"
style="right: 40px"
@click="editInfoVisible = true"
/>
</div>
</div>
<div class="card" v-if='updatePassword'>
<h3>修改密码</h3>
<div class="content">
<div class="content" style="align-items: flex-end">
<AIcon
type="LockOutlined"
style="color: #1d39c4; font-size: 70px"
/>
<span
style="margin-left: 5px; color: rgba(0, 0, 0, 0.55)"
>安全性高的密码可以使帐号更安全建议您定期更换密码,设置一个包含字母,符号或数字中至少两项且长度超过8位的密码</span
>
</div>
<span class="edit">
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
@click="editPasswordVisible = true"
>
<AIcon type="EditOutlined" style="color: #1d39c4" />
</PermissionButton>
</span>
</div>
</div>
<!-- 社区版不显示 -->
<div class="card" v-if="isNoCommunity">
<h3>绑定三方账号</h3>
<div class="content">
<div class="account-card" v-for="item in bindList">
<img
:src="item.logoUrl || getImage(bindIcon[item.provider])"
style="height: 50px;width: 50px"
width='50px'
height='50px'
alt=""
/>
<Ellipsis style="width: 150px; font-size: 22px">
<div v-if="item.bound">
<div>绑定名{{ item.others.name }}</div>
<div>
绑定时间{{
moment(item.bindTime).format(
'YYYY-MM-DD HH:mm:ss',
)
}}
</div>
</div>
<div v-else>{{ item.name }}未绑定</div>
</Ellipsis>
<j-popconfirm
v-if="item.bound"
title="确认解除绑定嘛?"
@confirm="() => unBind(item.id)"
>
<j-button>解除绑定</j-button>
</j-popconfirm>
<j-button
v-else
type="primary"
@click="clickBind(item.id)"
>立即绑定</j-button
>
</div>
</div>
</div>
<!-- 第三方用户不显示 -->
<div class="card" v-if="!isApiUser">
<h3>首页视图</h3>
<div class="choose-view">
<j-row class="view-content" :gutter="24">
<j-col
:span="6"
class="select-item"
:class="{ selected: currentView === 'device' }"
@click="currentView = 'device'"
>
<img :src="getImage('/home/device.png')" alt="" />
</j-col>
<j-col
:span="6"
class="select-item"
:class="{ selected: currentView === 'ops' }"
@click="currentView = 'ops'"
>
<img :src="getImage('/home/ops.png')" alt="" />
</j-col>
<j-col
:span="6"
class="select-item"
:class="{
selected: currentView === 'comprehensive',
}"
@click="currentView = 'comprehensive'"
>
<img
:src="getImage('/home/comprehensive.png')"
alt=""
/>
</j-col>
</j-row>
<j-button type="primary" class="btn" @click="confirm"
>确定</j-button
>
</div>
</div>
<EditInfoDialog
v-if="editInfoVisible"
v-model:visible="editInfoVisible"
:data="{ ...userInfo }"
@ok="getUserInfo"
/>
<EditPasswordDialog
v-if="editPasswordVisible"
v-model:visible="editPasswordVisible"
/>
</div>
</page-container>
</template>
<script setup lang="ts" name="Center">
import PermissionButton from '@/components/PermissionButton/index.vue';
import EditInfoDialog from './components/EditInfoDialog.vue';
import EditPasswordDialog from './components/EditPasswordDialog.vue';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { LocalStore, getImage, onlyMessage } from '@/utils/comm'
import { message, UploadChangeParam, UploadFile } from 'ant-design-vue';
import {
getMeInfo_api,
getSsoBinds_api,
unBind_api,
updateMeInfo_api
} from '@/api/account/center';
import moment from 'moment';
import { getMe_api, getView_api, setView_api } from '@/api/home';
import { isNoCommunity } from '@/utils/utils';
import { userInfoType } from './typing';
import { usePermissionStore } from 'store/permission'
const btnHasPermission = usePermissionStore().hasPermission;
const updatePassword = btnHasPermission('account-center:user-center-passwd-update')
const permission = 'system/User';
const userInfo = ref<userInfoType>({} as any);
//
const bindList = ref<any[]>([]);
const bindIcon = {
'dingtalk-ent-app': '/notice/dingtalk.png',
'wechat-webapp': '/notice/wechat.png',
'internal-standalone': '/apply/provider1.png',
'third-party': '/apply/provider5.png',
};
const unBind = (id: string) => {
unBind_api(id).then((resp) => {
if (resp.status === 200) {
message.success('解绑成功');
getSsoBinds();
}
});
};
const clickBind = (id: string) => {
window.open(
`${BASE_API_PATH}/application/sso/${id}/login?autoCreateUser=false`,
);
localStorage.setItem('onBind', 'false');
localStorage.setItem('onLogin', 'yes');
window.onstorage = (e) => {
if (e.newValue) {
getSsoBinds();
}
};
};
const upload = reactive({
fileList: [] as any[],
uploadLoading: false,
changeBackUpload: (info: UploadChangeParam<UploadFile<any>>) => {
if (info.file.status === 'uploading') {
upload.uploadLoading = true;
} else if (info.file.status === 'done') {
info.file.url = info.file.response?.result;
upload.uploadLoading = false;
userInfo.value.avatar = info.file.response?.result;
updateMeInfo_api(userInfo.value).then(res => {
if(res.success) {
onlyMessage('上传成功')
}
})
} else if (info.file.status === 'error') {
upload.uploadLoading = false;
onlyMessage('logo上传失败请稍后再试', 'error');
}
},
beforeUpload: ({ size, type }: File) => {
const imageTypes = ['jpg', 'png', 'jfif', 'pjp', 'pjpeg', 'jpeg'];
const typeBool =
imageTypes.filter((typeStr) => type.includes(typeStr)).length > 0;
const sizeBool = size < 4 * 1024 * 1024;
(typeBool && sizeBool) || message.error('请上传正确格式的图片');
return typeBool && sizeBool;
},
});
//
const isApiUser = ref<boolean>();
const currentView = ref<string>('');
const confirm = () => {
setView_api({
name: 'view',
content: currentView.value,
}).then(() => message.success('保存成功'));
};
const editInfoVisible = ref<boolean>(false);
const editPasswordVisible = ref<boolean>(false);
init();
function init() {
getUserInfo();
isNoCommunity && getSsoBinds();
getViews();
}
/**
* 获取用户信息
*/
function getUserInfo() {
getMeInfo_api().then((resp) => {
userInfo.value = resp.result as userInfoType;
});
}
/**
* 获取绑定第三方账号
*/
function getSsoBinds() {
getSsoBinds_api().then((resp: any) => {
if (resp.status === 200) bindList.value = resp.result;
});
}
/**
* 获取首页视图
*/
function getViews() {
// api
getMe_api()
.then((resp: any) => {
if (resp && resp.status === 200) {
isApiUser.value = resp.result.dimensions.find(
(item: any) =>
item.type === 'api-client' ||
item.type.id === 'api-client',
);
if (!isApiUser.value) return getView_api();
}
})
.then((resp: any) => {
if (resp?.status === 200) {
if (resp.result) currentView.value = resp.result?.content;
else if (resp.result?.username === 'admin') {
currentView.value = 'comprehensive';
} else currentView.value = 'init';
}
});
}
</script>
<style lang="less" scoped>
.center-container {
background-color: #f0f2f5;
min-height: 100vh;
.card {
margin: 16px 0;
padding: 24px;
background-color: #fff;
position: relative;
h3 {
font-size: 22px;
&::before {
display: inline-block;
width: 3px;
height: 0.7em;
content: '';
background-color: #2f54eb;
margin: 0 8px;
}
}
.content {
display: flex;
margin-top: 24px;
flex-wrap: wrap;
gap: 24px;
.content-item {
margin-right: 24px;
.default-avatar {
background-color: #ccc;
color: #fff;
border-radius: 50%;
font-size: 70px;
width: 140px;
height: 140px;
display: flex;
justify-content: center;
align-items: center;
}
.info-card {
width: 25%;
:first-child {
font-weight: bold;
}
:last-child {
color: #666363d9;
}
}
&.flex-item {
display: flex;
flex-wrap: wrap;
}
}
.edit {
position: absolute;
cursor: pointer;
top: 30px;
right: 24px;
color: #1d39c4;
}
.account-card {
width: 415px;
background-image: url(/images/notice/dingtalk-background.png);
border-right: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px;
}
}
.choose-view {
width: 100%;
margin-top: 48px;
.view-content {
display: flex;
flex-flow: row wrap;
.select-item {
border: 2px solid transparent;
img {
width: 100%;
}
&.selected {
border-color: #10239e;
}
}
}
.btn {
display: block;
margin: 48px auto;
margin-bottom: 0;
}
}
}
}
</style>

View File

@ -1,481 +1,219 @@
<template>
<page-container>
<div class="center-container">
<div class="card">
<div class="content" style="margin-top: 0">
<div
class="content-item flex-item"
style="width: 350px; justify-content: center"
>
<img
v-if="userInfo.avatar"
:src="userInfo.avatar"
style="width: 140px; border-radius: 70px"
alt=""
/>
<div class="default-avatar" v-else>
<AIcon type="UserOutlined" />
<div class="person">
<div class="person-header">
<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>
</div>
<div class="person-header-item-info-right">
<div class="person-header-item-info-right-top">
<span>xx部门 · xx角色</span>
</div>
<div
style="
width: 100%;
text-align: center;
margin-top: 20px;
"
>
<j-upload
v-model:file-list="upload.fileList"
accept=".jpg,.png,.jfif,.pjp,.pjpeg,.jpeg"
:maxCount="1"
:show-upload-list="false"
:headers="{
[TOKEN_KEY]: LocalStore.get(TOKEN_KEY),
}"
:action="`${BASE_API_PATH}/file/static`"
@change="upload.changeBackUpload"
:beforeUpload="upload.beforeUpload"
<div class="person-header-item-info-right-info">
<div>用户名 {{user.userInfos?.username}}</div>
<div>账号ID {{user.userInfos?.id}}</div>
</div>
</div>
</div>
<div class="person-header-item-action">
<div class="person-header-item-action-left">
<j-space>
<j-button
@click="onActivated(item.key)"
v-for="item in list"
:type="
activeKey === item.key
? 'primary'
: 'default'
"
:key="item.key"
>{{ item.title }}</j-button
>
<j-button>
<AIcon type="UploadOutlined" />
更换头像
</j-button>
</j-upload>
</div>
</j-space>
</div>
<div
class="content-item flex-item"
style="flex: 1; padding: 15px 0"
>
<div class="info-card">
<p>用户名</p>
<p>{{ userInfo.username }}</p>
</div>
<div class="info-card">
<p>账号ID</p>
<p>{{ userInfo.id }}</p>
</div>
<div class="info-card">
<p>注册时间</p>
<p>
{{
userInfo.createTime ? moment(userInfo.createTime).format(
'YYYY-MM-DD HH:mm:ss',
) : '-'
}}
</p>
</div>
<div class="info-card">
<p>电话</p>
<p>{{ userInfo.telephone || '-' }}</p>
</div>
<div class="info-card">
<p>姓名</p>
<p>{{ userInfo.name }}</p>
</div>
<div class="info-card">
<p>角色</p>
<p>
{{
(userInfo.roleList &&
userInfo.roleList
.map((item) => item.name)
.join(',')) ||
'-'
}}
</p>
</div>
<div class="info-card">
<p>组织</p>
<p>
{{
(userInfo.orgList &&
userInfo.orgList
.map((item) => item.name)
.join(',')) ||
'-'
}}
</p>
</div>
<div class="info-card">
<p>邮箱</p>
<p>{{ userInfo.email || '-' }}</p>
</div>
</div>
<AIcon
type="EditOutlined"
class="edit"
style="right: 40px"
@click="editInfoVisible = true"
/>
</div>
</div>
<div class="card" v-if='updatePassword'>
<h3>修改密码</h3>
<div class="content">
<div class="content" style="align-items: flex-end">
<AIcon
type="LockOutlined"
style="color: #1d39c4; font-size: 70px"
/>
<span
style="margin-left: 5px; color: rgba(0, 0, 0, 0.55)"
>安全性高的密码可以使帐号更安全建议您定期更换密码,设置一个包含字母,符号或数字中至少两项且长度超过8位的密码</span
>
</div>
<span class="edit">
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
@click="editPasswordVisible = true"
>
<AIcon type="EditOutlined" style="color: #1d39c4" />
</PermissionButton>
</span>
</div>
</div>
<!-- 社区版不显示 -->
<div class="card" v-if="isNoCommunity">
<h3>绑定三方账号</h3>
<div class="content">
<div class="account-card" v-for="item in bindList">
<img
:src="item.logoUrl || getImage(bindIcon[item.provider])"
style="height: 50px;width: 50px"
width='50px'
height='50px'
alt=""
/>
<Ellipsis style="width: 150px; font-size: 22px">
<div v-if="item.bound">
<div>绑定名{{ item.others.name }}</div>
<div>
绑定时间{{
moment(item.bindTime).format(
'YYYY-MM-DD HH:mm:ss',
)
}}
</div>
</div>
<div v-else>{{ item.name }}未绑定</div>
</Ellipsis>
<j-popconfirm
v-if="item.bound"
title="确认解除绑定嘛?"
@confirm="() => unBind(item.id)"
>
<j-button>解除绑定</j-button>
</j-popconfirm>
<j-button
v-else
type="primary"
@click="clickBind(item.id)"
>立即绑定</j-button
>
<div class="person-header-item-action-right">
<j-space :size="24">
<j-tooltip title="查看详情"
><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"
><AIcon
style="font-size: 24px"
type="FormOutlined" /></j-button
></j-tooltip>
<j-tooltip title="修改密码"
><j-button shape="circle"
@click="editPasswordVisible = true"
><AIcon
style="font-size: 24px"
type="LockOutlined" /></j-button
></j-tooltip>
</j-space>
</div>
</div>
</div>
<!-- 第三方用户不显示 -->
<div class="card" v-if="!isApiUser">
<h3>首页视图</h3>
<div class="choose-view">
<j-row class="view-content" :gutter="24">
<j-col
:span="6"
class="select-item"
:class="{ selected: currentView === 'device' }"
@click="currentView = 'device'"
>
<img :src="getImage('/home/device.png')" alt="" />
</j-col>
<j-col
:span="6"
class="select-item"
:class="{ selected: currentView === 'ops' }"
@click="currentView = 'ops'"
>
<img :src="getImage('/home/ops.png')" alt="" />
</j-col>
<j-col
:span="6"
class="select-item"
:class="{
selected: currentView === 'comprehensive',
}"
@click="currentView = 'comprehensive'"
>
<img
:src="getImage('/home/comprehensive.png')"
alt=""
/>
</j-col>
</j-row>
<j-button type="primary" class="btn" @click="confirm"
>确定</j-button
>
</div>
</div>
<EditInfoDialog
v-if="editInfoVisible"
v-model:visible="editInfoVisible"
:data="{ ...userInfo }"
@ok="getUserInfo"
/>
<EditPasswordDialog
v-if="editPasswordVisible"
v-model:visible="editPasswordVisible"
/>
</div>
</page-container>
<div class="person-content">
<div class="person-content-item">
<FullPage>
<div class="person-content-item-content">
<component :is="tabs[activeKey]" />
</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" />
</div>
</template>
<script setup lang="ts" name="Center">
import PermissionButton from '@/components/PermissionButton/index.vue';
import EditInfoDialog from './components/EditInfoDialog.vue';
import EditPasswordDialog from './components/EditPasswordDialog.vue';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { LocalStore, getImage, onlyMessage } from '@/utils/comm'
import { message, UploadChangeParam, UploadFile } from 'ant-design-vue';
import {
getMeInfo_api,
getSsoBinds_api,
unBind_api,
updateMeInfo_api
} from '@/api/account/center';
import moment from 'moment';
import { getMe_api, getView_api, setView_api } from '@/api/home';
import { isNoCommunity } from '@/utils/utils';
import { userInfoType } from './typing';
import { usePermissionStore } from 'store/permission'
import HomeView from './components/HomeView/index.vue';
import BindThirdAccount from './components/BindThirdAccount/index.vue';
import Subscribe from './components/Subscribe/index.vue';
import StationMessage from './components/StationMessage/index.vue';
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';
const btnHasPermission = usePermissionStore().hasPermission;
const updatePassword = btnHasPermission('account-center:user-center-passwd-update')
const permission = 'system/User';
const userInfo = ref<userInfoType>({} as any);
//
const bindList = ref<any[]>([]);
const bindIcon = {
'dingtalk-ent-app': '/notice/dingtalk.png',
'wechat-webapp': '/notice/wechat.png',
'internal-standalone': '/apply/provider1.png',
'third-party': '/apply/provider5.png',
};
const unBind = (id: string) => {
unBind_api(id).then((resp) => {
if (resp.status === 200) {
message.success('解绑成功');
getSsoBinds();
}
});
};
const clickBind = (id: string) => {
window.open(
`${BASE_API_PATH}/application/sso/${id}/login?autoCreateUser=false`,
);
localStorage.setItem('onBind', 'false');
localStorage.setItem('onLogin', 'yes');
window.onstorage = (e) => {
if (e.newValue) {
getSsoBinds();
}
};
};
const upload = reactive({
fileList: [] as any[],
uploadLoading: false,
changeBackUpload: (info: UploadChangeParam<UploadFile<any>>) => {
if (info.file.status === 'uploading') {
upload.uploadLoading = true;
} else if (info.file.status === 'done') {
info.file.url = info.file.response?.result;
upload.uploadLoading = false;
userInfo.value.avatar = info.file.response?.result;
updateMeInfo_api(userInfo.value).then(res => {
if(res.success) {
onlyMessage('上传成功')
}
})
} else if (info.file.status === 'error') {
upload.uploadLoading = false;
onlyMessage('logo上传失败请稍后再试', 'error');
}
const user = useUserInfo();
type KeyType = 'HomeView' | 'BindThirdAccount' | 'Subscribe' | 'StationMessage';
const list: { key: KeyType; title: string }[] = [
{
key: 'HomeView',
title: '首页视图',
},
beforeUpload: ({ size, type }: File) => {
const imageTypes = ['jpg', 'png', 'jfif', 'pjp', 'pjpeg', 'jpeg'];
const typeBool =
imageTypes.filter((typeStr) => type.includes(typeStr)).length > 0;
const sizeBool = size < 4 * 1024 * 1024;
(typeBool && sizeBool) || message.error('请上传正确格式的图片');
return typeBool && sizeBool;
{
key: 'BindThirdAccount',
title: '绑定第三方账号',
},
});
//
const isApiUser = ref<boolean>();
const currentView = ref<string>('');
const confirm = () => {
setView_api({
name: 'view',
content: currentView.value,
}).then(() => message.success('保存成功'));
{
key: 'Subscribe',
title: '我的订阅',
},
{
key: 'StationMessage',
title: '站内信',
},
];
const tabs = {
HomeView,
BindThirdAccount,
Subscribe,
StationMessage,
};
const activeKey = ref<KeyType>('HomeView');
const visible = ref<boolean>(false);
const editInfoVisible = ref<boolean>(false);
const editPasswordVisible = ref<boolean>(false);
init();
function init() {
getUserInfo();
isNoCommunity && getSsoBinds();
getViews();
const onActivated = (_key: KeyType) => {
activeKey.value = _key;
};
const onSave = () => {
user.getUserInfo()
editInfoVisible.value = false
}
/**
* 获取用户信息
*/
function getUserInfo() {
getMeInfo_api().then((resp) => {
userInfo.value = resp.result as userInfoType;
});
}
/**
* 获取绑定第三方账号
*/
function getSsoBinds() {
getSsoBinds_api().then((resp: any) => {
if (resp.status === 200) bindList.value = resp.result;
});
}
/**
* 获取首页视图
*/
function getViews() {
// api
getMe_api()
.then((resp: any) => {
if (resp && resp.status === 200) {
isApiUser.value = resp.result.dimensions.find(
(item: any) =>
item.type === 'api-client' ||
item.type.id === 'api-client',
);
if (!isApiUser.value) return getView_api();
}
})
.then((resp: any) => {
if (resp?.status === 200) {
if (resp.result) currentView.value = resp.result?.content;
else if (resp.result?.username === 'admin') {
currentView.value = 'comprehensive';
} else currentView.value = 'init';
}
});
const onPasswordSave = () => {
editPasswordVisible.value = false
}
</script>
<style lang="less" scoped>
.center-container {
background-color: #f0f2f5;
min-height: 100vh;
.card {
margin: 16px 0;
padding: 24px;
background-color: #fff;
position: relative;
.person {
.person-header {
width: 100%;
height: 150px;
padding: 0 150px;
background-color: rgba(2, 125, 180, 0.368);
h3 {
font-size: 22px;
&::before {
display: inline-block;
width: 3px;
height: 0.7em;
content: '';
background-color: #2f54eb;
margin: 0 8px;
}
}
.content {
display: flex;
margin-top: 24px;
flex-wrap: wrap;
gap: 24px;
.content-item {
margin-right: 24px;
.default-avatar {
background-color: #ccc;
color: #fff;
border-radius: 50%;
font-size: 70px;
width: 140px;
height: 140px;
display: flex;
justify-content: center;
align-items: center;
.person-header-item {
position: relative;
height: 100%;
.person-header-item-info {
padding-top: 30px;
display: flex;
.person-header-item-info-left {
margin-right: 30px;
}
.info-card {
width: 25%;
:first-child {
font-weight: bold;
}
:last-child {
color: #666363d9;
}
}
&.flex-item {
.person-header-item-info-right {
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: space-between;
.person-header-item-info-right-top {
span {
background-color: rgba(
255,
255,
128,
0.43137254901960786
);
border-radius: 5px;
padding: 0 10px;
}
}
.person-header-item-info-right-info {
color: #fff;
display: flex;
font-size: 16px;
> :not(:last-child) {
margin-right: 20px;
}
}
}
}
.edit {
.person-header-item-action {
position: absolute;
cursor: pointer;
top: 30px;
right: 24px;
color: #1d39c4;
}
.account-card {
width: 415px;
background-image: url(/images/notice/dingtalk-background.png);
border-right: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
width: 100%;
height: 50px;
z-index: 2;
left: 0;
bottom: -25px;
padding: 0 50px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px;
}
}
.choose-view {
width: 100%;
margin-top: 48px;
.view-content {
display: flex;
flex-flow: row wrap;
.select-item {
border: 2px solid transparent;
img {
width: 100%;
align-items: center;
.person-header-item-action-left {
button {
height: 35px;
padding: 0 40px;
}
}
&.selected {
border-color: #10239e;
.person-header-item-action-right {
button {
height: 50px;
width: 50px;
}
}
}
}
}
.btn {
display: block;
margin: 48px auto;
margin-bottom: 0;
}
.person-content {
width: 100%;
padding: 0 150px;
.person-content-item-content {
padding: 20px;
}
}
}

View File

@ -1,17 +0,0 @@
import { dictType } from '@/views/system/Department/typing';
export type userInfoType = {
avatar: string;
createTime: number;
email: string;
id: string;
name: string;
orgList: dictType;
roleList: dictType;
status: number;
telephone: string;
tenantDisabled: boolean;
type: { name: string; id: string };
username: string;
};

View File

@ -3922,6 +3922,99 @@ export default [
accessSupport: { text: "不支持", value: "unsupported" },
supportDataAccess: false
},
{
code: 'system/NoticeRule',
name: '通知规则',
owner: 'iot',
id: 'system/NoticeRule',
sortIndex: 11,
url: '/system/NoticeRule',
icon: 'icon-yingyongguanli',
showPage: ['application'],
permissions: [],
buttons: [
{
id: 'delete',
name: '删除',
permissions: [
// {
// permission: 'application',
// actions: ['query', 'delete'],
// },
],
},
{
id: 'add',
name: '新增',
permissions: [
// {
// permission: 'role',
// actions: ['query'],
// },
// {
// permission: 'menu',
// actions: ['query'],
// },
// {
// permission: 'application',
// actions: ['query', 'save'],
// },
// {
// permission: 'open-api',
// actions: ['query', 'save', 'delete'],
// },
],
},
{
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'],
// },
],
},
{
id: 'action',
name: '启/禁用',
permissions: [
// {
// permission: 'application',
// actions: ['save'],
// },
],
},
],
accessSupport: { text: "不支持", value: "unsupported" },
supportDataAccess: false
},
// {
// code: 'system/License',
// name: 'License管理',

View File

@ -203,6 +203,7 @@ const next = async () => {
const onCancel = () => {
emit('cancel');
};
const onOk = async () => {
let _data = null
if(variable.value.length){

View File

@ -0,0 +1,30 @@
<template>
<j-modal
:width="900"
visible
title="权限控制"
@cancel="emit('close')"
@ok="onSave"
>
<Role v-model="_selectedRowKeys" />
</j-modal>
</template>
<script lang="ts" setup>
import { onlyMessage } from '@/utils/comm';
import Role from '../Role/index.vue'
const emit = defineEmits(['close', 'save']);
const _selectedRowKeys = ref<string[]>([]);
const checked = ref<boolean>(false);
const onSave = () => {
if(_selectedRowKeys.value.length) {
emit('save', _selectedRowKeys.value);
} else {
onlyMessage('请配置角色权限', 'error')
}
};
</script>

View File

@ -0,0 +1,25 @@
<template>
<j-modal :width="700" visible title="配置详情" @cancel="emit('close')">
<j-descriptions bordered :column="2">
<j-descriptions-item label="通知方式">Cloud Database</j-descriptions-item>
<j-descriptions-item label="通知配置">Cloud Database</j-descriptions-item>
<j-descriptions-item label="通知模板">Cloud Database</j-descriptions-item>
<j-descriptions-item label="通知内容">Cloud Database</j-descriptions-item>
<j-descriptions-item label="模板变量">Cloud Database</j-descriptions-item>
<j-descriptions-item label="用户权限">Cloud Database</j-descriptions-item>
</j-descriptions>
<template #footer>
<j-button type="primary" @click="emit('close')">确定</j-button>
</template>
</j-modal>
</template>
<script lang="ts" setup>
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['close', 'save']);
</script>

View File

@ -0,0 +1,115 @@
<template>
<pro-search
style="padding: 0"
type="simple"
:columns="columns"
target="category"
@search="onSearch"
/>
<j-pro-table
ref="tableRef"
:columns="columns"
:request="queryRoleList"
model="TABLE"
:params="params"
:bodyStyle="{ padding: 0 }"
:scroll="{ y: 500 }"
:defaultParams="{
// pageSize: 10,
sorts: [
{ name: 'createTime', order: 'desc' },
{ name: 'id', order: 'desc' },
],
}"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onSelect: onSelect,
onSelectAll: onSelectAll,
onSelectNone: cancelSelect,
}"
>
<!-- <template #headerTitle>
<j-checkbox v-model:checked="checked">全选</j-checkbox>
</template> -->
</j-pro-table>
</template>
<script lang="ts" setup>
import { queryRoleList } from '@/api/system/noticeRule';
import { PropType } from 'vue';
const props = defineProps({
modelValue: {
type: Array as PropType<string[]>,
default: () => [],
},
});
const emit = defineEmits(['update:modelValue']);
const params = ref<any>();
const _selectedRowKeys = ref<string[]>([]);
watchEffect(() => {
_selectedRowKeys.value = props?.modelValue || [];
});
const columns = [
{
title: '标识',
dataIndex: 'id',
key: 'id',
ellipsis: true,
fixed: 'left',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '说明',
key: 'description',
ellipsis: true,
dataIndex: 'description',
},
];
const onSearch = (e: any) => {
params.value = e;
};
//
const cancelSelect = () => {
_selectedRowKeys.value = [];
};
const onSelect = (record: any, selected: boolean) => {
const _set = new Set(_selectedRowKeys.value);
if (selected) {
_set.add(record.id);
} else {
_set.delete(record.id);
}
emit('update:modelValue', [..._set.values()]);
};
const onSelectAll = (selected: boolean, _: any[], _keys: any[]) => {
const _set = new Set(_selectedRowKeys.value);
const arr = _keys.map((item: any) => item?.id);
if (selected) {
arr.map((i: any) => {
_set.add(i);
});
} else {
arr.map((i: any) => {
_set.delete(i);
});
}
emit('update:modelValue', [..._set.values()]);
};
</script>

View File

@ -0,0 +1,194 @@
<template>
<pro-search
:columns="columns"
type="simple"
target="action-notice-config"
@search="handleSearch"
class="action-search"
/>
<div style="height: 400px; overflow-y: auto">
<JProTable
:columns="columns"
:request="query"
model="CARD"
:bodyStyle="{
padding: 0,
}"
:params="params"
:gridColumn="2"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange,
}"
>
<template #card="slotProps">
<CardBox
:showStatus="false"
:value="slotProps"
:showTool="false"
:actions="[]"
v-bind="slotProps"
@click="handleClick"
:active="_selectedRowKeys.includes(slotProps.id)"
>
<template #img>
<slot name="img">
<img
:src="
getLogo(slotProps.type, slotProps.provider)
"
class="notify-logo"
/>
</slot>
</template>
<template #content>
<Ellipsis style="width: calc(100% - 100px)">
<span style="font-size: 16px; font-weight: 600">
{{ slotProps.name }}
</span>
</Ellipsis>
<j-row>
<j-col :span="12">
<div class="card-item-content-text">
通知方式
</div>
<div>
{{ getMethodTxt(slotProps.type) }}
</div>
</j-col>
<j-col :span="12">
<div class="card-item-content-text">说明</div>
<Ellipsis>
{{ slotProps.description }}
</Ellipsis>
</j-col>
</j-row>
</template>
</CardBox>
</template>
</JProTable>
</div>
</template>
<script lang="ts" setup>
import ConfigApi from '@/api/notice/config';
import { MSG_TYPE, NOTICE_METHOD } from '@/views/notice/const';
const props = defineProps({
notifyType: {
type: String,
default: '',
},
value: {
type: String,
default: '',
},
});
const emit = defineEmits(['update:value', 'change']);
const getLogo = (type: string, provider: string) => {
return MSG_TYPE[type].find((f: any) => f.value === provider)?.logo;
};
const getMethodTxt = (type: string) => {
return NOTICE_METHOD.find((f) => f.value === type)?.label;
};
const params = ref<Record<string, any>>({});
const _selectedRowKeys = ref<string[]>([]);
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'string',
},
},
{
title: 'ID',
dataIndex: 'id',
key: 'id',
search: {
type: 'string',
},
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
search: {
type: 'string',
},
},
];
const query = (e: Record<string, any>) =>
ConfigApi.list({
...e,
terms: [
...e?.terms,
{
terms: [
{
termType: 'eq',
column: 'type',
value: props.notifyType,
},
],
},
],
sorts: [
{ name: 'id', value: props.value },
{ name: 'createTime', order: 'desc' },
],
});
const handleSearch = (_params: any) => {
params.value = _params;
};
const onSelectChange = (keys: string[]) => {
_selectedRowKeys.value = [...keys];
};
const handleClick = (dt: any) => {
if (_selectedRowKeys.value.includes(dt.id)) {
_selectedRowKeys.value = [];
emit('update:value', undefined);
emit('change', { provider: undefined });
} else {
_selectedRowKeys.value = [dt.id];
emit('update:value', dt.id);
emit('change', { provider: dt?.provider });
}
};
watch(
() => props.value,
(newValue) => {
if (newValue) {
_selectedRowKeys.value = [newValue];
} else {
_selectedRowKeys.value = [];
}
},
{
deep: true,
immediate: true,
},
);
</script>
<style lang="less">
.action-search {
padding: 0;
}
.notify-logo {
width: 88px;
height: 88px;
}
</style>

View File

@ -0,0 +1,196 @@
<template>
<pro-search
:columns="columns"
type="simple"
target="action-notice-template"
@search="handleSearch"
class="action-search"
/>
<div style="height: 400px; overflow-y: auto">
<JProTable
:columns="columns"
:request="(e) => handleData(e)"
model="CARD"
:bodyStyle="{
padding: 0
}"
:params="params"
:gridColumn="2"
:noPagination="true"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange,
}"
>
<template #card="slotProps">
<CardBox
:showStatus="false"
:value="slotProps"
:showTool="false"
:actions="[]"
v-bind="slotProps"
@click="handleClick"
:active="_selectedRowKeys.includes(slotProps.id)"
>
<template #img>
<slot name="img">
<img
:src="
getLogo(slotProps.type, slotProps.provider)
"
class="notify-logo"
/>
</slot>
</template>
<template #content>
<Ellipsis style="width: calc(100% - 100px)">
<span style="font-size: 16px; font-weight: 600">
{{ slotProps.name }}
</span>
</Ellipsis>
<j-row>
<j-col :span="12">
<div class="card-item-content-text">
通知方式
</div>
<div>
{{ getMethodTxt(slotProps.type) }}
</div>
</j-col>
<j-col :span="12">
<div class="card-item-content-text">说明</div>
<Ellipsis>
{{ slotProps.description }}
</Ellipsis>
</j-col>
</j-row>
</template>
</CardBox>
</template>
</JProTable>
</div>
</template>
<script lang="ts" setup>
import TemplateApi from '@/api/notice/template';
import { MSG_TYPE, NOTICE_METHOD } from '@/views/notice/const';
const props = defineProps({
notifierId: {
type: String,
default: '',
},
value: {
type: String,
default: '',
},
});
const emit = defineEmits(['update:value', 'change', 'update:detail']);
const getLogo = (type: string, provider: string) => {
return MSG_TYPE[type].find((f: any) => f.value === provider)?.logo;
};
const getMethodTxt = (type: string) => {
return NOTICE_METHOD.find((f) => f.value === type)?.label;
};
const params = ref<Record<string, any>>({});
const _selectedRowKeys = ref<string[]>([]);
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'string',
},
},
{
title: 'ID',
dataIndex: 'id',
key: 'id',
search: {
type: 'string',
},
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
search: {
type: 'string',
},
},
];
const handleSearch = (_params: any) => {
params.value = _params;
};
const handleClick = (dt: any) => {
if (_selectedRowKeys.value.includes(dt.id)) {
_selectedRowKeys.value = [];
emit('update:value', undefined);
emit('change', { templateName: undefined });
emit('update:detail', undefined);
} else {
_selectedRowKeys.value = [dt.id];
emit('update:value', dt.id);
emit('change', { templateName: dt?.name });
emit('update:detail', dt);
}
};
const onSelectChange = (keys: string[]) => {
_selectedRowKeys.value = [...keys];
};
const handleData = async (e: any) => {
const sorts = [
{ name: 'id', value: props.value },
{ name: 'createTime', order: 'desc' },
];
const resp = await TemplateApi.getListByConfigId(props.notifierId, {
...e,
sorts: sorts,
});
return {
code: resp.message,
result: {
data: resp.result ? resp.result : [],
pageIndex: 0,
pageSize: resp.result.length,
total: resp.result.length,
},
status: resp.status,
};
};
watch(
() => props.value,
(newValue) => {
if (newValue) {
_selectedRowKeys.value = [newValue];
} else {
_selectedRowKeys.value = [];
}
},
{
deep: true,
immediate: true,
},
);
</script>
<style lang="less">
.action-search {
padding: 0;
}
.notify-logo {
width: 88px;
height: 88px;
}
</style>

View File

@ -0,0 +1,137 @@
<template>
<j-spin :spinning="loading">
<div class="notify-type-warp" :class="{ disabled: disabled }">
<div
:key="item.id"
v-for="item in options"
class="notify-type-item"
:class="{ active: notifyType === item.value }"
@click="onSelect(item.value)"
>
<div class="notify-type-item-image">
<img :width="106" :src="item.iconUrl" />
</div>
<div class="notify-type-item-title">{{item.label}}</div>
</div>
</div>
</j-spin>
</template>
<script lang="ts" setup>
import { getImage } from '@/utils/comm';
import notice from '@/api/notice/config';
const iconMap = new Map();
iconMap.set('dingTalk', getImage('/notice/dingtalk.png'));
iconMap.set('weixin', getImage('/notice/wechat.png'));
iconMap.set('email', getImage('/notice/email.png'));
iconMap.set('voice', getImage('/notice/voice.png'));
iconMap.set('sms', getImage('/notice/sms.png'));
iconMap.set('webhook', getImage('/notice/webhook.png'));
const props = defineProps({
value: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:value', 'change']);
const loading = ref<boolean>(false);
const notifyType = ref('');
const options = ref<any[]>([]);
watch(
() => props.value,
(newVal) => {
notifyType.value = newVal;
},
{ deep: true, immediate: true },
);
const onSelect = (val: string) => {
if (!props.disabled) {
emit('update:value', val);
emit('change', val);
}
};
onMounted(() => {
loading.value = true;
notice.queryMessageType().then((resp) => {
if (resp.status === 200) {
options.value = (resp.result as any[]).filter(i => i.id !== 'webhook').map((item) => {
return {
label: item.name,
value: item.id,
iconUrl: iconMap.get(item.id),
};
});
}
loading.value = false;
});
notifyType.value = props.value;
});
</script>
<style lang="less" scoped>
.notify-type-warp {
display: flex;
flex-wrap: wrap;
gap: 16px 24px;
width: 100%;
.notify-type-item {
display: flex;
flex-direction: column;
align-items: center;
width: 172px;
border: 1px solid #e0e4e8;
border-radius: 2px;
cursor: pointer;
transition: all 0.3s;
.notify-type-item-title {
margin-bottom: 8px;
font-weight: 500;
font-size: 14px;
}
.notify-type-item-image {
width: 106px;
margin: 16px 33px;
}
&:hover {
color: @primary-color-hover;
opacity: 0.8;
}
&.active {
border-color: @primary-color-active;
opacity: 1;
}
}
&.disabled {
.notify-type-item {
cursor: not-allowed;
&:hover {
color: initial;
opacity: 0.6;
}
&.active {
opacity: 1;
}
}
}
}
</style>

View File

@ -0,0 +1,138 @@
<template>
<j-form
v-if="variableDefinitions.length"
:layout="'vertical'"
ref="formRef"
:model="modelRef"
>
<j-form-item
:name="`${item?.id}`"
:label="item?.name"
v-for="(item) in variableDefinitions"
:key="item.id"
:required="getType(item) !== 'file' ? true : false"
:rules="[
{
validator: (_rule, value) => checkValue(_rule, value, item),
trigger: ['blur', 'change'],
},
]"
>
<Tag
:notify="notify"
v-if="getType(item) === 'tag'"
v-model:value="modelRef[item.id]"
/>
<InputFile
v-else-if="getType(item) === 'file'"
v-model:value="modelRef[item.id]"
/>
<j-input
v-else-if="getType(item) === 'link'"
v-model:value="modelRef[item.id]"
/>
<BuildIn
v-else
:item="item"
v-model:value="modelRef[item.id]"
/>
</j-form-item>
</j-form>
<j-empty v-else style="margin: 20px 0" description="当前模版暂无变量" />
</template>
<script lang="ts" setup>
import BuildIn from './variableItem/BuildIn.vue';
import Tag from './variableItem/Tag.vue';
import InputFile from './variableItem/InputFile.vue';
import { PropType } from 'vue';
const props = defineProps({
variableDefinitions: {
type: Array as PropType<any>,
default: () => [],
},
value: {
type: Object,
default: () => ({}),
},
notify: {
type: Object,
default: () => ({}),
},
template: {
type: Object,
default: () => ({}),
},
});
const formRef = ref();
const modelRef = reactive({});
watchEffect(() => {
Object.assign(modelRef, props?.value);
});
const getType = (item: any) => {
return item.expands?.businessType || item.type;
};
const checkValue = (_rule: any, value: any, item: any) => {
if(!value){
return Promise.resolve();
}
const type = item.expands?.businessType || item?.type;
if (type === 'file') {
return Promise.resolve();
} else if (type === 'link') {
if (!value) {
return Promise.reject(new Error('请输入' + item.name));
} else if (value.length > 64) {
return Promise.reject(new Error('最多64个字符'));
}
} else if (type === 'tag' && !value) {
return Promise.reject(new Error('请选择' + item.name));
} else if (['date'].includes(type)) {
if (!value) {
return Promise.reject(new Error('请选择' + item.name));
} else {
if (value?.source === 'upper') {
if (!value?.upperKey) {
return Promise.reject(new Error('请选择' + item.name));
} else {
return Promise.resolve();
}
} else {
if (!value?.value) {
return Promise.reject(new Error('请选择' + item.name));
} else {
return Promise.resolve();
}
}
}
} else if (value?.source === 'fixed' && !value?.value) {
return Promise.reject(new Error('请输入' + item.name));
} else if (
value?.source === 'relation' &&
!value?.value &&
!value?.relation
) {
return Promise.reject(new Error('请选择' + item.name));
} else if (value?.source === 'upper' && !value.upperKey) {
return Promise.reject(new Error('请选择' + item.name));
}
return Promise.resolve();
};
const onSave = () =>
new Promise((resolve, reject) => {
formRef.value?.validate().then((_data: any) => {
resolve(_data);
}).catch(() => {
reject(false)
})
});
defineExpose({ onSave });
</script>

View File

@ -0,0 +1,187 @@
<template>
<j-input-group compact>
<j-select
:options="[
{ label: '手动输入', value: 'fixed' },
{ label: '内置参数', value: 'upper' },
]"
style="width: 120px"
:value="value?.source"
@change="sourceChange"
/>
<template v-if="source === 'upper'">
<j-tree-select
v-model:value="upperKey"
:treeData="builtInList"
placeholder="请选择参数"
style="width: calc(100% - 120px)"
:fieldNames="{ label: 'name', value: 'id' }"
@change="(val, label, extra) => itemOnChange(undefined, val, label, extra)"
>
<template #title="{ fullName, description }">
<j-space>
{{ fullName }}
<span style="color: grey; margin-left: 5px">{{
description
}}</span>
</j-space>
</template>
</j-tree-select>
</template>
<template v-else>
<j-date-picker
:value="value.value"
allowClear
valueFormat='YYYY-MM-DD HH:mm:ss'
format="YYYY-MM-DD HH:mm:ss"
style="width: calc(100% - 120px)"
v-if="item.type === 'date'"
@change="(_, dateString) => itemOnChange(dateString)"
/>
<j-input-number
:value="value.value"
allowClear
style="width: calc(100% - 120px)"
v-else-if="item.type === 'number'"
:placeholder="`请输入${item.name}`"
@change="itemOnChange"
/>
<j-input
:value="value.value"
allowClear
style="width: calc(100% - 120px)"
v-else
:placeholder="`请输入${item.name}`"
@change="(e) => itemOnChange(e.target.value)"
/>
</template>
</j-input-group>
</template>
<script lang="ts" setup name='NotifyBuildIn'>
import { queryBuiltInParams } from '@/api/rule-engine/scene';
import { useSceneStore } from '@/store/scene';
import { storeToRefs } from 'pinia';
const sceneStore = useSceneStore();
const { data } = storeToRefs(sceneStore);
const props = defineProps({
value: {
type: Object,
default: () => {
return {
source: 'fixed',
value: undefined,
upperKey: undefined,
};
},
},
item: {
type: Object,
default: () => {},
},
name: {
type: Number,
default: 0,
},
});
const emit = defineEmits(['update:value', 'change']);
const source = computed(() => {
return props.value?.source || 'fixed';
});
const builtInList = ref<any[]>([]);
const upperKey = ref(props.value?.upperKey);
const sourceChange = (val: any) => {
emit('update:value', {
...props.value,
source: val,
value: undefined,
});
};
const itemOnChange = (val: any, _upperKey?: string, label?: any, extra?: any) => {
const item = extra?.triggerNode?.props
let othersColumns = ''
if (item && item.metadata) {
othersColumns = item.column
}
emit('update:value', {
...props.value,
value: val,
upperKey: _upperKey,
});
emit('change', {
sendTo: label?.[0] || val,
}, othersColumns);
};
const treeDataFilter = (arr: any[], type: string) => {
if (Array.isArray(arr) && arr.length) {
const list: any[] = [];
arr.map((item: any) => {
if (item.children) {
const children = treeDataFilter(item.children, type);
if (children.length) {
list.push({
...item,
title: item.name,
value: item.id,
disabled: true,
children,
});
}
} else {
if (
item.type === type ||
(type === 'double' &&
['int', 'float', 'double', 'long'].includes(item.type))
) {
list.push(item);
}
}
});
return list;
} else {
return [];
}
};
watch(
() => source.value,
(newVal) => {
const v = newVal;
if (v === 'upper') {
const params =
props.name - 1 >= 0 ? { action: props.name - 1 } : undefined;
queryBuiltInParams(unref(data), params).then((resp) => {
if (resp.status === 200) {
const arr = treeDataFilter(
resp.result as any[],
props.item.expands?.businessType || props.item?.type,
);
builtInList.value = arr;
}
});
}
},
{ deep: true, immediate: true },
);
watch(
() => props.value.upperKey,
(newVal) => {
upperKey.value = newVal;
},
{ immediate: true },
);
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,77 @@
<template>
<j-input
allowClear
placeholder="请上传文件"
v-model:value="url"
@change="onChange"
>
<template #addonAfter>
<j-upload
name="file"
:showUploadList="false"
:accept="'image/jpeg,image/png'"
:disabled="loading"
:headers="{
[TOKEN_KEY]: LocalStore.get(TOKEN_KEY),
}"
:action="`${BASE_API_PATH}/file/static`"
@change="handleChange"
@beforeUpload="handleBeforeUpload"
>
<j-button type="link" style="height: 30px">
<AIcon type="LoadingOutlined" v-if="loading" />
<AIcon type="PlusOutlined" v-else />
上传附件
</j-button>
</j-upload>
</template>
</j-input>
</template>
<script lang="ts" setup>
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { LocalStore, onlyMessage } from '@/utils/comm';
const props = defineProps({
id: {
type: String,
default: '',
},
value: {
type: String,
default: '',
},
});
const emit = defineEmits(['update:value']);
const url = ref(props.value || undefined);
const loading = ref<boolean>(false);
const handleChange = (info: any) => {
if (info.file.status === 'uploading') {
loading.value = true;
}
if (info.file.status === 'done') {
info.file.url = info.file.response?.result;
loading.value = false;
const result = info.file.response?.result;
emit('update:value', result);
}
};
const handleBeforeUpload = (file: any) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
onlyMessage('请上传正确格式图片', 'error');
}
const isSize = file.size / 1024 / 1024 < 4;
if (!isSize) {
onlyMessage(`图片大小必须小于4M`, 'error');
}
return isJpgOrPng && isSize;
};
const onChange = (e: any) => {
emit('update:value', e.target.value);
};
</script>

View File

@ -0,0 +1,65 @@
<template>
<j-select
style="width: 100%"
v-model:value="keys"
placeholder="请选择标签"
:options="tagsList"
@change="onChange"
/>
</template>
<script lang="ts" setup>
import TemplateApi from '@/api/notice/template';
const props = defineProps({
notify: {
type: Object,
default: () => {},
},
value: {
type: String,
default: ''
},
});
const emit = defineEmits(['update:value', 'change']);
const tagsList = ref<any[]>([]);
const keys = ref<string | undefined>(undefined);
const getDepartment = async (id: string) => {
const resp = await TemplateApi.getTags(id);
if (resp.status === 200) {
tagsList.value = resp.result.map((item: any) => ({value: item.id, label: item.name}))
}
};
watch(
() => props.value,
(newVal) => {
keys.value = newVal || undefined
},
{ immediate: true },
);
watch(
() => props.notify.notifierId,
(newVal) => {
if (newVal) {
getDepartment(newVal);
}
},
{ deep: true, immediate: true },
);
const onChange = (key: string, option: any) => {
emit('update:value', {
source: 'fixed',
value: key,
});
emit('change', option ? option?.label : '')
};
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,151 @@
<template>
<j-modal
:width="900"
visible
title="配置通知方式"
@cancel="emit('close')"
@ok="onSave"
>
<j-steps :current="current" size="small" @change="onChange">
<j-step v-for="item in stepList" :title="item" :key="item" />
</j-steps>
<div style="margin: 20px">
<template v-if="current === 0">
<NotifyWay v-model:value="formModel.notifyType" />
</template>
<template v-if="current === 1">
<NotifyConfig
v-model:value="formModel.notifierId"
:notifyType="formModel.notifyType"
/>
</template>
<template v-if="current === 2">
<NotifyTemplate
v-model:value="formModel.templateId"
:notifierId="formModel.notifierId"
/>
</template>
<template v-if="current === 3">
<VariableDefinitions
:variableDefinitions="_variableDefinitions"
:value="formModel.variables"
:notify="formModel"
ref="variableRef"
/>
</template>
<template v-if="current === 4">
<Role v-model="formModel.role" />
</template>
</div>
<template #footer>
<j-space>
<j-button v-if="current === 0" @click="emit('close')"
>取消</j-button
>
<j-button v-else @click="onPrev">上一步</j-button>
<j-button
type="primary"
@click="onNext"
v-if="current !== stepList.length - 1"
>下一步</j-button
>
<j-button type="primary" @click="onSave" v-else>确认</j-button>
</j-space>
</template>
</j-modal>
</template>
<script lang="ts" setup>
import NotifyWay from './components/NotifyWay.vue';
import NotifyConfig from './components/NotifyConfig.vue';
import NotifyTemplate from './components/NotifyTemplate.vue';
import VariableDefinitions from './components/VariableDefinitions.vue';
import Role from '../Role/index.vue';
import { onlyMessage } from '@/utils/comm';
import Template from '@/api/notice/template';
const emit = defineEmits(['close', 'save']);
const stepList = [
'选择通知方式',
'选择通知配置',
'选择通知模板',
'配置模板变量',
'配置用户权限',
];
const current = ref<number>(0);
const variable = ref([]);
const formModel = reactive({
notifyType: '',
notifierId: '',
templateId: '',
variables: undefined,
role: [],
});
const variableRef = ref();
const _variableDefinitions = computed(() => {
const arr = ['user', 'org']
return variable.value.filter((item: any) => {
const _type = item.expands?.businessType || item.type || ''
return !arr.includes(_type)
})
})
const jumpStep = async (val: number) => {
if (val === 1) {
if (formModel.notifyType) {
current.value = val;
} else {
onlyMessage('请选择通知方式', 'error');
}
} else if (val === 2) {
if (formModel.notifierId) {
current.value = val;
} else {
onlyMessage('请选择通知配置', 'error');
}
} else if (val === 3) {
if (formModel.templateId) {
const resp = await Template.getTemplateDetail(formModel.templateId);
if (resp.status === 200) {
variable.value = resp.result?.variableDefinitions || [];
current.value = val;
}
} else {
onlyMessage('请选择通知模板', 'error');
}
} else if (val === 4) {
if (_variableDefinitions.value.length) {
formModel.variables = await variableRef.value.onSave();
if (formModel.variables) {
current.value = val;
} else {
onlyMessage('请配置模版变量', 'error');
}
} else {
current.value = val;
}
}
};
const onPrev = () => {
current.value -= 1;
};
const onNext = () => {
jumpStep(current.value + 1);
};
const onChange = (cur: number) => {
jumpStep(cur);
};
const onSave = () => {
if (formModel.role.length) {
emit('save');
} else {
onlyMessage('请配置角色权限', 'error');
}
};
</script>

View File

@ -0,0 +1,424 @@
<template>
<page-container>
<FullPage>
<div style="padding: 24px">
<div class="alert">
<AIcon type="InfoCircleOutlined" />
你可以为每种通知类型配置不同的通知方式与通知模版
</div>
<div class="alert">
<AIcon type="InfoCircleOutlined" />
默认平台中所有用户都能接收到通知如需限制接收权限可以在配置通知方式时完成或在通知类型后方的权限控制处配置外层权限
</div>
<div style="margin-top: 20px">
<j-collapse :bordered="false" v-model:activeKey="activeKey">
<template #expandIcon="{ isActive }">
<AIcon
type="CaretRightOutlined"
:rotate="isActive ? 90 : 0"
/>
</template>
<j-collapse-panel
v-for="item in dataSource"
:key="item.id"
class="custom"
>
<template #header
><h3>{{ item.name }}</h3></template
>
<div class="child">
<template
v-for="child in item.children"
:key="child.id"
>
<div class="child-item">
<div class="child-item-left">
<div style="font-weight: 600">
{{ child.name }}
</div>
<div
class="child-item-left-auth"
:class="{
active: child.active,
}"
@click="onAuth(item, child)"
>
<AIcon type="UserOutlined" />
<span>权限控制</span>
</div>
</div>
<div class="child-item-right">
<MCarousel :data="child.children">
<template #card="slotProps">
<div class="box-item">
<j-dropdown>
<div
class="box-item-img"
>
<img
style="
width: 100%;
"
:src="
getImage(
`/notice/${slotProps?.type}.png`,
)
"
/>
</div>
<template #overlay>
<j-menu mode="">
<j-menu-item>
<PermissionButton
@click="
onView(
item,
child,
)
"
type="link"
:hasPermission="
true
"
>
查看
</PermissionButton>
</j-menu-item>
<j-menu-item>
<PermissionButton
@click="
onEdit(
item,
child,
)
"
type="link"
:hasPermission="
true
"
>
编辑
</PermissionButton>
</j-menu-item>
<j-menu-item>
<PermissionButton
@click="
onDelete(
item,
child,
)
"
danger
type="link"
:hasPermission="
true
"
>
删除
</PermissionButton>
</j-menu-item>
</j-menu>
</template>
</j-dropdown>
<div
class="box-item-text"
>
{{ item.name }}
</div>
</div>
</template>
<template #add>
<div class="box-item">
<div
@click="
onAdd(
item,
child,
)
"
class="box-item-img"
>
<AIcon
style="
font-size: 20px;
"
type="PlusOutlined"
/>
</div>
<div
class="box-item-text"
></div>
</div>
</template>
</MCarousel>
</div>
</div>
</template>
</div>
</j-collapse-panel>
</j-collapse>
</div>
</div>
</FullPage>
<Save v-if="visible" @close="visible = false" @save="onSave" />
<Detail v-if="detailVisible" @close="detailVisible = false" />
<Auth
v-if="authVisible"
@close="authVisible = false"
@save="onAuthSave"
/>
</page-container>
</template>
<script lang="ts" setup>
import MCarousel from '@/components/MCarousel/index.vue';
import { getImage } from '@/utils/comm';
import Save from './components/Save/index.vue';
import Detail from './components/Detail/index.vue';
import Auth from './components/Auth/index.vue';
const dataSource = ref([
{
id: 'a',
name: '告警',
children: [
{
id: 'product',
name: '产品告警',
active: true,
children: [
{
id: 'sms9',
name: '站内信',
type: 'sms',
},
{
id: 'dingtalk8',
name: '钉钉',
type: 'dingtalk',
},
{
id: 'wechat7',
name: '微信',
type: 'wechat',
},
{
id: 'email6',
name: '邮箱',
type: 'email',
},
{
id: 'dingtalk5',
name: '钉钉',
type: 'dingtalk',
},
{
id: 'wechat4',
name: '微信',
type: 'wechat',
},
{
id: 'email3',
name: '邮箱',
type: 'email',
},
{
id: 'email2',
name: '邮箱',
type: 'email',
},
{
id: 'email1',
name: '邮箱',
type: 'email',
},
],
},
{
id: 'device',
name: '设备告警',
active: false,
children: [
{
id: 'sms11',
name: '站内信',
type: 'sms',
},
{
id: 'wechat11',
name: '微信',
type: 'wechat',
},
{
id: 'voice11',
name: '语音',
type: 'voice',
},
],
},
],
},
{
id: 'b',
name: '系统监控',
children: [
{
id: 'cache',
name: '缓冲区数据丢弃',
active: false,
children: [
{
id: 'message111',
name: '站内信',
type: 'sms',
},
],
},
{
id: 'mqtt',
name: 'MQTT并发限制',
active: false,
children: [
{
id: 'message22',
name: '站内信',
type: 'sms',
},
],
},
],
},
{
id: 'c',
name: '业务监控',
children: [
{
id: 'error',
name: '透传消息解析异常',
active: false,
children: [
{
id: 'message333',
name: '站内信',
type: 'sms',
},
],
},
],
},
]);
const activeKey = ['a', 'b', 'c'];
const visible = ref<boolean>(false);
const detailVisible = ref<boolean>(false);
const authVisible = ref<boolean>(false);
const current = reactive({
item: {},
child: {},
});
const onAdd = (_item: any, _child: any) => {
(current.child = _child), (current.item = _item);
visible.value = true;
};
const onView = (_item: any, _child: any) => {
(current.child = _child), (current.item = _item);
detailVisible.value = true;
};
const onEdit = (_item: any, _child: any) => {
(current.child = _child), (current.item = _item);
visible.value = true;
};
const onDelete = (_item: any, _child: any) => {};
const onSave = () => {
visible.value = false;
};
const onAuth = (_item: any, _child: any) => {
(current.child = _child), (current.item = _item);
authVisible.value = true;
};
const onAuthSave = (_data: string[]) => {
console.log(_data);
authVisible.value = false;
};
</script>
<style lang="less" scoped>
.alert {
height: 40px;
padding-left: 10px;
margin-bottom: 10px;
color: rgba(0, 0, 0, 0.55);
line-height: 40px;
background-color: #f6f6f6;
}
.custom {
background: #f7f7f7;
border-radius: 4px;
border: 0;
overflow: hidden;
}
.child {
background-color: white;
padding: 10px;
.child-item {
padding: 10px 20px;
margin: 5px;
background: #f7f7f7;
display: flex;
justify-content: space-between;
align-items: center;
.child-item-left {
display: flex;
align-items: center;
div {
display: flex;
margin-right: 30px;
flex-direction: column;
justify-content: center;
align-items: center;
}
.child-item-left-auth {
cursor: pointer;
&:hover {
color: @primary-color-hover;
}
&.active {
color: @primary-color;
}
}
}
.child-item-right {
display: flex;
.box-item {
margin-left: 10px;
.box-item-img {
background-color: #fff;
width: 48px;
height: 48px;
display: flex;
justify-content: center;
align-items: center;
}
.box-item-text {
width: 100%;
text-align: center;
height: 20px;
}
}
}
}
}
</style>