Подготовить HY2XS к production-сборке

This commit is contained in:
2026-04-25 23:13:12 +05:00
commit 84a4e94567
277 changed files with 26513 additions and 0 deletions
@@ -0,0 +1,45 @@
<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>
</transition>
</router-view>
</section>
</template>
<style lang="scss" scoped>
.app-main {
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>
@@ -0,0 +1,140 @@
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useRoute, useRouter } from "vue-router";
import { useAppStore } from "@/store/modules/app";
import { useTagsViewStore } from "@/store/modules/tagsView";
import { useAccountStore } from "@/store/modules/account";
const appStore = useAppStore();
const tagsViewStore = useTagsViewStore();
const accountStore = useAccountStore();
const route = useRoute();
const router = useRouter();
const { device } = storeToRefs(appStore); // 设备类型:desktop-宽屏设备 || mobile-窄屏设备
/**
* 左侧菜单栏显示/隐藏
*/
function toggleSideBar() {
appStore.toggleSidebar(true);
}
/**
* vueUse 全屏
*/
const { isFullscreen, toggle } = useFullscreen();
/**
* 注销
*/
function logout() {
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
accountStore
.logout()
.then(() => {
tagsViewStore.delAllViews();
})
.then(() => {
router.push(`/login?redirect=${route.fullPath}`);
});
});
}
</script>
<template>
<!-- 顶部导航栏 -->
<div class="navbar">
<!-- 左侧面包屑 -->
<div class="flex">
<hamburger
:is-active="appStore.sidebar.opened"
@toggleClick="toggleSideBar"
/>
<breadcrumb />
</div>
<!-- 右侧导航设置 -->
<div class="flex">
<!-- 导航栏设置(窄屏隐藏)-->
<div class="setting-container" v-if="device !== 'mobile'">
<!--全屏 -->
<div class="setting-item" @click="toggle">
<svg-icon
:icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'"
/>
</div>
<!-- 布局大小 -->
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select class="setting-item" />
</el-tooltip>
<!--语言选择-->
<lang-select class="setting-item" />
</div>
<!-- 用户头像 -->
<el-dropdown trigger="click">
<div class="avatar-container">
<img src="/src/assets/logo.png" />
<i-ep-caret-bottom class="w-3 h-3" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="logout">
{{ $t("navbar.logout") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<style lang="scss" scoped>
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
height: 50px;
background-color: #fff;
box-shadow: 0 0 1px #0003;
.setting-container {
display: flex;
align-items: center;
.setting-item {
display: inline-block;
width: 30px;
height: 50px;
line-height: 50px;
color: #5a5e66;
text-align: center;
cursor: pointer;
&:hover {
background: rgb(249 250 251 / 100%);
}
}
}
.avatar-container {
display: flex;
align-items: center;
justify-items: center;
margin: 0 5px;
cursor: pointer;
img {
width: 40px;
height: 40px;
border-radius: 5px;
}
}
}
</style>
@@ -0,0 +1,165 @@
<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);
/**
* 切换布局
*/
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">开启 Tags-View</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">侧边栏 Logo</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>
@@ -0,0 +1,37 @@
<script lang="ts" setup>
import { computed } from "vue";
import { isExternal } from "@/utils/index";
import { useRouter } from "vue-router";
import { useAppStore } from "@/store/modules/app";
const appStore = useAppStore();
const sidebar = computed(() => appStore.sidebar);
const device = computed(() => appStore.device);
const props = defineProps({
to: {
type: String,
required: true,
},
});
const router = useRouter();
function push() {
if (device.value === "mobile" && sidebar.value.opened == true) {
appStore.closeSideBar(false);
}
router.push(props.to).catch((err) => {
console.error(err);
});
}
</script>
<template>
<a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener">
<slot />
</a>
<div v-else @click="push">
<slot />
</div>
</template>
@@ -0,0 +1,53 @@
<script lang="ts" setup>
import { useSettingsStore } from "@/store/modules/settings";
const settingsStore = useSettingsStore();
defineProps({
collapse: {
type: Boolean,
required: true,
},
});
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)]">
<transition name="sidebarLogoFade">
<router-link
v-if="collapse"
key="collapse"
class="h-full w-full flex items-center justify-center"
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>
</router-link>
<router-link
v-else
key="expand"
class="h-full w-full flex items-center justify-center"
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>
</router-link>
</transition>
</div>
</template>
<style lang="scss" scoped>
// https://cn.vuejs.org/guide/built-ins/transition.html#the-transition-component
.sidebarLogoFade-enter-active {
transition: opacity 2s;
}
.sidebarLogoFade-leave-active,
.sidebarLogoFade-enter-from,
.sidebarLogoFade-leave-to {
opacity: 0;
}
</style>
@@ -0,0 +1,121 @@
<script setup lang="ts">
import path from "path-browserify";
import { isExternal } from "@/utils/index";
import AppLink from "./Link.vue";
import { translateRouteTitleI18n } from "@/utils/i18n";
import SvgIcon from "@/components/SvgIcon/index.vue";
const props = defineProps({
/**
* 路由(eg:level_3_1)
*/
item: {
type: Object,
required: true,
},
/**
* 父层级完整路由路径(eg:/level/level_3/level_3_1)
*/
basePath: {
type: String,
required: true,
},
});
const onlyOneChild = ref(); // 临时变量,唯一子路由
/**
* 判断当前路由是否只有一个子路由
*
* 1:如果只有一个子路由: 返回 true
* 2:如果无子路由 :返回 true
*
* @param children 子路由数组
* @param parent 当前路由
*/
function hasOneShowingChild(children = [], parent: any) {
// 需要显示的子路由数组
const showingChildren = children.filter((item: any) => {
if (item.meta?.hidden) {
return false; // 过滤不显示的子路由
} else {
onlyOneChild.value = item; // 唯一子路由赋值(多个子路由情况 onlyOneChild 变量是用不上的)
return true;
}
});
// 1:如果只有一个子路由, 返回 true
if (showingChildren.length === 1) {
return true;
}
// 2:如果无子路由, 复制当前路由信息作为其子路由,满足只拥有一个子路由的条件,所以返回 true
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
return true;
}
return false;
}
/**
* 解析路径
*
* @param routePath 路由路径
*/
function resolvePath(routePath: string) {
if (isExternal(routePath)) {
return routePath;
}
if (isExternal(props.basePath)) {
return props.basePath;
}
// 完整路径 = 父级路径(/level/level_3) + 路由路径
const fullPath = path.resolve(props.basePath, routePath); // 相对路径 → 绝对路径
return fullPath;
}
</script>
<template>
<div v-if="!item.meta || !item.meta.hidden">
<!-- 只包含一个子路由节点的路由显示其唯一子路由 -->
<template
v-if="
hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren)
"
>
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)">
<svg-icon
v-if="onlyOneChild.meta && onlyOneChild.meta.icon"
:icon-class="onlyOneChild.meta.icon"
/>
<template #title>
{{ translateRouteTitleI18n(onlyOneChild.meta.title) }}
</template>
</el-menu-item>
</app-link>
</template>
<!-- 包含多个子路由 -->
<el-sub-menu v-else :index="resolvePath(item.path)" teleported>
<template #title>
<svg-icon
v-if="item.meta && item.meta.icon"
:icon-class="item.meta.icon"
/>
<span v-if="item.meta && item.meta.title">{{
translateRouteTitleI18n(item.meta.title)
}}</span>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:item="child"
:base-path="resolvePath(child.path)"
/>
</el-sub-menu>
</div>
</template>
@@ -0,0 +1,45 @@
<script setup lang="ts">
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" />
<el-scrollbar>
<el-menu
:default-active="route.path"
:collapse="!appStore.sidebar.opened"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:active-text-color="variables.menuActiveText"
:unique-opened="false"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item
v-for="route in permissionStore.routes"
:item="route"
:key="route.path"
:base-path="route.path"
:is-collapse="!appStore.sidebar.opened"
/>
</el-menu>
</el-scrollbar>
</div>
</template>
@@ -0,0 +1,121 @@
<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>
@@ -0,0 +1,373 @@
<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>
<!-- tag标签操作菜单 -->
<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>
@@ -0,0 +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";
+129
View File
@@ -0,0 +1,129 @@
<script setup lang="ts">
import { computed, watchEffect } from "vue";
import { useWindowSize } from "@vueuse/core";
import { AppMain, Navbar, Settings, TagsView } 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();
/**
* 响应式布局容器固定宽度
*
* 大屏(>=1200px
* 中屏(>=992px
* 小屏(>=768px
*/
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,
openSidebar: appStore.sidebar.opened,
withoutAnimation: appStore.sidebar.withoutAnimation,
mobile: appStore.device === "mobile",
}));
watchEffect(() => {
if (width.value < WIDTH) {
appStore.toggleDevice("mobile");
appStore.closeSideBar(true);
} else {
appStore.toggleDevice("desktop");
if (width.value >= 1200) {
//大屏
appStore.openSideBar(true);
} else {
appStore.closeSideBar(true);
}
}
});
function handleOutsideClick() {
appStore.closeSideBar(false);
}
</script>
<template>
<div :class="classObj" class="app-wrapper">
<!-- 手机设备侧边栏打开遮罩层 -->
<div
v-if="classObj.mobile && classObj.openSidebar"
class="drawer-bg"
@click="handleOutsideClick"
></div>
<Sidebar class="sidebar-container" />
<div :class="{ hasTagsView: showTagsView }" class="main-container">
<div :class="{ 'fixed-header': fixedHeader }">
<navbar />
<tags-view v-if="showTagsView" />
</div>
<!--主页面-->
<app-main />
<!-- 设置面板 -->
<RightPanel v-if="showSettings">
<settings />
</RightPanel>
</div>
</div>
</template>
<style lang="scss" scoped>
.app-wrapper {
&::after {
display: table;
clear: both;
content: "";
}
position: relative;
width: 100%;
height: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.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% - 54px);
}
.mobile .fixed-header {
width: 100%;
}
.drawer-bg {
position: absolute;
top: 0;
z-index: 999;
width: 100%;
height: 100%;
background: #000;
opacity: 0.3;
}
</style>