feat: 🎸 初始化项目

This commit is contained in:
chenkl
2020-12-14 17:32:37 +08:00
commit 26d4c7c568
221 changed files with 23505 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
<template>
<section class="app-main">
<router-view v-slot="{ Component, route }">
<transition name="fade" mode="out-in" appear>
<keep-alive :include="getCaches">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
</router-view>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useCache } from '_c/ParentView/useCache'
export default defineComponent({
name: 'AppMain',
setup() {
const { getCaches } = useCache(true)
return {
getCaches
}
}
})
</script>
<style lang="less" scoped>
.app-main {
overflow: hidden;
padding: 20px;
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<router-link :class="['app-logo', 'app-logo-' + theme]" to="/">
<img :src="require('@/assets/img/logo.png')">
<div v-if="show" class="sidebar-title">{{ title }}</div>
</router-link>
</template>
<script lang="ts">
import { defineComponent, ref, watch, PropType } from 'vue'
import config from '_p/index/config'
export default defineComponent({
name: 'Logo',
props: {
collapsed: {
type: Boolean as PropType<boolean>,
required: true
},
theme: {
type: String as PropType<'light' | 'dark'>,
default: 'dark'
}
},
setup(props) {
const show = ref<boolean>(true)
watch(
() => props.collapsed,
(collapsed: boolean) => {
if (!collapsed) {
setTimeout(() => {
show.value = !collapsed
}, 400)
} else {
show.value = !collapsed
}
}
)
return {
show,
title: config.title
}
}
})
</script>
<style lang="less" scoped>
.app-logo {
display: flex;
align-items: center;
padding-left: 18px;
cursor: pointer;
height: @topSilderHeight;
max-width: 200px;
img {
width: 37px;
height: 37px;
}
.sidebar-title {
font-size: 14px;
font-weight: 700;
transition: .5s;
margin-left: 12px;
}
}
.app-logo-dark {
background-color: @menuBg;
.sidebar-title {
color: #fff;
}
}
.app-logo-light {
background-color: #fff;
.sidebar-title {
color: #000;
}
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<div class="navbar">
<hamburger :collapsed="collapsed" class="hamburger-container" @toggleClick="setCollapsed" />
<breadcrumb class="breadcrumb-container" />
<div class="right-menu">
<screenfull class="right-menu-item hover-effect" />
<user-info class="right-menu-item hover-effect" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import Hamburger from '_c/Hamburger/index.vue'
import Breadcrumb from '_c/Breadcrumb/index.vue'
import Screenfull from '_c/Screenfull/index.vue'
import UserInfo from './UserInfo.vue'
import { appStore } from '_p/index/store/modules/app'
export default defineComponent({
name: 'Navbar',
components: {
Hamburger,
Breadcrumb,
Screenfull,
UserInfo
},
setup() {
const collapsed = computed(() => appStore.collapsed)
function setCollapsed(collapsed: boolean): void {
appStore.SetCollapsed(collapsed)
}
return {
collapsed,
setCollapsed
}
}
})
</script>
<style lang="less" scoped>
.navbar {
.hamburger-container {
line-height: @navbarHeight;
float: left;
cursor: pointer;
margin-left: 15px;
&:hover {
background: rgba(0, 0, 0, .025);
}
}
.breadcrumb-container {
float: left;
}
.right-menu {
float: right;
height: 100%;
line-height: @navbarHeight;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div>
<!-- <i v-if="icon.includes('el-icon')" :class="[icon, 'sub-el-icon', 'anticon']" /> -->
<svg-icon :icon-class="icon" class="anticon" />
<slot name="title">
<span class="anticon-item">{{ title }}</span>
</slot>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'Item',
props: {
icon: {
type: String as PropType<string>,
default: ''
},
title: {
type: String as PropType<string>,
default: ''
}
}
})
</script>
<style lang="less" scoped>
.anticon-item {
opacity: 1;
transition: opacity .3s cubic-bezier(.645,.045,.355,1),width .3s cubic-bezier(.645,.045,.355,1);
}
</style>

View File

@@ -0,0 +1,92 @@
import { ref } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import path from 'path'
import { isExternal } from '@/utils/validate'
// import { setSidebarItem } from './types'
export function setSidebarItem() {
const onlyOneChild = ref<any>(null)
function hasOneShowingChild(children: RouteRecordRaw[] = [], parent: RouteRecordRaw): boolean {
const showingChildren: RouteRecordRaw[] = children.filter((item: RouteRecordRaw) => {
if (item.meta && item.meta.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
onlyOneChild.value = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
return true
}
return false
}
function resolvePath(basePath: string, routePath: string): string {
if (isExternal(routePath)) {
return routePath
}
return path.resolve(basePath, routePath)
}
function treeFindRouter(tree: any[], func: Function, result: RouteRecordRaw[] = []): RouteRecordRaw[] {
if (!tree) return []
for (const data of tree) {
result.push(data)
if (func(data)) return result
if (data.children) {
const findChildren = treeFindRouter(data.children, func, result)
if (findChildren.length) return findChildren
}
result.pop()
}
return []
}
function getFullPath(arr: string[]): string[] {
const result: string[] = []
let basePath = '/'
for (let i = 0; i < arr.length; i++) {
if (i === arr.length) {
continue
}
result.push(resolvePath(basePath, arr[i]))
basePath = resolvePath(basePath, arr[i])
}
return result
}
function findCurrentRoute(routers: RouteRecordRaw[], path: string, basePath = '/', result: Array<any> = []): any {
for (const item of routers) {
if (!item.meta?.hidden) {
const _basePath = resolvePath(basePath, item.path)
if (_basePath === path && !item.children) {
result.push(item)
} else {
if (item.children) {
findCurrentRoute(item.children, path, _basePath, result)
}
}
}
}
return result ? result[0] : null
}
return {
onlyOneChild,
hasOneShowingChild,
resolvePath,
treeFindRouter,
getFullPath,
findCurrentRoute
}
}

View File

@@ -0,0 +1,10 @@
import type { Ref } from 'vue'
import ref from 'vue'
export interface setSidebarItem = {
onlyOneChild: Ref<any | null>
hasOneShowingChild: Function<boolean>
resolvePath: Function<string>
treeFindPath: Function<string[]>
getFullPath: Function<string[]>
}

View File

@@ -0,0 +1,10 @@
.sidebar-container {
height: 100%;
}
.has-logo {
height: calc(~"100% - @{topSilderHeight}");
}
.menu-wrap {
height: 100%;
overflow: hidden;
}

View File

@@ -0,0 +1,3 @@
import Silder from './index'
export default Silder

View File

@@ -0,0 +1,176 @@
import { ref, defineComponent, PropType, computed, watch } from 'vue'
// import { Menu } from 'element-plus'
import { useRouter } from 'vue-router'
import type { RouteRecordRaw, RouteLocationNormalizedLoaded } from 'vue-router'
import Scrollbar from '_c/Scrollbar/index.vue'
import Item from './Item.vue'
import { permissionStore } from '_p/index/store/modules/permission'
import { setSidebarItem } from './hooks/setSidebarItem'
import { isExternal } from '@/utils/validate'
import { findIndex } from '@/utils'
import config from '_p/index/config'
const { show_logo } = config
import './index.less'
export default defineComponent({
name: 'Silder',
components: {
Scrollbar,
Item
},
props: {
collapsed: {
type: Boolean as PropType<boolean>,
default: true
},
inlineIndent: {
type: Number as PropType<number>,
default: 24
},
forceSubMenuRender: {
type: Boolean as PropType<boolean>,
default: false
},
mode: {
type: String as PropType<'vertical' | 'horizontal' | 'vertical-right' | 'inline'>,
default: 'inline'
},
theme: {
type: String as PropType<'light' | 'dark'>,
default: 'dark'
}
},
setup(props) {
const { currentRoute, push } = useRouter()
const { resolvePath, treeFindRouter, getFullPath, findCurrentRoute } = setSidebarItem()
const routers = computed((): RouteRecordRaw[] => {
return permissionStore.routers
})
const selectedKeys = ref<string[]>([])
const openKeys = ref<string[]>([])
const activeMenuName = ref<string>('')
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
setSelectedKeys(route)
},
{
immediate: true
}
)
watch(
() => props.collapsed,
(collapsed: boolean) => {
setOpenKeys(currentRoute.value, collapsed)
},
{
immediate: true
}
)
function handleOpenChange(keyArr: string[]) {
// 需要自己管理openKeys
openKeys.value = keyArr
}
function setOpenKeys(route: RouteLocationNormalizedLoaded, collapsed: boolean) {
let currentRoute: any = null
if (route.meta.activeMenu) {
currentRoute = findCurrentRoute(routers.value, route.meta.activeMenu)
} else {
currentRoute = route
}
const parentRoute: RouteRecordRaw[] = treeFindRouter(permissionStore.routers, (data: any) => data.name === currentRoute.name)
openKeys.value = collapsed ? [] : getFullPath(parentRoute.map((v: RouteRecordRaw) => v.path))
}
function setSelectedKeys(route: RouteLocationNormalizedLoaded) {
const { meta, path, name } = route
activeMenuName.value = name as string
if (meta.activeMenu) {
selectedKeys.value = [meta.activeMenu]
return
}
selectedKeys.value = [path]
}
function highlightMenu(className: string, basePath: string) {
const parentRoute: RouteRecordRaw[] = treeFindRouter(permissionStore.routers, (data: any) => data.name === currentRoute.value.name)
const parentFullPath: string[] = getFullPath(parentRoute.map((v: RouteRecordRaw) => v.path))
return findIndex(parentFullPath, (item: string) => item === basePath) !== -1 ? className : ''
}
function handleMenuClick({ key }: any) {
console.log(key)
if (isExternal(key)) {
window.open(key)
} else {
push({ path: key })
}
}
// function renderMenu(routers: RouteRecordRaw[], basePath = '/') {
// if (routers && routers.length) {
// return routers.map((item: RouteRecordRaw) => {
// if (!item.meta?.hidden) {
// const _basePath = resolvePath(basePath, item.path)
// const { onlyOneChild, hasOneShowingChild } = setSidebarItem()
// if (hasOneShowingChild(item.children, item) && (!onlyOneChild.value.children || onlyOneChild.value.noShowingChildren) && !item.meta?.alwaysShow) {
// return (
// <Menu.Item key={resolvePath(_basePath, onlyOneChild.value.path)} v-slots={{
// default: () => [
// <Item
// icon={onlyOneChild.value.meta && onlyOneChild.value.meta.icon}
// title={onlyOneChild.value.meta?.title}
// />
// ]
// }}>
// </Menu.Item>
// )
// } else {
// return (
// <Menu.SubMenu
// key={_basePath}
// class={highlightMenu(props.theme + '-active-item', _basePath)}
// popupClassName={highlightMenu(props.theme + '-popup-active-item', _basePath)}
// v-slots={{
// title: () => [
// <Item
// icon={item.meta && item.meta.icon}
// title={item.meta?.title}
// />
// ]
// }}
// >
// {renderMenu(item.children as any, _basePath)}
// </Menu.SubMenu>
// )
// }
// }
// })
// }
// }
// return () => {
// return (
// // <div class={{ 'has-logo': show_logo, 'sidebar-container': true }}>
// // <scrollbar class='menu-wrap'>
// // <Menu
// // selectedKeys={selectedKeys.value}
// // openKeys={openKeys.value}
// // theme={props.theme}
// // mode={props.mode}
// // onClick={handleMenuClick}
// // onOpenChange={handleOpenChange}
// // >
// // {renderMenu(routers.value)}
// // </Menu>
// // </scrollbar>
// // </div>
// )
// }
}
})

View File

@@ -0,0 +1,377 @@
<template>
<div ref="wrapper" class="tags-view-container">
<span class="move-btn prev-btn">
<a-button @click="move(-200)">
<template #icon><LeftOutlined /></template>
</a-button>
</span>
<scroll-pane ref="scrollPane" class="tags-view-wrapper">
<div>
<router-link
v-for="tag in visitedViews"
:ref="setTagRef"
:key="tag.path"
:class="isActive(tag) ? 'active' : ''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
class="tags-view-item"
@click.middle="closeSelectedTag(tag)"
@contextmenu.prevent="openMenu(tag, $event)"
>
{{ tag.title }}
<CloseOutlined v-if="!tag.meta.affix" class="icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
</router-link>
</div>
</scroll-pane>
<span class="move-btn next-btn">
<a-button @click="move(200)">
<template #icon><RightOutlined /></template>
</a-button>
</span>
<ul v-show="visible" :style="{left: left + 'px', top: top + 'px'}" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)">
刷新
</li>
<li v-if="!(selectedTag.meta&&selectedTag.meta.affix)" @click="closeSelectedTag(selectedTag)">
关闭
</li>
<li @click="closeOthersTags">
关闭其他
</li>
<li @click="closeAllTags(selectedTag)">
关闭全部
</li>
</ul>
</div>
</template>
<script lang="ts">
import ScrollPane from '_c/ScrollPane/index.vue'
import path from 'path'
import { defineComponent, ref, unref, computed, watch, onMounted, nextTick } from 'vue'
import { tagsViewStore } from '_p/index/store/modules/tagsView'
import { permissionStore } from '_p/index/store/modules/permission'
import { useRouter } from 'vue-router'
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
// import { LeftOutlined, RightOutlined, CloseOutlined } from '@ant-design/icons-vue'
export default defineComponent({
name: 'TagsView',
components: {
ScrollPane,
// CloseOutlined,
// LeftOutlined,
// RightOutlined
},
setup() {
const { currentRoute, push, replace } = useRouter()
const wrapper = ref<HTMLElement | null>(null)
const scrollPane = ref<HTMLElement | null>(null)
const visible = ref<boolean>(false)
const top = ref<number>(0)
const left = ref<number>(0)
const selectedTag = ref<any>({})
const affixTags = ref<any[]>([])
const visitedViews = computed(() => tagsViewStore.visitedViews)
const routers = computed(() => permissionStore.routers)
const tagRefs = ref<any[]>([])
function setTagRef(el: any): void {
tagRefs.value.push(el)
}
function isActive(route: RouteLocationNormalizedLoaded): boolean {
return route.path === currentRoute.value.path
}
function filterAffixTags(routes: RouteRecordRaw[], basePath = '/'): any[] {
let tags: any[] = []
routes.forEach((route: RouteRecordRaw) => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta }
})
}
if (route.children) {
const tempTags: any[] = filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
})
return tags
}
function initTags(): void {
affixTags.value = filterAffixTags(routers.value)
const affixTagArr: any[] = affixTags.value
for (const tag of affixTagArr) {
// Must have tag name
if (tag.name) {
tagsViewStore.addVisitedView(tag)
}
}
}
function addTags(): void | boolean {
const { name } = currentRoute.value
if (name) {
tagsViewStore.addView(currentRoute.value)
}
return false
}
function moveToCurrentTag() {
// TODO 要手动清除tagRefs不然会一直重复后续看看有没有什么解决办法
tagRefs.value = []
const tags = unref(tagRefs)
nextTick(() => {
for (const tag of tags) {
if (tag && tag.to.path === currentRoute.value.path) {
(unref(scrollPane) as any).moveToTarget(tag)
// when query is different then update
if (tag.to.fullPath !== currentRoute.value.fullPath) {
tagsViewStore.updateVisitedView(currentRoute.value)
}
break
}
}
})
}
async function refreshSelectedTag(view: RouteLocationNormalizedLoaded) {
await tagsViewStore.delCachedView()
const { fullPath } = view
nextTick(() => {
replace({
path: '/redirect' + fullPath
})
})
}
async function closeSelectedTag(view: RouteLocationNormalizedLoaded) {
const views: any = await tagsViewStore.delView(view)
if (isActive(view)) {
toLastView(views.visitedViews)
}
}
function closeOthersTags() {
push(selectedTag.value)
tagsViewStore.delOthersViews(selectedTag.value).then(() => {
moveToCurrentTag()
})
}
async function closeAllTags(view: RouteLocationNormalizedLoaded) {
const views: any = await tagsViewStore.delAllViews()
if (affixTags.value.some(tag => tag.path === view.path)) {
return
}
toLastView(views.visitedViews)
}
function toLastView(visitedViews: RouteLocationNormalizedLoaded[]) {
const latestView = visitedViews.slice(-1)[0]
setTimeout(() => {
if (latestView) {
push(latestView)
} else {
// You can set another route
push('/')
}
}, 100)
}
function openMenu(tag: RouteLocationNormalizedLoaded, e: any) {
const menuMinWidth = 105
const offsetLeft: number = (unref(wrapper) as any).getBoundingClientRect().left // container margin left
const offsetWidth: number = (unref(wrapper) as any).offsetWidth // container width
const maxLeft: number = offsetWidth - menuMinWidth// left boundary
const itemLeft: number = e.clientX - offsetLeft + 4
if (itemLeft > maxLeft) {
left.value = maxLeft
} else {
left.value = itemLeft
}
top.value = e.offsetY
visible.value = true
selectedTag.value = tag
}
function closeMenu() {
visible.value = false
}
function move(to: number) {
(unref(scrollPane) as any).moveTo(to)
}
onMounted(() => {
initTags()
addTags()
})
watch(
() => currentRoute.value,
() => {
addTags()
moveToCurrentTag()
}
)
watch(
() => visible.value,
(visible: boolean) => {
if (visible) {
document.body.addEventListener('click', closeMenu)
} else {
document.body.removeEventListener('click', closeMenu)
}
}
)
return {
wrapper, scrollPane,
visible, top, left,
selectedTag, affixTags,
visitedViews, routers,
tagRefs, setTagRef,
isActive,
filterAffixTags,
initTags,
addTags,
moveToCurrentTag,
refreshSelectedTag,
closeSelectedTag,
closeOthersTags,
closeAllTags,
toLastView,
openMenu,
closeMenu,
move
}
}
})
</script>
<style lang="less" scoped>
.tags-view-container {
height: @tagsViewHeight;
width: 100%;
background: #fff;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
position: relative;
display: flex;
z-index: 100;
&::after {
content: "";
width: 100%;
height: 1px;
border-top: #d8dce5;
position: absolute;
left: 0;
bottom: 0;
}
.move-btn {
display: inline-block;
width: @tagsViewHeight;
height: @tagsViewHeight;
line-height: @tagsViewHeight;
text-align: center;
.ant-btn {
width: @tagsViewHeight;
height: @tagsViewHeight;
line-height: @tagsViewHeight;
}
}
.tags-view-wrapper {
width: calc(~"100% - 78px");
.tags-view-item {
display: inline-block;
position: relative;
cursor: pointer;
height: 30px;
line-height: 30px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
&:last-of-type {
margin-right: 4px;
}
&.active {
background-color: #304156;
color: #fff;
border-color: #304156;
&::before {
content: '';
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 5px;
}
}
.icon-close {
width: 16px;
height: 16px;
border-radius: 50%;
text-align: center;
line-height: 16px;
transition: all .3s cubic-bezier(.645, .045, .355, 1);
transform-origin: 100% 50%;
margin-left: 5px;
position: relative;
top: -1px;
&:before {
transform: scale(.6);
display: inline-block;
}
&:hover {
background-color: #b4bccc;
color: #fff;
}
}
}
}
.contextmenu {
margin: 0;
background: #fff;
z-index: 200;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
}
@{deep}(.scrollbar__view) {
height: @tagsViewHeight;
line-height: @tagsViewHeight;
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<a-dropdown class="avatar-container" :trigger="['hover']">
<div>
<div class="avatar-wrapper">
<img :src="require('@/assets/img/avatar.gif')" class="user-avatar">
<span class="name-item">管理员</span>
</div>
</div>
<template #overlay>
<a-menu>
<a-menu-item key="1">
<router-link to="/">
首页
</router-link>
</a-menu-item>
<a-menu-item key="2">
<span style="display: block;" @click="loginOut">退出登录</span>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { resetRouter } from '_p/index/router'
import wsCache from '@/cache'
import { useRouter } from 'vue-router'
export default defineComponent({
name: 'UserInfo',
setup() {
const { replace } = useRouter()
async function loginOut(): Promise<void> {
wsCache.clear()
await resetRouter() // 重置静态路由表
// this.$store.dispatch('delAllViews') // 删除所有的tags标签页
replace('/login')
}
return {
loginOut
}
}
})
</script>
<style lang="less" scoped>
.avatar-container {
margin-right: 30px;
padding: 0 10px;
.avatar-wrapper {
display: flex;
align-items: center;
height: 100%;
cursor: pointer;
.user-avatar {
width: 30px;
height: 30px;
border-radius: 10px;
}
.name-item {
font-size: 14px;
font-weight: 600;
display: inline-block;
margin-left: 5px;
}
}
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<div class="app-wrapper">
<component :is="component" />
</div>
</template>
<script lang="ts">
import Classic from './modules/Classic.vue'
import Top from './modules/Top.vue'
import LeftTop from './modules/LeftTop.vue'
import Test from './modules/Test.vue'
import { defineComponent, ref } from 'vue'
import config from '_p/index/config'
export default defineComponent({
name: 'Layout',
components: {
Classic,
Top,
LeftTop,
Test
},
setup() {
const { layout } = config
const component = ref<string>(layout)
return {
component
}
}
})
</script>
<style lang="less" scoped>
.app-wrapper {
position: relative;
height: 100%;
width: 100%;
}
</style>

View File

@@ -0,0 +1,112 @@
<template>
<a-layout class="app-wrapper">
<a-layout-sider
v-model:collapsed="collapsed"
:trigger="null"
collapsible
:class="'ant-layout-sider--' + theme"
class="sidebar-container-wrap"
>
<logo
v-if="show_logo"
:collapsed="collapsed"
:theme="theme"
/>
<silder
:collapsed="collapsed"
:theme="theme"
/>
</a-layout-sider>
<a-layout>
<a-layout-header :class="{'ant-layout-header-collapsed': collapsed}">
<navbar />
</a-layout-header>
<a-layout-content :class="{'layout-content-has-tags':has_tags}">
<scrollbar class="main-wrap">
<tags-view :class="{'has-tags':has_tags, 'has-tags-collapsed': collapsed && has_tags}" />
<app-main class="classic-module--wrap" />
</scrollbar>
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from 'vue'
import { appStore } from '_p/index/store/modules/app'
import Silder from '../components/Silder'
import Navbar from '../components/Navbar.vue'
import AppMain from '../components/AppMain.vue'
import TagsView from '../components/TagsView.vue'
import Logo from '../components/Logo.vue'
import Scrollbar from '_c/Scrollbar/index.vue'
import config from '_p/index/config'
const { show_logo, has_tags, theme } = config
export default defineComponent({
name: 'Classic',
components: {
Silder,
Navbar,
AppMain,
TagsView,
Logo,
Scrollbar
},
props: {
theme: {
type: String as PropType<'light' | 'dark'>,
default: theme
}
},
setup() {
const collapsed = computed(() => appStore.collapsed)
return {
collapsed,
show_logo,
has_tags
}
}
})
</script>
<style lang="less" scoped>
@{deep}(.ant-layout-header) {
line-height: @navbarHeight;
height: @navbarHeight;
position: fixed;
top: 0;
left: @menuWidth;
width: calc(~"100% - @{menuWidth}");
padding: 0;
background: #fff;
transition: all 0.2s;
}
.ant-layout-sider--light {
background: @menuLightBg;
}
.ant-layout-sider--dark {
background: @menuBg;
}
.ant-layout-content {
margin-top: @navbarHeight;
.mian-wrap {
background-color: @contentBg;
}
}
.layout-content-has-tags {
margin-top: @navbarHeight + @tagsViewHeight;
}
.has-tags {
position: fixed;
top: @navbarHeight;
left: @menuWidth;
width: calc(~"100% - @{menuWidth}");
transition: all 0.2s;
}
.ant-layout-header-collapsed,
.has-tags-collapsed {
left: 80px;
width: calc(~"100% - 80px");
}
</style>

View File

@@ -0,0 +1,185 @@
<template>
<a-layout>
<a-layout-header :class="'ant-layout-header--' + silderTheme">
<logo
v-if="show_logo"
:theme="silderTheme"
/>
<silder
:theme="silderTheme"
mode="horizontal"
class="header-silder--wrap"
/>
<div class="right-menu">
<screenfull class="right-menu-item hover-effect screenfull-item" />
<user-info class="right-menu-item hover-effect" />
</div>
</a-layout-header>
<a-layout class="layout-content">
<a-layout-sider
v-model:collapsed="collapsed"
:trigger="null"
collapsible
:class="'ant-layout-sider--' + theme"
class="sidebar-container-wrap"
>
<silder
:collapsed="collapsed"
:theme="theme"
class="left-sider-wrap"
/>
</a-layout-sider>
<a-layout>
<div class="navbar-wrap">
<hamburger :collapsed="collapsed" class="hamburger-container" @toggleClick="setCollapsed" />
<breadcrumb class="breadcrumb-container" />
</div>
<a-layout-content :class="{'layout-content-has-tags':has_tags}">
<scrollbar class="main-wrap">
<tags-view :class="{'has-tags':has_tags, 'has-tags-collapsed': collapsed && has_tags}" />
<app-main class="classic-module--wrap" />
</scrollbar>
</a-layout-content>
</a-layout>
</a-layout>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from 'vue'
import Logo from '../components/Logo.vue'
import Silder from '../components/Silder'
import UserInfo from '../components/UserInfo.vue'
import TagsView from '../components/TagsView.vue'
import AppMain from '../components/AppMain.vue'
import Screenfull from '_c/Screenfull/index.vue'
import Scrollbar from '_c/Scrollbar/index.vue'
import Hamburger from '_c/Hamburger/index.vue'
import Breadcrumb from '_c/Breadcrumb/index.vue'
import { appStore } from '_p/index/store/modules/app'
import config from '_p/index/config'
const { show_logo, has_tags, theme } = config
export default defineComponent({
name: 'LeftTop',
components: {
Logo,
Silder,
UserInfo,
TagsView,
AppMain,
Screenfull,
Scrollbar,
Hamburger,
Breadcrumb
},
props: {
theme: {
type: String as PropType<'light' | 'dark'>,
default: theme
},
silderTheme: {
type: String as PropType<'light' | 'dark'>,
default: 'light'
}
},
setup() {
const collapsed = computed(() => appStore.collapsed)
function setCollapsed(collapsed: boolean): void {
appStore.SetCollapsed(collapsed)
}
return {
collapsed,
show_logo,
has_tags,
setCollapsed
}
}
})
</script>
<style lang="less" scoped>
@{deep}(.ant-layout-header) {
height: @topSilderHeight;
display: flex;
flex-direction: row;
justify-content: space-between;
position: fixed;
width: 100%;
top: 0;
left: 0;
z-index: 20;
&--light {
background: @menuLightBg;
}
&--dark {
background: @menuBg;
.screenfull-item,
.name-item {
color: #fff;
}
}
.header-silder--wrap {
flex: 1;
margin: 0 50px;
height: 100% !important;
.ant-menu-horizontal {
height: @topSilderHeight;
line-height: @topSilderHeight;
border-bottom: 0;
}
.ant-menu-light {
height: calc(~"@{topSilderHeight} - 4px");
line-height: calc(~"@{topSilderHeight} - 2px");
}
}
.right-menu {
display: flex;
.screenfull-item {
line-height: @topSilderHeight;
}
.avatar-container {
margin-right: 0;
}
}
}
.ant-layout-sider--light {
background: @menuLightBg;
}
.ant-layout-sider--dark {
background: @menuBg;
}
.layout-content {
margin-top: @topSilderHeight;
height: calc(~"100vh - @{topSilderHeight}");
.navbar-wrap {
background: #fff;
padding: 0 20px;
border-top: 1px solid #f0f0f0;
height: @navbarHeight;
line-height: @navbarHeight;
display: flex;
}
.layout-content-has-tags {
margin-top: @tagsViewHeight;
.mian-wrap {
background-color: @contentBg;
}
}
.left-sider-wrap {
height: 100%;
}
.has-tags {
position: fixed;
top: @topSilderHeight + @navbarHeight;
left: @menuWidth;
width: calc(~"100% - @{menuWidth}");
z-index: 20;
transition: all 0.2s;
}
.has-tags-collapsed {
left: 80px;
width: calc(~"100% - 80px");
}
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<div class="app__wrap">
<div class="sidebar__wrap">
<logo
v-if="show_logo"
:collapsed="collapsed"
/>
<sider /></div>
<div class="main__wrap">
<div class="navbar__wrap" />
<div class="tags__wrap" />
<div class="main__wrap" />
</div>
<!-- <sidebar class="sidebar-wrap" />
<div :class="{hasTagsView: has_tags}" class="main-wrap">
<div>
<navbar />
<tags-view v-if="has_tags" />
</div>
<app-main />
</div> -->
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { appStore } from '_p/index/store/modules/app'
import AppMain from '../components/AppMain.vue'
import TagsView from '../components/TagsView.vue'
import Logo from '_c/Logo/index.vue'
import Scrollbar from '_c/Scrollbar/index.vue'
import Sider from '_c/Sider/index.vue'
import Navbar from '../components/Navbar.vue'
import config from '_p/index/config'
const { show_logo, has_tags } = config
export default defineComponent({
name: 'Layout',
components: {
Sider,
Navbar,
AppMain,
TagsView,
Logo,
Scrollbar
},
setup() {
return {
show_logo, has_tags
}
}
})
</script>
<style lang="less" scoped>
.app-wrap {
position: relative;
height: 100%;
width: 100%;
}
</style>

View File

@@ -0,0 +1,128 @@
<template>
<a-layout>
<a-layout-header :class="'ant-layout-header--' + theme">
<logo
v-if="show_logo"
:theme="theme"
/>
<silder
:theme="theme"
mode="horizontal"
class="header-silder--wrap"
/>
<div class="right-menu">
<screenfull class="right-menu-item hover-effect screenfull-item" />
<user-info class="right-menu-item hover-effect" />
</div>
</a-layout-header>
<a-layout-content :class="{'layout-content-has-tags':has_tags}">
<scrollbar class="main-wrap">
<tags-view :class="{'has-tags':has_tags}" />
<app-main class="top-module--wrap" />
</scrollbar>
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import Logo from '../components/Logo.vue'
import Silder from '../components/Silder'
import UserInfo from '../components/UserInfo.vue'
import TagsView from '../components/TagsView.vue'
import AppMain from '../components/AppMain.vue'
import Screenfull from '_c/Screenfull/index.vue'
import Scrollbar from '_c/Scrollbar/index.vue'
import config from '_p/index/config'
const { show_logo, has_tags, theme } = config
export default defineComponent({
name: 'Top',
components: {
Logo,
Silder,
UserInfo,
TagsView,
AppMain,
Screenfull,
Scrollbar
},
props: {
theme: {
type: String as PropType<'light' | 'dark'>,
default: theme
}
},
setup() {
return {
show_logo,
has_tags
}
}
})
</script>
<style lang="less" scoped>
@{deep}(.ant-layout-header) {
height: @topSilderHeight;
display: flex;
flex-direction: row;
justify-content: space-between;
position: fixed;
width: 100%;
top: 0;
left: 0;
z-index: 20;
&--light {
background: @menuLightBg;
}
&--dark {
background: @menuBg;
.screenfull-item,
.name-item {
color: #fff;
}
}
.header-silder--wrap {
flex: 1;
margin: 0 50px;
height: 100% !important;
.ant-menu-horizontal {
height: @topSilderHeight;
line-height: @topSilderHeight;
border-bottom: 0;
}
.ant-menu-light {
height: calc(~"@{topSilderHeight} - 4px");
line-height: calc(~"@{topSilderHeight} - 4px");
}
}
.right-menu {
display: flex;
.screenfull-item {
line-height: @topSilderHeight;
}
.avatar-container {
margin-right: 0;
}
}
}
.ant-layout-content {
margin-top: @topSilderHeight;
height: calc(~"100vh - @{topSilderHeight}");
.main-wrap {
background-color: @contentBg;
}
}
.layout-content-has-tags {
margin-top: @topSilderHeight + @tagsViewHeight;
height: calc(~"100vh - @{topSilderHeight} - @{tagsViewHeight}");
}
.has-tags {
position: fixed;
top: @topSilderHeight;
left: 0;
width: 100%;
z-index: 20;
}
</style>