1122
This commit is contained in:
25
cdTMP/src/layout/404.vue
Normal file
25
cdTMP/src/layout/404.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="page max-7xl mx-auto text-center">
|
||||
<div class="bg mx-auto">
|
||||
<img src="@/assets/404.svg" />
|
||||
<div class="mt-2">页面未找到,请返回</div>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<a-button type="primary" @click="$router.push({ name: 'Workplace' })">返回第一个有权限页面</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.page {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: -200px;
|
||||
margin-left: -195px;
|
||||
}
|
||||
.bg,
|
||||
.bg img {
|
||||
width: 390px;
|
||||
}
|
||||
</style>
|
||||
15
cdTMP/src/layout/components/footer.vue
Normal file
15
cdTMP/src/layout/components/footer.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-layout-footer class="flex items-center justify-center h-10 footer text-center"
|
||||
>成都测试管理平台</a-layout-footer
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.footer {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
</style>
|
||||
@@ -77,13 +77,13 @@
|
||||
</a-space>
|
||||
</a-doption>
|
||||
<a-doption>
|
||||
<a-space @click="$router.push({ name: 'Info' })">
|
||||
<a-space @click="$router.push({ name: 'Usercenter' })">
|
||||
<icon-user />
|
||||
<span> 用户中心 </span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
<a-doption>
|
||||
<a-space @click="$router.push({ name: 'Setting' })">
|
||||
<a-space @click="$router.push({ name: 'Usercenter' })">
|
||||
<icon-settings />
|
||||
<span> 用户设置 </span>
|
||||
</a-space>
|
||||
|
||||
@@ -13,78 +13,77 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import { listenerRouteChange, removeRouteListener } from '@/utils/route-listener'
|
||||
import { useAppStore, useTabBarStore } from '@/store'
|
||||
import tabItem from './tab-item.vue'
|
||||
<script setup>
|
||||
import { ref, computed, watch, onUnmounted } from "vue"
|
||||
import { listenerRouteChange, removeRouteListener } from "@/utils/route-listener"
|
||||
import { useAppStore, useTabBarStore } from "@/store"
|
||||
import tabItem from "./tab-item.vue"
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tabBarStore = useTabBarStore()
|
||||
const appStore = useAppStore()
|
||||
const tabBarStore = useTabBarStore()
|
||||
|
||||
const affixRef = ref()
|
||||
const tagList = computed(() => {
|
||||
return tabBarStore.getTabList
|
||||
})
|
||||
const offsetTop = computed(() => {
|
||||
return appStore.navbar ? 60 : 0
|
||||
})
|
||||
const affixRef = ref()
|
||||
const tagList = computed(() => {
|
||||
return tabBarStore.getTabList
|
||||
})
|
||||
const offsetTop = computed(() => {
|
||||
return appStore.navbar ? 60 : 0
|
||||
})
|
||||
|
||||
watch(
|
||||
() => appStore.navbar,
|
||||
() => {
|
||||
affixRef.value.updatePosition()
|
||||
}
|
||||
)
|
||||
listenerRouteChange((route) => {
|
||||
if (!route.meta.noAffix && !tagList.value.some((tag) => tag.fullPath === route.fullPath)) {
|
||||
tabBarStore.updateTabList(route)
|
||||
}
|
||||
}, true)
|
||||
watch(
|
||||
() => appStore.navbar,
|
||||
() => {
|
||||
affixRef.value.updatePosition()
|
||||
}
|
||||
)
|
||||
listenerRouteChange((route) => {
|
||||
if (!route.meta.noAffix && !tagList.value.some((tag) => tag.fullPath === route.fullPath)) {
|
||||
tabBarStore.updateTabList(route)
|
||||
}
|
||||
}, true)
|
||||
|
||||
onUnmounted(() => {
|
||||
removeRouteListener()
|
||||
})
|
||||
onUnmounted(() => {
|
||||
removeRouteListener()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tab-bar-container {
|
||||
position: relative;
|
||||
.tab-bar-container {
|
||||
position: relative;
|
||||
background-color: var(--color-bg-2);
|
||||
.tab-bar-box {
|
||||
display: flex;
|
||||
padding: 0 0 0 20px;
|
||||
background-color: var(--color-bg-2);
|
||||
.tab-bar-box {
|
||||
display: flex;
|
||||
padding: 0 0 0 20px;
|
||||
background-color: var(--color-bg-2);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
.tab-bar-scroll {
|
||||
height: 32px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
.tags-wrap {
|
||||
padding: 4px 0;
|
||||
height: 48px;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
.tab-bar-scroll {
|
||||
height: 32px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
.tags-wrap {
|
||||
padding: 4px 0;
|
||||
height: 48px;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
|
||||
:deep(.arco-tag) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
&:first-child {
|
||||
.arco-tag-close-btn {
|
||||
display: none;
|
||||
}
|
||||
:deep(.arco-tag) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
&:first-child {
|
||||
.arco-tag-close-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-bar-operation {
|
||||
width: 100px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-bar-operation {
|
||||
width: 100px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
185
cdTMP/src/layout/components/tab-item.vue
Normal file
185
cdTMP/src/layout/components/tab-item.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<a-dropdown trigger="contextMenu" :popup-max-height="false" @select="actionSelect">
|
||||
<span
|
||||
class="arco-tag arco-tag-size-medium arco-tag-checked"
|
||||
:class="{ 'link-activated': itemData.fullPath === $route.fullPath }"
|
||||
@click="goto(itemData)"
|
||||
>
|
||||
<span class="tag-link"> {{ itemData.title }} </span>
|
||||
<span
|
||||
class="arco-icon-hover arco-tag-icon-hover arco-icon-hover-size-medium arco-tag-close-btn"
|
||||
@click.stop="tagClose(itemData, index)"
|
||||
>
|
||||
<icon-close />
|
||||
</span>
|
||||
</span>
|
||||
<template #content>
|
||||
<a-doption :disabled="disabledReload" :value="Eaction.reload">
|
||||
<icon-refresh />
|
||||
<span>重新加载</span>
|
||||
</a-doption>
|
||||
<a-doption class="sperate-line" :disabled="disabledCurrent" :value="Eaction.current">
|
||||
<icon-close />
|
||||
<span>关闭当前标签页</span>
|
||||
</a-doption>
|
||||
<a-doption :disabled="disabledLeft" :value="Eaction.left">
|
||||
<icon-to-left />
|
||||
<span>关闭左侧标签页</span>
|
||||
</a-doption>
|
||||
<a-doption class="sperate-line" :disabled="disabledRight" :value="Eaction.right">
|
||||
<icon-to-right />
|
||||
<span>关闭右侧标签页</span>
|
||||
</a-doption>
|
||||
<a-doption :value="Eaction.others">
|
||||
<icon-swap />
|
||||
<span>关闭其它标签页</span>
|
||||
</a-doption>
|
||||
<a-doption :value="Eaction.all">
|
||||
<icon-folder-delete />
|
||||
<span>关闭全部标签页</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType, computed } from "vue"
|
||||
import { useRouter, useRoute } from "vue-router"
|
||||
import { useTabBarStore } from "@/store"
|
||||
import { DEFAULT_ROUTE_NAME, REDIRECT_ROUTE_NAME } from "@/router/constants"
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
enum Eaction {
|
||||
reload = "reload",
|
||||
current = "current",
|
||||
left = "left",
|
||||
right = "right",
|
||||
others = "others",
|
||||
all = "all"
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
itemData: {
|
||||
type: Object,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const tabBarStore = useTabBarStore()
|
||||
|
||||
const goto = (tag) => {
|
||||
router.push({ ...tag })
|
||||
}
|
||||
const tagList = computed(() => {
|
||||
return tabBarStore.getTabList
|
||||
})
|
||||
|
||||
const disabledReload = computed(() => {
|
||||
return props.itemData.fullPath !== route.fullPath
|
||||
})
|
||||
|
||||
const disabledCurrent = computed(() => {
|
||||
return props.index === 0
|
||||
})
|
||||
|
||||
const disabledLeft = computed(() => {
|
||||
return [0, 1].includes(props.index)
|
||||
})
|
||||
|
||||
const disabledRight = computed(() => {
|
||||
return props.index === tagList.value.length - 1
|
||||
})
|
||||
|
||||
const tagClose = (tag, idx) => {
|
||||
tabBarStore.deleteTag(idx, tag)
|
||||
if (props.itemData.fullPath === route.fullPath) {
|
||||
const latest = tagList.value[idx - 1] // 获取队列的前一个tab
|
||||
router.push({ name: latest.name })
|
||||
}
|
||||
}
|
||||
|
||||
const findCurrentRouteIndex = () => {
|
||||
return tagList.value.findIndex((el) => el.fullPath === route.fullPath)
|
||||
}
|
||||
const actionSelect = async (value: any) => {
|
||||
const { itemData, index } = props
|
||||
const copyTagList = [...tagList.value]
|
||||
if (value === Eaction.current) {
|
||||
tagClose(itemData, index)
|
||||
} else if (value === Eaction.left) {
|
||||
const currentRouteIdx = findCurrentRouteIndex()
|
||||
copyTagList.splice(1, props.index - 1)
|
||||
|
||||
tabBarStore.freshTabList(copyTagList)
|
||||
if (currentRouteIdx < index) {
|
||||
router.push({ name: itemData.name })
|
||||
}
|
||||
} else if (value === Eaction.right) {
|
||||
const currentRouteIdx = findCurrentRouteIndex()
|
||||
copyTagList.splice(props.index + 1)
|
||||
|
||||
tabBarStore.freshTabList(copyTagList)
|
||||
if (currentRouteIdx > index) {
|
||||
router.push({ name: itemData.name })
|
||||
}
|
||||
} else if (value === Eaction.others) {
|
||||
const filterList = tagList.value.filter((el, idx) => {
|
||||
return idx === 0 || idx === props.index
|
||||
})
|
||||
tabBarStore.freshTabList(filterList)
|
||||
router.push({ name: itemData.name })
|
||||
} else if (value === Eaction.reload) {
|
||||
tabBarStore.deleteCache(itemData)
|
||||
await router.push({
|
||||
name: REDIRECT_ROUTE_NAME,
|
||||
params: {
|
||||
path: route.fullPath
|
||||
}
|
||||
})
|
||||
tabBarStore.addCache(itemData.name)
|
||||
} else {
|
||||
tabBarStore.resetTabList()
|
||||
router.push({ name: DEFAULT_ROUTE_NAME })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tag-link {
|
||||
color: var(--color-text-2);
|
||||
text-decoration: none;
|
||||
}
|
||||
.link-activated {
|
||||
color: rgb(var(--link-6));
|
||||
.tag-link {
|
||||
color: rgb(var(--link-6));
|
||||
}
|
||||
& + .arco-tag-close-btn {
|
||||
color: rgb(var(--link-6));
|
||||
}
|
||||
}
|
||||
:deep(.arco-dropdown-option-content) {
|
||||
span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.arco-dropdown-open {
|
||||
.tag-link {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
.arco-tag-close-btn {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
}
|
||||
.sperate-line {
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
</style>
|
||||
@@ -3,8 +3,8 @@
|
||||
<div class="navbar layout-navbar">
|
||||
<NavBar />
|
||||
</div>
|
||||
<a-layout>
|
||||
<a-layout>
|
||||
<a-layout class="layout">
|
||||
<a-layout class="layout">
|
||||
<a-layout-sider
|
||||
v-if="renderMenu"
|
||||
v-show="!hideMenu"
|
||||
@@ -33,7 +33,11 @@
|
||||
<Menu />
|
||||
</a-drawer>
|
||||
<a-layout class="layout-content" :style="paddingStyle">
|
||||
|
||||
<TabBar v-if="appStore.tabBar" />
|
||||
<a-layout-content class="work-area">
|
||||
<PageLayout />
|
||||
</a-layout-content>
|
||||
<Footer v-if="footer" />
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
@@ -45,12 +49,19 @@ import { ref, computed, watch, provide, onMounted } from "vue"
|
||||
import { useAppStore, useUserStore } from "@/store"
|
||||
import NavBar from "@/layout/components/navbar.vue"
|
||||
import Menu from "@/layout/components/menu.vue"
|
||||
import TabBar from "@/layout/components/tab-bar.vue"
|
||||
import PageLayout from "@/layout/page-layout.vue"
|
||||
import usePermission from "@/hooks/permission"
|
||||
import Footer from "@/layout/components/footer.vue"
|
||||
import useResponsive from "@/hooks/responsive"
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
const permission = usePermission()
|
||||
// 响应式屏幕
|
||||
useResponsive(true)
|
||||
// 初始化数据
|
||||
const isInit = ref(false)
|
||||
const footer = computed(() => appStore.footer)
|
||||
// 是否渲染左侧菜单
|
||||
const renderMenu = computed(() => appStore.menu && !appStore.topMenu)
|
||||
const hideMenu = computed(() => appStore.hideMenu)
|
||||
@@ -161,7 +172,7 @@ const paddingStyle = computed(() => {
|
||||
|
||||
.layout-content {
|
||||
min-height: 100vh;
|
||||
overflow-y: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: var(--color-fill-2);
|
||||
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||
}
|
||||
|
||||
21
cdTMP/src/layout/page-layout.vue
Normal file
21
cdTMP/src/layout/page-layout.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component, route }" class="mx-2 my-2">
|
||||
<transition name="ma-fade" mode="out-in" appear>
|
||||
<!-- 这里主要在路由定义是否缓存页面 -->
|
||||
<component :is="Component" v-if="route.meta.ignoreCache" :key="route.fullPath" />
|
||||
<keep-alive v-else :include="cacheList">
|
||||
<component :is="Component" :key="route.fullPath"></component>
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue"
|
||||
import { useTabBarStore } from "@/store"
|
||||
// 获取缓存列表
|
||||
const tabBarStore = useTabBarStore()
|
||||
const cacheList = computed(() => tabBarStore.getCacheList)
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
Reference in New Issue
Block a user