当前位置: 首页 > news >正文

基于cornerstone3D的dicom影像浏览器 第二十九章 自定义菜单组件

文章目录

  • 前言
  • 一、程序结构
    • 1. 菜单数据结构
    • 2. XMenu.vue
    • 3. XSubMenu.vue
    • 4. XSubMenuSlot.vue
    • 5. XMenuItem.vue
  • 二、调用流程
  • 总结


前言

菜单用于组织程序功能,为用户提供导航。是用户与程序交互非常重要的接口。
开源组件库像Element Plus和Ant Design中都提供了功能强大,使用方便的菜单组件 。
本章提供一个自定义菜单组件,核心思想是调用者提供菜单数据和显示位置,就能在指定位置显示出菜单组件。
效果如下:
在这里插入图片描述


一、程序结构

一共四个组件:
XMenu.vue 主菜单
XSubMenu.vue 递归子菜单
XSubMenuSlot.vue 子菜单与菜单项插槽
XMenuItem.vue 菜单项

1. 菜单数据结构

id: String,必填
text: String,菜单项文字,必填
icon: String,菜单项最左边图标,选填
checkable: Boolean,菜单项最左边是否显示选中图标 ✓ 与check共同作用, 选填
check: Boolean,是否选中,选填
image: String,菜单项文字区图片url,选填
separate: String,菜单项分割线,值:bottom/up/both,选填
subMenu: Array,子菜单数据,选填

示例:

let menuList = ref([{id: "view",text: "视图",icon: "",checkable: true,checked: true,separate: "bottom"},{id: "edit",text: "编辑文字",icon: "icon-jawbone",},{id: "pseudo",text: "伪彩1",icon: "",image: require("@/assets/images/hot_h1.png"),},{id: "file",text: "文件",icon: "",subMenu: [{id: "open",text: "打开",icon: "",separate: "bottom",},{id: "save",text: "保存刚才的工作",icon: "icon-lungs-line",},{id: "close",text: "关闭",icon: "",},{id: "menuitem1",text: "菜单项1",icon: "",},{id: "menuitem2",text: "菜单项2",icon: "",},{id: "menuitem3",text: "菜单项3",icon: "",},],},]);

显示效果:
在这里插入图片描述

2. XMenu.vue

用户入口

  1. 接受菜单数据和菜单位置
  2. 发送菜单项点击事件menuclick
  3. 自动计算菜单高度和宽度
  4. 菜单展开/收缩动画
<script lang="js" setup name="XMenu">
import { ref, computed, onMounted, reactive, h } from "vue";
import XSubMenu from "./XSubMenu.vue";
import XMenuItem from "./XMenuItem.vue";const emit = defineEmits(["menuclick"]);
let menuList = ref([]);const xMenu = ref(null);
const menuPos = reactive({ left: 0, top: 0 });
const ItemH = 32;
const fontSize = ref(14);
const showMenu = ref(false);const show = (menu, pos) => {menuList.value = menu;calcMenuPos(pos);showMenu.value = true;
}const calcMenuPos = (pos) => {const maxHeight = document.body.clientHeight;const maxWidth = document.body.clientWidth;if (pos.left + menuWidth.value > maxWidth) {menuPos.left = maxWidth - menuWidth.value;} else {menuPos.left = pos.left;}if (pos.top + menuHeight.value > maxHeight) {menuPos.top = pos.top2 - menuHeight.value;} else {menuPos.top = pos.top;}}const hide = () => {showMenu.value = false;
}const getRect = () => {return xMenu.value.getBoundingClientRect();
}const menuWidth = computed(() => {let w = 80;menuList.value.forEach((menu) => {const menuW = getMenuWidth(menu, fontSize.value);w = menuW > w ? menuW : w;});const maxW = document.body.clientWidth / 2;w += 40;const ret = w > maxW ? maxW : w;return ret;
});const menuHeight = computed(() => {let h = ItemH * menuList.value.length;return h;
});const menuStyle = computed(() => {return {width: menuWidth.value + "px",left: menuPos.left + "px",top: menuPos.top + "px",};
});const getMenuWidth = (menu, fontSize) => {const el = document.createElement("span");const text = menu.text;el.innerText = text;el.style.fontSize = fontSize + "px";el.style.position = "absolute";document.body.appendChild(el);let w = el.offsetWidth + 50;if (menu.image) {w += 100;}document.body.removeChild(el);return w;
}const hasChild = (menu) => {return menu.subMenu && menu.subMenu.length > 0;
}const onMenuClick = (menu) => {if (menu.checkable && menu.checked !== undefined) {menu.checked = !menu.checked;}hide();emit("menuclick", menu);
}const transBeforeEnter = (el) => {el.style.height = "0px";el.style.overflow = "hidden";
}
const transEnter = (el) => {el.style.height = "auto";const h = el.offsetHeight;el.style.height = "0px";requestAnimationFrame(() => {el.style.height = h + "px";el.style.transition = ".4s";});
}const transAfterEnter = (el) => {el.style.transition = "initial";el.style.overflow = null;
}const transBeforeLeave = (el) => {el.style.overflow = "hidden";el.style.transition = ".2s";
}
const transLeave = (el) => {el.style.height = "0px";
}
const transAfterLeave = (el) => {
}defineExpose({show,hide,getRect
});</script><template><!-- <Teleport to="body"> --><Transition@beforeEnter="transBeforeEnter"@enter="transEnter"@afterEnter="transAfterEnter"@before-leave="transBeforeLeave"@leave="transLeave"><ul class="x-menu" ref="xMenu" v-show="showMenu" :style="menuStyle"><template v-for="(item, index) in menuList"><XSubMenu v-if="hasChild(item)" :key="item.id" :menu="item" :index="index" @menuclick="onMenuClick" /><XMenuItem v-else :menu="item" @click="onMenuClick(item)" /></template></ul></Transition><!-- </Teleport> -->
</template><style lang="scss" scoped>
.x-menu {position: absolute;background-color: var(--color-theme-bg);color: var(--color-theme-text);border: 1px solid #aaa;z-index: 9999;
}
</style>

