first commit

This commit is contained in:
muzi
2025-06-16 14:42:26 +08:00
commit 6f06721506
149 changed files with 56883 additions and 0 deletions

View File

@ -0,0 +1,52 @@
<!--
* @Author: 田鑫
* @Date: 2023-03-05 18:14:16
* @LastEditors: 田鑫
* @LastEditTime: 2023-03-05 19:17:52
* @Description:
-->
<script lang="ts" setup>
import type { RouteLocationNormalized } from 'vue-router';
const router = useRouter();
const route = useRoute();
const matched = computed(() => {
if (route.matched.length === 1 && route.matched[0].path === '/') {
return [];
} else {
return route.matched.reduce((t: RouteLocationNormalized[], o) => {
const isExist = t.find((c) => c.name === o.name);
return isExist ? t : [...t, router.resolve(o)];
}, []);
}
});
</script>
<template>
<view></view>
<!-- <a-breadcrumb class="container-breadcrumb">
<a-breadcrumb-item v-for="{ meta, name } in matched" :key="name">
<router-link v-slot="{ href, navigate }" :to="{ name }" custom>
<a-link v-if="meta.needNavigate" :href="href" @click="navigate">{{
meta.locale ? meta.locale : '主页'
}}</a-link>
<a-link v-else disabled>{{ meta.locale ? meta.locale : '主页' }}</a-link>
</router-link>
</a-breadcrumb-item>
</a-breadcrumb> -->
</template>
<style scoped lang="less">
.container-breadcrumb {
margin: 16px 0;
:deep(.arco-breadcrumb-item) {
> a {
color: rgb(var(--gray-6));
}
&:last-child {
color: rgb(var(--gray-8));
}
}
}
</style>

View File

@ -0,0 +1,5 @@
export { default as Navbar } from './navbar/index.vue';
export { default as Menu } from './menu/index.vue';
export { default as TabBar } from './tab-bar/index.vue';
export { default as Breadcrumb } from './breadcrumb/index.vue';
export { default as ModalSimple } from './modal/index.vue';

View File

@ -0,0 +1,140 @@
<script lang="tsx">
import type { RouteMeta, RouteRecordRaw } from 'vue-router';
import { useAppStore } from '@/stores';
import { listenerRouteChange } from '@/utils/route-listener';
import { openWindow, regexUrl } from '@/utils';
import useMenuTree from './use-menu-tree';
export default defineComponent({
emit: ['collapse'],
setup() {
const appStore = useAppStore();
const router = useRouter();
const route = useRoute();
const { menuTree } = useMenuTree();
const collapsed = computed({
get() {
if (appStore.device === 'desktop') return appStore.menuCollapse;
return false;
},
set(value: boolean) {
appStore.updateSettings({ menuCollapse: value });
},
});
const topMenu = computed(() => appStore.topMenu);
const openKeys = ref<string[]>([]);
const selectedKey = ref<string[]>([]);
const goto = (item: RouteRecordRaw) => {
// Open external link
if (regexUrl.test(item.path)) {
openWindow(item.path);
selectedKey.value = [item.name as string];
return;
}
// Eliminate external link side effects
const { hideInMenu, activeMenu } = item.meta as RouteMeta;
if (route.name === item.name && !hideInMenu && !activeMenu) {
selectedKey.value = [item.name as string];
return;
}
// Trigger router change
router.push({
name: item.name,
});
};
const findMenuOpenKeys = (target: string) => {
const result: string[] = [];
let isFind = false;
const backtrack = (item: RouteRecordRaw, keys: string[]) => {
if (item.name === target) {
isFind = true;
result.push(...keys);
return;
}
if (item.children?.length) {
item.children.forEach((el) => {
backtrack(el, [...keys, el.name as string]);
});
}
};
menuTree.value.forEach((el: RouteRecordRaw) => {
if (isFind) return; // Performance optimization
backtrack(el, [el.name as string]);
});
return result;
};
listenerRouteChange((newRoute) => {
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
if (requiresAuth && (!hideInMenu || activeMenu)) {
const menuOpenKeys = findMenuOpenKeys((activeMenu || newRoute.name) as string);
const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
openKeys.value = [...keySet];
selectedKey.value = [activeMenu || menuOpenKeys[menuOpenKeys.length - 1]];
}
}, true);
const setCollapse = (val: boolean) => {
if (appStore.device === 'desktop') appStore.updateSettings({ menuCollapse: val });
};
const renderSubMenu = () => {
function travel(_route: RouteRecordRaw[], nodes = []) {
if (_route) {
_route.forEach((element) => {
// This is demo, modify nodes as needed
const icon = element?.meta?.icon ? () => h(element?.meta?.icon as object) : null;
const node =
element?.children && element?.children.length !== 0 ? (
<a-sub-menu
key={element?.name}
v-slots={{
icon,
title: () => element?.meta?.locale || '',
}}
>
{travel(element?.children)}
</a-sub-menu>
) : (
<a-menu-item key={element?.name} v-slots={{ icon }} onClick={() => goto(element)}>
{element?.meta?.locale || ''}
</a-menu-item>
);
nodes.push(node as never);
});
}
return nodes;
}
return travel(menuTree.value);
};
return () => (
<a-menu
mode={topMenu.value ? 'horizontal' : 'vertical'}
v-model:collapsed={collapsed.value}
v-model:open-keys={openKeys.value}
show-collapse-button={appStore.device !== 'mobile'}
auto-open={false}
selected-keys={selectedKey.value}
auto-open-selected={true}
level-indent={34}
style="height: 100%;width:100%;"
onCollapse={setCollapse}
>
{renderSubMenu()}
</a-menu>
);
},
});
</script>
<style lang="less" scoped>
:deep(.arco-menu-inner) {
.arco-menu-inline-header {
display: flex;
align-items: center;
}
.arco-icon {
&:not(.arco-icon-down) {
font-size: 18px;
}
}
}
</style>

