---初始化后台管理web页面项目
This commit is contained in:
204
src/layouts/default/header/components/Breadcrumb.vue
Normal file
204
src/layouts/default/header/components/Breadcrumb.vue
Normal file
@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div :class="[prefixCls, `${prefixCls}--${theme}`]">
|
||||
<a-breadcrumb :routes="routes">
|
||||
<template #itemRender="{ route, routes: routesMatched, paths }">
|
||||
<Icon :icon="getIcon(route)" v-if="getShowBreadCrumbIcon && getIcon(route)" />
|
||||
<span v-if="!hasRedirect(routesMatched, route)">
|
||||
{{ t(route.name || route.meta.title) }}
|
||||
</span>
|
||||
<router-link v-else to="" @click="handleClick(route, paths, $event)">
|
||||
{{ t(route.name || route.meta.title) }}
|
||||
</router-link>
|
||||
</template>
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { RouteLocationMatched } from 'vue-router';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { Menu } from '/@/router/types';
|
||||
|
||||
import { defineComponent, ref, watchEffect } from 'vue';
|
||||
|
||||
import { Breadcrumb } from 'ant-design-vue';
|
||||
import Icon from '/@/components/Icon';
|
||||
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
import { useGo } from '/@/hooks/web/usePage';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { isString } from '/@/utils/is';
|
||||
import { filter } from '/@/utils/helper/treeHelper';
|
||||
import { getMenus } from '/@/router/menus';
|
||||
|
||||
import { REDIRECT_NAME } from '/@/router/constant';
|
||||
import { getAllParentPath } from '/@/router/helper/menuHelper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LayoutBreadcrumb',
|
||||
components: { Icon, [Breadcrumb.name]: Breadcrumb },
|
||||
props: {
|
||||
theme: propTypes.oneOf(['dark', 'light']),
|
||||
},
|
||||
setup() {
|
||||
const routes = ref<RouteLocationMatched[]>([]);
|
||||
const { currentRoute } = useRouter();
|
||||
const { prefixCls } = useDesign('layout-breadcrumb');
|
||||
const { getShowBreadCrumbIcon } = useRootSetting();
|
||||
const go = useGo();
|
||||
|
||||
const { t } = useI18n();
|
||||
watchEffect(async () => {
|
||||
if (currentRoute.value.name === REDIRECT_NAME) return;
|
||||
const menus = await getMenus();
|
||||
|
||||
const routeMatched = currentRoute.value.matched;
|
||||
const cur = routeMatched?.[routeMatched.length - 1];
|
||||
let path = currentRoute.value.path;
|
||||
|
||||
if (cur && cur?.meta?.currentActiveMenu) {
|
||||
path = cur.meta.currentActiveMenu as string;
|
||||
}
|
||||
|
||||
const parent = getAllParentPath(menus, path);
|
||||
const filterMenus = menus.filter((item) => item.path === parent[0]);
|
||||
const matched = getMatched(filterMenus, parent) as any;
|
||||
|
||||
if (!matched || matched.length === 0) return;
|
||||
|
||||
const breadcrumbList = filterItem(matched);
|
||||
|
||||
if (currentRoute.value.meta?.currentActiveMenu) {
|
||||
breadcrumbList.push({
|
||||
...currentRoute.value,
|
||||
name: currentRoute.value.meta?.title || currentRoute.value.name,
|
||||
} as unknown as RouteLocationMatched);
|
||||
}
|
||||
routes.value = breadcrumbList;
|
||||
});
|
||||
|
||||
function getMatched(menus: Menu[], parent: string[]) {
|
||||
const metched: Menu[] = [];
|
||||
menus.forEach((item) => {
|
||||
if (parent.includes(item.path)) {
|
||||
metched.push({
|
||||
...item,
|
||||
name: item.meta?.title || item.name,
|
||||
});
|
||||
}
|
||||
if (item.children?.length) {
|
||||
metched.push(...getMatched(item.children, parent));
|
||||
}
|
||||
});
|
||||
return metched;
|
||||
}
|
||||
|
||||
function filterItem(list: RouteLocationMatched[]) {
|
||||
return filter(list, (item) => {
|
||||
const { meta, name } = item;
|
||||
if (!meta) {
|
||||
return !!name;
|
||||
}
|
||||
const { title, hideBreadcrumb, hideMenu } = meta;
|
||||
if (!title || hideBreadcrumb || hideMenu) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).filter((item) => !item.meta?.hideBreadcrumb);
|
||||
}
|
||||
|
||||
function handleClick(route: RouteLocationMatched, paths: string[], e: Event) {
|
||||
e?.preventDefault();
|
||||
const { children, redirect, meta } = route;
|
||||
|
||||
if (children?.length && !redirect) {
|
||||
e?.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (meta?.carryParam) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (redirect && isString(redirect)) {
|
||||
go(redirect);
|
||||
} else {
|
||||
let goPath = '';
|
||||
if (paths.length === 1) {
|
||||
goPath = paths[0];
|
||||
} else {
|
||||
const ps = paths.slice(1);
|
||||
const lastPath = ps.pop() || '';
|
||||
goPath = `${lastPath}`;
|
||||
}
|
||||
goPath = /^\//.test(goPath) ? goPath : `/${goPath}`;
|
||||
go(goPath);
|
||||
}
|
||||
}
|
||||
|
||||
function hasRedirect(routes: RouteLocationMatched[], route: RouteLocationMatched) {
|
||||
return routes.indexOf(route) !== routes.length - 1;
|
||||
}
|
||||
|
||||
function getIcon(route) {
|
||||
return route.icon || route.meta?.icon;
|
||||
}
|
||||
|
||||
return { routes, t, prefixCls, getIcon, getShowBreadCrumbIcon, handleClick, hasRedirect };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-layout-breadcrumb';
|
||||
|
||||
.@{prefix-cls} {
|
||||
display: flex;
|
||||
padding: 0 8px;
|
||||
align-items: center;
|
||||
|
||||
.ant-breadcrumb-link {
|
||||
.anticon {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&--light {
|
||||
.ant-breadcrumb-link {
|
||||
color: @breadcrumb-item-normal-color;
|
||||
|
||||
a {
|
||||
color: rgb(0 0 0 / 65%);
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-breadcrumb-separator {
|
||||
color: @breadcrumb-item-normal-color;
|
||||
}
|
||||
}
|
||||
|
||||
&--dark {
|
||||
.ant-breadcrumb-link {
|
||||
color: rgb(255 255 255 / 60%);
|
||||
|
||||
a {
|
||||
color: rgb(255 255 255 / 80%);
|
||||
|
||||
&:hover {
|
||||
color: @white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-breadcrumb-separator,
|
||||
.anticon {
|
||||
color: rgb(255 255 255 / 80%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
48
src/layouts/default/header/components/ErrorAction.vue
Normal file
48
src/layouts/default/header/components/ErrorAction.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<Tooltip
|
||||
:title="t('layout.header.tooltipErrorLog')"
|
||||
placement="bottom"
|
||||
:mouseEnterDelay="0.5"
|
||||
@click="handleToErrorList"
|
||||
>
|
||||
<Badge :count="getCount" :offset="[0, 10]" :overflowCount="99">
|
||||
<Icon icon="ion:bug-outline" />
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { Tooltip, Badge } from 'ant-design-vue';
|
||||
import Icon from '/@/components/Icon';
|
||||
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useErrorLogStore } from '/@/store/modules/errorLog';
|
||||
import { PageEnum } from '/@/enums/pageEnum';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ErrorAction',
|
||||
components: { Icon, Tooltip, Badge },
|
||||
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { push } = useRouter();
|
||||
const errorLogStore = useErrorLogStore();
|
||||
|
||||
const getCount = computed(() => errorLogStore.getErrorLogListCount);
|
||||
|
||||
function handleToErrorList() {
|
||||
push(PageEnum.ERROR_LOG_PAGE).then(() => {
|
||||
errorLogStore.setErrorLogListCount(0);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
getCount,
|
||||
handleToErrorList,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
35
src/layouts/default/header/components/FullScreen.vue
Normal file
35
src/layouts/default/header/components/FullScreen.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<Tooltip :title="getTitle" placement="bottom" :mouseEnterDelay="0.5">
|
||||
<span @click="toggle">
|
||||
<Icon icon="material-symbols:fit-screen-rounded" size="26" v-if="!isFullscreen" />
|
||||
<Icon icon="mingcute:fullscreen-exit-fill" size="24" v-else />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, unref } from 'vue';
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FullScreen',
|
||||
components: { Icon, Tooltip },
|
||||
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { toggle, isFullscreen } = useFullscreen();
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return unref(isFullscreen) ? t('退出全屏') : t('全屏');
|
||||
});
|
||||
|
||||
return {
|
||||
getTitle,
|
||||
isFullscreen,
|
||||
toggle,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
166
src/layouts/default/header/components/childSystem.vue
Normal file
166
src/layouts/default/header/components/childSystem.vue
Normal file
@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<AMenu
|
||||
v-if="getShowContent && getShowBread"
|
||||
:selectedKeys="cur"
|
||||
mode="horizontal"
|
||||
:class="[prefixCls, `${prefixCls}--${theme}`]"
|
||||
>
|
||||
<MenuItem v-for="item in system" @click="changeSystem(item.id)" :key="item.id" class="item-system">
|
||||
{{ item.name }}
|
||||
</MenuItem>
|
||||
</AMenu>
|
||||
<Tooltip title="子系统" :mouseEnterDelay="0.5">
|
||||
<Dropdown
|
||||
v-if="getShowTopMenu && !getIsMobile"
|
||||
placement="bottom"
|
||||
:trigger="['click']"
|
||||
:dropMenuList="system"
|
||||
:selectedKeys="cur"
|
||||
@menu-event="handleMenuEvent"
|
||||
overlayClassName="app-locale-picker-overlay"
|
||||
>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item :key="item.event" v-for="item in system">
|
||||
<Icon
|
||||
:icon="item.icon || 'ant-design:appstore-outlined'"
|
||||
color="#DEDFFF"
|
||||
size="26"
|
||||
style="vertical-align: -7px"
|
||||
/>
|
||||
{{ item.text }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<span class="cursor-pointer flex items-center p-2">
|
||||
<Icon icon="icon-park-outline:system" size="22" />
|
||||
</span>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref, watch } from 'vue';
|
||||
import type { MenuTheme } from 'ant-design-vue';
|
||||
import { Breadcrumb, Tooltip, Menu } from 'ant-design-vue';
|
||||
import Icon from '/@/components/Icon';
|
||||
import { Dropdown } from '/@/components/Dropdown';
|
||||
import type { DropMenu } from '/@/components/Dropdown';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useAppInject } from '/@/hooks/web/useAppInject';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { usePermissionStore } from '/@/store/modules/permission';
|
||||
export default defineComponent({
|
||||
name: 'LayoutBreadcrumb',
|
||||
components: {
|
||||
Icon,
|
||||
[Breadcrumb.name]: Breadcrumb,
|
||||
Tooltip,
|
||||
AMenu: Menu,
|
||||
MenuItem: Menu.Item,
|
||||
Dropdown,
|
||||
},
|
||||
props: {
|
||||
theme: propTypes.oneOf(['dark', 'light']),
|
||||
},
|
||||
setup() {
|
||||
const permissionStore = usePermissionStore();
|
||||
const { subSystemList } = storeToRefs(permissionStore);
|
||||
const Mtheme = ref<MenuTheme>('dark');
|
||||
const cur = ref<string[]>([permissionStore.getSubSystem]);
|
||||
|
||||
const { prefixCls } = useDesign('layout-breadcrumb');
|
||||
const { getIsMobile } = useAppInject();
|
||||
const system = ref<any[]>([]);
|
||||
const { t } = useI18n();
|
||||
const { getShowTopMenu } = useMenuSetting();
|
||||
const { getShowContent, getShowBread } = useHeaderSetting();
|
||||
watch(
|
||||
() => subSystemList.value,
|
||||
(v) => {
|
||||
system.value = v;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
onMounted(async () => {
|
||||
await permissionStore.changeSubsystem(getShowTopMenu.value, getIsMobile.value);
|
||||
system.value = permissionStore.getSubSysList;
|
||||
if(system.value.length>0){
|
||||
changeSystem(system.value[0].id);
|
||||
}
|
||||
});
|
||||
|
||||
//切换系统
|
||||
function changeSystem(systemId: string) {
|
||||
permissionStore.setSubSystem(systemId);
|
||||
cur.value = [systemId];
|
||||
}
|
||||
|
||||
function handleMenuEvent(menu: DropMenu) {
|
||||
if (cur.value[0] === menu.event) {
|
||||
return;
|
||||
}
|
||||
cur.value = [menu.event as string];
|
||||
|
||||
permissionStore.setSubSystem(menu.event as string);
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
prefixCls,
|
||||
cur,
|
||||
changeSystem,
|
||||
system,
|
||||
Mtheme,
|
||||
getShowContent,
|
||||
getShowTopMenu,
|
||||
getShowBread,
|
||||
getIsMobile,
|
||||
handleMenuEvent,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-layout-breadcrumb';
|
||||
|
||||
.@{prefix-cls} {
|
||||
&.ant-menu {
|
||||
background: none;
|
||||
border: 0;
|
||||
|
||||
.ant-menu-item {
|
||||
padding: 0 10px;
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.ant-menu-item-icon {
|
||||
background: rgb(171 170 205 / 30%);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgb(171 170 205 / 30%);
|
||||
margin: 0 5px;
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-menu-item-selected {
|
||||
background: none;
|
||||
|
||||
.ant-menu-item-icon {
|
||||
border: 1px solid rgb(222 223 255);
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
14
src/layouts/default/header/components/index.ts
Normal file
14
src/layouts/default/header/components/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import FullScreen from './FullScreen.vue';
|
||||
|
||||
export const UserDropDown = createAsyncComponent(() => import('./user-dropdown/index.vue'), {
|
||||
loading: true,
|
||||
});
|
||||
|
||||
export const LayoutBreadcrumb = createAsyncComponent(() => import('./childSystem.vue'));
|
||||
|
||||
export const Notify = createAsyncComponent(() => import('./notify/index.vue'));
|
||||
|
||||
export const ErrorAction = createAsyncComponent(() => import('./ErrorAction.vue'));
|
||||
|
||||
export { FullScreen };
|
||||
126
src/layouts/default/header/components/lock/LockModal.vue
Normal file
126
src/layouts/default/header/components/lock/LockModal.vue
Normal file
@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
:footer="null"
|
||||
:title="t('锁定屏幕')"
|
||||
v-bind="$attrs"
|
||||
:class="prefixCls"
|
||||
@register="register"
|
||||
>
|
||||
<div :class="`${prefixCls}__entry`">
|
||||
<div :class="`${prefixCls}__header`">
|
||||
<img :src="avatar" :class="`${prefixCls}__header-img`" />
|
||||
<p :class="`${prefixCls}__header-name`">
|
||||
{{ getRealName }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BasicForm @register="registerForm" />
|
||||
|
||||
<div :class="`${prefixCls}__footer`">
|
||||
<a-button type="primary" block class="mt-2" @click="handleLock">
|
||||
{{ t('锁定') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal/index';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { useLockStore } from '/@/store/modules/lock';
|
||||
import headerImg from '/@/assets/images/header.jpg';
|
||||
export default defineComponent({
|
||||
name: 'LockModal',
|
||||
components: { BasicModal, BasicForm },
|
||||
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { prefixCls } = useDesign('header-lock-modal');
|
||||
const userStore = useUserStore();
|
||||
const lockStore = useLockStore();
|
||||
|
||||
const getRealName = computed(() => userStore.getUserInfo?.realName);
|
||||
const [register, { closeModal }] = useModalInner();
|
||||
|
||||
const [registerForm, { validateFields, resetFields }] = useForm({
|
||||
showActionButtonGroup: false,
|
||||
schemas: [
|
||||
{
|
||||
field: 'password',
|
||||
label: t('锁屏密码'),
|
||||
colProps: {
|
||||
span: 24,
|
||||
},
|
||||
component: 'InputPassword',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
async function handleLock() {
|
||||
const values = (await validateFields()) as any;
|
||||
const password: string | undefined = values.password;
|
||||
closeModal();
|
||||
|
||||
lockStore.setLockInfo({
|
||||
isLock: true,
|
||||
pwd: password,
|
||||
});
|
||||
await resetFields();
|
||||
}
|
||||
|
||||
const avatar = computed(() => {
|
||||
const { avatar } = userStore.getUserInfo;
|
||||
return avatar || headerImg;
|
||||
});
|
||||
|
||||
return {
|
||||
t,
|
||||
prefixCls,
|
||||
getRealName,
|
||||
register,
|
||||
registerForm,
|
||||
handleLock,
|
||||
avatar,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-header-lock-modal';
|
||||
|
||||
.@{prefix-cls} {
|
||||
&__entry {
|
||||
position: relative;
|
||||
//height: 240px;
|
||||
padding: 130px 30px 30px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: calc(50% - 45px);
|
||||
width: auto;
|
||||
text-align: center;
|
||||
|
||||
&-img {
|
||||
width: 70px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&-name {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
189
src/layouts/default/header/components/notify/NoticeList.vue
Normal file
189
src/layouts/default/header/components/notify/NoticeList.vue
Normal file
@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<a-list :class="prefixCls" bordered :pagination="getPagination">
|
||||
<template v-for="item in getData" :key="item.id">
|
||||
<a-list-item class="list-item">
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<div class="title">
|
||||
<a-typography-paragraph
|
||||
@click="handleTitleClick(item)"
|
||||
style="width: 100%; margin-bottom: 0 !important"
|
||||
:style="{ cursor: isTitleClickable ? 'pointer' : '' }"
|
||||
:delete="!!item.titleDelete"
|
||||
:ellipsis="
|
||||
$props.titleRows && $props.titleRows > 0
|
||||
? { rows: $props.titleRows, tooltip: !!item.title }
|
||||
: false
|
||||
"
|
||||
:content="item.title"
|
||||
/>
|
||||
<div class="extra" v-if="item.extra">
|
||||
<a-tag class="tag" :color="item.color">
|
||||
{{ item.extra }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #avatar>
|
||||
<a-avatar v-if="item.avatar" class="avatar" :src="item.avatar" />
|
||||
<span v-else> {{ item.avatar }}</span>
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
<div>
|
||||
<div class="description" v-if="item.description">
|
||||
<a-typography-paragraph
|
||||
style="width: 100%; margin-bottom: 0 !important"
|
||||
:ellipsis="
|
||||
$props.descRows && $props.descRows > 0
|
||||
? { rows: $props.descRows, tooltip: !!item.description }
|
||||
: false
|
||||
"
|
||||
:content="item.description"
|
||||
/>
|
||||
</div>
|
||||
<div class="datetime">
|
||||
{{ item.datetime }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType, ref, watch, unref } from 'vue';
|
||||
import { ListItem } from './data';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { List, Avatar, Tag, Typography } from 'ant-design-vue';
|
||||
import { isNumber } from '/@/utils/is';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
[Avatar.name]: Avatar,
|
||||
[List.name]: List,
|
||||
[List.Item.name]: List.Item,
|
||||
AListItemMeta: List.Item.Meta,
|
||||
ATypographyParagraph: Typography.Paragraph,
|
||||
[Tag.name]: Tag,
|
||||
},
|
||||
props: {
|
||||
list: {
|
||||
type: Array as PropType<ListItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
pageSize: {
|
||||
type: [Boolean, Number] as PropType<Boolean | Number>,
|
||||
default: 5,
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
titleRows: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
descRows: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
onTitleClick: {
|
||||
type: Function as PropType<(Recordable) => void>,
|
||||
},
|
||||
},
|
||||
emits: ['update:currentPage'],
|
||||
setup(props, { emit }) {
|
||||
const { prefixCls } = useDesign('header-notify-list');
|
||||
const current = ref(props.currentPage || 1);
|
||||
const getData = computed(() => {
|
||||
const { pageSize, list } = props;
|
||||
if (pageSize === false) return [];
|
||||
let size = isNumber(pageSize) ? pageSize : 5;
|
||||
return list.slice(size * (unref(current) - 1), size * unref(current));
|
||||
});
|
||||
watch(
|
||||
() => props.currentPage,
|
||||
(v) => {
|
||||
current.value = v;
|
||||
},
|
||||
);
|
||||
const isTitleClickable = computed(() => !!props.onTitleClick);
|
||||
const getPagination = computed(() => {
|
||||
const { list, pageSize } = props;
|
||||
if (pageSize > 0 && list && list.length > pageSize) {
|
||||
return {
|
||||
total: list.length,
|
||||
pageSize,
|
||||
//size: 'small',
|
||||
current: unref(current),
|
||||
onChange(page) {
|
||||
current.value = page;
|
||||
emit('update:currentPage', page);
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
function handleTitleClick(item: ListItem) {
|
||||
props.onTitleClick && props.onTitleClick(item);
|
||||
}
|
||||
|
||||
return { prefixCls, getPagination, getData, handleTitleClick, isTitleClickable };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-header-notify-list';
|
||||
|
||||
.@{prefix-cls} {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::v-deep(.ant-pagination-disabled) {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
&-item {
|
||||
padding: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
font-weight: normal;
|
||||
|
||||
.extra {
|
||||
float: right;
|
||||
margin-top: -1.5px;
|
||||
margin-right: 0;
|
||||
font-weight: normal;
|
||||
|
||||
.tag {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.datetime {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
58
src/layouts/default/header/components/notify/data.ts
Normal file
58
src/layouts/default/header/components/notify/data.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
const { t } = useI18n();
|
||||
export interface ListItem {
|
||||
id: string;
|
||||
avatar?: string;
|
||||
// 通知的标题内容
|
||||
title: string;
|
||||
// 是否在标题上显示删除线
|
||||
titleDelete?: boolean;
|
||||
datetime?: string;
|
||||
type?: string;
|
||||
read?: number;
|
||||
description?: string;
|
||||
clickClose?: boolean;
|
||||
extra?: string;
|
||||
color?: string;
|
||||
taskId?: string;
|
||||
processId?: string;
|
||||
schemaId?: string;
|
||||
timeFormat?: string;
|
||||
}
|
||||
|
||||
export interface TabItem {
|
||||
key: string;
|
||||
name: string;
|
||||
unreadNum: number;
|
||||
list: ListItem[];
|
||||
read?: ListItem[];
|
||||
unreadlist?: ListItem[];
|
||||
}
|
||||
|
||||
export const tabListData: TabItem[] = [
|
||||
{
|
||||
key: '1',
|
||||
name: t('新闻'),
|
||||
list: [],
|
||||
unreadNum: 0,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
name: t('通知公告'),
|
||||
list: [],
|
||||
unreadNum: 0,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
name: t('日程'),
|
||||
list: [],
|
||||
unreadNum: 0,
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
name: t('工作流'),
|
||||
list: [],
|
||||
read: [],
|
||||
unreadNum: 0,
|
||||
},
|
||||
];
|
||||
461
src/layouts/default/header/components/notify/index.vue
Normal file
461
src/layouts/default/header/components/notify/index.vue
Normal file
@ -0,0 +1,461 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<Popover title="" trigger="click" :overlayClassName="`${prefixCls}__overlay`">
|
||||
<Badge :count="count" dot :numberStyle="numberStyle">
|
||||
<Icon icon="ion:notifcations" size="22" />
|
||||
</Badge>
|
||||
<template #content>
|
||||
<Tabs>
|
||||
<template v-for="item in listData" :key="item.key">
|
||||
<TabPane>
|
||||
<template #tab>
|
||||
{{ item.name }}
|
||||
<span v-if="item.unreadNum !== 0">({{ item.unreadNum }})</span>
|
||||
</template>
|
||||
|
||||
<div v-if="item.key === '4'" class="min-h-88">
|
||||
<div>
|
||||
<div class="list-item">
|
||||
<span class="header-title">{{ t('流程审批') }}</span>
|
||||
<router-link
|
||||
class="opr"
|
||||
:to="{
|
||||
path: '/task/processtasks',
|
||||
query: {},
|
||||
}"
|
||||
>
|
||||
{{ t('查看更多') }} 》
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-if="item.list.length > 0">
|
||||
<div class="readed-mark" v-for="it in item.list" :key="it.id">
|
||||
<div
|
||||
class="list-data-item"
|
||||
:class="it.read ? 'readed' : ''"
|
||||
@click="ApprovalHandle(it, item.key, 1)"
|
||||
>
|
||||
<span class="list-item-title">{{ it.title }}</span>
|
||||
<span class="list-item-time">{{ it.timeFormat }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-empty :image="simpleImage" v-else />
|
||||
</div>
|
||||
<div>
|
||||
<div class="list-item">
|
||||
<span class="header-title">{{ t('流程传阅') }}</span>
|
||||
<router-link
|
||||
class="opr"
|
||||
:to="{
|
||||
path: '/task/processtasks',
|
||||
query: { name: 'MyCirculation' },
|
||||
}"
|
||||
>
|
||||
{{ t('查看更多') }} 》
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-if="item.read!.length>0">
|
||||
<div class="list-item readed-mark" v-for="it in item.read" :key="it.id">
|
||||
<div
|
||||
class="list-data-item"
|
||||
:class="it.read ? 'readed' : ''"
|
||||
@click="ApprovalHandle(it, item.key, 2)"
|
||||
>
|
||||
<span class="list-item-title">{{ it.title }}</span>
|
||||
<span class="list-item-time">{{ it.timeFormat }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-empty :image="simpleImage" v-else />
|
||||
</div>
|
||||
<div class="notice-footer">
|
||||
<span @click="setReadAll(item.key)">{{ t('全部设置已读') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="h-88">
|
||||
<div v-if="item.list.length > 0" class="h-82">
|
||||
<div
|
||||
class="list-item readed-mark"
|
||||
v-for="it in item.list"
|
||||
:key="it.id"
|
||||
:class="it.read ? 'readed' : ''"
|
||||
@click="
|
||||
() => {
|
||||
it.read = 1;
|
||||
setReadSingle(it.id, item.key);
|
||||
}
|
||||
"
|
||||
>
|
||||
<span class="list-item-title">
|
||||
<a-tooltip>
|
||||
<template #title>{{ it.title }}</template>
|
||||
{{ it.title }}
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<span class="list-item-time">{{ it.timeFormat }}</span>
|
||||
</div>
|
||||
<div class="notice-footer"
|
||||
><span @click="setReadAll(item.key)">{{ t('全部设置已读') }}</span
|
||||
><span>{{ t('查看更多') }} 》</span></div
|
||||
>
|
||||
</div>
|
||||
<a-empty :image="simpleImage" v-else />
|
||||
</div>
|
||||
</TabPane>
|
||||
</template>
|
||||
</Tabs>
|
||||
<ApprovalProcess
|
||||
v-if="Approval.visible"
|
||||
:taskId="Approval.taskId || ''"
|
||||
:processId="Approval.processId || ''"
|
||||
:schemaId="Approval.schemaId || ''"
|
||||
:visible="Approval.visible"
|
||||
@close="
|
||||
() => {
|
||||
Approval.visible = false;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<LookProcess
|
||||
v-if="LookData.visible"
|
||||
:taskId="LookData.taskId || ''"
|
||||
:processId="LookData.processId || ''"
|
||||
:visible="LookData.visible"
|
||||
@close="
|
||||
() => {
|
||||
LookData.visible = false;
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, onUnmounted } from 'vue';
|
||||
import { Popover, Tabs, Badge } from 'ant-design-vue';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { tabListData } from './data';
|
||||
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
|
||||
import {
|
||||
getOaMessage,
|
||||
getOaNews,
|
||||
setOaRead,
|
||||
setSingleRead,
|
||||
setWorkReadAll,
|
||||
getScheduleMsg,
|
||||
setScheduleRead,
|
||||
setScheduleReadAll,
|
||||
} from '/@/api/system/login';
|
||||
import { Empty } from 'ant-design-vue';
|
||||
|
||||
import ApprovalProcess from '/@/views/workflow/task/components/ApprovalProcess.vue';
|
||||
import LookProcess from '/@/views/workflow/task/components/LookProcess.vue';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
const { t } = useI18n();
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Popover,
|
||||
Tabs,
|
||||
TabPane: Tabs.TabPane,
|
||||
Badge,
|
||||
Icon,
|
||||
ApprovalProcess,
|
||||
LookProcess,
|
||||
},
|
||||
setup() {
|
||||
const Approval = ref<{
|
||||
taskId?: string;
|
||||
processId?: string;
|
||||
schemaId?: string;
|
||||
visible: boolean;
|
||||
}>({
|
||||
visible: false,
|
||||
});
|
||||
const LookData = ref<{
|
||||
taskId?: string;
|
||||
processId?: string;
|
||||
visible: boolean;
|
||||
}>({
|
||||
visible: false,
|
||||
});
|
||||
const { prefixCls } = useDesign('header-notify');
|
||||
let times: any = ref();
|
||||
const listData = ref(tabListData);
|
||||
const simpleImage = ref(Empty.PRESENTED_IMAGE_SIMPLE);
|
||||
getDatas();
|
||||
times.value = setInterval(() => {
|
||||
getDatas();
|
||||
}, 10000);
|
||||
async function getDatas() {
|
||||
listData.value.forEach((o) => {
|
||||
o.list = [];
|
||||
o.unreadNum = 0;
|
||||
if (o.read) o.read = [];
|
||||
});
|
||||
try {
|
||||
let res = import.meta.env.VITE_GLOB_DISABLE_NEWS ? [] : await getOaNews(1);
|
||||
res.list.forEach((o) => {
|
||||
if (!o.readId) listData.value[0].unreadNum += 1;
|
||||
listData.value[0].list.push({
|
||||
id: o.id,
|
||||
avatar: '',
|
||||
title: o.briefHead,
|
||||
description: '',
|
||||
datetime: o.releaseTime,
|
||||
color: '',
|
||||
type: '3',
|
||||
read: o.isRead,
|
||||
});
|
||||
});
|
||||
let res1 = import.meta.env.VITE_GLOB_DISABLE_NEWS ? [] : await getOaNews(2);
|
||||
res1.list.forEach((o) => {
|
||||
if (!o.readId) listData.value[1].unreadNum += 1;
|
||||
listData.value[1].list.push({
|
||||
id: o.id,
|
||||
avatar: '',
|
||||
title: o.briefHead,
|
||||
description: '',
|
||||
datetime: o.releaseTime,
|
||||
color: '',
|
||||
type: '3',
|
||||
read: o.isRead,
|
||||
});
|
||||
});
|
||||
let res2 = import.meta.env.VITE_GLOB_DISABLE_NEWS ? [] : await getOaMessage();
|
||||
res2.forEach((o) => {
|
||||
if (o.messageType === 0) {
|
||||
if (!o.isRead) listData.value[2].unreadNum += 1;
|
||||
listData.value[2].list.push({
|
||||
id: o.id,
|
||||
avatar: '',
|
||||
title: o.messageContent,
|
||||
description: '',
|
||||
datetime: o.sendTime,
|
||||
timeFormat: o.timeFormat,
|
||||
color: '',
|
||||
type: '3',
|
||||
read: o.isRead,
|
||||
});
|
||||
} else if (o.messageType == 1) {
|
||||
if (!o.isRead) listData.value[3].unreadNum += 1;
|
||||
listData.value[3].list.push({
|
||||
id: o.id,
|
||||
avatar: '',
|
||||
title: o.messageContent,
|
||||
description: '',
|
||||
datetime: o.sendTime,
|
||||
timeFormat: o.timeFormat,
|
||||
color: '',
|
||||
type: '3',
|
||||
taskId: o.objectId,
|
||||
processId: o.processId,
|
||||
schemaId: o.schemaId,
|
||||
read: o.isRead,
|
||||
});
|
||||
} else if (o.messageType == 2) {
|
||||
if (!o.isRead) listData.value[3].unreadNum += 1;
|
||||
listData.value[3].read?.push({
|
||||
id: o.id,
|
||||
avatar: '',
|
||||
title: o.messageContent,
|
||||
description: '',
|
||||
datetime: o.sendTime,
|
||||
read: o.isRead,
|
||||
timeFormat: o.timeFormat,
|
||||
color: '',
|
||||
type: '3',
|
||||
taskId: o.objectId,
|
||||
processId: o.processId,
|
||||
});
|
||||
}
|
||||
});
|
||||
let res3 = await getScheduleMsg();
|
||||
res3.list.forEach((item) => (item.read = item.isRead));
|
||||
listData.value[2].unreadNum = res3.list.filter((x) => !x.isRead).length;
|
||||
listData.value[2].list.push(...res3.list);
|
||||
} catch (error) {
|
||||
clearInterval(times.value);
|
||||
}
|
||||
}
|
||||
|
||||
const count = computed(() => {
|
||||
let count = 0;
|
||||
for (let i = 0; i < listData.value.length; i++) {
|
||||
count += listData.value[i].unreadNum;
|
||||
}
|
||||
return count;
|
||||
});
|
||||
|
||||
async function setReadAll(type) {
|
||||
if (type == 1 || type == 2) {
|
||||
let ids: string[] = [];
|
||||
|
||||
listData.value[type - 1].list.forEach((o) => {
|
||||
o.read = 1;
|
||||
ids.push(o.id);
|
||||
});
|
||||
|
||||
await setOaRead(ids);
|
||||
} else if (type == 3) {
|
||||
await setScheduleReadAll();
|
||||
listData.value[type - 1].list.forEach((o) => {
|
||||
o.read = 1;
|
||||
});
|
||||
} else if (type == 4) {
|
||||
listData.value[type - 1].list.forEach((o) => {
|
||||
o.read = 1;
|
||||
});
|
||||
await setWorkReadAll();
|
||||
}
|
||||
listData.value[type - 1].unreadNum = 0;
|
||||
}
|
||||
async function setReadSingle(ids, num) {
|
||||
if (num == 3) {
|
||||
await setScheduleRead([ids]);
|
||||
} else if (num == 4) {
|
||||
await setSingleRead(ids);
|
||||
} else {
|
||||
await setOaRead([ids]);
|
||||
}
|
||||
if (listData.value[num - 1].unreadNum > 0) listData.value[num - 1].unreadNum -= 1;
|
||||
}
|
||||
onUnmounted(() => {
|
||||
clearInterval(times.value);
|
||||
});
|
||||
function ApprovalHandle(it, key, type) {
|
||||
if (type == 1) {
|
||||
Approval.value = {
|
||||
taskId: it.taskId,
|
||||
processId: it.processId,
|
||||
schemaId: it.schemaId,
|
||||
visible: true,
|
||||
};
|
||||
} else {
|
||||
LookData.value = {
|
||||
taskId: it.taskId,
|
||||
processId: it.processId,
|
||||
visible: true,
|
||||
};
|
||||
}
|
||||
it.read = 1;
|
||||
setReadSingle(it.id, key);
|
||||
}
|
||||
return {
|
||||
prefixCls,
|
||||
listData,
|
||||
count,
|
||||
setReadAll,
|
||||
simpleImage,
|
||||
numberStyle: {
|
||||
top: '18px',
|
||||
right: '8px',
|
||||
},
|
||||
setReadSingle,
|
||||
ApprovalHandle,
|
||||
Approval,
|
||||
LookData,
|
||||
t,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-header-notify';
|
||||
|
||||
.@{prefix-cls} {
|
||||
padding-top: 2px;
|
||||
|
||||
&__overlay {
|
||||
width: 380px;
|
||||
}
|
||||
|
||||
.ant-tabs-content {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.ant-badge {
|
||||
font-size: 18px;
|
||||
|
||||
.ant-badge-multiple-words {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 0.9em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="less">
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
|
||||
.header-title {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.opr {
|
||||
color: #02a7f0;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&.readed {
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
.list-data-item {
|
||||
display: flex;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&.readed {
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
.list-item-title {
|
||||
width: 280px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.list-item-time {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.readed-mark {
|
||||
color: #02a7f0;
|
||||
}
|
||||
|
||||
.notice-footer {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
font-size: 12px;
|
||||
color: #02a7f0;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
173
src/layouts/default/header/components/user-dropdown/DropDown.vue
Normal file
173
src/layouts/default/header/components/user-dropdown/DropDown.vue
Normal file
@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<Dropdown :overlayClassName="`${prefixCls}-dropdown-overlay`" placement="bottomRight">
|
||||
<span :class="[prefixCls, `${prefixCls}--${theme}`]" class="flex">
|
||||
<span style="border-left: 1px solid rgb(255 255 255 / 30%); height: 30px; padding-right: 15px"></span>
|
||||
<div style="margin-right: 12px; height: 30px; margin-top: -10px">
|
||||
<a-image :height="24" :src="getUserInfo.avatar" :width="24" :fallback="headerImg" />
|
||||
</div>
|
||||
<span :class="`${prefixCls}__info hidden md:block`">
|
||||
<span :class="`${prefixCls}__name `" class="truncate">
|
||||
{{ getUserInfo.name }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<template #overlay>
|
||||
<AMenu @click="handleMenuClick">
|
||||
<MenuItem key="usercenter" :text="t('用户中心')" icon="ant-design:user-switch-outlined" />
|
||||
<MenuItem v-if="getUseLockPage" key="lock" :text="t('锁定屏幕')" icon="ion:lock-closed-outline" />
|
||||
<MenuItem v-if="showSettings" key="sysSettings" icon="ion:color-filter-outline" text="主题设置" @click="showThemeSetting" />
|
||||
<MenuDivider />
|
||||
<MenuItem key="logout" :text="t('退出系统')" icon="ion:power-outline" />
|
||||
</AMenu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<LockAction @register="register" />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// components
|
||||
import { Dropdown, Menu } from 'ant-design-vue';
|
||||
|
||||
import { defineComponent, computed } from 'vue';
|
||||
|
||||
import { DOC_URL } from '/@/settings/siteSetting';
|
||||
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
|
||||
import headerImg from '/@/assets/images/header.jpg';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { openWindow } from '/@/utils';
|
||||
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import { PageEnum } from '/@/enums/pageEnum';
|
||||
import { useGo } from '/@/hooks/web/usePage';
|
||||
|
||||
// type MenuEvent = 'logout' | 'doc' | 'lock' | 'usercenter';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UserDropdown',
|
||||
components: {
|
||||
Dropdown,
|
||||
AMenu: Menu,
|
||||
MenuItem: createAsyncComponent(() => import('./DropMenuItem.vue')),
|
||||
MenuDivider: Menu.Divider,
|
||||
LockAction: createAsyncComponent(() => import('../lock/LockModal.vue'))
|
||||
},
|
||||
props: {
|
||||
theme: propTypes.oneOf(['dark', 'light']),
|
||||
showSettings: Boolean
|
||||
},
|
||||
setup() {
|
||||
const { prefixCls } = useDesign('header-user-dropdown');
|
||||
const { t } = useI18n();
|
||||
const { getUseLockPage } = useHeaderSetting();
|
||||
const userStore = useUserStore();
|
||||
const go = useGo();
|
||||
|
||||
const getUserInfo = computed(() => {
|
||||
const { name = '', avatar, desc } = userStore.getUserInfo || {};
|
||||
return { name, avatar: avatar || headerImg, desc };
|
||||
});
|
||||
|
||||
const [register, { openModal }] = useModal();
|
||||
|
||||
function handleLock() {
|
||||
openModal(true);
|
||||
}
|
||||
|
||||
// login out
|
||||
function handleLoginOut() {
|
||||
userStore.confirmLoginOut();
|
||||
}
|
||||
|
||||
// open doc
|
||||
function openDoc() {
|
||||
openWindow(DOC_URL);
|
||||
}
|
||||
|
||||
// updatePwd
|
||||
function openUserCenter() {
|
||||
go(PageEnum.USER_CENTER);
|
||||
}
|
||||
|
||||
function handleMenuClick(e) {
|
||||
switch (e.key) {
|
||||
case 'logout':
|
||||
handleLoginOut();
|
||||
break;
|
||||
case 'doc':
|
||||
openDoc();
|
||||
break;
|
||||
case 'lock':
|
||||
handleLock();
|
||||
break;
|
||||
case 'usercenter':
|
||||
openUserCenter();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
prefixCls,
|
||||
t,
|
||||
getUserInfo,
|
||||
handleMenuClick,
|
||||
register,
|
||||
getUseLockPage,
|
||||
headerImg
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
showThemeSetting() {
|
||||
this.$emit('menuClick', 'themeSetting');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-header-user-dropdown';
|
||||
|
||||
.@{prefix-cls} {
|
||||
height: @header-height;
|
||||
padding: 0 0 0 10px;
|
||||
padding-right: 10px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// &--dark {
|
||||
// &:hover {
|
||||
// background-color: @header-dark-bg-hover-color;
|
||||
// }
|
||||
// }
|
||||
|
||||
&--light {
|
||||
&:hover {
|
||||
background-color: @header-light-bg-hover-color;
|
||||
}
|
||||
|
||||
.@{prefix-cls}__name {
|
||||
color: @text-color-base;
|
||||
}
|
||||
|
||||
.@{prefix-cls}__desc {
|
||||
color: @header-light-desc-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-dropdown-overlay {
|
||||
.ant-dropdown-menu-item {
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<MenuItem :key="itemKey">
|
||||
<span class="flex items-center">
|
||||
<Icon :icon="icon" class="mr-1" />
|
||||
<span>{{ text }}</span>
|
||||
</span>
|
||||
</MenuItem>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Menu } from 'ant-design-vue';
|
||||
|
||||
import { computed, defineComponent, getCurrentInstance } from 'vue';
|
||||
|
||||
import Icon from '/@/components/Icon/index';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DropdownMenuItem',
|
||||
components: { MenuItem: Menu.Item, Icon },
|
||||
props: {
|
||||
// eslint-disable-next-line
|
||||
key: propTypes.string,
|
||||
text: propTypes.string,
|
||||
icon: propTypes.string,
|
||||
},
|
||||
setup(props) {
|
||||
const instance = getCurrentInstance();
|
||||
const itemKey = computed(() => props.key || instance?.vnode?.props?.key);
|
||||
return { itemKey };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
179
src/layouts/default/header/components/user-dropdown/index.vue
Normal file
179
src/layouts/default/header/components/user-dropdown/index.vue
Normal file
@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<Dropdown placement="bottomRight" :overlayClassName="`${prefixCls}-dropdown-overlay`">
|
||||
<span :class="[prefixCls, `${prefixCls}--${theme}`]" class="flex">
|
||||
<span
|
||||
style="border-left: 1px solid rgb(255 255 255 / 30%); height: 30px; padding-right: 15px"
|
||||
></span>
|
||||
<div style="margin-right: 12px; height: 30px; margin-top: -12px">
|
||||
<a-image
|
||||
:width="24"
|
||||
:height="24"
|
||||
:src="getUserInfo.avatar"
|
||||
fallback="src/assets/images/header.jpg"
|
||||
/>
|
||||
</div>
|
||||
<span :class="`${prefixCls}__info hidden md:block`">
|
||||
<span :class="`${prefixCls}__name `" class="truncate">
|
||||
{{ getUserInfo.name }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<template #overlay>
|
||||
<AMenu @click="handleMenuClick">
|
||||
<MenuItem key="usercenter" :text="t('用户中心')" icon="ant-design:user-switch-outlined" />
|
||||
<MenuItem key="doc" :text="t('文档')" icon="ion:document-text-outline" v-if="getShowDoc" />
|
||||
<MenuDivider v-if="getShowDoc" />
|
||||
<MenuItem
|
||||
v-if="getUseLockPage"
|
||||
key="lock"
|
||||
:text="t('锁定屏幕')"
|
||||
icon="ion:lock-closed-outline"
|
||||
/>
|
||||
<MenuItem key="logout" :text="t('退出系统')" icon="ion:power-outline" />
|
||||
</AMenu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<LockAction @register="register" />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// components
|
||||
import { Dropdown, Menu } from 'ant-design-vue';
|
||||
|
||||
import { defineComponent, computed } from 'vue';
|
||||
|
||||
import { DOC_URL } from '/@/settings/siteSetting';
|
||||
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
|
||||
import headerImg from '/@/assets/images/header.jpg';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { openWindow } from '/@/utils';
|
||||
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import { PageEnum } from '/@/enums/pageEnum';
|
||||
import { useGo } from '/@/hooks/web/usePage';
|
||||
|
||||
// type MenuEvent = 'logout' | 'doc' | 'lock' | 'usercenter';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UserDropdown',
|
||||
components: {
|
||||
Dropdown,
|
||||
AMenu: Menu,
|
||||
MenuItem: createAsyncComponent(() => import('./DropMenuItem.vue')),
|
||||
MenuDivider: Menu.Divider,
|
||||
LockAction: createAsyncComponent(() => import('../lock/LockModal.vue')),
|
||||
},
|
||||
props: {
|
||||
theme: propTypes.oneOf(['dark', 'light']),
|
||||
},
|
||||
setup() {
|
||||
const { prefixCls } = useDesign('header-user-dropdown');
|
||||
const { t } = useI18n();
|
||||
const { getShowDoc, getUseLockPage } = useHeaderSetting();
|
||||
const userStore = useUserStore();
|
||||
const go = useGo();
|
||||
|
||||
const getUserInfo = computed(() => {
|
||||
const { name = '', avatar, desc } = userStore.getUserInfo || {};
|
||||
return { name, avatar: avatar || headerImg, desc };
|
||||
});
|
||||
|
||||
const [register, { openModal }] = useModal();
|
||||
|
||||
function handleLock() {
|
||||
openModal(true);
|
||||
}
|
||||
|
||||
// login out
|
||||
function handleLoginOut() {
|
||||
userStore.confirmLoginOut();
|
||||
}
|
||||
|
||||
// open doc
|
||||
function openDoc() {
|
||||
openWindow(DOC_URL);
|
||||
}
|
||||
|
||||
// updatePwd
|
||||
function openUserCenter() {
|
||||
go(PageEnum.USER_CENTER);
|
||||
}
|
||||
|
||||
function handleMenuClick(e) {
|
||||
switch (e.key) {
|
||||
case 'logout':
|
||||
handleLoginOut();
|
||||
break;
|
||||
case 'doc':
|
||||
openDoc();
|
||||
break;
|
||||
case 'lock':
|
||||
handleLock();
|
||||
break;
|
||||
case 'usercenter':
|
||||
openUserCenter();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
prefixCls,
|
||||
t,
|
||||
getUserInfo,
|
||||
handleMenuClick,
|
||||
getShowDoc,
|
||||
register,
|
||||
getUseLockPage,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-header-user-dropdown';
|
||||
|
||||
.@{prefix-cls} {
|
||||
height: @header-height;
|
||||
padding: 0 0 0 10px;
|
||||
padding-right: 10px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// &--dark {
|
||||
// &:hover {
|
||||
// background-color: @header-dark-bg-hover-color;
|
||||
// }
|
||||
// }
|
||||
|
||||
&--light {
|
||||
&:hover {
|
||||
background-color: @header-light-bg-hover-color;
|
||||
}
|
||||
|
||||
.@{prefix-cls}__name {
|
||||
color: @text-color-base;
|
||||
}
|
||||
|
||||
.@{prefix-cls}__desc {
|
||||
color: @header-light-desc-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-dropdown-overlay {
|
||||
.ant-dropdown-menu-item {
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user