Compare commits

...

8 Commits

Author SHA1 Message Date
lijiahangmax
e5aec94207 Merge pull request #4 from lijiahangmax/dev
Dev
2024-03-25 00:14:33 +08:00
lijiahangmax
2ff090a974 📝 docs. 2024-03-25 00:13:05 +08:00
lijiahangmax
cc823d5bcd 🔖 升级版本 2024-03-25 00:04:21 +08:00
lijiahangmax
fade56b12a 🎨 粘贴安全策略提示. 2024-03-24 23:07:16 +08:00
lijiahangmax
cf188451dd 🐛 终端无法粘贴. 2024-03-24 14:40:42 +08:00
lijiahangmax
1125ef8a5a 🐛 终端无法粘贴. 2024-03-23 23:31:21 +08:00
lijiahang
a3d4420754 ⬆️ 升级 arco 版本. 2024-03-22 20:01:05 +08:00
lijiahang
27c1e16e57 🐛 用户登录后 404. 2024-03-22 16:32:37 +08:00
48 changed files with 7686 additions and 4732 deletions

View File

@@ -28,7 +28,7 @@
<br/>
当前版本: **1.0.2**
当前版本: **1.0.3**
github: https://github.com/lijiahangmax/orion-ops-pro
gitee: https://gitee.com/lijiahangmax/orion-ops-pro
文档: https://lijiahangmax.gitee.io/orion-ops-pro/#/

View File

@@ -1,7 +1,7 @@
version: '3.3'
services:
orion-ops-pro:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-ops-pro:1.0.2
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-ops-pro:1.0.3
ports:
- 1081:80
environment:

View File

@@ -1,3 +1,3 @@
mv ../../orion-ops-launch/target/orion-ops-launch.jar ./
mv ../../orion-ops-ui/dist ./dist
docker build -t orion-ops-pro:1.0.2 .
docker build -t orion-ops-pro:1.0.3 .

View File

@@ -29,7 +29,7 @@
<br/>
当前版本: **1.0.2**
当前版本: **1.0.3**
github: https://github.com/lijiahangmax/orion-ops-pro
gitee: https://gitee.com/lijiahangmax/orion-ops-pro
文档: https://lijiahangmax.gitee.io/orion-ops-pro/#/

View File

@@ -1,4 +1,4 @@
# orion-ops-pro <small>1.0.2</small>
# orion-ops-pro <small>1.0.3</small>
> 一款开箱即用的运维平台。

View File

@@ -1,6 +1,21 @@
> 版本号严格遵循 Semver 规范。
## v1.0.2
## v1.0.3
`2024-03-25` `release`
* 🚀 升级 `arco design``2.55.0`
* 🐞 修复 新创建的用户登录会跳转到 **404**
* 🐞 修复 分配菜单模态框没有子菜单不显示的问题
* 🐞 修复 工作台页面快捷操作面板会展示隐藏的菜单
* 🐞 修复 主机终端无法粘贴 (浏览器安全策略)
* 🐞 修复 卡片列表组件控制台 warn 提示
* 🐞 修复 关闭终端时控制台提示 handleResize 错误信息
* 🔨 修改 系统菜单渲染逻辑 (移除 JSX 构建时不会提示 JSX.IntrinsicElements)
[如何升级](/about/update.md?id=_v103)
## v1.0.3
`2024-03-22` `release`

View File

@@ -12,3 +12,7 @@
* 资产授权 UI 改版
* RDP 远程桌面
* 接入 config 后端动态配置
## 已知问题 🐞
* 顶部菜单折叠宽度计算有问题 (arco 框架内问题)

View File

@@ -1,5 +1,13 @@
⚡ 注意: 应用不支持跨版本升级, 可以进行多次升级
## v1.0.3
> sql 脚本
```sql
DELETE FROM preference WHERE type = 'TERMINAL';
```
## v1.0.2
> sql 脚本

View File

@@ -14,7 +14,7 @@
<url>https://github.com/lijiahangmax/orion-ops-pro</url>
<properties>
<revision>1.0.2</revision>
<revision>1.0.3</revision>
<spring.boot.version>2.7.17</spring.boot.version>
<spring.boot.admin.version>2.7.15</spring.boot.admin.version>
<flatten.maven.plugin.version>1.5.0</flatten.maven.plugin.version>

View File