View File

@ -0,0 +1,60 @@
import type { RouteRecordRaw, RouteRecordNormalized } from 'vue-router';
import { useAppStore } from '@/stores';
import appClientMenus from '@/router/app-menus';
export default function useMenuTree() {
const appStore = useAppStore();
const appRoute = computed(() => {
if (appStore.menuFromServer) {
// return appClientMenus.concat(toRaw(appStore.appAsyncMenus));
return toRaw(appStore.appAsyncMenus);
}
return appClientMenus;
});
const menuTree = computed(() => {
const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
return (a.meta.order || 0) - (b.meta.order || 0);
});
function travel(_routes: RouteRecordRaw[], layer: number) {
if (!_routes) return null;
const collector: any = _routes.map((element) => {
// leaf node
if (element.meta?.hideChildrenInMenu || !element.children) {
element.children = [];
return element;
}
// route filter hideInMenu true
element.children = element.children.filter((x) => x.meta?.hideInMenu !== true);
// Associated child node
const subItem = travel(element.children, layer + 1);
if (subItem.length) {
element.children = subItem;
return element;
}
// the else logic
if (layer > 1) {
element.children = subItem;
return element;
}
if (element.meta?.hideInMenu === false) {
return element;
}
return null;
});
return collector.filter(Boolean);
}
return travel(copyRouter, 0);
});
return {
menuTree,
};
}

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import type { Component, DefineComponent } from 'vue';
import IconHover from '@arco-design/web-vue/es/_components/icon-hover';
defineProps<{
title?: string;
content?: string | (() => DefineComponent | Component);
}>();
defineEmits(['close']);
</script>
<template>
<slot name="header">
<div class="flex justify-end mb7">
<slot name="close">
<icon-hover @click="$emit('close')">
<icon-close />
</icon-hover>
</slot>
</div>
</slot>
<slot>
<div class="flex flex-col text-center">
<div v-if="title" class="mb4 text-lg font-600">{{ title }}</div>
<template v-else />
<component :is="content" v-if="typeof content === 'function'" />
<div v-else>{{ content }}</div>
</div>
</slot>
</template>

View File

