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,138 +0,0 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { addClass, removeClass } from "@/utils/index";
const show = ref(false);
defineProps({
buttonTop: {
default: 250,
type: Number,
},
});
watch(show, (value) => {
if (value) {
addEventClick();
}
if (value) {
addClass(document.body, "showRightPanel");
} else {
removeClass(document.body, "showRightPanel");
}
});
function addEventClick() {
window.addEventListener("click", closeSidebar, { passive: true });
}
function closeSidebar(evt: any) {
// Клик по выбору темы не закрывает панель
let parent = evt.target.closest(".right-panel-container");
if (!parent) {
show.value = false;
window.removeEventListener("click", closeSidebar);
}
}
const rightPanel = ref();
function insertToBody() {
const body = document.querySelector("body") as any;
body.insertBefore(rightPanel.value, body.firstChild);
}
onMounted(() => {
insertToBody();
});
onBeforeUnmount(() => {
rightPanel.value.remove();
});
</script>
<template>
<div :class="{ show: show }" ref="rightPanel">
<div class="right-panel-overlay" />
<div class="right-panel-container">
<div
class="right-panel-btn"
:style="{
top: buttonTop + 'px',
}"
@click="show = !show"
>
<i-ep-close v-show="show" />
<i-ep-setting v-show="!show" />
</div>
<div>
<slot />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.showRightPanel {
position: relative;
width: calc(100% - 15px);
overflow: hidden;
}
.right-panel-overlay {
position: fixed;
top: 0;
left: 0;
background: rgb(0 0 0 / 20%);
}
.right-panel-container {
position: fixed;
top: 0;
right: 0;
z-index: 999;
width: 100%;
max-width: 300px;
height: 100vh;
background-color: var(--el-bg-color-overlay);
box-shadow: 0 0 15px 0 rgb(0 0 0 / 5%);
transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
transform: translate(100%);
}
.show {
transition: all 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
.right-panel-overlay {
z-index: 99;
width: 100%;
height: 100%;
opacity: 1;
}
.right-panel-container {
transform: translate(0);
}
}
.right-panel-btn {
position: absolute;
left: -36px;
width: 36px;
height: 36px;
color: var(--el-color-white);
text-align: center;
cursor: pointer;
background-color: var(--el-color-primary);
border-radius: 6px 0 0 6px;
svg {
width: 20px;
height: 20px;
vertical-align: -10px;
}
}
</style>
@@ -1,16 +1,8 @@
<script setup lang="ts">
import { useTagsViewStore } from "@/store/modules/tagsView";
const tagsViewStore = useTagsViewStore();
</script>
<template> <template>
<section class="app-main"> <section class="app-main">
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<transition name="router-fade" mode="out-in"> <transition name="router-fade" mode="out-in">
<keep-alive :include="tagsViewStore.cachedViews">
<component :is="Component" :key="route.fullPath" /> <component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition> </transition>
</router-view> </router-view>
</section> </section>
@@ -21,27 +13,10 @@ const tagsViewStore = useTagsViewStore();
position: relative; position: relative;
width: 100%; width: 100%;
/* 50= navbar 50 */
min-height: calc(100vh - 50px); min-height: calc(100vh - 50px);
overflow: hidden; overflow: hidden;
background-color: var(--el-bg-color-page); 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> </style>
@@ -3,11 +3,9 @@ import { storeToRefs } from "pinia";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useAppStore } from "@/store/modules/app"; import { useAppStore } from "@/store/modules/app";
import { useTagsViewStore } from "@/store/modules/tagsView";
import { useAdminStore } from "@/store/modules/admin"; import { useAdminStore } from "@/store/modules/admin";
const appStore = useAppStore(); const appStore = useAppStore();
const tagsViewStore = useTagsViewStore();
const adminStore = useAdminStore(); const adminStore = useAdminStore();
const { t } = useI18n(); const { t } = useI18n();
@@ -39,9 +37,6 @@ function logout() {
}).then(() => { }).then(() => {
adminStore adminStore
.logout() .logout()
.then(() => {
tagsViewStore.delAllViews();
})
.then(() => { .then(() => {
router.push(`/login?redirect=${route.fullPath}`); 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> <script lang="ts" setup>
import { useSettingsStore } from "@/store/modules/settings";
const settingsStore = useSettingsStore();
defineProps({ defineProps({
collapse: { collapse: {
@@ -14,33 +11,58 @@ const logo = ref(new URL(`../../../assets/logo.png`, import.meta.url).href);
</script> </script>
<template> <template>
<div class="w-full h-[50px] bg-gray-800 dark:bg-[var(--el-bg-color-overlay)]"> <div class="sidebar-logo">
<transition name="sidebarLogoFade"> <transition name="sidebarLogoFade">
<router-link <router-link
v-if="collapse" v-if="collapse"
key="collapse" key="collapse"
class="h-full w-full flex items-center justify-center" class="sidebar-logo-link"
to="/" to="/"
> >
<img v-if="settingsStore.sidebarLogo" :src="logo" class="w-5 h-5" /> <img :src="logo" class="sidebar-logo-image" />
<span v-else class="ml-3 text-white text-sm font-bold">HY2XS</span>
</router-link> </router-link>
<router-link <router-link
v-else v-else
key="expand" key="expand"
class="h-full w-full flex items-center justify-center" class="sidebar-logo-link"
to="/" to="/"
> >
<img v-if="settingsStore.sidebarLogo" :src="logo" class="w-5 h-5" /> <img :src="logo" class="sidebar-logo-image" />
<span class="ml-3 text-white text-sm font-bold">HY2XS</span> <span class="sidebar-logo-title">HY2XS</span>
</router-link> </router-link>
</transition> </transition>
</div> </div>
</template> </template>
<style lang="scss" scoped> <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 { .sidebarLogoFade-enter-active {
transition: opacity 2s; transition: opacity 2s;
} }
@@ -4,23 +4,19 @@ import { useRoute } from "vue-router";
import SidebarItem from "./SidebarItem.vue"; import SidebarItem from "./SidebarItem.vue";
import Logo from "./Logo.vue"; import Logo from "./Logo.vue";
import { useSettingsStore } from "@/store/modules/settings";
import { usePermissionStore } from "@/store/modules/permission"; import { usePermissionStore } from "@/store/modules/permission";
import { useAppStore } from "@/store/modules/app"; import { useAppStore } from "@/store/modules/app";
import { storeToRefs } from "pinia";
import variables from "@/styles/variables.module.scss"; import variables from "@/styles/variables.module.scss";
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore();
const appStore = useAppStore(); const appStore = useAppStore();
const { sidebarLogo } = storeToRefs(settingsStore);
const route = useRoute(); const route = useRoute();
</script> </script>
<template> <template>
<div :class="{ 'has-logo': sidebarLogo }"> <div class="has-logo">
<logo v-if="sidebarLogo" :collapse="!appStore.sidebar.opened" /> <logo :collapse="!appStore.sidebar.opened" />
<el-scrollbar> <el-scrollbar>
<el-menu <el-menu
:default-active="route.path" :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 Navbar } from "./Navbar.vue";
export { default as AppMain } from "./AppMain.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"> <script setup lang="ts">
import { computed, watchEffect } from "vue"; import { computed, watchEffect } from "vue";
import { useWindowSize } from "@vueuse/core"; 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 Sidebar from "./components/Sidebar/index.vue";
import RightPanel from "@/components/RightPanel/index.vue";
import { useAppStore } from "@/store/modules/app"; import { useAppStore } from "@/store/modules/app";
import { useSettingsStore } from "@/store/modules/settings";
const { width } = useWindowSize(); const { width } = useWindowSize();
@@ -20,11 +18,6 @@ const { width } = useWindowSize();
const WIDTH = 992; const WIDTH = 992;
const appStore = useAppStore(); const appStore = useAppStore();
const settingsStore = useSettingsStore();
const fixedHeader = computed(() => settingsStore.fixedHeader);
const showTagsView = computed(() => settingsStore.tagsView);
const showSettings = computed(() => settingsStore.showSettings);
const classObj = computed(() => ({ const classObj = computed(() => ({
hideSidebar: !appStore.sidebar.opened, hideSidebar: !appStore.sidebar.opened,
@@ -65,19 +58,13 @@ function handleOutsideClick() {
<Sidebar class="sidebar-container" /> <Sidebar class="sidebar-container" />
<div :class="{ hasTagsView: showTagsView }" class="main-container"> <div class="main-container">
<div :class="{ 'fixed-header': fixedHeader }"> <div class="layout-header">
<navbar /> <navbar />
<tags-view v-if="showTagsView" />
</div> </div>
<!--Основная страница--> <!--Основная страница-->
<app-main /> <app-main />
<!-- Панель настроек -->
<RightPanel v-if="showSettings">
<settings />
</RightPanel>
</div> </div>
</div> </div>
</template> </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 { .drawer-bg {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -128,4 +98,3 @@ function handleOutsideClick() {
} }
</style> </style>
-1
View File
@@ -13,7 +13,6 @@ import "virtual:svg-icons-register";
import i18n from "@/lang/index"; import i18n from "@/lang/index";
// Стили // Стили
import "element-plus/theme-chalk/dark/css-vars.css";
import "@/styles/index.scss"; import "@/styles/index.scss";
import "uno.css"; import "uno.css";
-53
View File
@@ -1,63 +1,10 @@
// Системные настройки
interface DefaultSettings { interface DefaultSettings {
/**
* Заголовок системы
*/
title: string;
/**
* Показывать настройки
*/
showSettings: boolean;
/**
* Показывать навигацию по вкладкам
*/
tagsView: boolean;
/**
*Фиксировать header
*/
fixedHeader: boolean;
/**
* Показывать логотип в боковом меню
*/
sidebarLogo: boolean;
/**
* Layout навигации
*/
layout: string;
/**
* Цвет темы
*/
themeColor: string;
/**
* Тема интерфейса
*/
theme: string;
/**
* Язык
*/
language: string; language: string;
} }
const defaultSettings: DefaultSettings = { const defaultSettings: DefaultSettings = {
title: "HY2XS admin",
showSettings: true,
tagsView: true,
fixedHeader: false,
sidebarLogo: true,
layout: "left",
themeColor: "#409EFF",
/**
* Тема интерфейса
*
* dark:Тёмная тема
* light: Светлая тема
*/
theme: "dark",
language: "ru", // ru | en language: "ru", // ru | en
}; };
export default defaultSettings; export default defaultSettings;
@@ -1,58 +0,0 @@
import { defineStore } from "pinia";
import defaultSettings from "@/settings";
import { useStorage } from "@vueuse/core";
export const useSettingsStore = defineStore("setting", () => {
// state
const tagsView = useStorage<boolean>("tagsView", defaultSettings.tagsView);
const showSettings = ref<boolean>(defaultSettings.showSettings);
const fixedHeader = ref<boolean>(defaultSettings.fixedHeader);
const sidebarLogo = ref<boolean>(defaultSettings.sidebarLogo);
const layout = useStorage<string>("layout", defaultSettings.layout);
const themeColor = useStorage<string>(
"themeColor",
defaultSettings.themeColor
);
// actions
function changeSetting(param: { key: string; value: any }) {
const { key, value } = param;
switch (key) {
case "showSettings":
showSettings.value = value;
break;
case "fixedHeader":
fixedHeader.value = value;
break;
case "tagsView":
tagsView.value = value;
break;
case "sidevarLogo":
sidebarLogo.value = value;
break;
case "layout":
layout.value = value;
break;
case "themeColor":
themeColor.value = value;
break;
default:
break;
}
}
return {
showSettings,
tagsView,
fixedHeader,
sidebarLogo,
layout,
themeColor,
changeSetting,
};
});
-220
View File
@@ -1,220 +0,0 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { RouteLocationNormalized } from "vue-router";
export interface TagView extends Partial<RouteLocationNormalized> {
title?: string;
}
// setup
export const useTagsViewStore = defineStore("tagsView", () => {
// state
const visitedViews = ref<TagView[]>([]);
const cachedViews = ref<string[]>([]);
// actions
function addVisitedView(view: TagView) {
if (visitedViews.value.some((v) => v.path === view.path)) return;
if (view.meta && view.meta.affix) {
visitedViews.value.unshift(
Object.assign({}, view, {
title: view.meta?.title || "no-name",
})
);
} else {
visitedViews.value.push(
Object.assign({}, view, {
title: view.meta?.title || "no-name",
})
);
}
}
function addCachedView(view: TagView) {
const viewName = view.name as string;
if (cachedViews.value.includes(viewName)) return;
if (view.meta?.keepAlive) {
cachedViews.value.push(viewName);
}
}
function delVisitedView(view: TagView) {
return new Promise((resolve) => {
for (const [i, v] of visitedViews.value.entries()) {
if (v.path === view.path) {
visitedViews.value.splice(i, 1);
break;
}
}
resolve([...visitedViews.value]);
});
}
function delCachedView(view: TagView) {
const viewName = view.name as string;
return new Promise((resolve) => {
const index = cachedViews.value.indexOf(viewName);
index > -1 && cachedViews.value.splice(index, 1);
resolve([...cachedViews.value]);
});
}
function delOtherVisitedViews(view: TagView) {
return new Promise((resolve) => {
visitedViews.value = visitedViews.value.filter((v) => {
return v.meta?.affix || v.path === view.path;
});
resolve([...visitedViews.value]);
});
}
function delOtherCachedViews(view: TagView) {
const viewName = view.name as string;
return new Promise((resolve) => {
const index = cachedViews.value.indexOf(viewName);
if (index > -1) {
cachedViews.value = cachedViews.value.slice(index, index + 1);
} else {
// if index = -1, there is no cached tags
cachedViews.value = [];
}
resolve([...cachedViews.value]);
});
}
function updateVisitedView(view: TagView) {
for (let v of visitedViews.value) {
if (v.path === view.path) {
v = Object.assign(v, view);
break;
}
}
}
function addView(view: TagView) {
addVisitedView(view);
addCachedView(view);
}
function delView(view: TagView) {
return new Promise((resolve) => {
delVisitedView(view);
delCachedView(view);
resolve({
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value],
});
});
}
function delOtherViews(view: TagView) {
return new Promise((resolve) => {
delOtherVisitedViews(view);
delOtherCachedViews(view);
resolve({
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value],
});
});
}
function delLeftViews(view: TagView) {
return new Promise((resolve) => {
const currIndex = visitedViews.value.findIndex(
(v) => v.path === view.path
);
if (currIndex === -1) {
return;
}
visitedViews.value = visitedViews.value.filter((item, index) => {
// affix:true Фиксированная вкладка, например главная
if (index >= currIndex || (item.meta && item.meta.affix)) {
return true;
}
const cacheIndex = cachedViews.value.indexOf(item.name as string);
if (cacheIndex > -1) {
cachedViews.value.splice(cacheIndex, 1);
}
return false;
});
resolve({
visitedViews: [...visitedViews.value],
});
});
}
function delRightViews(view: TagView) {
return new Promise((resolve) => {
const currIndex = visitedViews.value.findIndex(
(v) => v.path === view.path
);
if (currIndex === -1) {
return;
}
visitedViews.value = visitedViews.value.filter((item, index) => {
// affix:true Фиксированная вкладка, например главная
if (index <= currIndex || (item.meta && item.meta.affix)) {
return true;
}
const cacheIndex = cachedViews.value.indexOf(item.name as string);
if (cacheIndex > -1) {
cachedViews.value.splice(cacheIndex, 1);
}
return false;
});
resolve({
visitedViews: [...visitedViews.value],
});
});
}
function delAllViews() {
return new Promise((resolve) => {
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix);
visitedViews.value = affixTags;
cachedViews.value = [];
resolve({
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value],
});
});
}
function delAllVisitedViews() {
return new Promise((resolve) => {
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix);
visitedViews.value = affixTags;
resolve([...visitedViews.value]);
});
}
function delAllCachedViews() {
return new Promise((resolve) => {
cachedViews.value = [];
resolve([...cachedViews.value]);
});
}
return {
visitedViews,
cachedViews,
addVisitedView,
addCachedView,
delVisitedView,
delCachedView,
delOtherVisitedViews,
delOtherCachedViews,
updateVisitedView,
addView,
delView,
delOtherViews,
delLeftViews,
delRightViews,
delAllViews,
delAllVisitedViews,
delAllCachedViews,
};
});
-35
View File
@@ -1,35 +0,0 @@
html.dark {
--menuBg: var(--el-bg-color-overlay);
--menuText: #fff;
--menuActiveText: var(--el-menu-active-color);
--menuHover: rgb(0 0 0 / 20%);
--subMenuBg: var(--el-menu-bg-color);
--subMenuActiveText: var(--el-menu-active-color);
--subMenuHover: rgb(0 0 0 / 20%);
.navbar {
color: var(--el-text-color-regular);
background-color: var(--el-bg-color);
.setting-container .setting-item:hover {
background: var(--el-fill-color-light);
}
}
.right-panel-btn {
background-color: var(--el-color-primary-dark);
}
.svg-icon,
svg {
fill: var(--el-text-color-regular);
}
.sidebar-container {
.el-menu-item.is-active .svg-icon {
fill: var(--el-color-primary);
}
}
}
@@ -1,9 +1,3 @@
:root {
// Здесь можно задать пользовательские переменные цвета
// Цвет active-состояния основной кнопки Element Plus, меняется вместе с темой
--el-color-primary-dark: #0d84ff;
}
// Переопределение стилей Element Plus // Переопределение стилей Element Plus
.el-breadcrumb__inner, .el-breadcrumb__inner,
.el-breadcrumb__inner a { .el-breadcrumb__inner a {
+23 -4
View File
@@ -1,19 +1,38 @@
@import "./sidebar"; @import "./sidebar";
@import "./reset"; @import "./reset";
@import "./dark";
@import "./element-plus"; @import "./element-plus";
.app-container { .app-container {
margin: 20px; margin: 20px;
.search { .search,
padding: 18px 0 0 10px; .page-toolbar {
margin-bottom: 10px; padding: 16px 20px;
margin-bottom: 12px;
background-color: var(--el-bg-color-overlay); background-color: var(--el-bg-color-overlay);
border: 1px solid var(--el-border-color-light); border: 1px solid var(--el-border-color-light);
border-radius: 4px; border-radius: 4px;
box-shadow: var(--el-box-shadow-light); box-shadow: var(--el-box-shadow-light);
} }
.page-toolbar {
.el-form {
display: flex;
flex-wrap: wrap;
gap: 12px 16px;
align-items: center;
}
.el-form-item {
margin-right: 0;
margin-bottom: 0;
}
}
.search {
padding: 18px 0 0 10px;
margin-bottom: 10px;
}
} }
+24 -1
View File
@@ -54,6 +54,28 @@
height: 48px; height: 48px;
line-height: normal; line-height: normal;
padding: 0 16px !important; padding: 0 16px !important;
color: $menuText !important;
}
.el-menu-item:hover,
.el-sub-menu__title:hover {
background-color: $menuHover !important;
}
.el-menu-item.is-active {
position: relative;
color: $menuActiveText !important;
background-color: $menuActiveBg !important;
}
.el-menu-item.is-active::before {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 2px;
content: "";
background-color: $menuActiveBorder;
} }
.svg-icon { .svg-icon {
@@ -61,6 +83,8 @@
width: 18px; width: 18px;
height: 18px; height: 18px;
margin-right: 0; margin-right: 0;
color: currentColor;
fill: currentColor;
} }
.menu-title { .menu-title {
@@ -236,4 +260,3 @@
} }
} }
} }
+19 -8
View File
@@ -1,13 +1,23 @@
// Глобальные SCSS-переменные // Глобальные SCSS-переменные
:root { :root {
--menuBg: #304156; --el-color-primary: #ff4e2f;
--menuText: #bfcbd9; --el-color-primary-dark-2: #cc3e26;
--menuActiveText: #409eff; --el-color-primary-light-3: #ff826d;
--menuHover: #263445; --el-color-primary-light-5: #ffa797;
--subMenuBg: #1f2d3d; --el-color-primary-light-7: #ffc8bd;
--subMenuActiveText: #f4f4f5; --el-color-primary-light-8: #ffdcd6;
--subMenuHover: #001528; --el-color-primary-light-9: #ffedea;
--menuBg: #181818;
--menuText: #fff;
--menuActiveText: #fff;
--menuHover: #2a2d2e;
--subMenuBg: #1f1f1f;
--subMenuActiveText: #fff;
--subMenuHover: #2a2d2e;
--menuActiveBg: #37373d;
--menuActiveBorder: #ff4e2f;
} }
$menuBg: var(--menuBg); $menuBg: var(--menuBg);
@@ -18,8 +28,9 @@ $menuHover: var(--menuHover);
$subMenuBg: var(--subMenuBg); $subMenuBg: var(--subMenuBg);
$subMenuActiveText: var(--subMenuActiveText); $subMenuActiveText: var(--subMenuActiveText);
$subMenuHover: var(--subMenuHover); $subMenuHover: var(--subMenuHover);
$menuActiveBg: var(--menuActiveBg);
$menuActiveBorder: var(--menuActiveBorder);
$sideBarWidth: 210px; $sideBarWidth: 210px;
$sideBarCollapsedWidth: 54px; $sideBarCollapsedWidth: 54px;
-4
View File
@@ -14,7 +14,6 @@ declare module '@vue/runtime-core' {
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
@@ -45,19 +44,16 @@ declare module '@vue/runtime-core' {
ElUpload: typeof import('element-plus/es')['ElUpload'] ElUpload: typeof import('element-plus/es')['ElUpload']
Hamburger: typeof import('./../components/Hamburger/index.vue')['default'] Hamburger: typeof import('./../components/Hamburger/index.vue')['default']
IEpCaretBottom: typeof import('~icons/ep/caret-bottom')['default'] IEpCaretBottom: typeof import('~icons/ep/caret-bottom')['default']
IEpClose: typeof import('~icons/ep/close')['default']
IEpDownload: typeof import('~icons/ep/download')['default'] IEpDownload: typeof import('~icons/ep/download')['default']
IEpMoreFilled: typeof import('~icons/ep/more-filled')['default'] IEpMoreFilled: typeof import('~icons/ep/more-filled')['default']
IEpRefresh: typeof import('~icons/ep/refresh')['default'] IEpRefresh: typeof import('~icons/ep/refresh')['default']
IEpRefreshRight: typeof import('~icons/ep/refresh-right')['default'] IEpRefreshRight: typeof import('~icons/ep/refresh-right')['default']
IEpSetting: typeof import('~icons/ep/setting')['default']
IEpUpload: typeof import('~icons/ep/upload')['default'] IEpUpload: typeof import('~icons/ep/upload')['default']
IEpUserFilled: typeof import('~icons/ep/user-filled')['default'] IEpUserFilled: typeof import('~icons/ep/user-filled')['default']
ImputMultiple: typeof import('./../components/ImputMultiple/index.vue')['default'] ImputMultiple: typeof import('./../components/ImputMultiple/index.vue')['default']
LangSelect: typeof import('./../components/LangSelect/index.vue')['default'] LangSelect: typeof import('./../components/LangSelect/index.vue')['default']
MapAdd: typeof import('./../components/MapAdd/index.vue')['default'] MapAdd: typeof import('./../components/MapAdd/index.vue')['default']
Pagination: typeof import('./../components/Pagination/index.vue')['default'] Pagination: typeof import('./../components/Pagination/index.vue')['default']
RightPanel: typeof import('./../components/RightPanel/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default'] SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default']
@@ -1,6 +1,6 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<div class="search"> <div class="page-toolbar">
<el-form inline> <el-form inline>
<el-form-item> <el-form-item>
<el-button type="primary" @click="submitForm" :icon="Select"> <el-button type="primary" @click="submitForm" :icon="Select">
@@ -169,7 +169,7 @@ import {
UploadFile, UploadFile,
UploadRawFile, UploadRawFile,
UploadRequestOptions, UploadRequestOptions,
} from "element-plus/lib/components"; } from "element-plus";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
@@ -1,6 +1,6 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<div class="search"> <div class="page-toolbar">
<el-form :inline="true"> <el-form :inline="true">
<el-form-item> <el-form-item>
<el-button @click="handleExport"> <el-button @click="handleExport">
@@ -997,7 +997,7 @@ import { assignWith, deepCopy } from "@/utils/copy";
import { import {
UploadFile, UploadFile,
UploadRequestOptions, UploadRequestOptions,
} from "element-plus/lib/components"; } from "element-plus";
import { dashboardSummaryApi } from "@/api/dashboard"; import { dashboardSummaryApi } from "@/api/dashboard";
import { UploadUserFile } from "element-plus"; import { UploadUserFile } from "element-plus";
@@ -1,6 +1,6 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<div class="search"> <div class="page-toolbar">
<el-form ref="queryFormRef" :model="queryParams" :inline="true"> <el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item :label="$t('log.numLine')" prop="numLine"> <el-form-item :label="$t('log.numLine')" prop="numLine">
<el-select <el-select
+1 -1
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<div class="search"> <div class="page-toolbar">
<el-form ref="queryFormRef" :model="queryParams" :inline="true"> <el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item :label="$t('log.numLine')" prop="numLine"> <el-form-item :label="$t('log.numLine')" prop="numLine">
<el-select <el-select
+3 -3
View File
@@ -194,7 +194,7 @@ const handleLogin = () => {
width: 100%; width: 100%;
min-height: 100%; min-height: 100%;
overflow: hidden; overflow: hidden;
background-color: #2d3a4b; background-color: var(--menuBg);
.login-form { .login-form {
width: 520px; width: 520px;
@@ -206,8 +206,8 @@ const handleLogin = () => {
} }
.el-form-item { .el-form-item {
background: rgb(0 0 0 / 10%); background: var(--subMenuBg);
border: 1px solid rgb(255 255 255 / 10%); border: 1px solid rgb(255 255 255 / 12%);
border-radius: 5px; border-radius: 5px;
} }
+1 -1
View File
@@ -165,7 +165,7 @@ import {
exportPeerApi, exportPeerApi,
} from "@/api/peer"; } from "@/api/peer";
import { PeerPageDto, PeerSaveDto, PeerUpdateDto, PeerVo } from "@/api/peer/types"; import { PeerPageDto, PeerSaveDto, PeerUpdateDto, PeerVo } from "@/api/peer/types";
import { UploadFile, UploadRawFile, UploadRequestOptions } from "element-plus/lib/components"; import { UploadFile, UploadRawFile, UploadRequestOptions } from "element-plus";
const { t } = useI18n(); const { t } = useI18n();
const loading = ref(false); const loading = ref(false);
+37
View File
@@ -105,6 +105,43 @@ export default defineConfig((): UserConfig => {
build: { build: {
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks(id: string) {
if (!id.includes("node_modules")) {
return;
}
if (
id.includes("echarts") ||
id.includes("zrender") ||
id.includes("vue-echarts")
) {
return "charts";
}
if (
id.includes("element-plus") ||
id.includes("@element-plus") ||
id.includes("@popperjs")
) {
return "element-plus";
}
if (
id.includes("/vue/") ||
id.includes("vue-router") ||
id.includes("pinia") ||
id.includes("vue-i18n") ||
id.includes("@vueuse")
) {
return "vue-core";
}
if (id.includes("qrcode.vue") || id.includes("vuedraggable")) {
return "ui-extra";
}
return "vendor";
},
sanitizeFileName(name: string): string { sanitizeFileName(name: string): string {
// https://github.com/rollup/rollup/blob/master/src/utils/sanitizeFileName.ts // https://github.com/rollup/rollup/blob/master/src/utils/sanitizeFileName.ts
const match = DRIVE_LETTER_REGEX.exec(name); const match = DRIVE_LETTER_REGEX.exec(name);