fix(frontend): полная зачистка UI legacy и унификация брендовой палитры (fix41)

This commit is contained in:
2026-05-21 15:14:56 +05:00
parent 85798bebb6
commit 782520848d
27 changed files with 152 additions and 1287 deletions
@@ -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";
+3 -34
View File
@@ -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>