【V3.5.0】1、【新增】轻量级定时任务 SmartJob;2、【新增】站内信;3、【新增】个人中心;4、【新增】岗位管理;5、【优化】部门员工管理

This commit is contained in:
zhuoda
2024-07-16 00:20:02 +08:00
parent 23e8ea55e1
commit 716b6303e3
504 changed files with 59745 additions and 1110 deletions

View File

@@ -1,24 +1,28 @@
<!--
* 头像
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:02:01
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:02:01
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-dropdown class="header-trigger">
<div class="wrapper">
<a-avatar style="margin: 0 5px" :size="24" id="smartAdminAvatar">
<img class="avatar-image" :src="avatar" v-if="avatar" />
<a-avatar v-else style="margin: 0 5px" :size="20" id="smartAdminAvatar">
{{ avatarName }}
</a-avatar>
<span class="name">{{ actualName }}</span>
</div>
<template #overlay>
<a-menu :class="['avatar-menu']">
<a-menu-item @click="showUpdatePwdModal">
<a-menu-item @click="toAccount()">
<span>个人中心</span>
</a-menu-item>
<a-menu-item @click="toAccount(ACCOUNT_MENU.PASSWORD.menuId)">
<span>修改密码</span>
</a-menu-item>
<a-menu-item @click="onLogout">
@@ -36,6 +40,8 @@
import { localClear } from '/@/utils/local-util';
import { smartSentry } from '/@/lib/smart-sentry';
import HeaderResetPassword from './header-reset-password-modal/index.vue';
import { useRouter } from 'vue-router';
import { ACCOUNT_MENU } from '/@/views/system/account/account-menu.js';
// 头像背景颜色
const AVATAR_BACKGROUND_COLOR_ARRAY = ['#87d068', '#00B853', '#f56a00', '#1890ff'];
@@ -53,6 +59,15 @@
}
}
// ------------------------ 个人中心 ------------------------
const router = useRouter();
function toAccount(menuId) {
router.push({
path: '/account',
query: { menuId },
});
}
// ------------------------ 修改密码 ------------------------
const resetPasswordRef = ref();
@@ -63,7 +78,9 @@
// ------------------------ 以下是 头像和姓名 相关 ------------------------
const avatarName = ref('');
const avatar = computed(() => useUserStore().avatar);
const actualName = computed(() => useUserStore().actualName);
// 更新头像信息
function updateAvatar() {
if (useUserStore().actualName) {
@@ -98,7 +115,15 @@
cursor: pointer;
display: flex;
align-items: center;
.avatar-image {
width: 20px;
height: 20px;
object-fit: cover;
border-radius: 50%;
}
}
.header-trigger {
height: @header-user-height;
line-height: @header-user-height;

View File

@@ -1,77 +1,65 @@
<!--
* 消息通知
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:17:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:17:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-dropdown trigger="click" v-model:open="show">
<div @click="fetchMessage">
<a-badge count="12">
<a-button type="text" @click="queryMessage" style="padding: 4px 5px">
<a-badge :count="unreadMessageCount">
<div style="width: 26px; height: 26px">
<BellOutlined :style="{ fontSize: '18px' }" />
<BellOutlined :style="{ fontSize: '16px' }" />
</div>
</a-badge>
</div>
</a-button>
<template #overlay>
<div>
<a-card class="message-container" :bodyStyle="{ padding: 0 }">
<a-spin :spinning="loading">
<a-tabs class="dropdown-tabs" centered :tabBarStyle="{ textAlign: 'center' }" style="width: 300px">
<a-tab-pane tab="通知" key="1">
<a-tab-pane tab="未读消息" key="message">
<a-list class="tab-pane" size="small">
<a-list-item>
<a-list-item-meta description="7天前">
<a-list-item v-for="item in messageList" :key="item.messageId">
<a-list-item-meta>
<template #title>
<a href="#">今天洛阳天气39°c洛阳变各阳...</a>
<div class="title">
<a @click="gotoMessage">{{ item.title }}</a>
</div>
</template>
<template #avatar>
<a-avatar src="https://zhuoda.vip/images/blog/profile-128-128.jpg" />
</template>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta description="7天前">
<template #title>
<a href="#">六月的雨 就是无情的你...</a>
</template>
<template #avatar>
<a-avatar src="https://zhuoda.vip/images/blog/profile-128-128.jpg" />
</template>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta description="7天前">
<template #title>
<a href="#">今年河南天气炎热河南变可南...</a>
</template>
<template #avatar>
<a-avatar src="https://zhuoda.vip/images/blog/profile-128-128.jpg" />
<template #description>
<span> {{ timeago(item.createTime) }}</span>
</template>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane tab="消息" key="2">
<a-list class="tab-pane" />
</a-tab-pane>
<a-tab-pane tab="待办" key="3">
<a-tab-pane tab="待办工作" key="todo">
<a-list class="tab-pane" />
</a-tab-pane>
</a-tabs>
</a-spin>
</div>
</a-card>
</template>
</a-dropdown>
</template>
<script setup>
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { BellOutlined } from '@ant-design/icons-vue';
import { useUserStore } from '/@/store/modules/system/user.js';
import { smartSentry } from '/@/lib/smart-sentry.js';
import { messageApi } from '/@/api/support/message-api.js';
import dayjs from 'dayjs';
import { theme } from 'ant-design-vue';
import { useRouter } from 'vue-router';
const { useToken } = theme;
const { token } = useToken();
defineExpose({ showMessage });
@@ -82,19 +70,97 @@
const loading = ref(false);
const show = ref(false);
const fetchMessage = () => {
if (loading.value) {
// ------------------------- 查询消息 -------------------------
// 未读消息
const unreadMessageCount = computed(() => {
return useUserStore().unreadMessageCount;
});
// 消息列表
const messageList = ref([]);
// 查询我的未读消息
async function queryMessage() {
try {
loading.value = true;
let responseModel = await messageApi.queryMessage({
pageNum: 1,
pageSize: 3,
readFlag: false,
});
messageList.value = responseModel.data.list;
} catch (e) {
smartSentry.captureError(e);
} finally {
loading.value = false;
return;
}
loading.value = true;
setTimeout(() => {
loading.value = false;
}, 700);
};
}
const router = useRouter();
function gotoMessage() {
show.value = false;
router.push({ path: '/account', query: { menuId: 'message' } });
}
// ------------------------- 时间计算 -------------------------
function timeago(dateStr) {
let dateTimeStamp = dayjs(dateStr).toDate().getTime();
let result = '';
let minute = 1000 * 60; //把分,时,天,周,半个月,一个月用毫秒表示
let hour = minute * 60;
let day = hour * 24;
let week = day * 7;
let month = day * 30;
let now = new Date().getTime(); //获取当前时间毫秒
let diffValue = now - dateTimeStamp; //时间差
if (diffValue < 0) {
return '刚刚';
}
let minC = diffValue / minute; //计算时间差的分,时,天,周,月
let hourC = diffValue / hour;
let dayC = diffValue / day;
let weekC = diffValue / week;
let monthC = diffValue / month;
if (monthC >= 1 && monthC <= 3) {
result = ' ' + parseInt(monthC) + '月前';
} else if (weekC >= 1 && weekC <= 3) {
result = ' ' + parseInt(weekC) + '周前';
} else if (dayC >= 1 && dayC <= 6) {
result = ' ' + parseInt(dayC) + '天前';
} else if (hourC >= 1 && hourC <= 23) {
result = ' ' + parseInt(hourC) + '小时前';
} else if (minC >= 1 && minC <= 59) {
result = ' ' + parseInt(minC) + '分钟前';
} else if (diffValue >= 0 && diffValue <= minute) {
result = '刚刚';
} else {
let datetime = new Date();
datetime.setTime(dateTimeStamp);
let year = datetime.getFullYear();
let month = datetime.getMonth() + 1 < 10 ? '0' + (datetime.getMonth() + 1) : datetime.getMonth() + 1;
let date = datetime.getDate() < 10 ? '0' + datetime.getDate() : datetime.getDate();
result = year + '-' + month + '-' + date;
}
return result;
}
</script>
<style lang="less">
<style lang="less" scoped>
@smart-page-tag-operate-width: 40px;
@color-primary: v-bind('token.colorPrimary');
.message-icon-div {
cursor: pointer;
height: 32px;
width: 42px;
padding-left: 10px;
}
.message-icon-div:hover {
background: @hover-bg-color !important;
}
.header-notice {
display: inline-block;
transition: all 0.3s;
@@ -108,6 +174,17 @@
}
}
.title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.message-container {
border: #eeeeee solid 1px;
}
.dropdown-tabs {
background-color: @base-bg-color;
border-radius: 4px;

View File

@@ -11,8 +11,13 @@
<template>
<a-drawer :title="$t('setting.title')" placement="right" :open="visible" @close="close">
<a-form layout="horizontal" :label-col="{ span: 8 }">
<a-form-item label="语言/Language">
<a-select v-model:value="formState.language" @change="changeLanguage" style="width: 120px">
<a-select-option v-for="item in i18nList" :key="item.value" :value="item.value">{{ item.text }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('setting.color')">
<div style="display: flex; align-items: center">
<div class="color-container">
<template v-for="(item, index) in themeColors">
<div v-if="index === formState.colorIndex" class="color">
<CheckSquareFilled :style="{ color: item.primaryColor, fontSize: '22px' }" />
@@ -38,6 +43,14 @@
<a-form-item :label="$t('setting.border.radius')">
<a-slider v-model:value="formState.borderRadius" :min="0" :max="6" @change="changeBorderRadius" />
</a-form-item>
<a-form-item :label="$t('setting.menu.width')" v-if="formState.layout === LAYOUT_ENUM.SIDE.value">
<a-input-number @change="changeSideMenuWidth" v-model:value="formState.sideMenuWidth" :min="1" />
像素px
</a-form-item>
<a-form-item :label="$t('setting.page.width')" v-if="formState.layout === LAYOUT_ENUM.TOP.value">
<a-input @change="changePageWidth" v-model:value="formState.pageWidth" />
像素px或者 百分比
</a-form-item>
<a-form-item :label="$t('setting.compact')">
<a-radio-group v-model:value="formState.compactFlag" button-style="solid" @change="changeCompactFlag">
<a-radio-button :value="false">默认</a-radio-button>
@@ -57,35 +70,39 @@
<a-radio-button value="light">Light</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item :label="$t('setting.menu.width')" v-if="formState.layout === LAYOUT_ENUM.SIDE.value">
<a-input-number @change="changeSideMenuWidth" v-model:value="formState.sideMenuWidth" :min="1" />
像素px
</a-form-item>
<a-form-item :label="$t('setting.page.width')" v-if="formState.layout === LAYOUT_ENUM.TOP.value">
<a-input @change="changePageWidth" v-model:value="formState.pageWidth" />
像素px或者 百分比
</a-form-item>
<a-form-item label="语言/Language">
<a-select v-model:value="formState.language" @change="changeLanguage" style="width: 120px">
<a-select-option v-for="item in i18nList" :key="item.value" :value="item.value">{{ item.text }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('setting.bread')">
<a-switch @change="changeBreadCrumbFlag" v-model:checked="formState.breadCrumbFlag" checked-children="显示" un-checked-children="隐藏" />
<a-form-item :label="$t('setting.pagetag.style')">
<a-radio-group v-model:value="formState.pageTagStyle" button-style="solid" @change="changePageTagStyle">
<a-radio-button value="default">默认</a-radio-button>
<a-radio-button value="antd">Ant Design</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item :label="$t('setting.pagetag')">
<a-switch @change="changePageTagFlag" v-model:checked="formState.pageTagFlag" checked-children="显示" un-checked-children="隐藏" />
</a-form-item>
<a-form-item :label="$t('setting.bread')">
<a-switch @change="changeBreadCrumbFlag" v-model:checked="formState.breadCrumbFlag" checked-children="显示" un-checked-children="隐藏" />
</a-form-item>
<a-form-item :label="$t('setting.footer')">
<a-switch @change="changeFooterFlag" v-model:checked="formState.footerFlag" checked-children="显示" un-checked-children="隐藏" />
</a-form-item>
<a-form-item :label="$t('setting.helpdoc')">
<a-switch @change="changeHelpDocFlag" v-model:checked="formState.helpDocFlag" checked-children="显示" un-checked-children="隐藏" />
</a-form-item>
<a-form-item :label="$t('setting.watermark')">
<a-switch @change="changeWatermarkFlag" v-model:checked="formState.watermarkFlag" checked-children="显示" un-checked-children="隐藏" />
</a-form-item>
<a-form-item :label="$t('setting.helpdoc')">
<a-switch @change="changeHelpDocFlag" v-model:checked="formState.helpDocFlag" checked-children="显示" un-checked-children="隐藏" />
</a-form-item>
<a-form-item :label="$t('setting.helpdoc.expand')" v-if="formState.helpDocFlag">
<a-switch
@change="changeHelpDocExpandFlag"
v-model:checked="formState.helpDocExpandFlag"
checked-children="默认展开"
un-checked-children="默认不展开"
/>
</a-form-item>
<br />
<br />
</a-form>
<div class="footer">
<a-button style="margin-right: 8px" type="primary" @click="copy">复制配置信息</a-button>
<a-button type="block" danger @click="reset">恢复默认配置 </a-button>
@@ -176,12 +193,16 @@
borderRadius: appConfigStore.borderRadius,
// 标签页
pageTagFlag: appConfigStore.pageTagFlag,
// 标签页 样式
pageTagStyle: appConfigStore.pageTagStyle,
// 面包屑
breadCrumbFlag: appConfigStore.breadCrumbFlag,
// 页脚
footerFlag: appConfigStore.footerFlag,
// 帮助文档
helpDocFlag: appConfigStore.helpDocFlag,
// 帮助文档 默认展开
helpDocExpandFlag: appConfigStore.helpDocExpandFlag,
// 水印
watermarkFlag: appConfigStore.watermarkFlag,
};
@@ -250,6 +271,12 @@
});
}
function changePageTagStyle(e) {
appConfigStore.$patch({
pageTagStyle: e.target.value,
});
}
function changeFooterFlag(e) {
appConfigStore.$patch({
footerFlag: e,
@@ -262,6 +289,12 @@
});
}
function changeHelpDocExpandFlag(e) {
appConfigStore.$patch({
helpDocExpandFlag: e,
});
}
function changeWatermarkFlag(e) {
appConfigStore.$patch({
watermarkFlag: e,
@@ -281,9 +314,14 @@
z-index: 1;
}
.color-container {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
}
.color {
margin-left: 8px;
display: inline;
height: 26px;
width: 26px;
display: flex;

View File

@@ -1,11 +1,11 @@
<!--
* 头部一整行
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:18:20
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:18:20
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-space :size="10">
@@ -34,9 +34,9 @@
<HeaderAvatar />
</div>
<!---帮助文档--->
<div class="user-space-item" @click="showHelpDoc" v-if="!showHelpDocFlag">
<div class="user-space-item" @click="showHelpDoc" v-if="showHelpDocFlag">
<span>帮助文档</span>
<DoubleLeftOutlined v-if="!showHelpDocFlag" />
<DoubleLeftOutlined v-if="!helpDocExpandFlag" />
</div>
<HeaderSetting ref="headerSetting" />
@@ -72,6 +72,10 @@
return useAppConfigStore().helpDocFlag;
});
const helpDocExpandFlag = computed(() => {
return useAppConfigStore().helpDocExpandFlag;
});
//搜索
function search() {
window.open('https://1024lab.net');
@@ -100,7 +104,7 @@
.user-space-item:hover {
color: v-bind('token.colorPrimary');
background: @hover-bg-color;
background-color: @hover-bg-color !important;
}
.setting {

View File

@@ -0,0 +1,223 @@
<!--
* 使用ant design <a-tabs> 组件
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<!-- 标签页共两部分1标签 2标签操作区 -->
<a-row style="border-bottom: 1px solid #eeeeee; position: relative" v-show="pageTagFlag">
<a-dropdown :trigger="['contextmenu']">
<div class="smart-page-tag">
<a-tabs style="width: 100%" type="card" :tab-position="mode" v-model:activeKey="selectedKey" size="small" @tabClick="selectTab">
<a-tab-pane v-for="item in tagNav" :key="item.menuName">
<template #tab>
<span>
{{ item.menuTitle }}
<close-outlined @click.stop="closeTag(item, false)" v-if="item.menuName !== HOME_PAGE_NAME" class="smart-page-tag-close" />
</span>
</template>
</a-tab-pane>
</a-tabs>
</div>
<template #rightExtra>
<a-menu>
<a-menu-item @click="closeByMenu(false)">关闭其他</a-menu-item>
<a-menu-item @click="closeByMenu(true)">关闭所有</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-dropdown>
<!--标签页操作区-->
<div class="smart-page-tag-operate">
<div class="smart-page-tag-operate-icon">
<AppstoreOutlined />
</div>
</div>
<template #overlay>
<a-menu>
<a-menu-item @click="closeByMenu(false)">关闭其他</a-menu-item>
<a-menu-item @click="closeByMenu(true)">关闭所有</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-row>
</template>
<script setup>
import { AppstoreOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { useUserStore } from '/@/store/modules/system/user';
import { theme } from 'ant-design-vue';
//标签页 是否显示
const pageTagFlag = computed(() => useAppConfigStore().$state.pageTagFlag);
const router = useRouter();
const route = useRoute();
const mode = ref('top');
const tagNav = computed(() => useUserStore().getTagNav || []);
const selectedKey = ref(route.name);
watch(
() => route.name,
(newValue, oldValue) => {
selectedKey.value = newValue;
},
{ immediate: true }
);
//选择某个标签页
function selectTab(name) {
if (selectedKey.value === name) {
return;
}
// 寻找tag
let tag = tagNav.value.find((e) => e.menuName === name);
if (!tag) {
router.push({ name: HOME_PAGE_NAME });
return;
}
// router.push({ name, query: Object.assign({ _keepAlive: 1 }, tag.menuQuery) });
router.push({ name, query: tag.menuQuery });
}
//通过菜单关闭
function closeByMenu(closeAll) {
let find = tagNav.value.find((e) => e.menuName === selectedKey.value);
if (!find || closeAll) {
closeTag(null, true);
} else {
closeTag(find, true);
}
}
//直接关闭
function closeTag(item, closeAll) {
// 关闭单个tag
if (item && !closeAll) {
let goName = HOME_PAGE_NAME;
let goQuery = undefined;
if (item.fromMenuName && item.fromMenuName !== item.menuName && tagNav.value.some((e) => e.menuName === item.fromMenuName)) {
goName = item.fromMenuName;
goQuery = item.fromMenuQuery;
} else {
// 查询左侧tag
let index = tagNav.value.findIndex((e) => e.menuName === item.menuName);
if (index > 0) {
// 查询左侧tag
let leftTagNav = tagNav.value[index - 1];
goName = leftTagNav.menuName;
goQuery = leftTagNav.menuQuery;
}
}
// router.push({ name: goName, query: Object.assign({ _keepAlive: 1 }, goQuery) });
router.push({ name: goName, query: goQuery });
} else if (!item && closeAll) {
// 关闭所有tag
router.push({ name: HOME_PAGE_NAME });
}
// 关闭其他tag不做处理 直接调用closeTagNav
useUserStore().closeTagNav(item ? item.menuName : null, closeAll);
}
const { useToken } = theme;
const { token } = useToken();
const borderRadius = computed(() => {
return token.value.borderRadius + 'px';
});
</script>
<style scoped lang="less">
@smart-page-tag-operate-width: 40px;
@color-primary: v-bind('token.colorPrimary');
.smart-page-tag-operate {
width: @smart-page-tag-operate-width;
height: @smart-page-tag-operate-width;
background-color: #ffffff;
font-size: 17px;
text-align: center;
vertical-align: middle;
line-height: @smart-page-tag-operate-width;
padding-right: 10px;
cursor: pointer;
color: #606266;
.smart-page-tag-operate-icon {
width: 20px;
height: 20px;
transition: all 1s;
transform-origin: 10px 20px;
}
.smart-page-tag-operate-icon:hover {
width: 20px;
height: 20px;
transform: rotate(360deg);
}
}
.smart-page-tag-operate:hover {
color: @color-primary;
}
.smart-page-tag {
position: relative;
box-sizing: border-box;
display: flex;
align-content: center;
align-items: center;
justify-content: space-between;
min-height: @page-tag-height;
padding-right: 20px;
padding-left: 20px;
user-select: none;
background: #fff;
width: calc(100% - @smart-page-tag-operate-width);
.smart-page-tag-close {
margin-left: 5px;
font-size: 10px;
color: #666666;
}
/** 覆盖 ant design vue的 tabs 样式,变小一点 **/
:deep(.ant-tabs-nav) {
margin: 0;
}
:deep(.ant-tabs-nav::before) {
border-bottom: 1px solid #ffffff;
}
:deep(.ant-tabs-small > .ant-tabs-nav .ant-tabs-tab) {
padding: 5px 8px 3px 15px;
margin: 8px 0 0 5px;
min-width: 60px;
height: 32px;
border-radius: v-bind(borderRadius) v-bind(borderRadius) 0 0;
border-bottom: 0;
}
:deep(.ant-tabs-tab-active) {
.smart-page-tag-close {
color: @color-primary;
}
}
:deep(.ant-tabs-nav .ant-tabs-tab:hover) {
background-color: white;
.smart-page-tag-close {
color: @color-primary;
}
}
}
</style>

View File

@@ -0,0 +1,222 @@
<!--
* 标签页
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<!-- 标签页共两部分1标签 2标签操作区 -->
<a-row style="border-bottom: 1px solid #eeeeee; position: relative" v-show="pageTagFlag">
<a-dropdown :trigger="['contextmenu']">
<div class="smart-page-tag">
<a-tabs style="width: 100%" :tab-position="mode" v-model:activeKey="selectedKey" size="small" @tabClick="selectTab">
<a-tab-pane v-for="item in tagNav" :key="item.menuName">
<template #tab>
<span>
{{ item.menuTitle }}
<close-outlined @click.stop="closeTag(item, false)" v-if="item.menuName !== HOME_PAGE_NAME" class="smart-page-tag-close" />
<home-outlined style="font-size: 12px" v-if="item.menuName === HOME_PAGE_NAME" class="smart-page-tag-close" />
</span>
</template>
</a-tab-pane>
</a-tabs>
</div>
<template #rightExtra>
<a-menu>
<a-menu-item @click="closeByMenu(false)">关闭其他</a-menu-item>
<a-menu-item @click="closeByMenu(true)">关闭所有</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-dropdown>
<!--标签页操作区-->
<div class="smart-page-tag-operate">
<div class="smart-page-tag-operate-icon">
<AppstoreOutlined />
</div>
</div>
<template #overlay>
<a-menu>
<a-menu-item @click="closeByMenu(false)">关闭其他</a-menu-item>
<a-menu-item @click="closeByMenu(true)">关闭所有</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-row>
</template>
<script setup>
import { AppstoreOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { useUserStore } from '/@/store/modules/system/user';
import { theme } from 'ant-design-vue';
//标签页 是否显示
const pageTagFlag = computed(() => useAppConfigStore().$state.pageTagFlag);
const router = useRouter();
const route = useRoute();
const mode = ref('top');
const tagNav = computed(() => useUserStore().getTagNav || []);
const selectedKey = ref(route.name);
watch(
() => route.name,
(newValue, oldValue) => {
selectedKey.value = newValue;
},
{ immediate: true }
);
//选择某个标签页
function selectTab(name) {
if (selectedKey.value === name) {
return;
}
// 寻找tag
let tag = tagNav.value.find((e) => e.menuName === name);
if (!tag) {
router.push({ name: HOME_PAGE_NAME });
return;
}
// router.push({ name, query: Object.assign({ _keepAlive: 1 }, tag.menuQuery) });
router.push({ name, query: tag.menuQuery });
}
//通过菜单关闭
function closeByMenu(closeAll) {
let find = tagNav.value.find((e) => e.menuName === selectedKey.value);
if (!find || closeAll) {
closeTag(null, true);
} else {
closeTag(find, true);
}
}
//直接关闭
function closeTag(item, closeAll) {
// 关闭单个tag
if (item && !closeAll) {
let goName = HOME_PAGE_NAME;
let goQuery = undefined;
if (item.fromMenuName && item.fromMenuName !== item.menuName && tagNav.value.some((e) => e.menuName === item.fromMenuName)) {
goName = item.fromMenuName;
goQuery = item.fromMenuQuery;
} else {
// 查询左侧tag
let index = tagNav.value.findIndex((e) => e.menuName === item.menuName);
if (index > 0) {
// 查询左侧tag
let leftTagNav = tagNav.value[index - 1];
goName = leftTagNav.menuName;
goQuery = leftTagNav.menuQuery;
}
}
// router.push({ name: goName, query: Object.assign({ _keepAlive: 1 }, goQuery) });
router.push({ name: goName, query: goQuery });
} else if (!item && closeAll) {
// 关闭所有tag
router.push({ name: HOME_PAGE_NAME });
}
// 关闭其他tag不做处理 直接调用closeTagNav
useUserStore().closeTagNav(item ? item.menuName : null, closeAll);
}
const { useToken } = theme;
const { token } = useToken();
const borderRadius = token.value.borderRadius + 'px';
</script>
<style scoped lang="less">
@smart-page-tag-operate-width: 40px;
@color-primary: v-bind('token.colorPrimary');
.smart-page-tag-operate {
width: @smart-page-tag-operate-width;
height: @smart-page-tag-operate-width;
background-color: #ffffff;
font-size: 17px;
text-align: center;
vertical-align: middle;
line-height: @smart-page-tag-operate-width;
padding-right: 10px;
cursor: pointer;
color: #606266;
.smart-page-tag-operate-icon {
width: 20px;
height: 20px;
transition: all 1s;
transform-origin: 10px 20px;
}
.smart-page-tag-operate-icon:hover {
width: 20px;
height: 20px;
transform: rotate(360deg);
}
}
.smart-page-tag-operate:hover {
color: @color-primary;
}
.smart-page-tag {
position: relative;
box-sizing: border-box;
display: flex;
align-content: center;
align-items: center;
justify-content: space-between;
min-height: @page-tag-height;
padding-right: 20px;
padding-left: 20px;
user-select: none;
background: #fff;
width: calc(100% - @smart-page-tag-operate-width);
.smart-page-tag-close {
margin-left: 5px;
font-size: 10px;
color: #666666;
}
/** 覆盖 ant design vue的 tabs 样式,变小一点 **/
:deep(.ant-tabs-nav) {
margin: 0;
padding: 0 0 2px 0;
max-height: 32px;
}
:deep(.ant-tabs-nav::before) {
border-bottom: 1px solid #ffffff;
}
:deep(.ant-tabs-small > .ant-tabs-nav .ant-tabs-tab) {
padding: 5px 8px 3px 20px;
border-radius: v-bind(borderRadius);
margin: 0 0 0 5px !important;
}
:deep(.ant-tabs-tab-active) {
background-color: #eeeeee;
.smart-page-tag-close {
color: @color-primary;
}
}
:deep(.ant-tabs-nav .ant-tabs-tab:hover) {
background-color: #eeeeee;
.smart-page-tag-close {
color: @color-primary;
}
}
}
</style>

View File

@@ -1,221 +1,23 @@
<!--
* 标签页
* 标签页 入口支持三种模式默认a-tabs, chrome-tabs
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Date: 2024-06-12 20:55:04
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<!-- 标签页共两部分1标签 2标签操作区 -->
<a-row style="border-bottom: 1px solid #eeeeee; position: relative" v-show="pageTagFlag">
<a-dropdown :trigger="['contextmenu']">
<div class="smart-page-tag">
<a-tabs style="width: 100%" :tab-position="mode" v-model:activeKey="selectedKey" size="small" @tabClick="selectTab">
<a-tab-pane v-for="item in tagNav" :key="item.menuName">
<template #tab>
<span>
{{ item.menuTitle }}
<close-outlined @click.stop="closeTag(item, false)" v-if="item.menuName !== HOME_PAGE_NAME" class="smart-page-tag-close" />
</span>
</template>
</a-tab-pane>
</a-tabs>
</div>
<template #rightExtra>
<a-menu>
<a-menu-item @click="closeByMenu(false)">关闭其他</a-menu-item>
<a-menu-item @click="closeByMenu(true)">关闭所有</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-dropdown>
<!--标签页操作区-->
<div class="smart-page-tag-operate">
<div class="smart-page-tag-operate-icon">
<AppstoreOutlined />
</div>
</div>
<template #overlay>
<a-menu>
<a-menu-item @click="closeByMenu(false)">关闭其他</a-menu-item>
<a-menu-item @click="closeByMenu(true)">关闭所有</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-row>
<DefaultTab v-if="pageTagStyle === PAGE_TAG_ENUM.DEFAULT.value" />
<AntdTab v-if="pageTagStyle === PAGE_TAG_ENUM.ANTD.value" />
</template>
<script setup>
import { AppstoreOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { computed } from 'vue';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { useUserStore } from '/@/store/modules/system/user';
import { theme } from 'ant-design-vue';
import DefaultTab from './components/default-tab.vue';
import AntdTab from './components/antd-tab.vue';
import { PAGE_TAG_ENUM } from '/@/constants/layout-const.js';
//标签页 是否显示
const pageTagFlag = computed(() => useAppConfigStore().$state.pageTagFlag);
const router = useRouter();
const route = useRoute();
const mode = ref('top');
const tagNav = computed(() => useUserStore().getTagNav || []);
const selectedKey = ref(route.name);
watch(
() => route.name,
(newValue, oldValue) => {
selectedKey.value = newValue;
},
{ immediate: true }
);
//选择某个标签页
function selectTab(name) {
if (selectedKey.value === name) {
return;
}
// 寻找tag
let tag = tagNav.value.find((e) => e.menuName === name);
if (!tag) {
router.push({ name: HOME_PAGE_NAME });
return;
}
// router.push({ name, query: Object.assign({ _keepAlive: 1 }, tag.menuQuery) });
router.push({ name, query: tag.menuQuery });
}
//通过菜单关闭
function closeByMenu(closeAll) {
let find = tagNav.value.find((e) => e.menuName === selectedKey.value);
if (!find || closeAll) {
closeTag(null, true);
} else {
closeTag(find, true);
}
}
//直接关闭
function closeTag(item, closeAll) {
// 关闭单个tag
if (item && !closeAll) {
let goName = HOME_PAGE_NAME;
let goQuery = undefined;
if (item.fromMenuName && tagNav.value.some((e) => e.menuName === item.fromMenuName)) {
goName = item.fromMenuName;
goQuery = item.fromMenuQuery;
} else {
// 查询左侧tag
let index = tagNav.value.findIndex((e) => e.menuName === item.menuName);
if (index > 0) {
// 查询左侧tag
let leftTagNav = tagNav.value[index - 1];
goName = leftTagNav.menuName;
goQuery = leftTagNav.menuQuery;
}
}
// router.push({ name: goName, query: Object.assign({ _keepAlive: 1 }, goQuery) });
router.push({ name: goName, query: goQuery });
} else if (!item && closeAll) {
// 关闭所有tag
router.push({ name: HOME_PAGE_NAME });
}
// 关闭其他tag不做处理 直接调用closeTagNav
useUserStore().closeTagNav(item ? item.menuName : null, closeAll);
}
const { useToken } = theme;
const { token } = useToken();
const borderRadius = computed(() => {
return token.value.borderRadius + 'px';
});
const pageTagStyle = computed(() => useAppConfigStore().$state.pageTagStyle);
</script>
<style scoped lang="less">
@smart-page-tag-operate-width: 40px;
@color-primary: v-bind('token.colorPrimary');
.smart-page-tag-operate {
width: @smart-page-tag-operate-width;
height: @smart-page-tag-operate-width;
background-color: #ffffff;
font-size: 17px;
text-align: center;
vertical-align: middle;
line-height: @smart-page-tag-operate-width;
padding-right: 10px;
cursor: pointer;
color: #606266;
.smart-page-tag-operate-icon {
width: 20px;
height: 20px;
transition: all 1s;
transform-origin: 10px 20px;
}
.smart-page-tag-operate-icon:hover {
width: 20px;
height: 20px;
transform: rotate(360deg);
}
}
.smart-page-tag-operate:hover {
color: @color-primary;
}
.smart-page-tag {
position: relative;
box-sizing: border-box;
display: flex;
align-content: center;
align-items: center;
justify-content: space-between;
min-height: @page-tag-height;
padding-right: 20px;
padding-left: 20px;
user-select: none;
background: #fff;
width: calc(100% - @smart-page-tag-operate-width);
.smart-page-tag-close {
margin-left: 5px;
font-size: 10px;
color: #666666;
}
/** 覆盖 ant design vue的 tabs 样式,变小一点 **/
:deep(.ant-tabs-nav) {
margin: 0;
padding: 0 0 2px 0;
}
:deep(.ant-tabs-nav::before) {
border-bottom: 1px solid #ffffff;
}
:deep(.ant-tabs-small > .ant-tabs-nav .ant-tabs-tab) {
padding: 5px 8px 3px 10px;
border-radius: v-bind(borderRadius);
}
:deep(.ant-tabs-tab-active) {
background-color: #eeeeee;
.smart-page-tag-close {
color: @color-primary;
}
}
:deep(.ant-tabs-nav .ant-tabs-tab:hover) {
background-color: #eeeeee;
.smart-page-tag-close {
color: @color-primary;
}
}
}
</style>

View File

@@ -1,11 +1,11 @@
<!--
* 展开菜单模式
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:40:16
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:40:16
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-layout class="admin-layout" style="min-height: 100%">
@@ -70,7 +70,15 @@
<a-back-top :target="backTopTarget" :visibilityHeight="80" />
</a-layout>
<!-- 右侧帮助文档 help-doc -->
<a-layout-sider v-show="helpDocFlag" theme="light" :width="180" class="help-doc-sider" :trigger="null" style="min-height: 100%">
<a-layout-sider
v-if="helpDocFlag"
v-show="helpDocExpandFlag"
theme="light"
:width="180"
class="help-doc-sider"
:trigger="null"
style="min-height: 100%"
>
<SideHelpDoc />
</a-layout-sider>
</a-layout>
@@ -99,6 +107,8 @@
const pageTagFlag = computed(() => useAppConfigStore().$state.pageTagFlag);
// 是否显示帮助文档
const helpDocFlag = computed(() => useAppConfigStore().$state.helpDocFlag);
// 是否默认展开帮助文档
const helpDocExpandFlag = computed(() => useAppConfigStore().$state.helpDocExpandFlag);
// 是否显示页脚
const footerFlag = computed(() => useAppConfigStore().$state.footerFlag);
// 是否显示水印

View File

@@ -64,7 +64,15 @@
<a-back-top :target="backTopTarget" :visibilityHeight="80" />
</a-layout>
<!-- 右侧帮助文档 help-doc -->
<a-layout-sider v-show="helpDocFlag" theme="light" :width="180" class="help-doc-sider" :trigger="null" style="min-height: 100%">
<a-layout-sider
v-if="helpDocFlag"
v-show="helpDocExpandFlag"
theme="light"
:width="180"
class="help-doc-sider"
:trigger="null"
style="min-height: 100%"
>
<SideHelpDoc />
</a-layout-sider>
</a-layout>
@@ -95,6 +103,8 @@
const pageTagFlag = computed(() => useAppConfigStore().$state.pageTagFlag);
// 是否显示帮助文档
const helpDocFlag = computed(() => useAppConfigStore().$state.helpDocFlag);
// 是否默认展开帮助文档
const helpDocExpandFlag = computed(() => useAppConfigStore().$state.helpDocExpandFlag);
// 是否显示页脚
const footerFlag = computed(() => useAppConfigStore().$state.footerFlag);
// 是否显示水印