improve: 菜单及样式优化

This commit is contained in:
From-wh 2023-02-23 19:51:59 +08:00
parent 7d43be051e
commit efddcd0d0c
10 changed files with 498 additions and 150 deletions

View File

@ -22,6 +22,7 @@
<style>
.search .ivu-select-selection {
margin-right: 20px;
border-radius: 30px;
}
.search .ivu-select-visible .ivu-select-selection {
box-shadow: unset !important;
@ -33,6 +34,9 @@
.search .select .ivu-select-item {
font-size: 14px !important;
}
.ivu-select-input{
background-color: rgba(255,255,255,0.3);
}
</style>
<script>
import { menusListApi } from '@/api/account';

View File

@ -66,7 +66,6 @@ export default {
},
methods: {
handleClick(name) {
console.log(name, this.activeMenuPath);
this.$emit('on-click', name, this.activeMenuPath);
},
handleMousemove(event, children) {
@ -94,4 +93,7 @@ export default {
justify-content space-between
width 100%
}
.drop-menu-a /deep/ .ivu-dropdown-rel {
min-width: 75px !important;
}
</style>

View File

@ -6,7 +6,7 @@
<div class="parent-menu">
<Menu
ref="menu"
:active-name="activeMenuPath"
:active-name="headerName"
:open-names="openedNames"
:accordion="accordion"
:theme="theme"
@ -14,7 +14,7 @@
@on-open-change="openNameData"
@on-select="handleSelect"
>
<template v-for="item in menuList">
<template v-for="item in header">
<template>
<menu-item :name="item.path" :key="`menu${item.path}`"
><common-icon :type="item.icon || ''" /><span class="title">{{ item.title }}</span></menu-item
@ -23,20 +23,19 @@
</template>
</Menu>
</div>
<div class="child-menu" v-if="childList.length">
<div class="cat-name">{{ catName }}</div>
<div class="child-menu" v-show="sider.length">
<div class="cat-name">{{ oneMenuName }}</div>
<Menu
ref="childMenu"
:active-name="activePath"
:open-names="openMenus"
:open-names="openNames"
:accordion="accordion"
:theme="theme"
width="145px"
@on-open-change="openChildNameData"
@on-select="handleChildSelect"
>
<template v-for="item in childList">
<template v-for="item in sider">
<template v-if="item.auth === undefined">
<template v-if="item.children && item.children.length >= 1">
<side-menu-item
@ -72,7 +71,7 @@
v-if="item.children && item.children.length > 0"
@on-click="collHandleSelect"
:hide-title="true"
:activeMenuPath="activeMenuPath"
:activeMenuPath="headerName"
:root-icon-size="rootIconSize"
:icon-size="iconSize"
:theme="theme"
@ -83,7 +82,7 @@
<a
@click="collHandleSelect(item)"
class="drop-menu-a"
:class="{ on: item.path == activeMenuPath }"
:class="{ on: item.path == headerName }"
:style="{ textAlign: 'center' }"
><common-icon :color="textColor" :type="item.icon || (item.children && item.children[0].icon)" />
<span class="title">{{ item.title }}</span>
@ -97,7 +96,7 @@
import SideMenuItem from './side-menu-item.vue';
import CollapsedMenu from './collapsed-menu.vue';
import { getUnion } from '@/libs/tools';
import { mapState } from 'vuex';
import { mapState, mapGetters } from 'vuex';
import mixin from './mixin';
import itemMixin from './item-mixin';
import { setCookies } from '@/libs/util';
@ -134,10 +133,10 @@ export default {
default: 16,
},
accordion: Boolean,
openNames: {
type: Array,
default: () => [],
},
// openNames: {
// type: Array,
// default: () => [],
// },
},
data() {
return {
@ -145,74 +144,49 @@ export default {
childList: [],
activeChildName: '',
childOptions: [],
activePath: '',
// activePath: '',
activeMenuPath: '',
catName: '',
};
},
computed: {
...mapState('menus', ['openMenus']),
...mapState('menu', ['activePath', 'openNames', 'header', 'headerName', 'sider', 'oneMenuName']),
...mapGetters('menu', ['filterSider']),
textColor() {
return this.theme === 'dark' ? '#fff' : '#495060';
},
},
watch: {
activeName(name) {
if (this.accordion) this.openedNames = this.getOpenedNamesByActiveName();
else this.openedNames = getUnion(this.openedNames, this.getOpenedNamesByActiveName());
// this.handleSelect(this.activeName);
},
openNames(newNames) {
this.openedNames = newNames;
},
openedNames() {
this.$nextTick(() => {
this.$refs.menu.updateOpened();
this.$refs.childMenu.updateActiveName();
});
},
oneMenuName() {
this.$nextTick(() => {});
},
activePath() {
this.$nextTick(() => {
console.log();
this.$refs.childMenu.updateOpened();
this.$refs.childMenu.updateActiveName();
});
},
collapsed(val) {
if (!val) {
console.log(this.$route, this.findParentById(this.menuList, this.$route.path));
this.handleSelect(this.findParentById(this.menuList, this.$route.path));
this.$nextTick(() => {
this.$refs.menu.updateOpened();
this.$refs.childMenu.updateActiveName();
this.$refs.childMenu.updateOpened();
});
}
},
$route(newRoute) {},
$route: {
handler(newRoute) {
console.log(newRoute, 'newRoutenewRoute');
this.activePath = newRoute.path;
// this.activeMenuPath = newRoute.matched[0].path;
if (this.collapsed) {
sessionStorage.setItem('menuActive', newRoute.matched[0].path);
this.activeMenuPath = newRoute.matched[0].path;
}
this.handleUpdateMenuState();
},
immediate: true,
},
},
mounted() {
console.log(this.menuList);
this.openedNames = getUnion(this.openedNames, this.getOpenedNamesByActiveName());
if (sessionStorage.getItem('menuActive')) {
this.activeMenuPath = sessionStorage.getItem('menuActive');
this.catName = sessionStorage.getItem('menuActiveTitle');
this.getChildrenList(sessionStorage.getItem('menuActive'));
} else {
this.handleSelect(this.openedNames[0]);
}
},
mounted() {},
methods: {
getChildrenList(path) {
this.menuList.map((e) => {
if (e.path === path) {
this.childList = e.children || [];
}
});
},
handleCollpasedChange(state) {
console.log(state);
// this.collapsed = state;
@ -220,90 +194,35 @@ export default {
// setCookies('collapsed', state);
},
handleSelect(name, type) {
this.childOptions = [];
console.log(name, 'name');
this.menuList.map((e) => {
if (e.path === name) {
if (e.children && e.children.length) {
this.jump(e.children);
this.catName = e.title;
// this.activeMenuPath = e.path;
this.childList = e.children || [];
sessionStorage.setItem('menuActive', e.path);
sessionStorage.setItem('menuActiveTitle', e.title);
this.activeMenuPath = e.path;
// this.activeChildName = e.children[0].path;
// this.childOptions = [e.children[0].path];
this.$store.commit('menus/childMenuList', this.childList);
// if (!type) {
// this.$emit('on-select', e.children[0].path);
// }
} else {
if (!type) {
this.$emit('on-select', name);
}
this.activeMenuPath = e.path;
this.childList = [];
this.$store.commit('menus/childMenuList', []);
// if (!type) {
// this.$emit('on-select', name);
// }
}
}
});
},
handleChildSelect(name) {
this.turnToPage(name);
},
jump(data) {
if (data[0].children && data[0].children.length) {
this.jump(data[0].children);
} else {
this.catName = data[0].title;
// this.activeMenuPath = data[0].path;
this.activeChildName = data[0].path;
this.childOptions = [data[0].path];
this.$store.commit('menus/childMenuList', this.childList);
this.$emit('on-select', data[0].path);
console.log(data[0].path, 'data[0].path');
this.turnToPage(data[0].path);
}
},
handleChildSelect(name) {
this.turnToPage(name);
},
collHandleSelect(name) {
console.log(this.menuList)
this.activeMenuPath = this.findParentById(this.menuList, name);
this.turnToPage(name);
},
findParentById(arr, path) {
var parentId = '',
hasParentId = (function loop(arr) {
return arr.some((item) => {
if (item.path === path) {
return true;
} else if (Array.isArray(item.children)) {
parentId = item.path;
return loop(item.children);
} else {
return false;
}
});
})(arr);
return hasParentId ? parentId : '未找到对应父元素';
},
// findParentsById(arr, id) {
// var parentIds = [],
// index = 0,
// hasParentId = (function loop(arr, index) {
// return arr.some((item) => {
// if (item.MENU_ID === id) {
// parentIds = parentIds.slice(0, index);
// return true;
// } else if (Array.isArray(item.MENU_INFO)) {
// parentIds[index] = item.MENU_ID;
// return loop(item.MENU_INFO, index + 1);
// } else {
// return false;
// }
// });
// })(arr, index);
// return hasParentId ? parentIds : [];
// },
collHandleSelect(name) {
this.turnToPage(name);
},
turnToPage(route, all) {
let { path, name, params, query } = {};
if (typeof route === 'string' && !all) path = route;
@ -321,13 +240,6 @@ export default {
query,
});
},
getOpenedNamesByActiveName() {
return this.$route.matched.map((item) => item.path).filter((item) => item !== name);
},
updateOpenName(name) {
if (name === this.$config.homeName) this.openedNames = [];
else this.openedNames = this.getOpenedNamesByActiveName();
},
openNameData(n) {
// this.openedNames = n
// this.$store.commit('menus/getopenMenus', n)
@ -335,14 +247,6 @@ export default {
openChildNameData(e) {
console.log(e);
},
handleUpdateMenuState() {
this.$nextTick(() => {
if (this.$refs.childMenu) {
this.$refs.childMenu.updateActiveName();
if (this.accordion) this.$refs.childMenu.updateOpened();
}
});
},
},
};
</script>

View File

@ -24,7 +24,7 @@
<Sider
hide-trigger
collapsible
:width="childMenuList.length ? 220 : 90"
:width="sider.length ? 220 : 90"
:collapsed-width="isMobile ? 0 : 90"
v-model="collapsed"
:style="{ overflow: 'hidden' }"
@ -52,13 +52,13 @@
<router-view v-if="reload" style="min-height: 600px" />
</keep-alive> -->
<keep-alive>
<router-view v-if="$route.meta.keepAlive && reload" style="min-height: 600px"></router-view>
<router-view v-if="$route.meta.keepAlive && reload" class="main-warper"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive && reload" style="min-height: 600px"></router-view>
<router-view v-if="!$route.meta.keepAlive && reload" class="main-warper"></router-view>
<!-- <router-view v-if="reload" style="min-height: 600px" /> -->
<!--<ABackTop :height="100" :bottom="80" :right="50" container=".content-wrapper"></ABackTop>-->
<ABackTop :height="100" :bottom="80" :right="50" container=".content-wrapper"></ABackTop>
<i-copyright v-if="!headMenuNoShow" />
</Content>
<i-copyright v-if="!headMenuNoShow" />
</Layout>
</Content>
</Layout>
@ -118,9 +118,14 @@ export default {
headMenuNoShow: false,
};
},
watch: {
sider(val) {
console.log(val);
},
},
computed: {
...mapState('menus', ['childMenuList']),
...mapGetters(['errorCount']),
...mapState('menu', ['sider']),
...mapState('media', ['isMobile']),
tagNavList() {
return this.$store.state.app.tagNavList;
@ -313,4 +318,7 @@ export default {
width: 800px;
}
}
.main-warper{
min-height: calc(~'100vh - 166px');
}
</style>

View File

@ -0,0 +1,224 @@
/**
* 系统内置方法集,正常情况下您不应该修改或移除此文件
* */
import { cloneDeep } from 'lodash';
/**
* @description 根据当前路由,找打顶部菜单名称
* @param {String} currentPath 当前路径
* @param {Array} menuList 所有路径
* */
function getHeaderName(to, menuList) {
const allMenus = [];
menuList.forEach((menu) => {
const headerName = menu.path || '';
const menus = transferMenu(menu, headerName);
allMenus.push({
path: menu.path,
header: headerName,
});
menus.forEach((item) => allMenus.push(item));
});
const currentMenu = allMenus.find((item) => {
if (item.path === to.path) {
return true;
} else {
return to.path === getPath(to, item.path);
}
});
return currentMenu ? currentMenu.header : null;
}
function getPath(to, path) {
let params = [];
let query = [];
Object.keys(to.params).forEach((item) => {
params.push(to.params[item]);
});
Object.keys(to.query).forEach((item) => {
query.push(item + '=' + to.query[item]);
});
return path + (params.length ? '/' + params.join('/') : '') + (query.length ? '?' + query.join('&') : '');
}
function transferMenu(menu, headerName) {
if (menu.children && menu.children.length) {
return menu.children.reduce((all, item) => {
all.push({
path: item.path,
header: headerName,
});
const foundChildren = transferMenu(item, headerName);
return all.concat(foundChildren);
}, []);
} else {
return [menu];
}
}
export { getHeaderName };
/**
* @description 根据当前路由,找打顶部菜单名称
* @param {String} currentPath 当前路径
* @param {Array} menuList 所有路径
* */
function getHeaderSider(menuList) {
return menuList.filter((item) => item.is_header === 1);
}
export { getHeaderSider };
/**
* @description 根据当前路由,找以及菜单名称
* @param {String} currentPath 当前路径
* @param {Array} menuList 所有路径
* */
function getOneHeaderName(menuList, path) {
return menuList.filter((item) => item.path === path);
}
export { getOneHeaderName };
/**
* @description 根据当前顶栏菜单 name,找到对应的二级菜单
* @param {Array} menuList 所有的二级菜单
* @param {String} headerName 当前顶栏菜单的 name
* */
function getMenuSider(menuList, headerName = '') {
if (headerName) {
return menuList.filter((item) => item.path === headerName);
} else {
return menuList;
}
}
export { getMenuSider };
/**
* @description 根据当前路由,找到其所有父菜单 path,作为展开侧边栏 open-names 依据
* @param {String} currentPath 当前路径
* @param {Array} menuList 所有路径
* */
// function getSiderSubmenu (currentPath, menuList) {
// const allMenus = [];
// menuList.forEach(menu => {
// const menus = transferSubMenu(menu, []);
// allMenus.push({
// path: menu.path,
// openNames: []
// });
// menus.forEach(item => allMenus.push(item));
// });
// const currentMenu = allMenus.find(item => item.path === currentPath);
// return currentMenu ? currentMenu.openNames : [];
// }
function getSiderSubmenu(to, menuList) {
const allMenus = [];
menuList.forEach((menu) => {
const menus = transferSubMenu(menu, []);
allMenus.push({
path: menu.path,
openNames: [],
});
menus.forEach((item) => allMenus.push(item));
});
const currentMenu = allMenus.find((item) => {
if (item.openNames.length) {
return item.path === to.path || to.path === getPath(to, item.path);
}
});
return currentMenu ? currentMenu.openNames : [];
}
function transferSubMenu(menu, openNames) {
if (menu.children && menu.children.length) {
const itemOpenNames = openNames.concat([menu.path]);
return menu.children.reduce((all, item) => {
all.push({
path: item.path,
openNames: itemOpenNames,
});
const foundChildren = transferSubMenu(item, itemOpenNames);
return all.concat(foundChildren);
}, []);
} else {
return [menu].map((item) => {
return {
path: item.path,
openNames: openNames,
};
});
}
}
export { getSiderSubmenu };
/**
* @description 递归获取所有子菜单
* */
function getAllSiderMenu(menuList) {
let allMenus = [];
menuList.forEach((menu) => {
if (menu.children && menu.children.length) {
const menus = getMenuChildren(menu);
menus.forEach((item) => allMenus.push(item));
} else {
allMenus.push(menu);
}
});
return allMenus;
}
function getMenuChildren(menu) {
if (menu.children && menu.children.length) {
return menu.children.reduce((all, item) => {
const foundChildren = getMenuChildren(item);
return all.concat(foundChildren);
}, []);
} else {
return [menu];
}
}
export { getAllSiderMenu };
/**
* @description 将菜单转为平级
* */
function flattenSiderMenu(menuList, newList) {
menuList.forEach((menu) => {
let newMenu = {};
for (let i in menu) {
if (i !== 'children') newMenu[i] = cloneDeep(menu[i]);
}
newList.push(newMenu);
menu.children && flattenSiderMenu(menu.children, newList);
});
return newList;
}
export { flattenSiderMenu };
/**
* @description 判断列表1中是否包含了列表2中的某一项
* 因为用户权限 access 为数组,includes 方法无法直接得出结论
* */
function includeArray(list1, list2) {
let status = false;
if (list1 === true) {
return true;
} else {
if (typeof list2 !== 'object') {
return false;
}
list2.forEach((item) => {
if (list1.includes(item)) status = true;
});
return status;
}
}
export { includeArray };

View File

@ -55,7 +55,7 @@ import Viewer from 'v-viewer';
import VueDND from 'awe-dnd';
import formCreate from '@form-create/iview';
import modalForm from '@/utils/modalForm';
import exportExcel from '@/utils/newToExcel.js'
import exportExcel from '@/utils/newToExcel.js';
import videoCloud from '@/utils/videoCloud';
import { modalSure } from '@/utils/public';
import { authLapse } from '@/utils/authLapse';
@ -67,6 +67,8 @@ import timeOptions from '@/libs/timeOptions';
import scroll from '@/libs/loading';
import * as tools from '@/libs/tools';
import VueTreeList from 'vue-tree-list';
import { getHeaderName, getHeaderSider, getMenuSider, getSiderSubmenu } from '@/libs/system';
// 复制到粘贴板插件
import VueClipboard from 'vue-clipboard2';
@ -88,9 +90,9 @@ const routerPush = Router.prototype.push;
Router.prototype.push = function push(location) {
return routerPush.call(this, location).catch((error) => error);
};
import settings from '@/setting'
import settings from '@/setting';
Vue.prototype.$routeProStr = settings.routePre
Vue.prototype.$routeProStr = settings.routePre;
// 实际打包时应该不引入mock
/* eslint-disable */
@ -193,11 +195,45 @@ new Vue({
watch: {
// 监听路由 控制侧边栏显示 标记当前顶栏菜单(如需要)
$route(to, from) {
const path = to.path;
let menus = this.$store.state.menus.menusName;
console.log(menus, 'menus');
const menuSider = menus;
this.$store.commit('menu/setActivePath', path);
const openNames = getSiderSubmenu(to, menuSider);
this.$store.commit('menu/setOpenNames', openNames);
// 设置顶栏菜单 后台添加一个接口,设置顶部菜单
const headerSider = getHeaderSider(menuSider);
console.log(headerSider, 'headerSider');
this.$store.commit('menu/setHeader', headerSider);
// 指定当前侧边栏隶属顶部菜单名称。如果你没有使用顶部菜单,则设置为默认的(一般为 home)名称即可
const headerName = getHeaderName(to, menuSider);
this.$store.commit('menu/setHeaderName', headerName);
// 获取侧边栏菜单
const filterMenuSider = getMenuSider(menuSider, headerName);
console.log(filterMenuSider, 'filterMenuSider');
// 指定当前显示的侧边菜单
this.$store.commit('menu/setOpenMenuName', filterMenuSider[0].title);
this.$store.commit('menu/setSider', filterMenuSider[0]?.children || []);
if (to.meta.kefu) {
document.getElementsByTagName('body')[0].className = 'kf_mobile';
} else {
document.getElementsByTagName('body')[0].className = '';
}
// var storage = window.localStorage;
// let menus = JSON.parse(storage.getItem('menuList'));
// this.getMenus().then(menus => {
// 处理手动清除db 跳转403问题
if (!menus.length) {
if (path !== '/admin/login') {
this.$router.replace('/admin/login');
}
return;
}
// 在 404 时,是没有 headerName 的
// });
},
},
});

View File

@ -3251,7 +3251,6 @@ export default {
font-size: 12px;
font-weight: 400;
color: #999999;
margin-top: 14px;
}
.videbox {

View File

@ -225,4 +225,7 @@ export default {
min-height: 600px;
margin-top 0px !important ;
}
.article-manager /deep/ .ivu-form-item{
margin-bottom: 20px !important;
}
</style>

View File

@ -15,6 +15,7 @@ import VuexPersistence from 'vuex-persist';
import user from './module/user';
import app from './module/app';
import menus from './module/menus';
import menu from './module/menu';
import userInfo from './module/userInfo';
import userLevel from './module/userLevel';
import order from './module/order';
@ -51,6 +52,7 @@ export default new Vuex.Store({
user: state.user, //这个就是存入localStorage的值
app: state.app,
menus: state.menus,
menu: state.menu,
userInfo: state.userInfo,
userLevel: state.userLevel,
order: state.order,
@ -67,6 +69,7 @@ export default new Vuex.Store({
user,
app,
menus,
menu,
userInfo,
userLevel,
order,

View File

@ -0,0 +1,165 @@
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2021 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
/**
* 菜单
* */
import { cloneDeep } from 'lodash';
import { includeArray } from '@/libs/system';
// 根据 menu 配置的权限,过滤菜单
function filterMenu(menuList, access, lastList) {
menuList.forEach((menu) => {
let menuAccess = menu.auth;
if (!menuAccess || includeArray(menuAccess, access)) {
let newMenu = {};
for (let i in menu) {
if (i !== 'children') newMenu[i] = cloneDeep(menu[i]);
}
if (menu.children && menu.children.length) newMenu.children = [];
lastList.push(newMenu);
menu.children && filterMenu(menu.children, access, newMenu.children);
}
});
return lastList;
}
// 递归处理顶部菜单问题
function getChilden(data) {
if (data.children) {
return getChilden(data.children[0]);
}
return data.path;
}
export default {
namespaced: true,
state: {
// 顶部菜单
header: [],
// 一级菜单名称
oneMenuName: '',
// 侧栏菜单
sider: [],
// 当前顶栏菜单的 name
headerName: '',
// 当前所在菜单的 path
activePath: '',
// 展开的子菜单 name 集合
openNames: [],
},
getters: {
/**
* @description 根据 user 里登录用户权限,对侧边菜单进行鉴权过滤
* */
filterSider(state, getters, rootState) {
const userInfo = rootState.user.info;
// @权限
const access = userInfo.access;
if (access && access.length) {
return filterMenu(state.sider, access, []);
} else {
return filterMenu(state.sider, [], []);
}
},
// 处理顶部路由递归
/**
* @description 根据 user 里登录用户权限,对顶栏菜单进行鉴权过滤
* */
filterHeader(state, getters, rootState) {
// 调用递归函数
state.header.forEach((item) => {
item.path = getChilden(item);
});
// @权限
const userInfo = rootState.admin.user.info;
const access = userInfo.access;
if (access && access.length) {
return state.header.filter((item) => {
let state = true;
if (item.auth && !includeArray(item.auth, access)) state = false;
return state;
});
} else {
return state.header.filter((item) => {
let state = true;
if (item.auth && item.auth.length) state = false;
return state;
});
}
},
/**
* @description 当前 header 的全部信息
* */
currentHeader(state) {
return state.header.find((item) => item.name === state.headerName);
},
/**
* @description 在当前 header 下,是否隐藏 sider(及折叠按钮)
* */
hideSider(state, getters) {
let visible = false;
if (getters.currentHeader && 'hideSider' in getters.currentHeader) visible = getters.currentHeader.hideSider;
return visible;
},
},
mutations: {
/**
* @description 设置侧边栏菜单
* @param {Object} state vuex state
* @param {Array} menu menu
*/
setSider(state, menu) {
state.sider = menu;
},
/**
* @description 设置侧边栏菜单
* @param {Object} state vuex state
* @param {Array} menu menu
*/
setOpenMenuName(state, menu) {
state.oneMenuName = menu;
},
/**
* @description 设置顶栏菜单
* @param {Object} state vuex state
* @param {Array} menu menu
*/
setHeader(state, menu) {
state.header = menu;
},
/**
* @description 设置当前顶栏菜单 name
* @param {Object} state vuex state
* @param {Array} name headerName
*/
setHeaderName(state, name) {
state.headerName = name;
},
/**
* @description 设置当前所在菜单的 path,用于侧栏菜单高亮当前项
* @param {Object} state vuex state
* @param {Array} path fullPath
*/
setActivePath(state, path) {
state.activePath = path;
},
/**
* @description 设置当前所在菜单的全部展开父菜单的 names 集合
* @param {Object} state vuex state
* @param {Array} names openNames
*/
setOpenNames(state, names) {
state.openNames = names;
},
},
};