fix(frontend): полная зачистка UI legacy и унификация брендовой палитры (fix41)
This commit is contained in:
@@ -1,16 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore } from "@/store/modules/tagsView";
|
||||
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="router-fade" mode="out-in">
|
||||
<keep-alive :include="tagsViewStore.cachedViews">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</section>
|
||||
@@ -21,27 +13,10 @@ const tagsViewStore = useTagsViewStore();
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
/* 50= navbar 50 */
|
||||
min-height: calc(100vh - 50px);
|
||||
overflow: hidden;
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
min-height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
min-height: 100vh;
|
||||
padding-top: 84px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -3,11 +3,9 @@ import { storeToRefs } from "pinia";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { useTagsViewStore } from "@/store/modules/tagsView";
|
||||
import { useAdminStore } from "@/store/modules/admin";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
const adminStore = useAdminStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -39,9 +37,6 @@ function logout() {
|
||||
}).then(() => {
|
||||
adminStore
|
||||
.logout()
|
||||
.then(() => {
|
||||
tagsViewStore.delAllViews();
|
||||
})
|
||||
.then(() => {
|
||||
router.push(`/login?redirect=${route.fullPath}`);
|
||||
});
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
|
||||
import IconEpSunny from "~icons/ep/sunny";
|
||||
import IconEpMoon from "~icons/ep/moon";
|
||||
|
||||
/**
|
||||
* Тёмная тема.
|
||||
*/
|
||||
const settingsStore = useSettingsStore();
|
||||
const isDark = useDark();
|
||||
const toggleDark = () => useToggle(isDark);
|
||||
|
||||
/**
|
||||
* Переключение layout.
|
||||
*/
|
||||
function changeLayout(layout: string) {
|
||||
settingsStore.changeSetting({ key: "layout", value: layout });
|
||||
window.document.body.setAttribute("layout", settingsStore.layout);
|
||||
}
|
||||
|
||||
// Цвета темы
|
||||
const themeColors = ref<string[]>([
|
||||
"#409EFF",
|
||||
"#304156",
|
||||
"#11a983",
|
||||
"#13c2c2",
|
||||
"#6959CD",
|
||||
"#f5222d",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Переключение цвета темы.
|
||||
*/
|
||||
function changeThemeColor(color: string) {
|
||||
settingsStore.changeSetting({ key: "themeColor", value: color });
|
||||
document.documentElement.style.setProperty(
|
||||
"--el-color-primary",
|
||||
settingsStore.themeColor
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.document.body.setAttribute("layout", settingsStore.layout);
|
||||
document.documentElement.style.setProperty(
|
||||
"--el-color-primary",
|
||||
settingsStore.themeColor
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<h3 class="text-base font-bold">Настройки проекта</h3>
|
||||
<el-divider>Тема</el-divider>
|
||||
|
||||
<div class="flex justify-center" @click.stop>
|
||||
<el-switch
|
||||
v-model="isDark"
|
||||
@change="toggleDark"
|
||||
inline-prompt
|
||||
:active-icon="IconEpMoon"
|
||||
:inactive-icon="IconEpSunny"
|
||||
active-color="var(--el-fill-color-dark)"
|
||||
inactive-color="var(--el-color-primary)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-divider>Настройки интерфейса</el-divider>
|
||||
<div class="py-[8px] flex justify-between">
|
||||
<span class="text-xs">Включить вкладки</span>
|
||||
<el-switch v-model="settingsStore.tagsView" />
|
||||
</div>
|
||||
|
||||
<div class="py-[8px] flex justify-between">
|
||||
<span class="text-xs">Зафиксировать header</span>
|
||||
<el-switch v-model="settingsStore.fixedHeader" />
|
||||
</div>
|
||||
|
||||
<div class="py-[8px] flex justify-between">
|
||||
<span class="text-xs">Логотип в боковом меню</span>
|
||||
<el-switch v-model="settingsStore.sidebarLogo" />
|
||||
</div>
|
||||
|
||||
<el-divider>Цвет темы</el-divider>
|
||||
|
||||
<ul class="w-full space-x-2 flex justify-center py-2">
|
||||
<li
|
||||
class="inline-block w-[30px] h-[30px] cursor-pointer"
|
||||
v-for="(color, index) in themeColors"
|
||||
:key="index"
|
||||
:style="{ background: color }"
|
||||
@click="changeThemeColor(color)"
|
||||
></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings-container {
|
||||
padding: 16px;
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
width: 18%;
|
||||
height: 45px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: #f0f2f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&-item.is-active {
|
||||
border: 2px solid var(--el-color-primary);
|
||||
}
|
||||
|
||||
&-mix div:nth-child(1) {
|
||||
width: 100%;
|
||||
height: 30%;
|
||||
background: #1b2a47;
|
||||
box-shadow: 0 0 1px #888;
|
||||
}
|
||||
|
||||
&-mix div:nth-child(2) {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 30%;
|
||||
height: 70%;
|
||||
background: #1b2a47;
|
||||
box-shadow: 0 0 1px #888;
|
||||
}
|
||||
|
||||
&-top div:nth-child(1) {
|
||||
width: 100%;
|
||||
height: 30%;
|
||||
background: #1b2a47;
|
||||
box-shadow: 0 0 1px #888;
|
||||
}
|
||||
|
||||
&-left div:nth-child(1) {
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
background: #1b2a47;
|
||||
}
|
||||
|
||||
&-left div:nth-child(2) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 70%;
|
||||
height: 30%;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 1px #888;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
defineProps({
|
||||
collapse: {
|
||||
@@ -14,33 +11,58 @@ const logo = ref(new URL(`../../../assets/logo.png`, import.meta.url).href);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-[50px] bg-gray-800 dark:bg-[var(--el-bg-color-overlay)]">
|
||||
<div class="sidebar-logo">
|
||||
<transition name="sidebarLogoFade">
|
||||
<router-link
|
||||
v-if="collapse"
|
||||
key="collapse"
|
||||
class="h-full w-full flex items-center justify-center"
|
||||
class="sidebar-logo-link"
|
||||
to="/"
|
||||
>
|
||||
<img v-if="settingsStore.sidebarLogo" :src="logo" class="w-5 h-5" />
|
||||
<span v-else class="ml-3 text-white text-sm font-bold">HY2XS</span>
|
||||
<img :src="logo" class="sidebar-logo-image" />
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-else
|
||||
key="expand"
|
||||
class="h-full w-full flex items-center justify-center"
|
||||
class="sidebar-logo-link"
|
||||
to="/"
|
||||
>
|
||||
<img v-if="settingsStore.sidebarLogo" :src="logo" class="w-5 h-5" />
|
||||
<span class="ml-3 text-white text-sm font-bold">HY2XS</span>
|
||||
<img :src="logo" class="sidebar-logo-image" />
|
||||
<span class="sidebar-logo-title">HY2XS</span>
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// https://cn.vuejs.org/guide/built-ins/transition.html#the-transition-component
|
||||
.sidebar-logo {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background-color: var(--menuBg);
|
||||
border-bottom: 1px solid rgb(255 255 255 / 6%);
|
||||
}
|
||||
|
||||
.sidebar-logo-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar-logo-image {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.sidebar-logo-title {
|
||||
margin-left: 12px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sidebarLogoFade-enter-active {
|
||||
transition: opacity 2s;
|
||||
}
|
||||
|
||||
@@ -4,23 +4,19 @@ import { useRoute } from "vue-router";
|
||||
import SidebarItem from "./SidebarItem.vue";
|
||||
import Logo from "./Logo.vue";
|
||||
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
import { usePermissionStore } from "@/store/modules/permission";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { storeToRefs } from "pinia";
|
||||
import variables from "@/styles/variables.module.scss";
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { sidebarLogo } = storeToRefs(settingsStore);
|
||||
const route = useRoute();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ 'has-logo': sidebarLogo }">
|
||||
<logo v-if="sidebarLogo" :collapse="!appStore.sidebar.opened" />
|
||||
<div class="has-logo">
|
||||
<logo :collapse="!appStore.sidebar.opened" />
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:default-active="route.path"
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore, TagView } from "@/store/modules/tagsView";
|
||||
|
||||
const tagAndTagSpacing = ref(4);
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
const emits = defineEmits(["scroll"]);
|
||||
const emitScroll = () => {
|
||||
emits("scroll");
|
||||
};
|
||||
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const scrollWrapper = computed(
|
||||
() => proxy?.$refs.scrollContainer.$refs.wrapRef
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
scrollWrapper.value.addEventListener("scroll", emitScroll, true);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
scrollWrapper.value.removeEventListener("scroll", emitScroll);
|
||||
});
|
||||
|
||||
function handleScroll(e: WheelEvent) {
|
||||
const eventDelta = (e as any).wheelDelta || -e.deltaY * 40;
|
||||
scrollWrapper.value.scrollLeft =
|
||||
scrollWrapper.value.scrollLeft + eventDelta / 4;
|
||||
}
|
||||
|
||||
function moveToTarget(currentTag: TagView) {
|
||||
const $container = proxy.$refs.scrollContainer.$el;
|
||||
const $containerWidth = $container.offsetWidth;
|
||||
const $scrollWrapper = scrollWrapper.value;
|
||||
|
||||
let firstTag = null;
|
||||
let lastTag = null;
|
||||
|
||||
// find first tag and last tag
|
||||
if (tagsViewStore.visitedViews.length > 0) {
|
||||
firstTag = tagsViewStore.visitedViews[0];
|
||||
lastTag = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1];
|
||||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = 0;
|
||||
} else if (lastTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
|
||||
} else {
|
||||
const tagListDom = document.getElementsByClassName("tags-item");
|
||||
const currentIndex = tagsViewStore.visitedViews.findIndex(
|
||||
(item) => item === currentTag
|
||||
);
|
||||
let prevTag = null;
|
||||
let nextTag = null;
|
||||
for (const k in tagListDom) {
|
||||
if (k !== "length" && Object.hasOwnProperty.call(tagListDom, k)) {
|
||||
if (
|
||||
(tagListDom[k] as any).dataset.path ===
|
||||
tagsViewStore.visitedViews[currentIndex - 1].path
|
||||
) {
|
||||
prevTag = tagListDom[k];
|
||||
}
|
||||
if (
|
||||
(tagListDom[k] as any).dataset.path ===
|
||||
tagsViewStore.visitedViews[currentIndex + 1].path
|
||||
) {
|
||||
nextTag = tagListDom[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft =
|
||||
(nextTag as any).offsetLeft +
|
||||
(nextTag as any).offsetWidth +
|
||||
tagAndTagSpacing.value;
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft =
|
||||
(prevTag as any).offsetLeft - tagAndTagSpacing.value;
|
||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
|
||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
moveToTarget,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-scrollbar
|
||||
ref="scrollContainer"
|
||||
class="scroll-container"
|
||||
:vertical="false"
|
||||
@wheel.prevent="handleScroll"
|
||||
>
|
||||
<slot />
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.scroll-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.el-scrollbar__bar {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.el-scrollbar__wrap {
|
||||
height: 49px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,375 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
ref,
|
||||
watch,
|
||||
onMounted,
|
||||
ComponentInternalInstance,
|
||||
} from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
import path from "path-browserify";
|
||||
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
import { translateRouteTitleI18n } from "@/utils/i18n";
|
||||
|
||||
import { usePermissionStore } from "@/store/modules/permission";
|
||||
import { useTagsViewStore, TagView } from "@/store/modules/tagsView";
|
||||
import ScrollPane from "./ScrollPane.vue";
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const permissionStore = usePermissionStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const { visitedViews } = storeToRefs(tagsViewStore);
|
||||
|
||||
const selectedTag = ref({});
|
||||
const scrollPaneRef = ref();
|
||||
const left = ref(0);
|
||||
const top = ref(0);
|
||||
const affixTags = ref<TagView[]>([]);
|
||||
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
addTags();
|
||||
moveToCurrentTag();
|
||||
},
|
||||
{
|
||||
// Выполнить сразу при инициализации
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const tagMenuVisible = ref(false); // Состояние отображения меню действий с вкладкой
|
||||
watch(tagMenuVisible, (value) => {
|
||||
if (value) {
|
||||
document.body.addEventListener("click", closeTagMenu);
|
||||
} else {
|
||||
document.body.removeEventListener("click", closeTagMenu);
|
||||
}
|
||||
});
|
||||
|
||||
function filterAffixTags(routes: any[], basePath = "/") {
|
||||
let tags: TagView[] = [];
|
||||
|
||||
routes.forEach((route) => {
|
||||
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 childTags = filterAffixTags(route.children, route.path);
|
||||
if (childTags.length >= 1) {
|
||||
tags = tags.concat(childTags);
|
||||
}
|
||||
}
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
function initTags() {
|
||||
const tags: TagView[] = filterAffixTags(permissionStore.routes);
|
||||
affixTags.value = tags;
|
||||
for (const tag of tags) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
tagsViewStore.addVisitedView(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addTags() {
|
||||
if (route.name) {
|
||||
tagsViewStore.addView(route);
|
||||
}
|
||||
}
|
||||
|
||||
function moveToCurrentTag() {
|
||||
nextTick(() => {
|
||||
for (const r of tagsViewStore.visitedViews) {
|
||||
if (r.path === route.path) {
|
||||
scrollPaneRef.value.moveToTarget(r);
|
||||
// when query is different then update
|
||||
if (r.fullPath !== route.fullPath) {
|
||||
tagsViewStore.updateVisitedView(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isActive(tag: TagView) {
|
||||
return tag.path === route.path;
|
||||
}
|
||||
|
||||
function isAffix(tag: TagView) {
|
||||
return tag.meta && tag.meta.affix;
|
||||
}
|
||||
|
||||
function isFirstView() {
|
||||
try {
|
||||
return (
|
||||
(selectedTag.value as TagView).fullPath ===
|
||||
tagsViewStore.visitedViews[1].fullPath ||
|
||||
(selectedTag.value as TagView).fullPath === "/index"
|
||||
);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isLastView() {
|
||||
try {
|
||||
return (
|
||||
(selectedTag.value as TagView).fullPath ===
|
||||
tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].fullPath
|
||||
);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshSelectedTag(view: TagView) {
|
||||
tagsViewStore.delCachedView(view);
|
||||
const { fullPath } = view;
|
||||
nextTick(() => {
|
||||
router.replace({ path: "/redirect" + fullPath }).catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toLastView(visitedViews: TagView[], view?: any) {
|
||||
const latestView = visitedViews.slice(-1)[0];
|
||||
if (latestView && latestView.fullPath) {
|
||||
router.push(latestView.fullPath);
|
||||
} else {
|
||||
// now the default is to redirect to the home page if there is no tags-view,
|
||||
// you can adjust it according to your needs.
|
||||
if (view.name === "Dashboard") {
|
||||
// to reload home page
|
||||
router.replace({ path: "/redirect" + view.fullPath });
|
||||
} else {
|
||||
router.push("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeSelectedTag(view: TagView) {
|
||||
tagsViewStore.delView(view).then((res: any) => {
|
||||
if (isActive(view)) {
|
||||
toLastView(res.visitedViews, view);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeLeftTags() {
|
||||
tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => {
|
||||
if (
|
||||
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
|
||||
) {
|
||||
toLastView(res.visitedViews);
|
||||
}
|
||||
});
|
||||
}
|
||||
function closeRightTags() {
|
||||
tagsViewStore.delRightViews(selectedTag.value).then((res: any) => {
|
||||
if (
|
||||
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
|
||||
) {
|
||||
toLastView(res.visitedViews);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeOtherTags() {
|
||||
router.push(selectedTag.value);
|
||||
tagsViewStore.delOtherViews(selectedTag.value).then(() => {
|
||||
moveToCurrentTag();
|
||||
});
|
||||
}
|
||||
|
||||
function closeAllTags(view: TagView) {
|
||||
tagsViewStore.delAllViews().then((res: any) => {
|
||||
toLastView(res.visitedViews, view);
|
||||
});
|
||||
}
|
||||
|
||||
function openTagMenu(tag: TagView, e: MouseEvent) {
|
||||
const menuMinWidth = 105;
|
||||
|
||||
// console.log("test", proxy?.$el);
|
||||
|
||||
const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left
|
||||
const offsetWidth = proxy?.$el.offsetWidth; // container width
|
||||
const maxLeft = offsetWidth - menuMinWidth; // left boundary
|
||||
const l = e.clientX - offsetLeft + 15; // 15: margin right
|
||||
|
||||
if (l > maxLeft) {
|
||||
left.value = maxLeft;
|
||||
} else {
|
||||
left.value = l;
|
||||
}
|
||||
|
||||
top.value = e.clientY;
|
||||
tagMenuVisible.value = true;
|
||||
selectedTag.value = tag;
|
||||
}
|
||||
|
||||
function closeTagMenu() {
|
||||
tagMenuVisible.value = false;
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
closeTagMenu();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTags();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tags-container">
|
||||
<scroll-pane ref="scrollPaneRef" @scroll="handleScroll">
|
||||
<router-link
|
||||
:class="'tags-item ' + (isActive(tag) ? 'active' : '')"
|
||||
v-for="tag in visitedViews"
|
||||
:key="tag.path"
|
||||
:data-path="tag.path"
|
||||
:to="{ path: tag.path, query: tag.query }"
|
||||
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
||||
@contextmenu.prevent="openTagMenu(tag, $event)"
|
||||
>
|
||||
{{ translateRouteTitleI18n(tag.meta?.title) }}
|
||||
<span
|
||||
v-if="!isAffix(tag)"
|
||||
class="tags-item-close"
|
||||
@click.prevent.stop="closeSelectedTag(tag)"
|
||||
>
|
||||
<i-ep-close class="text-[10px]" />
|
||||
</span>
|
||||
</router-link>
|
||||
</scroll-pane>
|
||||
|
||||
<!-- Меню действий с вкладкой -->
|
||||
<ul
|
||||
v-show="tagMenuVisible"
|
||||
class="tag-menu"
|
||||
:style="{ left: left + 'px', top: top + 'px' }"
|
||||
>
|
||||
<li @click="refreshSelectedTag(selectedTag)">
|
||||
<svg-icon icon-class="refresh" />
|
||||
Обновить
|
||||
</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||
<svg-icon icon-class="close" />
|
||||
Закрыть
|
||||
</li>
|
||||
<li @click="closeOtherTags">
|
||||
<svg-icon icon-class="close_other" />
|
||||
Закрыть остальные
|
||||
</li>
|
||||
<li v-if="!isFirstView()" @click="closeLeftTags">
|
||||
<svg-icon icon-class="close_left" />
|
||||
Закрыть слева
|
||||
</li>
|
||||
<li v-if="!isLastView()" @click="closeRightTags">
|
||||
<svg-icon icon-class="close_right" />
|
||||
Закрыть справа
|
||||
</li>
|
||||
<li @click="closeAllTags(selectedTag)">
|
||||
<svg-icon icon-class="close_all" />
|
||||
Закрыть все
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-container {
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
background-color: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
box-shadow: 0 1px 1px var(--el-box-shadow-light);
|
||||
|
||||
.tags-item {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
margin: 4px 0 0 5px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
|
||||
&::before {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 5px;
|
||||
content: "";
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&-close {
|
||||
border-radius: 100%;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: rgb(0 0 0 / 16%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-menu {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
font-size: 12px;
|
||||
background: var(--el-bg-color-overlay);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
|
||||
li {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export { default as Navbar } from "./Navbar.vue";
|
||||
export { default as AppMain } from "./AppMain.vue";
|
||||
export { default as Settings } from "./Settings/index.vue";
|
||||
export { default as TagsView } from "./TagsView/index.vue";
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watchEffect } from "vue";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
import { AppMain, Navbar, Settings, TagsView } from "./components/index";
|
||||
import { AppMain, Navbar } from "./components/index";
|
||||
import Sidebar from "./components/Sidebar/index.vue";
|
||||
import RightPanel from "@/components/RightPanel/index.vue";
|
||||
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
|
||||
const { width } = useWindowSize();
|
||||
|
||||
@@ -20,11 +18,6 @@ const { width } = useWindowSize();
|
||||
const WIDTH = 992;
|
||||
|
||||
const appStore = useAppStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const fixedHeader = computed(() => settingsStore.fixedHeader);
|
||||
const showTagsView = computed(() => settingsStore.tagsView);
|
||||
const showSettings = computed(() => settingsStore.showSettings);
|
||||
|
||||
const classObj = computed(() => ({
|
||||
hideSidebar: !appStore.sidebar.opened,
|
||||
@@ -65,19 +58,13 @@ function handleOutsideClick() {
|
||||
|
||||
<Sidebar class="sidebar-container" />
|
||||
|
||||
<div :class="{ hasTagsView: showTagsView }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }">
|
||||
<div class="main-container">
|
||||
<div class="layout-header">
|
||||
<navbar />
|
||||
<tags-view v-if="showTagsView" />
|
||||
</div>
|
||||
|
||||
<!--Основная страница-->
|
||||
<app-main />
|
||||
|
||||
<!-- Панель настроек -->
|
||||
<RightPanel v-if="showSettings">
|
||||
<settings />
|
||||
</RightPanel>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -100,23 +87,6 @@ function handleOutsideClick() {
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: calc(100% - #{$sideBarWidth});
|
||||
transition: width 0.28s;
|
||||
}
|
||||
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - #{$sideBarCollapsedWidth});
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawer-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -128,4 +98,3 @@ function handleOutsideClick() {
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user