初始化
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="d2-contentmenu-list" @click="rowClick">
|
||||
<div v-for="item in menulist" :key="item.value" :data-value="item.value" class="d2-contentmenu-item" flex="cross:center main:center">
|
||||
<d2-icon v-if="item.icon" :name="item.icon"/>
|
||||
<div class="d2-contentmenu-item-title" flex-box="1">
|
||||
{{item.title}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'd2-contextmenu-list',
|
||||
props: {
|
||||
menulist: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rowClick (event) {
|
||||
let target = event.target
|
||||
while (!target.dataset.value) {
|
||||
target = target.parentNode
|
||||
}
|
||||
this.$emit('rowClick', target.dataset.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.d2-contentmenu-list {
|
||||
.d2-contentmenu-item {
|
||||
padding: 8px 20px 8px 15px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: #ecf5ff;
|
||||
color: #66b1ff;
|
||||
}
|
||||
.d2-contentmenu-item-title {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="d2-contextmenu" v-show="flag" :style="style">
|
||||
<slot/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'd2-contextmenu',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
x: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
y: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
flag: {
|
||||
get () {
|
||||
if (this.visible) {
|
||||
// 注册全局监听事件 [ 目前只考虑鼠标解除触发 ]
|
||||
window.addEventListener('mousedown', this.watchContextmenu)
|
||||
}
|
||||
return this.visible
|
||||
},
|
||||
set (newVal) {
|
||||
this.$emit('update:visible', newVal)
|
||||
}
|
||||
},
|
||||
style () {
|
||||
return {
|
||||
left: this.x + 'px',
|
||||
top: this.y + 'px',
|
||||
display: this.visible ? 'block' : 'none '
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
watchContextmenu (event) {
|
||||
if (!this.$el.contains(event.target) || event.button !== 0) this.flag = false
|
||||
window.removeEventListener('mousedown', this.watchContextmenu)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// 将菜单放置到body下
|
||||
document.querySelector('body').appendChild(this.$el)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.d2-contextmenu {
|
||||
position: absolute;
|
||||
padding: 5px 0;
|
||||
z-index: 2018;
|
||||
background: #FFF;
|
||||
border: 1px solid #cfd7e5;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<el-tooltip effect="dark" :content="active ? '退出全屏' : '全屏'" placement="bottom">
|
||||
<el-button class="d2-mr btn-text can-hover" type="text" @click="toggle">
|
||||
<d2-icon v-if="active" name="compress"/>
|
||||
<d2-icon v-else name="arrows-alt" style="font-size: 16px"/>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex'
|
||||
export default {
|
||||
computed: {
|
||||
...mapState('d2admin/fullscreen', [
|
||||
'active'
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
...mapActions('d2admin/fullscreen', [
|
||||
'toggle'
|
||||
])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<el-dropdown placement="bottom" size="small" @command="onChangeLocale">
|
||||
<el-button class="d2-mr btn-text can-hover" type="text">
|
||||
<d2-icon name="language" style="font-size: 16px;"/>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item
|
||||
v-for="language in $languages"
|
||||
:key="language.value"
|
||||
:command="language.value">
|
||||
<d2-icon :name="$i18n.locale === language.value ? 'dot-circle-o' : 'circle-o'" class="d2-mr-5"/>
|
||||
{{ language.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import localeMixin from '@/locales/mixin.js'
|
||||
export default {
|
||||
mixins: [
|
||||
localeMixin
|
||||
]
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<el-tooltip effect="dark" :content="tooltipContent" placement="bottom">
|
||||
<el-button class="d2-ml-0 d2-mr btn-text can-hover" type="text" @click="handleClick">
|
||||
<el-badge v-if="logLength > 0" :max="99" :value="logLengthError" :is-dot="logLengthError === 0">
|
||||
<d2-icon :name="logLengthError === 0 ? 'dot-circle-o' : 'bug'" style="font-size: 20px"/>
|
||||
</el-badge>
|
||||
<d2-icon v-else name="dot-circle-o" style="font-size: 20px"/>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters('d2admin', {
|
||||
logLength: 'log/length',
|
||||
logLengthError: 'log/lengthError'
|
||||
}),
|
||||
tooltipContent () {
|
||||
return this.logLength === 0
|
||||
? '没有日志或异常'
|
||||
: `${this.logLength} 条日志${this.logLengthError > 0
|
||||
? ` | 包含 ${this.logLengthError} 个异常`
|
||||
: ''}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('d2admin/log', [
|
||||
'clean'
|
||||
]),
|
||||
handleClick () {
|
||||
this.$router.push({
|
||||
name: 'log'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<el-button class="d2-mr btn-text can-hover" type="text" @click="handleClick">
|
||||
<d2-icon name="search" style="font-size: 18px;"/>
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
handleClick () {
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<el-dropdown placement="bottom" size="small" @command="handleChange">
|
||||
<el-button class="d2-mr btn-text can-hover" type="text">
|
||||
<d2-icon name="font" style="font-size: 16px;"/>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item v-for="item in options" :key="item.value" :command="item.value">
|
||||
<d2-icon :name="iconName(item.value)" class="d2-mr-5"/>{{item.label}}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations, mapActions } from 'vuex'
|
||||
export default {
|
||||
name: 'd2-header-size',
|
||||
data () {
|
||||
return {
|
||||
options: [
|
||||
{ label: '默认', value: 'default' },
|
||||
{ label: '中', value: 'medium' },
|
||||
{ label: '小', value: 'small' },
|
||||
{ label: '最小', value: 'mini' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('d2admin/size', [
|
||||
'value'
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
pageKeepAliveClean: 'd2admin/page/keepAliveClean'
|
||||
}),
|
||||
...mapActions({
|
||||
sizeSet: 'd2admin/size/set'
|
||||
}),
|
||||
handleChange (value) {
|
||||
this.sizeSet(value)
|
||||
this.$notify({
|
||||
title: '提示',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: '已更新页面内 <b>组件</b> 的 <b>默认尺寸</b><br/>例如按钮大小,<b>非字号</b>',
|
||||
type: 'success'
|
||||
})
|
||||
},
|
||||
iconName (name) {
|
||||
return name === this.value ? 'dot-circle-o' : 'circle-o'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<el-table :data="list" v-bind="table">
|
||||
<el-table-column prop="title" align="center" width="160"/>
|
||||
<el-table-column label="预览" width="120">
|
||||
<div slot-scope="scope" class="theme-preview" :style="{ backgroundImage: `url(${$baseUrl}${scope.row.preview})` }"/>
|
||||
</el-table-column>
|
||||
<el-table-column prop="address" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button v-if="activeName === scope.row.name" type="success" icon="el-icon-check" round>已激活</el-button>
|
||||
<el-button v-else round @click="handleSelectTheme(scope.row.name)">使用</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex'
|
||||
export default {
|
||||
name: 'd2-theme-list',
|
||||
data () {
|
||||
return {
|
||||
table: {
|
||||
showHeader: false,
|
||||
border: true
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('d2admin/theme', [
|
||||
'list',
|
||||
'activeName'
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
...mapActions('d2admin/theme', [
|
||||
'set'
|
||||
]),
|
||||
handleSelectTheme (name) {
|
||||
this.set(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.theme-preview {
|
||||
height: 50px;
|
||||
width: 100px;
|
||||
border-radius: 4px;
|
||||
background-size: cover;
|
||||
border: 1px solid $color-border-1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="主题"
|
||||
placement="bottom">
|
||||
<el-button
|
||||
class="d2-ml-0 d2-mr btn-text can-hover"
|
||||
type="text"
|
||||
@click="dialogVisible = true">
|
||||
<d2-icon
|
||||
name="diamond"
|
||||
style="font-size: 16px"/>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-dialog
|
||||
title="主题"
|
||||
width="600px"
|
||||
:visible.sync="dialogVisible"
|
||||
:append-to-body="true">
|
||||
<d2-theme-list style="margin-top: -25px;"/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import themeList from './components/d2-theme-list'
|
||||
export default {
|
||||
components: {
|
||||
'd2-theme-list': themeList
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
dialogVisible: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @description 创建菜单
|
||||
* @param {Function} h createElement
|
||||
* @param {Object} menu 菜单项
|
||||
*/
|
||||
export function elMenuItem (h, menu) {
|
||||
let icon = null
|
||||
if (menu.icon) icon = <i class={ `fa fa-${menu.icon}` }/>
|
||||
else if (menu.iconSvg) icon = <d2-icon-svg name={ menu.iconSvg }/>
|
||||
else icon = <i class="fa fa-file-o"/>
|
||||
return <el-menu-item
|
||||
key={ menu.path }
|
||||
index={ menu.path }>
|
||||
{ icon }
|
||||
<span slot="title">{ menu.title || '未命名菜单' }</span>
|
||||
</el-menu-item>
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 创建子菜单
|
||||
* @param {Function} h createElement
|
||||
* @param {Object} menu 菜单项
|
||||
*/
|
||||
export function elSubmenu (h, menu) {
|
||||
let icon = null
|
||||
if (menu.icon) icon = <i slot="title" class={ `fa fa-${menu.icon}` }/>
|
||||
else if (menu.iconSvg) icon = <d2-icon-svg slot="title" name={ menu.iconSvg }/>
|
||||
else icon = <i slot="title" class="fa fa-folder-o"/>
|
||||
return <el-submenu
|
||||
key={ menu.path }
|
||||
index={ menu.path }>
|
||||
{ icon }
|
||||
<span slot="title">{ menu.title || '未命名菜单' }</span>
|
||||
{ menu.children.map(child => createMenu.call(this, h, child)) }
|
||||
</el-submenu>
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在组件中调用此方法渲染菜单项目
|
||||
* @param {Function} h createElement
|
||||
* @param {Object} menu 菜单项
|
||||
*/
|
||||
export function createMenu (h, menu) {
|
||||
if (menu.children === undefined) return elMenuItem.call(this, h, menu)
|
||||
return elSubmenu.call(this, h, menu)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { throttle } from 'lodash'
|
||||
import { mapState } from 'vuex'
|
||||
import menuMixin from '../mixin/menu'
|
||||
import { createMenu } from '../libs/util.menu'
|
||||
|
||||
export default {
|
||||
name: 'd2-layout-header-aside-menu-header',
|
||||
mixins: [
|
||||
menuMixin
|
||||
],
|
||||
render (h) {
|
||||
return <div
|
||||
flex="cross:center"
|
||||
class={ { 'd2-theme-header-menu': true, 'is-scrollable': this.isScroll } }
|
||||
ref="page">
|
||||
<div
|
||||
ref="content"
|
||||
class="d2-theme-header-menu__content"
|
||||
flex-box="1"
|
||||
flex>
|
||||
<div
|
||||
class="d2-theme-header-menu__scroll"
|
||||
flex-box="0"
|
||||
style={ { transform: `translateX(${this.currentTranslateX}px)` } }
|
||||
ref="scroll">
|
||||
<el-menu
|
||||
mode="horizontal"
|
||||
defaultActive={ this.active }
|
||||
onSelect={ this.handleMenuSelect }>
|
||||
{ this.header.map(menu => createMenu.call(this, h, menu)) }
|
||||
</el-menu>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
this.isScroll
|
||||
? [
|
||||
<div
|
||||
class="d2-theme-header-menu__prev"
|
||||
flex="main:center cross:center"
|
||||
flex-box="0"
|
||||
onClick={ () => this.scroll('left') }>
|
||||
<i class="el-icon-arrow-left"></i>
|
||||
</div>,
|
||||
<div
|
||||
class="d2-theme-header-menu__next"
|
||||
flex="main:center cross:center"
|
||||
flex-box="0"
|
||||
onClick={ () => this.scroll('right') }>
|
||||
<i class="el-icon-arrow-right"></i>
|
||||
</div>
|
||||
]
|
||||
: []
|
||||
}
|
||||
</div>
|
||||
},
|
||||
computed: {
|
||||
...mapState('d2admin/menu', [
|
||||
'header'
|
||||
])
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
active: '',
|
||||
isScroll: false,
|
||||
scrollWidth: 0,
|
||||
contentWidth: 0,
|
||||
currentTranslateX: 0,
|
||||
throttledCheckScroll: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.matched': {
|
||||
handler (val) {
|
||||
this.active = val[val.length - 1].path
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scroll (direction) {
|
||||
if (direction === 'left') {
|
||||
// 向右滚动
|
||||
this.currentTranslateX = 0
|
||||
} else {
|
||||
// 向左滚动
|
||||
if (this.contentWidth * 2 - this.currentTranslateX <= this.scrollWidth) {
|
||||
this.currentTranslateX -= this.contentWidth
|
||||
} else {
|
||||
this.currentTranslateX = this.contentWidth - this.scrollWidth
|
||||
}
|
||||
}
|
||||
},
|
||||
checkScroll () {
|
||||
let contentWidth = this.$refs.content.clientWidth
|
||||
let scrollWidth = this.$refs.scroll.clientWidth
|
||||
if (this.isScroll) {
|
||||
// 页面依旧允许滚动的情况,需要更新width
|
||||
if (this.contentWidth - this.scrollWidth === this.currentTranslateX) {
|
||||
// currentTranslateX 也需要相应变化【在右端到头的情况时】
|
||||
this.currentTranslateX = contentWidth - scrollWidth
|
||||
// 快速的滑动依旧存在判断和计算时对应的contentWidth变成正数,所以需要限制一下
|
||||
if (this.currentTranslateX > 0) {
|
||||
this.currentTranslateX = 0
|
||||
}
|
||||
}
|
||||
// 更新元素数据
|
||||
this.contentWidth = contentWidth
|
||||
this.scrollWidth = scrollWidth
|
||||
// 判断何时滚动消失: 当scroll > content
|
||||
if (contentWidth > scrollWidth) {
|
||||
this.isScroll = false
|
||||
}
|
||||
}
|
||||
// 判断何时滚动出现: 当scroll < content
|
||||
if (!this.isScroll && contentWidth < scrollWidth) {
|
||||
this.isScroll = true
|
||||
// 注意,当isScroll变为true,对应的元素盒子大小会发生变化
|
||||
this.$nextTick(() => {
|
||||
contentWidth = this.$refs.content.clientWidth
|
||||
scrollWidth = this.$refs.scroll.clientWidth
|
||||
this.contentWidth = contentWidth
|
||||
this.scrollWidth = scrollWidth
|
||||
this.currentTranslateX = 0
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// 初始化判断
|
||||
// 默认判断父元素和子元素的大小,以确定初始情况是否显示滚动
|
||||
this.checkScroll()
|
||||
// 全局窗口变化监听,判断父元素和子元素的大小,从而控制isScroll的开关
|
||||
this.throttledCheckScroll = throttle(this.checkScroll, 300)
|
||||
window.addEventListener('resize', this.throttledCheckScroll)
|
||||
},
|
||||
beforeDestroy () {
|
||||
// 取消监听
|
||||
window.removeEventListener('resize', this.throttledCheckScroll)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import util from '@/libs/util.js'
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
handleMenuSelect (index, indexPath) {
|
||||
if (/^d2-menu-empty-\d+$/.test(index) || index === undefined) {
|
||||
this.$message.warning('临时菜单')
|
||||
} else if (/^https:\/\/|http:\/\//.test(index)) {
|
||||
util.open(index)
|
||||
} else {
|
||||
this.$router.push({
|
||||
path: index
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="d2-panel-search-item" :class="hoverMode ? 'can-hover' : ''" flex>
|
||||
<div class="d2-panel-search-item__icon" flex-box="0">
|
||||
<div class="d2-panel-search-item__icon-box" flex="main:center cross:center">
|
||||
<d2-icon v-if="item.icon" :name="item.icon"/>
|
||||
<d2-icon-svg v-else-if="item.iconSvg" :name="item.iconSvg"/>
|
||||
<d2-icon v-else name="file-o"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d2-panel-search-item__info" flex-box="1" flex="dir:top">
|
||||
<div class="d2-panel-search-item__info-title" flex-box="1" flex="cross:center">
|
||||
<span>{{item.title}}</span>
|
||||
</div>
|
||||
<div class="d2-panel-search-item__info-fullTitle" flex-box="0">
|
||||
<span>{{item.fullTitle}}</span>
|
||||
</div>
|
||||
<div class="d2-panel-search-item__info-path" flex-box="0">
|
||||
<span>{{item.path}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
item: {
|
||||
default: () => ({})
|
||||
},
|
||||
hoverMode: {
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.d2-panel-search-item {
|
||||
height: 64px;
|
||||
margin: 0px -20px;
|
||||
&.can-hover {
|
||||
@extend %unable-select;
|
||||
margin: 0px;
|
||||
&:hover {
|
||||
background-color: #F5F7FA;
|
||||
.d2-panel-search-item__icon {
|
||||
.d2-panel-search-item__icon-box {
|
||||
i {
|
||||
font-size: 24px;
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
.d2-panel-search-item__info {
|
||||
.d2-panel-search-item__info-title {
|
||||
color: $color-text-main;
|
||||
}
|
||||
.d2-panel-search-item__info-fullTitle {
|
||||
color: $color-text-normal;
|
||||
}
|
||||
.d2-panel-search-item__info-path {
|
||||
color: $color-text-normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.d2-panel-search-item__icon {
|
||||
width: 64px;
|
||||
.d2-panel-search-item__icon-box {
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
border-right: 1px solid $color-border-3;
|
||||
i {
|
||||
font-size: 20px;
|
||||
color: $color-text-sub;
|
||||
}
|
||||
svg {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.d2-panel-search-item__info {
|
||||
margin-left: 10px;
|
||||
.d2-panel-search-item__info-title {
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
font-weight: bold;
|
||||
color: $color-text-normal;
|
||||
}
|
||||
.d2-panel-search-item__info-fullTitle {
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
color: $color-text-placehoder;
|
||||
}
|
||||
.d2-panel-search-item__info-path {
|
||||
margin-bottom: 4px;
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
color: $color-text-placehoder;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="panel-search" flex="dir:top">
|
||||
<div class="panel-search__input-group" flex-box="0" flex="dir:top main:center cross:center" @click.self="handlePanelClick">
|
||||
<d2-icon-svg class="panel-search__logo" name="d2-admin-text"/>
|
||||
<el-autocomplete
|
||||
class="panel-search__input"
|
||||
ref="input"
|
||||
v-model="searchText"
|
||||
suffix-icon="el-icon-search"
|
||||
placeholder="搜索页面"
|
||||
:fetch-suggestions="querySearch"
|
||||
:trigger-on-focus="false"
|
||||
:clearable="true"
|
||||
@keydown.esc.native="handleEsc"
|
||||
@select="handleSelect">
|
||||
<d2-panel-search-item slot-scope="{ item }" :item="item"/>
|
||||
</el-autocomplete>
|
||||
<div class="panel-search__tip">
|
||||
您可以使用快捷键
|
||||
<span class="panel-search__key">{{hotkey.open}}</span>
|
||||
唤醒搜索面板,按
|
||||
<span class="panel-search__key">{{hotkey.close}}</span>
|
||||
关闭
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="resultsList.length > 0" class="panel-search__results-group" flex-box="1">
|
||||
<el-card shadow="never">
|
||||
<div class="panel-search__results-group-inner">
|
||||
<d2-panel-search-item
|
||||
v-for="(item, index) in resultsList"
|
||||
:key="index"
|
||||
:item="item"
|
||||
:hover-mode="true"
|
||||
@click.native="handleResultsGroupItemClick(item.path)"/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Fuse from 'fuse.js'
|
||||
import { mapState } from 'vuex'
|
||||
import mixin from '../mixin/menu'
|
||||
export default {
|
||||
mixins: [
|
||||
mixin
|
||||
],
|
||||
components: {
|
||||
'd2-panel-search-item': () => import('./components/panel-search-item/index.vue')
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
searchText: '',
|
||||
results: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('d2admin/search', [
|
||||
'hotkey',
|
||||
'pool'
|
||||
]),
|
||||
// 这份数据是展示在搜索面板下面的
|
||||
resultsList () {
|
||||
return (this.results.length === 0 && this.searchText === '') ? this.pool.map(e => ({
|
||||
value: e.fullTitle,
|
||||
...e
|
||||
})) : this.results
|
||||
},
|
||||
// 根据 pool 更新 fuse 实例
|
||||
fuse () {
|
||||
return new Fuse(this.pool, {
|
||||
shouldSort: true,
|
||||
tokenize: true,
|
||||
threshold: 0.6,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
keys: [
|
||||
'fullTitle',
|
||||
'path'
|
||||
]
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @description 过滤选项 这个方法在每次输入框的值发生变化时会触发
|
||||
*/
|
||||
querySearch (queryString, callback) {
|
||||
const results = this.fuse.search(queryString).map(e => e.item)
|
||||
this.results = results
|
||||
callback(results)
|
||||
},
|
||||
/**
|
||||
* @description 聚焦输入框
|
||||
*/
|
||||
focus () {
|
||||
this.input = ''
|
||||
setTimeout(() => {
|
||||
if (this.$refs.input) {
|
||||
this.$refs.input.focus()
|
||||
}
|
||||
// 还原
|
||||
this.searchText = ''
|
||||
this.results = []
|
||||
}, 500)
|
||||
},
|
||||
/**
|
||||
* @description 接收用户在列表中选择项目的事件
|
||||
*/
|
||||
handleResultsGroupItemClick (path) {
|
||||
// 如果用户选择的就是当前页面 就直接关闭搜索面板
|
||||
if (path === this.$route.path) {
|
||||
this.handleEsc()
|
||||
return
|
||||
}
|
||||
// 用户选择的是其它页面
|
||||
this.handleMenuSelect(path)
|
||||
},
|
||||
/**
|
||||
* @description 接收用户在下拉菜单中选中事件
|
||||
*/
|
||||
async handleSelect ({ path }) {
|
||||
// 如果用户选择的就是当前页面 就直接关闭搜索面板
|
||||
if (path === this.$route.path) {
|
||||
this.handleEsc()
|
||||
return
|
||||
}
|
||||
// 用户选择的是其它页面
|
||||
await this.$nextTick()
|
||||
this.handleMenuSelect(path)
|
||||
},
|
||||
/**
|
||||
* @augments 关闭输入框的下拉菜单
|
||||
*/
|
||||
closeSuggestion () {
|
||||
if (this.$refs.input.activated) {
|
||||
this.$refs.input.suggestions = []
|
||||
this.$refs.input.activated = false
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @augments 接收用户点击空白区域的关闭
|
||||
*/
|
||||
handlePanelClick () {
|
||||
this.handleEsc()
|
||||
},
|
||||
/**
|
||||
* @augments 接收用户触发的关闭
|
||||
*/
|
||||
async handleEsc () {
|
||||
this.closeSuggestion()
|
||||
await this.$nextTick()
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-search {
|
||||
margin: 20px;
|
||||
width: 100%;
|
||||
.panel-search__input-group {
|
||||
height: 240px;
|
||||
.panel-search__logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.panel-search__input {
|
||||
width: 500px;
|
||||
}
|
||||
.panel-search__tip {
|
||||
@extend %unable-select;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 40px;
|
||||
font-size: 12px;
|
||||
color: $color-text-sub;
|
||||
.panel-search__key {
|
||||
padding: 1px 5px;
|
||||
margin: 0px 2px;
|
||||
border-radius: 2px;
|
||||
background-color: $color-text-normal;
|
||||
color: $color-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel-search__results-group {
|
||||
overflow: auto;
|
||||
margin-bottom: -20px;
|
||||
.panel-search__results-group-inner {
|
||||
margin: -20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="d2-multiple-page-control-group" flex>
|
||||
<div class="d2-multiple-page-control-content" flex-box="1">
|
||||
<div class="d2-multiple-page-control-content-inner">
|
||||
<d2-contextmenu
|
||||
:visible.sync="contextmenuFlag"
|
||||
:x="contentmenuX"
|
||||
:y="contentmenuY">
|
||||
<d2-contextmenu-list
|
||||
:menulist="tagName === '/index' ? contextmenuListIndex : contextmenuList"
|
||||
@rowClick="contextmenuClick"/>
|
||||
</d2-contextmenu>
|
||||
<el-tabs
|
||||
class="d2-multiple-page-control d2-multiple-page-sort"
|
||||
:value="current"
|
||||
type="card"
|
||||
@tab-click="handleClick"
|
||||
@tab-remove="handleTabRemove"
|
||||
@contextmenu.native="handleContextmenu">
|
||||
<el-tab-pane
|
||||
v-for="page in opened"
|
||||
:key="page.fullPath"
|
||||
:label="page.meta.title || '未命名'"
|
||||
:name="page.fullPath"
|
||||
:closable="isTabClosable(page)"/>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d2-multiple-page-control-btn" flex-box="0">
|
||||
<el-dropdown
|
||||
size="default"
|
||||
split-button
|
||||
@click="closeAll"
|
||||
@command="command => handleControlItemClick(command)">
|
||||
<d2-icon name="times-circle"/>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="left">
|
||||
<d2-icon name="arrow-left" class="d2-mr-10"/>
|
||||
关闭左侧
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="right">
|
||||
<d2-icon name="arrow-right" class="d2-mr-10"/>
|
||||
关闭右侧
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="other">
|
||||
<d2-icon name="times" class="d2-mr-10"/>
|
||||
关闭其它
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="all">
|
||||
<d2-icon name="times-circle" class="d2-mr-10"/>
|
||||
全部关闭
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex'
|
||||
import Sortable from 'sortablejs'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
D2Contextmenu: () => import('../contextmenu'),
|
||||
D2ContextmenuList: () => import('../contextmenu/components/contentmenuList')
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
contextmenuFlag: false,
|
||||
contentmenuX: 0,
|
||||
contentmenuY: 0,
|
||||
contextmenuListIndex: [
|
||||
{ icon: 'times-circle', title: '关闭全部', value: 'all' }
|
||||
],
|
||||
contextmenuList: [
|
||||
{ icon: 'refresh', title: '刷新', value: 'refresh' },
|
||||
{ icon: 'arrow-left', title: '关闭左侧', value: 'left' },
|
||||
{ icon: 'arrow-right', title: '关闭右侧', value: 'right' },
|
||||
{ icon: 'times', title: '关闭其它', value: 'other' },
|
||||
{ icon: 'times-circle', title: '关闭全部', value: 'all' }
|
||||
],
|
||||
tagName: '/index'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('d2admin/page', [
|
||||
'opened',
|
||||
'current'
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
...mapActions('d2admin/page', [
|
||||
'close',
|
||||
'closeLeft',
|
||||
'closeRight',
|
||||
'closeOther',
|
||||
'closeAll',
|
||||
'openedSort'
|
||||
]),
|
||||
/**
|
||||
* @description 计算某个标签页是否可关闭
|
||||
* @param {Object} page 其中一个标签页
|
||||
*/
|
||||
isTabClosable (page) {
|
||||
return page.name !== 'index'
|
||||
},
|
||||
/**
|
||||
* @description 右键菜单功能点击
|
||||
* @param {Object} event 事件
|
||||
*/
|
||||
handleContextmenu (event) {
|
||||
let target = event.target
|
||||
// fix https://github.com/d2-projects/d2-admin/issues/54
|
||||
let flag = false
|
||||
if (target.className.indexOf('el-tabs__item') > -1) flag = true
|
||||
else if (target.parentNode.className.indexOf('el-tabs__item') > -1) {
|
||||
target = target.parentNode
|
||||
flag = true
|
||||
}
|
||||
if (flag) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
this.contentmenuX = event.clientX
|
||||
this.contentmenuY = event.clientY
|
||||
this.tagName = target.getAttribute('aria-controls').slice(5)
|
||||
this.contextmenuFlag = true
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description 右键菜单的 row-click 事件
|
||||
* @param {String} command 事件类型
|
||||
*/
|
||||
contextmenuClick (command) {
|
||||
this.handleControlItemClick(command, this.tagName)
|
||||
},
|
||||
/**
|
||||
* @description 接收点击关闭控制上选项的事件
|
||||
* @param {String} command 事件类型
|
||||
* @param {String} tagName tab 名称
|
||||
*/
|
||||
handleControlItemClick (command, tagName = null) {
|
||||
if (tagName) this.contextmenuFlag = false
|
||||
const params = { pageSelect: tagName }
|
||||
switch (command) {
|
||||
case 'refresh': this.$router.push({ name: 'refresh' }); break
|
||||
case 'left': this.closeLeft(params); break
|
||||
case 'right': this.closeRight(params); break
|
||||
case 'other': this.closeOther(params); break
|
||||
case 'all': this.closeAll(); break
|
||||
default: this.$message.error('无效的操作'); break
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description 接收点击 tab 标签的事件
|
||||
* @param {object} tab 标签
|
||||
* @param {object} event 事件
|
||||
*/
|
||||
handleClick (tab, event) {
|
||||
// 找到点击的页面在 tag 列表里是哪个
|
||||
const page = this.opened.find(page => page.fullPath === tab.name)
|
||||
if (page) {
|
||||
const { name, params, query } = page
|
||||
this.$router.push({ name, params, query })
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description 点击 tab 上的删除按钮触发这里
|
||||
* @param {String} tagName tab 名称
|
||||
*/
|
||||
handleTabRemove (tagName) {
|
||||
this.close({ tagName })
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
const el = document.querySelectorAll('.d2-multiple-page-sort .el-tabs__nav')[0]
|
||||
Sortable.create(el, {
|
||||
onEnd: (evt) => {
|
||||
const { oldIndex, newIndex } = evt
|
||||
this.openedSort({ oldIndex, newIndex })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,3 @@
|
||||
import layout from './layout'
|
||||
|
||||
export default layout
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="d2-layout-header-aside-group" :style="styleLayoutMainGroup" :class="{grayMode: grayActive}">
|
||||
<!-- 半透明遮罩 -->
|
||||
<div class="d2-layout-header-aside-mask"></div>
|
||||
<!-- 主体内容 -->
|
||||
<div class="d2-layout-header-aside-content" flex="dir:top">
|
||||
<!-- 顶栏 -->
|
||||
<div class="d2-theme-header" :style="{ opacity: this.searchActive ? 0.5 : 1 }" flex-box="0" flex>
|
||||
<router-link
|
||||
to="/index"
|
||||
:class="{'logo-group': true, 'logo-transition': asideTransition}"
|
||||
:style="{width: '65px'}"
|
||||
flex-box="0">
|
||||
<img :src="`${$baseUrl}image/theme/${themeActiveSetting.name}/logo/icon-only.png`">
|
||||
</router-link>
|
||||
<d2-menu-header flex-box="1"/>
|
||||
<!-- 顶栏右侧 -->
|
||||
<div v-if="actionBtnVisible" class="d2-header-right" flex-box="0">
|
||||
<d2-header-search @click="handleSearchClick"/>
|
||||
<!-- 如果你只想在开发环境显示这个按钮请添加 v-if="$env === 'development'" -->
|
||||
<d2-header-log v-if="$env === 'development'"/>
|
||||
<d2-header-fullscreen/>
|
||||
<d2-header-theme/>
|
||||
<d2-header-size/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 下面 主体 -->
|
||||
<div class="d2-theme-container" flex-box="1" flex>
|
||||
<!-- 主体 侧边栏 -->
|
||||
<div flex-box="0" :style="{width: '20px'}"></div>
|
||||
<!-- 主体 -->
|
||||
<div class="d2-theme-container-main" flex-box="1" flex>
|
||||
<!-- 搜索 -->
|
||||
<transition name="fade-scale">
|
||||
<div v-if="searchActive" class="d2-theme-container-main-layer" flex>
|
||||
<d2-panel-search ref="panelSearch" @close="searchPanelClose"/>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- 内容 -->
|
||||
<transition name="fade-scale">
|
||||
<div v-if="!searchActive" class="d2-theme-container-main-layer" flex="dir:top">
|
||||
<!-- tab -->
|
||||
<!-- <div class="d2-theme-container-main-header" flex-box="0">-->
|
||||
<!-- <d2-tabs/>-->
|
||||
<!-- </div>-->
|
||||
<!-- 页面 -->
|
||||
<div class="d2-theme-container-main-body" flex-box="1">
|
||||
<transition :name="transitionActive ? 'fade-transverse' : ''">
|
||||
<keep-alive :include="keepAlive">
|
||||
<router-view :key="routerViewKey" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import d2MenuHeader from './components/menu-header'
|
||||
// import d2Tabs from './components/tabs'
|
||||
import d2HeaderFullscreen from './components/header-fullscreen'
|
||||
import d2HeaderSearch from './components/header-search'
|
||||
import d2HeaderSize from './components/header-size'
|
||||
import d2HeaderTheme from './components/header-theme'
|
||||
import d2HeaderLog from './components/header-log'
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import mixinSearch from './mixins/search'
|
||||
export default {
|
||||
name: 'd2-layout-header-aside',
|
||||
mixins: [
|
||||
mixinSearch
|
||||
],
|
||||
components: {
|
||||
d2MenuHeader,
|
||||
// d2Tabs,
|
||||
d2HeaderFullscreen,
|
||||
d2HeaderSearch,
|
||||
d2HeaderSize,
|
||||
d2HeaderTheme,
|
||||
d2HeaderLog
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
// [侧边栏宽度] 正常状态
|
||||
asideWidth: '200px',
|
||||
// [侧边栏宽度] 折叠状态
|
||||
asideWidthCollapse: '65px',
|
||||
// 窗口宽度
|
||||
windowWidth: 375,
|
||||
// 功能按钮显现
|
||||
actionBtnVisible: true
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// 窗口大小改变事件
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
this.windowWidth = window.innerWidth
|
||||
this.actionBtnVisible = this.windowWidth >= 404
|
||||
})()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 实时监听screenWidth的变化
|
||||
// screenWidth: {
|
||||
// handler: function (val, oldVal) {
|
||||
// if (val < 1550) {
|
||||
// console.log('屏幕宽度小于1550px')
|
||||
// } else {
|
||||
// console.log('屏幕宽度大于1550px')
|
||||
// }
|
||||
// },
|
||||
// immediate: true
|
||||
// }
|
||||
},
|
||||
computed: {
|
||||
...mapState('d2admin', {
|
||||
keepAlive: state => state.page.keepAlive,
|
||||
grayActive: state => state.gray.active,
|
||||
transitionActive: state => state.transition.active,
|
||||
asideCollapse: state => state.menu.asideCollapse,
|
||||
asideTransition: state => state.menu.asideTransition
|
||||
}),
|
||||
...mapGetters('d2admin', {
|
||||
themeActiveSetting: 'theme/activeSetting'
|
||||
}),
|
||||
/**
|
||||
* @description 用来实现带参路由的缓存
|
||||
*/
|
||||
routerViewKey () {
|
||||
// 默认情况下 key 类似 __transition-n-/foo
|
||||
// 这里的字符串操作是为了最终 key 的格式和原来相同 类似 __transition-n-__stamp-time-/foo
|
||||
const stamp = this.$route.meta[`__stamp-${this.$route.path}`] || ''
|
||||
return `${stamp ? `__stamp-${stamp}-` : ''}${this.$route.path}`
|
||||
},
|
||||
/**
|
||||
* @description 最外层容器的背景图片样式
|
||||
*/
|
||||
styleLayoutMainGroup () {
|
||||
return this.themeActiveSetting.backgroundImage
|
||||
? { backgroundImage: `url('${this.$baseUrl}${this.themeActiveSetting.backgroundImage}')` }
|
||||
: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// 注册主题
|
||||
@import '~@/assets/style/theme/register.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
|
||||
import hotkeys from 'hotkeys-js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'd2-panel-search': () => import('../components/panel-search')
|
||||
},
|
||||
mounted () {
|
||||
// 绑定搜索功能快捷键 [ 打开 ]
|
||||
hotkeys(this.searchHotkey.open, event => {
|
||||
event.preventDefault()
|
||||
this.searchPanelOpen()
|
||||
})
|
||||
// 绑定搜索功能快捷键 [ 关闭 ]
|
||||
hotkeys(this.searchHotkey.close, event => {
|
||||
event.preventDefault()
|
||||
this.searchPanelClose()
|
||||
})
|
||||
},
|
||||
beforeDestroy () {
|
||||
hotkeys.unbind(this.searchHotkey.open)
|
||||
hotkeys.unbind(this.searchHotkey.close)
|
||||
},
|
||||
computed: {
|
||||
...mapState('d2admin', {
|
||||
searchActive: state => state.search.active,
|
||||
searchHotkey: state => state.search.hotkey
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
searchToggle: 'd2admin/search/toggle',
|
||||
searchSet: 'd2admin/search/set'
|
||||
}),
|
||||
/**
|
||||
* 接收点击搜索按钮
|
||||
*/
|
||||
handleSearchClick () {
|
||||
this.searchToggle()
|
||||
if (this.searchActive) {
|
||||
setTimeout(() => {
|
||||
if (this.$refs.panelSearch) {
|
||||
this.$refs.panelSearch.focus()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
searchPanelOpen () {
|
||||
if (!this.searchActive) {
|
||||
this.searchSet(true)
|
||||
setTimeout(() => {
|
||||
if (this.$refs.panelSearch) {
|
||||
this.$refs.panelSearch.focus()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
// 关闭搜索面板
|
||||
searchPanelClose () {
|
||||
if (this.searchActive) {
|
||||
this.searchSet(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user