@ -0,0 +1,114 @@
<script lang="ts" setup>
import { useAppStore } from '@/stores';
import { IconExport, IconFile, IconCaretDown } from '@arco-design/web-vue/es/icon';
import { fetchMenusTree } from '@/api/all';
const lists = ref([]);
const getMenus = async () => {
const res = await fetchMenusTree();
lists.value = res;
};
onMounted(() => {
getMenus();
});
const appStore = useAppStore();
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
const avatar = computed(
() => '//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image',
);
const topMenu = computed(() => appStore.topMenu && appStore.menu);
const toggleDrawerMenu = inject('toggleDrawerMenu') as () => void;
function setServerMenu() {
appStore.fetchServerMenuConfig();
console.log(appStore.serverMenu);
}
const handleSelect = (index: any) => {
console.log(index);
};
</script>
<template>
<div class="navbar">
<div class="left-side">
<a-space>
<img src="@/assets/LOGO.svg" alt="" />
</a-space>
</div>
<div class="center-side">
<div class="menu-demo">
<a-menu mode="horizontal" :default-selected-keys="['1']">
<a-menu-item :key="'1'">
<view>工作台</view>
</a-menu-item>
<a-menu-item v-for="(item, index) in lists" :key="index + 2">
<a-dropdown @select="handleSelect" :popup-max-height="false">
<a-button>{{ item.name }}<icon-caret-down /></a-button>
<template #content>
<a-doption v-for="(child, index) in item.children" :key="index">{{ child.name }}</a-doption>
</template>
</a-dropdown>
</a-menu-item>
</a-menu>
</div>
</div>
<ul class="right-side">
<li>
<a-dropdown trigger="click">
<a-avatar class="cursor-pointer" :size="32">
<img alt="avatar" :src="avatar" />
</a-avatar>
</a-dropdown>
</li>
</ul>
</div>
</template>
<style scoped lang="less">
.navbar {
display: flex;
justify-content: space-between;
height: 100%;
background-color: var(--color-bg-2);
border-bottom: 1px solid var(--color-border);
}
.left-side {
display: flex;
align-items: center;
padding-left: 20px;
}
.center-side {
flex: 1;
display: flex;
align-items: center;
margin-left: 40px;
}
.cneter-tip {
font-size: 16px;
font-weight: 400;
color: var(--color-text-1);
}
.menu-demo {
flex: 1;
}
.right-side {
display: flex;
padding-right: 20px;
list-style: none;
li {
display: flex;
align-items: center;
padding: 0 10px;
}
a {
color: var(--color-text-1);
text-decoration: none;
}
.nav-btn {
border-color: rgb(var(--gray-2));
color: rgb(var(--gray-8));
font-size: 16px;
}
}
</style>

View File

@ -0,0 +1,83 @@
<script lang="ts" setup>
import type { RouteLocationNormalized } from 'vue-router';
import { listenerRouteChange, removeRouteListener } from '@/utils/route-listener';
import { useAppStore, useTabBarStore } from '@/stores';
import TabItem from './tab-item.vue';
const appStore = useAppStore();
const tabBarStore = useTabBarStore();
const affixRef = ref();
const tagList = computed(() => {
return tabBarStore.getTabList;
});
const offsetTop = computed(() => {
return appStore.navbar ? 60 : 0;
});
watch(
() => appStore.navbar,
() => {
affixRef.value.updatePosition();
},
);
listenerRouteChange((route: RouteLocationNormalized) => {
if (!route.meta.noAffix && !tagList.value.some((tag) => tag.fullPath === route.fullPath)) {
tabBarStore.updateTabList(route);
}
}, true);
onUnmounted(() => {
removeRouteListener();
});
</script>
<template>
<div class="tab-bar-container">
<a-affix ref="affixRef" :offset-top="offsetTop">
<div class="tab-bar-box">
<div class="tab-bar-scroll">
<div class="tags-wrap">
<tab-item v-for="(tag, index) in tagList" :key="tag.fullPath" :index="index" :item-data="tag" />
</div>
</div>
<div class="tag-bar-operation"></div>
</div>
</a-affix>
</div>
</template>
<style scoped lang="less">
.tab-bar-container {
position: relative;
background-color: var(--color-bg-2);
.tab-bar-box {
display: flex;
padding: 0 0 0 20px;
background-color: var(--color-bg-2);
border-bottom: 1px solid var(--color-border);
.tab-bar-scroll {
height: 32px;
flex: 1;
overflow: hidden;
.tags-wrap {
padding: 4px 0;
height: 48px;
white-space: nowrap;
overflow-x: auto;
:deep(.arco-tag) {
display: inline-flex;
align-items: center;
margin-right: 6px;
cursor: pointer;
&:first-child {
.arco-tag-close-btn {
display: none;
}
}
}
}
}
}
.tag-bar-operation {
width: 100px;
height: 32px;
}
}
</style>

View File

