🔖 项目重命名.
This commit is contained in:
42
orion-visor-ui/src/components/app/app-footer/index.vue
Normal file
42
orion-visor-ui/src/components/app/app-footer/index.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<a-layout-footer class="footer">
|
||||
<a-space direction="vertical" size="small">
|
||||
<a-space size="large">
|
||||
<!-- <a-link target="_blank" href="https://github.com/lijiahangmax/orion-visor">官网</a-link> -->
|
||||
<!-- <a-link target="_blank" href="https://github.com/lijiahangmax/orion-visor">教程</a-link> -->
|
||||
<a-link target="_blank" href="https://github.com/lijiahangmax/orion-visor">github</a-link>
|
||||
<a-link target="_blank" href="https://gitee.com/lijiahangmax/orion-visor">gitee</a-link>
|
||||
<a-link target="_blank" href="https://lijiahangmax.github.io/orion-visor">文档</a-link>
|
||||
<a-link target="_blank" href="https://github.com/lijiahangmax/orion-visor/blob/main/LICENSE">License</a-link>
|
||||
<a-link target="_blank" :href="`https://github.com/lijiahangmax/orion-visor/releases/tag/v${version}`">v{{ version }} Community</a-link>
|
||||
</a-space>
|
||||
<span class="copyright">
|
||||
Copyright<icon-copyright /> {{ new Date().getFullYear() }} Li Jiahang All rights reserved.
|
||||
</span>
|
||||
</a-space>
|
||||
</a-layout-footer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const version = import.meta.env.VITE_APP_VERSION;
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
height: 64px;
|
||||
|
||||
a, span {
|
||||
text-decoration: none;
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
.copyright {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
37
orion-visor-ui/src/components/app/breadcrumb/index.vue
Normal file
37
orion-visor-ui/src/components/app/breadcrumb/index.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<a-breadcrumb class="container-breadcrumb">
|
||||
<a-breadcrumb-item>
|
||||
<icon-apps />
|
||||
</a-breadcrumb-item>
|
||||
<a-breadcrumb-item v-for="item in items" :key="item">
|
||||
{{ item }}
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
items?: Array<string>;
|
||||
}>(), {
|
||||
items: () => {
|
||||
return useRoute().matched
|
||||
.map(s => s.meta?.locale as string)
|
||||
.filter(Boolean) || [];
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container-breadcrumb {
|
||||
|
||||
:deep(.arco-breadcrumb-item) {
|
||||
color: rgb(var(--gray-6));
|
||||
|
||||
&:last-child {
|
||||
color: rgb(var(--gray-8));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1
orion-visor-ui/src/components/app/navbar/const.ts
Normal file
1
orion-visor-ui/src/components/app/navbar/const.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const preferenceTipsKey = 'home:preference';
|
||||
417
orion-visor-ui/src/components/app/navbar/index.vue
Normal file
417
orion-visor-ui/src/components/app/navbar/index.vue
Normal file
@@ -0,0 +1,417 @@
|
||||
<template>
|
||||
<div class="navbar">
|
||||
<!-- 左侧按钮 -->
|
||||
<div class="left-side">
|
||||
<a-space>
|
||||
<!-- LOGO -->
|
||||
<img class="left-side-logo"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
src="@/assets/images/logo.svg?url" />
|
||||
<!-- 标头 -->
|
||||
<a-typography-title :heading="5"
|
||||
:style="{ margin: 0, fontSize: '18px', height: '1.4em', overflow: 'hidden' }">
|
||||
Orion Visor
|
||||
</a-typography-title>
|
||||
<!-- 收缩菜单 -->
|
||||
<icon-menu-fold v-if="!topMenu && appStore.device === 'mobile'"
|
||||
style="font-size: 22px; cursor: pointer"
|
||||
@click="toggleDrawerMenu" />
|
||||
</a-space>
|
||||
</div>
|
||||
<!-- 顶部菜单 -->
|
||||
<div class="center-side">
|
||||
<system-menu-tree v-if="topMenu" />
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<ul class="right-side">
|
||||
<!-- 主机终端 -->
|
||||
<li v-permission="['asset:host-terminal:access']">
|
||||
<a-tooltip content="主机终端">
|
||||
<a-button class="nav-btn"
|
||||
type="outline"
|
||||
shape="circle"
|
||||
@click="openNewRoute({ name: 'terminal' })">
|
||||
<template #icon>
|
||||
<icon-code-square />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</li>
|
||||
<!-- 切换语言 -->
|
||||
<li v-if="false">
|
||||
<a-tooltip content="语言">
|
||||
<a-button class="nav-btn"
|
||||
type="outline"
|
||||
shape="circle"
|
||||
@click="setLocalesVisible">
|
||||
<template #icon>
|
||||
<icon-language />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-dropdown trigger="click" @select="s => changeLocale(s as string)">
|
||||
<div ref="localeRef" class="trigger-btn" />
|
||||
<template #content>
|
||||
<a-doption v-for="item in locales"
|
||||
:key="item.value"
|
||||
:value="item.value">
|
||||
<template #icon>
|
||||
<icon-check v-show="item.value === currentLocale" />
|
||||
</template>
|
||||
{{ item.label }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</li>
|
||||
<!-- 暗色模式 -->
|
||||
<li>
|
||||
<a-tooltip :content="theme === 'light'
|
||||
? '点击切换为暗黑模式'
|
||||
: '点击切换为亮色模式'">
|
||||
<a-button class="nav-btn"
|
||||
type="outline"
|
||||
shape="circle"
|
||||
@click="handleToggleTheme">
|
||||
<template #icon>
|
||||
<icon-moon-fill v-if="theme === 'dark'" />
|
||||
<icon-sun-fill v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</li>
|
||||
<!-- 系统消息 -->
|
||||
<li>
|
||||
<a-tooltip content="系统消息" :show-arrow="false">
|
||||
<div class="message-box-trigger">
|
||||
<a-badge :count="messageCount" dot>
|
||||
<a-button class="nav-btn"
|
||||
type="outline"
|
||||
shape="circle"
|
||||
@click="setMessageBoxVisible">
|
||||
<icon-notification />
|
||||
</a-button>
|
||||
</a-badge>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-popover trigger="click"
|
||||
content-class="message-popover"
|
||||
position="br"
|
||||
:show-arrow="false"
|
||||
:popup-style="{ marginLeft: '198px' }"
|
||||
:content-style="{ padding: 0, width: '428px' }"
|
||||
@hide="pullHasUnreadMessage">
|
||||
<div ref="messageRef" class="ref-btn" />
|
||||
<template #content>
|
||||
<message-box />
|
||||
</template>
|
||||
</a-popover>
|
||||
</li>
|
||||
<!-- 全屏模式 -->
|
||||
<li>
|
||||
<a-tooltip :content="isFullscreen
|
||||
? '点击退出全屏模式'
|
||||
: '点击切换全屏模式'">
|
||||
<a-button class="nav-btn"
|
||||
type="outline"
|
||||
shape="circle"
|
||||
@click="toggleFullScreen">
|
||||
<template #icon>
|
||||
<icon-fullscreen-exit v-if="isFullscreen" />
|
||||
<icon-fullscreen v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</li>
|
||||
<!-- 刷新页面 -->
|
||||
<li>
|
||||
<a-tooltip content="刷新页面">
|
||||
<a-button class="nav-btn"
|
||||
type="outline"
|
||||
shape="circle"
|
||||
@click="reloadCurrent">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</li>
|
||||
<!-- 偏好设置 -->
|
||||
<li>
|
||||
<a-popover :popup-visible="tippedPreference" position="br">
|
||||
<template #title>
|
||||
💡 点击这里可以修改系统偏好~
|
||||
</template>
|
||||
<template #content>
|
||||
<span style="line-height: 1.8">
|
||||
◾ 可以修改页面布局<br>
|
||||
◾ 可以切换显示视图
|
||||
</span>
|
||||
<div class="tips-buttons">
|
||||
<a-button size="mini" class="mr8" @click="closePreferenceTip(false)">关闭</a-button>
|
||||
<a-button size="mini" type="primary" @click="closePreferenceTip(true)">我知道了</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-tooltip content="偏好设置">
|
||||
<a-button class="nav-btn"
|
||||
type="outline"
|
||||
shape="circle"
|
||||
@click="openAppSetting">
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-popover>
|
||||
</li>
|
||||
<!-- 用户信息 -->
|
||||
<li>
|
||||
<a-dropdown trigger="click" position="br">
|
||||
<!-- 头像 -->
|
||||
<a-avatar draggable="false"
|
||||
:size="32"
|
||||
:style="{ cursor: 'pointer', backgroundColor: 'rgb(var(--primary-6))', userSelect: 'none' }">
|
||||
{{ nickname }}
|
||||
</a-avatar>
|
||||
<template #content>
|
||||
<!-- 个人中心 -->
|
||||
<a-doption>
|
||||
<a-space @click="$router.push({ name: 'userInfo' })">
|
||||
<icon-user />
|
||||
<span>个人中心</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
<!-- 修改密码 -->
|
||||
<a-doption>
|
||||
<a-space @click="() => updatePasswordRef.open()">
|
||||
<icon-lock />
|
||||
<span>修改密码</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
<!-- 退出登录 -->
|
||||
<a-doption>
|
||||
<a-space @click="handleLogout">
|
||||
<icon-export />
|
||||
<span>退出登录</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- 修改密码模态框-->
|
||||
<update-password-modal ref="updatePasswordRef" @updated="handleLogout" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, onUnmounted, ref } from 'vue';
|
||||
import useLocale from '@/hooks/locale';
|
||||
import useUser from '@/hooks/user';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useDark, useFullscreen, useToggle } from '@vueuse/core';
|
||||
import { useAppStore, useTabBarStore, useTipsStore, useUserStore } from '@/store';
|
||||
import { LOCALE_OPTIONS } from '@/locale';
|
||||
import { triggerMouseEvent } from '@/utils/event';
|
||||
import { openAppSettingKey, toggleDrawerMenuKey } from '@/types/symbol';
|
||||
import { preferenceTipsKey } from './const';
|
||||
import { REDIRECT_ROUTE_NAME, routerToTag } from '@/router/constants';
|
||||
import { openNewRoute } from '@/router';
|
||||
import { checkHasUnreadMessage } from '@/api/system/message';
|
||||
import SystemMenuTree from '@/components/system/menu/tree/index.vue';
|
||||
import MessageBox from '@/components/system/message-box/index.vue';
|
||||
import UpdatePasswordModal from '@/components/user/user/update-password-modal/index.vue';
|
||||
|
||||
const tipsStore = useTipsStore();
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const tabBarStore = useTabBarStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { logout } = useUser();
|
||||
const { changeLocale, currentLocale } = useLocale();
|
||||
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
|
||||
|
||||
// 主题
|
||||
const darkTheme = useDark({
|
||||
selector: 'body',
|
||||
attribute: 'arco-theme',
|
||||
valueDark: 'dark',
|
||||
valueLight: 'light',
|
||||
storageKey: 'arco-theme',
|
||||
onChanged(dark: boolean) {
|
||||
appStore.updateSettings({
|
||||
theme: dark ? 'dark' : 'light'
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 用户名
|
||||
const nickname = computed(() => userStore.nickname?.substring(0, 1));
|
||||
// 是否展示顶部菜单
|
||||
const topMenu = computed(() => appStore.topMenu && appStore.menu);
|
||||
// 当前主题
|
||||
const theme = computed(() => appStore.theme);
|
||||
|
||||
const locales = [...LOCALE_OPTIONS];
|
||||
// 偏好提示
|
||||
const tippedPreference = ref(tipsStore.isNotTipped(preferenceTipsKey));
|
||||
// 修改密码
|
||||
const updatePasswordRef = ref();
|
||||
// 消息
|
||||
const messageRef = ref();
|
||||
// 语言
|
||||
const localeRef = ref();
|
||||
// 消息数量
|
||||
const messageCount = ref(0);
|
||||
const messageIntervalId = ref();
|
||||
|
||||
// 打开应用设置
|
||||
const openAppSetting = inject(openAppSettingKey) as () => void;
|
||||
|
||||
// 注入收缩菜单
|
||||
const toggleDrawerMenu = inject(toggleDrawerMenuKey) as () => void;
|
||||
|
||||
// 切换主题
|
||||
const handleToggleTheme = () => {
|
||||
useToggle(darkTheme)();
|
||||
};
|
||||
|
||||
// 打开消息
|
||||
const setMessageBoxVisible = () => {
|
||||
triggerMouseEvent(messageRef);
|
||||
};
|
||||
|
||||
// 打开语言切换
|
||||
const setLocalesVisible = () => {
|
||||
triggerMouseEvent(localeRef);
|
||||
};
|
||||
|
||||
// 刷新页面
|
||||
const reloadCurrent = async () => {
|
||||
if (appStore.tabBar) {
|
||||
// 重新加载 tab
|
||||
const itemData = routerToTag(route);
|
||||
tabBarStore.deleteCache(itemData);
|
||||
await router.push({
|
||||
name: REDIRECT_ROUTE_NAME,
|
||||
params: { path: route.fullPath },
|
||||
});
|
||||
tabBarStore.addCache(itemData.name);
|
||||
} else {
|
||||
// 刷新页面
|
||||
router.go(0);
|
||||
}
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
};
|
||||
|
||||
// 获取是否有未读的消息
|
||||
const pullHasUnreadMessage = () => {
|
||||
// 查询
|
||||
checkHasUnreadMessage().then(({ data }) => {
|
||||
messageCount.value = data ? 1 : 0;
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭偏好提示
|
||||
const closePreferenceTip = (ack: boolean) => {
|
||||
tippedPreference.value = false;
|
||||
if (ack) {
|
||||
tipsStore.setTipped(preferenceTipsKey);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 查询未读消息
|
||||
pullHasUnreadMessage();
|
||||
// 注册未读消息轮询
|
||||
messageIntervalId.value = setInterval(pullHasUnreadMessage, 30000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理消息轮询
|
||||
clearInterval(messageIntervalId.value);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
background-color: var(--color-bg-2);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.left-side {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
color: var(--color-text-1);
|
||||
|
||||
&-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.center-side {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.right-side {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
|
||||
:deep(.locale-select) {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.trigger-btn,
|
||||
.ref-btn {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
}
|
||||
|
||||
.trigger-btn {
|
||||
margin-left: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.message-popover {
|
||||
.arco-popover-content {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tips-buttons {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
90
orion-visor-ui/src/components/app/setting/block.vue
Normal file
90
orion-visor-ui/src/components/app/setting/block.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="block">
|
||||
<h5 class="title">{{ title }}</h5>
|
||||
<template v-for="option in options" :key="option.name">
|
||||
<div class="option-wrapper"
|
||||
v-permission="option.permission || []"
|
||||
:style="{ 'margin': option.margin || '' }">
|
||||
<!-- 偏好项 -->
|
||||
<span>{{ option.name }}</span>
|
||||
<!-- 偏好值 -->
|
||||
<form-wrapper :name="option.key"
|
||||
:type="option.type as string"
|
||||
:default-value="option.defaultVal"
|
||||
:options="option.options"
|
||||
@input-change="handleChange" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { RadioOption } from '@arco-design/web-vue/es/radio/interface';
|
||||
import type { SelectOption } from '@arco-design/web-vue/es/select/interface';
|
||||
import { useAppStore } from '@/store';
|
||||
import { updatePreference } from '@/api/user/preference';
|
||||
import FormWrapper from './form-wrapper.vue';
|
||||
|
||||
interface OptionsProps {
|
||||
name: string;
|
||||
key: string;
|
||||
type?: string;
|
||||
permission?: string[];
|
||||
defaultVal?: boolean | string | number;
|
||||
options?: Array<RadioOption | SelectOption>;
|
||||
margin?: string;
|
||||
}
|
||||
|
||||
defineProps<Partial<{
|
||||
title: string;
|
||||
options: Array<OptionsProps>;
|
||||
}>>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
/**
|
||||
* 修改配置
|
||||
*/
|
||||
const handleChange = async ({ key, value, }: {
|
||||
key: string;
|
||||
value: unknown;
|
||||
}) => {
|
||||
// 顶部菜单
|
||||
if (key === 'topMenu') {
|
||||
appStore.updateSettings({
|
||||
menuCollapse: false,
|
||||
});
|
||||
}
|
||||
// 修改配置
|
||||
appStore.updateSettings({ [key]: value });
|
||||
// 同步偏好
|
||||
try {
|
||||
await updatePreference({
|
||||
type: 'SYSTEM',
|
||||
item: key,
|
||||
value
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.block {
|
||||
margin-bottom: 24px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 10px 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.option-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
55
orion-visor-ui/src/components/app/setting/form-wrapper.vue
Normal file
55
orion-visor-ui/src/components/app/setting/form-wrapper.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<!-- 数字框 -->
|
||||
<a-input-number v-if="type === 'number'"
|
||||
:style="{ width: '80px' }"
|
||||
size="small"
|
||||
:precision="0"
|
||||
:default-value="defaultValue as number"
|
||||
@change="handleChange"
|
||||
hide-button />
|
||||
<!-- 开关 -->
|
||||
<a-switch v-else-if="type === 'switch'"
|
||||
type="round"
|
||||
:default-checked="defaultValue as boolean"
|
||||
size="small"
|
||||
@change="handleChange" />
|
||||
<!-- 单选按钮 -->
|
||||
<a-radio-group v-else-if="type === 'radio-group'"
|
||||
type="button"
|
||||
size="small"
|
||||
:default-value="defaultValue"
|
||||
:options="options as Array<RadioOption>"
|
||||
@change="handleChange" />
|
||||
<!-- 选择框 -->
|
||||
<a-select v-else-if="type === 'select'"
|
||||
size="small"
|
||||
style="width: 128px;"
|
||||
:default-value="defaultValue"
|
||||
:options="options as Array<SelectOption>"
|
||||
@change="handleChange" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { RadioOption } from '@arco-design/web-vue/es/radio/interface';
|
||||
import type { SelectOption } from '@arco-design/web-vue/es/select/interface';
|
||||
|
||||
const props = withDefaults(defineProps<Partial<{
|
||||
type: string;
|
||||
name: string;
|
||||
defaultValue: string | boolean | number;
|
||||
options: Array<RadioOption | SelectOption>;
|
||||
}>>(), {
|
||||
type: 'switch',
|
||||
name: '',
|
||||
defaultValue: '',
|
||||
options: () => []
|
||||
});
|
||||
const emit = defineEmits(['inputChange']);
|
||||
|
||||
const handleChange = (value: unknown) => {
|
||||
emit('inputChange', {
|
||||
value,
|
||||
key: props.name,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
163
orion-visor-ui/src/components/app/setting/index.vue
Normal file
163
orion-visor-ui/src/components/app/setting/index.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<!-- 左侧固定配置按钮 -->
|
||||
<div v-if="!appStore.navbar" class="fixed-settings" @click="open">
|
||||
<a-button type="primary">
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
<!-- 偏好配置抽屉 -->
|
||||
<a-drawer v-model:visible="visible"
|
||||
title="偏好设置"
|
||||
:width="300"
|
||||
:footer="false"
|
||||
:unmount-on-close="true"
|
||||
@cancel="() => setVisible(false)">
|
||||
<div class="preference-containers">
|
||||
<!-- 布局设置 -->
|
||||
<block :options="layoutOpts" title="布局设置" />
|
||||
<!-- 数据设置 -->
|
||||
<block :options="dataOpts" title="数据设置" />
|
||||
<!-- 页面视图 -->
|
||||
<block :options="viewsOpts" title="页面视图" />
|
||||
</div>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useAppStore } from '@/store';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { CardPageSizeOptions, TablePageSizeOptions } from '@/types/const';
|
||||
import Block from './block.vue';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { visible, setVisible } = useVisible();
|
||||
|
||||
// 打开
|
||||
const open = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
defineExpose({ open });
|
||||
|
||||
// 布局设置
|
||||
const layoutOpts = computed(() => [
|
||||
{
|
||||
name: '导航栏',
|
||||
key: 'navbar',
|
||||
defaultVal: appStore.navbar
|
||||
},
|
||||
{
|
||||
name: '菜单栏',
|
||||
key: 'menu',
|
||||
defaultVal: appStore.menu,
|
||||
},
|
||||
{
|
||||
name: '顶部菜单栏',
|
||||
key: 'topMenu',
|
||||
defaultVal: appStore.topMenu,
|
||||
},
|
||||
{
|
||||
name: '底部页脚',
|
||||
key: 'footer',
|
||||
defaultVal: appStore.footer
|
||||
},
|
||||
{
|
||||
name: '多页签',
|
||||
key: 'tabBar',
|
||||
defaultVal: appStore.tabBar
|
||||
},
|
||||
{
|
||||
name: '色弱模式',
|
||||
key: 'colorWeak',
|
||||
defaultVal: appStore.colorWeak,
|
||||
},
|
||||
{
|
||||
name: '菜单宽度 (px)',
|
||||
key: 'menuWidth',
|
||||
type: 'number',
|
||||
defaultVal: appStore.menuWidth,
|
||||
},
|
||||
]);
|
||||
|
||||
// 布局设置
|
||||
const dataOpts = computed(() => [
|
||||
{
|
||||
name: '表格默认页数',
|
||||
key: 'defaultTablePageSize',
|
||||
type: 'select',
|
||||
margin: '0 0 4px 0',
|
||||
defaultVal: appStore.defaultTablePageSize,
|
||||
options: TablePageSizeOptions.map(s => {
|
||||
return {
|
||||
value: s,
|
||||
label: `${s} 条/页`
|
||||
};
|
||||
})
|
||||
},
|
||||
{
|
||||
name: '卡片默认页数',
|
||||
key: 'defaultCardPageSize',
|
||||
type: 'select',
|
||||
defaultVal: appStore.defaultCardPageSize,
|
||||
options: CardPageSizeOptions.map(s => {
|
||||
return {
|
||||
value: s,
|
||||
label: `${s} 条/页`
|
||||
};
|
||||
})
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
// 页面视图配置
|
||||
const viewsOpts = computed(() => [
|
||||
{
|
||||
name: '主机列表',
|
||||
key: 'hostView',
|
||||
type: 'radio-group',
|
||||
margin: '0 0 4px 0',
|
||||
permission: ['asset:host:query'],
|
||||
defaultVal: appStore.hostView,
|
||||
options: [{ value: 'table', label: '表格' }, { value: 'card', label: '卡片' }]
|
||||
},
|
||||
{
|
||||
name: '主机秘钥',
|
||||
key: 'hostKeyView',
|
||||
type: 'radio-group',
|
||||
margin: '0 0 4px 0',
|
||||
permission: ['asset:host-key:query'],
|
||||
defaultVal: appStore.hostKeyView,
|
||||
options: [{ value: 'table', label: '表格' }, { value: 'card', label: '卡片' }]
|
||||
},
|
||||
{
|
||||
name: '主机身份',
|
||||
key: 'hostIdentityView',
|
||||
type: 'radio-group',
|
||||
margin: '0 0 4px 0',
|
||||
permission: ['asset:host-identity:query'],
|
||||
defaultVal: appStore.hostIdentityView,
|
||||
options: [{ value: 'table', label: '表格' }, { value: 'card', label: '卡片' }]
|
||||
},
|
||||
]);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.fixed-settings {
|
||||
position: fixed;
|
||||
top: 280px;
|
||||
right: 0;
|
||||
|
||||
svg {
|
||||
font-size: 18px;
|
||||
vertical-align: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
.preference-containers{
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
</style>
|
||||
108
orion-visor-ui/src/components/app/tab-bar/index.vue
Normal file
108
orion-visor-ui/src/components/app/tab-bar/index.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<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>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { routerToTag } from '@/router/constants';
|
||||
import { listenerRouteChange, removeRouteListener, } from '@/utils/route-listener';
|
||||
import { useAppStore, useTabBarStore } from '@/store';
|
||||
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.path === route.path)
|
||||
) {
|
||||
// 固定并且没有此 tab 则添加
|
||||
tabBarStore.addTab(routerToTag(route), route.meta?.ignoreCache as unknown as boolean);
|
||||
}
|
||||
}, true);
|
||||
|
||||
onUnmounted(() => {
|
||||
removeRouteListener();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tab-bar-container {
|
||||
position: relative;
|
||||
background-color: var(--color-bg-2);
|
||||
|
||||
.tab-bar-box {
|
||||
display: flex;
|
||||
padding: 0 0 0 6px;
|
||||
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-link {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-bar-operation {
|
||||
width: 100px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
199
orion-visor-ui/src/components/app/tab-bar/tab-item.vue
Normal file
199
orion-visor-ui/src/components/app/tab-bar/tab-item.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<a-dropdown trigger="contextMenu"
|
||||
:popup-max-height="false"
|
||||
@select="actionSelect">
|
||||
<span class="arco-tag arco-tag-size-medium arco-tag-checked"
|
||||
:class="{ 'link-activated': itemData?.path === $route.path }"
|
||||
@click="goto(itemData as TagProps)">
|
||||
<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 as TagProps, index)">
|
||||
<icon-close />
|
||||
</span>
|
||||
</span>
|
||||
<template #content>
|
||||
<a-doption :disabled="disabledReload" :value="Option.reload">
|
||||
<icon-refresh />
|
||||
<span>重新加载</span>
|
||||
</a-doption>
|
||||
<a-doption
|
||||
class="group-line"
|
||||
:disabled="disabledCurrent"
|
||||
:value="Option.current">
|
||||
<icon-close />
|
||||
<span>关闭当前标签页</span>
|
||||
</a-doption>
|
||||
<a-doption :disabled="disabledLeft" :value="Option.left">
|
||||
<icon-to-left />
|
||||
<span>关闭左侧标签页</span>
|
||||
</a-doption>
|
||||
<a-doption
|
||||
class="group-line"
|
||||
:disabled="disabledRight"
|
||||
:value="Option.right">
|
||||
<icon-to-right />
|
||||
<span>关闭右侧标签页</span>
|
||||
</a-doption>
|
||||
<a-doption :value="Option.others">
|
||||
<icon-swap />
|
||||
<span>关闭其它标签页</span>
|
||||
</a-doption>
|
||||
<a-doption :value="Option.all">
|
||||
<icon-folder-delete />
|
||||
<span>关闭全部标签页</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TagProps } from '@/store/modules/tab-bar/types';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useTabBarStore } from '@/store';
|
||||
import { DEFAULT_ROUTE_NAME, REDIRECT_ROUTE_NAME } from '@/router/constants';
|
||||
|
||||
enum Option {
|
||||
reload = 'reload',
|
||||
current = 'current',
|
||||
left = 'left',
|
||||
right = 'right',
|
||||
others = 'others',
|
||||
all = 'all',
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
index: number;
|
||||
itemData: TagProps;
|
||||
}>();
|
||||
|
||||
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.path !== route.path;
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
// 关闭 tag
|
||||
const tagClose = (tag: TagProps, idx: number) => {
|
||||
tabBarStore.deleteTab(idx, tag);
|
||||
if (props.itemData.path === route.path) {
|
||||
// 获取队列的前一个 tab
|
||||
const latest = tagList.value[idx - 1];
|
||||
router.push({ name: latest.name });
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前路由索引
|
||||
const findCurrentRouteIndex = () => {
|
||||
return tagList.value.findIndex((el) => el.path === route.path);
|
||||
};
|
||||
|
||||
// 选择操作
|
||||
const actionSelect = async (value: any) => {
|
||||
const { itemData, index } = props;
|
||||
const copyTagList = [...tagList.value];
|
||||
if (value === Option.current) {
|
||||
// 关闭当前
|
||||
tagClose(itemData, index);
|
||||
} else if (value === Option.left) {
|
||||
// 关闭左侧
|
||||
const currentRouteIdx = findCurrentRouteIndex();
|
||||
copyTagList.splice(1, props.index - 1);
|
||||
|
||||
tabBarStore.freshTabList(copyTagList);
|
||||
if (currentRouteIdx < index) {
|
||||
await router.push({ name: itemData.name });
|
||||
}
|
||||
} else if (value === Option.right) {
|
||||
// 关闭右侧
|
||||
const currentRouteIdx = findCurrentRouteIndex();
|
||||
copyTagList.splice(props.index + 1);
|
||||
|
||||
tabBarStore.freshTabList(copyTagList);
|
||||
if (currentRouteIdx > index) {
|
||||
await router.push({ name: itemData.name });
|
||||
}
|
||||
} else if (value === Option.others) {
|
||||
// 关闭其他
|
||||
const filterList = tagList.value.filter((el, idx) => {
|
||||
return idx === 0 || idx === props.index;
|
||||
});
|
||||
tabBarStore.freshTabList(filterList);
|
||||
await router.push({ name: itemData.name });
|
||||
} else if (value === Option.reload) {
|
||||
// 重新加载
|
||||
tabBarStore.deleteCache(itemData);
|
||||
await router.push({
|
||||
name: REDIRECT_ROUTE_NAME,
|
||||
params: { path: route.fullPath },
|
||||
});
|
||||
tabBarStore.addCache(itemData.name);
|
||||
} else {
|
||||
// 关闭全部
|
||||
tabBarStore.resetTabList();
|
||||
await router.push({ name: DEFAULT_ROUTE_NAME });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.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));
|
||||
}
|
||||
}
|
||||
|
||||
.group-line {
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user