3. XSubMenu.vue

<script lang="js" setup name="XSubMenu">
import { ref, computed, onMounted } from "vue";
import XSubMenuSlot from "./XSubMenuSlot.vue";
import XMenuItem from "./XMenuItem.vue";const emit = defineEmits(["menuclick"]);const props = defineProps({menu: {type: Object,required: true},index: {type: Number,required: true}
});const hasChild = (menu) => {return menu.subMenu && menu.subMenu.length > 0;
};const onMenuClick = (menu) => {emit("menuclick", menu);
};
</script><template><XSubMenuSlot :menu="menu" :index="index"><template #title>{{ menu.text }}</template><ul><template v-for="(item, index) in menu.subMenu"><XSubMenu v-if="hasChild(item)" :key="item.id" :menu="item" @menuclick="onMenuClick" /><XMenuItem v-else :key="item.id" :menu="item" @click="onMenuClick(item)" /></template></ul></XSubMenuSlot>
</template><style lang="scss" scoped></style>

4. XSubMenuSlot.vue

两个插槽,分别显示XMenuItem、XSubMenu
与XMenu类似:

  1. 计算子菜单宽度和高度
  2. 计算子菜单显示位置
  3. 子菜单展开/收缩动画
<script lang="js" setup name="XSubMenuSlot">
import { ref, computed, onMounted, reactive } from "vue";const props = defineProps({menu: {type: Object,required: true},index: {type: Number,required: true}
});const ItemH = 32;
const showMenu = ref(false);
const subMenuPos = reactive({left: 0,top: 0
});const fontSize = ref(14);
const subMenuWidth = computed(() => {let w = 80;props.menu.subMenu.forEach((item) => {const menuW = getMenuWidth(item, fontSize.value);w = menuW > w ? menuW : w;});const maxW = document.body.clientWidth / 2;// menu sidebar-icon width: 40pxw += 40;const ret = w > maxW ? maxW : w;console.log('menuWidth', ret);return ret;
});const subMenuHeight = computed(() => {const height = ItemH * props.menu.subMenu.length;return height;
});const getMenuWidth = (menu, fontSize) => {const el = document.createElement("span");const text = menu.text;el.innerText = text;el.style.fontSize = fontSize + "px";el.style.position = "absolute";document.body.appendChild(el);let w = el.offsetWidth + 50;if (menu.image) {w += 100;}document.body.removeChild(el);return w;
}const subitemStyle = computed(() => {if (props.menu.separate) {const border = {};switch (props.menu.separate) {case "top":border.borderTop = "1px solid #aaa";break;case "bottom":border.borderBottom = "1px solid #aaa";break;case "both":border.borderTop = "1px solid #aaa";border.borderBottom = "1px solid #aaa";break;}return border;}
});const subMenuStyle = computed(() => {return {left: subMenuPos.left + "px",top: subMenuPos.top + "px",width: subMenuWidth.value + "px"}
});const show = (e) => {showMenu.value = true;const el = e.currentTarget;calcSubMenuPos(el);
}const calcSubMenuPos = (el) => {const maxWidth = document.body.clientWidth;const maxHeight = document.body.clientHeight;const rect = el.getBoundingClientRect();const xEnd = rect.right + subMenuWidth.value;const yEnd = rect.y + props.index*ItemH + subMenuHeight.value;if (xEnd > maxWidth) {subMenuPos.left = 0 - subMenuWidth.value;} else {subMenuPos.left = rect.width;}if (yEnd > maxHeight) {subMenuPos.top = ItemH - subMenuHeight.value;} else {subMenuPos.top = 0;}}const hide = (e) => {showMenu.value = false;
}const transBeforeEnter = (el) => {el.style.height = "0px";el.style.overflow = "hidden";
}
const transEnter = (el) => {el.style.height = "auto";const h = el.offsetHeight;el.style.height = "0px";requestAnimationFrame(() => {el.style.height = h + "px";el.style.transition = ".4s";});
}const transAfterEnter = (el) => {el.style.transition = "initial";el.style.overflow = null;
}const transBeforeLeave = (el) => {el.style.overflow = "hidden";el.style.transition = ".2s";
}
const transLeave = (el) => {el.style.height = "0px";
}
const transAfterLeave = (el) => {
}
</script><template><div class="container" @mouseenter="show" @mouseleave="hide"><li class="subitem" :style="subitemStyle"><span class="subitem-bar"></span><span class="subitem-text"><slot name="title"></slot></span><div class="subitem-right"><span class="subitem-right-icon"></span></div></li><Transition@beforeEnter="transBeforeEnter"@enter="transEnter"@afterEnter="transAfterEnter"@before-leave="transBeforeLeave"@leave="transLeave"><div class="submenu" v-show="showMenu" :style="subMenuStyle"><slot></slot></div></Transition></div>
</template><style lang="scss" scoped>
.container {position: relative;
}
.submenu {position: absolute;border: 1px solid #aaa;
}.subitem {display: flex;flex-direction: row;width: 100%;height: 32px;background-color: #fdfdfd;background-color: var(--color-menu-bg);z-index: 9999;overflow: hidden;
}.subitem-bar {width: 38px;height: 100%;background-color: var(--color-menu-bar);fill: var(--color-theme-text);
}.subitem-text {flex: 1;height: 32px;line-height: 32px;font-size: 16px;padding-left: 10px;cursor: default;user-select: none;-webkit-user-select: none; /* Safari */-moz-user-select: none; /* Firefox */-ms-user-select: none; /* IE 10 and IE 11 */
}.subitem-right {width: 30px;height: 100%;padding-right: 6px;
}.subitem-right-icon {float: right;width: 16px;height: 32px;background: url(../../assets/images/arrow_right.png) no-repeat center center;background-size: 16px auto;
}.subitem:hover {background-color: lightblue;color: blue;border: 1px solid lightskyblue;
}
</style>

