导航面包屑修改为tab页签

This commit is contained in:
灌糖包子 2023-10-22 00:24:48 +08:00
parent ce19454b5e
commit ac3388f35d
Signed by: sookie
GPG Key ID: 0599BECB75C1E68D
10 changed files with 86 additions and 131 deletions

5
components.d.ts vendored
View File

@ -9,10 +9,7 @@ declare module '@vue/runtime-core' {
export interface GlobalComponents {
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAside: typeof import('element-plus/es')['ElAside']
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
@ -37,6 +34,8 @@ declare module '@vue/runtime-core' {
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTransfer: typeof import('element-plus/es')['ElTransfer']
ElTree: typeof import('element-plus/es')['ElTree']

View File

@ -20,12 +20,5 @@ export default [
{ title: '图片资源库', path: '/api/sourceImage' },
{ title: '歌曲库', path: '/api/music' }
]
},{
name: 'tool',
title: '工具',
icon: 'Tools',
child: [
{ title: 'SQL占位符替换', path: '/tool/sqlReplace' }
]
}
]

View File

@ -1,6 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
import { router, routePathes, filterExclude } from './router'
import { router, filterExclude } from './router'
import store from './store'
import VueAxios from 'vue-axios'
@ -50,7 +50,10 @@ service.interceptors.response.use(res=> {
// 全局路由导航前置守卫
router.beforeEach(function (to, from, next) {
mountedApp.$store.commit('setBreadcrumb', routePathes[to.path] || [])
if (to.meta?.title) {
mountedApp.$store.commit('addTab', { title: to.meta.title, path: to.path, name: to.name })
}
mountedApp.$store.state.activeTab = to.path
if(filterExclude.indexOf(to.path) !== -1 || mountedApp.$store.state.loginInfo.token) {
next()
} else {

View File

@ -8,17 +8,16 @@ const routes: Array<RouteRecordRaw> = [
{ path: '/login', name: 'Login', component: Login },
{ path: '/', name: 'Home', component: Home, children: [
{ path: '/', name: 'Welcome', component: Welcome },
{ path: '/system/user', name: 'SystemUser', component: () => import( /* webpackChunkName: "system" */ '@/views/system/SystemUser.vue') },
{ path: '/system/role', name: 'SystemRole', component: () => import( /* webpackChunkName: "system" */ '@/views/system/SystemRole.vue') },
{ path: '/system/config', name: 'SystemConfig', component: () => import( /* webpackChunkName: "system" */ '@/views/system/SystemConfig.vue') },
{ path: '/system/article', name: 'Article', component: () => import( /* webpackChunkName: "system" */ '@/views/system/Article.vue') },
{ path: '/system/statistics', name: 'Statistics', component: () => import( /* webpackChunkName: "system" */ '@/views/system/Statistics.vue') },
{ path: '/system/user', name: 'SystemUser', component: () => import( /* webpackChunkName: "system" */ '@/views/system/SystemUser.vue'), meta: { title: '用户管理' } },
{ path: '/system/role', name: 'SystemRole', component: () => import( /* webpackChunkName: "system" */ '@/views/system/SystemRole.vue'), meta: { title: '角色管理' } },
{ path: '/system/config', name: 'SystemConfig', component: () => import( /* webpackChunkName: "system" */ '@/views/system/SystemConfig.vue'), meta: { title: '系统配置' } },
{ path: '/system/article', name: 'Article', component: () => import( /* webpackChunkName: "system" */ '@/views/system/Article.vue'), meta: { title: '博客文章' } },
{ path: '/system/statistics', name: 'Statistics', component: () => import( /* webpackChunkName: "system" */ '@/views/system/Statistics.vue'), meta: { title: '分析统计' } },
{ path: '/api/music', name: 'Music', component: () => import( /* webpackChunkName: "api" */ '@/views/api/Music.vue') },
{ path: '/api/hitokoto', name: 'Hitokoto', component: () => import( /* webpackChunkName: "api" */ '@/views/api/Hitokoto.vue') },
{ path: '/api/photoWall', name: 'PhotoWall', component: () => import( /* webpackChunkName: "api" */ '@/views/api/PhotoWall.vue') },
{ path: '/api/sourceImage', name: 'SourceImage', component: () => import( /* webpackChunkName: "api" */ '@/views/api/SourceImage.vue') },
{ path: '/tool/sqlReplace', name: 'SqlReplace', component: () => import( /* webpackChunkName: "api" */ '@/views/tool/SqlReplace.vue') },
{ path: '/api/music', name: 'Music', component: () => import( /* webpackChunkName: "api" */ '@/views/api/Music.vue'), meta: { title: '歌曲库' } },
{ path: '/api/hitokoto', name: 'Hitokoto', component: () => import( /* webpackChunkName: "api" */ '@/views/api/Hitokoto.vue'), meta: { title: '一言' } },
{ path: '/api/photoWall', name: 'PhotoWall', component: () => import( /* webpackChunkName: "api" */ '@/views/api/PhotoWall.vue'), meta: { title: '照片墙' } },
{ path: '/api/sourceImage', name: 'SourceImage', component: () => import( /* webpackChunkName: "api" */ '@/views/api/SourceImage.vue'), meta: { title: '图片资源库' } },
]}
]
@ -27,14 +26,4 @@ export const router = createRouter({
routes
})
import menus from './config/menu'
export const routePathes : {[propName: string]: string[]} = {
'/': ['首页'],
}
for(let menu of menus) {
for(let submenu of menu.child) {
routePathes[submenu.path] = ['首页', menu.title, submenu.title]
}
}
export const filterExclude = ['/login']

View File

@ -1,4 +1,3 @@
@panel-shadow-color: #ddd;
html,body,#app,.layout {
margin: 0;
padding: 0;
@ -25,14 +24,14 @@ html,body,#app,.layout {
padding: 10px !important;
display: flex !important;
flex-direction: column;
> .layout-breadcrumb{
padding: 10px 15px 0;
> .layout-tabs{
margin: 15px 15px 0 15px;
}
> .layout-content{
margin: 15px;
margin: 0 15px 15px 15px;
overflow: auto;
background: #fff;
box-shadow: -2px -2px 5px 0px @panel-shadow-color;
border: 1px solid #e1e1e1;
padding: 10px;
flex-grow: 1;
flex-basis: 0;
@ -74,4 +73,8 @@ html,body,#app,.layout {
> .el-drawer__body {
overflow: auto;
}
}
.el-tabs--card.nav-tabs > .el-tabs__header {
margin-bottom: 0;
border-bottom: none;
}

View File

@ -1,16 +1,17 @@
import { createStore } from 'vuex'
import { StateType, UserInfo } from './types'
import { StateType, UserInfo, TabItem } from './types'
class Store {
constructor(breadcrumb: string[]) {
this.state.breadcrumb = breadcrumb
constructor(tabs: TabItem[]) {
this.state.tabs = tabs
}
state: StateType = {
loginInfo: {
userInfo: null,
token: localStorage.getItem('login_token')
},
breadcrumb: [],
tabs: [],
activeTab: null,
pageSizeOpts: [10, 20, 50, 100],
pageLayout: 'sizes, prev, pager, next, total, ->, jumper'
}
@ -35,16 +36,30 @@ class Store {
state.loginInfo.userInfo = null
},
/**
*
*
* @param {Object} state
* @param {Array} breadcrumbArr
* @param {TabItem} tab
*/
setBreadcrumb(state: StateType, breadcrumbArr: string[]): void {
localStorage.setItem('breadcrumb', JSON.stringify(breadcrumbArr))
state.breadcrumb = breadcrumbArr
addTab(state: StateType, tab: TabItem): void {
if (state.tabs.findIndex(item => item.path === tab.path) === -1) {
state.tabs.push(tab)
localStorage.setItem('tabs', JSON.stringify(state.tabs))
}
},
/**
*
* @param {Object} state
* @param {TabItem} name
*/
removeTab(state: StateType, path: string): void {
const removeIndex = state.tabs.findIndex(item => item.path === path)
if (removeIndex !== -1) {
state.tabs.splice(removeIndex, 1)
localStorage.setItem('tabs', JSON.stringify(state.tabs))
}
}
}
}
const breadcrumbStr = localStorage.getItem('breadcrumb')
export default createStore(new Store(breadcrumbStr ? JSON.parse(breadcrumbStr) : []))
const tabsStr = localStorage.getItem('tabs')
export default createStore(new Store(tabsStr ? JSON.parse(tabsStr) : []))

View File

@ -5,12 +5,19 @@ export interface UserInfo {
role_ids: string[] // 角色ID
}
export interface TabItem {
title: string // 标题文字
path: string // 路由地址
name: string // 组件名称
}
export interface StateType {
loginInfo: { // 登录信息
userInfo: null | UserInfo
token: null | string
}
breadcrumb: string[] // 面包屑导航文字
tabs: TabItem[] // 导航页签
activeTab: string | null // 当前激活的页签
pageSizeOpts: number[] // 分页大小可选列表
pageLayout: string // 分页工具栏
}

View File

@ -21,7 +21,7 @@
</el-header>
<el-container>
<el-aside width="200px">
<el-menu :default-active="activeMenuItem" :default-openeds="openMenuNames" style="height: 100%;">
<el-menu :default-active="defaultActiveMenuKey" :default-openeds="openMenuNames" style="height: 100%;">
<el-sub-menu v-for="menu in menus" :key="menu.name" :index="menu.name">
<template #title>
<component :is="menu.icon" style="width: 18px; margin-right: 5px;"></component>
@ -34,13 +34,18 @@
</el-menu>
</el-aside>
<el-main class="layout-main">
<div class="layout-breadcrumb">
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="(item,index) in $store.state.breadcrumb" :key="index">{{item}}</el-breadcrumb-item>
</el-breadcrumb>
<div class="layout-tabs">
<el-tabs type="card" class="nav-tabs" v-model="$store.state.activeTab" @tab-change="openMenu" @tab-remove="removeTab">
<el-tab-pane label="首页" name="/"></el-tab-pane>
<el-tab-pane v-for="tab in $store.state.tabs" :key="tab.name" :label="tab.title" :name="tab.path" closable></el-tab-pane>
</el-tabs>
</div>
<div class="layout-content">
<router-view class="main-view"></router-view>
<router-view class="main-view" v-slot="{ Component }">
<keep-alive :include="keepViews">
<component :is="Component" />
</keep-alive>
</router-view>
</div>
<div class="layout-copy">2016-{{currentYear}} &copy; colorfulsweet 上次更新{{ version }}</div>
</el-main>
@ -60,16 +65,21 @@ export default class Home extends Vue{
currentYear = new Date().getFullYear()
//
menus = menus
activeMenuItem: string | null = null
defaultActiveMenuKey: string | null = null
openMenuNames: string[] = []
get realname(): null | string { //
return this.$store.state.loginInfo.userInfo
? this.$store.state.loginInfo.userInfo.realname : null
}
get keepViews(): string[] {
return this.$store.state.tabs
.filter(item => item.name)
.map(item => item.name)
}
async created(): Promise<void> {
this.activeMenuItem = this.$route.path
if(this.activeMenuItem) {
let result = /^\/(.*)\//.exec(this.activeMenuItem)
this.defaultActiveMenuKey = this.$route.path
if(this.defaultActiveMenuKey) {
let result = /^\/(.*)\//.exec(this.defaultActiveMenuKey)
if(result) {
this.openMenuNames.push(result[1])
}
@ -103,5 +113,12 @@ export default class Home extends Vue{
openMenu(path: string): void {
this.$router.push(path)
}
removeTab(path: string): void {
this.$store.commit('removeTab', path)
if (this.$store.state.activeTab === path) { //
const { tabs } = this.$store.state
this.openMenu(tabs[tabs.length - 1]?.path || '/')
}
}
}
</script>

View File

@ -106,7 +106,7 @@
v-model="markdownPreview.show"
:title="markdownPreview.title"
direction="rtl"
custom-class="custom-drawer">
class="custom-drawer">
<div v-html="markdownPreview.content"></div>
</el-drawer>
</div>

View File

@ -1,71 +0,0 @@
<template>
<div>
<el-form :label-width="100">
<el-form-item label="SQL语句">
<el-input v-model="sql" type="textarea" :rows="5"/>
</el-form-item>
<el-form-item label="参数">
<el-input v-model="params" type="textarea" :rows="3"/>
</el-form-item>
<el-form-item label="替换结果">
<el-input v-model="replaceResult" ref="resultInput" readonly/>
</el-form-item>
</el-form>
<div style="text-align:center">
<el-button-group size="large" >
<el-button type="primary" icon="DocumentCopy" @click="replacePlaceholder">替换占位符</el-button>
<el-button type="default" @click="clear">清空</el-button>
</el-button-group>
</div>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
import { ElInput, ElMessage } from 'element-plus'
@Options({
name: 'SqlReplace'
})
export default class SqlReplace extends Vue {
sql: string = ''
params: string = ''
replaceResult: string = ''
replacePlaceholder() {
let sql = this.sql
const reg = /(.+?)\s*\((String|Integer|Boolean)\),?/g
let execResult = reg.exec(this.params)
let replaceMent = ''
while(execResult && sql.indexOf('?') !== -1) {
switch(execResult[2]) {
case 'String':
replaceMent = `'${execResult[1].trim()}'`
break
case 'Integer':
replaceMent = execResult[1].trim()
break
case 'Boolean':
replaceMent = eval(execResult[1]) ? '1' : '0'
break
}
sql = sql.replace('?', replaceMent)
execResult = reg.exec(this.params)
}
this.replaceResult = sql
Promise.resolve('已复制到剪贴板').then(message => {
(this.$refs.resultInput as typeof ElInput).select()
if (document.execCommand('copy')) {
ElMessage.success(message)
} else {
ElMessage.warning('复制失败,请手动复制')
}
})
}
clear() {
this.sql = ''
this.params = ''
this.replaceResult = ''
}
}
</script>