@ -0,0 +1,177 @@
<script lang="ts" setup>
import type { PropType } from 'vue';
import type { TagProps } from '@/stores/modules/tab-bar/types';
import { useTabBarStore } from '@/stores';
import { DEFAULT_ROUTE_NAME, REDIRECT_ROUTE_NAME } from '@/router/constants';
const props = defineProps({
itemData: {
type: Object as PropType<TagProps>,
default() {
return [];
},
},
index: {
type: Number,
default: 0,
},
});
// eslint-disable-next-line no-shadow
enum Eaction {
reload = 'reload',
current = 'current',
left = 'left',
right = 'right',
others = 'others',
all = 'all',
}
const router = useRouter();
const route = useRoute();
const tabBarStore = useTabBarStore();
const goto = (tag: TagProps) => {
router.push({ ...tag });
};
const tagList = computed(() => {
return tabBarStore.getTabList;
});
const disabledReload = computed(() => {
return props.itemData.fullPath !== route.fullPath;
});
const disabledCurrent = computed(() => {
return props.index === 0;
});
const disabledLeft = computed(() => {
return [0, 1].includes(props.index);
});
const disabledRight = computed(() => {
return props.index === tagList.value.length - 1;
});
const tagClose = (tag: TagProps, idx: number) => {
tabBarStore.deleteTag(idx, tag);
if (props.itemData.fullPath === route.fullPath) {
const latest = tagList.value[idx - 1]; // 获取队列的前一个tab
router.push({ name: latest.name });
}
};
const findCurrentRouteIndex = () => {
return tagList.value.findIndex((el) => el.fullPath === route.fullPath);
};
const actionSelect = async (value: any) => {
const { itemData, index } = props;
const copyTagList = [...tagList.value];
if (value === Eaction.current) {
tagClose(itemData, index);
} else if (value === Eaction.left) {
const currentRouteIdx = findCurrentRouteIndex();
copyTagList.splice(1, props.index - 1);
tabBarStore.freshTabList(copyTagList);
if (currentRouteIdx < index) {
router.push({ name: itemData.name });
}
} else if (value === Eaction.right) {
const currentRouteIdx = findCurrentRouteIndex();
copyTagList.splice(props.index + 1);
tabBarStore.freshTabList(copyTagList);
if (currentRouteIdx > index) {
router.push({ name: itemData.name });
}
} else if (value === Eaction.others) {
const filterList = tagList.value.filter((el, idx) => {
return idx === 0 || idx === props.index;
});
tabBarStore.freshTabList(filterList);
router.push({ name: itemData.name });
} else if (value === Eaction.reload) {
tabBarStore.deleteCache(itemData);
await router.push({
name: REDIRECT_ROUTE_NAME,
params: {
path: route.fullPath,
},
});
tabBarStore.addCache(itemData.name);
} else {
tabBarStore.resetTabList();
router.push({ name: DEFAULT_ROUTE_NAME });
}
};
</script>
<template>
<a-dropdown trigger="contextMenu" :popup-max-height="false" @select="actionSelect">
<span
:class="[
'arco-tag arco-tag-size-medium arco-tag-checked',
{ 'link-activated': itemData.fullPath === $route.fullPath },
]"
@click="goto(itemData)"
>
<span class="tag-link">{{ itemData.title }}</span>
<span
class="arco-icon-hover arco-tag-icon-hover arco-icon-hover-size-medium arco-tag-close-btn"
@click.stop="tagClose(itemData, index)"
>
<icon-close />
</span>
</span>
<template #content>
<a-doption :disabled="disabledReload" :value="Eaction.reload">
<icon-refresh />
<span>重新加载</span>
</a-doption>
<a-doption class="sperate-line" :disabled="disabledCurrent" :value="Eaction.current">
<icon-close />
<span>关闭当前标签页</span>
</a-doption>
<a-doption :disabled="disabledLeft" :value="Eaction.left">
<icon-to-left />
<span>关闭左侧标签页</span>
</a-doption>
<a-doption class="sperate-line" :disabled="disabledRight" :value="Eaction.right">
<icon-to-right />
<span>关闭右侧标签页</span>
</a-doption>
<a-doption :value="Eaction.others">
<icon-swap />
<span>关闭其它标签页</span>
</a-doption>
<a-doption :value="Eaction.all">
<icon-folder-delete />
<span>关闭全部标签页</span>
</a-doption>
</template>
</a-dropdown>
</template>
<style scoped lang="less">
.tag-link {
color: var(--color-text-2);
text-decoration: none;
}
.link-activated {
color: rgb(var(--link-6));
.tag-link {
color: rgb(var(--link-6));
}
& + .arco-tag-close-btn {
color: rgb(var(--link-6));
}
}
:deep(.arco-dropdown-option-content) {
span {
margin-left: 10px;
}
}
.arco-dropdown-open {
.tag-link {
color: rgb(var(--danger-6));
}
.arco-tag-close-btn {
color: rgb(var(--danger-6));
}
}
.sperate-line {
border-bottom: 1px solid var(--color-neutral-3);
}
</style>