5. XMenuItem.vue

菜单项,显示icon, text, image, separate

<script lang="js" setup name="XMenuItem">
import { ref, computed, onMounted } from "vue";
import SvgIcon from "../SvgIcon.vue";const props = defineProps({menu: {type: Object,required: true}
});const emit = defineEmits(["menuclick"]);const menuItemStyle = computed(() => {if (props.menu.separate) {const border = {};switch (props.menu.separate) {case "top":border.borderTop = "1px solid #aaa";break;case "bottom":border.borderBottom = "1px solid #aaa";break;case "both":border.borderTop = "1px solid #aaa";border.borderBottom = "1px solid #aaa";break;}return border;}
});const iconStyle = computed(() => {if (props.menu.checkable) {return {backgroundImage: props.menu.checked? `url("src/assets/images/choose.png")`: "",};} else {return {backgroundImage: props.menu.icon,};}
});
</script><template><li class="menuitem" :style="menuItemStyle"><div class="menuitem-bar"><svg-icon v-if="menu.icon" class="menuitem-bar__icon" :icon="menu.icon" size="28px" /><div v-else class="menuitem-bar__icon" :style="iconStyle"></div></div><div class="menuitem-text">{{ menu.text }}</div><img v-if="menu.image" class="menuitem-image" :src="menu.image" /></li>
</template><style lang="scss" scoped>
.menuitem {display: flex;flex-direction: row;align-items: center;height: 32px;background-color: var(--color-menu-bg);z-index: 9999;overflow: hidden;
}.menuitem-bar {display: flex;flex-direction: row;align-items: center;justify-content: center;width: 38px;height: 100%;background-color: var(--color-menu-bar);
}.menuitem-bar__icon {width: 38px;height: 100%;background-repeat: no-repeat;background-position: center center;background-size: 24px auto;fill: var(--color-theme-text);
}.menuitem-text {line-height: 32px;font-size: 16px;padding-left: 10px;cursor: default;user-select: none;-webkit-user-select: none; /* Safari */-moz-user-select: none; /* Firefox */-ms-user-select: none; /* IE 10 and IE 11 */
}.menuitem-image {margin-left: 8px;
}.menuitem:hover {background-color: lightblue;color: blue;border: 1px solid lightskyblue;
}
</style>

