This commit is contained in:
ky_sunl
2021-04-16 08:51:15 +00:00
parent 0b2316e9e5
commit 18f1d28d27
11 changed files with 256 additions and 133 deletions

View File

@@ -2,6 +2,7 @@
.dark {
.main(@nav-background: @layout-header-background;
@nav-box-shadow-color: fade(@black, 25%);
@nav-scrollbar-background: fade(@white, 50%);
@logo-color: @white;
@logo-box-shadow: none;
@header-action-color: fade(@white, 60%);

View File

@@ -2,6 +2,7 @@
.light {
.main(@nav-background: @white;
@nav-box-shadow-color: fade(@black, 5%);
@nav-scrollbar-background: fade(@black, 30%);
@logo-color: @black;
@logo-box-shadow: inset -1px -1px 1px @border-color-split;
@header-action-color: fade(@black, 35%);

View File

@@ -3,6 +3,7 @@
.main(@nav-background: @layout-header-background,
@nav-box-shadow-color: fade(@black, 25%),
@nav-scrollbar-background: fade(@white, 30%),
@logo-color: @white,
@logo-box-shadow: none,
@header-action-color: fade(@white, 60%),
@@ -35,12 +36,14 @@
box-shadow: inset 1px 1px 10px rgba(0, 0, 0, .05);
}
}
>.ant-input-search {
>.ant-input-search,
>.ant-select-auto-complete {
display: flex;
align-items: center;
margin: 5px @padding-md;
.ant-input {
width: 180px;
height: 34px;
padding: 5px 30px 5px 11px;
@@ -52,6 +55,9 @@
box-shadow: none;
}
}
.ant-select-selection {
background-color: transparent;
}
}
}
}
@@ -194,7 +200,8 @@
}
}
}
>.ant-input-search {
>.ant-input-search,
>.ant-select-auto-complete {
.ant-input {
&:focus {
background-color: fade(@black, 5%);
@@ -256,9 +263,27 @@
height: 100%;
box-shadow: 2px 0 8px @nav-box-shadow-color;
.swiper-scrollbar {
transition: @animation-duration-slow;
transition-property: opacity;
opacity: 0;
}
.swiper-scrollbar-drag {
background-color: @nav-scrollbar-background;
}
&:hover {
.swiper-scrollbar {
opacity: 1;
}
}
}
.swiper-slide {
height: auto;
min-height: 100%;
>.ant-spin-nested-loading {
height: 100%;
}
}
}
}
@@ -298,7 +323,8 @@
}
}
}
>.ant-input-search {
>.ant-input-search,
>.ant-select-auto-complete {
.ant-input {
color: @header-search-color;
background-color: @header-search-background;
@@ -306,7 +332,7 @@
background-color: @header-search-focus-background;
}
}
.ant-input-search-icon {
.anticon-search {
color: @header-search-icon-color;
&:hover {
color: @header-search-icon-hover-color;

View File

@@ -81,6 +81,7 @@ export default {
data.forEach((p, i) => {
n[n.length - 1] = i
p.key = n.join('-')
p.scopedSlots = { title: 'title' }
if (p.children) {
this.generateKey(p.children, Object.assign([], n))
}
@@ -125,14 +126,20 @@ export default {
mousewheel: true,
}
const slots = {
const scopedSlots = {
title: ({ title }) => {
return (
<div>
<span v-if={title.indexOf(this.searchValue) > -1}>
{title.replace(this.searchValue, <span style="color: #f50">{this.searchValue}</span>)}
{
title.indexOf(this.searchValue) > -1 ?
<span>
{title.substr(0, title.indexOf(this.searchValue))}
<span style="color: #f50">{this.searchValue}</span>
{title.substr(title.indexOf(this.searchValue) + this.searchValue.length)}
</span>
<span v-else>{{ title }}</span>
:
<span>{title}</span>
}
</div>
);
@@ -150,20 +157,14 @@ export default {
<swiper options={swiperOptions}>
<a-spin style={{ height: '100%' }} spinning={this.loading}>
<a-icon slot="indicator" type="loading" spin />
<swiper-slide>
<a-alert closable banner type="error">
<template slot="message">
<div>搜索无法高亮</div>
</template>
</a-alert>
<a-tree
{...{ slots }}
treeData={this.data}
expandedKeys={this.expandedKeys}
autoExpandParent={this.autoExpandParent}
onExpand={this.onExpand}
onSelect={this.onSelect}
{...{ scopedSlots }}
/>
</swiper-slide>
</a-spin>

View File

@@ -14,10 +14,10 @@
</a-form-model-item>
<template v-if="mode == 'add'">
<a-form-model-item label="密码" prop="password">
<a-input placeholder="请输入密码" type="password" v-model="form.password" />
<a-input-password placeholder="请输入密码" v-model="form.password" />
</a-form-model-item>
<a-form-model-item label="确认密码" prop="confirm">
<a-input placeholder="请确认密码" type="password" v-model="form.confirm" />
<a-input-password placeholder="请确认密码" v-model="form.confirm" />
</a-form-model-item>
</template>
<a-form-model-item label="昵称">

View File

@@ -138,6 +138,7 @@ export default {
},
{
title: '性别',
width: '100px',
dataIndex: 'sex',
scopedSlots: {
customRender: 'sex',
@@ -149,6 +150,7 @@ export default {
},
{
title: '状态',
width: '200px',
dataIndex: 'status',
scopedSlots: {
customRender: 'status',

View File

@@ -10,7 +10,29 @@
>
<a-icon :type="$root.global.settings.siderCollapsed ? 'menu-unfold' : 'menu-fold'" />
</a>
<a-input-search placeholder="请输入检索关键字" />
<!-- <a-input-search placeholder="请输入检索关键字" /> -->
<a-auto-complete
:dropdown-match-select-width="false"
@search="onSearch"
@select="onSearchSelect"
option-label-prop="value"
placeholder="请输入检索关键字"
v-model="searchText"
>
<template slot="dataSource">
<a-select-opt-group :key="menu.id" v-for="menu in searchResult">
<span slot="label">{{ menu.meta.title }}</span>
<a-select-option
:key="opt.id"
:value="opt.id"
v-for="opt in menu.children"
>{{ opt.meta.title }}</a-select-option>
</a-select-opt-group>
</template>
<a-input>
<a-icon class="certain-category-icon" slot="suffix" type="search" />
</a-input>
</a-auto-complete>
</div>
<div class="header-actions">
<a @click="$emit('reload')" class="header-action">
@@ -32,7 +54,29 @@
<Sider :nav="nav" @open="(nav) => $emit('open', nav)" />
</div>
<div class="header-actions">
<a-input-search placeholder="请输入检索关键字" />
<!-- <a-input-search placeholder="请输入检索关键字" /> -->
<a-auto-complete
:dropdown-match-select-width="false"
@search="onSearch"
@select="onSearchSelect"
option-label-prop="value"
placeholder="请输入检索关键字"
v-model="searchText"
>
<template slot="dataSource">
<a-select-opt-group :key="menu.id" v-for="menu in searchResult">
<span slot="label">{{ menu.meta.title }}</span>
<a-select-option
:key="opt.id"
:value="opt.component"
v-for="opt in menu.children"
>{{ opt.meta.title }}</a-select-option>
</a-select-opt-group>
</template>
<a-input>
<a-icon class="certain-category-icon" slot="suffix" type="search" />
</a-input>
</a-auto-complete>
<a @click="$emit('reload')" class="header-action">
<a-icon type="reload" />
</a>
@@ -68,5 +112,48 @@ export default {
type: Object,
},
},
data() {
return {
searchText: '',
searchResult: [],
};
},
created() {
window._ = this.$_;
},
methods: {
onSearch(value) {
const menus = this.$_.cloneDeep(this.nav.menus);
const search = (m) => {
if (!value) return [];
return m.filter((p) => {
if (p.children) {
p.children = search(p.children);
} else {
return p.meta.title.indexOf(value) > -1;
}
return p.children.length;
});
};
this.searchResult = search(menus);
},
onSearchSelect(value, node) {
this.searchText = '';
this.onSearch(this.searchText);
const id = node.componentOptions.propsData.value;
//const menu = this.nav.menus.
// this.openContentWindow({
// key: menu.id,
// title: menu.meta.title,
// icon: menu.meta.icon,
// path: menu.component,
// });
},
},
};
</script>

View File

@@ -6,16 +6,19 @@
width="200"
>
<Logo />
<swiper :options="siderSwiperOptions" :style="{ height: '100%' }" ref="sider-swiper">
<swiper-slide :style="{ height: 'auto' }">
<swiper :options="siderSwiperOptions" ref="sider-swiper">
<swiper-slide>
<a-spin :spinning="nav.loading">
<a-icon slot="indicator" spin type="loading" />
<Menu
:menu-style="{ height: '100%', borderRight: 0 }"
:nav="nav"
@openChange="onMenuOpenChange"
mode="inline"
/>
</a-spin>
</swiper-slide>
<div class="swiper-scrollbar"></div>
<div class="swiper-scrollbar" id="layout--swiper-scrollbar" slot="scrollbar"></div>
</swiper>
</a-layout-sider>
<template v-else-if="$root.global.settings.layout === 'top-nav'">
@@ -43,6 +46,7 @@ export default {
return {
modules: [],
menus: [],
loading: false,
};
},
type: Object,
@@ -55,7 +59,7 @@ export default {
slidesPerView: 'auto',
freeMode: true,
scrollbar: {
el: '.swiper-scrollbar',
el: '#layout--swiper-scrollbar',
},
mousewheel: true,
},

View File

@@ -0,0 +1,80 @@
export default {
props: {
nav: {
default() {
return {
modules: [],
menus: [],
}
},
type: Object,
},
menuStyle: {
default() {
return {}
},
type: Object || String,
},
mode: {
default: 'inline',
type: String,
},
},
methods: {
renderMenu(menu) {
return menu.map((p) => {
return p.children ? this.renderSubMenu(p) : this.renderMenuItem(p)
})
},
renderSubMenu(menu) {
return (
<a-sub-menu key={menu.id}>
<span slot="title">
{menu.meta.icon ? <a-icon type={menu.meta.icon} /> : null}
<span>{menu.meta.title}</span>
</span>
{this.renderMenu(menu.children)}
</a-sub-menu>
)
},
renderMenuItem(menu) {
return (
<a-menu-item key={menu.id} onClick={() => this.onOpenContentWindow(menu)}>
{menu.meta.icon ? <a-icon type={menu.meta.icon} /> : null}
<span>{menu.meta.title}</span>
</a-menu-item>
)
},
onMenuOpenChange() {
this.$emit('openChange')
},
onOpenContentWindow(menu) {
this.openContentWindow({
key: menu.id,
title: menu.meta.title,
icon: menu.meta.icon,
path: menu.component,
})
},
},
render() {
const props = {
mode: this.mode,
selectable: false,
style: this.menuStyle,
theme: this.$root.global.settings.navTheme,
}
const on = {
openChange: this.onMenuOpenChange,
}
return <a-menu {...{ props, on }}>{this.renderMenu(this.nav.menus)}</a-menu>
},
}

View File

@@ -1,100 +0,0 @@
<template>
<a-menu
:mode="mode"
:selectable="false"
:style="menuStyle"
:theme="$root.global.settings.navTheme"
@openChange="onMenuOpenChange"
>
<template v-for="c1 in menu">
<a-sub-menu :key="c1.id" v-if="c1.children && c1.children.length">
<span slot="title">
<a-icon :type="c1.meta.icon" v-if="c1.meta.icon" />
<span>{{ c1.meta.title }}</span>
</span>
<template v-for="c2 in c1.children">
<a-menu-item :key="c2.id" @click="onOpenContentWindow(c2)">
<a-icon :type="c2.meta.icon" v-if="c2.meta.icon" />
<span>{{ c2.meta.title }}</span>
</a-menu-item>
</template>
</a-sub-menu>
<a-menu-item :key="c1.id" @click="onOpenContentWindow(c1)" v-else>
<a-icon :type="c1.meta.icon" v-if="c1.meta.icon" />
<span>{{ c1.meta.title }}</span>
</a-menu-item>
</template>
</a-menu>
</template>
<script>
export default {
props: {
nav: {
default() {
return {
modules: [],
menus: [],
};
},
type: Object,
},
menuStyle: {
default() {
return {};
},
type: Object || String,
},
mode: {
default: 'inline',
type: String,
},
},
data() {
return {
menu: [],
};
},
created() {
this.serializeMenu();
},
watch: {
nav: {
deep: true,
handler() {
this.serializeMenu();
},
},
},
methods: {
serializeMenu() {
const menu = this.$_.cloneDeep(this.nav.menus);
const children = this.$_.groupBy(menu, 'pid');
this.menu = menu
.filter((p) => !p.pid)
.map((p) => {
return {
...p,
children: children[p.id],
};
});
},
onMenuOpenChange() {
this.$emit('openChange');
},
onOpenContentWindow(menu) {
this.openContentWindow({
key: menu.id,
title: menu.meta.title,
icon: menu.meta.icon,
path: menu.component,
});
},
},
};
</script>

View File

@@ -79,8 +79,10 @@ export default {
visible: false,
},
menus: [],
nav: {
loaded: false,
loading: false,
modules: [],
menus: [],
},
@@ -88,6 +90,8 @@ export default {
},
created() {
this.nav.loading = true;
this.$api.getLoginUser().then(({ data }) => {
const info = { ...data };
delete info.apps;
@@ -95,8 +99,9 @@ export default {
setGlobal(info);
this.nav.modules = data.apps;
this.nav.menus = data.menus;
this.nav.loaded = true;
this.menus = data.menus;
this.serializeMenu();
this.nav.loading = false;
});
},
@@ -226,6 +231,22 @@ export default {
if (!key) key = this.tabActived;
this.$refs.content.onLoadContentWindow(key);
},
serializeMenu() {
const menu = this.$_.cloneDeep(this.menus);
const children = this.$_.groupBy(menu, 'pid');
const serialize = (m) => {
m.map((p) => {
if (children[p.id]) {
p.children = serialize(children[p.id]);
}
});
return m;
};
this.nav.menus = serialize(children[0]);
},
},
};
</script>