first commit

This commit is contained in:
ye1023
2026-04-09 15:09:34 +08:00
commit d8e3a63df1
2246 changed files with 352109 additions and 0 deletions
@@ -0,0 +1,162 @@
<template>
<div :style="getPlaceholderDomStyle" v-if="getIsShowPlaceholderDom"></div>
<div :style="getWrapStyle" :class="getClass">
<LayoutHeader v-if="getShowHeader" />
<MultipleTabs v-if="getShowTabs" />
</div>
</template>
<script lang="ts">
import { defineComponent, unref, computed, CSSProperties } from 'vue';
import LayoutHeader from './index.vue';
import MultipleTabs from '../tabs/index.vue';
import { useAppStore } from "@/store/modules/app";
import { useGlobSetting } from "/@/hooks/setting";
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useFullContent } from '/@/hooks/web/useFullContent';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
import { useAppInject } from '/@/hooks/web/useAppInject';
import { useDesign } from '/@/hooks/web/useDesign';
import { useLayoutHeight } from '../content/useContentViewHeight';
import { TabsThemeEnum } from '/@/enums/appEnum';
import { MenuTypeEnum } from '/@/enums/menuEnum';
// update-begin--author:liaozhiyang---date:20240407---for:【QQYUN-8774】网站header区域加高
const HEADER_HEIGHT = 60;
// update-begin--author:liaozhiyang---date:20240407---for:【【QQYUN-8774】网站header区域加高
// updateBy:sunjianlei---updateDate:2021-09-03---修改tab切换栏样式:更改高度
const TABS_HEIGHT = 32;
const TABS_HEIGHT_CARD = 50;
const TABS_HEIGHT_SMOOTH = 50;
export default defineComponent({
name: 'LayoutMultipleHeader',
components: { LayoutHeader, MultipleTabs },
setup() {
const { setHeaderHeight } = useLayoutHeight();
const { prefixCls } = useDesign('layout-multiple-header');
const appStore = useAppStore()
const glob = useGlobSetting()
const { getCalcContentWidth, getSplit, getMenuType } = useMenuSetting();
const { getIsMobile } = useAppInject();
const { getFixed, getShowInsetHeaderRef, getShowFullHeaderRef, getHeaderTheme } = useHeaderSetting();
const { getFullContent } = useFullContent();
const { getShowMultipleTab, getTabsTheme } = useMultipleTabSetting();
const getShowHeader = computed(() => {
// 控制是否显示顶部
if (appStore.getLayoutHideHeader) {
return false;
}
return unref(getShowInsetHeaderRef);
})
const getShowTabs = computed(() => {
// 控制是否显示多Tabs切换
if (appStore.getLayoutHideMultiTabs) {
return false;
}
return unref(getShowMultipleTab) && !unref(getFullContent);
});
const getIsShowPlaceholderDom = computed(() => {
return unref(getFixed) || unref(getShowFullHeaderRef);
});
const getWrapStyle = computed((): CSSProperties => {
const style: CSSProperties = {};
if (unref(getFixed) && !glob.isQiankunMicro) {
style.width = unref(getIsMobile) ? '100%' : unref(getCalcContentWidth);
}
if (unref(getShowFullHeaderRef)) {
style.top = `${HEADER_HEIGHT}px`;
}
return style;
});
const getIsFixed = computed(() => {
return unref(getFixed) || unref(getShowFullHeaderRef);
});
// updateBy:sunjianlei---updateDate:2021-09-08---根据主题的不同,动态计算tabs高度
const getTabsThemeHeight = computed(() => {
let tabsTheme = unref(getTabsTheme);
if (tabsTheme === TabsThemeEnum.CARD) {
return TABS_HEIGHT_CARD;
} else if (tabsTheme === TabsThemeEnum.SMOOTH) {
return TABS_HEIGHT_SMOOTH;
} else {
return TABS_HEIGHT;
}
});
const getPlaceholderDomStyle = computed((): CSSProperties => {
let height = 0;
// update-begin--author:liaozhiyang---date:20241216---for:【issues/7561】主题切换为顶部混合模式时,页面顶部内容显示不出来,被遮盖
if ((unref(getShowFullHeaderRef) || !unref(getSplit)) && unref(getShowHeader) && !unref(getFullContent) || unref(getMenuType) == MenuTypeEnum.MIX) {
height += HEADER_HEIGHT;
}
// update-end--author:liaozhiyang---date:20241216---for:【issues/7561】主题切换为顶部混合模式时,页面顶部内容显示不出来,被遮盖
if (unref(getShowTabs) && !unref(getFullContent)) {
height += unref(getTabsThemeHeight);
}
setHeaderHeight(height);
return {
height: `${height}px`,
};
});
const getClass = computed(() => {
return [prefixCls, `${prefixCls}--${unref(getHeaderTheme)}`, {
[`${prefixCls}--fixed`]: unref(getIsFixed),
// 【JEECG作为乾坤子应用】
[`${prefixCls}--qiankun-micro`]: glob.isQiankunMicro,
}];
});
return {
glob,
getClass,
prefixCls,
getPlaceholderDomStyle,
getIsFixed,
getWrapStyle,
getIsShowPlaceholderDom,
getShowTabs,
getShowHeader,
};
},
});
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-layout-multiple-header';
.@{prefix-cls} {
transition: width 0.2s;
flex: 0 0 auto;
&--dark {
margin-left: -1px;
}
&--fixed {
position: fixed;
top: 0;
z-index: @multiple-tab-fixed-z-index;
width: 100%;
}
// 【JEECG作为乾坤子应用】
&--qiankun-micro {
position: absolute;
}
}
</style>
@@ -0,0 +1,223 @@
<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)" style="color: inherit">
{{ 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 } = meta;
if (!title || hideBreadcrumb) {
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;
}
}
// update-begin--author:liaozhiyang---date:20240408---for:【QQYUN-8922】面包屑样式调整
&--light {
.ant-breadcrumb-link {
color: rgba(0, 0, 0, 0.85);
a {
color: rgba(0, 0, 0, 0.85);
&:hover {
color: @primary-color;
}
}
}
.ant-breadcrumb-separator,
.anticon {
color: rgba(0, 0, 0, 0.85);
}
}
}
html[data-theme='dark'] {
.@{prefix-cls} {
&--dark {
.ant-breadcrumb-link {
color: rgba(255, 255, 255, 0.85);
a {
color: rgba(255, 255, 255, 0.85);
&:hover {
color: @white;
}
}
}
.ant-breadcrumb-separator,
.anticon {
color: rgba(255, 255, 255, 0.85);
}
}
}
}
html[data-theme='light'] {
.@{prefix-cls} {
&--dark {
.ant-breadcrumb-link {
color: rgba(255, 255, 255, 1);
a {
color: rgba(255, 255, 255, 1);
&:hover {
color: @white;
}
}
}
.ant-breadcrumb-separator,
.anticon {
color: rgba(255, 255, 255, 1);
}
}
}
}
// update-end--author:liaozhiyang---date:20240408---for:【QQYUN-8922】面包屑样式调整
</style>
@@ -0,0 +1,43 @@
<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>
@@ -0,0 +1,35 @@
<template>
<Tooltip :title="getTitle" placement="bottom" :mouseEnterDelay="0.5">
<span @click="toggle">
<FullscreenOutlined v-if="!isFullscreen" />
<FullscreenExitOutlined 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 { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name: 'FullScreen',
components: { FullscreenExitOutlined, FullscreenOutlined, Tooltip },
setup() {
const { t } = useI18n();
const { toggle, isFullscreen } = useFullscreen();
const getTitle = computed(() => {
return unref(isFullscreen) ? t('layout.header.tooltipExitFull') : t('layout.header.tooltipEntryFull');
});
return {
getTitle,
isFullscreen,
toggle,
};
},
});
</script>
@@ -0,0 +1,47 @@
<template>
<Tooltip :title="t('layout.header.tooltipLock')" placement="bottom" :mouseEnterDelay="0.5" @click="handleLock">
<LockOutlined />
</Tooltip>
<LockModal ref="modalRef" v-if="lockModalVisible" @register="register" />
</template>
<script lang="ts">
import { defineComponent, computed, ref } from 'vue';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { Tooltip } from 'ant-design-vue';
import { LockOutlined } from '@ant-design/icons-vue';
import Icon from '/@/components/Icon';
import { useI18n } from '/@/hooks/web/useI18n';
import { useModal } from '/@/components/Modal';
import { getRefPromise } from '/@/utils/index';
export default defineComponent({
name: 'LockScreen',
inheritAttrs: false,
components: {
Icon,
Tooltip,
LockOutlined,
LockModal: createAsyncComponent(() => import('./lock/LockModal.vue')),
},
setup() {
const { t } = useI18n();
const [register, { openModal }] = useModal();
// update-begin--author:liaozhiyang---date:20230901---for:【QQYUN-6333】空路由问题—首次访问资源太大
const lockModalVisible = ref(false);
const modalRef = ref(null);
async function handleLock() {
lockModalVisible.value = true;
await getRefPromise(modalRef);
openModal(true);
}
// update-end--author:liaozhiyang---date:20230901---for:【QQYUN-6333】空路由问题—首次访问资源太大
return {
t,
register,
handleLock,
lockModalVisible,
modalRef,
};
},
});
</script>
@@ -0,0 +1,16 @@
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('./Breadcrumb.vue'));
export const Notify = createAsyncComponent(() => import('./notify/index.vue'));
export const ErrorAction = createAsyncComponent(() => import('./ErrorAction.vue'));
export const LockScreen = createAsyncComponent(() => import('./LockScreen.vue'));
export { FullScreen };
@@ -0,0 +1,125 @@
<template>
<BasicModal :footer="null" :title="t('layout.header.lockScreen')" v-bind="$attrs" :class="prefixCls" @register="register" :canFullscreen="false">
<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('layout.header.lockScreenBtn') }}
</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 from '/@/components/Form/src/BasicForm.vue';
import { useForm } from '/@/components/Form/src/hooks/useForm';
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({
//update-begin---author:wangshuai---date:2024-04-08---for:【QQYUN-8895】锁屏样式修改---
labelWidth: 74,
labelAlign:'left',
wrapperCol:{},
//update-end---author:wangshuai---date:2024-04-08---for:【QQYUN-8895】锁屏样式修改---
showActionButtonGroup: false,
schemas: [
{
field: 'password',
label: t('layout.header.lockScreenPassword'),
component: 'InputPassword',
componentProps: {
autocomplete: "new-password",
},
},
],
});
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 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>
@@ -0,0 +1,232 @@
<template>
<a-list :class="prefixCls" :pagination="getPagination">
<template v-for="item in getData" :key="item.id">
<a-list-item class="list-item" @click="handleTitleClick(item)" :style="{ cursor: isTitleClickable ? 'pointer' : '' }">
<a-list-item-meta>
<template #title>
<div class="title">
<a-typography-paragraph
style="width: 100%; margin-bottom: 0 !important"
: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" />
<template v-else-if="item.priority">
<a-avatar v-if="item.priority === PriorityTypes.L" class="avatar priority-L" title="一般消息">
<template #icon>
<Icon icon="entypo:info" />
</template>
</a-avatar>
<a-avatar v-if="item.priority === PriorityTypes.M" class="avatar priority-M" title="重要消息">
<template #icon>
<Icon icon="bi:exclamation-lg" />
</template>
</a-avatar>
<a-avatar v-if="item.priority === PriorityTypes.H" class="avatar priority-H" title="紧急消息">
<template #icon>
<Icon icon="ant-design:warning-filled" />
</template>
</a-avatar>
</template>
<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">
<Time :value="item.datetime" :title="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 { PriorityTypes, ListItem } from './data';
import { useDesign } from '/@/hooks/web/useDesign';
import { List, Avatar, Tag, Typography } from 'ant-design-vue';
import { Time } from '/@/components/Time';
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,
Time,
},
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: 1,
},
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,
PriorityTypes,
};
},
});
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-header-notify-list';
.@{prefix-cls} {
width: 340px;
&::-webkit-scrollbar {
display: none;
}
: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;
}
}
}
.list-item {
.priority-L,
.priority-M,
.priority-H {
font-size: 12px;
}
.priority-L {
background-color: #7cd1ff;
}
.priority-M {
background-color: #ffa743;
}
.priority-H {
background-color: #f8766c;
}
.description {
font-size: 12px;
line-height: 18px;
}
}
}
</style>
@@ -0,0 +1,206 @@
export interface ListItem {
id: string;
avatar: string;
// 通知的标题内容
title: string;
// 是否在标题上显示删除线
titleDelete?: boolean;
datetime: string;
type: string;
read?: boolean;
description: string;
clickClose?: boolean;
extra?: string;
color?: string;
// 优先级
priority?: string;
}
export enum PriorityTypes {
// 低优先级,一般消息
L = 'L',
// 中优先级,重要消息
M = 'M',
// 高优先级,紧急消息
H = 'H',
}
export interface TabItem {
key: string;
name: string;
list: ListItem[];
unreadlist?: ListItem[];
count: number;
}
export const tabListData: TabItem[] = [
{
key: '1',
name: '通知',
list: [
{
id: '000000001',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '你收到了 14 份新周报',
description: '',
datetime: '2017-08-09',
type: '1',
},
{
id: '000000002',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
title: '你推荐的 曲妮妮 已通过第三轮面试',
description: '',
datetime: '2017-08-08',
type: '1',
},
{
id: '000000003',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
title: '这种模板可以区分多种通知类型',
description: '',
datetime: '2017-08-07',
// read: true,
type: '1',
},
{
id: '000000004',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2017-08-07',
type: '1',
},
{
id: '000000005',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '标题可以设置自动显示省略号,本例中标题行数已设为1行,如果内容超过1行将自动截断并支持tooltip显示完整标题。',
description: '',
datetime: '2017-08-07',
type: '1',
},
{
id: '000000006',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2017-08-07',
type: '1',
},
{
id: '000000007',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2017-08-07',
type: '1',
},
{
id: '000000008',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2017-08-07',
type: '1',
},
{
id: '000000009',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2017-08-07',
type: '1',
},
{
id: '000000010',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2017-08-07',
type: '1',
},
],
count: 0,
},
{
key: '2',
name: '系统消息',
list: [
{
id: '000000006',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '曲丽丽 评论了你',
description: '描述信息描述信息描述信息',
datetime: '2017-08-07',
type: '2',
clickClose: true,
},
{
id: '000000007',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '朱偏右 回复了你',
description: '这种模板用于提醒谁与你发生了互动',
datetime: '2017-08-07',
type: '2',
clickClose: true,
},
{
id: '000000008',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '标题',
description:
'请将鼠标移动到此处,以便测试超长的消息在此处将如何处理。本例中设置的描述最大行数为2,超过2行的描述内容将被省略并且可以通过tooltip查看完整内容',
datetime: '2017-08-07',
type: '2',
clickClose: true,
},
],
count: 0,
},
// {
// key: '3',
// name: '待办',
// list: [
// {
// id: '000000009',
// avatar: '',
// title: '任务名称',
// description: '任务需要在 2017-01-12 20:00 前启动',
// datetime: '',
// extra: '未开始',
// color: '',
// type: '3',
// },
// {
// id: '000000010',
// avatar: '',
// title: '第三方紧急代码变更',
// description: '冠霖 需在 2017-01-07 前完成代码变更任务',
// datetime: '',
// extra: '马上到期',
// color: 'red',
// type: '3',
// },
// {
// id: '000000011',
// avatar: '',
// title: '信息安全考试',
// description: '指派竹尔于 2017-01-09 前完成更新并发布',
// datetime: '',
// extra: '已耗时 8 天',
// color: 'gold',
// type: '3',
// },
// {
// id: '000000012',
// avatar: '',
// title: 'ABCD 版本发布',
// description: '指派竹尔于 2017-01-09 前完成更新并发布',
// datetime: '',
// extra: '进行中',
// color: 'blue',
// type: '3',
// },
// ],
// },
];
@@ -0,0 +1,308 @@
<template>
<div :class="prefixCls">
<Badge :count="messageCount" :overflowCount="9" :offset="[-4, 18]" :numberStyle="numberStyle" @click="clickBadge">
<BellOutlined />
</Badge>
<DynamicNotice ref="dynamicNoticeRef" v-bind="dynamicNoticeProps" />
<DetailModal @register="registerDetail" />
<sys-message-modal @register="registerMessageModal" @refresh="reloadCount" :messageCount="messageCount"></sys-message-modal>
<address-book @register="registerBookModal"></address-book>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, unref, reactive, onMounted, getCurrentInstance } from 'vue';
import { Popover, Tabs, Badge } from 'ant-design-vue';
import { BellOutlined } from '@ant-design/icons-vue';
// import { tabListData } from './data';
import { getUnreadMessageCount, editCementSend, clearAllUnReadMessage } from './notify.api';
import NoticeList from './NoticeList.vue';
import DetailModal from '/@/views/monitor/mynews/DetailModal.vue';
import DynamicNotice from '/@/views/monitor/mynews/DynamicNotice.vue';
import { useModal } from '/@/components/Modal';
import { useDesign } from '/@/hooks/web/useDesign';
import { useGlobSetting } from '/@/hooks/setting';
import { useUserStore } from '/@/store/modules/user';
import { connectWebSocket, onWebSocket } from '/@/hooks/web/useWebSocket';
import { readAllMsg } from '/@/views/monitor/mynews/mynews.api';
import { getToken } from '/@/utils/auth';
import md5 from 'crypto-js/md5';
import SysMessageModal from '/@/views/system/message/components/SysMessageModal.vue'
export default defineComponent({
components: {
Popover,
BellOutlined,
Tabs,
TabPane: Tabs.TabPane,
Badge,
NoticeList,
DetailModal,
DynamicNotice,
SysMessageModal,
},
setup() {
const { prefixCls } = useDesign('header-notify');
const instance: any = getCurrentInstance();
const userStore = useUserStore();
const glob = useGlobSetting();
const dynamicNoticeProps = reactive({ path: '', formData: {} });
const [registerDetail, detailModal] = useModal();
// const listData = ref(tabListData);
// const count = computed(() => {
// let count = 0;
// for (let i = 0; i < listData.value.length; i++) {
// count += listData.value[i].count;
// }
// return count;
// });
const chatRef = ref();
const [registerMessageModal, { openModal: openMessageModal }] = useModal();
const [registerBookModal, { openModal: openBookModal }] = useModal();
function clickBadge(){
// //消息列表弹窗前去除角标
// for (let i = 0; i < listData.value.length; i++) {
// listData.value[i].count = 0;
// }
openMessageModal(true, {})
}
const popoverVisible = ref<boolean>(false);
onMounted(() => {
initWebSocket();
});
const messageCount = ref<number>(0)
function mapAnnouncement(item) {
return {
...item,
title: item.titile,
description: item.msgAbstract,
datetime: item.sendTime,
};
}
// 获取系统消息
async function loadData() {
try {
// let { anntMsgList, sysMsgList, anntMsgTotal, sysMsgTotal } = await listCementByUser({
// pageSize: 5,
// });
// listData.value[0].list = anntMsgList.map(mapAnnouncement);
// listData.value[1].list = sysMsgList.map(mapAnnouncement);
// listData.value[0].count = anntMsgTotal;
//update-begin-author:taoyan date:2022-8-30 for: 消息数量改变触发chat组件事件
// listData.value[1].count = sysMsgTotal;
//let msgCount = anntMsgTotal+sysMsgTotal;
let msgCount = await getUnreadMessageCount();
//update-begin-author:wangshuai date:2022-09-02 for: 消息未读数为0也需要传递,因为聊天需要计算总数
chatRef.value.updateMessageCount(msgCount);
messageCount.value = msgCount
//update-end-author:wangshuai date:2022-09-02 for: 消息未读数为0也需要传递,因为聊天需要计算总数
//update-end-author:taoyan date:2022-8-30 for: 消息数量改变触发chat组件事件
} catch (e) {
console.warn('系统消息通知异常:', e);
}
}
loadData();
function onNoticeClick(record) {
try {
editCementSend(record.id);
loadData();
} catch (e) {
console.error(e);
}
if (record.openType === 'component') {
dynamicNoticeProps.path = record.openPage;
dynamicNoticeProps.formData = { id: record.busId };
instance.refs.dynamicNoticeRef?.detail(record.openPage);
} else {
detailModal.openModal(true, {
record,
isUpdate: true,
});
}
popoverVisible.value = false;
}
// 初始化 WebSocket
function initWebSocket() {
let token = getToken();
//将登录token生成一个短的标识
let wsClientId = md5(token);
let userId = unref(userStore.getUserInfo).id + "_" + wsClientId;
// WebSocket与普通的请求所用协议有所不同,ws等同于http,wss等同于https
let url = glob.domainUrl?.replace('https://', 'wss://').replace('http://', 'ws://') + '/websocket/' + userId;
connectWebSocket(url);
onWebSocket(onWebSocketMessage);
}
function onWebSocketMessage(data) {
if (data.cmd === 'topic' || data.cmd === 'user') {
//update-begin-author:taoyan date:2022-7-13 for: VUEN-1674【严重bug】系统通知,为什么必须刷新右上角才提示
//后台保存数据太慢 前端延迟刷新消息
setTimeout(()=>{
loadData();
}, 1000)
//update-end-author:taoyan date:2022-7-13 for: VUEN-1674【严重bug】系统通知,为什么必须刷新右上角才提示
}
}
// 清空消息
function onEmptyNotify() {
popoverVisible.value = false;
readAllMsg({}, loadData);
}
async function reloadCount(id){
try {
await editCementSend(id);
await loadData();
} catch (e) {
console.error(e);
}
}
/**
* 获取消息未读数
*/
function getSystemUnreadNum() {
chatRef.value.updateMessageCount(messageCount.value);
}
function clickAddressBook() {
openBookModal(true,{})
}
/**
* 清除全部未读消息
*/
function clearAllUnMessage() {
clearAllUnReadMessage().then((res) =>{
if(res.success){
loadData();
}
})
}
return {
prefixCls,
// listData,
// count,
clickBadge,
registerMessageModal,
reloadCount,
onNoticeClick,
onEmptyNotify,
numberStyle: {},
popoverVisible,
registerDetail,
dynamicNoticeProps,
chatRef,
getSystemUnreadNum,
clickAddressBook,
registerBookModal,
messageCount,
clearAllUnMessage,
};
},
});
</script>
<style lang="less">
//noinspection LessUnresolvedVariable
@prefix-cls: ~'@{namespace}-header-notify';
.@{prefix-cls} {
/* padding-top: 2px;*/
&__overlay {
max-width: 340px;
.ant-popover-inner-content {
padding: 0;
}
.ant-tabs-nav {
margin-bottom: 12px;
}
.ant-list-item {
padding: 12px 24px;
transition: background-color 300ms;
}
.bottom-buttons {
text-align: center;
border-top: 1px solid #f0f0f0;
height: 42px;
.ant-btn {
border: 0;
height: 100%;
&:first-child {
border-right: 1px solid #f0f0f0;
}
}
}
}
.ant-tabs-content {
width: 300px;
}
.ant-badge {
font-size: 18px;
.ant-badge-count {
@badget-size: 16px;
width: @badget-size;
height: @badget-size;
min-width: @badget-size;
line-height: @badget-size;
padding: 0;
.ant-scroll-number-only > p.ant-scroll-number-only-unit {
font-size: 14px;
height: @badget-size;
}
}
.ant-badge-multiple-words {
padding: 0 0 0 2px;
font-size: 12px;
}
svg {
width: 0.9em;
}
}
}
// 兼容黑暗模式
[data-theme='dark'] .@{prefix-cls} {
&__overlay {
.ant-list-item {
&:hover {
background-color: #111b26;
}
}
.bottom-buttons {
border-top: 1px solid #303030;
.ant-btn {
&:first-child {
border-right: 1px solid #303030;
}
}
}
}
}
</style>
@@ -0,0 +1,272 @@
<template>
<div :class="prefixCls">
<Popover v-model:open="popoverVisible" title="" trigger="click" :overlayClassName="`${prefixCls}__overlay`">
<Badge :count="count" :overflowCount="9" :offset="[-4, 10]" :numberStyle="numberStyle">
<BellOutlined />
</Badge>
<template #content>
<Tabs>
<template v-for="item in listData" :key="item.key">
<TabPane>
<template #tab>
{{ item.name }}
<span v-if="item.list.length !== 0">({{ item.count }})</span>
</template>
<!-- 绑定title-click事件的通知列表中标题是可点击-->
<NoticeList :list="item.list" @title-click="onNoticeClick" />
</TabPane>
</template>
</Tabs>
<a-row class="bottom-buttons">
<a-col :span="count === 0 ? 0 : 12">
<a-button @click="onEmptyNotify" type="dashed" block>清空消息</a-button>
</a-col>
<a-col :span="count === 0 ? 24 : 12">
<a-button @click="popoverVisible = false" type="dashed" block>
<router-link to="/monitor/mynews">查看更多</router-link>
</a-button>
</a-col>
</a-row>
</template>
</Popover>
<DynamicNotice ref="dynamicNoticeRef" v-bind="dynamicNoticeProps" />
<DetailModal @register="registerDetail" />
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, unref, reactive, onMounted, getCurrentInstance, onUnmounted } from 'vue';
import { Popover, Tabs, Badge } from 'ant-design-vue';
import { BellOutlined } from '@ant-design/icons-vue';
import { tabListData } from './data';
import { listCementByUser, editCementSend } from './notify.api';
import NoticeList from './NoticeList.vue';
import DetailModal from '/@/views/monitor/mynews/DetailModal.vue';
import DynamicNotice from '/@/views/monitor/mynews/DynamicNotice.vue';
import { useModal } from '/@/components/Modal';
import { useDesign } from '/@/hooks/web/useDesign';
import { useGlobSetting } from '/@/hooks/setting';
import { useUserStore } from '/@/store/modules/user';
import { connectWebSocket, onWebSocket, result } from '/@/hooks/web/useWebSocket';
import { readAllMsg } from '/@/views/monitor/mynews/mynews.api';
import { getToken } from '/@/utils/auth';
export default defineComponent({
components: {
Popover,
BellOutlined,
Tabs,
TabPane: Tabs.TabPane,
Badge,
NoticeList,
DetailModal,
DynamicNotice,
},
setup() {
const { prefixCls } = useDesign('header-notify');
const instance: any = getCurrentInstance();
const userStore = useUserStore();
const glob = useGlobSetting();
const dynamicNoticeProps = reactive({ path: '', formData: {} });
const [registerDetail, detailModal] = useModal();
const popoverVisible = ref<boolean>(false);
const listData = ref(tabListData);
listData.value[0].list = [];
listData.value[1].list = [];
listData.value[0].count = 0;
listData.value[1].count = 0;
onMounted(() => {
initWebSocket();
});
const count = computed(() => {
let count = 0;
for (let i = 0; i < listData.value.length; i++) {
count += listData.value[i].count;
}
return count;
});
function mapAnnouncement(item) {
return {
...item,
title: item.titile,
description: item.msgAbstract,
datetime: item.sendTime,
};
}
// 获取系统消息
async function loadData() {
try {
let { anntMsgList, sysMsgList, anntMsgTotal, sysMsgTotal } = await listCementByUser({
pageSize: 5,
});
listData.value[0].list = anntMsgList.map(mapAnnouncement);
listData.value[1].list = sysMsgList.map(mapAnnouncement);
listData.value[0].count = anntMsgTotal;
listData.value[1].count = sysMsgTotal;
} catch (e) {
console.warn('系统消息通知异常:', e);
}
}
loadData();
function onNoticeClick(record) {
try {
editCementSend(record.id);
loadData();
} catch (e) {
console.error(e);
}
if (record.openType === 'component') {
dynamicNoticeProps.path = record.openPage;
dynamicNoticeProps.formData = { id: record.busId };
instance.refs.dynamicNoticeRef?.detail(record.openPage);
} else {
detailModal.openModal(true, {
record,
isUpdate: true,
});
}
popoverVisible.value = false;
}
// 初始化 WebSocket
function initWebSocket() {
let userId = unref(userStore.getUserInfo).id;
let token = getToken();
// WebSocket与普通的请求所用协议有所不同,ws等同于http,wss等同于https
let url = glob.domainUrl?.replace('https://', 'wss://').replace('http://', 'ws://') + '/websocket/' + userId;
connectWebSocket(url);
onWebSocket(onWebSocketMessage);
}
function onWebSocketMessage(data) {
console.log('---onWebSocketMessage---', data)
if (data.cmd === 'topic' || data.cmd === 'user') {
//update-begin-author:taoyan date:2022-7-13 for: VUEN-1674【严重bug】系统通知,为什么必须刷新右上角才提示
//后台保存数据太慢 前端延迟刷新消息
setTimeout(()=>{
loadData();
}, 1000)
//update-end-author:taoyan date:2022-7-13 for: VUEN-1674【严重bug】系统通知,为什么必须刷新右上角才提示
}
}
// 清空消息
function onEmptyNotify() {
popoverVisible.value = false;
readAllMsg({}, loadData);
}
return {
prefixCls,
listData,
count,
onNoticeClick,
onEmptyNotify,
numberStyle: {},
popoverVisible,
registerDetail,
dynamicNoticeProps,
};
},
});
</script>
<style lang="less" scoped>
//noinspection LessUnresolvedVariable
@prefix-cls: ~'@{namespace}-header-notify';
.@{prefix-cls} {
padding-top: 2px;
&__overlay {
max-width: 340px;
.ant-popover-inner-content {
padding: 0;
}
.ant-tabs-nav {
margin-bottom: 12px;
}
.ant-list-item {
padding: 12px 24px;
transition: background-color 300ms;
&:hover {
background-color: #e6f7ff;
}
}
.bottom-buttons {
text-align: center;
border-top: 1px solid #f0f0f0;
height: 42px;
.ant-btn {
border: 0;
height: 100%;
&:first-child {
border-right: 1px solid #f0f0f0;
}
}
}
}
.ant-tabs-content {
width: 300px;
}
.ant-badge {
font-size: 18px;
.ant-badge-count {
@badget-size: 16px;
width: @badget-size;
height: @badget-size;
min-width: @badget-size;
line-height: @badget-size;
padding: 0;
.ant-scroll-number-only > p.ant-scroll-number-only-unit {
font-size: 14px;
height: @badget-size;
}
}
.ant-badge-multiple-words {
padding: 0 0 0 2px;
font-size: 12px;
}
svg {
width: 0.9em;
}
}
}
// 兼容黑暗模式
[data-theme='dark'] .@{prefix-cls} {
&__overlay {
.ant-list-item {
&:hover {
background-color: #111b26;
}
}
.bottom-buttons {
border-top: 1px solid #303030;
.ant-btn {
&:first-child {
border-right: 1px solid #303030;
}
}
}
}
}
</style>
@@ -0,0 +1,27 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
listCementByUser = '/sys/annountCement/listByUser',
getUnreadMessageCount = '/sys/annountCement/getUnreadMessageCount',
editCementSend = '/sys/sysAnnouncementSend/editByAnntIdAndUserId',
clearAllUnReadMessage = '/sys/annountCement/clearAllUnReadMessage',
}
/**
* 获取系统通知消息列表
* @param params
*/
export const listCementByUser = (params?) => defHttp.get({ url: Api.listCementByUser, params });
/**
* 获取用户近两个月未读消息数量
* @param params
*/
export const getUnreadMessageCount = (params?) => defHttp.get({ url: Api.getUnreadMessageCount, params });
export const editCementSend = (anntId, params?) => defHttp.put({ url: Api.editCementSend, params: { anntId, ...params } });
/**
* 清空全部未读消息
*/
export const clearAllUnReadMessage = () => defHttp.post({ url: Api.clearAllUnReadMessage },{ isTransformResponse: false });
@@ -0,0 +1,271 @@
<template>
<BasicModal v-bind="config" :maxHeight="500" :title="currTitle" v-model:visible="visible" wrapClassName="loginSelectModal">
<a-form ref="formRef" v-bind="layout" :colon="false" class="loginSelectForm">
<a-form-item v-if="isMultiTenant" :validate-status="validate_status">
<!--label内容-->
<template #label>
<a-tooltip placement="topLeft">
<template #title>
<span>您隶属于多租户请选择当前所属租户</span>
</template>
<a-avatar style="background-color: #87d068" :size="30"> 租户 </a-avatar>
</a-tooltip>
</template>
<!--部门下拉内容-->
<a-select v-model:value="tenantSelected" placeholder="请选择登录部门" :class="{ 'valid-error': validate_status == 'error' }">
<template #suffixIcon>
<Icon icon="ant-design:gold-outline" />
</template>
<template v-for="tenant in tenantList" :key="tenant.id">
<a-select-option :value="tenant.id">{{ tenant.name }}</a-select-option>
</template>
</a-select>
</a-form-item>
<a-form-item v-if="isMultiDepart" :validate-status="validate_status1">
<!--label内容-->
<template #label>
<a-tooltip placement="topLeft">
<template #title>
<span>您隶属于多部门请选择当前所在部门</span>
</template>
<a-avatar style="background-color: rgb(104, 208, 203)" :size="30"> 部门 </a-avatar>
</a-tooltip>
</template>
<!--部门下拉内容-->
<a-select v-model:value="departSelected" placeholder="请选择登录部门" :class="{ 'valid-error': validate_status1 == 'error' }">
<template #suffixIcon>
<Icon icon="ant-design:gold-outline" />
</template>
<template v-for="depart in departList" :key="depart.orgCode">
<a-select-option :value="depart.orgCode">{{ depart.departName }}</a-select-option>
</template>
</a-select>
</a-form-item>
</a-form>
<template #footer>
<a-button @click="close">关闭</a-button>
<a-button @click="handleSubmit" type="primary">确认</a-button>
</template>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, watch, unref } from 'vue';
import { Avatar } from 'ant-design-vue';
import { BasicModal } from '/@/components/Modal';
import { getUserDeparts, selectDepart } from '/@/views/system/depart/depart.api';
import { getUserTenants } from '/@/views/system/tenant/tenant.api';
import { useMessage } from '/@/hooks/web/useMessage';
import { useUserStore } from '/@/store/modules/user';
const userStore = useUserStore();
const { createMessage, notification } = useMessage();
const props = defineProps({
title: { type: String, default: '部门选择' },
closable: { type: Boolean, default: false },
username: { type: String, default: '' },
});
const layout = {
labelCol: { span: 4 },
wrapperCol: { span: 18 },
};
const config = {
maskClosable: false,
closable: false,
canFullscreen: false,
width: '500px',
minHeight: 20,
maxHeight: 20,
};
const currTitle = ref('');
const isMultiTenant = ref(false);
const currentTenantName = ref('');
const tenantSelected = ref();
const tenantList = ref([]);
const validate_status = ref('');
const isMultiDepart = ref(false);
const currentDepartName = ref('');
const departSelected = ref('');
const departList = ref([]);
const validate_status1 = ref('');
//弹窗显隐
const visible = ref(false);
/**
* 弹窗打开前处理
*/
async function show() {
//加载部门
await loadDepartList();
//加载租户
await loadTenantList();
//标题配置
if (unref(isMultiTenant) && unref(isMultiDepart)) {
currTitle.value = '切换租户和部门';
} else if (unref(isMultiTenant)) {
currTitle.value =
unref(currentTenantName) && unref(currentTenantName).length > 0 ? `租户切换(当前租户 :${unref(currentTenantName)}` : props.title;
} else if (unref(isMultiDepart)) {
currTitle.value =
unref(currentDepartName) && unref(currentDepartName).length > 0 ? `部门切换(当前部门 :${unref(currentDepartName)}` : props.title;
}
//model显隐
if (unref(isMultiTenant) || unref(isMultiDepart)) {
visible.value = true;
}
}
/**
*加载部门信息
*/
async function loadDepartList() {
const result = await getUserDeparts();
if (!result.list || result.list.length == 0) {
return;
}
let currentDepart = result.list.filter((item) => item.orgCode == result.orgCode);
departList.value = result.list;
departSelected.value = currentDepart && currentDepart.length > 0 ? result.orgCode : '';
currentDepartName.value = currentDepart && currentDepart.length > 0 ? currentDepart[0].departName : '';
isMultiDepart.value = true;
}
/**
*加载租户信息
*/
async function loadTenantList() {
const result = await getUserTenants();
if (!result.list || result.list.length == 0) {
return;
}
let tenantId = userStore.getTenant;
let currentTenant = result.list.filter((item) => item.id == tenantId);
currentTenantName.value = currentTenant && currentTenant.length > 0 ? currentTenant[0].name : '';
tenantList.value = result.list;
tenantSelected.value = tenantId;
isMultiTenant.value = true;
}
/**
* 提交数据
*/
async function handleSubmit() {
if (unref(isMultiTenant) && unref(tenantSelected)==null) {
validate_status.value = 'error';
return false;
}
if (unref(isMultiDepart) && !unref(departSelected)) {
validate_status1.value = 'error';
return false;
}
departResolve()
.then(() => {
if (unref(isMultiTenant)) {
userStore.setTenant(unref(tenantSelected));
}
createMessage.success('切换成功');
//切换租户后要刷新首页
window.location.reload();
})
.catch((e) => {
console.log('登录选择出现问题', e);
})
.finally(() => {
if (unref(isMultiTenant)) {
userStore.setTenant(unref(tenantSelected));
}
close();
});
}
/**
*切换选择部门
*/
function departResolve() {
return new Promise(async (resolve, reject) => {
if (!unref(isMultiDepart)) {
resolve();
} else {
const result = await selectDepart({
username: userStore.getUserInfo.username,
orgCode: unref(departSelected),
loginTenantId: unref(tenantSelected),
});
if (result.userInfo) {
const userInfo = result.userInfo;
userStore.setUserInfo(userInfo);
resolve();
} else {
requestFailed(result);
userStore.logout();
reject();
}
}
});
}
/**
* 请求失败处理
*/
function requestFailed(err) {
notification.error({
message: '登录失败',
description: ((err.response || {}).data || {}).message || err.message || '请求出现错误,请稍后再试',
duration: 4,
});
}
/**
* 关闭model
*/
function close() {
departClear();
}
/**
*初始化数据
*/
function departClear() {
currTitle.value = '';
isMultiTenant.value = false;
currentTenantName.value = '';
tenantSelected.value = '';
tenantList.value = [];
validate_status.value = '';
isMultiDepart.value = false;
currentDepartName.value = '';
departSelected.value = '';
departList.value = [];
validate_status1.value = '';
visible.value = false;
}
/**
* 监听username
*/
watch(
() => props.username,
(value) => {
value && loadDepartList();
}
);
defineExpose({
show,
});
</script>
<style lang="less" scoped>
.loginSelectForm {
margin-bottom: -20px;
}
.loginSelectModal {
top: 20px;
}
.valid-error .ant-select-selection__placeholder {
color: #f5222d;
}
</style>
@@ -0,0 +1,34 @@
<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: {
// 【issues/6855】
itemKey: propTypes.string,
text: propTypes.string,
icon: propTypes.string,
},
setup(props) {
const instance = getCurrentInstance();
// update-begin--author:liaozhiyang---date:20240717---for:【issues/6855】组件使用key作props报警告,改为itemKey
const itemKey = computed(() => props.itemKey || instance?.vnode?.props?.itemKey);
// update-end--author:liaozhiyang---date:20240717---for:【issues/6855】组件使用key作props报警告,改为itemKey
return { itemKey };
},
});
</script>
@@ -0,0 +1,100 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" @ok="handleSubmit" width="600px">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, unref } from 'vue';
import { rules } from '/@/utils/helper/validator';
import { defHttp } from '/@/utils/http/axios';
import { BasicModal, useModalInner } from '/@/components/Modal';
import BasicForm from '/@/components/Form/src/BasicForm.vue';
import { useForm } from '/@/components/Form/src/hooks/useForm';
import { useMessage } from '/@/hooks/web/useMessage';
import { useLocaleStore } from '/@/store/modules/locale';
import { useI18n } from '/@/hooks/web/useI18n';
const localeStore = useLocaleStore();
const { t } = useI18n();
// 声明Emits
const emit = defineEmits(['register']);
const $message = useMessage();
const formRef = ref();
const username = ref('');
// update-begin--author:liaozhiyang---date:20240124---for:【QQYUN-7970】国际化
const title = ref(t('layout.changePassword.changePassword'));
//表单配置
const [registerForm, { resetFields, validate, clearValidate }] = useForm({
schemas: [
{
label: t('layout.changePassword.oldPassword'),
field: 'oldpassword',
component: 'InputPassword',
required: true,
},
{
label: t('layout.changePassword.newPassword'),
field: 'password',
component: 'StrengthMeter',
componentProps: {
placeholder: t('layout.changePassword.pleaseEnterNewPassword'),
},
rules: [
{
required: true,
message: t('layout.changePassword.pleaseEnterNewPassword'),
},
],
},
{
label: t('layout.changePassword.confirmNewPassword'),
field: 'confirmpassword',
component: 'InputPassword',
dynamicRules: ({ values }) => rules.confirmPassword(values, true),
},
],
showActionButtonGroup: false,
wrapperCol: null,
labelWidth: localeStore.getLocale == 'zh_CN' ? 100 : 160,
});
// update-end--author:liaozhiyang---date:20240124---for:【QQYUN-7970】国际化
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner();
//表单提交事件
async function handleSubmit() {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
let params = Object.assign({ username: unref(username) }, values);
defHttp.put({ url: '/sys/user/updatePassword', params }, { isTransformResponse: false }).then((res) => {
if (res.success) {
$message.createMessage.success(res.message);
//关闭弹窗
closeModal();
} else {
$message.createMessage.warning(res.message);
}
});
} finally {
setModalProps({ confirmLoading: false });
}
}
async function show(name) {
if (!name) {
$message.createMessage.warning('当前系统无登录用户!');
return;
} else {
username.value = name;
await setModalProps({ visible: true });
await resetFields();
await clearValidate();
}
}
defineExpose({
title,
show,
});
</script>
@@ -0,0 +1,254 @@
<template>
<Dropdown placement="bottomLeft" :overlayClassName="`${prefixCls}-dropdown-overlay`">
<span :class="[prefixCls, `${prefixCls}--${theme}`]" class="flex">
<img :class="`${prefixCls}__header`" :src="getAvatarUrl" />
<span :class="`${prefixCls}__info hidden md:block`">
<span :class="`${prefixCls}__name `" class="truncate">
{{ getUserInfo.realname }}
</span>
</span>
</span>
<template #overlay>
<Menu @click="handleMenuClick">
<MenuItem itemKey="doc" :text="t('layout.header.dropdownItemDoc')" icon="ion:document-text-outline" v-if="getShowDoc" />
<MenuDivider v-if="getShowDoc" />
<MenuItem itemKey="account" :text="t('layout.header.dropdownItemSwitchAccount')" icon="ant-design:setting-outlined" />
<MenuItem itemKey="password" :text="t('layout.header.dropdownItemSwitchPassword')" icon="ant-design:edit-outlined" />
<MenuItem itemKey="depart" :text="t('layout.header.dropdownItemSwitchDepart')" icon="ant-design:cluster-outlined" />
<MenuItem itemKey="cache" :text="t('layout.header.dropdownItemRefreshCache')" icon="ion:sync-outline" />
<!-- <MenuItem
v-if="getUseLockPage"
itemKey="lock"
:text="t('layout.header.tooltipLock')"
icon="ion:lock-closed-outline"
/>-->
<MenuItem itemKey="logout" :text="t('layout.header.dropdownItemLoginOut')" icon="ion:power-outline" />
</Menu>
</template>
</Dropdown>
<LockAction v-if="lockActionVisible" ref="lockActionRef" @register="register" />
<DepartSelect ref="loginSelectRef" />
<UpdatePassword v-if="passwordVisible" ref="updatePasswordRef" />
</template>
<script lang="ts">
// components
import { Dropdown, Menu } from 'ant-design-vue';
import { defineComponent, computed, ref } from 'vue';
import { SITE_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 { useMessage } from '/src/hooks/web/useMessage';
import { useGo } from '/@/hooks/web/usePage';
import headerImg from '/@/assets/images/header.jpg';
import { propTypes } from '/@/utils/propTypes';
import { openWindow } from '/@/utils';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { refreshCache, queryAllDictItems } from '/@/views/system/dict/dict.api';
import { DB_DICT_DATA_KEY } from '/src/enums/cacheEnum';
import { removeAuthCache, setAuthCache } from '/src/utils/auth';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { getRefPromise } from '/@/utils/index';
type MenuEvent = 'logout' | 'doc' | 'lock' | 'cache' | 'depart';
const { createMessage } = useMessage();
export default defineComponent({
name: 'UserDropdown',
components: {
Dropdown,
Menu,
MenuItem: createAsyncComponent(() => import('./DropMenuItem.vue')),
MenuDivider: Menu.Divider,
LockAction: createAsyncComponent(() => import('../lock/LockModal.vue')),
DepartSelect: createAsyncComponent(() => import('./DepartSelect.vue')),
UpdatePassword: createAsyncComponent(() => import('./UpdatePassword.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 passwordVisible = ref(false);
const lockActionVisible = ref(false);
const lockActionRef = ref(null);
const getUserInfo = computed(() => {
const { realname = '', avatar, desc } = userStore.getUserInfo || {};
return { realname, avatar: avatar || headerImg, desc };
});
const getAvatarUrl = computed(() => {
let { avatar } = getUserInfo.value;
if (avatar == headerImg) {
return avatar;
} else {
return getFileAccessHttpUrl(avatar);
}
});
const [register, { openModal }] = useModal();
/**
* 多部门弹窗逻辑
*/
const loginSelectRef = ref();
// update-begin--author:liaozhiyang---date:20230901---for:【QQYUN-6333】空路由问题—首次访问资源太大
async function handleLock() {
await getRefPromise(lockActionRef);
openModal(true);
}
// update-end--author:liaozhiyang---date:20230901---for:【QQYUN-6333】空路由问题—首次访问资源太大
// login out
function handleLoginOut() {
userStore.confirmLoginOut();
}
// open doc
function openDoc() {
openWindow(SITE_URL);
}
// 清除缓存
async function clearCache() {
const result = await refreshCache();
if (result.success) {
const res = await queryAllDictItems();
removeAuthCache(DB_DICT_DATA_KEY);
setAuthCache(DB_DICT_DATA_KEY, res.result);
// update-begin--author:liaozhiyang---date:20240124---for:【QQYUN-7970】国际化
createMessage.success(t('layout.header.refreshCacheComplete'));
// update-end--author:liaozhiyang---date:20240124---for:【QQYUN-7970】国际化
// update-begin--author:wangshuai---date:20241112---for:【issues/7433】vue3 数据字典优化建议
userStore.setAllDictItems(res.result);
// update-end--author:wangshuai---date:20241112---for:【issues/7433】vue3 数据字典优化建议
} else {
// update-begin--author:liaozhiyang---date:20240124---for:【QQYUN-7970】国际化
createMessage.error(t('layout.header.refreshCacheFailure'));
// update-end--author:liaozhiyang---date:20240124---for:【QQYUN-7970】国际化
}
}
// 切换部门
function updateCurrentDepart() {
loginSelectRef.value.show();
}
// 修改密码
const updatePasswordRef = ref();
// update-begin--author:liaozhiyang---date:20230901---for:【QQYUN-6333】空路由问题—首次访问资源太大
async function updatePassword() {
passwordVisible.value = true;
await getRefPromise(updatePasswordRef);
updatePasswordRef.value.show(userStore.getUserInfo.username);
}
// update-end--author:liaozhiyang---date:20230901---for:【QQYUN-6333】空路由问题—首次访问资源太大
function handleMenuClick(e: { key: MenuEvent }) {
switch (e.key) {
case 'logout':
handleLoginOut();
break;
case 'doc':
openDoc();
break;
case 'lock':
handleLock();
break;
case 'cache':
clearCache();
break;
case 'depart':
updateCurrentDepart();
break;
case 'password':
updatePassword();
break;
case 'account':
//update-begin---author:wangshuai ---date:20221125 for:进入用户设置页面------------
go(`/system/usersetting`);
//update-end---author:wangshuai ---date:20221125 for:进入用户设置页面--------------
break;
}
}
return {
prefixCls,
t,
getUserInfo,
getAvatarUrl,
handleMenuClick,
getShowDoc,
register,
getUseLockPage,
loginSelectRef,
updatePasswordRef,
passwordVisible,
lockActionVisible,
};
},
});
</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;
img {
width: 24px;
height: 24px;
margin-right: 12px;
}
&__header {
border-radius: 50%;
}
&__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 {
// update-begin--author:liaozhiyang---date:20231226---for:【QQYUN-7512】顶部账号划过首次弹出时位置会变更一下
width: 160px;
// update-end--author:liaozhiyang---date:20231226---for:【QQYUN-7512】顶部账号划过首次弹出时位置会变更一下
.ant-dropdown-menu-item {
min-width: 160px;
}
}
}
</style>
+200
View File
@@ -0,0 +1,200 @@
@header-trigger-prefix-cls: ~'@{namespace}-layout-header-trigger';
@header-prefix-cls: ~'@{namespace}-layout-header';
@breadcrumb-prefix-cls: ~'@{namespace}-layout-breadcrumb';
@logo-prefix-cls: ~'@{namespace}-app-logo';
.@{header-prefix-cls} {
display: flex;
height: @header-height;
padding: 0;
margin-left: -1px;
line-height: @header-height;
color: @white;
background-color: @white;
align-items: center;
justify-content: space-between;
&--mobile {
.@{breadcrumb-prefix-cls},
.error-action,
.notify-item,
.lock-item,
.fullscreen-item {
display: none;
}
.@{logo-prefix-cls} {
min-width: unset;
padding-right: 0;
&__title {
display: none;
}
}
.@{header-trigger-prefix-cls} {
padding: 0 4px 0 8px !important;
}
.@{header-prefix-cls}-action {
padding-right: 4px;
}
}
&--fixed {
position: fixed;
top: 0;
left: 0;
z-index: @layout-header-fixed-z-index;
width: 100%;
}
&-logo {
height: @header-height;
min-width: 192px;
padding: 0 10px;
font-size: 14px;
img {
width: @logo-width;
height: @logo-width;
margin-right: 2px;
}
}
&-left {
display: flex;
height: 100%;
align-items: center;
.@{header-trigger-prefix-cls} {
display: flex;
height: 100%;
padding: 1px 10px 0 10px;
cursor: pointer;
align-items: center;
.anticon {
font-size: 22px;
}
&.light {
&:hover {
background-color: @header-light-bg-hover-color;
}
svg {
fill: #000;
}
}
&.dark {
&:hover {
background-color: @header-dark-bg-hover-color;
}
}
}
}
&-menu {
height: 100%;
min-width: 0;
flex: 1;
align-items: center;
}
&-action {
display: flex;
min-width: 180px;
// padding-right: 12px;
align-items: center;
&__item {
display: flex !important;
height: @header-height;
padding: 0 2px;
font-size: 1.2em;
cursor: pointer;
align-items: center;
.ant-badge {
height: @header-height;
line-height: @header-height;
}
.ant-badge-dot {
top: 10px;
right: 2px;
}
}
span[role='img'] {
padding: 0 8px;
}
}
&--light {
background-color: @white !important;
border-bottom: 1px solid @header-light-bottom-border-color;
border-left: 1px solid @header-light-bottom-border-color;
.@{header-prefix-cls}-logo {
color: @text-color-base;
&:hover {
background-color: @header-light-bg-hover-color;
}
}
.@{header-prefix-cls}-action {
&__item {
color: @text-color-base;
.app-iconify {
padding: 0 10px;
font-size: 16px !important;
}
&:hover {
background-color: @header-light-bg-hover-color;
}
}
&-icon,
span[role='img'] {
color: @text-color-base;
}
}
}
&--dark {
background-color: @header-dark-bg-color !important;
// border-bottom: 1px solid @border-color-base;
border-left: 1px solid @border-color-base;
.@{header-prefix-cls}-logo {
&:hover {
background-color: @header-dark-bg-hover-color;
}
}
.@{header-prefix-cls}-action {
&__item {
.app-iconify {
padding: 0 10px;
font-size: 16px !important;
}
.ant-badge {
span {
color: @white;
}
}
&:hover {
background-color: @header-dark-bg-hover-color;
}
}
}
}
}
+261
View File
@@ -0,0 +1,261 @@
<template>
<Header :class="getHeaderClass">
<!-- left start -->
<div :class="`${prefixCls}-left`">
<!-- logo -->
<AppLogo v-if="getShowHeaderLogo || getIsMobile" :class="`${prefixCls}-logo`" :theme="getHeaderTheme" :style="getLogoWidth" />
<LayoutTrigger
v-if="(getShowContent && getShowHeaderTrigger && !getSplit && !getIsMixSidebar) || getIsMobile"
:theme="getHeaderTheme"
:sider="false"
/>
<LayoutBreadcrumb v-if="getShowContent && getShowBread" :theme="getHeaderTheme" />
<!-- 欢迎语 -->
<span v-if="getShowContent && getShowBreadTitle && !getIsMobile" :class="[prefixCls, `${prefixCls}--${getHeaderTheme}`,'headerIntroductionClass']"> {{t('layout.header.welcomeIn')}} {{ title }} </span>
</div>
<!-- left end -->
<!-- menu start -->
<div :class="`${prefixCls}-menu`" v-if="getShowTopMenu && !getIsMobile">
<LayoutMenu :isHorizontal="true" :theme="getHeaderTheme" :splitType="getSplitType" :menuMode="getMenuMode" />
</div>
<!-- menu-end -->
<!-- action -->
<div :class="`${prefixCls}-action`">
<AppSearch :class="`${prefixCls}-action__item `" v-if="getShowSearch" />
<ErrorAction v-if="getUseErrorHandle" :class="`${prefixCls}-action__item error-action`" />
<Notify v-if="getShowNotice" :class="`${prefixCls}-action__item notify-item`" />
<FullScreen v-if="getShowFullScreen" :class="`${prefixCls}-action__item fullscreen-item`" />
<LockScreen v-if="getUseLockPage" />
<AppLocalePicker v-if="getShowLocalePicker" :reload="true" :showText="false" :class="`${prefixCls}-action__item`" />
<UserDropDown :theme="getHeaderTheme" />
<SettingDrawer v-if="getShowSetting" :class="`${prefixCls}-action__item`" />
<!-- ai助手 -->
<Aide v-if="getAiIconShow"></Aide>
</div>
</Header>
<LoginSelect ref="loginSelectRef" @success="loginSelectOk"></LoginSelect>
</template>
<script lang="ts">
import { defineComponent, unref, computed, ref, onMounted, toRaw } from 'vue';
import { useGlobSetting } from '/@/hooks/setting';
import { propTypes } from '/@/utils/propTypes';
import { Layout } from 'ant-design-vue';
import { AppLogo } from '/@/components/Application';
import LayoutMenu from '../menu/index.vue';
import LayoutTrigger from '../trigger/index.vue';
import { AppSearch } from '/@/components/Application';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
import { SettingButtonPositionEnum } from '/@/enums/appEnum';
import { AppLocalePicker } from '/@/components/Application';
import { UserDropDown, LayoutBreadcrumb, FullScreen, Notify, ErrorAction, LockScreen } from './components';
import { useAppInject } from '/@/hooks/web/useAppInject';
import { useDesign } from '/@/hooks/web/useDesign';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { useLocale } from '/@/locales/useLocale';
import LoginSelect from '/@/views/sys/login/LoginSelect.vue';
import { useUserStore } from '/@/store/modules/user';
import { useI18n } from '/@/hooks/web/useI18n';
import Aide from "@/views/dashboard/ai/components/aide/index.vue"
const { t } = useI18n();
export default defineComponent({
name: 'LayoutHeader',
components: {
Header: Layout.Header,
AppLogo,
LayoutTrigger,
LayoutBreadcrumb,
LayoutMenu,
UserDropDown,
AppLocalePicker,
FullScreen,
Notify,
AppSearch,
ErrorAction,
LockScreen,
LoginSelect,
SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue'), {
loading: true,
}),
Aide
},
props: {
fixed: propTypes.bool,
},
setup(props) {
const { prefixCls } = useDesign('layout-header');
const userStore = useUserStore();
const { getShowTopMenu, getShowHeaderTrigger, getSplit, getIsMixMode, getMenuWidth, getIsMixSidebar } = useMenuSetting();
const { getUseErrorHandle, getShowSettingButton, getSettingButtonPosition, getAiIconShow } = useRootSetting();
const { title } = useGlobSetting();
const {
getHeaderTheme,
getShowFullScreen,
getShowNotice,
getShowContent,
getShowBread,
getShowHeaderLogo,
getShowHeader,
getShowSearch,
getUseLockPage,
getShowBreadTitle,
} = useHeaderSetting();
const { getShowLocalePicker } = useLocale();
const { getIsMobile } = useAppInject();
const getHeaderClass = computed(() => {
const theme = unref(getHeaderTheme);
return [
prefixCls,
{
[`${prefixCls}--fixed`]: props.fixed,
[`${prefixCls}--mobile`]: unref(getIsMobile),
[`${prefixCls}--${theme}`]: theme,
},
];
});
const getShowSetting = computed(() => {
if (!unref(getShowSettingButton)) {
return false;
}
const settingButtonPosition = unref(getSettingButtonPosition);
if (settingButtonPosition === SettingButtonPositionEnum.AUTO) {
return unref(getShowHeader);
}
return settingButtonPosition === SettingButtonPositionEnum.HEADER;
});
const getLogoWidth = computed(() => {
if (!unref(getIsMixMode) || unref(getIsMobile)) {
return {};
}
const width = unref(getMenuWidth) < 180 ? 180 : unref(getMenuWidth);
return { width: `${width}px` };
});
const getSplitType = computed(() => {
return unref(getSplit) ? MenuSplitTyeEnum.TOP : MenuSplitTyeEnum.NONE;
});
const getMenuMode = computed(() => {
return unref(getSplit) ? MenuModeEnum.HORIZONTAL : null;
});
/**
* 首页多租户部门弹窗逻辑
*/
const loginSelectRef = ref();
function showLoginSelect() {
//update-begin---author:liusq Date:20220101 for:判断登录进来是否需要弹窗选择租户----
//判断是否是登陆进来
const loginInfo = toRaw(userStore.getLoginInfo) || {};
if (!!loginInfo.isLogin) {
loginSelectRef.value.show(loginInfo);
}
//update-end---author:liusq Date:20220101 for:判断登录进来是否需要弹窗选择租户----
}
function loginSelectOk() {
console.log('成功。。。。。');
}
onMounted(() => {
showLoginSelect();
});
return {
prefixCls,
getHeaderClass,
getShowHeaderLogo,
getHeaderTheme,
getShowHeaderTrigger,
getIsMobile,
getShowBreadTitle,
getShowBread,
getShowContent,
getSplitType,
getSplit,
getMenuMode,
getShowTopMenu,
getShowLocalePicker,
getShowFullScreen,
getShowNotice,
getUseErrorHandle,
getLogoWidth,
getIsMixSidebar,
getShowSettingButton,
getShowSetting,
getShowSearch,
getUseLockPage,
loginSelectOk,
loginSelectRef,
title,
t,
getAiIconShow
};
},
});
</script>
<style lang="less">
@import './index.less';
//update-begin---author:scott ---date:2022-09-30 for:默认隐藏顶部菜单面包屑-----------
//顶部欢迎语展示样式
@prefix-cls: ~'@{namespace}-layout-header';
.ant-layout .@{prefix-cls} {
display: flex;
padding: 0 8px;
// update-begin--author:liaozhiyang---date:20240407---for:【QQYUN-8762】顶栏高度
height: @header-height;
// update-end--author:liaozhiyang---date:20240407---for:【QQYUN-8762】顶栏高度
align-items: center;
.headerIntroductionClass {
margin-right: 4px;
margin-bottom: 2px;
border-bottom: 0px;
border-left: 0px;
}
&--light {
.headerIntroductionClass {
color: #000;
}
}
&--dark {
.headerIntroductionClass {
color: rgba(255, 255, 255, 1);
}
.anticon, .truncate {
color: rgba(255, 255, 255, 1);
}
}
//update-end---author:scott ---date::2022-09-30 for:默认隐藏顶部菜单面包屑--------------
}
</style>