二、调用流程

  1. 导入XMenu.vue
  2. 在模板中添加XMenu
  3. 绑定变量
  4. 定义菜单数据
  5. 调用show函数
import XMenu from "./XMenu/XMenu.vue";const xMenu = ref(null);const menuList = ref([{id: "view",text: "视图",icon: "",checkable: true,checked: true,separate: "bottom"},{id: "edit",text: "编辑文字",icon: "icon-jawbone",},{id: "pseudo",text: "伪彩1",icon: "",image: require("@/assets/images/hot_h1.png"),},{id: "file",text: "文件",icon: "",subMenu: [{id: "open",text: "打开",icon: "",separate: "bottom",},{id: "save",text: "保存刚才的工作",icon: "icon-lungs-line",},{id: "close",text: "关闭",icon: "",},{id: "menuitem1",text: "菜单项1",icon: "",},{id: "menuitem2",text: "菜单项2",icon: "",},{id: "menuitem3",text: "菜单项3",icon: "",},],},]);const showMenu = (e) => {xMenu.value.show(menuList.value, { left: e.clientX, top: e.clientY})
}const hideMenu = () => {xMenu.value.hide();
}<template><div class="toolbar"><XMenu ref="xMenu" @mouseleave="hideMenu" />...<div class="toolbar-row"><el-button @click="showMenu">菜单2</el-button><el-button @click="dcmtag">DICOM标签</el-button><el-button @click="mprvr">MPR+VR</el-button><el-button @click="showMenu">菜单3</el-button></div>...</div>
</template>

总结

本章实现自定义菜单组件,支持图标、分隔线、选中图标、文字区图片、子菜单、展开/收缩动画。如需要显示更复杂内容,可自行扩展XMenuItem.vue
调用方便,只需要提供菜单数据和菜单显示位置。

相关文章:

  • 安装VUE客户端@vue/cli报错警告npm WARN deprecated解决方法 无法将“vue”项识别为 cmdlet、函数
  • 机器学习框架PyTorch
  • 装饰模式(Decorator Pattern)重构java邮件发奖系统实战
  • 知识图谱技术概述
  • RetroMAE 预训练任务
  • ant-design4.xx实现数字输入框; 某些输入法数字需要连续输入两次才显示
  • JS实现OSS断点续传
  • 实战设计模式之模板方法模式
  • 手机号段数据库的作用
  • MySQL 索引优化(Explain执行计划) 详细讲解
  • 【Oracle APEX开发小技巧12】
  • Elasticsearch集群手动分片分配指南:原理与实践
  • 大模型在脑梗塞后遗症风险预测及治疗方案制定中的应用研究
  • Codeforces EDU Round 179 A~D
  • 仿真每日一练 | ABAQUS连接单元的应用——螺栓预紧力
  • 关于Web安全:8. Web 攻击流量分析与自动化
  • 学习笔记(26):线性代数-张量的降维求和,简单示例
  • Halcon透视矩阵
  • 学习笔记(25):线性代数,矩阵-矩阵乘法原理
  • 【Android】Android Studio项目代码异常错乱问题处理(2020.3版本)
  • 国外b2b网站是什么意思/高端网站定制
  • 十大免费ppt网站流氓下载/注册网站怎么注册
  • o2o平台有哪些网站/网站优化策划书
  • wordpress主题几个网站/品牌营销策略分析论文
  • 做好的网站启用/短视频培训学校
  • 邹城做网站/网站域名查询官网