@@ -12,7 +12,7 @@ public interface OrionOpsProConst {
/**
* 同 ${orion.version} 迭代时候需要手动更改
*/
String VERSION = "1.0.2";
String VERSION = "1.0.3";
String GITHUB = "https://github.com/lijiahangmax/orion-ops-pro";

View File

@@ -26,6 +26,8 @@ import java.util.List;
@Component
public class SftpListHandler extends AbstractTerminalHandler<SftpListRequest> {
private static final String HOME_PATH = "~";
@Override
public void handle(WebSocketSession channel, SftpListRequest payload) {
// 获取会话
@@ -37,7 +39,7 @@ public class SftpListHandler extends AbstractTerminalHandler<SftpListRequest> {
List<SftpFileVO> list = Lists.empty();
try {
// 空目录则直接获取 home 目录
if (Strings.isBlank(path)) {
if (HOME_PATH.equals(path)) {
path = session.getHome();
}
// 文件列表

View File

@@ -74,7 +74,7 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
new TerminalPreferenceModel.ShortcutKeysModel("changeToNextSession", true, false, true, "BracketRight", true),
// 终端快捷键
new TerminalPreferenceModel.ShortcutKeysModel("copy", true, true, false, "KeyC", true),
new TerminalPreferenceModel.ShortcutKeysModel("paste", true, true, false, "KeyV", true),
new TerminalPreferenceModel.ShortcutKeysModel("paste", true, true, false, "Insert", true),
new TerminalPreferenceModel.ShortcutKeysModel("toTop", true, true, false, "ArrowUp", true),
new TerminalPreferenceModel.ShortcutKeysModel("toBottom", true, true, false, "ArrowDown", true),
new TerminalPreferenceModel.ShortcutKeysModel("selectAll", true, true, false, "KeyA", true),
@@ -94,7 +94,7 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
.selectAll(false)
.search(true)
.copy(true)
.paste(true)
.paste(false)
.interrupt(false)
.enter(false)
.fontSizePlus(false)
@@ -112,7 +112,7 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
.theme(new JSONObject())
.displaySetting(JSONObject.parseObject(defaultDisplaySetting))
.actionBarSetting(JSONObject.parseObject(actionBarSetting))
.rightMenuSetting(Lists.of("copy", "paste", "checkAll", "search", "clear"))
.rightMenuSetting(Lists.of("selectAll", "copy", "fontSizePlus", "fontSizeSubtract", "search", "clear"))
.interactSetting(JSONObject.parseObject(defaultInteractSetting))
.pluginsSetting(JSONObject.parseObject(defaultPluginsSetting))
.sessionSetting(JSONObject.parseObject(defaultSessionSetting))

View File

@@ -1,12 +1,13 @@
{
"name": "orion-ops-pro-ui",
"description": "Orion Ops Pro for Vue",
"version": "1.0.2",
"version": "1.0.3",
"private": true,
"author": "Jiahang Li",
"license": "Apache 2.0",
"scripts": {
"dev": "vite --config ./config/vite.config.dev.ts",
"dev:host": "vite --config ./config/vite.config.dev.ts --host",
"build": "vue-tsc --noEmit && vite build --config ./config/vite.config.prod.ts",
"report": "cross-env REPORT=true npm run build",
"preview": "npm run build && vite preview --host",
@@ -29,7 +30,7 @@
]
},
"dependencies": {
"@arco-design/web-vue": "^2.53.3",
"@arco-design/web-vue": "^2.55.0",
"@dangojs/a-query-header": "^0.0.31",
"@sanqi377/arco-vue-icon-picker": "^1.0.7",
"@vueuse/core": "^9.3.0",

11570
orion-ops-ui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@
</div>
<!-- 顶部菜单 -->
<div class="center-side">
<menu-tree v-if="topMenu" />
<system-menu-tree v-if="topMenu" />
</div>
<!-- 右侧操作 -->
<ul class="right-side">
@@ -214,7 +214,7 @@
import { preferenceTipsKey } from './const';
import { REDIRECT_ROUTE_NAME, routerToTag } from '@/router/constants';
import { openNewRoute } from '@/router';
import MenuTree from '@/components/system/menu/tree/index.vue';
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';

View File

@@ -4,7 +4,7 @@
<div class="tab-bar-box">
<div class="tab-bar-scroll">
<div class="tags-wrap">
<TabItem v-for="(tag, index) in tagList"
<tab-item v-for="(tag, index) in tagList"
:key="tag.fullPath"
:index="index"
:item-data="tag" />

View File

@@ -272,7 +272,9 @@ export default class LogAppender implements ILogAppender {
.filter(Boolean)
.forEach(s => s.dispose());
// 卸载终端
s.terminal?.dispose();
setTimeout(() => {
s.terminal?.dispose();
}, 300);
} catch (e) {
// 卸载可能会报错
}

View File

@@ -10,8 +10,10 @@
<tbody>
<a-checkbox-group v-model="checkedKeys" style="display: contents;">
<template v-for="parentMenu in menuData" :key="parentMenu.id">
<template v-for="(childrenMenu, i) in parentMenu.children" :key="childrenMenu.id">
<tr>
<!-- 有子菜单 -->
<template v-if="parentMenu.children?.length">
<tr v-for="(childrenMenu, i) in parentMenu.children"
:key="childrenMenu.id">
<!-- 父菜单 -->
<td v-if="i === 0" :rowspan="parentMenu.children.length">
<a-checkbox :value="parentMenu.id">
@@ -38,6 +40,23 @@
</td>
</tr>
</template>
<!-- 无子菜单 -->
<template v-else>
<tr>
<!-- 父菜单 -->
<td>
<a-checkbox :value="parentMenu.id">
{{ parentMenu.name }}
</a-checkbox>
</td>
<!-- 子菜单 -->
<td>
</td>
<!-- 功能 -->
<td>
</td>
</tr>
</template>
</template>
</a-checkbox-group>
</tbody>

View File

@@ -1,163 +1,160 @@
<script lang="tsx">
<template>
<a-menu class="full"
:mode="topMenu ? 'horizontal' : 'vertical'"
v-model:collapsed="collapsed"
v-model:open-keys="openKeys"
v-model:selected-keys="selectedKey"
:show-collapse-button="appStore.device !== 'mobile'"
:auto-open="false"
:auto-open-selected="true"
:level-indent="34"
@collapse="setCollapse">
<template v-for="menu in menuTree">
<!-- 一级菜单 -->
<a-menu-item v-if="!menu.children?.length"
:key="menu.name"
@click="(e) => goto(e, menu)">
<!-- 图标 -->
<template #icon>
<component v-if="menu.meta?.icon" :is="menu.meta?.icon" />
</template>
<!-- 名称 -->
{{ menu.meta?.locale || '' }}
</a-menu-item>
<!-- 父菜单 -->
<a-sub-menu v-else :key="menu.name">
<!-- 图标 -->
<template #icon>
<component v-if="menu.meta?.icon" :is="menu.meta?.icon" />
</template>
<!-- 名称 -->
<template #title>
{{ menu.meta?.locale || '' }}
</template>
<!-- 子菜单 -->
<a-menu-item v-for="child in menu.children"
:key="child.name"
@click="(e) => goto(e, child)">
<!-- 图标 -->
<template #icon v-if="child.meta?.icon">
<component :is="child.meta?.icon" />
</template>
<!-- 名称 -->
{{ child.meta?.locale || '' }}
</a-menu-item>
</a-sub-menu>
</template>
</a-menu>
</template>
<script lang="ts">
export default {
name: 'systemMenuTree'
};
</script>
<script lang="ts" setup>
import type { RouteMeta, RouteRecordRaw } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { compile, computed, defineComponent, h, ref } from 'vue';
import { computed, ref } from 'vue';
import { useAppStore } from '@/store';
import { listenerRouteChange } from '@/utils/route-listener';
import { openWindow, regexUrl } from '@/utils';
import { openNewRoute } from '@/router';
import useMenuTree from './use-menu-tree';
export default defineComponent({
name: 'menuTree',
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 emits = defineEmits(['collapse']);
const topMenu = computed(() => appStore.topMenu);
const openKeys = ref<string[]>([]);
const selectedKey = ref<string[]>([]);
const appStore = useAppStore();
const router = useRouter();
const route = useRoute();
const { menuTree } = useMenuTree();
// 跳转路由
const goto = (e: any, item: RouteRecordRaw) => {
// 打开外链
if (regexUrl.test(item.path)) {
openWindow(item.path);
return;
}
const { hideInMenu, activeMenu, newWindow } = item.meta as RouteMeta;
// 新页面打开
if (newWindow || e.ctrlKey) {
openNewRoute({
name: item.name,
});
return;
}
// 设置 selectedKey
if (route.name === item.name && !hideInMenu && !activeMenu) {
selectedKey.value = [item.name as string];
return;
}
// 触发跳转
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;
backtrack(el, [el.name as string]);
});
return result;
};
// 监听路由 设置打开的 key
listenerRouteChange((newRoute) => {
const { activeMenu, hideInMenu } = newRoute.meta;
if (!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(compile(`<${element?.meta?.icon}/>`))
: null;
const node =
element?.children && element?.children.length !== 0 ? (
<a-sub-menu
key={element?.name}
v-slots={{
icon,
// 去除国际化 title: () => h(compile(t(element?.meta?.locale || ''))),
title: () => h(compile(element?.meta?.locale || '')),
}}
>
{travel(element?.children)}
</a-sub-menu>
) : (
<a-menu-item
key={element?.name}
v-slots={{ icon }}
onClick={($event: any) => goto($event, 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>
);
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 = (e: any, item: RouteRecordRaw) => {
// 打开外链
if (regexUrl.test(item.path)) {
openWindow(item.path);
return;
}
const { hideInMenu, activeMenu, newWindow } = item.meta as RouteMeta;
// 新页面打开
if (newWindow || e.ctrlKey) {
openNewRoute({
name: item.name,
});
return;
}
// 设置 selectedKey
if (route.name === item.name && !hideInMenu && !activeMenu) {
selectedKey.value = [item.name as string];
return;
}
// 触发跳转
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;
backtrack(el, [el.name as string]);
});
return result;
};
// 监听路由 设置打开的 key
listenerRouteChange((newRoute) => {
const { activeMenu, hideInMenu } = newRoute.meta;
if (!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 });
};
</script>
<style lang="less" scoped>

View File

@@ -0,0 +1,189 @@
<!--<script lang="tsx">-->
<!-- import type { RouteMeta, RouteRecordRaw } from 'vue-router';-->
<!-- import { useRoute, useRouter } from 'vue-router';-->
<!-- import { compile, computed, defineComponent, h, ref } from 'vue';-->
<!-- import { useAppStore } from '@/store';-->
<!-- import { listenerRouteChange } from '@/utils/route-listener';-->
<!-- import { openWindow, regexUrl } from '@/utils';-->
<!-- import { openNewRoute } from '@/router';-->
<!-- import useMenuTree from './use-menu-tree';-->
<!-- export default defineComponent({-->
<!-- name: 'menuTree',-->
<!-- 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 = (e: any, item: RouteRecordRaw) => {-->
<!-- // 打开外链-->
<!-- if (regexUrl.test(item.path)) {-->
<!-- openWindow(item.path);-->
<!-- return;-->
<!-- }-->
<!-- const { hideInMenu, activeMenu, newWindow } = item.meta as RouteMeta;-->
<!-- // 新页面打开-->
<!-- if (newWindow || e.ctrlKey) {-->
<!-- openNewRoute({-->
<!-- name: item.name,-->
<!-- });-->
<!-- return;-->
<!-- }-->
<!-- // 设置 selectedKey-->
<!-- if (route.name === item.name && !hideInMenu && !activeMenu) {-->
<!-- selectedKey.value = [item.name as string];-->
<!-- return;-->
<!-- }-->
<!-- // 触发跳转-->
<!-- 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;-->
<!-- backtrack(el, [el.name as string]);-->
<!-- });-->
<!-- return result;-->
<!-- };-->
<!-- // 监听路由 设置打开的 key-->
<!-- listenerRouteChange((newRoute) => {-->
<!-- const { activeMenu, hideInMenu } = newRoute.meta;-->
<!-- if (!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(compile(`<${element?.meta?.icon}/>`))-->
<!-- : null;-->
<!-- const node =-->
<!-- element?.children && element?.children.length !== 0 ? (-->
<!-- <a-sub-menu-->
<!-- key={element?.name}-->
<!-- v-slots={{-->
<!-- icon,-->
<!-- // 去除国际化 title: () => h(compile(t(element?.meta?.locale || ''))),-->
<!-- title: () => h(compile(element?.meta?.locale || '')),-->
<!-- }}-->
<!-- >-->
<!-- {travel(element?.children)}-->
<!-- </a-sub-menu>-->
<!-- ) : (-->
<!-- <a-menu-item-->
<!-- key={element?.name}-->
<!-- v-slots={{ icon }}-->
<!-- onClick={($event: any) => goto($event, 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;-->
<!-- }-->
<!-- }-->
<!-- .arco-menu-icon {-->
<!-- margin-right: 10px !important;-->
<!-- }-->
<!-- .arco-menu-indent-list {-->
<!-- width: 28px;-->
<!-- display: inline-block;-->
<!-- }-->
<!-- .arco-menu-title {-->
<!-- user-select: none;-->
<!-- }-->
<!-- }-->
<!--</style>-->

View File

@@ -8,8 +8,10 @@ export default function useMenuTree() {
const appRoute = computed(() => {
return menuStore.appMenus;
});
const menuTree = computed(() => {
const menuTree = computed<RouteRecordNormalized[]>(() => {
const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];
// 排序
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
return (a.meta.order || 0) - (b.meta.order || 0);
});

View File

@@ -10,11 +10,11 @@
<div class="pagination-wrapper">
<a-pagination v-if="pagination"
size="mini"
v-model:current="(pagination as PaginationProps).current"
v-model:page-size="(pagination as PaginationProps).pageSize"
v-model:current="(pagination as any).current"
v-model:page-size="(pagination as any).pageSize"
v-bind="pagination as any"
:auto-adjust="false"
@change="page => bubblesEmitter(HeaderEmitter.PAGE_CHANGE, page, (pagination as PaginationProps).pageSize)"
@change="page => bubblesEmitter(HeaderEmitter.PAGE_CHANGE, page, (pagination as any).pageSize)"
@page-size-change="limit => bubblesEmitter(HeaderEmitter.PAGE_CHANGE, 1, limit)" />
</div>
</div>
@@ -104,7 +104,6 @@
</script>
<script lang="ts" setup>
import type { PaginationProps } from '@arco-design/web-vue';
import type { CardProps } from '../types/props';
import { ref, computed } from 'vue';
import { useAppStore } from '@/store';

View File

@@ -49,7 +49,10 @@
'field-value',
field.ellipsis ? 'field-value-ellipsis' : ''
]">
<slot :name="field.slotName" :record="item" :index="index" :key="item[key as string]">
<slot :name="field.slotName"
:record="item"
:index="index"
:rowKey="item[rowKey as string]">
<a-tooltip v-if="field.tooltip" :content="item[field.dataIndex]">
<span v-if="field.render" v-html="field.render({ record: item, index })" />
<span v-else>{{ item[field.dataIndex] }}</span>
@@ -87,7 +90,7 @@
const props = defineProps<CardProps & {
index: number,
item: CardRecord
item: CardRecord,
}>();
const emits = defineEmits(['emitter']);

View File

@@ -29,7 +29,7 @@
</a-col>
<!-- 数据卡片 -->
<a-col v-for="(item, index) in list"
:key="item[key]"
:key="item[rowKey]"
v-bind="cardLayoutCols"
:class="{ 'disabled-col': item.disabled === true }">
<!-- 右键菜单 -->
@@ -41,12 +41,18 @@
@emitter="dispatchEmitter">
<!-- 自定义插槽 -->
<template v-for="slot in Object.keys($slots)" :key="slot" #[slot]>
<slot :name="slot" :record="item" :index="index" :key="item[key]" />
<slot :name="slot"
:record="item"
:index="index"
:rowKey="item[rowKey]" />
</template>
</card-item>
<!-- 右键菜单 -->
<template v-if="contextMenu" #content>
<slot name="contextMenu" :record="item" :index="index" :key="item[key]" />
<slot name="contextMenu"
:record="item"
:index="index"
:rowKey="item[rowKey]" />
</template>
</a-dropdown>
</a-col>
@@ -82,7 +88,7 @@
import useEmitter from '@/hooks/emitter';
const props = withDefaults(defineProps<CardProps>(), {
key: 'id',
rowKey: 'id',
pagination: false,
loading: false,
cardHeight: '100%',

View File

@@ -4,7 +4,7 @@ import type { CardFieldConfig, CardPosition, CardRecord, ColResponsiveValue, Han
// 卡片属性
export interface CardProps {
key?: string;
rowKey?: string;
pagination?: PaginationProps | boolean;
loading?: boolean;
fieldConfig?: CardFieldConfig;

View File

@@ -1,15 +1,12 @@
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
const { copy: c } = useClipboard();
// 复制
export const copy = async (value: string | undefined, tips: string | boolean = `${value} 已复制`) => {
try {
if (!value) {
return;
}
await c(value);
await copyToClipboard(value);
if (tips) {
Message.success(tips as string);
}
@@ -20,7 +17,52 @@ export const copy = async (value: string | undefined, tips: string | boolean = `
// 获取剪切板内容
export const readText = () => {
return navigator.clipboard.readText();
if (navigator.clipboard) {
return navigator.clipboard.readText();
} else {
return new Promise<string>((resolve, reject) => {
const textarea = document.createElement('textarea');
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
const success = document.execCommand('paste');
if (!success) {
Message.error('当前环境无法读取剪切板内容');
}
resolve(textarea.value);
} catch (error) {
reject(error);
} finally {
document.body.removeChild(textarea);
}
});
}
};
// 复制到剪切板
export const copyToClipboard = async (value: string) => {
if (navigator.clipboard) {
await navigator.clipboard.writeText(value);
} else {
const textarea = document.createElement('textarea');
textarea.textContent = value;
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
const success = document.execCommand('copy');
if (!success) {
Message.error('当前环境无法复制到剪切板');
}
} catch (error) {
throw error;
} finally {
document.body.removeChild(textarea);
}
}
};
export default function useCopy() {

View File

@@ -1,6 +1,6 @@
import type { RouteLocationNormalized, RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
import { useMenuStore, useUserStore } from '@/store';
import { STATUS_ROUTER_LIST, WHITE_ROUTER_LIST } from '@/router/constants';
import { DEFAULT_ROUTER, STATUS_ROUTER_LIST, WHITE_ROUTER_LIST } from '@/router/constants';
import { AdminRoleCode } from '@/types/const';
export default function usePermission() {
@@ -16,7 +16,7 @@ export default function usePermission() {
return false;
}
// 检查路由是否存在于授权路由中
const menuConfig = [...menuStore.appMenus, ...WHITE_ROUTER_LIST, ...STATUS_ROUTER_LIST];
const menuConfig = [...menuStore.appMenus, ...WHITE_ROUTER_LIST, ...STATUS_ROUTER_LIST, DEFAULT_ROUTER];
let exist = false;
while (menuConfig.length && !exist) {
const element = menuConfig.shift();
@@ -44,7 +44,7 @@ export default function usePermission() {
hasAnyPermission(permission: string[]) {
return userStore.permission?.includes('*') ||
permission.map(s => userStore.permission?.includes(s))
.filter(Boolean).length > 0;
.filter(Boolean).length > 0;
},
/**
@@ -61,7 +61,7 @@ export default function usePermission() {
hasAnyRole(role: string[]) {
return userStore.roles?.includes(AdminRoleCode) ||
role.map(s => userStore.roles?.includes(s))
.filter(Boolean).length > 0;
.filter(Boolean).length > 0;
}
};
}

View File

@@ -2,9 +2,9 @@
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
<!-- tab bar -->
<div v-if="navbar" class="layout-navbar">
<NavBar />
<nav-bar />
</div>
<a-layout>
<a-layout style="flex-direction: row;">
<!-- 左侧菜单栏 -->
<a-layout-sider v-if="renderMenu"
v-show="!hideMenu"
@@ -17,7 +17,7 @@
:hide-trigger="true"
@collapse="setCollapsed">
<div class="menu-wrapper">
<menu-tree />
<system-menu-tree />
</div>
</a-layout-sider>
<!-- 顶部菜单栏 -->
@@ -29,12 +29,12 @@
mask-closable
:closable="false"
@cancel="drawerCancel">
<menu-tree style="padding: 12px 16px;" />
<system-menu-tree style="padding: 12px 16px;" />
</a-drawer>
<!-- body -->
<a-layout class="layout-content" :style="paddingStyle">
<!-- 页签 -->
<TabBar v-if="appStore.tabBar" />
<tab-bar v-if="appStore.tabBar" />
<!-- 页面 -->
<a-layout-content style="height: 100%; width: 100%;">
<!-- 水印 -->
@@ -43,11 +43,11 @@
:z-index="9999"
style="width: 100%; height: 100%;"
:content="userStore.username || ''">
<PageLayout />
<page-layout />
</a-watermark>
</a-layout-content>
<!-- 页脚 -->
<Footer v-if="visibleFooter" />
<app-footer v-if="visibleFooter" />
</a-layout>
</a-layout>
</a-layout>
@@ -60,17 +60,18 @@
import useResponsive from '@/hooks/responsive';
import { toggleDrawerMenuKey } from '@/types/symbol';
import PageLayout from './page-layout.vue';
import MenuTree from '@/components/system/menu/tree/index.vue';
import NavBar from '@/components/app/navbar/index.vue';
import Footer from '@/components/app/footer/index.vue';
import TabBar from '@/components/app/tab-bar/index.vue';
import AppFooter from '@/components/app/app-footer/index.vue';
import SystemMenuTree from '@/components/system/menu/tree/index.vue';
const isInit = ref(false);
const appStore = useAppStore();
const userStore = useUserStore();
const router = useRouter();
const route = useRoute();
useResponsive(true);
const isInit = ref(false);
const navbarHeight = `60px`;
const navbar = computed(() => appStore.navbar);
const renderMenu = computed(() => appStore.menu && !appStore.topMenu);

View File

@@ -13,6 +13,11 @@ export const DEFAULT_ROUTE_NAME = 'workplace';
export const DEFAULT_ROUTE_FULL_PATH = '/workplace';
/**
* 默认路由
*/
export const DEFAULT_ROUTER = { name: DEFAULT_ROUTE_NAME, children: [] };
/**
* 路由白名单
*/

View File

@@ -1,9 +1,9 @@
import type { PaginationProps, TableRowSelection } from '@arco-design/web-vue';
import type { TableExpandable } from '@arco-design/web-vue/es/table/interface';
import { reactive } from 'vue';
import { useAppStore } from '@/store';
import { isNumber } from '@/utils/is';
import { TablePageSizeOptions } from '@/types/const';
import { TableExpandable } from '@arco-design/web-vue/es/table/interface';
/**
* 创建列表分页

View File

@@ -1,5 +1,10 @@
const debug = import.meta.env.MODE !== 'production';
// 当前环境是否为安全环境
export const isSecureEnvironment = (() => {
return window.location.protocol === 'https:' || window.location.hostname === 'localhost';
})();
// http base url
export const httpBaseUrl = (() => {
const configBase = import.meta.env.VITE_API_BASE_URL;

View File

@@ -8,16 +8,16 @@
<div class="logo-text">Orion Ops Pro</div>
</div>
<!-- 左侧 banner -->
<LoginBanner />
<login-banner />
<!-- 主体部分 -->
<div class="content">
<div class="content-inner">
<!-- 登录表单 -->
<LoginForm />
<login-form />
</div>
<!-- 页脚 -->
<div class="footer">
<Footer />
<app-footer />
</div>
</div>
</div>
@@ -26,9 +26,9 @@
<script lang="ts" setup>
import { Notification } from '@arco-design/web-vue';
import { reLoginTipsKey } from '@/types/symbol';
import Footer from '@/components/app/footer/index.vue';
import LoginBanner from './components/banner.vue';
import LoginForm from './components/login-form.vue';
import AppFooter from '@/components/app/app-footer/index.vue';
// 登录提示
const tips = window.sessionStorage.getItem(reLoginTipsKey);

View File

@@ -34,6 +34,7 @@
.flat(1)
.filter(s => s.meta)
.map(s => s as RouteRecordNormalized)
.filter(s => s.meta.hideInMenu !== true)
.slice(0, 15);
// 打开路由

View File

@@ -4,7 +4,7 @@
<div class="top-side">
<!-- 提示 -->
<div class="panel">
<Banner />
<banner />
</div>
</div>
<div class="row-wrapper">

View File

@@ -13,7 +13,12 @@
</div>
<!-- 描述 -->
<div class="block-form-item-desc">
{{ desc }}
<template v-if="desc">
{{ desc }}
</template>
<template v-else>
<slot name="desc" />
</template>
</div>
</div>
</a-col>
@@ -29,13 +34,14 @@
defineProps<{
label: string,
desc: string,
desc?: string,
}>();
</script>
<style lang="less" scoped>
.block-form-item-wrapper {
width: 458px;
height: 100%;
min-height: 64px;
border-radius: 4px;

View File

@@ -8,6 +8,12 @@
</div>
<!-- 提示 -->
<a-alert class="mb16">修改后会立刻保存, 立即生效 (无需刷新页面)</a-alert>
<!-- 非安全环境提示 -->
<a-alert v-if="!isSecureEnvironment"
type="warning"
class="mb16">
当前环境非 HTTPS 环境, 因浏览器安全策略限制, 自定义 '粘贴' 功能无法使用
</a-alert>
<!-- 内容区域 -->
<div class="terminal-setting-body block-body setting-body">
<a-form class="terminal-setting-form"
@@ -52,6 +58,7 @@
import { useTerminalStore } from '@/store';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
import { ActionBarItems } from '../../../types/terminal.const';
import { isSecureEnvironment } from '@/utils/env';
import IconActions from '../../layout/icon-actions.vue';
const { preference, updateTerminalPreference } = useTerminalStore();

View File

@@ -8,6 +8,12 @@
</div>
<!-- 提示 -->
<a-alert class="mb16">修改后会立刻保存, 重新打开终端后生效 (无需刷新页面)</a-alert>
<!-- 非安全环境提示 -->
<a-alert v-if="!isSecureEnvironment"
type="warning"
class="mb16">
当前环境非 HTTPS 环境, 因浏览器安全策略限制, 自定义 '粘贴' 功能无法使用
</a-alert>
<!-- 内容区域 -->
<div class="terminal-setting-body block-body setting-body">
<!-- 功能项 -->
@@ -84,6 +90,7 @@
import { useTerminalStore } from '@/store';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
import { ActionBarItems } from '../../../types/terminal.const';
import { isSecureEnvironment } from '@/utils/env';
const { preference, updateTerminalPreference } = useTerminalStore();

View File

@@ -8,6 +8,12 @@
</div>
<!-- 提示 -->
<a-alert class="mb16">修改后会立刻保存, 刷新页面后生效</a-alert>
<!-- 非安全环境提示 -->
<a-alert v-if="!isSecureEnvironment"
type="warning"
class="mb16">
当前环境非 HTTPS 环境, 因浏览器安全策略限制, 自定义 '粘贴' 功能无法使用
</a-alert>
<!-- 内容区域 -->
<div class="terminal-setting-body setting-body">
<a-row class="mb16" align="stretch" :gutter="16">
@@ -40,23 +46,29 @@
<a-switch type="round"
v-model="formModel.copyAutoTrim" />
</block-setting-item>
<!-- 粘贴去除空格 -->
<block-setting-item label="粘贴去除空格" desc="粘贴文本前自动删除尾部空格 如: 命令输入框, 命令编辑器, 右键粘贴, 粘贴按钮, 右键菜单粘贴, 自定义粘贴快捷键">
<a-switch type="round"
v-model="formModel.pasteAutoTrim" />
</block-setting-item>
</a-row>
<a-row class="mb16" align="stretch" :gutter="16">
<!-- 右键粘贴 -->
<block-setting-item label="右键粘贴" desc="右键自动粘贴, 启用后需要关闭右键菜单 (若开启了右键选中词条, 有选中的文本时, 右键粘贴无效)">
<a-switch type="round"
v-model="formModel.rightClickPaste" />
</block-setting-item>
<!-- 启用右键菜单 -->
<block-setting-item label="启用右键菜单" desc="右键终端将打开自定义菜单, 启用后需要关闭右键粘贴">
<a-switch type="round"
v-model="formModel.enableRightClickMenu" />
</block-setting-item>
</a-row>
<a-row class="mb16" align="stretch" :gutter="16">
<!-- 右键粘贴 -->
<block-setting-item label="右键粘贴"
desc="启用右键自动粘贴需要关闭右键菜单. 如果启用右键选中词条且选中有文本时, 右键粘贴无效. 因浏览器安全策略限制, 此功能需要在 HTTPS 环境下使用">
<a-switch type="round"
v-model="formModel.rightClickPaste" />
</block-setting-item>
<!-- 粘贴去除空格 -->
<block-setting-item label="粘贴去除空格"
desc="粘贴文本前自动删除尾部空格 如: 命令输入框, 命令编辑器, 右键粘贴, 粘贴按钮, 右键菜单粘贴, 自定义粘贴快捷键. 默认粘贴快捷键无法去除空格">
<a-switch type="round"
v-model="formModel.pasteAutoTrim" />
<template #desc>
</template>
</block-setting-item>
</a-row>
<a-row class="mb16" align="stretch" :gutter="16">
<!-- 启用响铃 -->
@@ -87,6 +99,7 @@
import { ref, watch } from 'vue';
import { useTerminalStore } from '@/store';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
import { isSecureEnvironment } from '@/utils/env';
import BlockSettingItem from '../block-setting-item.vue';
const { preference, updateTerminalPreference } = useTerminalStore();

View File

@@ -10,6 +10,12 @@
<div class="terminal-setting-body setting-body">
<!-- 提示 -->
<a-alert class="mb16">点击保存按钮后需要刷新页面生效 (恢复默认配置后也需要点击保存按钮) (设置时需要避免与浏览器内置快捷键冲突)</a-alert>
<!-- 非安全环境提示 -->
<a-alert v-if="!isSecureEnvironment"
type="warning"
class="mb16">
当前环境非 HTTPS 环境, 因浏览器安全策略限制, 自定义 '粘贴' 功能无法使用
</a-alert>
<a-space class="action-container" size="mini">
<!-- 是否启用 -->
<a-switch v-model="value"
@@ -39,6 +45,7 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { isSecureEnvironment } from '@/utils/env';
const props = defineProps<{
enabled: boolean

View File

@@ -26,7 +26,7 @@
<!-- 启用-未修改 -->
<span v-else-if="item.enabled">{{ item.shortcutKey }}</span>
<!-- 禁用 -->
<span v-else />
<span v-else class="disabled-key">已禁用</span>
</div>
<!-- 操作 -->
<a-space class="shortcut-actions-container">
@@ -52,6 +52,13 @@
<icon-settings />
</div>
</a-space>
<!-- 描述 -->
<div class="shortcut-desc">
<!-- 粘贴描述 -->
<template v-if="item.item === TerminalShortcutKeys.PASTE">
Ctrl + Shift + V, Shift + Insert 为浏览器内置快捷键, 不受环境影响
</template>
</div>
</div>
</template>
</div>
@@ -68,6 +75,7 @@
import type { TerminalShortcutKeyEditable } from '@/store/modules/terminal/types';
import type { VNodeRef } from 'vue';
import { setAutoFocus } from '@/utils/dom';
import { TerminalShortcutKeys } from '../../../types/terminal.const';
defineProps<{
title: string;
@@ -116,6 +124,10 @@
.shortcut-actions-container {
display: flex;
}
.shortcut-desc {
display: none;
}
}
.shortcut-name {
@@ -132,6 +144,11 @@
}
}
.shortcut-desc {
display: flex;
font-size: 12px;
}
.shortcut-actions-container {
display: none;
@@ -144,6 +161,10 @@
}
}
}
.disabled-key {
color: var(--color-neutral-6);
}
}
</style>

View File

@@ -170,11 +170,11 @@
// 连接成功回调
const connectCallback = () => {
loadFiles(undefined);
loadFiles('~');
};
// 加载文件列表
const loadFiles = (path: string | undefined) => {
const loadFiles = (path: string) => {
setTableLoading(true);
session.value?.list(path);
};

View File

@@ -46,7 +46,7 @@ export default class SftpSession implements ISftpSession {
}
// 查询文件列表
list(path: string | undefined) {
list(path: string) {
this.channel.send(InputProtocol.SFTP_LIST, {
sessionId: this.sessionId,
showHiddenFile: ~~this.showHiddenFile,

View File

@@ -16,7 +16,12 @@ const preventKeys: Array<ShortcutKey> = [
altKey: false,
shiftKey: true,
code: 'KeyC'
}, {
},
];
// 内置快捷键
const builtinKeys: Array<ShortcutKey> = [
{
ctrlKey: true,
altKey: false,
shiftKey: true,
@@ -49,16 +54,13 @@ export default class SshSessionHandler implements ISshSessionHandler {
this.session = session;
this.inst = session.inst;
this.domRef = domRef;
const { preference, tabManager } = useTerminalStore();
const { preference } = useTerminalStore();
this.interactSetting = preference.interactSetting;
this.shortcutKeys = preference.shortcutSetting.keys;
}
// 检测是否忽略默认行为
checkPreventDefault(e: KeyboardEvent): boolean {
if (e.type !== 'keydown') {
return false;
}
return !!preventKeys.find(key => {
return key.code === e.code
&& key.altKey === e.altKey
@@ -67,6 +69,16 @@ export default class SshSessionHandler implements ISshSessionHandler {
});
}
// 检测是否为内置快捷键
checkIsBuiltin(e: KeyboardEvent): boolean {
return !!builtinKeys.find(key => {
return key.code === e.code
&& key.altKey === e.altKey
&& key.shiftKey === e.shiftKey
&& key.ctrlKey === e.ctrlKey;
});
}
// 启用状态
enabledStatus(option: string): boolean {
switch (option) {

View File

@@ -85,6 +85,10 @@ export default class SshSession implements ISshSession {
if (e.type !== 'keydown') {
return true;
}
// 检测是否为内置快捷键
if (this.handler.checkIsBuiltin(e)) {
return true;
}
// 检测是否阻止默认行为
if (this.handler.checkPreventDefault(e)) {
e.preventDefault();
@@ -244,7 +248,9 @@ export default class SshSession implements ISshSession {
.filter(Boolean)
.forEach(s => s.dispose());
// 卸载终端
this.inst.dispose();
setTimeout(() => {
this.inst.dispose();
}, 300);
} catch (e) {
// 卸载可能会报错
}

View File

@@ -212,6 +212,8 @@ export const TerminalShortcutKeys = {
CHANGE_TO_PREV_SESSION: 'changeToPrevSession',
// 切换至后一个会话
CHANGE_TO_NEXT_SESSION: 'changeToNextSession',
// 粘贴
PASTE: 'paste',
};
// 终端操作快捷键
@@ -257,7 +259,7 @@ export const TerminalShortcutItems: Array<ShortcutKeyItem> = [
content: '复制',
type: TerminalShortcutType.TERMINAL
}, {
item: 'paste',
item: TerminalShortcutKeys.PASTE,
content: '粘贴',
type: TerminalShortcutType.TERMINAL
}, {

View File

@@ -267,6 +267,8 @@ export interface ISshSession extends ITerminalSession {
export interface ISshSessionHandler {
// 检测是否忽略默认行为
checkPreventDefault: (e: KeyboardEvent) => boolean;
// 检测是否为内置快捷键
checkIsBuiltin: (e: KeyboardEvent) => boolean;
// 启用状态
enabledStatus: (option: string) => boolean;
// 调用处理方法
@@ -322,7 +324,7 @@ export interface ISftpSession extends ITerminalSession {
// 设置显示隐藏文件
setShowHiddenFile: (show: boolean) => void;
// 查询文件列表
list: (path: string | undefined) => void;
list: (path: string) => void;
// 创建文件夹
mkdir: (path: string) => void;
// 创建文件

View File

@@ -22,7 +22,7 @@
</modules>
<properties>
<revision>1.0.2</revision>
<revision>1.0.3</revision>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<maven.surefire.plugin.version>3.0.0-M5</maven.surefire.plugin.version>