
* docs: readme * fix: missing formPath * chore: 去除锁定的esbuild版本 * perf: 去除debug组件 * perf: 参数键值 自动高度 * refactor: 代码生成配置页面重构 去除步骤条 * perf: 移除文件 * docs: 文件夹说明 * chore: 移除一些配置项 * chore: 注释优化 * refactor: 移除ele和naive目录 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * perf: request support to set how to return response (#5436) * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * refactor: 登录超时的i18n * fix: requestClient缺失i18n内容 * refactor: 优化oss下载进度提示 * feat: 下载进度loading * fix: antd button icon style (#5421) * feat: oss下载进度(已下载的KB 无法作为进度显示 total返回为null) * fix: 下载文件时(responseType === 'blob')需要判断下载失败(返回json而非二进制)的情况 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * refactor: 新增后跳转到未发布流程 * fix: same name route * chore: 调整为部署json类型 * fix: mouse events ignored on modal loading (#5409) * docs: update docs (#5408) * refactor: 移除已经弃用的方法 * refactor: follow官方handleRangeTimeValue更新 * chore: 删除文件夹(前端路由需要的) * chore: 修改本地路由写法(新版)/新增本地菜单图标 * fix: form update state error before form mounted (#5406) * fix: demos route fixed (#5405) * chore: 不使用基础布局(仅在顶级生效) * feat: modal state locked on submitting (#5401) * chore: 修改zIndex * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * refactor: fix popup component zIndex (#5397) * style: element plus loading style fixed (#5393) * perf: improve fieldMappingTime to support format function (#5392) * fix: hide root route in breadcrumb * feat: support set default props for drawer and modal (#5390) * fix: root router config fixed (#5389) * fix: 修改Vxe默认zIndex为995 解决右上角全屏后modal/drawer(zIndex: 1000)被遮挡 * feat: add `noBasicLayout` in route meta (#5386) * chore: wechat image * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * chore: 改为全局参数配置 去除局部参数 * fix: spinner may stop playing animation after dismiss (#5365) * style: popover bgColor is too close to common (#5364) * docs: version update * docs: changelog * chore: 文件上传 描述 * ci: retry deploy while faild * feat: 文件上传 进度条+提示文字 * feat: 文件上传 进度条 * feat: 上传文件格式说明 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into warmflow * fix: useEcharts return invalid instance (#5360) * feat: popup component support overlay blur effect (#5359) * feat: improve `tippy` demo (#5357) * feat: integrate new component `Tippy` with demo (#5355) * chore: 优化表格图片显示 * perf: add nested modal demo (#5353) * chore: 默认显示右边的滚动条 防止出现滚动条被挤压 * perf: modal and drawer api support chain calls (#5351) * feat: allow close tab when mouse middle button click (#5347) * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into warmflow * refactor: 重构显示total的逻辑 * chore: 调整高度自适应代码 * chore: vxe升级4.10.0版本(锁定) * fix: 添加失效的option * fix: 需要为数组 * fix: locale switching logic correction (#5344) * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into warmflow * chore: 导入类型优化 解决eslint报红 * refactor: type/注释优化 去除大量any * fix: vxeGrid init without search form (#5342) * chore: 锁屏默认false 关闭该功能 * chore: 调整接口 * chore: update deps * fix: primaryColor calculation (#5337) * fix: form valid-error style in naive (#5336) * fix: form `fieldMappingTime` improve and `modelPropName` support (#5335) * fix: code lint * fix: form `fieldMappingTime` is not working (#5333) * chore: 选人组件样式 * fix: download from url triggered twice sometimes (#5319) * chore: 优化代码 * chore: 动态类名(无效)改为style * refactor: 字典相关功能重构 采用一个Map储存字典(之前为两个Map) * feat: 字典支持number类型存储 * chore: 调整样式 * chore: 修改选中border为1px * chore: 字段 * chore: 改为新窗口打开(适用于pdf/图片)而非直接下载 * chore: 更新样式 * chore: 更新字段 * chore: 改为computed * chore: 跳转到未发布流程tab * chore: 优化样式 * docs: readme * fix: name重复导致的404 * Merge branch 'dev' of https://gitee.com/dapppp/ruoyi-plus-vben5 into warmflow * chore: 使用legacy来保证copy的兼容性 * chore: 去除log 添加说明 * chore: 优化代码 * feat: 节点关联/节点独立的切换逻辑 * chore: remove logic * chore: vxe可编辑表格demo * chore: 不允许在按钮下添加数据 * docs: changelog * fix: wrong code * chore: 移除测试菜单 * chore: 优化代码 * refactor: 租户套餐菜单替换为新版 * refactor: 使用新版菜单勾选 * chore: 点行会勾选/取消全部权限 点权限不会勾选行 * chore: 全屏引导+样式优化 * chore: 调整间距 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * feat: useEcharts exports echarts instance#5294 (#5299) * chore: update quick-start.md (#5303) * chore: updateCheckedNumber * refactor: 优化代码 * chore: 优化代码 * chore: 优化样式 * chore: keys依赖于menu 需要先加载menu * chore: 菜单加载完毕再显示 * feat: 新的菜单选择组件(beta) * chore: $t * chore: 测试菜单页面 * chore: 优化代码 * feat: 对ossId回显的支持 * chore: 只获取一次默认密码而非每次打开modal都获取 * fix: vben select placeholder color (#5286) * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * perf: format code with better style (#5283) * chore: 工作流演示站 * fix: sidebar preferences fixed (#5276) * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * fix: breadcrumb setting not valid for `header-sidebar-nav` layout (#5275) * fix: header logo may not be collapsed in `header-sidebar-nav` layout (#5274) * feat: new layout `sidebar nav with full header` (#5270) * feat: drawer close icon placement (#5269) * docs: update dialog and drawer docs * feat: drawer support destroy on close * feat: drawer support `onOpened` & `onClosed` * feat: modal support destroy on close * fix: wrong boolean * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * chore: 调整tab位置 * chore: 删除历史流程 改为tab切换 * fix: header-mixed layout side-menu active (#5265) * feat: header mixed layout (#5263) * chore: release 5.5.2 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * chore: downgrade vue-tsc version * feat: header menu align support (#5256) * chore: update deps * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * chore: add apiSelect remote search demo (#5246) * chore: 审批改为description而非disabled的表单 * chore: 改为ts * chore: 错误的conetnt * refactor: 终止/转办/委托支持填写意见 * chore: 第一次拿到的是readonly的数据 如果需要修改 需要cloneDeep * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * fix: grid form submit button locale switch (#5205) * chore: 调整驳回 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * fix: build error (#5199) * fix: esbuild自动升级导致运行/打包报错 * fix: esbuild自动升级导致运行/打包报错 * chore: 流程定义 激活改为switch * chore: 流程申请支持上传文件 * chore: title 审批通过 * fix: vxeGrid top padding (#5193) * fix: 表格排序翻页会丢失排序参数 * chore: 去除log打印 * chore: 流程监控 待办任务 * chore: 我发起的 * chore: 去除已经移除的菜单页面 * chore: 我的已办 * chore: 页面优化 * chore: 重置tooltip * feat: 我的抄送搜索/优化重复触发的接口 * feat: 流程定义 历史 * chore: 修改分类 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * fix: grid tools in toolbar config not working as expected (#5190) * feat: add `resizable` and `ColPage` component (#5188) * chore: 条件 * chore: break-all * feat: 流程分类 搜索 * chore: 弹窗关闭后仍然显示表单浮层 * chore: 选人组件的样式 * chore: 搜索的样式 * chore: 漏掉的导入 * chore: 最大显示的头像数量 超过显示为省略号头像 * fix: 选人的一些问题 * Merge branch 'warmflow' of https://gitee.com/dapppp/ruoyi-plus-vben5 i… * chore: 没有更多数据了 * fix: sidebar header height (#5183) * chore: 搜索表单布局+申请人 * fix: remove the overlap caused by border-b (#5160) * docs: fix typos (#5169) * fix: resolve eslint errors as well as TS type errors (#5172) * chore: enter提交表单 * chore: 修改文案 * chore: 默认全部展开 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * feat: page content class override (#5179) * fix: sidebar style on focus (#5178) * fix: 抄送选人 最右侧已选中删除item无效 * feat: 复制 * chore: 昵称过长的显示 * chore: 默认选中第一个 * chore: 修改relative位置 * chore: 搜索 * feat: 我的待办 - 搜索条件 * chore: 流程监控 - 待办任务页面的id不唯一 改为前端处理 * feat: 修改办理人 * chore: 流程干预 - 加签/减签 * chore: avatar大小 * chore: 抄送需要手动添加createByName * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * chore: 审批通过 抄送 * feat: 流程实例-流程预览 * chore: spell * chore: clientid * chore: 分类条件 * chore: 修改办理人 * chore: 更改postMessage参数 * chore: 内嵌iframe高度根据表单高度调整 * chore: 流程详情 * feat: 抄送选择 * chore: 调整分类树 * fix: user homePath no effect sometimes (#5166) * feat: form compact mode support (#5165) * fix: form auto submit no effect when showDefaultActions is false (#5163) * chore: 修改width * feat: 待办任务 * feat: 我的抄送 * chore: 流程定义 样式 * chore: 退回后重新申请 * chore: 请假申请布局 * chore: 请假申请-并行会签网关 * chore: 分类去除根目录 * chore: 详情modal(未完成) * chore: 请假申请根据不同状态显示按钮 * chore: 流程删除/撤销 * chore: 审批完成后刷新当前页 * feat: 选人组件(未完成) 加签减签 * docs: fix docs-link and add `EllipsisText` docs (#5158) * chore: 新窗口打开文件 * chore: 审批通过 * chore: 使用useEventListener替换原生 * chore: 字段错误 * chore: iframe通信 加载完毕后才显示表单 解决卡顿问题 * chore: 审批终止/驳回 * chore: 附件图标 * chore: process_running显示按钮 * chore: label错误 * chore: 保存的事件 * chore: 需要加上clientId * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * feat: form `colon` support (#5156) * chore: 完善请假申请 * feat: improve code login demo (#5154) * chore: 客户端管理 行高自适应 * chore: 内嵌表单的路径 * chore: 修改avatar背景色 * chore: 注释 * chore: activePath * chore: leave表单 * chore: 修改请假demo路径 * chore: categoryId * chore: 我的已办 * chore: 我发起的 * chore: loading * chore: 历史版本 * chore: 完善task api * chore: 隐藏'菜单加载中' * chore: missing import * feat: add demo for modify menu badge data * chore: 流程实例 * chore: 审批附件 * chore: 我的待办 提取公共组件 * chore: 流程部署 * chore: 新增/编辑/导出xml * chore: 流程定义(除历史版本) * feat: `autoActivateChild` support more layout mode (#5148) * feat: auto activate subMenu on select root menu (#5147) * fix: `disabledOnChangeListener` not work in form (#5146) * fix: login expired modal z-index (#5145) * feat: user-dropdown support `hover` trigger (#5143) * fix: pinInput value synchronous update (#5142) * fix: vxeGrid default sort data no effect in first query (#5141) * fix: vscode debug profile (#5140) * fix: form component events bind (#5137) * chore: 在线用户样式 开启虚拟滚动 * chore: 去掉个人中心 在线设备的分页 * chore: 去掉在线用户的分页 * chore: changelog * refactor: 获取字典的方法 提取公共函数 减少冗余代码 * fix: element plus validate failed style (#5130) * chore: 使用私有桶的提示 * feat: tabbar support mouse wheel vertical (#5129) * fix: form support `disabledOnInputListener` (#5127) * fix: form submission not appropriate (#5126) * Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev * chore: release 5.5.1 * feat: table search form visible control (#5121) * chore: 需要隐藏菜单 * chore: 我的待办 & 请假 * chore: 流程定义(未完成) * chore: 流程定义(开发中) * Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev * Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev * Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev * chore: version * chore: 锁定vxe-table版本 4.9.8版本存在样式问题 * chore: 暂时锁定cspell版本 * refactor: 由于不能输入 需要使用watch监听 * chore: https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IB7ANL * chore: 移除冗余代码 * chore: 组件卸载时移除emitter * fix: the route path did not synchronize with the page (#4947) * style: typo (#4948) * chore: 替换为commonDownloadExcel * fix: 左边部门树错误emit导致会调用两次列表api * chore: label样式 * chore: 改为Textarea * chore: 滚动条宽度 * chore: 审批样式 * chore: 部门及以下或本人数据权限 * Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev * chore: 个人中心强退设备接口路径
605 lines
15 KiB
Vue
605 lines
15 KiB
Vue
<script setup lang="ts">
|
||
import type { CSSProperties } from 'vue';
|
||
|
||
import type { VbenLayoutProps } from './vben-layout';
|
||
|
||
import {
|
||
SCROLL_FIXED_CLASS,
|
||
useLayoutFooterStyle,
|
||
useLayoutHeaderStyle,
|
||
} from '@vben-core/composables';
|
||
import { Menu } from '@vben-core/icons';
|
||
import { VbenIconButton } from '@vben-core/shadcn-ui';
|
||
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
|
||
import { useMouse, useScroll, useThrottleFn } from '@vueuse/core';
|
||
import { computed, ref, watch } from 'vue';
|
||
|
||
import {
|
||
LayoutContent,
|
||
LayoutFooter,
|
||
LayoutHeader,
|
||
LayoutSidebar,
|
||
LayoutTabbar,
|
||
} from './components';
|
||
import { useLayout } from './hooks/use-layout';
|
||
|
||
interface Props extends VbenLayoutProps {}
|
||
|
||
defineOptions({
|
||
name: 'VbenLayout',
|
||
});
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
contentCompact: 'wide',
|
||
contentCompactWidth: 1200,
|
||
contentPadding: 0,
|
||
contentPaddingBottom: 0,
|
||
contentPaddingLeft: 0,
|
||
contentPaddingRight: 0,
|
||
contentPaddingTop: 0,
|
||
footerEnable: false,
|
||
footerFixed: true,
|
||
footerHeight: 32,
|
||
headerHeight: 50,
|
||
headerHidden: false,
|
||
headerMode: 'fixed',
|
||
headerToggleSidebarButton: true,
|
||
headerVisible: true,
|
||
isMobile: false,
|
||
layout: 'sidebar-nav',
|
||
sidebarCollapseShowTitle: false,
|
||
sidebarExtraCollapsedWidth: 60,
|
||
sidebarHidden: false,
|
||
sidebarMixedWidth: 80,
|
||
sidebarTheme: 'dark',
|
||
sidebarWidth: 180,
|
||
sideCollapseWidth: 60,
|
||
tabbarEnable: true,
|
||
tabbarHeight: 40,
|
||
zIndex: 200,
|
||
});
|
||
|
||
const emit = defineEmits<{ sideMouseLeave: []; toggleSidebar: [] }>();
|
||
const sidebarCollapse = defineModel<boolean>('sidebarCollapse');
|
||
const sidebarExtraVisible = defineModel<boolean>('sidebarExtraVisible');
|
||
const sidebarExtraCollapse = defineModel<boolean>('sidebarExtraCollapse');
|
||
const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');
|
||
const sidebarEnable = defineModel<boolean>('sidebarEnable', { default: true });
|
||
|
||
// side是否处于hover状态展开菜单中
|
||
const sidebarExpandOnHovering = ref(false);
|
||
const headerIsHidden = ref(false);
|
||
const contentRef = ref();
|
||
|
||
const {
|
||
arrivedState,
|
||
directions,
|
||
isScrolling,
|
||
y: scrollY,
|
||
} = useScroll(document);
|
||
|
||
const { setLayoutHeaderHeight } = useLayoutHeaderStyle();
|
||
const { setLayoutFooterHeight } = useLayoutFooterStyle();
|
||
|
||
const { y: mouseY } = useMouse({ target: contentRef, type: 'client' });
|
||
|
||
const {
|
||
currentLayout,
|
||
isFullContent,
|
||
isHeaderMixedNav,
|
||
isHeaderNav,
|
||
isMixedNav,
|
||
isSidebarMixedNav,
|
||
} = useLayout(props);
|
||
|
||
/**
|
||
* 顶栏是否自动隐藏
|
||
*/
|
||
const isHeaderAutoMode = computed(() => props.headerMode === 'auto');
|
||
|
||
const headerWrapperHeight = computed(() => {
|
||
let height = 0;
|
||
if (props.headerVisible && !props.headerHidden) {
|
||
height += props.headerHeight;
|
||
}
|
||
if (props.tabbarEnable) {
|
||
height += props.tabbarHeight;
|
||
}
|
||
return height;
|
||
});
|
||
|
||
const getSideCollapseWidth = computed(() => {
|
||
const { sidebarCollapseShowTitle, sidebarMixedWidth, sideCollapseWidth } =
|
||
props;
|
||
|
||
return sidebarCollapseShowTitle ||
|
||
isSidebarMixedNav.value ||
|
||
isHeaderMixedNav.value
|
||
? sidebarMixedWidth
|
||
: sideCollapseWidth;
|
||
});
|
||
|
||
/**
|
||
* 动态获取侧边区域是否可见
|
||
*/
|
||
const sidebarEnableState = computed(() => {
|
||
return !isHeaderNav.value && sidebarEnable.value;
|
||
});
|
||
|
||
/**
|
||
* 侧边区域离顶部高度
|
||
*/
|
||
const sidebarMarginTop = computed(() => {
|
||
const { headerHeight, isMobile } = props;
|
||
return isMixedNav.value && !isMobile ? headerHeight : 0;
|
||
});
|
||
|
||
/**
|
||
* 动态获取侧边宽度
|
||
*/
|
||
const getSidebarWidth = computed(() => {
|
||
const { isMobile, sidebarHidden, sidebarMixedWidth, sidebarWidth } = props;
|
||
let width = 0;
|
||
|
||
if (sidebarHidden) {
|
||
return width;
|
||
}
|
||
|
||
if (
|
||
!sidebarEnableState.value ||
|
||
(sidebarHidden &&
|
||
!isSidebarMixedNav.value &&
|
||
!isMixedNav.value &&
|
||
!isHeaderMixedNav.value)
|
||
) {
|
||
return width;
|
||
}
|
||
|
||
if ((isHeaderMixedNav.value || isSidebarMixedNav.value) && !isMobile) {
|
||
width = sidebarMixedWidth;
|
||
} else if (sidebarCollapse.value) {
|
||
width = isMobile ? 0 : getSideCollapseWidth.value;
|
||
} else {
|
||
width = sidebarWidth;
|
||
}
|
||
return width;
|
||
});
|
||
|
||
/**
|
||
* 获取扩展区域宽度
|
||
*/
|
||
const sidebarExtraWidth = computed(() => {
|
||
const { sidebarExtraCollapsedWidth, sidebarWidth } = props;
|
||
|
||
return sidebarExtraCollapse.value ? sidebarExtraCollapsedWidth : sidebarWidth;
|
||
});
|
||
|
||
/**
|
||
* 是否侧边栏模式,包含混合侧边
|
||
*/
|
||
const isSideMode = computed(
|
||
() =>
|
||
currentLayout.value === 'mixed-nav' ||
|
||
currentLayout.value === 'sidebar-mixed-nav' ||
|
||
currentLayout.value === 'sidebar-nav' ||
|
||
currentLayout.value === 'header-mixed-nav' ||
|
||
currentLayout.value === 'header-sidebar-nav',
|
||
);
|
||
|
||
/**
|
||
* header fixed值
|
||
*/
|
||
const headerFixed = computed(() => {
|
||
const { headerMode } = props;
|
||
return (
|
||
isMixedNav.value ||
|
||
headerMode === 'fixed' ||
|
||
headerMode === 'auto-scroll' ||
|
||
headerMode === 'auto'
|
||
);
|
||
});
|
||
|
||
const showSidebar = computed(() => {
|
||
return isSideMode.value && sidebarEnable.value && !props.sidebarHidden;
|
||
});
|
||
|
||
/**
|
||
* 遮罩可见性
|
||
*/
|
||
const maskVisible = computed(() => !sidebarCollapse.value && props.isMobile);
|
||
|
||
const mainStyle = computed(() => {
|
||
let width = '100%';
|
||
let sidebarAndExtraWidth = 'unset';
|
||
if (
|
||
headerFixed.value &&
|
||
currentLayout.value !== 'header-nav' &&
|
||
currentLayout.value !== 'mixed-nav' &&
|
||
currentLayout.value !== 'header-sidebar-nav' &&
|
||
showSidebar.value &&
|
||
!props.isMobile
|
||
) {
|
||
// fixed模式下生效
|
||
const isSideNavEffective =
|
||
(isSidebarMixedNav.value || isHeaderMixedNav.value) &&
|
||
sidebarExpandOnHover.value &&
|
||
sidebarExtraVisible.value;
|
||
|
||
if (isSideNavEffective) {
|
||
const sideCollapseWidth = sidebarCollapse.value
|
||
? getSideCollapseWidth.value
|
||
: props.sidebarMixedWidth;
|
||
const sideWidth = sidebarExtraCollapse.value
|
||
? props.sidebarExtraCollapsedWidth
|
||
: props.sidebarWidth;
|
||
|
||
// 100% - 侧边菜单混合宽度 - 菜单宽度
|
||
sidebarAndExtraWidth = `${sideCollapseWidth + sideWidth}px`;
|
||
width = `calc(100% - ${sidebarAndExtraWidth})`;
|
||
} else {
|
||
sidebarAndExtraWidth =
|
||
sidebarExpandOnHovering.value && !sidebarExpandOnHover.value
|
||
? `${getSideCollapseWidth.value}px`
|
||
: `${getSidebarWidth.value}px`;
|
||
width = `calc(100% - ${sidebarAndExtraWidth})`;
|
||
}
|
||
}
|
||
return {
|
||
sidebarAndExtraWidth,
|
||
width,
|
||
};
|
||
});
|
||
|
||
// 计算 tabbar 的样式
|
||
const tabbarStyle = computed((): CSSProperties => {
|
||
let width = '';
|
||
let marginLeft = 0;
|
||
|
||
// 如果不是混合导航,tabbar 的宽度为 100%
|
||
if (!isMixedNav.value || props.sidebarHidden) {
|
||
width = '100%';
|
||
} else if (sidebarEnable.value) {
|
||
// 鼠标在侧边栏上时,且侧边栏展开时的宽度
|
||
const onHoveringWidth = sidebarExpandOnHover.value
|
||
? props.sidebarWidth
|
||
: getSideCollapseWidth.value;
|
||
|
||
// 设置 marginLeft,根据侧边栏是否折叠来决定
|
||
marginLeft = sidebarCollapse.value
|
||
? getSideCollapseWidth.value
|
||
: onHoveringWidth;
|
||
|
||
// 设置 tabbar 的宽度,计算方式为 100% 减去侧边栏的宽度
|
||
width = `calc(100% - ${sidebarCollapse.value ? getSidebarWidth.value : onHoveringWidth}px)`;
|
||
} else {
|
||
// 默认情况下,tabbar 的宽度为 100%
|
||
width = '100%';
|
||
}
|
||
|
||
return {
|
||
marginLeft: `${marginLeft}px`,
|
||
width,
|
||
};
|
||
});
|
||
|
||
const contentStyle = computed((): CSSProperties => {
|
||
const fixed = headerFixed.value;
|
||
|
||
const { footerEnable, footerFixed, footerHeight } = props;
|
||
return {
|
||
marginTop:
|
||
fixed &&
|
||
!isFullContent.value &&
|
||
!headerIsHidden.value &&
|
||
(!isHeaderAutoMode.value || scrollY.value < headerWrapperHeight.value)
|
||
? `${headerWrapperHeight.value}px`
|
||
: 0,
|
||
paddingBottom: `${footerEnable && footerFixed ? footerHeight : 0}px`,
|
||
};
|
||
});
|
||
|
||
const headerZIndex = computed(() => {
|
||
const { zIndex } = props;
|
||
const offset = isMixedNav.value ? 1 : 0;
|
||
return zIndex + offset;
|
||
});
|
||
|
||
const headerWrapperStyle = computed((): CSSProperties => {
|
||
const fixed = headerFixed.value;
|
||
return {
|
||
height: isFullContent.value ? '0' : `${headerWrapperHeight.value}px`,
|
||
left: isMixedNav.value ? 0 : mainStyle.value.sidebarAndExtraWidth,
|
||
position: fixed ? 'fixed' : 'static',
|
||
top:
|
||
headerIsHidden.value || isFullContent.value
|
||
? `-${headerWrapperHeight.value}px`
|
||
: 0,
|
||
width: mainStyle.value.width,
|
||
'z-index': headerZIndex.value,
|
||
};
|
||
});
|
||
|
||
/**
|
||
* 侧边栏z-index
|
||
*/
|
||
const sidebarZIndex = computed(() => {
|
||
const { isMobile, zIndex } = props;
|
||
let offset = isMobile || isSideMode.value ? 1 : -1;
|
||
|
||
if (isMixedNav.value) {
|
||
offset += 1;
|
||
}
|
||
|
||
return zIndex + offset;
|
||
});
|
||
|
||
const footerWidth = computed(() => {
|
||
if (!props.footerFixed) {
|
||
return '100%';
|
||
}
|
||
|
||
return mainStyle.value.width;
|
||
});
|
||
|
||
const maskStyle = computed((): CSSProperties => {
|
||
return { zIndex: props.zIndex };
|
||
});
|
||
|
||
const showHeaderToggleButton = computed(() => {
|
||
return (
|
||
props.isMobile ||
|
||
(props.headerToggleSidebarButton &&
|
||
isSideMode.value &&
|
||
!isSidebarMixedNav.value &&
|
||
!isMixedNav.value &&
|
||
!props.isMobile)
|
||
);
|
||
});
|
||
|
||
const showHeaderLogo = computed(() => {
|
||
return !isSideMode.value || isMixedNav.value || props.isMobile;
|
||
});
|
||
|
||
watch(
|
||
() => props.isMobile,
|
||
(val) => {
|
||
if (val) {
|
||
sidebarCollapse.value = true;
|
||
}
|
||
},
|
||
{
|
||
immediate: true,
|
||
},
|
||
);
|
||
|
||
watch(
|
||
[() => headerWrapperHeight.value, () => isFullContent.value],
|
||
([height]) => {
|
||
setLayoutHeaderHeight(isFullContent.value ? 0 : height);
|
||
},
|
||
{
|
||
immediate: true,
|
||
},
|
||
);
|
||
|
||
watch(
|
||
() => props.footerHeight,
|
||
(height: number) => {
|
||
setLayoutFooterHeight(height);
|
||
},
|
||
{
|
||
immediate: true,
|
||
},
|
||
);
|
||
|
||
{
|
||
const mouseMove = () => {
|
||
mouseY.value > headerWrapperHeight.value
|
||
? (headerIsHidden.value = true)
|
||
: (headerIsHidden.value = false);
|
||
};
|
||
watch(
|
||
[() => props.headerMode, () => mouseY.value],
|
||
() => {
|
||
if (!isHeaderAutoMode.value || isMixedNav.value || isFullContent.value) {
|
||
if (props.headerMode !== 'auto-scroll') {
|
||
headerIsHidden.value = false;
|
||
}
|
||
return;
|
||
}
|
||
headerIsHidden.value = true;
|
||
mouseMove();
|
||
},
|
||
{
|
||
immediate: true,
|
||
},
|
||
);
|
||
}
|
||
|
||
{
|
||
const checkHeaderIsHidden = useThrottleFn((top, bottom, topArrived) => {
|
||
if (scrollY.value < headerWrapperHeight.value) {
|
||
headerIsHidden.value = false;
|
||
return;
|
||
}
|
||
if (topArrived) {
|
||
headerIsHidden.value = false;
|
||
return;
|
||
}
|
||
|
||
if (top) {
|
||
headerIsHidden.value = false;
|
||
} else if (bottom) {
|
||
headerIsHidden.value = true;
|
||
}
|
||
}, 300);
|
||
|
||
watch(
|
||
() => scrollY.value,
|
||
() => {
|
||
if (
|
||
props.headerMode !== 'auto-scroll' ||
|
||
isMixedNav.value ||
|
||
isFullContent.value
|
||
) {
|
||
return;
|
||
}
|
||
if (isScrolling.value) {
|
||
checkHeaderIsHidden(
|
||
directions.top,
|
||
directions.bottom,
|
||
arrivedState.top,
|
||
);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
function handleClickMask() {
|
||
sidebarCollapse.value = true;
|
||
}
|
||
|
||
function handleHeaderToggle() {
|
||
if (props.isMobile) {
|
||
sidebarCollapse.value = false;
|
||
} else {
|
||
emit('toggleSidebar');
|
||
}
|
||
}
|
||
|
||
const idMainContent = ELEMENT_ID_MAIN_CONTENT;
|
||
</script>
|
||
|
||
<template>
|
||
<div class="relative flex min-h-full w-full">
|
||
<LayoutSidebar
|
||
v-if="sidebarEnableState"
|
||
v-model:collapse="sidebarCollapse"
|
||
v-model:expand-on-hover="sidebarExpandOnHover"
|
||
v-model:expand-on-hovering="sidebarExpandOnHovering"
|
||
v-model:extra-collapse="sidebarExtraCollapse"
|
||
v-model:extra-visible="sidebarExtraVisible"
|
||
:collapse-width="getSideCollapseWidth"
|
||
:dom-visible="!isMobile"
|
||
:extra-width="sidebarExtraWidth"
|
||
:fixed-extra="sidebarExpandOnHover"
|
||
:header-height="isMixedNav ? 0 : headerHeight"
|
||
:is-sidebar-mixed="isSidebarMixedNav || isHeaderMixedNav"
|
||
:margin-top="sidebarMarginTop"
|
||
:mixed-width="sidebarMixedWidth"
|
||
:show="showSidebar"
|
||
:theme="sidebarTheme"
|
||
:width="getSidebarWidth"
|
||
:z-index="sidebarZIndex"
|
||
@leave="() => emit('sideMouseLeave')"
|
||
>
|
||
<template v-if="isSideMode && !isMixedNav" #logo>
|
||
<slot name="logo"></slot>
|
||
</template>
|
||
|
||
<template v-if="isSidebarMixedNav || isHeaderMixedNav">
|
||
<slot name="mixed-menu"></slot>
|
||
</template>
|
||
<template v-else>
|
||
<slot name="menu"></slot>
|
||
</template>
|
||
|
||
<template #extra>
|
||
<slot name="side-extra"></slot>
|
||
</template>
|
||
<template #extra-title>
|
||
<slot name="side-extra-title"></slot>
|
||
</template>
|
||
</LayoutSidebar>
|
||
|
||
<div
|
||
ref="contentRef"
|
||
class="flex flex-1 flex-col overflow-hidden transition-all duration-300 ease-in"
|
||
>
|
||
<div
|
||
:class="[
|
||
{
|
||
'shadow-[0_16px_24px_hsl(var(--background))]': scrollY > 20,
|
||
},
|
||
SCROLL_FIXED_CLASS,
|
||
]"
|
||
:style="headerWrapperStyle"
|
||
class="overflow-hidden transition-all duration-200"
|
||
>
|
||
<LayoutHeader
|
||
v-if="headerVisible"
|
||
:full-width="!isSideMode"
|
||
:height="headerHeight"
|
||
:is-mobile="isMobile"
|
||
:show="!isFullContent && !headerHidden"
|
||
:sidebar-width="sidebarWidth"
|
||
:theme="headerTheme"
|
||
:width="mainStyle.width"
|
||
:z-index="headerZIndex"
|
||
>
|
||
<template v-if="showHeaderLogo" #logo>
|
||
<slot name="logo"></slot>
|
||
</template>
|
||
|
||
<template #toggle-button>
|
||
<VbenIconButton
|
||
v-if="showHeaderToggleButton"
|
||
class="my-0 mr-1 rounded-md"
|
||
@click="handleHeaderToggle"
|
||
>
|
||
<Menu class="size-4" />
|
||
</VbenIconButton>
|
||
</template>
|
||
<slot name="header"></slot>
|
||
</LayoutHeader>
|
||
|
||
<LayoutTabbar
|
||
v-if="tabbarEnable"
|
||
:height="tabbarHeight"
|
||
:style="tabbarStyle"
|
||
>
|
||
<slot name="tabbar"></slot>
|
||
</LayoutTabbar>
|
||
</div>
|
||
|
||
<!-- </div> -->
|
||
<LayoutContent
|
||
:id="idMainContent"
|
||
:content-compact="contentCompact"
|
||
:content-compact-width="contentCompactWidth"
|
||
:padding="contentPadding"
|
||
:padding-bottom="contentPaddingBottom"
|
||
:padding-left="contentPaddingLeft"
|
||
:padding-right="contentPaddingRight"
|
||
:padding-top="contentPaddingTop"
|
||
:style="contentStyle"
|
||
class="transition-[margin-top] duration-200"
|
||
>
|
||
<slot name="content"></slot>
|
||
|
||
<template #overlay>
|
||
<slot name="content-overlay"></slot>
|
||
</template>
|
||
</LayoutContent>
|
||
|
||
<LayoutFooter
|
||
v-if="footerEnable"
|
||
:fixed="footerFixed"
|
||
:height="footerHeight"
|
||
:show="!isFullContent"
|
||
:width="footerWidth"
|
||
:z-index="zIndex"
|
||
>
|
||
<slot name="footer"></slot>
|
||
</LayoutFooter>
|
||
</div>
|
||
<slot name="extra"></slot>
|
||
<div
|
||
v-if="maskVisible"
|
||
:style="maskStyle"
|
||
class="bg-overlay fixed left-0 top-0 h-full w-full transition-[background-color] duration-200"
|
||
@click="handleClickMask"
|
||
></div>
|
||
</div>
|
||
</template>
|