v3.9.0【优化】typescript版本;【优化】App端消息;【优化】弹出层z-index;

This commit is contained in:
zhuoda
2024-11-04 20:15:49 +08:00
parent 17a3e1fd86
commit 69fa9088f5
1376 changed files with 10373 additions and 9712 deletions

View File

@@ -0,0 +1,47 @@
<!--
* 定期强制修改密码
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2024-08-06 20:40:16
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" width="620px" :footer="null" :bodyStyle="{ height: '420px' }" title="" :closable="false" :maskClosable="true">
<a-alert style="width: 550px" message="根据《网络安全法》和《数据安全法》要求,需要定期修改密码保障数据安全!" type="warning" show-icon />
<Password @on-success="refresh" />
</a-modal>
</template>
<script setup>
import { computed } from 'vue';
import Password from '/@/views/system/account/components/password/index.vue';
import { useUserStore } from '/@/store/modules/system/user.js';
import { loginApi } from '/@/api/system/login-api.js';
import { smartSentry } from '/@/lib/smart-sentry.js';
import { SmartLoading } from '/@/components/framework/smart-loading/index.js';
/**
* 修改密码弹窗
*/
const visible = computed(() => {
return useUserStore().$state.needUpdatePwdFlag;
});
/**
* 刷新
*/
async function refresh() {
try {
SmartLoading.show();
//获取登录用户信息
const res = await loginApi.getLoginInfo();
//更新用户信息到pinia
useUserStore().setUserLoginInfo(res.data);
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
</script>

View File

@@ -0,0 +1,142 @@
<!--
* 头像
*
* @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" :get-popup-container="getPopupContainer">
<div class="wrapper">
<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="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">
<span>退出登录</span>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<HeaderResetPassword ref="resetPasswordRef" />
</template>
<script setup>
import { computed, ref, onMounted } from 'vue';
import { loginApi } from '/@/api/system/login-api';
import { useUserStore } from '/@/store/modules/system/user';
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'];
//监听退出登录方法
async function onLogout() {
try {
await loginApi.logout();
} catch (e) {
smartSentry.captureError(e);
} finally {
useUserStore().logout();
location.reload();
}
}
// ------------------------ 个人中心 ------------------------
const router = useRouter();
function toAccount(menuId) {
router.push({
path: '/account',
query: { menuId },
});
}
function getPopupContainer() {
return document.body;
}
// ------------------------ 修改密码 ------------------------
const resetPasswordRef = ref();
function showUpdatePwdModal() {
resetPasswordRef.value.showModal();
}
// ------------------------ 以下是 头像和姓名 相关 ------------------------
const avatarName = ref('');
const avatar = computed(() => useUserStore().avatar);
const actualName = computed(() => useUserStore().actualName);
// 更新头像信息
function updateAvatar() {
if (useUserStore().actualName) {
avatarName.value = useUserStore().actualName.substr(0, 1);
const avatar = document.getElementById('smartAdminAvatar');
if (avatar) {
avatar.style.backgroundColor = AVATAR_BACKGROUND_COLOR_ARRAY[hashcode(avatarName.value) % 4];
}
}
}
/**
* 通过计算固定字符串的hash来选择颜色这也每次登录的颜色是相同的
*/
function hashcode(str) {
let hash = 1,
i,
chr;
if (str.length === 0) return hash;
for (i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
onMounted(updateAvatar);
</script>
<style lang="less" scoped>
.wrapper {
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;
.avatar {
vertical-align: middle;
}
.name {
margin-left: 5px;
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<a-modal v-model:open="showFlag" :cancelText="null" :width="800" title="消息内容" :closable="false" :maskClosable="false" :destroyOnClose="true" @ok="showFlag = false">
<a-descriptions bordered :column="2" size="small">
<a-descriptions-item :labelStyle="{ width: '80px' }" :span="1" label="类型"
>{{ $smartEnumPlugin.getDescByValue('MESSAGE_TYPE_ENUM', messageDetail.messageType) }}
</a-descriptions-item>
<a-descriptions-item :labelStyle="{ width: '120px' }" :span="1" label="发送时间">{{ messageDetail.createTime }}</a-descriptions-item>
<a-descriptions-item :labelStyle="{ width: '80px' }" :span="2" label="标题">{{ messageDetail.title }}</a-descriptions-item>
<a-descriptions-item :labelStyle="{ width: '80px' }" :span="2" label="内容">
<pre>{{ messageDetail.content }}</pre>
</a-descriptions-item>
</a-descriptions>
<template #footer>
<a-button type="primary" @click="showFlag = false">关闭</a-button>
</template>
</a-modal>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { messageApi } from '/@/api/support/message-api.js';
const emit = defineEmits(['refresh']);
const messageDetail = reactive({
messageType: '',
title: '',
content: '',
createTime: '',
});
const showFlag = ref(false);
function show(data) {
Object.assign(messageDetail, data);
showFlag.value = true;
read(data);
}
async function read(message) {
if (!message.readFlag) {
await messageApi.updateReadFlag(message.messageId);
emit('refresh');
}
}
defineExpose({ show });
</script>

View File

@@ -0,0 +1,262 @@
<!--
* 消息通知
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:17:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div>
<a-popover v-model:open="show" trigger="click" placement="bottomLeft" :getPopupContainer="getPopupContainer">
<a-button type="text" @click="showMessage" style="padding: 4px 5px">
<a-badge :count="unreadMessageCount + toBeDoneCount">
<div style="width: 26px; height: 26px">
<BellOutlined :style="{ fontSize: '16px' }" />
</div>
</a-badge>
</a-button>
<template #content>
<a-spin :spinning="loading">
<a-tabs class="dropdown-tabs" centered :tabBarStyle="{ textAlign: 'center' }" style="width: 300px; z-index: 1030">
<a-tab-pane key="message">
<template #tab>
未读消息
<a-badge :count="unreadMessageCount" :show-zero="false" :offset="[-5, -15]" />
</template>
<a-list class="tab-pane" size="small">
<a-list-item v-for="item in messageList" :key="item.messageId">
<a-list-item-meta>
<template #title>
<div class="title">
<a-badge status="error" />
<a @click="showMessageDetail(item)">{{ item.title }}</a>
</div>
</template>
<template #description>
<span> {{ timeago(item.createTime) }}</span>
</template>
</a-list-item-meta>
</a-list-item>
<a-list-item v-if="unreadMessageCount > 3">
<a-button type="link" @click="gotoMessage" style="margin: 0 auto"> 查看更多</a-button>
</a-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="to_be_done">
<template #tab>
待办工作
<a-badge :count="toBeDoneCount" :show-zero="false" :offset="[-5, -15]" />
</template>
<a-list class="tab-pane" size="small" :locale="{ emptyText: '暂无待办' }">
<a-list-item v-for="(item, index) in toBeDoneList" :key="index">
<a-list-item-meta>
<template #title>
<a-badge status="error" />
<a-tag v-if="item.starFlag" color="red">重要</a-tag>
<span>{{ item.title }}</span>
</template>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-tab-pane>
</a-tabs>
</a-spin>
</template>
</a-popover>
<MessageDetailModal ref="messageDetailModalRef" @refresh="queryMessage" />
</div>
</template>
<script setup>
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';
import MessageDetailModal from './header-message-detail-modal.vue';
import localKey from '/@/constants/local-storage-key-const';
import { localRead } from '/@/utils/local-util';
const { useToken } = theme;
const { token } = useToken();
function getPopupContainer() {
return document.body;
}
const loading = ref(false);
const show = ref(false);
// 点击按钮打开消息气泡卡片的同时刷新消息
function showMessage() {
show.value = true;
queryMessage();
loadToBeDoneList();
}
function closeMessage() {
show.value = false;
}
// ------------------------- 查询消息 -------------------------
// 未读消息
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;
// 若中途有新消息了 打开列表也能及时更新未读数量
useUserStore().queryUnreadMessageCount();
} catch (e) {
smartSentry.captureError(e);
} finally {
loading.value = false;
}
}
const messageDetailModalRef = ref();
function showMessageDetail(data) {
messageDetailModalRef.value.show(data);
closeMessage();
}
const router = useRouter();
function gotoMessage() {
show.value = false;
router.push({ path: '/account', query: { menuId: 'message' } });
}
// ------------------------- 待办工作 -------------------------
// 待办工作数
const toBeDoneCount = computed(() => {
return useUserStore().toBeDoneCount;
});
// 待办工作列表
const toBeDoneList = ref([]);
const loadToBeDoneList = async () => {
try {
loading.value = true;
let localToBeDoneList = localRead(localKey.TO_BE_DONE);
if (localToBeDoneList) {
toBeDoneList.value = JSON.parse(localToBeDoneList).filter((e) => !e.doneFlag);
}
} catch (err) {
smartSentry.captureError(err);
} finally {
loading.value = false;
}
};
// ------------------------- 时间计算 -------------------------
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" 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;
span {
vertical-align: initial;
}
.notice-badge {
color: inherit;
}
}
.title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.dropdown-tabs {
background-color: @base-bg-color;
border-radius: 4px;
}
.tab-pane {
height: auto;
}
</style>

View File

@@ -0,0 +1,88 @@
<!--
* 修改密码
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:02:01
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" title="修改密码" ok-text="确认" cancel-text="取消" @ok="updatePwd" @cancel="cancelModal">
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }">
<a-form-item label="原密码" name="oldPassword">
<a-input v-model:value.trim="form.oldPassword" type="password" placeholder="请输入原密码" />
</a-form-item>
<a-form-item label="新密码" name="newPassword" :help="tips">
<a-input v-model:value.trim="form.newPassword" type="password" placeholder="请输入新密码" />
</a-form-item>
<a-form-item label="确认密码" name="confirmPwd" :help="tips">
<a-input v-model:value.trim="form.confirmPwd" type="password" placeholder="请输入确认密码" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue';
import { employeeApi } from '/@/api/system/employee-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { smartSentry } from '/@/lib/smart-sentry';
const visible = ref(false);
const formRef = ref();
const tips = '密码必须为长度8-20位且包含大小写字母、数字、特殊符号三种及以上组合'; //校验规则
const reg = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,20}$/;
const rules = {
oldPassword: [{ required: true, message: '请输入原密码' }],
newPassword: [{ type: 'string', pattern: reg, message: '密码格式错误' }],
confirmPwd: [{ type: 'string', pattern: reg, message: '请输入确认密码' }],
};
const formDefault = {
oldPassword: '',
newPassword: '',
};
let form = reactive({
...formDefault,
});
async function updatePwd() {
formRef.value
.validate()
.then(async () => {
if (form.newPassword != form.confirmPwd) {
message.error('新密码与确认密码不一致');
return;
}
SmartLoading.show();
try {
await employeeApi.updateEmployeePassword(form);
message.success('修改成功');
visible.value = false;
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
function showModal() {
visible.value = true;
form.oldPassword = '';
form.newPassword = '';
form.confirmPwd = '';
}
function cancelModal() {
visible.value = false;
}
defineExpose({ showModal });
</script>

View File

@@ -0,0 +1,331 @@
<!--
* 设置模块
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:18:20
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<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 class="color-container">
<template v-for="(item, index) in themeColors" :key="index">
<div v-if="index === formState.colorIndex" class="color">
<CheckSquareFilled :style="{ color: item.primaryColor, fontSize: '22px' }" />
</div>
<div v-else @click="changeColor(index)" class="color">
<svg
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
:fill="item.primaryColor"
xmlns="http://www.w3.org/2000/svg"
width="26"
height="26"
>
<path
d="M128 160.01219c0-17.67619 14.336-32.01219 32.01219-32.01219h704c17.65181 0 31.98781 14.336 31.98781 32.01219v704c0 17.65181-14.336 31.98781-32.01219 31.98781H160.036571a31.98781 31.98781 0 0 1-32.01219-32.01219V160.036571z"
/>
</svg>
</div>
</template>
</div>
</a-form-item>
<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>
<a-radio-button :value="true">紧凑</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item :label="$t('setting.menu.layout')">
<a-radio-group @change="changeLayout" button-style="solid" v-model:value="formState.layout">
<a-radio-button v-for="item in $smartEnumPlugin.getValueDescList('LAYOUT_ENUM')" :key="item.value" :value="item.value">
{{ item.desc }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item :label="$t('setting.menu.theme')">
<a-radio-group v-model:value="formState.sideMenuTheme" button-style="solid" @change="changeMenuTheme">
<a-radio-button value="dark">Dark</a-radio-button>
<a-radio-button value="light">Light</a-radio-button>
</a-radio-group>
</a-form-item>
<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.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>
</div>
</a-drawer>
</template>
<script setup>
import { ref, reactive, h } from 'vue';
import { i18nList } from '/@/i18n/index';
import { useI18n } from 'vue-i18n';
import localStorageKeyConst from '/@/constants/local-storage-key-const';
import { LAYOUT_ENUM } from '/@/constants/layout-const';
import { localRead, localSave } from '/@/utils/local-util';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { Modal } from 'ant-design-vue';
import { appDefaultConfig } from '/@/config/app-config';
import { themeColors } from '/@/theme/color.js';
// ----------------- modal 显示与隐藏 -----------------
const visible = ref(false);
defineExpose({
show,
});
function close() {
visible.value = false;
}
function show() {
visible.value = true;
}
// ----------------- 配置信息操作 -----------------
function copy() {
let content = JSON.stringify(formState, null, 2);
// 创建元素用于复制
const aux = document.createElement('input');
// 设置元素内容
aux.setAttribute('value', content);
// 将元素插入页面进行调用
document.body.appendChild(aux);
// 复制内容
aux.select();
// 将内容复制到剪贴板
document.execCommand('copy');
// 删除创建元素
document.body.removeChild(aux);
Modal.success({
title: '复制成功',
content: h('div', {}, [h('p', '可以直接修改 /@/config/app-config.js 文件保存此配置')]),
});
}
function reset() {
for (const k in appDefaultConfig) {
formState[k] = appDefaultConfig[k];
}
appConfigStore.reset();
}
// ----------------- 表单数据实时保存到localstorage -----------------
const appConfigStore = useAppConfigStore();
useAppConfigStore().$subscribe((mutation, state) => {
localSave(localStorageKeyConst.APP_CONFIG, JSON.stringify(state));
});
// ----------------- 表单 -----------------
let formValue = {
// i18n 语言选择
language: appConfigStore.language,
// 布局: side 或者 side-expand
layout: appConfigStore.layout,
// 页面宽度
pageWidth: appConfigStore.pageWidth,
// 颜色
colorIndex: appConfigStore.colorIndex,
// 侧边菜单宽度
sideMenuWidth: appConfigStore.sideMenuWidth,
// 菜单主题
sideMenuTheme: appConfigStore.sideMenuTheme,
// 页面紧凑
compactFlag: appConfigStore.compactFlag,
// 页面圆角
borderRadius: appConfigStore.borderRadius,
// 标签页
pageTagFlag: appConfigStore.pageTagFlag,
// 标签页 样式
pageTagStyle: appConfigStore.pageTagStyle,
// 面包屑
breadCrumbFlag: appConfigStore.breadCrumbFlag,
// 页脚
footerFlag: appConfigStore.footerFlag,
// 帮助文档
helpDocFlag: appConfigStore.helpDocFlag,
// 帮助文档 默认展开
helpDocExpandFlag: appConfigStore.helpDocExpandFlag,
// 水印
watermarkFlag: appConfigStore.watermarkFlag,
};
let formState = reactive({ ...formValue });
const { locale } = useI18n();
function changeLanguage(languageValue) {
locale.value = languageValue;
appConfigStore.$patch({
language: languageValue,
});
}
function changeLayout(e) {
appConfigStore.$patch({
layout: e.target.value,
});
}
function changeColor(index) {
formState.colorIndex = index;
appConfigStore.$patch({
colorIndex: index,
});
}
function changeSideMenuWidth(value) {
appConfigStore.$patch({
sideMenuWidth: value,
});
}
function changePageWidth(e) {
appConfigStore.$patch({
pageWidth: e.target.value,
});
}
function changeMenuTheme(e) {
appConfigStore.$patch({
sideMenuTheme: e.target.value,
});
}
function changeCompactFlag(e) {
appConfigStore.$patch({
compactFlag: e.target.value,
});
}
function changeBorderRadius(e) {
appConfigStore.$patch({
borderRadius: e,
});
}
function changeBreadCrumbFlag(e) {
appConfigStore.$patch({
breadCrumbFlag: e,
});
}
function changePageTagFlag(e) {
appConfigStore.$patch({
pageTagFlag: e,
});
}
function changePageTagStyle(e) {
appConfigStore.$patch({
pageTagStyle: e.target.value,
});
}
function changeFooterFlag(e) {
appConfigStore.$patch({
footerFlag: e,
});
}
function changeHelpDocFlag(e) {
appConfigStore.$patch({
helpDocFlag: e,
});
}
function changeHelpDocExpandFlag(e) {
appConfigStore.$patch({
helpDocExpandFlag: e,
});
}
function changeWatermarkFlag(e) {
appConfigStore.$patch({
watermarkFlag: e,
});
}
</script>
<style lang="less" scoped>
.footer {
position: absolute;
right: 0;
bottom: 0;
width: 100%;
border-top: 1px solid #e9e9e9;
padding: 10px 16px;
background: #fff;
text-align: left;
z-index: 1;
}
.color-container {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
}
.color {
margin-left: 8px;
height: 26px;
width: 26px;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -0,0 +1,102 @@
<!--
* 头部一整行
*
* @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">
<div class="setting">
<!---消息通知--->
<HeaderMessage ref="headerMessage" />
<!---国际化--->
<!-- <a-button type="text" @click="showSetting" class="operate-icon">
<template #icon><switcher-outlined /></template>
i18n
</a-button> -->
<!---设置--->
<a-button type="text" @click="showSetting" class="operate-icon">
<template #icon><setting-outlined /></template>
</a-button>
</div>
<!---头像信息--->
<div class="user-space-item">
<HeaderAvatar />
</div>
<!---帮助文档--->
<div class="user-space-item" @click="showHelpDoc" v-if="showHelpDocFlag">
<span>帮助文档</span>
<DoubleLeftOutlined v-if="!helpDocExpandFlag" />
</div>
<HeaderSetting ref="headerSetting" />
</a-space>
</template>
<script setup>
import HeaderAvatar from './header-avatar.vue';
import HeaderSetting from './header-setting.vue';
import HeaderMessage from './header-message.vue';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { computed, ref } from 'vue';
import { theme } from 'ant-design-vue';
// 设置
const headerSetting = ref();
function showSetting() {
headerSetting.value.show();
}
//帮助文档
function showHelpDoc() {
useAppConfigStore().showHelpDoc();
}
const showHelpDocFlag = computed(() => {
return useAppConfigStore().helpDocFlag;
});
const helpDocExpandFlag = computed(() => {
return useAppConfigStore().helpDocExpandFlag;
});
const { useToken } = theme;
const { token } = useToken();
</script>
<style lang="less" scoped>
.user-space-item {
height: 100%;
color: inherit;
padding: 0 12px;
cursor: pointer;
align-self: center;
a {
color: inherit;
i {
font-size: 16px;
}
}
}
.user-space-item:hover {
color: v-bind('token.colorPrimary');
background-color: @hover-bg-color !important;
}
.setting {
height: @header-user-height;
line-height: @header-user-height;
vertical-align: middle;
display: flex;
align-items: center;
}
.operate-icon {
margin-left: 20px;
}
</style>

View File

@@ -0,0 +1,40 @@
<!--
* 面包屑
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-breadcrumb separator=">" v-if="breadCrumbFlag" class="breadcrumb">
<a-breadcrumb-item v-for="(item, index) in parentMenuList" :key="index">{{ item.title }}</a-breadcrumb-item>
<a-breadcrumb-item>{{ currentRoute.meta.title }}</a-breadcrumb-item>
</a-breadcrumb>
</template>
<script setup>
import { useRoute } from 'vue-router';
import { useUserStore } from '/@/store/modules/system/user';
import { computed } from 'vue';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
// 是否显示面包屑
const breadCrumbFlag = computed(() => useAppConfigStore().$state.breadCrumbFlag);
let currentRoute = useRoute();
//根据路由监听面包屑
const parentMenuList = computed(() => {
let currentName = currentRoute.name;
if (!currentName || typeof currentName !== 'string') {
return [];
}
let menuParentIdListMap = useUserStore().getMenuParentIdListMap;
return menuParentIdListMap.get(currentName) || [];
});
</script>
<style scoped lang="less">
.breadcrumb{
line-height: @page-tag-height;
}
</style>

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 #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-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 #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-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

@@ -0,0 +1,25 @@
<!--
* 标签页 入口支持三种模式默认a-tabs, chrome-tabs
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2024-06-12 20:55:04
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div id="smartAdminPageTag">
<DefaultTab v-if="pageTagStyle === PAGE_TAG_ENUM.DEFAULT.value" />
<AntdTab v-if="pageTagStyle === PAGE_TAG_ENUM.ANTD.value" />
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import DefaultTab from './components/default-tab.vue';
import AntdTab from './components/antd-tab.vue';
import { PAGE_TAG_ENUM } from '/@/constants/layout-const.js';
const pageTagStyle = computed(() => useAppConfigStore().$state.pageTagStyle);
</script>

View File

@@ -0,0 +1,75 @@
<!--
* 展开菜单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div class="menu-container">
<!-- 第一列一级导航 -->
<TopMenu ref="topMenuRef" class="top-menu" />
<!-- 第二列二级导航 -->
<RecursionMenu ref="recursionMenuRef" class="recursion-menu" />
</div>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import RecursionMenu from './recursion-menu.vue';
import TopMenu from './top-menu.vue';
import { useUserStore } from '/@/store/modules/system/user';
const props = defineProps({
placeholder: {
type: String,
default: '请选择',
},
});
// 选中的顶级菜单
const topMenuRef = ref();
// 二级菜单引用
const recursionMenuRef = ref();
let currentRoute = useRoute();
// 根据路由更新菜单展开和选中状态
function updateSelectKeyAndOpenKey() {
// 第一步,根据路由 更新选中 顶级菜单
let parentList = useUserStore().menuParentIdListMap.get(currentRoute.name) || [];
if (parentList.length === 0) {
topMenuRef.value.updateSelectKey(currentRoute.name);
return;
}
topMenuRef.value.updateSelectKey(parentList[0].name);
//第二步,根据路由 更新 二级菜单的selectKey和openKey
recursionMenuRef.value.updateSelectKeyAndOpenKey(parentList, currentRoute.name);
}
onMounted(updateSelectKeyAndOpenKey);
//监听路由的变化,进行更新菜单展开项目
watch(currentRoute, () => {
updateSelectKeyAndOpenKey();
});
</script>
<style scoped lang="less">
.menu-container {
display: flex;
height: 100%;
.top-menu {
width: 114px;
flex-shrink: 0;
}
.recursion-menu {
min-width: 126px;
flex: 1;
}
}
</style>

View File

@@ -0,0 +1,102 @@
<!--
* 递归菜单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div class="recursion-container" v-show="topMenu.children && topMenu.children.length > 0">
<!-- 顶部顶级菜单名称 -->
<div class="top-menu">
<span class="ant-menu">{{ topMenu.menuName }}</span>
</div>
<!-- 次级菜单展示 -->
<a-menu :selectedKeys="selectedKeys" :openKeys="openKeys" mode="inline">
<template v-for="item in topMenu.children" :key="item.menuId">
<template v-if="item.visibleFlag">
<template v-if="$lodash.isEmpty(item.children)">
<a-menu-item :key="item.menuId.toString()" @click="turnToPage(item)">
<template #icon v-if="item.icon">
<component :is="$antIcons[item.icon]" />
</template>
{{ item.menuName }}
</a-menu-item>
</template>
<template v-else>
<SubMenu :menu-info="item" :key="item.menuId" @turnToPage="turnToPage" />
</template>
</template>
</template>
</a-menu>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import SubMenu from './sub-menu.vue';
import { router } from '/@/router';
import _ from 'lodash';
import menuEmitter from './side-expand-menu-mitt';
import { useUserStore } from '/@/store/modules/system/user';
// 选中的顶级菜单
let topMenu = ref({});
menuEmitter.on('selectTopMenu', onSelectTopMenu);
// 监听选中顶级菜单事件
function onSelectTopMenu(selectedTopMenu) {
topMenu.value = selectedTopMenu;
if (selectedTopMenu.children && selectedTopMenu.children.length > 0) {
openKeys.value = _.map(selectedTopMenu.children, 'menuId').map((e) => e.toString());
} else {
openKeys.value = [];
}
selectedKeys.value = [];
}
//展开的菜单
const selectedKeys = ref([]);
const openKeys = ref([]);
function updateSelectKeyAndOpenKey(parentList, currentSelectKey) {
if (!parentList) {
return;
}
//获取需要展开的menu key集合
openKeys.value = _.map(parentList, 'name');
selectedKeys.value = [currentSelectKey];
}
// 页面跳转
function turnToPage(route) {
useUserStore().deleteKeepAliveIncludes(route.menuId.toString());
router.push({ name: route.menuId.toString() });
}
function goHome() {
router.push({ name: HOME_PAGE_NAME });
}
defineExpose({ updateSelectKeyAndOpenKey });
</script>
<style scoped lang="less">
.recursion-container {
height: 100%;
background: #ffffff;
}
.top-menu {
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
height: @header-user-height;
font-size: 16px;
color: #515a6e;
border-bottom: 1px solid #f3f3f3;
border-right: 1px solid #f3f3f3;
}
</style>

View File

@@ -0,0 +1,11 @@
/*
* 展开菜单 event bus
*
* @Author: 1024创新实验室-主任:卓大
* @Date: 2022-07-12 23:32:48
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*/
import mitt from 'mitt';
export default mitt();

View File

@@ -0,0 +1,46 @@
<!--
* 第二列菜单区域
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-sub-menu :key="props.menuInfo.menuId.toString()">
<template #icon>
<component :is="$antIcons[props.menuInfo.icon]" />
</template>
<template #title>{{ props.menuInfo.menuName }}</template>
<template v-for="item in props.menuInfo.children" :key="item.menuId">
<template v-if="item.visibleFlag">
<template v-if="!item.children">
<a-menu-item :key="item.menuId.toString()" @click="turnToPage(item)">
<template #icon>
<component :is="$antIcons[item.icon]" />
</template>
{{ item.menuName }}
</a-menu-item>
</template>
<template v-else>
<SubMenu :menu-info="item" :key="item.menuId" @turnToPage="turnToPage" />
</template>
</template>
</template>
</a-sub-menu>
</template>
<script setup>
let props = defineProps({
menuInfo: Object,
});
const emits = defineEmits(['turnToPage']);
const turnToPage = (route) => {
emits('turnToPage', route);
};
</script>
<style scoped lang="less">
::v-deep(.ant-menu-item-selected) {
border-right: 3px !important;
}
</style>

View File

@@ -0,0 +1,110 @@
<!--
* 第一列菜单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div class="top-menu-container">
<!-- 顶部logo区域 -->
<div class="logo" @click="onGoHome">
<img class="logo-img" :src="logoImg" />
<div class="title smart-logo">{{ websiteName }}</div>
</div>
<!-- 一级菜单展示 -->
<a-menu :selectedKeys="selectedKeys" mode="inline" :theme="theme">
<template v-for="item in menuTree" :key="item.menuId">
<template v-if="item.visibleFlag">
<a-menu-item :key="item.menuId.toString()" @click="onSelectMenu(item)">
<template #icon>
<component :is="$antIcons[item.icon]" />
</template>
{{ menuNameAdapter(item.menuName) }}
</a-menu-item>
</template>
</template>
</a-menu>
</div>
</template>
<script setup>
import _ from 'lodash';
import { computed, ref } from 'vue';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { MENU_TYPE_ENUM } from '/@/constants/system/menu-const';
import { router } from '/@/router';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { useUserStore } from '/@/store/modules/system/user';
import logoImg from '/@/assets/images/logo/smart-admin-logo.png';
import menuEmitter from './side-expand-menu-mitt';
const websiteName = computed(() => useAppConfigStore().websiteName);
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
const menuTree = computed(() => useUserStore().getMenuTree || []);
// 展开菜单的顶级目录名字适配,只展示两个字为好
function menuNameAdapter(name) {
return name.substr(0, 2);
}
// 选中的顶级菜单
const selectedKeys = ref([]);
// 选中菜单,页面跳转
function onSelectMenu(menuItem) {
selectedKeys.value = [menuItem.menuId.toString()];
if (menuItem.menuType === MENU_TYPE_ENUM.MENU.value && (_.isEmpty(menuItem.children) || menuItem.children.every((e) => !e.visibleFlag))) {
useUserStore().deleteKeepAliveIncludes(menuItem.menuId.toString());
router.push({ name: menuItem.menuId.toString() });
}
menuEmitter.emit('selectTopMenu', menuItem);
}
// 更新选中的菜单
function updateSelectKey(key) {
selectedKeys.value = [key];
let selectMenu = _.find(menuTree.value, { menuId: Number(key) });
if (selectMenu) {
menuEmitter.emit('selectTopMenu', selectMenu);
}
}
//点击logo回到首页
function onGoHome() {
router.push({ name: HOME_PAGE_NAME });
}
defineExpose({ updateSelectKey });
</script>
<style scoped lang="less">
.top-menu-container {
height: 100%;
}
.logo {
height: @header-user-height;
line-height: @header-user-height;
padding: 0px 15px 0px 15px;
width: 100%;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
.logo-img {
width: 30px;
height: 30px;
}
.title {
font-size: 16px;
font-weight: 600;
overflow: hidden;
word-wrap: break-word;
white-space: nowrap;
color: v-bind('theme === "light" ? "#001529": "#ffffff"');
}
}
</style>

View File

@@ -0,0 +1,102 @@
<!--
* 客服人员弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:40:16
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" width="600px" title="联系客服人员" :closable="false" :maskClosable="true">
<a-row><div style="margin-left: 180px;font-weight:bolder">客服(卓大)电话18637925892 ;</div> </a-row>
<br />
<div class="app-qr-box">
<div class="app-qr">
<img :src="zhuoda" />
<span class="qr-desc strong"> 卓大的微信号 </span>
<span class="qr-desc"> 骚扰卓大 :) </span>
</div>
<div class="app-qr">
<img :src="xiaozhen" />
<span class="qr-desc strong"> 六边形工程师 </span>
<span class="qr-desc"> 赚钱代码生活 </span>
</div>
<div class="app-qr">
<img :src="lab1024" />
<span class="qr-desc strong"> 1024创新实验室 </span>
<span class="qr-desc"> 官方账号 </span>
</div>
</div>
<template #footer>
<a-button type="primary" @click="hide">知道了</a-button>
</template>
</a-modal>
</template>
<script setup>
import { ref, reactive, nextTick, computed } from 'vue';
import zhuoda from '/@/assets/images/1024lab/zhuoda-wechat.jpg';
import lab1024 from '/@/assets/images/1024lab/1024lab-gzh.jpg';
import xiaozhen from '/@/assets/images/1024lab/gzh.jpg';
defineExpose({
show,
});
const visible = ref(false);
function show() {
visible.value = true;
}
function hide() {
visible.value = false;
}
</script>
<style lang="less" scoped>
.app-qr-box {
display: flex;
height: 170px;
align-items: center;
justify-content: space-around;
.app-qr {
display: flex;
align-items: center;
width: 33%;
justify-content: center;
flex-direction: column;
> img {
width: 100%;
max-width: 150px;
height: 100%;
max-height: 150px;
}
.strong {
font-weight: 600;
}
.qr-desc {
display: flex;
align-items: center;
font-size: 12px;
text-align: center;
overflow-x: hidden;
> img {
width: 15px;
height: 18px;
margin-right: 9px;
}
}
}
}
.ant-carousel :deep(.slick-slide) {
text-align: center;
height: 120px;
line-height: 120px;
width: 120px;
background: #364d79;
overflow: hidden;
}
.ant-carousel :deep(.slick-slide h3) {
color: #fff;
}
</style>

View File

@@ -0,0 +1,85 @@
<!--
* 意见反馈提交弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:40:16
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" title="意见反馈" :closable="false" :maskClosable="true" >
<a-form :labelCol="{ span: 6 }">
<a-form-item label="我要吐槽/建议:">
<a-textarea v-model:value="form.feedbackContent" placeholder="请输入让您不满意的点,我们争取做到更好~" :rows="3"/>
</a-form-item>
<a-form-item label="反馈图片:">
<Upload
accept=".jpg,.jpeg,.png,.gif"
:maxUploadSize="3"
buttonText="点击上传反馈图片"
:default-file-list="form.feedbackAttachment || []"
listType="picture-card"
@change="changeAttachment"
:folder="FILE_FOLDER_TYPE_ENUM.FEEDBACK.value"
/>
</a-form-item>
</a-form>
<template #footer>
<a-button @click="hide">取消</a-button>
<a-button type="primary" @click="submit">提交</a-button>
</template>
</a-modal>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { feedbackApi } from '/@/api/support/feedback-api';
import { message } from 'ant-design-vue';
import { FILE_FOLDER_TYPE_ENUM } from '/@/constants/support/file-const';
import Upload from '/@/components/support/file-upload/index.vue';
import { smartSentry } from '/@/lib/smart-sentry';
defineExpose({
show,
});
const visible = ref(false);
function show () {
Object.assign(form, formDefault);
console.log(form)
visible.value = true;
}
function hide () {
visible.value = false;
}
const formDefault = {
feedbackContent:'',
feedbackAttachment: ''
}
const form = reactive({ ...formDefault });
async function submit () {
try {
SmartLoading.show();
if(!form.feedbackContent){
message.warn('请填写具体内容');
return;
}
await feedbackApi.addFeedback(form);
message.success('提交成功');
hide();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
function changeAttachment (fileList) {
form.feedbackAttachment = fileList;
}
</script>

View File

@@ -0,0 +1,234 @@
<!--
* 帮助文档
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:40:16
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div class="help-doc-wrapper">
<!-----头部---->
<div class="help-doc-header">
<strong>帮助文档</strong>
<strong class="help-doc-close" @click="hideHelpDoc"><close-outlined /></strong>
</div>
<!-----联系客服区域---->
<div class="help-doc-contact" @click="contactModal.show">
<div class="help-doc-contact-left">
<phone-outlined style="font-size: 23px; line-height: 50px; margin-top: 5px" />
</div>
<div class="help-doc-contact-right">
<a>联系客服</a>
<div class="help-doc-contac-time">9:00-17:00 5x7小时</div>
</div>
</div>
<a-divider />
<!-----意见反馈---->
<div class="feedback">
<div>反馈让您不满意的点我们争取做到更好<smile-outlined style="margin-left: 5px" /></div>
<div class="feedback-message-list">
<div v-for="item in feedbackMessageList" :key="item.feedbackId" class="feedback-message">{{ item.feedbackContent }}</div>
</div>
<a @click="feedbackModal.show">我也要反馈</a>
</div>
<a-divider />
<!-----文档列表---->
<div class="help-doc-list">
<div class="help-doc-item-all">
<router-link tag="a" target="_blank" :to="{ path: '/help-doc/detail' }">系统帮助文档 >></router-link>
</div>
<div class="help-doc-item" v-for="item in helpDocList" :key="item.helpDocId">
<router-link tag="a" target="_blank" :to="{ path: '/help-doc/detail', query: { helpDocId: item.helpDocId } }">{{ item.title }}</router-link>
</div>
</div>
<!-----联系客服---->
<ContactModal ref="contactModal" />
<!----- 提交意见反馈 ---->
<FeedbackModal ref="feedbackModal" />
</div>
</template>
<script setup>
import { onMounted, ref, watch, reactive } from 'vue';
import { useRoute } from 'vue-router';
import _ from 'lodash';
import { helpDocApi } from '/@/api/support/help-doc-api';
import ContactModal from './components/contact-modal.vue';
import FeedbackModal from './components/feedback-modal.vue';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { feedbackApi } from '/@/api/support/feedback-api';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { smartSentry } from '/@/lib/smart-sentry';
function hideHelpDoc() {
useAppConfigStore().hideHelpDoc();
}
// ------------------ 联系客服 --------------------------
const contactModal = ref();
// ------------------ 意见反馈 --------------------------
let feedbackMessageList = ref([]);
onMounted(() => {
// 首先查询多一些意见反馈
queryFeedbackList();
// 更换显示
scheduleShowFeedback();
});
let scheduleShowInterval = null;
let scheduleShowIndex = 0;
function scheduleShowFeedback() {
if (scheduleShowInterval != null) {
return;
}
scheduleShowInterval = setInterval(() => {
if (feedbackList.length === 0) {
return;
}
// 显示两条意见反馈
for (let i = 0; i < 2; i++) {
if (scheduleShowIndex >= feedbackList.length) {
scheduleShowIndex = 0;
}
feedbackMessageList.value[i] = feedbackList[scheduleShowIndex];
scheduleShowIndex++;
}
}, 3000);
}
// 总页数
let pages = 1;
let currentPage = 1;
let feedbackList = [];
// 查询意见反馈列表
async function queryFeedbackList() {
try {
let param = {
pageNum: 1,
pageSize: 20,
};
let result = await feedbackApi.queryFeedback(param);
feedbackList = result.data.list;
pages = Math.ceil(feedbackList.length / 2);
} catch (e) {
smartSentry.captureError(e);
}
}
const feedbackModal = ref();
// ----------------- 帮助文档列表 -------------------
let currentRoute = useRoute();
let helpDocList = ref([]);
// 获取关联的文档集合
async function queryHelpDocList(menuId) {
let res = await helpDocApi.queryHelpDocByRelationId(menuId);
helpDocList.value = res.data;
}
watch(
currentRoute,
() => {
//SmartAdmin中 router的name 就是 后端存储menu的id
let menuId = -1;
try {
if (currentRoute.name === HOME_PAGE_NAME) {
menuId = 0;
} else {
menuId = _.toNumber(currentRoute.name);
}
} catch (e) {
smartSentry.captureError(e);
}
if (menuId > -1) {
queryHelpDocList(menuId);
}
},
{
immediate: true,
}
);
</script>
<style scoped lang="less">
.help-doc-wrapper {
border-left: 1px solid #ededed;
height: 100vh;
padding: 0 10px;
.help-doc-header {
line-height: @header-user-height;
display: flex;
justify-content: space-between;
height: @header-user-height;
border-bottom: 1px solid #f6f6f6;
.help-doc-close {
cursor: pointer;
}
}
.help-doc-contact {
height: 50px;
display: flex;
cursor: pointer;
margin-top: 5px;
justify-content: space-between;
.help-doc-contact-left {
width: 30px;
margin-top: 10px;
}
.help-doc-contact-right {
margin-top: 10px;
width: calc(100% - 40px);
.help-doc-contac-time {
color: #888;
font-size: 12px;
margin-top: 10px;
}
}
}
.feedback {
.feedback-message-list {
margin: 12px 0px;
height: 70px;
position: relative;
overflow: hidden;
.feedback-message {
margin: 10px 2px;
color: #a9a9a9;
font-size: 12px;
}
}
}
.help-doc-list {
.help-doc-item-all {
margin-top: 10px;
color: @primary-color;
}
.help-doc-item {
margin-top: 20px;
a {
color: #444;
}
a:hover {
color: @primary-color;
text-decoration: underline;
}
}
}
}
</style>

View File

@@ -0,0 +1,133 @@
<!--
* 传统菜单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<!--左侧菜单分为两部分1顶部logo区域包含 logo和名称;2下方菜单区域-->
<!-- 1顶部logo区域 -->
<div class="logo" @click="onGoHome" :style="sideMenuWidth" v-if="!collapsed">
<img class="logo-img" :src="logoImg" />
<div class="title smart-logo title-light" v-if="sideMenuTheme === 'light'">{{ websiteName }}</div>
<div class="title smart-logo title-dark" v-if="sideMenuTheme === 'dark'">{{ websiteName }}</div>
</div>
<div class="min-logo" @click="onGoHome" v-if="collapsed">
<img class="logo-img" :src="logoImg" />
</div>
<!-- 2下方菜单区域 这里使用一个递归菜单解决 -->
<div class="menu">
<RecursionMenu :collapsed="collapsed" ref="menuRef" />
</div>
</template>
<script setup>
import { computed, nextTick, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import RecursionMenu from './recursion-menu.vue';
import logoImg from '/@/assets/images/logo/smart-admin-logo.png';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
const websiteName = computed(() => useAppConfigStore().websiteName);
const sideMenuWidth = computed(() => 'width:' + useAppConfigStore().sideMenuWidth + 'px');
const sideMenuTheme = computed(() => useAppConfigStore().sideMenuTheme);
const props = defineProps({
collapsed: {
type: Boolean,
required: false,
default: false,
},
});
const menuRef = ref();
watch(
() => props.collapsed,
(newValue, oldValue) => {
// 如果是展开菜单的话,重新获取更新菜单的展开项: openkeys和selectKeys
if (!newValue) {
nextTick(() => menuRef.value.updateOpenKeysAndSelectKeys());
}
}
);
const router = useRouter();
function onGoHome() {
router.push({ name: HOME_PAGE_NAME });
}
const color = computed(() => {
let isLight = useAppConfigStore().$state.sideMenuTheme === 'light';
return {
background: isLight ? '#FFFFFF' : '#001529',
};
});
</script>
<style lang="less" scoped>
.shadow {
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
}
.side-menu {
min-height: 100vh;
overflow-y: auto;
z-index: 10;
.min-logo {
height: @header-user-height;
line-height: @header-user-height;
padding: 0px 15px 0px 15px;
background-color: v-bind('color.background');
position: fixed;
width: 80px;
z-index: 21;
display: flex;
justify-content: center;
align-items: center;
.logo-img {
width: 30px;
height: 30px;
}
}
.logo {
height: @header-user-height;
line-height: @header-user-height;
background-color: v-bind('color.background');
padding: 0px 15px 0px 15px;
position: fixed;
z-index: 21;
display: flex;
cursor: pointer;
justify-content: center;
align-items: center;
.logo-img {
width: 30px;
height: 30px;
}
.title {
font-size: 16px;
font-weight: 600;
margin-left: 8px;
}
.title-light {
color: #001529;
}
.title-dark {
color: #ffffff;
}
}
}
.menu {
margin-top: @header-user-height;
}
</style>

View File

@@ -0,0 +1,109 @@
<!--
* 传统菜单-递归菜单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-menu
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
class="smart-menu"
mode="inline"
:theme="theme"
:inlineCollapsed="collapsed"
>
<template v-for="item in menuTree" :key="item.menuId">
<template v-if="item.visibleFlag && !item.disabledFlag">
<template v-if="$lodash.isEmpty(item.children)">
<a-menu-item :key="item.menuId" @click="turnToPage(item)">
<template #icon>
<component :is="$antIcons[item.icon]" />
</template>
{{ item.menuName }}
</a-menu-item>
</template>
<template v-else>
<SubMenu :menu-info="item" :key="item.menuId" @turnToPage="turnToPage" />
</template>
</template>
</template>
</a-menu>
</template>
<script setup>
import _ from 'lodash';
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import SubMenu from './sub-menu.vue';
import { router } from '/@/router/index';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { useUserStore } from '/@/store/modules/system/user';
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
const props = defineProps({
collapsed: {
type: Boolean,
default: false,
},
});
const menuTree = computed(() => useUserStore().getMenuTree || []);
//展开的菜单
let currentRoute = useRoute();
const selectedKeys = ref([]);
const openKeys = ref([]);
// 页面跳转
function turnToPage(menu) {
useUserStore().deleteKeepAliveIncludes(menu.menuId.toString());
router.push({ path: menu.path });
}
/**
* SmartAdmin中 router的name 就是 后端存储menu的id
* 所以此处可以直接监听路由,根据路由更新菜单的选中和展开
*/
function updateOpenKeysAndSelectKeys() {
// 更新选中
selectedKeys.value = [_.toNumber(currentRoute.name)];
/**
* 更新展开1、获取新展开的menu key集合2、保留原有的openkeys然后把新展开的与之合并
*/
//获取需要展开的menu key集合
let menuParentIdListMap = useUserStore().getMenuParentIdListMap;
let parentList = menuParentIdListMap.get(currentRoute.name) || [];
// 如果是折叠菜单的话则不需要设置openkey
if(!props.collapsed){
// 使用lodash的union函数进行 去重合并两个数组
let needOpenKeys = _.map(parentList, 'name').map(Number);
openKeys.value = _.union(openKeys.value, needOpenKeys);
}
}
watch(
currentRoute,
() => {
updateOpenKeysAndSelectKeys();
},
{
immediate: true,
}
);
defineExpose({
updateOpenKeysAndSelectKeys,
});
</script>
<style lang="less" scoped>
.smart-menu {
position: relative;
}
</style>

View File

@@ -0,0 +1,45 @@
<!--
* 传统菜单-递归菜单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-sub-menu :key="menuInfo.menuId">
<template #icon>
<component :is="$antIcons[menuInfo.icon]" />
</template>
<template #title>{{ menuInfo.menuName }}</template>
<template v-for="item in menuInfo.children" :key="item.menuId">
<template v-if="item.visibleFlag && !item.disabledFlag">
<template v-if="!item.children">
<a-menu-item :key="item.menuId" @click="turnToPage(item)">
<template #icon>
<component :is="$antIcons[item.icon]" />
</template>
{{ item.menuName }}
</a-menu-item>
</template>
<template v-else>
<SubMenu :menu-info="item" :key="item.menuId" @turnToPage="turnToPage" />
</template>
</template>
</template>
</a-sub-menu>
</template>
<script setup>
const props = defineProps({
menuInfo: {
type: Object,
default: () => ({}),
},
});
const emits = defineEmits(['turnToPage']);
const turnToPage = (menu) => {
emits('turnToPage', menu);
};
</script>

View File

@@ -0,0 +1,32 @@
<!--
* 底部版权公司等信息
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div class="version">
<a target="_blank" class="smart-copyright" href="https://www.1024lab.net"> ©2012-{{ currentYear }} SmartAdmin | 1024创新实验室 </a>
</div>
</template>
<script setup>
import dayjs from 'dayjs';
const currentYear = dayjs().year();
</script>
<style lang="less" scoped>
.version {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
a {
color: rgba(0, 0, 0, 0.45);
}
a:hover {
color: @primary-color;
}
}
</style>

View File

@@ -0,0 +1,39 @@
/*
* keep-alive
*
* @Author: 1024创新实验室-主任:卓大
* @Date: 2022-09-06 20:39:54
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*/
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useUserStore } from '/@/store/modules/system/user';
export function smartKeepAlive() {
const route = useRoute();
const router = useRouter();
// 需要keep-alive的页面
const keepAliveIncludes = computed(() => {
return useUserStore().keepAliveIncludes || [];
});
// ----------------------- iframe相关 -----------------------
// 当前路由是否为不需要缓存的iframe页面
const iframeNotKeepAlivePageFlag = computed(() => route.meta.frameFlag && !route.meta.keepAlive);
// 打开中的tagNav
const tagNav = computed(() => useUserStore().getTagNav || []);
// 已打开的iframe列表
const keepAliveIframePages = computed(() => {
let routes = router.getRoutes();
return routes.filter((e) => e.meta.frameFlag && e.meta.keepAlive && tagNav.value.some((t) => t.menuName == e.name));
});
return {
route,
keepAliveIncludes,
iframeNotKeepAlivePageFlag,
keepAliveIframePages,
};
}

View File

@@ -0,0 +1,173 @@
<!--
* 顶部菜单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<!--总共3部分1logo区域包含 logo和名称; 2菜单区域 ;3用户操作区域-->
<div class="header-main">
<!-- 1logo区域 -->
<div class="logo" @click="onGoHome">
<img class="logo-img" :src="logoImg" />
<div class="title smart-logo title-light" v-if="sideMenuTheme === 'light'">{{ websiteName }}</div>
<div class="title smart-logo title-dark" v-if="sideMenuTheme === 'dark'">{{ websiteName }}</div>
</div>
<!-- 2菜单区域 -->
<RecursionMenu ref="menuRef" />
<!-- 3用户操作区域 -->
<div class="user-space">
<div class="setting">
<!---消息通知--->
<HeaderMessage ref="headerMessage" />
<!---设置--->
<a-button type="text" @click="showSetting" class="operate-icon">
<template #icon><setting-outlined /></template>
</a-button>
</div>
<!---头像信息--->
<div class="user-space-item">
<HeaderAvatar />
</div>
<HeaderSetting ref="headerSetting" />
</div>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import RecursionMenu from './recursion-menu.vue';
import logoImg from '/@/assets/images/logo/smart-admin-logo.png';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import HeaderAvatar from '../header-user-space/header-avatar.vue';
import HeaderSetting from '../header-user-space/header-setting.vue';
import HeaderMessage from '../header-user-space/header-message.vue';
// 设置
const headerSetting = ref();
function showSetting() {
headerSetting.value.show();
}
//消息通知
const headerMessage = ref();
function showMessage() {
headerMessage.value.showMessage();
}
const websiteName = computed(() => useAppConfigStore().websiteName);
const sideMenuTheme = computed(() => useAppConfigStore().sideMenuTheme);
const props = defineProps({
collapsed: {
type: Boolean,
required: false,
default: false,
},
});
const menuRef = ref();
watch(
() => props.collapsed,
(newValue, oldValue) => {
// 如果是展开菜单的话,重新获取更新菜单的展开项: openkeys和selectKeys
if (!newValue) {
menuRef.value.updateSelectKeys();
}
}
);
const color = computed(() => {
let isLight = useAppConfigStore().$state.sideMenuTheme === 'light';
return {
color: isLight ? '#001529' : '#FFFFFF',
background: isLight ? '#FFFFFF' : '#001529',
};
});
const router = useRouter();
function onGoHome() {
router.push({ name: HOME_PAGE_NAME });
}
</script>
<style lang="less" scoped>
.header-main {
display: flex;
flex-direction: row;
justify-content: flex-start;
width: 100%;
padding-left: 16px;
height: 48px;
z-index: 21;
border-bottom: 1px solid rgb(238, 238, 238);
.logo {
min-width: 192px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
.logo-img {
display: inline-block;
height: 30px;
vertical-align: middle;
}
.title {
font-size: 16px;
font-weight: 600;
margin-left: 8px;
}
.title-light {
color: #001529;
}
.title-dark {
color: #ffffff;
}
}
.user-space {
min-width: 208px;
margin-left: auto;
padding-right: 10px;
color: v-bind('color.color');
display: flex;
flex-direction: row;
vertical-align: middle;
align-items: center;
justify-content: flex-end;
.setting {
height: @header-user-height;
line-height: @header-user-height;
vertical-align: middle;
display: flex;
align-items: center;
:deep(.ant-badge) {
color: v-bind('color.color');
}
}
.operate-icon {
margin-left: 20px;
color: v-bind('color.color');
}
.user-space-item {
margin-left: 10px;
}
}
}
:deep(.ant-menu-horizontal) {
border-bottom: 0;
}
</style>

View File

@@ -0,0 +1,88 @@
<!--
* 顶部菜单-递归菜单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-menu
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
class="smart-menu"
mode="horizontal"
:theme="theme"
>
<template v-for="item in menuTree" :key="item.menuId">
<template v-if="item.visibleFlag && !item.disabledFlag">
<template v-if="$lodash.isEmpty(item.children)">
<a-menu-item :key="item.menuId" @click="turnToPage(item)">
<template #icon>
<component :is="$antIcons[item.icon]" />
</template>
{{ item.menuName }}
</a-menu-item>
</template>
<template v-else>
<SubMenu :menu-info="item" :key="item.menuId" @turnToPage="turnToPage" />
</template>
</template>
</template>
</a-menu>
</template>
<script setup>
import _ from 'lodash';
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import SubMenu from './sub-menu.vue';
import { router } from '/@/router/index';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { useUserStore } from '/@/store/modules/system/user';
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
const menuTree = computed(() => useUserStore().getMenuTree || []);
//展开的菜单
let currentRoute = useRoute();
const selectedKeys = ref([]);
const openKeys = ref([]);
// 页面跳转
function turnToPage(menu) {
useUserStore().deleteKeepAliveIncludes(menu.menuId.toString());
router.push({ path: menu.path });
}
/**
* SmartAdmin中 router的name 就是 后端存储menu的id
* 所以此处可以直接监听路由,根据路由更新菜单的选中和展开
*/
function updateSelectKeys() {
// 更新选中
selectedKeys.value = [_.toNumber(currentRoute.name)];
}
watch(
currentRoute,
() => {
updateSelectKeys();
},
{
immediate: true,
}
);
defineExpose({
updateSelectKeys,
});
</script>
<style lang="less" scoped>
.smart-menu {
position: relative;
}
</style>

View File

@@ -0,0 +1,45 @@
<!--
* 顶部菜单-递归菜单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:29:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-sub-menu :key="menuInfo.menuId">
<template #icon>
<component :is="$antIcons[menuInfo.icon]" />
</template>
<template #title>{{ menuInfo.menuName }}</template>
<template v-for="item in menuInfo.children" :key="item.menuId">
<template v-if="item.visibleFlag && !item.disabledFlag">
<template v-if="!item.children">
<a-menu-item :key="item.menuId" @click="turnToPage(item)">
<template #icon>
<component :is="$antIcons[item.icon]" />
</template>
{{ item.menuName }}
</a-menu-item>
</template>
<template v-else>
<SubMenu :menu-info="item" :key="item.menuId" @turnToPage="turnToPage" />
</template>
</template>
</template>
</a-sub-menu>
</template>
<script setup>
const props = defineProps({
menuInfo: {
type: Object,
default: () => ({}),
},
});
const emits = defineEmits(['turnToPage']);
const turnToPage = (menu) => {
emits('turnToPage', menu);
};
</script>

View File

@@ -0,0 +1,304 @@
<!--
* 帮助文档 layout
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:40:16
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<!--
中间内容一共三部分
1顶部
2中间内容区域
3底部一般是公司版权信息
-->
<a-layout class="help-doc-layout" id="smartAdminMain">
<!-- 顶部头部信息 -->
<a-layout-header class="layout-header">
<a-row class="layout-header-title">
<img class="logo-img" :src="logoImg" />
<div class="title">{{ websiteName }}</div>
<div class="title">帮助文档</div>
<a-col class="avatar">
<HeaderAvatar />
</a-col>
</a-row>
</a-layout-header>
<a-layout :style="`height: ${windowHeight}px`">
<!-- 侧边目录 side-menu -->
<a-layout-sider class="side-menu" :style="`height: ${windowHeight}px`" :collapsed="false" theme="light" :width="300">
<div class="help-doc-tree">
<!-- 目录内容 -->
<div>
<a-directory-tree
v-model:expandedKeys="expandedKeys"
v-model:selectedKeys="selectedKeys"
:tree-data="helpDocTreeData"
@select="selectHelpDoc"
/>
</div>
</div>
</a-layout-sider>
<!--中间内容-->
<a-layout-content id="smartAdminLayoutContent" class="help-doc-layout-content">
<router-view v-slot="{ Component }">
<div :key="route.fullPath">
<component :is="Component" />
</div>
</router-view>
<!-- footer 版权公司信息 -->
<a-layout-footer class="layout-footer">
<SmartFooter />
</a-layout-footer>
</a-layout-content>
</a-layout>
<a-back-top :target="backTopTarget" :visibilityHeight="80" />
</a-layout>
</template>
<script setup>
import _ from 'lodash';
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { smartSentry } from '../lib/smart-sentry';
import { useAppConfigStore } from '../store/modules/system/app-config';
import SmartFooter from './components/smart-footer/index.vue';
import { helpDocApi } from '/@/api/support/help-doc-api';
import { helpDocCatalogApi } from '/@/api/support/help-doc-catalog-api';
import logoImg from '/@/assets/images/logo/smart-admin-logo-white.png';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import watermark from '../lib/smart-watermark';
import { useUserStore } from '/@/store/modules/system/user';
import HeaderAvatar from './components/header-user-space/header-avatar.vue';
const websiteName = computed(() => useAppConfigStore().websiteName);
const windowHeight = window.innerHeight;
onMounted(() => {
watermark.set(LAYOUT_ELEMENT_IDS.content, useUserStore().actualName);
});
const backTopTarget = () => {
return document.getElementById(LAYOUT_ELEMENT_IDS.main);
};
const router = useRouter();
const route = useRoute();
function goHome() {
router.push({ name: HOME_PAGE_NAME });
}
// ----------------------- 选中 节点 -----------------------------
const expandedKeys = ref([]);
const selectedKeys = ref([]);
function selectHelpDoc(selectedKeys) {
let key = selectedKeys[0];
if (key.indexOf(TYPE_CATALOG_PREFIX) > -1) {
return;
}
let helpDocId = key.substr(TYPE_HELP_DOC_PREFIX.length);
router.push({ path: '/help-doc/detail', query: { helpDocId } });
}
// 更新展开节点
function updateExpandedKeys(helpDocId, helpDocList, catalogList) {
expandedKeys.value = [TYPE_HELP_DOC_PREFIX + helpDocId];
selectedKeys.value = [TYPE_HELP_DOC_PREFIX + helpDocId];
let helpDoc = helpDocList.filter((e) => e.helpDocId === helpDocId);
let catalogId = null;
if (helpDoc.length > 0) {
catalogId = helpDoc[0].helpDocCatalogId;
}
if (catalogId) {
expandedKeys.value.push(TYPE_CATALOG_PREFIX + catalogId);
}
let parentId = catalogId;
while (parentId !== 0) {
let catalog = catalogList.filter((e) => e.helpDocCatalogId === parentId);
if (catalog.length > 0) {
parentId = catalog[0].parentId;
expandedKeys.value.push(TYPE_CATALOG_PREFIX + catalog[0].helpDocCatalogId);
} else {
parentId = 0;
}
}
}
// ----------------------- 帮助文档 目录 树 -----------------------------
onMounted(queryHelpDocTree);
const helpDocTreeData = ref([]);
const TYPE_CATALOG_PREFIX = 'catalog_';
const TYPE_HELP_DOC_PREFIX = 'help_doc_';
//目录默认id为0
const HELP_DOC_CATALOG_PARENT_ID = 0;
//查询帮助文档树形结构
async function queryHelpDocTree() {
SmartLoading.show();
try {
let { data: catalogList } = await helpDocCatalogApi.getAll();
let { data: helpDocList } = await helpDocApi.getAllHelpDocList();
//设置前缀
for (const item of catalogList) {
item.key = TYPE_CATALOG_PREFIX + item.helpDocCatalogId;
item.title = item.name;
}
//转为map供递归使用
let helpDocMap = new Map();
for (const item of helpDocList) {
item.key = TYPE_HELP_DOC_PREFIX + item.helpDocId;
let list = helpDocMap.get(item.helpDocCatalogId);
if (!list) {
list = [];
helpDocMap.set(item.helpDocCatalogId, list);
}
list.push(item);
}
helpDocTreeData.value = buildHelpDocCatalogTree(catalogList, 0, helpDocMap);
if (!route.query.helpDocId && firstHelpDocId) {
selectHelpDoc([TYPE_HELP_DOC_PREFIX + firstHelpDocId]);
return;
}
//更新展开节点
updateExpandedKeys(route.query.helpDocId, helpDocList, catalogList);
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
// 记录第一个树
let firstHelpDocId = null;
// 构建目录树
function buildHelpDocCatalogTree(data, parentId, helpDocMap) {
let children = data.filter((e) => e.parentId === parentId) || [];
//排序
children = _.sortBy(children, (e) => e.sort);
let helpDocList = helpDocMap.get(parentId);
if (helpDocList) {
//排序
helpDocList = _.sortBy(helpDocList, (e) => e.sort);
if (!firstHelpDocId) {
firstHelpDocId = helpDocList[0].helpDocId;
}
children.push(...helpDocList);
}
for (const e of children) {
if (e.key.indexOf(TYPE_HELP_DOC_PREFIX) > -1) {
continue;
}
e.isLeaf = false;
e.children = buildHelpDocCatalogTree(data, e.helpDocCatalogId, helpDocMap);
}
return children;
}
</script>
<style lang="less" scoped>
:deep(.ant-layout-header) {
height: auto;
}
:deep(.layout-header) {
height: auto;
}
:deep(.ant-tree-treenode) {
margin: 2px 0;
}
.help-doc-layout {
overflow-y: hidden;
height: 100vh;
overflow-x: hidden;
}
.layout-header {
background: @primary-color;
padding: 0;
z-index: 999;
color: white;
height: @header-user-height;
line-height: @header-user-height;
display: flex;
justify-content: flex-start;
.layout-header-title {
height: @header-user-height;
line-height: @header-user-height;
padding: 0px 15px 0px 15px;
z-index: 9999;
display: flex;
cursor: pointer;
justify-content: flex-start;
margin-bottom: 10px;
.logo-img {
width: 40px;
height: @header-user-height;
}
.title {
font-size: 18px;
font-weight: 600;
margin-left: 10px;
text-align: center;
color: '#001529';
}
.avatar {
position: fixed;
top: 0;
right: 18px;
}
}
}
.layout-container {
height: calc(100vh - @header-height);
overflow-x: hidden;
overflow-y: auto;
}
.side-menu {
height: 100vh;
overflow: scroll;
.help-doc-tree {
color: #001529;
margin-top: 10px;
font-size: 16px;
}
}
.help-doc-layout-content {
min-height: auto;
position: relative;
overflow-y: scroll;
overflow-x: hidden;
margin-left: 5px;
margin-top: 8px;
height: calc(100% - 40px);
}
.layout-footer {
padding: 0 !important;
position: fixed;
bottom: 0;
right: calc(50% - 300px);
display: flex;
height: 30px;
justify-content: center;
}
</style>
../lib/smart-watermark

View File

@@ -0,0 +1,30 @@
<!--
* layout 多种模式
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:40:16
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<!--左侧菜单 模式-->
<SideLayout v-if="layout === LAYOUT_ENUM.SIDE.value" />
<!--左侧展开菜单 模式-->
<SideExpandLayout v-if="layout === LAYOUT_ENUM.SIDE_EXPAND.value" />
<!--顶部菜单 模式-->
<TopLayout v-if="layout === LAYOUT_ENUM.TOP.value" />
<!--定期修改密码-->
<RegularChangePasswordModal />
</template>
<script setup>
import { computed } from 'vue';
import { LAYOUT_ENUM } from '/@/constants/layout-const';
import SideExpandLayout from './side-expand-layout.vue';
import SideLayout from './side-layout.vue';
import TopLayout from './top-layout.vue';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import RegularChangePasswordModal from './components/change-password/regular-change-password-modal.vue';
const layout = computed(() => useAppConfigStore().$state.layout);
</script>

View File

@@ -0,0 +1,9 @@
/**
* layout 相关元素 id
*/
export const LAYOUT_ELEMENT_IDS = {
menu: 'smartAdminMenu',
main: 'smartAdminMain',
header: 'smartAdminHeader',
content: 'smartAdminLayoutContent',
}

View File

@@ -0,0 +1,277 @@
<!--
* 展开菜单模式
*
* @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%">
<!-- 侧边菜单 side-menu -->
<a-layout-sider :id="LAYOUT_ELEMENT_IDS.menu" :theme="theme" class="side-menu" :collapsed="collapsed" :trigger="null">
<!-- 左侧菜单 -->
<SideExpandMenu :collapsed="collapsed" />
</a-layout-sider>
<!--中间内容一共三部分1顶部;2中间内容区域;3底部一般是公司版权信息;-->
<a-layout class="admin-layout-main" :style="`height: ${windowHeight}px`" :id="LAYOUT_ELEMENT_IDS.main">
<!-- 顶部头部信息 -->
<a-layout-header class="smart-layout-header" :id="LAYOUT_ELEMENT_IDS.header">
<a-row justify="space-between" class="smart-layout-header-user">
<a-col class="smart-layout-header-left">
<span class="collapsed-button">
<menu-unfold-outlined v-if="collapsed" class="trigger" @click="() => (collapsed = !collapsed)" />
<menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
</span>
<a-tooltip placement="bottom">
<template #title>首页</template>
<span class="home-button" @click="goHome">
<home-outlined class="trigger" />
</span>
</a-tooltip>
<span class="location-breadcrumb">
<MenuLocationBreadcrumb />
</span>
</a-col>
<!---用戶操作区域-->
<a-col class="smart-layout-header-right">
<HeaderUserSpace />
</a-col>
</a-row>
<PageTag />
</a-layout-header>
<!--中间内容-->
<a-layout-content class="admin-layout-content" :id="LAYOUT_ELEMENT_IDS.content">
<!--不keepAlive的iframe使用单个iframe组件-->
<IframeIndex v-show="iframeNotKeepAlivePageFlag" :key="route.name" :name="route.name" :url="route.meta.frameUrl" />
<!--keepAlive的iframe 每个页面一个iframe组件-->
<IframeIndex
v-for="item in keepAliveIframePages"
v-show="route.name === item.name"
:key="item.name"
:name="item.name"
:url="item.meta.frameUrl"
/>
<!--非iframe使用router-view-->
<div v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name !== e.name)">
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveIncludes">
<component :is="Component" :key="route.name" />
</keep-alive>
</router-view>
</div>
</a-layout-content>
<!-- footer 版权公司信息 -->
<a-layout-footer class="smart-layout-footer" v-show="footerFlag"> <SmartFooter /></a-layout-footer>
<!---- 回到顶部 --->
<a-back-top :target="backTopTarget" :visibilityHeight="80" />
</a-layout>
<!-- 右侧帮助文档 help-doc -->
<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>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue';
import HeaderUserSpace from './components/header-user-space/index.vue';
import MenuLocationBreadcrumb from './components/menu-location-breadcrumb/index.vue';
import PageTag from './components/page-tag/index.vue';
import SideExpandMenu from './components/side-expand-menu/index.vue';
import SmartFooter from './components/smart-footer/index.vue';
import { smartKeepAlive } from './components/smart-keep-alive';
import IframeIndex from '/@/components/framework/iframe/iframe-index.vue';
import watermark from '../lib/smart-watermark';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { useUserStore } from '/@/store/modules/system/user';
import SideHelpDoc from './components/side-help-doc/index.vue';
import { useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const.js';
const windowHeight = ref(window.innerHeight);
//主题颜色
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
//是否显示标签页
const pageTagFlag = computed(() => useAppConfigStore().$state.pageTagFlag);
// 是否显示帮助文档
const helpDocFlag = computed(() => useAppConfigStore().$state.helpDocFlag);
// 是否默认展开帮助文档
const helpDocExpandFlag = computed(() => useAppConfigStore().$state.helpDocExpandFlag);
// 是否显示页脚
const footerFlag = computed(() => useAppConfigStore().$state.footerFlag);
// 是否显示水印
const watermarkFlag = computed(() => useAppConfigStore().$state.watermarkFlag);
// 多余高度
const dueHeight = computed(() => {
let due = 40;
if (useAppConfigStore().$state.pageTagFlag) {
due = due + 40;
}
if (useAppConfigStore().$state.footerFlag) {
due = due + 40;
}
return due;
});
//是否隐藏菜单
const collapsed = ref(false);
//页面初始化的时候加载水印
onMounted(() => {
if (watermarkFlag.value) {
watermark.set(LAYOUT_ELEMENT_IDS.content, useUserStore().actualName);
} else {
watermark.clear();
}
});
watch(
() => watermarkFlag.value,
(newValue) => {
if (newValue) {
watermark.set(LAYOUT_ELEMENT_IDS.content, useUserStore().actualName);
} else {
watermark.clear();
}
}
);
window.addEventListener('resize', function () {
windowHeight.value = window.innerHeight;
});
//回到顶部
const backTopTarget = () => {
return document.getElementById(LAYOUT_ELEMENT_IDS.main);
};
// ----------------------- keep-alive相关 -----------------------
let { route, keepAliveIncludes, iframeNotKeepAlivePageFlag, keepAliveIframePages } = smartKeepAlive();
const router = useRouter();
function goHome() {
router.push({ name: HOME_PAGE_NAME });
}
</script>
<style scoped lang="less">
:deep(.ant-layout-header) {
height: auto;
}
:deep(.layout-header) {
height: auto;
}
.smart-layout-header {
background: #fff;
padding: 0;
z-index: 21;
}
.smart-layout-header-user {
height: @header-user-height;
border-bottom: 1px solid #f6f6f6;
}
.smart-layout-header-left {
display: flex;
height: @header-user-height;
.collapsed-button {
margin-left: 10px;
line-height: @header-user-height;
}
.home-button {
margin-left: 15px;
cursor: pointer;
padding: 0 5px;
line-height: @header-user-height;
}
.home-button:hover {
background-color: #efefef;
}
.location-breadcrumb {
margin-left: 15px;
line-height: @header-user-height;
}
}
.smart-layout-header-right {
display: flex;
height: @header-user-height;
}
.admin-layout {
.side-menu {
flex: 0 !important;
min-width: inherit !important;
max-width: none !important;
width: auto !important;
&.fixed-side {
position: fixed;
height: 100vh;
left: 0;
top: 0;
}
}
.help-doc-sider {
flex: 0 !important;
min-width: 100px;
height: 100vh;
max-width: 100px;
width: auto !important;
&.fixed-side {
position: fixed;
height: 100vh;
right: 0;
top: 0;
}
}
.virtual-side {
transition: all 0.2s;
}
.virtual-header {
transition: all 0.2s;
opacity: 0;
&.fixed-tabs.multi-page:not(.fixed-header) {
height: 0;
}
}
.admin-layout-main {
overflow-x: hidden;
}
.admin-layout-content {
background-color: inherit;
min-height: auto;
position: relative;
padding: 10px 10px 0px 10px;
height: calc(100% - v-bind(dueHeight) px);
overflow-x: hidden;
}
}
.smart-layout-footer {
position: relative;
padding: 10px 0;
display: flex;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,295 @@
<template>
<a-layout class="admin-layout" style="min-height: 100%">
<!-- 侧边菜单 side-menu -->
<a-layout-sider :id="LAYOUT_ELEMENT_IDS.menu" class="side-menu" :width="sideMenuWidth" :collapsed="collapsed" :theme="theme">
<!-- 左侧菜单 -->
<SideMenu :collapsed="collapsed" />
</a-layout-sider>
<!--中间内容一共三部分1顶部;2中间内容区域;3底部一般是公司版权信息;-->
<a-layout :id="LAYOUT_ELEMENT_IDS.main" :style="`height: ${windowHeight}px`" class="admin-layout-main">
<!-- 顶部头部信息 -->
<a-layout-header class="layout-header" :id="LAYOUT_ELEMENT_IDS.header">
<a-row class="layout-header-user" justify="space-between">
<a-col class="layout-header-left">
<span class="collapsed-button">
<menu-unfold-outlined v-if="collapsed" class="trigger" @click="() => (collapsed = !collapsed)" />
<menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
</span>
<a-tooltip placement="bottom">
<template #title>首页</template>
<span class="home-button" @click="goHome">
<home-outlined class="trigger" />
</span>
</a-tooltip>
<span class="location-breadcrumb">
<MenuLocationBreadcrumb />
</span>
</a-col>
<!---用戶操作区域-->
<a-col class="layout-header-right">
<HeaderUserSpace />
</a-col>
</a-row>
<PageTag />
</a-layout-header>
<!--中间内容-->
<a-layout-content :id="LAYOUT_ELEMENT_IDS.content" class="admin-layout-content">
<!--不keepAlive的iframe使用单个iframe组件-->
<IframeIndex v-if="iframeNotKeepAlivePageFlag" :key="route.name" :name="route.name" :url="route.meta.frameUrl" />
<!--keepAlive的iframe 每个页面一个iframe组件-->
<IframeIndex
v-for="item in keepAliveIframePages"
v-show="route.name === item.name"
:key="item.name"
:name="item.name"
:url="item.meta.frameUrl"
/>
<!--非iframe使用router-view-->
<div v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name != e.name)">
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveIncludes">
<component :is="Component" :key="route.name" />
</keep-alive>
</router-view>
</div>
</a-layout-content>
<!-- footer 版权公司信息 -->
<a-layout-footer class="layout-footer" v-show="footerFlag">
<smart-footer />
</a-layout-footer>
<!--- 回到顶部 -->
<a-back-top :target="backTopTarget" :visibilityHeight="80" />
</a-layout>
<!-- 右侧帮助文档 help-doc -->
<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>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue';
import { useAppConfigStore } from '../store/modules/system/app-config';
import HeaderUserSpace from './components/header-user-space/index.vue';
import MenuLocationBreadcrumb from './components/menu-location-breadcrumb/index.vue';
import PageTag from './components/page-tag/index.vue';
import SideMenu from './components/side-menu/index.vue';
import SmartFooter from './components/smart-footer/index.vue';
import { smartKeepAlive } from './components/smart-keep-alive';
import IframeIndex from '/@/components/framework/iframe/iframe-index.vue';
import watermark from '../lib/smart-watermark';
import { useUserStore } from '/@/store/modules/system/user';
import SideHelpDoc from './components/side-help-doc/index.vue';
import { useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const.js';
const windowHeight = ref(window.innerHeight);
//菜单宽度
const sideMenuWidth = computed(() => useAppConfigStore().$state.sideMenuWidth);
//主题颜色
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
//是否显示标签页
const pageTagFlag = computed(() => useAppConfigStore().$state.pageTagFlag);
// 是否显示帮助文档
const helpDocFlag = computed(() => useAppConfigStore().$state.helpDocFlag);
// 是否默认展开帮助文档
const helpDocExpandFlag = computed(() => useAppConfigStore().$state.helpDocExpandFlag);
// 是否显示页脚
const footerFlag = computed(() => useAppConfigStore().$state.footerFlag);
// 是否显示水印
const watermarkFlag = computed(() => useAppConfigStore().$state.watermarkFlag);
// 多余高度
const dueHeight = computed(() => {
let due = 40;
if (useAppConfigStore().$state.pageTagFlag) {
due = due + 40;
}
if (useAppConfigStore().$state.footerFlag) {
due = due + 40;
}
return due;
});
//是否隐藏菜单
const collapsed = ref(false);
//页面初始化的时候加载水印
onMounted(() => {
if (watermarkFlag.value) {
watermark.set(LAYOUT_ELEMENT_IDS.content, useUserStore().actualName);
} else {
watermark.clear();
}
});
watch(
() => watermarkFlag.value,
(newValue) => {
if (newValue) {
watermark.set(LAYOUT_ELEMENT_IDS.content, useUserStore().actualName);
} else {
watermark.clear();
}
}
);
//回到顶部
const backTopTarget = () => {
return document.getElementById(LAYOUT_ELEMENT_IDS.main);
};
const router = useRouter();
function goHome() {
router.push({ name: HOME_PAGE_NAME });
}
window.addEventListener('resize', function () {
windowHeight.value = window.innerHeight;
});
// ----------------------- keep-alive相关 -----------------------
let { route, keepAliveIncludes, iframeNotKeepAlivePageFlag, keepAliveIframePages } = smartKeepAlive();
</script>
<style lang="less" scoped>
:deep(.ant-layout-header) {
height: auto;
}
:deep(.layout-header) {
height: auto;
}
.layout-header {
background: #fff;
padding: 0;
z-index: 21;
}
.layout-header-user {
height: @header-user-height;
border-bottom: 1px solid #f6f6f6;
}
.layout-header-left {
display: flex;
height: @header-user-height;
.collapsed-button {
margin-left: 10px;
line-height: @header-user-height;
}
.home-button {
margin-left: 15px;
cursor: pointer;
padding: 0 5px;
line-height: @header-user-height;
}
.home-button:hover {
background-color: #efefef;
}
.location-breadcrumb {
margin-left: 15px;
line-height: @header-user-height;
}
}
.layout-header-right {
display: flex;
height: @header-user-height;
}
.layout-container {
height: calc(100vh - @header-height);
overflow-x: hidden;
}
.admin-layout {
.side-menu {
height: 100vh;
overflow-x: hidden;
overflow-y: scroll;
&.fixed-side {
position: fixed;
height: 100vh;
left: 0;
top: 0;
}
}
.side-menu::-webkit-scrollbar {
width: 4px;
}
.side-menu::-webkit-scrollbar-thumb {
border-radius: 10px;
background: rgba(0, 0, 0, 0.2);
}
.side-menu::-webkit-scrollbar-track {
border-radius: 0;
background: rgba(0, 0, 0, 0.1);
}
.help-doc-sider {
flex: 0 !important;
min-width: 100px;
height: 100vh;
max-width: 100px;
width: auto !important;
&.fixed-side {
position: fixed;
height: 100vh;
right: 0;
top: 0;
}
}
.virtual-side {
transition: all 0.2s;
}
.virtual-header {
transition: all 0.2s;
opacity: 0;
&.fixed-tabs.multi-page:not(.fixed-header) {
height: 0;
}
}
.admin-layout-main {
overflow-y: hidden;
overflow-x: hidden;
}
.admin-layout-content {
background-color: inherit;
min-height: auto;
position: relative;
overflow-x: hidden;
padding: 10px 10px 0px 10px;
height: calc(100% - v-bind(dueHeight) px);
}
}
.layout-footer {
position: relative;
padding: 7px 0px;
display: flex;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,169 @@
<template>
<a-layout class="admin-layout">
<!-- 顶部菜单 -->
<a-layout-header class="top-menu" :theme="theme" :id="LAYOUT_ELEMENT_IDS.menu">
<TopMenu />
</a-layout-header>
<!--中间内容-->
<a-layout-content :id="LAYOUT_ELEMENT_IDS.content" class="admin-layout-content">
<!---标签页-->
<div class="page-tag-div" v-show="pageTagFlag" :id="LAYOUT_ELEMENT_IDS.header">
<PageTag />
</div>
<!--不keepAlive的iframe使用单个iframe组件-->
<IframeIndex v-if="iframeNotKeepAlivePageFlag" :key="route.name" :name="route.name" :url="route.meta.frameUrl" />
<!--keepAlive的iframe 每个页面一个iframe组件-->
<IframeIndex
v-for="item in keepAliveIframePages"
v-show="route.name === item.name"
:key="item.name"
:name="item.name"
:url="item.meta.frameUrl"
/>
<!--非iframe使用router-view-->
<div v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name !== e.name)">
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveIncludes">
<component :is="Component" :key="route.name" />
</keep-alive>
</router-view>
</div>
</a-layout-content>
<!-- footer 版权公司信息 -->
<a-layout-footer class="layout-footer" v-show="footerFlag">
<smart-footer />
</a-layout-footer>
<!--- 回到顶部 -->
<a-back-top :target="backTopTarget" :visibilityHeight="80" />
</a-layout>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue';
import { useAppConfigStore } from '../store/modules/system/app-config';
import PageTag from './components/page-tag/index.vue';
import TopMenu from './components/top-menu/index.vue';
import SmartFooter from './components/smart-footer/index.vue';
import { smartKeepAlive } from './components/smart-keep-alive';
import IframeIndex from '/@/components/framework/iframe/iframe-index.vue';
import watermark from '../lib/smart-watermark';
import { useUserStore } from '/@/store/modules/system/user';
import { useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const.js';
const windowHeight = ref(window.innerHeight);
//主题颜色
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
const color = computed(() => {
let isLight = useAppConfigStore().$state.sideMenuTheme === 'light';
return {
color: isLight ? '#001529' : '#FFFFFF',
background: isLight ? '#FFFFFF' : '#001529',
};
});
//是否显示标签页
const pageTagFlag = computed(() => useAppConfigStore().$state.pageTagFlag);
// 是否显示页脚
const footerFlag = computed(() => useAppConfigStore().$state.footerFlag);
// 是否显示水印
const watermarkFlag = computed(() => useAppConfigStore().$state.watermarkFlag);
// 页面宽度
const pageWidth = computed(() => useAppConfigStore().$state.pageWidth);
// 多余高度
const dueHeight = computed(() => {
let due = '45px';
if (useAppConfigStore().$state.pageTagFlag) {
due = '85px';
}
return due;
});
//页面初始化的时候加载水印
onMounted(() => {
if (watermarkFlag.value) {
watermark.set(LAYOUT_ELEMENT_IDS.content, useUserStore().actualName);
} else {
watermark.clear();
}
});
watch(
() => watermarkFlag.value,
(newValue) => {
if (newValue) {
watermark.set(LAYOUT_ELEMENT_IDS.content, useUserStore().actualName);
} else {
watermark.clear();
}
}
);
//回到顶部
const backTopTarget = () => {
return document.getElementById(LAYOUT_ELEMENT_IDS.main);
};
const router = useRouter();
function goHome() {
router.push({ name: HOME_PAGE_NAME });
}
window.addEventListener('resize', function () {
windowHeight.value = window.innerHeight;
});
// ----------------------- keep-alive相关 -----------------------
let { route, keepAliveIncludes, iframeNotKeepAlivePageFlag, keepAliveIframePages } = smartKeepAlive();
</script>
<style lang="less" scoped>
.admin-layout {
min-height: 100%;
.top-menu {
padding: 0px;
height: 48px;
line-height: 48px;
width: 100%;
z-index: 3;
right: 0;
position: fixed;
background-color: v-bind('color.background');
}
.admin-layout-content {
background-color: inherit;
min-height: auto;
position: relative;
overflow-x: hidden;
padding: 10px 0;
width: v-bind(pageWidth);
margin-top: v-bind(dueHeight);
margin-left: auto;
margin-right: auto;
.page-tag-div {
position: fixed;
top: 48px;
width: v-bind(pageWidth);
height: 40px;
line-height: 40px;
z-index: 3;
}
}
}
.layout-footer {
position: relative;
padding: 7px 0px;
display: flex;
justify-content: center;
}
</style>